目录
一、继承概念
二、继承的语法
2.1继承关系和访问限定符
2.2派生类继承基类后的成员权限
三、基类和派生类的对象赋值转换
四、继承中的作用域
五、派生类的默认成员函数
六、继承与友元
七、继承与静态成员
八、复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
虚拟继承解决数据冗余和二义性的原理
九、继承的总结
十、继承的一些面试题
1. 什么是菱形继承?菱形继承的问题是什么?
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
继承是面向对象程序设计使代码复用的重要手段,继承中有父类(基类)和子类(派生类);
继承中派生类可以使用父类的成员函数和成员变量;
class A // 基类、父类
{
public:
int _a;
};
class B : public A // 派生类、子类
{
private:
int _b;
};
下面我们看到的Student是派生类(子类),Person是基类(父类)
继承关系和访问限定符是类似的;
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类中的public成员 | 派生类中的public成员 | 派生类中的protected成员 | 派生类中的private成员 |
基类中的protected成员 | 派生类中的protected成员 | 派生类中的protected成员 | 派生类中的privete成员 |
基类中的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
虽然有这么多的继承方法组合,但是我们最常使用的只有public继承、public成员、protected成员组合的方式继承;
私有成员的意义?
不想被子类继承的成员,可以设计成私有;
保护成员的的意义?
基类中想给子类复用,但是又不想直接暴露访问成员,就该定义成保护;
注意:
struct默认继承/访问限定符 : public
class默认继承/访问限定符 :private
总结:
class Person
{
public:
Person(string name = "zs", int age = 0)
:_name(name)
,_age(age)
{
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
}
public:
string _name;
int _age;
};
class Student : public Person
{
public:
Student(int No = 213310)
:_No(No)
{
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _No << endl;
}
private:
int _No; //编号
};
int main()
{
Person p1;
Student s1;
s1.Print();
p1._name = "张三";//修改基类数值
p1._age = 18;
p1 = s1; // 将子类赋值给父类
p1.Print();
//结果两次打印的结果不变
return 0;
}
上面的运行结果子类已将将父类重新赋值;
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
class A
{
public:
void Print() { cout << "class A" << endl; }
public:
int _a = 1;
};
class B : public A
{
public:
void Print(){ cout << "class B" << endl; }
public:
int _b = 2;
};
class C : public A
{
public:
void Print() { A::Print(); }
public:
int _c = 2;
};
int main()
{
B b;
b.Print(); // 调用b中重定义的Print()
C c;
c.Print(); // 通过c显示调用A中的Print()
return 0;
}
上面的图我们可以从下面的几点去理解:
1.派生类的默认构造函数会调用基类的构造函数,如果基类没有构造函数,那么必须在派生类的的初始化列表阶段显示调用;
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的初始化;
3.派生类的operator=必须要调用基类的operator=完成基类的复制;
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造;
6. 派生类对象析构清理先调用派生类析构再调基类的析构;
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系;
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例;
class A
{
public:
void Print() { cout << "class A" << endl; }
public:
int _a = 1;
};
class B : public A // 只继承了A类
{
public:
void Print(){ cout << "class B" << endl; }
public:
int _b = 2;
};
class A
{
public:
void Print() { cout << "class A" << endl; }
public:
int _a = 1;
};
class B
{
public:
void Print() { cout << "class B" << endl; }
public:
int _b = 2;
};
class C : public A, public B // 继承了A类和B类
{
public:
void Print() { A::Print(); }
public:
int _c = 2;
};
class A
{
public:
void Print() { cout << "class A" << endl; }
public:
int _a = 1;
};
class B : public A
{
public:
void Print() { cout << "class B" << endl; }
public:
int _b = 2;
};
class C : public A
{
public:
void Print() { A::Print(); }
public:
int _c = 3;
};
class D : public B, public C
{
public:
void Print() { A::Print(); }
public:
int _d = 4;
};
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用。虚继承即在继承方式前加virtual关键字;
使用了virtual虚拟继承后,派生类会将基类的成员放到一个公共的区域,所有的派生类共享这个区域,会产生一个指针指向一个虚表,虚表中存放各个派生类相对于公共区域的偏移量,当派生类需要访问基类的成员时就需要处理一下偏移量,找到基类的成员,再进行访问;
public继承是一种is-a的关系,也就是说每一个派生类对象都是一个基类;
组合是一种has-a的关系,假设B组合了A,每一个对象中都有一个A对象;
菱形继承:多继承的一种特殊,派生类A和派生类B继承了一个基类,同时派生类C又多继承了A和B就构成了菱形继承
解决方法:使用virtual虚拟继承
菱形虚拟继承:在继承方式前面加vritual关键字
原理:加了virtual后,派生类将基类继承的成员放在一个公共的区域,同时多创建了一个指针指向一个虚表,虚表中存放该派生类相对于基类成员的偏移量,在派生类访问基类时再进行偏移量的处理
继承:is-a也就是说每一个派生类对象都是一个基类;
组合:has-a假设B组合了A,每一个对象中都有一个A对象;