C++语言概述
C++语言概述 C++语言是一种应用较广的面向对象的程序设计语言,使用它可以实现面向对象的程序设计。面向对象的设计与面向过程的设计是有很大区别的,面向对象的程序设计是在面向过程的程序设计的基础上一个质的飞跃。要学会面向对象的程序设计,首先要学会一种面向对象的语言,即要学会用VC编程,就要先有C++的基础,而学习C++语言首先要认识它面向对象的特性和实现面向对象的方法。 C++是一种面向对象的程序设计语言 当你首次学习C++时,总会碰到一些在C语言从未见过的概念,如:类、对象、抽象、封装、继承、多态性、虚函数等等。这些概念是C++所具有,下面简单的介绍一下C++对面向对象程序设计方法的支持和实现。 1、C++支持数据封装 支持数据封装就是支持数据抽象。在C++中,类是支持数据封装的工具,对象则是数据封装的实现。面向过程的程序设计方法与面向对象的程序设计方法在对待数据和函数关系上是不同的,在面向对象的程序设计中,将数据和对该数据进行合法操作的函数封装在一起作为一个类的定义,数据将被隐藏在封装体中,该封装体通过操作接口与外界交换信息。对象被说明具有一个给定类的变量,类类似于C语言中的结构,在C语言中可以定义结构,但这种结构中包含数据,而不包含函数。C++中的类是数据和函数的封装体。在C++中,结构可作为一种特殊的类,它虽然可以包含函数,但是它没有私有或保护的成员。 2、C++类中包含私有、公有和保护成员 C++类中可定义三种不同访控制权限的成员。一种是私有(Private)成员,只有在类中说明的函数才能访问该类的私有成员,而在该类外的函数不可以访问私有成员;另一种是公有(Public)成员,类外面也可访问公有成员,成为该类的接口;还有一种是保护(Protected)成员,这种成员只有该类的派生类可以访问,其余的在这个类外不能访问。 3、C++中通过发关消息来处理对象 C++中是通过向对象发关消息来处理对象的,每个对象根据所接收到的消息的性质来决定需要采取的行动,以响应这个消息。响应这些消息是一系列的方法,方法是在类定义中使用函数来定义的,使用一种类似于函数调用的机制把消息发送到一个对象上。 4、C++中允许友元破坏封装性 类中的私有成员一般是不允许该类外面的任何函数访问的,但是友元便可打破这条禁令,它可以访问该类的私有成员(包含数据成员和成员函数)。友元可以是在类外定义的函数,也可以是在类外定义的整个类,前者称友元函数,后者称为友元类。友元打破了类的封装性,它是C++另一个面向对象的重要牲。 5、C++允许函数名和运算符重载 C++支持多态性,C++允许一个相同的标识符或运算符代表多个不同实现的函数,这就称标识符或运算符的重载,用户可以根据需要定义标识符重载或运算符重载。 6、C++支持继承性 C++中可以允许单继承和多继承。一个类可以根据需要生成派生类。派生类继承了基类的所有方法,另外派生类自身还可以定义所需要的不包含在父类中的新方法。一个子类的每个对象包含有从父类那里继承来的数据成员以及自己所特有的数据成员。 7、C++支持动态联编 C++中可以定义虚函数,通过定义虚函数来支持动态联编。 以上是所讲的是C++对面向对象程序设计中的一些主要特征的支持。 C++的词法及词法规则 1、C++的字符集 字符是一些可以区分的最小符号。C++的字符集由大小写英文字母(a-z和A-Z)、数据字符(0-9)、特殊字符(空格,!,#,%,^,&,*,_,<,>,?,\,,)组成。 2、单词及词法规则 单词又称词法记号,它是由若干个字符组成的具有一定意义的最小词法单元。C++共有6种单词,分别是:标识符、关键字、运算符、分隔符、常量、注释符,在编码时要特别注意这些单词的词法规则。要注意的是C++中的空白符:C++中经常使用空白符,实际上,空白符不是一个字符,它是空格符、换行符和水平制表符的统称。注意,空白符不等于空格符,只是空白符包含空格符。还有一个空字符,要把它与空白符分开。空字符是指ASCII码值为0的那个字符。空字符在C++中有特殊用途,用它来作为字符串的结束符。存放在内存中的字符串常量都在最后有一个结束符,即用空字符,它用转义序列方法表示为’\0’。 C++程序结构的组成 C++程序结构的基本组成部分 1 预处理命令,C++提供了三类预处理命令:宏定义命令、文件包含命令、条件编译命令。 2 输入输出,C++程序中总是少不了输入和输出的语句,实现与程序内部的信息交流。特别是屏幕输出的功能,几乎每个程序都要用到,使用它把计算机的结果显示在屏幕上。 3 函数,C++的程序是由若干个文件组成的,每个文件又是由若干个函数组成,因此,可以认为C++的程序就是函数串,即由若干个函数组成,函数与函数之间是相对的,并且是并行的,函数之间可以调用。在组成一个程序的若干个函中,必须有一个main()。 4 语句,语句是组成程序的基本单元。函数是由若干条语句组成的。但是,空函数是没有语句的。语句是由单词组成,单词间用空格符分隔,C++程序中的语句又是以以分号结束。语句除了有表达式语句和空语句之外,还有复合语句、分支语句、循环语句和转向语句等若干类。 5 变量,多数程序都需要说明和使用变量。广义讲,对象包含了变量,即将变量也称为一种对象,狭义讲,将对象看作是类的实例,对象是指某个类的对象。 6 其他,除了以上讲述的5个部分以外,还有其他组成部分。例如,符号常量和注释信息也是程序的一部分。C++中都尽量把常量定义为符号常量,在C++的程序中出现的是符号常量,该符号常量代表着某个确定的常量值。 C++程序的书写格式 在编程时应该注意C++的书写格式,基本原则是:一行一般写一条语句。短语句可以一行写多个。长语句可以一条写多行。分行原则是不能将一个单词分开。用双引号引用的一个字符串也最好不分开,如果一定要分开,有的编译系统要求在行尾加续行符(“\”) C++程序的实现 C++源程序的实现与其他高级语言源程序实现的原理是一样的。一般都要经过编辑、编译、运行。其中最要的是编译过程,C++是以编译方式实现的高级语言。C++程序的实现,必须要使用某种C++语言的编译器对程序进行编译。编译器的功能是将程序的源代码转换成为机器代码的形式,称为目标代码;然后,再使目标代码进行连接,生成可执行文件。该过程可分为三个子过程:预处理过程、编译过程(词法分析、语法分析、符号表、错误处理程序、生成目标代码)、连接过程。 C++常类型(const) 常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。因此,定义或说明常类型时必须进行初始化。 一般常量和对象常量 1. 一般常量 一般常量是指简单类型的常量。这种常量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。如: int const x=2; 或 const int x=2; 定义或说明一个常数组可采用如下格式: <类型说明符> const <数组名>[<大小>]… 或者 const <类型说明符> <数组名>[<大小>]… 例如: int const a[5]={1, 2, 3, 4, 5}; 2. 常对象 常对象是指对象常量,定义格式如下: <类名> const <对象名> 或者 const <类名> <对象名> 定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。 常指针和常引用 1. 常指针 使用const修饰指针时,由于const的位置不同,而含意不同。下面举两个例子,说明它们的区别。 下面定义的一个指向字符串的常量指针: char * const prt1 = stringprt1; 其中,ptr1是一个常量指针。因此,下面赋值是非法的。 ptr1 = stringprt2; 而下面的赋值是合法的: *ptr1 = "m"; 因为指针ptr1所指向的变量是可以更新的,不可更新的是常量指针ptr1所指的方向(别的字符串)。 下面定义了一个指向字符串常量的指针: const * ptr2 = stringprt1; 其中,ptr2是一个指向字符串常量的指针。ptr2所指向的字符串不能更新的,而ptr2是可以更新的。因此, *ptr2 = "x"; 是非法的,而: ptr2 = stringptr2; 是合法的。 所以,在使用const修饰指针时,应该注意const的位置。定义一个指向字符串的指针常量和定义一个指向字符串常量的指针时,const修饰符的位置不同,前者const放在*和指针名之间,后者const放在类型说明符前。 2. 常引用 使用const修饰符也可以说明引用,被说明的引用为常引用,该引用所引用的对象不能被更新。其定义格式如下: const <类型说明符> & <引用名> 例如: const double & v; 在实际应用中,常指针和常引用往往用来作函数的形参,这样的参数称为常参数。 在C++面向对象的程序设计中,指针和引用使用得较多,其中使用const修饰的常指针和常引用用得更多。使用常参数则表明该函数不会更新某个参数所指向或所引用的对象,这样,在参数传递过程中就不需要执行拷贝初始化构造函数,这将会改善程序的运行效率。 下面举一例子说明常指针作函数参数的作法。 #include const int N = 6; void print(const int *p, int n); void main() { int array[N]; for (int i=0; i cin>>array[i]; print(array, N); } void print(const int *p, int n) { cout<<"{"<<*p; for (int i=1; i cout<<","<<*(p+i); cout<<"}"< } 常成员函数 使用const关键字进行说明的成员函数,称为常成员函数。只有常成员函数才有资格操作常量或常对象,没有使用const关键字说明的成员函数不能用来操作常对象。常成员函数说明格式如下: <类型说明符> <函数名> (<参数表>) const; 其中,const是加在函数说明后面的类型修饰符,它是函数类型的一个组成部分,因此,在函数实现部分也要带const关键字。下面举一例子说明常成员函数的特征。 #include class R { public: R(int r1, int r2) { R1=r1; R2=r2; } void print(); void print() const; private: int R1, R2; }; void R::print() { cout< } void R::print() const { cout< } void main() { R a(5, 4); a.print(); const R b(20, 52); b.print(); } 该例子的输出结果为: 5,4 20;52 该程序的类声明了两个成员函数,其类型是不同的(其实就是重载成员函数)。有带const修饰符的成员函数处理const常量,这也体现出函数重载的特点。 常数据成员 类型修饰符const不仅可以说明成员函数,也可以说明数据成员。 由于const类型对象必须被初始化,并且不能更新,因此,在类中说明了const数据成员时,只能通过成员初始化列表的方式来生成构造函数对数据成员初始化。 下面通过一个例子讲述使用成员初始化列表来生成构造函数。 #include class A { public: A(int i); void print(); const int &r; private: const int a; static const int b; }; const int A::b=10; A::A(int i):a(i), r(a) { } void A::print() { cout< } void main() { A a1(100), a2(0); a1.print(); a2.print(); } 该程序的运行结果为: 100:10:100 0:10:0 在该程序中,说明了如下三个常类型数据成员: const int & r; const int a; static const int b; 其中,r是常int型引用,a是常int型变量,b是静态常int型变量。 程序中对静态数据成员b进行初始化。 值得注意的是构造函数的格式如下所示: A(int i):a(i),r(a) { } 其中,冒号后边是一个数据成员初始化列表,它包含两个初始化项,用逗号进行了分隔,因为数据成员a和r都是常类型的,需要采用初始化格式。 浅谈C++函数的参数 函数参数的求值顺序 当一个函数带有多个参数时,C++语言没有规定在函数调用时实参的求值顺序。而编译器根据对代码进行优化的需要自行规定对实参的求值顺序。有的编译器规定自左至右,有的编译器规定自右至左,这种对求值顺序的不同规定,对一般参数来讲没有影响。但是,如果实参表达式中带有副作用的运算符时,就有可能产生由于求值顺序不同而造成了二义性。例如:int z = add_int(++x, x+y);,这样,在不同的编译器就有可能生产不同的结果。 设置参数的默认值 在C++语言中,允许在函数的说明或定义时给一个或多个参数指定默认值。但是,要求在一个指定了默认值的参数的右边,不能出现没有指定默认值的参数。例如: int add_int(int x, int 10); 在上述对函数add_int()的说明中,对该函数的最右边的一个参数指定了默认值。 在函数调用时,编译器按从左至右的顺序将实参与形参结合,当实参的数目不足时,编译器将按同样的顺序用说明中或定义中的默认值来补足所缺少的实参。例如,如有下列的函数调用表达式: add_int(15) 它将与下列调用表达式: add_int(15, 10) 是等价的。 在给某个参数指定默认值是,不仅可以是一个数值,而且还可以是任意复杂的表达式。 使用数组作函数参数 数组作函数参数可以分为如下三种情况:(这三种情况的结果相同,只是所采用的调用机制不同) 1. 形参和实参都用数组 调用函数的实参用数组名,被调用函数的形参用数组,这种调用的机制是形参和实参共用内存中的同一个数组。因此,在被调用函数中改变了数组中某个无素的值,对调用函数该数组的该元素值也被改变,因为它们是共用同一个数组。 2. 形参和实参都用对应数组的指针 在C++中,数组名被规定为是一个指针,该指针便是指向该数组的首元素的指针,国为它的值是该数组首元素的地址值,因此,数组名是一个常量指针。 实际中,形参和实参一个用指针,另一个用数组也是可以的。在使用指针时可以用数组名,也可以用另外定义的指向数组的指针。 3. 实参用数组名形参用引用 如何对数组类型使用引用方式,这里先做如下说明:先用类型定义语句定义一个int型的数组类型,如下所示: typedef int array[8]; 然后,使用array来定义数组和引用。 示例: #include typedef int array[8]; int a[8] = {1, 3, 5, 7, 9, 11, 13}; void fun(array &b, int n) { for(int i=0; i b[7]+=b[i]; } void main() { int m=8; fun(a, m); cout< } 该程序中,在fun()函数中,使用了引用作形参,调用时所对应的实参应该是一个数组名,这里的引用是给数组起个别名。在fun()函数中对数组b的操作,就相当于b所引用数组a的操作。在C++中,常用这种调用方式。 C++语法之函数重载 所谓函数重载是指同一个函数名可以对应着多个函数的实现。例如,可以给函数名add()定义多个函数实现,该函数的功能是求和,即求两个操作数的和。其中,一个函数实现是求两个int型数之和,另一个实现是求两个浮点型数之和,再一个实现是求两个复数的和。每种实现对应着一个函数体,这些函数的名字相同,但是函数的参数的类型不同。这就是函数重载的概念。函数重载在类和对象的应用尤其重要。 函数重载要求编译器能够唯一地确定调用一个函数时应执行哪个函数代码,即采用哪个函数实现。确定函数实现时,要求从函数参数的个数和类型上来区分。这就是说,进行函数重载时,要求同名函数在参数个数上不同,或者参数类型上不同。否则,将无法实现重载。 参数类型上不同的重载函数 下面举一个在参数类型不同的重载函数的例子: #include int add(int, int); double add(double, double); void main() { cout< cout< } int add(int x, int y) { return x+y; } double add(double a, double b) { return a+b; } 该程序中,main()函数中调用相同名字add的两个函数,前边一个add()函数对应的是两个int型数求和的函数实现,而后边一个add()函数对应的是两个double型数求和的函数实现。这便是函数的重载。 以上程序输出结果为: 15 15.5 参数个数上不同的重载函数 下面举一个在参数个数上不相同的重载函数的例子: #include int min(int a, int b); int min(int a, int b, int c); int min(int a, int b, int c, int d); void main() { cout< cout< } int min(int a, int b) { return a } int min(int a, int b, int c) { int t = min(a, b); return min(t,c); } int min(int a, int b, int c, int d) { int t1 = min(a, b); int t2 = min(c, d); return min(t1, t2); } 该程序中出现了函数重载,函数名min对应有三个不同的实现,函数的区分依据参数个数不同,这里的三个函数实现中,参数个数分别为2,3和4,在调用函数时根据实参的个数来选取不同的函数实现。 函数重载在类和对象应用比较多,尤其是在类的多态性中。在以后我们将碰到更多的在类型不同的函数重载,尤其是在结合类的继承性和指针类型的不同,而这些都是我们以后用VC编程中经常要用到的。 C++子对象和堆对象 子对象 当一个类的成员是某一个类的对象时,该对象就为子对象。子对象实际就是对象成员。如: class A { public: … private: … }; class B { public: … private: A a; … }; 其中,B类中成员a就是子对象,它是A类的对象作为B类的成员。 在类中出现了子对象或称对象成员时,该类的构造函数要包含对子对象的初始化,通常采用成员初始化表的方法来初始化子对象。在成员初始化表中包含对子对象的初始化和对类中其他成员的初始化。下面举一例子说明成员初始化的构造。 #include class A { public: A(int i, int j) { A1=i; A2=j; } void print() { cout< private: int A1, A2; }; class B { public: B(int i, int j, int k):a(i, j), b(k) { } void print(); private: A a; file://子对象 int b; }; void B::print() { a.print(); cout< } void main() { B b(6, 7, 8); b.print(); } 该程序的输出结果为: 6,7 8 其中,a(i, j), b(k)是成员初始化表,它有二项,前一项是给子对象a初始化,其格式如下: <子对象名> (<参数表>) 后一项是给类B的数据成员b初始化。这一项也可以写在构造函数的函数体内,使用赋值表达式语句 b = k; 给类B的数据成员初始化。 堆对象 所谓堆对象是指在程序运行过程中根据需要随时可以建立或删除的对象。这种堆对象被创建在内存一些空闲的存储单元中,这些存储单元被称为堆。它们可以被创建的堆对象占有,也可以通过删除堆对象而获得释放。 创建或删除堆对象时,需要如下两个运算符: new delete 这两个运算符又称为动态分配内存空间运算符。new相当于C语言中malloc()函数,而delete相当于C语言中free()函数。 1. 运算符new的用法 该运算符的功能是用来创建堆对象,或者说,它是用来动态地创建对象。 new运算符使用格式如下: new <类型说明符> (<初始值列表>) 它表明在堆中建立一个由<类型说明符>给定的类型的对象,并且由括号中的<初始值列表>给出被创建对象的初始值。如果省去括号和括号中的初始值,则被创建的对象选用缺省值。 使用new运算符创建对象时,它可以根据其参数来选择适当的构造函数,它不用sizeof来计算对象所占的字节数,而可以计算其大小。 new运算符返回一个指针,指针类型将与new所分配对象相匹配,如果不匹配可以通过强制类型的方法,否则将出现编译错。 如果new运算符不能分配到所需要的内存,它将返回0,这时的指针为空指针。 运算符new也可以用来创建数组类型的对象,即对象数组。其格式如下: new <类名> [<算术表达式>] 其中,<算术表达式>的值为所创建的对象数组的大小。如: A *ptr; ptr = new A[5]; new还可用来创建一般类型的数组。如: int *p; p = new int[10]; 使用new[]创建的对象数组或一般数组时,不能为该数组指定初始值,其初始值为缺省值。 2. 运算符delete的用法 该运算符的功能是用来删除使用new创建的对象或一般类型的指针。其格式如下: delete <指针名> 例如: A *ptr; ptr = new A(5, 6); delete ptr; 运算符delete也可用来删除使用new创建对象数组,其使用格式如下: delete[] <指针名> 同样,delete也可以删除由new创建的一般类型的数组。如: int *p; p = new int[10]; delete[] p; 使用运算符delete时,应注意如下几点: (1) 它必须使用于由运算符new返回的指针; (2) 该运算符也适用于空指针(即其值为0的指针); (3) 指针名前只用一对方括号符,并且不管所删除数组的维数,忽略方括号内的任何数字。 下面举一例子说明new运算符和delete运算符的使用方法。 #include class AA { public: AA(int i, int j) { A=i; B=j; cout<<"构造函数.\n"; } ~AA() { cout<<"析构函数.\n"; } void print(); private: int A, B; }; void AA::print() { cout< } void main() { AA *a1, *a2; a1 = new AA(1, 2); a2 = new AA(5, 6); a1->print(); a2->print(); delete a1; delete a2; } 该程序的输出结果为: 构造函数. 构造函数. 1, 2 5, 6 构造函数. 构造函数. 从程序中可以看到:用new创建对象时,要调用构造函数,用delete删除对象时,要调用析构函数。如果创建或删除的时对象数组,对象数组有多少,就调用多少次构造函数或构造函数。 在实际应用中,经常对于new运算符返回的指针进行检验,看是否分配了有效的内存空间。结合本例给出检验方法如下: if (!a1) { cout<<"Heap erroe!\n"; exit(1); } 下面再举一个使用new和delete运算符对一般指针和数组的例子。 #include #include void fun() { int *p; if (p = new int) { *p = 5; cout<<*p< delete p; } else cout<<"Heap error!\n"; } void main() { fun(); int *pa; pa = new int[5]; if (!pa) { cout<<"Heap error!\n"; exit(1); } for (int i=0; i<5; i++) pa[i] = i+1; for (i=0; i<5; i++) cout< (<派生类构造函数总参数表>):<基类构造函数>(参数表1),<子对象名>(<参数表2>) { <派生类中数据成员初始化> }; 派生类构造函数的调用顺序如下: · 基类的构造函数 · 子对象类的构造函数(如果有的话) · 派生类构造函数 在前面的例子中,B::B(int i, int j, int k):A(i), bb(j), bbb(k)就是派生类构造函数的定义,下面再举一个构造派生类构造函数的例子。 #include class A { public: A() { a=0; cout<<"类A的缺省构造函数.\n"; } A(int i) { a=i; cout<<"类A的构造函数.\n"; } ~A() { cout<<"类A的析构函数.\n"; } void Print() const { cout< int Geta() { reutrn a; } private: int a; } class B : public A { public: B() { b=0; cout<<"类B的缺省构造函数.\n"; } B(int i, int j, int k); ~B() { cout<<"类B的析构函数.\n"; } void Print(); private: int b; A aa; } B::B(int i, int j, int k):A(i), aa(j) { b=k; cout<<"类B的构造函数.\n"; } void B::Print() { A::Print(); cout< } void main() { B bb[2]; bb[0] = B(1, 2, 5); bb[1] = B(3, 4, 7); for(int i=0; i<2; i++) bb[i].Print(); } 2. 构造函数 当对象被删除时,派生类的析构函数被执行。由于析构函数也不能被继承,因此在执行派生类的析构函数时,基类的析构函数也将被调用。执行顺序是先执行派生类的构造函数,再执行基类的析构函数,其顺序与执行构造函数时的顺序正好相反。这一点从前面讲过的例子可以看出,请读者自行分析。 3. 派生类构造函数使用中应注意的问题 (1) 派生类构造函数的定义中可以省略对基类构造函数的调用,其条件是在基类中必须有缺省的构造函数或者根本没有定义构造函数。当然,基类中没有定义构造函数,派生类根本不必负责调用基类的析构函数。 (2) 当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数途径。在有的情况下,派生类构造函数的函数体可能为空,仅起到参数传递作用。如本讲第一个例子就属此种情况。 子类型化和类型适应 1. 子类型化 子类型的概念涉及到行为共享,它与继承有着密切关系。 有一个特定的类型S,当且仅当它至少提供了类型T的行为,由称类型S是类型T的子类型。子类型是类型之间的一般和特殊的关系。 在继承中,公有继承可以实现子类型。例如: class A { public: void Print() const { cout<<"A::print() called.\n"; } }; class B : public A { public: void f() {} }; 类B继承了类A,并且是公有继承方式。因此,可以说类B是类A的一个子类型。类A还可以有其他的子类型。类B是类A的子类型,类B具备类A中的操作,或者说类A中的操作可被用于操作类B的对象。 子类型关系是不可逆的。这就是说,已知B是A的子类型,而认为A也是B的子类型是错误的,或者说,子类型关系是不对称不。 因此,可以说公有继承可以实现子类型化。 2. 类型适应 类型适应是指两种类型之间的关系。例如,B类型适应A类型是指B类型的对象能够用于A类型的对象所能使用的场合。 前面讲过的派生类的对象可以用于基类对象所能使用的场合,我们说派生类适应于基类。 同样道理,派生类对象的指针和引用也适应于基类对象的指针和引用。 子类型化与类型适应是致的。A类型是B类型的子类型,那么A类型必将适应于B类型。 子类型的重要性就在于减轻程序人员编写程序代码的负担。因为一个函数可以用于某类型的对象,则它也可以用于该类型的各个子类型的对象,这样就不必为处理这些子类型的对象去重载该函数。 C++多继承 多继承可以看作是单继承的扩展。所谓多继承是指派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。 多继承下派生类的定义格式如下: class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… { <派生类类体> }; 其中,<继承方式1>,<继承方式2>,…是三种继承方式:public、private、protected之一。例如: class A { … }; class B { … }; class C : public A, public, B { … }; 其中,派生类C具有两个基类(类A和类B),因此,类C是多继承的。按照继承的规定,派生类C的成员包含了基类B中成员以及该类本身的成员。 多继承的构造函数 在多继承的情况下,派生类的构造函数格式如下: <派生类名>(<总参数表>):<基类名1>(<参数表1>),<基类名2>(<参数表2>),… <子对象名>(<参数表n+1>),… { <派生类构造函数体> } 其中,<总参数表>中各个参数包含了其后的各个分参数表。 多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参数个数。 派生类构造函数执行顺序是先执行所胡基类的构造函数,再执行派生类本身构造函数,处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的各基类顺序,与派生类构造函数中所定义的成员初始化列表的各项顺序无关。也就是说,执行基类构造函数的顺序取决于定义派生类时基类的顺序。可见,派生类构造函数的成员初始化列表中各项顺序可以任意地排列。 下面通过一个例子来说明派生类构造函数的构成及其执行顺序。 #include class B1 { public: B1(int i) { b1 = i; cout<<"构造函数 B1."< } void print() { cout< private: int b1; }; class B2 { public: B2(int i) { b2 = i; cout<<"构造函数 B2."< } void print() { cout< private: int b2; }; class B3 { public: B3(int i) { b3 = i; cout<<"构造函数 B3."< } int getb3() { return b3; } private: int b3; }; class A : public B2, public B1 { public: A(int i, int j, int k, int l):B1(i), B2(j), bb(k) { a = l; cout<<"构造函数 A."< } void print() { B1::print(); B2::print(); cout< } private: int a; B3 bb; }; void main() { A aa(1, 2, 3, 4); aa.print(); } 该程序的输出结果为: 构造函数 B2.2 构造函数 B1.1 构造函数 B3.3 构造函数 A.4 1 2 4, 3 在该程序中,作用域运算符::用于解决作用域冲突的问题。在派生类A中的print()函数的定义中,使用了B1::print;和B2::print();语句分别指明调用哪一个类中的print()函数,这种用法应该学会。 C++多继承 8/27/2001 8:37:55· ·--··pcvc 上一页 1 2 二义性问题 一般说来,在派生类中对基类成员的访问应该是唯一的,但是,由于多继承情况下,可能造成对基类中某成员的访问出现了不唯一的情况,则称为对基类成员访问的二义性问题。 实际上,在上例已经出现过这一问题,回忆一下上例中,派生类A的两基类B1和B2中都有一个成员函数print()。如果在派生类中访问print()函数,到底是哪一个基类的呢?于是出现了二义性。但是在上例中解决了这个问题,其办法是通过作用域运算符::进行了限定。如果不加以限定,则会出现二义性问题。 下面再举一个简单的例子,对二义性问题进行深入讨论。例如: class A { public: void f(); }; class B { public: void f(); void g(); }; class C : public A, public B { public: void g(); void h(); }; 如果定义一个类C的对象c1: C c1; 则对函数f()的访问 c1.f(); 便具有二义性:是访问类A中的f(),还是访问类B中的f()呢? 解决的方法可用前面用过的成员名限定法来消除二义性,例如: c1.A::f(); 或者 c1.B::f(); 但是,最好的解决办法是在类C中定义一个同名成员f(),类C中的f()再根据需要来决定调用A::f(),还是B::f(),还是两者皆有,这样,c1.f()将调用C::f()。 同样地,类C中成员函数调用f()也会出现二义性问题。例如: viod C::h() { f(); } 这里有二义性问题,该函数应修改为: void C::h() { A::f(); } 或者 void C::h() { B::f(); } 或者 void C::f() { A::f(); B::f(); } 另外,在前例中,类B中有一个成员函数g(),类C中也有一个成员函数g()。这时, c1.g(); 不存在二义性,它是指C::g(),而不是指B::g()。因为这两个g()函数,一个出现在基类B,一个出现在派生类C,规定派生类的成员将支配基类中的同名成员。因此,上例中类C中的g()支配类B中的g(),不存在二义性,可选择支配者的那个名字。 当一个派生类从多个基类派生类,而这些基类又有一个共同的基类,则对该基类中说明的成员进行访问时,也可能会出现二义性。例如: class A { public: int a; }; class B1 : public A { private: int b1; }; class B2 : public A { private: int b2; }; class C : public B1, public B2 { public: int f(); private: int c; }; 已知:C c1; 下面的两个访问都有二义性: c1.a; c1.A::a; 而下面的两个访问是正确的: c1.B1::a; c1.B2::a; 类C的成员函数f()用如下定义可以消除二义性: int C::f() { retrun B1::a + B2::a; } 由于二义性的原因,一个类不可以从同一个类中直接继承一次以上,例如: class A : public B, public B { … } 这是错误的。 C++ 对象与数组 对象数组是指数组元素为对象的数组。该数组中若干个元素必须是同一个类的若干个对象。对象数组的定义、赋值和引用与普通数组一样,只是数组的元素与普通数组不同,它是同类的若干个对象。 1. 对象数组的定义 对象数组定义格式如下: <类名><数组名>[<大小>]... 其中,<类名>指出该数组元素是属于该类的对象,方括号内的<大小>给出某一维的元素个数。一维对象数组只有一个方括号,二维对象数组要有两个方括号,等等,例如: DATE dates[7]; 表明dates是一维对象数组名,该数组有7个元素,每个元素都是类DATE的对象。 2. 对象数组的赋值 对象数组可以被赋初值,也可以被赋值。例如: class DATE { public: DATE(int m, int d, int y); void printf(); private: int month, day, year; }; 下面是定义对象数组并赋初值和赋值: DATE dates[4]={ DATE(7, 7, 2001), DATE(7, 8, 2001), DATE(7, 9, 2001), DATE(7, 10, 2001) } 或者 dates[0] = DATE(7, 7, 2001); dates[1] = DATE(7, 8, 2001); dates[2] = DATE(7, 9, 2001); dates[3] = DATE(7, 10, 2001); 指向数组的指针和指针数组 指向数组的指针和指针数组是两个完全不同的概念,现放在一起介绍是中为两者在定义格式相似,千万不要把它们搞混了。 1. 指向数组的指针 指向一般数组的指针定义格式如下: <类型说明符>(*<指针名>)[<大小>]... 其中,用来说明指针的 * 要与<指针名>括在一起。后面用一个方括号表示该指针指向一维数组,后面用二个方括号表示该指针指向二维数组。<类型说明符>用来说明指针所指向的数组的元素的类型。例如: int (*P)[3]; P是一个指向一维数组的指针,该数组有3个int型元素。 而指向对象数组的指针,则把<类型说明符>改为<类名>即可: <类名>(*<指针名>)[<大小>]... 指向数组的指针的主要应用思想是:将数组的首地址(二维数组的某个行地址)赋给指针,然后通过循环(for)改变指针指向的地址,从而动态的访问数组中各个元素。 2. 指针数组 所谓指针数组指的是数组元素为指针的那类数组。一个数组的元素可以是指向同一类型的一般指针,也可以是指向同一类类型的对象。 一般指针数组的定义格式如下: <类型名>*<数组名>[<大小>]... 其中,*加在<数组名>前面表示该数组为指针数组。[<大小>]表示某一维的大小,即该维的元素个数,…表示可以是多维指针数组,每一个[<大小>]表示一维。例如: int * pa[3]; char * pc[2][5]; 在C++编程中,经常使用char型的指针数组用来存放若干个字符串。下面是一个一维指针数组的例子。 #include #include const int N = 5; void main() { char *strings[N]; file://定义一个一维指针数组strings char str[80]; cout<<"At each prompt, enter a string:\n"; for (int i=0; i { cout<<"Enter a string #"< cin.getline(str, sizeof(str)); strings[i] = new char[strlen(str) + 1]; strcpy(strings[i], str); } cout< for (i=0; i cout<<"String #"< } 对象指针数组的定义如下: 对象指针数组是指该数组的元素是指向对象的指针,它要求所有数组元素都是指向同一个类类型的对象的指针。格式如下: <类名>*<数组名>[<大小>]... 它与前面讲过的一般的指针数组所不同的地方仅在于该数组一定是指向对象的指针。即指向对象的指针用来作该数组的元素。下面通过一个例子看一下对象指针数组的用法。 #include class A { public: A(int i=0, int j=0) { a=i; b=j; } void print(); private: int a, b; }; void A::print() { cout< } void main() { A a1(7, 8), a2, a3(5, 7); A *b[3] = { &a3, &a2, &a1 }; for (int i=0; i<3; i++) b[i]->print(); } 带参数的main()参数 前面讲过的main()函数都是不带参数的。在实际编程中,有时需要main()带参数。通过main()函数的参数给程序增加一些处理信息。一般地说,当使用C++编写的源程序经过编译连接生成的可执行文件在执行时,需要还命令行参数,由该源程序的主函数main()就需要带参数。使用所还有的参数来存放命令行中的参数,以便在程序中对命令行参数进行处理。 带有参数的main()函数头格式如下: void main(int argc, char * argv[]) 其中,第一个参数argc是int型的,它用来存放命令行参数的个数,实际上argc所存放的数值比命令行参数的个数多1,即将命令字(可执行文件名)也计算在内。第二个参数argv是一个一维的一级指针数组,它是用来存放命令行中各个参数和命令字的字符串的,并且规定: argv[0]存放命令字 argv[1]存放命令行中第一个参数 argv[2]存放命令行中第二个参数 … 这里,argc的值和argv[]各元素的值都是系统自动组赋值的。 在这里讲述带参数的main()函数实际上是对指针数组应用的一个具体实例。 #include void main(int argc, char *argv[]) { cout<<"The number of command line arguments is:"< cout<<"The program name is:"< if (argc>1) { cout<<"The command line arguments:\n"; for (int i=1; i cout< } } 上述编译连接后的EXE文件,可在DOS命令行下调试。 关于命令行参数的使用,其基本方法是直接引用指针数组argv[]中某个元素所存放的字符串,可用下标方式,也可用指针方式。 C++ 类的静态成员(static) 静态成员的提出是为了解决数据共享的问题。实现共享有许多方法,如:设置全局性的变量或对象是一种方法。但是,全局变量或对象是有局限性的。这一章里,我们主要讲述类的静态成员来实现数据的共享。 静态数据成员 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。 使用静态数据成员可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。 静态数据成员的使用方法和注意事项如下: 1、静态数据成员在定义或说明时前面加关键字static。 2、静态成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式如下: <数据类型><类名>::<静态数据成员名>=<值> 这表明: (1) 初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆。 (2) 初始化时不加该成员的访问权限控制符private,public等。 (3) 初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员。 3、静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化。 4、引用静态数据成员时,采用如下格式: <类名>::<静态成员名> 如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员。 下面举一例子,说明静态数据成员的应用: #include class Myclass { public: Myclass(int a, int b, int c); void GetNumber(); void GetSum(); private: int A, B, C; static int Sum; }; int Myclass::Sum = 0; Myclass::Myclass(int a, int b, int c) { A = a; B = b; C = c; Sum += A+B+C; } void Myclass::GetNumber() { cout<<"Number="< } void Myclass::GetSum() { cout<<"Sum="< } void main() { Myclass M(3, 7, 10),N(14, 9, 11); M.GetNumber(); N.GetNumber(); M.GetSum(); N.GetSum(); } 从输出结果可以看到Sum的值对M对象和对N对象都是相等的。这是因为在初始化M对象时,将M对象的三个int型数据成员的值求和后赋给了Sum,于是Sum保存了该值。在初始化N对象时,对将N对象的三个int型数据成员的值求和后又加到Sum已有的值上,于是Sum将保存另后的值。所以,不论是通过对象M还是通过对象N来引用的值都是一样的,即为54。 静态成员函数 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数中要引用非静态成员时,可通过对象来引用。下面通过例子来说明这一点。 #include class M { public: M(int a) { A=a; B+=a;} static void f1(M m); private: int A; static int B; }; void M::f1(M m) { cout<<"A="< cout<<"B="< } int M::B=0; void main() { M P(5),Q(10); M::f1(P); file://调用时不用对象名 M::f1(Q); } 读者可以自行分析其结果。从中可看出,调用静态成员函数使用如下格式: <类名>::<静态成员函数名>(<参数表>); C++ 虚基类 在《多继承》中讲过的例子中,由类A,类B1和类B2以及类C组成了类继承的层次结构。在该结构中,类C的对象将包含两个类A的子对象。由于类A是派生类C两条继承路径上的一个公共基类,那么这个公共基类将在派生类的对象中产生多个基类子对象。如果要想使这个公共基类在派生类中只产生一个基类子对象,则必须将这个基类设定为虚基类。 虚基类的引入和说明 前面简单地介绍了要引进虚基类的原因。实际上,引进虚基类的真正目的是为了解决二义性问题。 虚基类说明格式如下: virtual <继承方式><基类名> 其中,virtual是虚类的关键字。虚基类的说明是用在定义派生类时,写在派生类名的后面。例如: class A { public: void f(); protected: int a; }; class B : virtual public A { protected: int b; }; class C : virtual public A { protected: int c: }; class D : public B, public C { public: int g(); private: int d; }; 由于使用了虚基类,使得类A,类B,类C和类D之间关系用DAG图示法表示如下: A{ f(), a } / \ B{b} C{c} \ / D{g(),d} 从该图中可见不同继承路径的虚基类子对象被合并成为一个对象。这便是虚基类的作用,这样将消除了合并之前可能出现的二义性。这时,在类D的对象中只存在一个类A的对象。因此,下面的引用都是正确的: D n; n.f(); file://对f()引用是正确的。 void D::g() { f(); file://对f()引用是正确的。 } 下面程序段是正确的。 D n; A *pa; pa = &n; 其中,pa是指向类A对象的指针,n是类D的一个对象,&n是n对象的地址。pa=&n是让pa指针指向类D的对象,这是正确的,并且也无二义性。 C++ 虚基类 9/3/2001 8:22:51· ·--··pcvc 上一页 1 2 虚基类的构造函数 前面讲过,为了初始化基类的子对象,派生类的构造函数要调用基类的构造函数。对于虚基类来讲,由于派生类的对象中只有一个虚基类子对象。为保证虚基类子对象只被初始化一次,这个虚基类构造函数必须只被调用一次。由于继承结构的层次可能很深,规定将在建立对象时所指定的类称为最派生类。C++规定,虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始列表中必须列出对虚基类构造函数的调用。如果未被列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类子对象。 从虚基类直接或间接继承的派生类中的构造函数的成员初始化列表中都要列出这个虚基类构造函数的调用。但是,只有用于建立对象的那个最派生类的构造函数调用虚基类的构造函数,而该派生类的基类中所列出的对这个虚基类的构造函数调用在执行中被忽略,这样便保证了对虚基类的对象只初始化一次。 C++又规定,在一个成员初始化列表中出现对虚基类和非虚基类构造函数的调用,则虚基类的构造函数先于非虚基类的构造函数的执行。 下面举一例子说明具有虚基类的派生类的构造函数的用法。 #include class A { public: A(const char *s) { cout< ~A() {} }; class B : virtual public A { public: B(const char *s1, const char *s2):A(s1) { cout< } }; class C : virtual public A { public: C(const char *s1, const char *s2):A(s1) { cout< } }; class D : public B, public C { public: D(const char *s1, const char *s2, const char *s3, const char *s4) :B(s1, s2), C(s1, s3), A(s1) { cout< } }; void main() { D *ptr = new D("class A", "class B", "class C", "class D"); delete ptr; } 该程序的输出结果为: class A class B class C class D 在派生类B和C中使用了虚基类,使得建立的D类对象只有一个虚基类子对象。 在派生类B,C,D的构造函数的成员初始化列表中都包含了对虚基类A的构造函数。 在建立类D对象时,只有类D的构造函数的成员初始化列表中列出的虚基类构造函数被调用,并且仅调用一次,而类D基类的构造函数的成员初始化列表中列出的虚基类构造函数不被执行。这一点将从该程序的输出结果可以看出。