从前一座大山下住着一名老翁,他家门前有两座大山,切断了他家和外界的联系。因此他决心把山平掉,另一个“聪明”的智叟笑他太傻, 认为不能。老翁说:“汝心之固,固不可彻,曾不若孀妻弱子。虽我之死,有子存焉;子又生孙,孙又生子;子又有子,子又有孙;子子孙孙无穷匮也,而山不加增,何苦而不平?”
大家都知道是谁吧,当初看到继承这个概念,我第一反应就是愚公的那句,“虽我之死,有子存焉;子又生孙,孙又生子;子又有子,子又有孙;子子孙孙无穷匮也”。它的理想信念今生完不成,继承给他儿子,儿子完不成再继承给孙子,这样继承下去,早晚有一天山会移完的。
故事听到这里也该回到正题了,通过这个故事,我想说的是我最早从课本上生动的了解到的关于继承的概念就是来自它了。而今天,作为计算机专业学生,就让我向你们讲述什么是C++编程语言的继承。Let’s go~~~
第一步·初识继承
- 继承是什么
面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。 - 定义格式:
1. class 派生类名 : 继承方式 基类名
{
派生类新增的数据成员和成员函数;
}
- 继承关系&访问限定符
3种类成员访问限定符: public(公有),private(私有),protected(保护)
3种继承关系:public(公有继承),private(私有继承),protected(保护继承) - 继承权限
- (1)公有继承
①基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员在派生类中存在不可访问。
②派生类的成员函数可直接访问基类的public和protected成员,但不能访问基类的private成员。
③通过派生类的对象只能访问基类的public成员。
(2)私有继承
①基类的public和protected成员都以private身份出现在派生类中,但基类的private成员在派生类中存在不可访问 。
②派生类中的成员函数可以直接访问基类中的public和protected成员,但不能访问基类的private成员。
③通过派生类的对象补不能访问基类中的任何成员。
(3)保护继承
①基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员在派生类中存在不可访问。
②派生类中的成员函数可以直接访问基类中的public和protected成员,但不能访问基类的private成员。
③通过派生类的对象不能访问基类中的任何成员。 - 总结
(1)私有成员继承下来了,在派生类中存在但不能访问。基类的私有成员无论以哪一种继承方式,在派生类中都是不可访问的。其本质可解释为作用域变了,基类和派生类是两个不同的作用域,而私有成员在类外是不能够被访问的。
(2)在类不参与继承时,private和protected成员的属性都可以被认为是私有的。但当类参与继承时,基类的private成员不论哪种继承,在派生类中是不能被访问的,在类外更不可能通过派生类对象访问;基类的protected成员不论哪种继承,在派生类中都可以被访问,但在类外不能通过派生类对象访问。可以看出保护成员限定符是因继承才出现的,这就是private与protected的不同。这样,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
(3)使用关键字class时默认的继承方式是private,使用关键字struct时默认的继承方式是public,不过最好显示的写出继承方式。
(4)同名隐藏。当基类的成员或成员函数和派生类的成员或成员函数同名,通过派生类的对象访问成员或成员函数时,优先访问派生类自己的。此时若想访问基类的,只需在前面加上作用域限定符。
例:
Derived d;
d._pub = 2;//访问派生类自己的
d.Base::_pub = 1;//访问基类的
!!要注意的是。当派生类的成员函数与基类的成员函数同名时,此时并不构成重载,因为不在同一作用域。通过派生类的对象访问成员函数时,优先访问派生类自己的。而不是按照有无参数,有无返回值去找适合它的那个成员函数。
例:
class Base
{
public:
Fun(int x);
int a;
}
class Derived:public Base
{
public:
Fun();
int b;
}
int main()
{
Derived d;
d.Fun(25);
//此处出错,因为优先找派生类的Fun()发现不合适,报错,它只会优先 调用派生类自己的,而不是调适合自己的
return 0;
}
第二步· 继承相关要点
1.对象模型
指的是对象中各个成员变量的分布格式。
以简单的派生类对象模型为例,见如下代码。
class Base
{
public:
int _b;
};
class Derived:public Base
{
public:
int _d;
};
int main()
{
Derived d;
d._b = 10;
d._d = 11;
return 0;
}
其他的对象模型我们在后面还会提到。
2.派生类中构造函数/析构函数的调用
先给出结论。
派生类中构造函数的调用顺序:
派生类构造函数——>基类构造函数
派生类中构造函数的执行顺序:
基类构造函数——>派生类构造函数
派生类中析构函数的调用顺序:
派生类析构函数——>基类析构函数
如下代码为例:
class Base1
{
public:
Base1(int data):_data(data)
{ cout<<"Base1()"<<endl; }
~ Base1( ){ cout<<"~Base1()"<<endl; }
protected:
int _data;
};
class Base2
{
public:
Base2(int data):_data2(data)
{ cout<<"Base2()"<<endl; }
~ Base2( ){ cout<<"~Base2()"<<endl; }
protected:
int _data2;
} ;
class Derive:public Base1,public Base2
{
public:
Derive():Base1(0),Base2(1),_d(3)
{ cout<<"Derive()"<<endl; }
~ Derive( ){ cout<<"~Derive()"<<endl ; }
protected:
int _d;
};
int main()
{
Derive d;
return 0;
}
通过结果,验证了我们之前的结论。
说明:
之所以派生类中构造函数的调用顺序和执行顺序不同。是因为在调用派生类构造函数时,在派生类初始化列表的位置要先调用基类构造函数,完成对基类构造函数的初始化,完成后再接着执行派生类构造函数。所以它是先调用派生类,后调用基类;但却是先执行基类的,再执行派生类的。
派生类中含有子对象时构造函数/析构函数的调用
先给出结论。
派生类中含有子对象时构造函数的执行顺序:
基类构造函数——>派生类子对象构造函数——>派生类构造函数
派生类中含有子对象时析构函数的调用顺序:
派生类析构函数 ——>派生类子对象析构函数——>基类析构函数
用以下代码来验证:
class Base1
{
public:
Base1(int data):_data(data)
{ cout<<"Base1()"<<endl; }
~ Base1( ){ cout<<"~Base1()"<<endl; }
protected:
int _data;
};
class Derive:public Base1
{
public:
Derive():Base1(0),b1(1)
{ cout<<"Derive()"<<endl; }
~ Derive( ){ cout<<"~Derive()"<<endl ; }
protected:
Base1 b1;
};
int main()
{
Derive d;
return 0;
}
通过结果,验证了我们之前的结论。
知识点总结:
①派生类初始化列表中的顺序是按照派生类对象模型中的顺序进行的。(自上而下)
②构造函数执行顺序在对象模型中是自上而下;析构函数调用顺序在对象模型中是自下而上。
③基类和派生类是两个不同的作用域。
④派生类不能继承基类的构造函数和析构函数。
⑤基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数。
说明⑤:
当基类没有构造函数,则派生类也可以不用定义,全部由系统默认提供,系统提供的是缺省构造函数。就是以下这种。
Derived( )
:Base( )
{ }
⑥基类定义了带参构造函数,派生类就一定要自定义构造函数。
说明⑥:
但当基类有带参构造函数时,派生类就一定得自定义构造函数。如此时派生类不自定义构造函数,系统就会提供一个默认构造函数,如上。
此时系统所提供的构造函数在初始化列表中调用的是Base( ),而不是调用基类的带参构造函数Base(int x)。因为系统还无法智能到调用构造函数时,还给出参数值。若基类此时没有Base( ),就会出错,提示Base( )找不到。
⑦基类没有缺省构造函数(无参和全缺省的构造函数都没有,则派生类必须要在初始化列表中显式给出基类名和参数列表。
由上可知,当基类有缺省构造函数。派生类没有构造函数时,系统就会为派生类合成一个默认的构造函数。
3.赋值兼容规则
①子类对象可以赋值给父类对象
②父类对象不能赋值给子类对象
③父类的指针或引用可以指向子类的对象
④子类的指针或引用可以指向父类的对象
⑤如果函数的形参是基类的对象或引用,在调用函数时可以用派生类对象作为实参
4.友元、静态成员和继承
(1)友元不是类的成员函数,故友元函数是不可以被继承的。
(2)静态成员是类的属性,所以是可继承的。但无论被继承几次,这些继承下来的静态成员都只有一个实体。
第三步· 单继承&多继承&菱形继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
单继承比较简单,对象模型之前已经分析了,这里不再多说。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
多继承里最经典的就是菱形继承了。我们来看一下。
class B
{
public:
int _b;
};
class C1:public B
{
public:
int _c1;
};
class C2:public B
{
public:
int _c2;
};
class D:public C1,public C2
{
public:
int _d;
};
int main()
{
D d;
d._b = 1;//错误,error C2385: 对“_b”的访问不明确
return 0;
}
出现对“_b”的访问不明确的原因是因为,类C1和C2中都继承了来自B的_b,而D又继承于C1和C2,所以通过D的对象去访问_b时,不知道
访问的是谁的_b,出现了二义性问题。其中,D的对象模型如下:
为了解决多继承中的二义性问题,我们引入虚拟继承。
利用虚拟继承以后的代码如下:
class B
{
public:
int _b;
};
class C1:virtual public B
{
public:
int _c1;
};
class C2:virtual public B
{
public:
int _c2;
};
class D:public C1,public C2
{
public:
int _d;
};
int main()
{
D d;
d._b = 1;//解决了二义性问题
d._c1 = 2;
d._c2 = 3;
d._d = 4;
cout<<sizeof(d)<<endl;//大小等于24,分析为什么?
return 0;
}
从内存上看一下,果然是24字节。
这样我们可以得到D的对象模型,可以发现虚拟继承时,D的对象模型发生了改变。
说明:
(1)C1类和C2类都继承了来自B类的成员_b,但C1和C2要访问_b时,只能通过自己的偏移表指针找到偏移表,拿到相对于_b的偏移量,才能访问到_b,所以C1和C2的大小是12字节。
(2)_b在D类中最终只保留了一份。D类可以直接访问_b.
总结:
①虚拟继承时派生类对象模型如下
②把偏移表指针放在派生类对象模型的前4个字节
③在编译时,偏移表就已形成,在调用构造函数时把偏移表指针放在派生类对象模型的前4个字节。
④编译器为了区分是普通继承还是虚拟继承,在虚拟继承时先将1压栈,在普通继承时先将0压栈。
⑤一定要注意virtual关键字放的位置,放的位置不同,表示的含义不同。
示例代码:
class B
{
public:
int _b;
};
class C1: public B
{
public:
int _c1;
};
class C2: public B
{
public:
int _c2;
};
class D:virtual public C1,virtual public C2
{
public:
int _d;
};
int main()
{
D d;
//d._b = 1;//错误,error C2385: 对“_b”的访问不明确,没有解决二义性问题
d._c1 = 1;
d.C1::_b = 2;
d._c2 = 3;
d.C2::_b = 4;
d._d = 5;
cout<<sizeof(d)<<endl;//大小为24,分析为什么?
return 0;
}
查看内存分布如下:
所以我们可以得到D的对象模型如下:
这样一来,随着virtual位置的不同,表达的含义就变了,所以一定要注意。
以上就是我关于继承部分的归纳总结,若有什么错误,一定要告诉我~~~~