PPC的C/C++和人工智能学习笔记
每一篇学习笔记,都只是为了更好地掌握和理解

C++语言基础(9)_类的继承(续1)

C++基础,今天继续学习类的继承

 

基类或子对象只有带参构造函数:

上一次的说到,有继承和有子对象的派生类的构造次序,是先按从左到右的顺序先构造基类,再按从上到下的次序构造子对象,最后才是构造本身。我们测试的时候,并没有给基类和子对象使用带参构造,假如基类或者子对象只有带参构造的时候,这个参数应该怎么给呢?

答案是:只能写在该类的构造成员初始化列表上。看例子:

class A {

public:

int x;

A(int x) { this->x = x; } //只有带参构造

~A(){}

};

 

class B:public A { //有继承

public:

A a; //子对象,虽然A类只有带参构造,但是参数也只能在成员初始化列表中给出。不能写成A a(1)这样的格式

A *b; //子对象指针,不需要和上面那样处理,因为这里只是个指针变量而已

B(int x, int y) :A(x), a(y) { //次序没有关系,基类和子对象的带参构造必须在这里赋值

b = new A(x); //指针子对象在 new的时候给参数就可以了。

}

};

 

派生类有与基类同名的数据成员:

数据成员肯定是被派生类继承了的,但是直接用a.x访问,就是本类的数据,访问基类的需要使用a:A::x的方式。

class A {

public:

int x;

};

class B :public A{

public:

int x;

};

class C :public B{

public:

int x; //基类有x,基类的基类有x

};

int main() {

C c;

c.x = 10;

c.B::x = 20;

c.B::A::x = 30;

return 0;

}

 

基类对象和派生类对象之间的赋值关系(1):

class A{};

class B:public A{};

A a;

B b;

a = b; //可以

b = a; //不可以

派生类对象可以给基类对象赋值,反之不行。原因是基类对象的成员一般情况下比派生类对象的成员少,就是一样多,也不行。

接上面的例子:(指针表达)

A * pa=&b; //可以

B * pb=&a; //不可以

接上面的例子:(引用表达)

A &fa = b; //可以

B &fb = a; //不可以

 

假如,一个类继承了2个父类呢,是怎么赋值的?

class A{public: int x;};

class B{public: int x;};

class C:public A,public B{ public: int x; };

A a;

B b;

C c;

c.A::x = 1; c.B::x=2; c.x=3;

a=c; //正确 此时 a.x == 1

b=c; //正确 此时 b.x == 2

上例说明:一个派生类同时继承多个基类时,派生类对象都是可以直接赋值给多个基类对象的,并且,它会自动匹配派生类对象中那个隐藏的无名基类对象。

虚继承和虚基类:

class A{};

class B:virtual public A{};

使用了virtual关键字,就是虚继承,此时基类称为虚基类。

特点:

1:在类中只要出virtual关键字,这个类就会多四字节;(就算有多个虚继承,也只有4个字节),这个多出来的4字节,是放在该类对象的最前面的。

2:虚继承的virtual只是理解为做一次判断,虚基类是否已经构造,如果构造将不再构造,所以这个多出来的4字节就是一个函数指针,指向一个判断虚基类是否已经构造的函数。

3:虚基类如果带参,那么不管是直接派生还是间接派生都要给虚基类传参。

4:在继承链中,只要出现虚基类,虚基类是最优先构造的。

5:所以不管哪个派生类都需要给虚基类传参。

 

验证一:混合继承类对象的构造次序,当然,析构次序完全相反。

验证一的代码:

class A {public: int i=1; A() { cout << “A” << endl; }};

class B {public: int i=2; B() { cout << “B” << endl; }};

class C {public: int i=3; C() { cout << “C” << endl; }};

class D {public: int i=4; D() { cout << “D” << endl; }};

class AA :public A { public: int i=5; AA() { cout << “AA” << endl; } };

class AA1 :virtual public A { public: int i=6; AA1() { cout << “AA1” << endl; } };

class BB:virtual public A,public B{ public: int i=7; BB() { cout << “BB” << endl; } };

class BB1:public B,public C{ public: int i=8; BB1() { cout << “BB1” << endl; } };

class CC:public C, virtual public D{ public: int i=9; CC() { cout << “CC” << endl; } };

class XXX :public AA, public AA1, virtual public BB, public BB1, virtual public CC,

virtual public D {

public: int i=10; XXX() { cout << “XXX” << endl; }};

XXX x;

//期望:

/*每一个继承的构造:

A AA

A(虚) AA1

A(虚) B BB(虚) //因为 BB(虚),所以B必须比BB(虚)先构造

B C BB1

C D(虚) CC(虚) //因为CC(虚),所以C必须比CC(虚)先构造

D(虚)//虚只构造1次

XXX

 

期望:

A(虚),B,BB(虚),D(虚),C,CC(虚),A,AA,AA1,B,C,BB1,XXX

运行结果:

A,    B,BB,    D,    C,CC,   A,AA,AA1,B,C,BB1,XXX

*/

 

验证二:虚继承的字节数。

字节数是64,这个看后面的内存分析吧,感觉有点晕乎了。反正计算就是虚基类只计算1次字节数,但是同时有虚继承和正常继承的话,那么这2个都是存在的。

验证三:混合继承类对象的数据内存:

方法:打开调试的内存,通过类似printf(“%p\n”, &x.AA::A::i);的方式来寻找相对应的内存地址。

下图是打印出了该对象的内存地址(从低到高),一共是64个字节,每4个字节一行,分16行来显示的。

验证四:虚基类如果带参,那么不管是直接派生还是间接派生都要给虚基类传参

class A {

public:

int i;

A(int i) { this->i = i; }

};

class B :virtual public A {

public:

int j;

B(int i) :A(20) { j = 200; }

B() :A(10) { j = 100; }

//构造C的时候没有显式调用,所以自动调用这个无参的,

//但是显然:这里的 A(10)没有调用,因为A已经构造成功了

};

class C :public B {

public:

C(int i):A(30){}

//实际上,C c(1); c.B::A::i == 30; 或者 c.i ==30

//因为虚基类首先被构造

};

 

验证五:虚继承的情况下,派生类对象还可以给基类对象赋值吗?

结论是可以的,还是会自动匹配赋值。

但是:假如派生类里面有多个基类的copy,不管是虚继承还是正常继承引起的,那么会引起歧义(ambiguous),此时无法赋值。

class A {

public:

int i;

A(int i) { this->i = i; }

};

class B :virtual public A {

public:

int i ;

B(int i):A(i+1) { this->i = i; }

};

class C :public B {

public:

int i;

C(int ic, int ib, int ia) :B(ib), A(ia) { i = ic; }

};

 

int main() {

A a(1); // i=1

B b(2); // i=2 隐藏i=3

C c(30,20,10); //i=30,隐藏分别20,10

cout << c.i<<“,”<<c.B::i <<“,”<<c.B::A::i << endl; //输出 30,20,10

a = b;

cout << a.i << endl; //输出3

a = c;

cout << a.i << endl; //输出10

b = c;

cout << b.i << endl; //输出20

return 0;

}

练习:

1:请解释以下代码为什么会出现那样的结果,提示,自己去想类的成员函数的调用方式

class CNpc

{

int hp;

public:

CNpc() {hp = 10;}

int GetHp() const {return hp;}

};

 

class CHero

{

int mp;

public:

CHero() {mp = 20;}

int GetMp() const {return mp;}

};

 

CNpc n;

CHero *pHero = (CHero *)&n;

cout << pHero->GetMp() << endl; //输出10

 

解答:

第一:根据sizeof(类名),我们看到在类的对象存放的内存中,其实是只有对象的成员数据的,那么猜测类的成员函数是另外存放的,而且不管对象是不是创建,都已经存放在内存的某个地方(比如代码区)。验证:class A{public: void abc(){cout <<”hello”;}}; 定义了一个类A,该类有个公有函数abc,就是打印hello。我现在 A *p=NULL; p->abc(); 说明:p是一个指向A类型的指针,而且设置为NULL,可以认为,此时*p对象根本没有构建,仅仅是声明了一下(*p)是A类型的,但是实际测试中发现 p->abc(); 能够成功调用,打印出了hello。说明上面的猜测是正确的。

第二:实际上,类的成员函数,在调用的时候,会自动增加一个参数,就是this指针,该指针指向的就是对象的首地址。而上面的那个p->abc()可以正确调用,是因为仅仅传了一个地址而已,没有用到该地址里面的数据,所以才正确输出了。所以我们得出结论:什么类型的对象,他去调用成员函数的时候,就是调用该类型的成员函数,只是增加一个this指针的参数。

第三:上面代码中,类CNpc的对象n被强制转换为类CHero的对象并把首地址赋值给CHero类型的指针pHero,决定了pHero去调用成员函数的时候,就只能调用CHero类的成员函数。所以只能是pHero->GetMp(); 而通过强转后,此时内存中的4字节就是对应的mp的值了,而原先内存中是10,所以mp就是10了。

 

2:继承中出现多个虚基类,构造顺序是怎么样的?

按从左到右的次序。但是在复杂的多级继承里,假如的虚继承的上面有正常继承,为了实现这个虚继承,也会先实现必要的正常继承。

 

3:然后描述清楚派生类的内存分布

假如有虚继承,首先出现的是虚继承的4个字节。

然后是按从左到右次序的正常继承的数据依次存放。

然后是本身的数据,从上到下次序。

然后是虚继承的数据,按从左到右次序。

 

(2017-03-25 www.vsppc.com)

学习笔记未经允许不得转载:PPC的C/C++和人工智能学习笔记 » C++语言基础(9)_类的继承(续1)

分享到:更多 ()

评论 抢沙发

评论前必须登录!