1:继承的概念及定义
1.1继承的概念
继承机制面向对象程序设计使代码可以复用的最重要手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类,继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
#include<iostream>
#include<string>
using namespace std;
class person {
public:
void print() {
cout << _name << endl;
cout << _age << endl;
}
private:
string _name = "李四";
int _age = 18;
};
class student :public person {
private:
int _stuid;
};
class Teacher :public person {
private:
int _jobid;
};
int main() {
student s;
Teacher t;
cout << sizeof(person) << endl;//32
cout << sizeof(s) << endl;//36
cout << sizeof(t) << endl;//36
s.print();
t.print();
return 0;
}
1.2继承的定义
继承方式和访问限定符
继承关系:public继承 protected继承 private继承
访问限定符:public访问 protected访问 private访问
继承基类成员访问方式的变化
总结:
基类private成员在派生类中无论以什么方式继承都是不可见的,这里的不可见其实是基类中的私有成员还是被继承呆了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
基类private成员在派生类中不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中被访问,就定义为protected,可以看出保护成员限定符是因继承才出现的。
使用关键字class默认的 继承方式是private,使用struct默认的继承方式是public,不过最好显式的写出继承方式
2.赋值兼容规则
2.1 子类对象可以直接赋值给父类对象
#include<iostream>
using namespace std;
class Parent
{
private:
int a;
public:
};
class Child :public Parent
{
private:
int b;
public:
};
int main()
{
Child c;
Parent p;
p = c; //子类对象可以直接赋值给父类对象
return 0;
编译会报错,但出错点在于Child类对象c未初始化。而在语法上,子类对象给父类对象直接赋值是可行的。
本质是子类中包含了父类全部的内容,一个子类对象中肯定含有能构成一个父类对象的元素,因此可以直接赋值。但是子类中有可能会有父类中没有的元素,因此父类对象不能对子类对象直接赋值。
2.2 子类对象可以当做父类对象使用
#include<iostream>
using namespace std;
class Parent
{
private:
int a;
public:
void PrintP()
{
cout << "a = " << a << endl;
}
};
class Child :public Parent
{
private:
int b;
public:
};
int main()
{
Child c;
c.PrintP();//c能当做父类p的对象调用父类的函数
return 0;
}
本质上是子类完全继承了父类的成员函数,可以通过子类去调用。
2.3 子类对象可以直接初始化父类对象
#include<iostream>
using namespace std;
class Parent
{
private:
int a;
public:
};
class Child :public Parent
{
private:
int b;
public:
};
int main()
{
Child c;
Parent p = c;//子类对象直接对父类对象初始化
return 0;
}
同第一点原理一样,子类对象可以给父类赋值,自然也可以初始化父类对象。
2.4 父类指针可以直接指向子类对象
#include<iostream>
using namespace std;
class Parent
{
private:
int a;
public:
};
class Child :public Parent
{
private:
int b;
public:
};
int main()
{
Parent p;//定义父类对象
Child c;//定义子类对象
Parent *pp = NULL;//定义父类指针
Child *CP = NULL;//定义子类指针
pp = &c;//用父类指针指向子类对象
return 0;
}
子类中必定包含了父类的全部成员,因此可以用父类指针指向子类对象(注意:该指针只能指向子类对象中的继承了父类的成员,无法指向子类特有成员)。但在子类中可能会有父类没有的成员,用子类类型创建的子类指针中会含有父类不存在的成员,因此不能用子类指针指向父类对象。
2.5 父类引用可以直接引用子类对象
与第四点原理一致
3.继承中的作用域
在继承体系中基类和派生类都有独立的作用域
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义
如果是成员函数的隐藏,只需函数名相同就构成隐藏
4.派生类的默认成员函数
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝构造
派生类的operator=必须调用基类的operator=完成基类的复制
派生类的析构函数会在调用完成后自动调用基类的析构函数清理基类成员,因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
派生类对象初始化先调用基类构造再调用派生类构造
派生类对象析构先调用派生类析构在调用基类析构
class B {
public:
B(int b)
:_b(b)
{}
B(const B& b)
:_b(b._b)
{}
B& operator=(const B& b) {
if (this != &b) {
_b = b._b;
}
return *this;
}
~B() {
cout << "B::~B()" << endl;
}
protected:
int _b;
};
class D :public B {
public:
D(int b,int d)
:B(b)
,_d(d)
{}
D(const D& d)
:B(d._b)
,_d(d._d)
{}
D& operator=(const D& d) {
if (this != &d) {
B::operator=(d);
_d = d._d;
}
return *this;
}
~D() {
cout << "D::~D()" << endl;
}
protected:
int _d;
};
int main() {
D d(10,20);
return 0;
}
打印次序:基类构造 派生类构造 派生类析构 基类析构
调用次序:创建哪个类,调用那个类的构造 析构那个类,调用哪个类的析构
4.2 实现一个不能被继承的类
c++11中:使用关键字final
class A final{
public:
A(int a)
:_a(a)
{}
protected:
int _a;
};
class B :public A {};
int main() {
B b;
return 0;
}
5.继承与友元
class B {
friend void TestFunc();
public:
protected:
int _b;
};
class D : public B {
protected:
int _d;
};
void TestFunc() {
//该函数为基类的友元
//可以在该函数中访问基类保护或者私有的成员
B b;
b._b = 10;
//不能访问子类中私有或保护的成员
D d;
d._d = 20;
//结论:友元关系不能被继承
//继承:子类一定继承的是基类的成员,友元不属于基类的成员
}
int main() {
return 0;
}
6:继承和静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例
class B {
public:
void Test() {
cout << "B:" << &_count << endl;
}
static int _count;
};
int B::_count = 0;
class D1 :public B {
public:
void TestFunc() {
cout << "D1:" << &_count << endl;
_count = 10;
}
};
class D2 :public B {
public:
void TestFunc() {
cout << "D2:" << &_count << endl;
_count = 10;
}
};
int main() {
D1 d1;
d1.TestFunc();
D2 d2;
d2.TestFunc();
return 0;
}
7:菱形继承和菱形虚拟继承
单继承:一个子类只有一个基类
多继承:一个子类可以有两个或两个以上的基类
菱形继承的问题:有数据冗余和二义性的问题
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;
cout << sizeof(d) << endl;
//可以解决二义性问题,但数据冗余问题无法解决
d.C1::_b = 10;
d._c1 = 20;
d.C2::_b = 30;
d._c2 = 40;
d._d = 50;
return 0;
}
菱形虚拟继承可以解决数据冗余的问题
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 = 10;
d._c1 = 20;
d._c2 = 30;
d._d = 40;
cout << sizeof(d) << endl;
return 0;
}
什么是菱形继承
两个子类继承同一个父类,而又有子类同时继承者两个子类
菱形继承问题
会产生数据冗余和二义性问题
什么是菱形虚拟继承
在子类继承父类时,使用virtual
菱形虚拟继承如何解决二义性和数据冗余问题
将classC1和classC2设置为虚函数,编译器将classA存放在最下端,并在C1和C2类的前四个字节中存放一个地址,当我们继续向下在多看四个字节时,就会发现其中存放了一个数字,这个数字类似于偏移量,记录了该类的首地址与父类首地址的字节差距,classC1中,我们找到的对应数字为20,在classC1的首地址加上20个字节就是classA的首地址,class C2中,数字为12