目录
一、继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
1.2 继承定义
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
1.3继承方式&访问限定符
继承方式 | public继承 |
protected继承 | |
private继承 |
访问限定符 | public访问 |
protected访问 | |
private访问 |
1.4继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的protected 成员 | 派生类的protected 成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的private成 员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
1.基类的private成员不管什么方式继承都在派生类中不可见。(虽然不可见,但是基类的私有成员还是被继承到派生类对象中,只不过语法上限制派生类对象在类里和类外都不能被访问)
2.基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
3.基类除private成员外的其他成员在子类的访问方式 = 访问限定符和继承方式中较小的(public > protected > private)。
4.使用class关键字时默认继承方式是private,使用struct时默认继承方式时public。最好写出继承方式。
5.实际中一般都用public继承,几乎很少使用protected和private继承。
二、基类和派生类赋值转换
(1)派生类对象可以赋值给 基类对象/基类的指针/基类的引用。 形象的说法叫做切片或者切割。赋值的过程不产生类型转换。
赋值后的对象/指针/引用 只能指向基类内的成员。
(2)基类对象不能赋值给派生类对象。
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_No = 10;
}
三、继承中的作用域
(1)在继承体系中基类和派生类都有独立的作用域。
(2)子类和父类中有同名成员时(只要同名就可以),子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏,也叫重定义。(在子类成员函数中,可以使用 基类 : : 基类成员 显示访问。即便同名函数参数不同,依然构成隐藏)。
(3)需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
(4)在实际中在继承体系里面最好不要定义同名的成员。
四、派生类的默认成员函数
4.1普通类的默认成员函数
(1)构造函数和析构函数:
对内置类型不处理;对自定义类型去调用自定义类型的构造和析构函数。
(2)拷贝构造和重载函数:
对内置类型完成浅拷贝/值拷贝;对自定义类型去调用自定义类型的拷贝构造和重载函数。
(3)由于取地址重载和 const 取地址重载:
这两个默认成员函数我们一般使用编译器自动生成的即可,所以在这里我们不考虑它们。
4.2派生类的默认成员函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
总结
1.父类中的成员要交由父类成员函数处理,子类中的成员要交由子类成员处理。
2.子类的构造函数必须先调用父类的构造函数来构造父类的成员。如果父类没有默认的构造函数则需要手动显示调用。
3.子类的析构函数不需要显式调用父类的析构函数,因为出了子类析构函数的作用域,编译器会自动调用父类的析构。手动调用父类析构将会造成重复析构。
4.派生类的operator=和拷贝构造也必须调用父类成员函数来完成。
4.3用法
(1)父类没有默认构造函数,需要在子类的构造函数里补充
class Person
{
public:
Person(const char* name)
: _name(name)
{}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name = "张三", int num = 10)
: Person(name)//必须调用父类的构造函数进行初始化
, _num(num)
{}
protected:
int _num; //学号
};
父类有提供默认构造函数就可以不用在子类写了。
(2)在子类中显式写拷贝构造
class Person
{
public:
Person(const Person& p)//形参:引用切片对象
: _name(p._name)
{}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const Student& s)
:Person(s)//切片
,_num(s._num)
{}
protected:
int _num; //学号
};
利用切片传入父类对象构造父类。Student s1(s2),在初始化列表中,利用s2中的父类成员去拷贝构造s1中的父类成员。
(3)在子类中显式写赋值运算符重载
class Person
{
public:
Person& operator=(const Person& p)
{
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student& operator=(const Student& s)
{
if (this != &s)//防止自己给自己赋值
{
Person::operator=(s);//切片传入父类赋值运算符重载中
//根据子类成员进行深浅拷贝
_num = s._num;
}
return *this;
}
protected:
int _num; //学号
};
在子类赋值运算符重载中调用父类赋值运算符重载,通过切片,完成父类成员的赋值。
(4)不需要显式调用析构函数
~Student()
{
Person::~Person();
}
//子类析构函数结束后会调用一次父类的析构函数
~Person前必须加类域Person。因为析构函数的名字会被编译器统一处理为destructor(),子类的析构函数和父类的析构函数之间构成隐藏,所以这里需要写明类域。
但是,我们并不需要显式调用父类的析构函数,因为出了子类析构函数的作用域,编译器会自动调用父类的析构。手动调用父类析构将会造成重复析构。
五、继承和友元、静态成员的关系
1、友元关系不能被继承
2、父类中的静态成员也会被继承,但是整个继承关系中共用这个静态成员
六、菱形继承和菱形虚拟继承
6.1单继承、多继承和菱形继承及其二义性
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
6.2虚拟继承解决数据冗余和二义性
如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
6.3虚拟继承的原理
菱形继承图示:
菱形虚拟继承图示:
这里是通过了B和C的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A。
class A {
public:
int _a;
};
class B : public A {
public:
int _b;
};
class C : public A {
public:
int _c;
};
class D : public B, public C {
public:
int _d;
};
实际使用时,尽量不要使用用菱形继承,因为它本质就是C++设计的一个坑!
七、继承和组合的区别
组合也是一种类复用的手段。
1、组合的使用场景
适用组合的代码:轮胎和车的关系
class Tire
{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 18; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "xxxxx"; // 车牌号
Tire _t; // 轮胎
};
2、继承和组合的区别
(1)public继承是一种is-a的关系,每个子类对象都是一个父类对象,例如“学生”是“人”(子类学生,父类是人)。
(2)组合是一种has-a的关系,B组合了A,每个B对象中都有一个A,例如“车”包含“轮胎”
(3)如果两个类既可以是is-a,又可以是has-a的关系,那么优先使用组合。
继承是一种白盒复用,父类内部的细节对子类可见,破坏了封装。子类将会继承父类的公有和保护成员,一旦父类修改了这些成员的实现,将会影响子类的功能。子类和父类之间的依赖关系强,耦合度高。
组合是一种黑盒复用,父类内部的细节对子类不可见,子类仅可使用父类的公有成员,只要父类的公有成员的实现细节不变,子类影响较小。父子之间没有很强的依赖关系,耦合度较低。