对于类以及类继承, ⼏个主要的问题:
1) 继承⽅式: public/protected/private继承.
这是c++搞的, 实际上继承⽅式是⼀种允许⼦类控制的思想. ⼦类通过public继承, 可以把基类真实还原, ⽽private继承则完全把基类屏蔽掉. 这种屏蔽是相对于对象层⽽⾔的, 就是说⼦类的对象完全看不到基类的⽅法, 如果继承⽅式是private的话, 即使⽅法在基类中为public的⽅法.但继承⽅式并不影响垂直⽅向的访问特性, 那就是⼦类的函数对基类的成员访问是不受继承⽅式的影响的.
⽐较(java): java是简化的, 其实可认为是c++中的public继承. 实在没必要搞private/protected继承, 因为如果想控制,就直接在基类控制就好了.
2) 对象初始化顺序: c++搞了个成员初始化列表, 并确明确区分初时化跟赋值的区别. c++对象的初始化顺序是:(a) 基类初始化(b) 对象成员初时化(c) 构造函数的赋值语句
举例:
假设 class C : public A, public B {D d;//}
则初始化的顺序是A, B, D, C的构造函数.
这⾥基类的初始化顺序是按照声明的顺序, 成员对象也是按照声明的顺序. 因此 c(int i, int j) : B(i), A(j) {} //这⾥成员初始化列表的顺序是不起作⽤的;
析构函数的顺序则刚好是调过来, 构造/析构顺序可看作是⼀种栈的顺序;
⽐较(java): java中初始化赋值是⼀回事. ⽽且对基类的构造函数调⽤必须显⽰声明, 按照你⾃⼰写的顺序. 对成员对象, 也叫由你初始化.没有什么系统安排的顺序问题, 让你感觉很舒服;
3) 多继承问题: c++⽀持多继承, 会导致\"根\"不唯⼀. ⽽java则没有该问题;
此外c++没有统⼀的root object, java所有对象都存在Object类使得很多东西很⽅便. ⽐如公共的seriall, persistent等等.
4) 继承中的重载: c++中, 派⽣类会继承所有基类的成员函数, 但构造函数, 析构函数除外.
这意味着如果B 继承A, A(int i)是基类构造函数, 则⽆法B b(i)定义对象. 除⾮B也定义同样的构造函数. c++的理由是, 假如派⽣类定义了新成员, 则基类初始化函数⽆法初始化派⽣类的所有新增成员.
⽐较(java): java中则不管, 就算有新增对象基类函数没有考虑到, ⼤不了就是null, 或者你⾃⼰有缺省值. 也是合理的.
5) 继承中的同名覆盖和⼆义性: 同名覆盖的意思是说, 当派⽣类跟基类有完全⼀样的成员变量或者函数的时候, 派⽣类的会覆盖基类的.
类似于同名的局部变量覆盖全局变量⼀样. 但被覆盖的基类成员还是可以访问的.如B继承A, A, B都有成员变量a,则B b, b.a为访问B的a, b.A::a则为访问基类中的a. 这对于成员函数也成⽴.
但需要注意的是, 同名函数必须要完全⼀样才能覆盖. int func(int j)跟int func(long j)其实是不⼀样的. 如果基类,派⽣类有这两个函数, 则不会同名覆盖.
最重要的是, 两者也不构成重载函数. 因此假如A有函数int func(int j), B有函数int func(long j). 则B的对象b.func(int)调⽤为错误的. 因为B中的func跟它根本就不构成重载.
同名覆盖导致的问题是⼆义性. 假如C->B=>A, 这⾥c继承B, B继承A. 假如A, B都有同样的成员fun, 则C的对象c.fun存在⼆义性. 它到底是指A的还是B的fun呢?
解决办法是⽤域限定符号c.A::fun来引⽤A的fun.
另外⼀个导致⼆义性的是多重继承. 假设B1, B2都继承⾃B, D则继承B1, B2. 那么D有两个B⽽产⽣⼆义性.
这种情况的解决办法是⽤虚基类. class B1 : virtual public B, class B2:virtual public B, D则为class D : public B1, public B2. 这样D中的成员只包含⼀份B的成员使得不会产⽣⼆义性.
⽐较(java). java中是直接覆盖. 不给机会这么复杂, 还要保存基类同名的东西. 同名的就直接覆盖, 没有同名的就直接继承.
虚基类的加⼊, 也影响到类的初始化顺序. 原则是每个派⽣类的成员化初始化列表都必须包含对虚基类的初始化.
最终初始化的时候, 只有真正实例化对象的类的调⽤会起作⽤. 其它类的对虚基类的调⽤都是被忽略的. 这可以保证虚基类只会被初始化⼀次.
c++没有显式接⼝的概念, 我觉得是c++语⾔的败点. 这也是导致c++要⽀持组件级的重⽤⾮常⿇烦. 虽然没有显式的接⼝, 但c++中的纯虚函数以及抽象类的⽀持, 事实上是等同于接⼝设施的. 当⼀个类中, 所有成员函数都是纯虚函数, 则该类其实就是接⼝.java c++
接⼝ 类(所有成员函数都是纯虚函数)抽象类 类(部分函数是虚函数)对象类 对象类
C++构造函数调⽤顺序
1. 如果类⾥⾯有成员类,成员类的构造函数优先被调⽤;
2. 创建派⽣类的对象,基类的构造函数优先被调⽤(也优先于派⽣类⾥的成员类);
3. 基类构造函数如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺序⽽不是它们在成员初始化表中的顺序;
4. 成员类对象构造函数如果有多个成员类对象,则构造函数的调⽤顺序是对象在类中被声明的顺序⽽不是它们出现在成员初始化表中的顺序;
5. 派⽣类构造函数,作为⼀般规则派⽣类构造函数应该不能直接向⼀个基类数据成员赋值⽽是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供⼀组适当的基类构造函数)举例:
#include class B {public:B{…}~B{…}}; class D {public:D{…}~D{…}}; class E {public:E{…}~E{…}}; class C :public A,public B {public:C{…}private:D objD_; E objE_;~C{…}} int main(void){ C test; return 0; } 运⾏结果是: A{…}//派⽣表中的顺序B{…} D{…}//成员类的构造函数优先被调⽤E{…}C{…}~C{…}~E{…}~D{…}~B{…}~A{…} 从概念上来讲,构造函数的执⾏可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段:初始化阶段: 所有类类型(class type)的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中; 计算阶段: ⼀般⽤于执⾏构造函数体内的赋值操作。 下⾯的代码定义两个结构体,其中Test1有构造函数,拷贝构造函数及赋值运算符,为的是⽅便查看结果,Test2是个测试类,它以Test1的对象为成员,我们看⼀下Test2的构造函数是怎么样执⾏的。class Test1{ Test1() //⽆参构造函数{ cout << \"Construct Test1\" << endl ;} Test1(const Test1& t1) //拷贝构造函数{ cout << \"Copy constructor for Test1\" << endl ;this->a = t1.a ;} Test1& operator = (const Test1& t1) //赋值运算符{ cout << \"assignment for Test1\" << endl ;this->a = t1.a ;return *this;} int a ;}; struct Test2{ Test1 test1 ;Test2(Test1 &t1){ test1 = t1 ;}}; 调⽤代码:Test1 t1 ;Test2 t2(t1) ;输出: Construct Test1Construct Test1 assignment for Test1解释⼀下: 第⼀⾏输出对应调⽤代码中第⼀⾏,构造⼀个Test1对象; 第⼆⾏输出对应Test2构造函数中的代码,⽤默认的构造函数初始化对象test1 // 这就是所谓的初始化阶段;第三⾏输出对应Test2的赋值运算符,对test1执⾏赋值操作 // 这就是所谓的计算阶段; 为什么使⽤初始化列表? 初始化类的成员有两种⽅式,⼀是使⽤初始化列表,⼆是在构造函数体内进⾏赋值操作。 主要是性能问题,对于内置类型,如int, float等,使⽤初始化类表和在构造函数体内初始化差别不是很⼤,但是对于类类型来说,最好使⽤初始化列表,为什么呢? 由下⾯的测试可知,使⽤初始化列表少了⼀次调⽤默认构造函数的过程,这对于数据密集型的类来说,是⾮常⾼效的。同样看上⾯的例⼦,我们使⽤初始化列表来实现Test2的构造函数。struct Test2{ Test1 test1 ; Test2(Test1 &t1):test1(t1){}} 使⽤同样的调⽤代码,输出结果如下:Construct Test1 Copy constructor for Test1 第⼀⾏输出对应 调⽤代码的第⼀⾏ 第⼆⾏输出对应Test2的初始化列表,直接调⽤拷贝构造函数初始化test1,省去了调⽤默认构造函数的过程。所以⼀个好的原则是,能使⽤初始化列表的时候尽量使⽤初始化列表; 除了性能问题之外,有些时场合初始化列表是不可或缺的,以下⼏种情况时必须使⽤初始化列表:1.常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表⾥⾯; 2.引⽤类型,引⽤必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表⾥⾯; 3.没有默认构造函数的类类型,因为使⽤初始化列表可以不必调⽤默认构造函数来初始化,⽽是直接调⽤拷贝构造函数初始化; struct Test1 {Test1(int a):i(a){}int i;}; struct Test2 {Test1 test1 ;}; 以上代码⽆法通过编译,因为Test2的构造函数中 test1 = t1 这⼀⾏实际上分成两步执⾏:1. 调⽤Test1的默认构造函数来初始化test1; 由于Test1没有默认的构造函数,所以1 ⽆法执⾏,故⽽编译错误。正确的代码如下,使⽤初始化列表代替赋值操作,struct Test2 {Test1 test1 ; Test2(int x):test1(x){}} 成员变量的初始化顺序: 先定义的成员变量先初始化 成员是按照他们在类中出现的顺序进⾏初始化的,⽽不是按照他们在初始化列表出现的顺序初始化的,看代码:struct foo {int i ;int j ; foo(int x):i(x), j(i){}; // ok, 先初始化i,后初始化j}; 再看下⾯的代码:struct foo {int i ;int j ; foo(int x):j(x), i(j){} // i值未定义}; 这⾥i的值是未定义的因为虽然j在初始化列表⾥⾯出现在i前⾯,但是i先于j定义,所以先初始化i,⽽i由j初始化,此时j尚未初始化,所以导致i的值未定义。 ⼀个好的习惯是,按照成员定义的顺序进⾏初始化。 对于全局对象(global object),VC下是先定义先初始化,但C++标准没做规定。 全局对象默认是静态的,全局静态(static)对象必须在main()函数前已经被构造,告知编译器将变量存储在程序的静态存储区,由C++ 编译器startup代码实现。