目录
1.继承的概念和定义
(1).概念
(2).定义
(3).继承父类成员访问方式的变化
2.父类和子类对象赋值转换(公有继承)
3.继承中的作用域
4.子类的默认成员函数
(1).子类成员函数规则
(2).子类默认生成的成员函数相关
*在子类中显示调用父类的六个特殊成员函数
Q:为什么这里多调用一次析构函数不会报错?
Q:如何设计一个不能被继承的类
注意事项:
7.菱形继承及菱形虚拟继承
Q:虚拟继承解决数据冗余和二义性的原理
Q:为什么要弄一个偏移量呢?直接在D里面找到A不就行了吗?
继承(inheritance)是面向对象中使代码可以复用的最重要的手段,继承让我们在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,叫做子类。继承的实现过程是一个从一般到特殊的过程,体现出了面向对象设计的层次结构。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用(+复用+=),继承是类设计层次的复用(student和teacher都有名称,将名称定义在Person类里面,让student和teacher继承person)。
如下图所示,Teacher是子类,public是继承方式,Person是父类
如图所示
继承方式和访问限定符
类成员/继承方式 | public继承 | protected继承 | private继承 |
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类的private成员 | 在子类中不可见 | 在子类中不可见 | 在子类中不可见 |
总结:
1. 父类private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它。
2. 父类private成员在子类中是不能被访问,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为protected(类内可以访问,类外面不能访问)。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见。父类的其他成员在子类的访问方式 == Min(成员在父类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在子类的类里
面使用,实际中扩展维护性不强。
6.父类成员基本上都是保护和公有(如果父类要被继承,父类成员变量可以尽量用保护)
然后继承方式基本都是公有继承
注意:这里的赋值转换必须是公有继承
比如子类对象给父类对象,就像把子类切开,然后把子类中和父类相同的成员赋值过去。
int main() {
Person p;
Teacher t;
p = t;
Person& pr = s;
Person* pt = &s;
return 0;
}
这里Teacher继承Person
注意:只要公有继承才能进行赋值转换,父类对象,引用,指针不能修改子类中新加的变量
1. 在继承体系中父类和子类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问(和局部优先类似,先访问子类,再访问父类),这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 父类::父类成员 显示访问,静态成员变量也是通过域作用限定符来访问的)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏(不会重载,因为没有在同一个作用域里面),调用函数只能找到子类的,如果子类中的函数有参数,则调用的时候必须传入参数。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
1. 子类的构造函数必须调用父类的构造函数初始化继承下来的父类成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
2.拷贝构造,赋值运算符同理,都需要先调用父类的拷贝构造和赋值运算符对继承的父类成员进行初始化
3.析构函数需要先调用子类的析构函数,再调用父类的析构函数(和之前的相反)
1.编译器会默认生成六个成员函数,默认生成的构造函数,拷贝构造函数,赋值运算符重载函数,都会默认先调用父类对应的函数对继承下来的父类成员变量进行初始化。
2.默认生成的析构函数,调用完子类的析构函数后,会调用父类的析构函数
3.编译器默认生成的六个成员函数对子类自己成员变量的处理规则和以前都是类似的。(对内置类型不做处理。对自定义类型调用自定义类型中对应的默认成员函数)
1).显示调用构造函数
1.如果自己写了一个构造函数,就算没有显示调用父类的构造函数,在初始化列表也会自动调用父类的默认构造函数,如果父类没有默认的构造函数,必须在初始化列表显示调用(拷贝构造同理)。
2.初始化列表里面怎么顺序写都行(因为初始化列表里面的顺序并不是初始化的顺序,是和声明(不是定义,类里面的都是声明,类外定义对象的时候变量才真正被定义)的顺序有关的,父类默认会在最前面)
如下所示,在初始化列表中,Person的位置没有影响,最后执行的时候都会在最前面执行
class Teacher : public Person
{
public:
Teacher(const char* name = "", int num = 0)
:_num(num)
, Person(name)
{
cout << "Teacher(const char* name = "", int num = 0)" << endl;
}
};
class Teacher : public Person
{
public:
Teacher(const char* name = "", int num = 0)
::Person(name)
,_num(num)
{
cout << "Teacher(const char* name = "", int num = 0)" << endl;
}
};
2).如何在子类的拷贝构造中显示调用父类的拷贝构造对继承下来的父类成员进行初始化?(默认自己也会调用,涉及深拷贝的时候要自己写)
直接在初始化列表里面调用父类的构造函数即可,把Student对象传给父类Person的引用(这里实际上就是切割或者切片)
Person类里面的拷贝构造函数:
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Student类里面拷贝构造函数显示调用Person的拷贝构造函数完成对父类对象部分的初始化
Teacher(const Teacher& s)
:Person(s)
, _num(s._num)
{
cout << "Teacher(const Teacher& s)" << endl;
}
3).如何在子类的赋值运算符重载中显示调用父类的赋值运算符重载对继承下来的父类成员进行初始化?(涉及深拷贝的时候要自己写)
直接调用父类的operator=()即可,注意这里需要用指定一下作用域是父类的才行,不然会优先调子类的,会循环调用子类的operator=导致栈溢出
如下是错误代码,调用的时候没有指定父类的operator作用域,会循环调用子类的operator
Teacher& operator=(const Teacher& s)
{
if (this != &s)
{
operator=(s);
_num = s._num;
}
cout << "Teacher& operator=(const Teacher& s)" << endl;
return *this;
}
需要指定父类的作用域,下面是正确代码,可以看看有什么不同
Teacher& operator=(const Teacher& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
cout << "Teacher& operator=(const Teacher& s)" << endl;
return *this;
}
4).如何在子类的析构函数中显示调用父类的析构函数?
直接调用会报错,因为父子类的析构函数构成隐藏关系--原因:下一节多态的需要,析构函数名统一会被处理成destructor(),给上作用域就不会报错,但是会多调用一次,因为子类析构函数结束后编译器会为我们自动调用一次父类的析构函数。
为了保证析构顺序,先子后父,
子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用
A:因为这里析构函数内部并没有对资源构成实质的清理,如果我们在里面进行free/delete操作对空间进行释放了,那就会报错。真正会导致报错的是对同一个空间释放两次这样的操作。
A:将父类的构造函数私有化,因为子类的构造函数必须调用父类的构造函数。而父类中私有的成员子类中无法直接访问,固无法继承,定义子类对象的时候就会报错。
由于构造函数变成私有,所以我们需要重新写一个静态函数返回构造函数生成的对象,这样父类也就可以实例化了。
class A
{
public:
static A CreateObj()
{
return A();
}
private:
A() {}
}
1.友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员
2.父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
多继承导致可能会有菱形继承,菱形继承会导致数据冗余和二义性
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
如下所示(省略了部分细节,主要看一下virtual加的具体位置)
class Person
...
class Student : virtual public Person
...
class Teacher : virtual public Person
...
class Assistant : public Student, public Teacher
...
例子:A里面有int a,B里面有int b.....
然后B,C继承A,D继承B,C
class A
{
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
菱形继承内存情况:(实际上D自己的数据占的空间只有一小个,有两个A中的冗余数据)
菱形虚拟继承的内存情况:
A在D中只存放了一份,B和C中本来应该存放A的地方存放了一个虚基表指针,指针指向的区域存放的是当前B和C的存储位置距离A存储位置的偏移量(一个存放偏移量的地址)。B存储位置的首地址+20就是A存储位置的地址。
可以看到B中存放的偏移量是14(通过地址去找),B的地址是0x005BF7C4,A的地址0x005BF7D4,相差的正好就是16进制的14,也就是B中存放的偏移量
如果现在定义了一个B对象,和一个D对象,此时有两个指针p1和p2,分别指向B和D, 想要获取A中的a。此时两个对象的内容是不相同的,而p1,p2两个指针指向的都是对象的起始位置,此时A的位置离p1,p2所指的位置都是不一样的,但是虚基表的位置是一样的,p1,p2不需要关心自己指向的是哪个对象,只需要按照相同的逻辑去虚基表中找到存储A位置偏移量,然后找到A就可以。
通过菱形虚拟继承虽然完美的解决了二义性,但是这个虚基表的模型影响了存取数组的效率,因为多多少少多了一些计算和访问。