各位看官,大家好!今天我们将探讨C++中的三大特性之一:继承。继承是一种面向对象编程的重要概念,它允许我们通过创建新的类,从而复用和扩展现有类的功能。通过继承,我们不仅能够提高代码的可重用性和可维护性,还能更好地体现现实世界中事物的层次结构。希望大家通过今天的学习,能够深入理解继承的核心原理,并能在实际编程中灵活应用这一强大的工具。
目录
一、继承的概念及定义
1.1继承的概念
1.2 继承定义
1.2.1定义格式
1.2.2继承关系和访问限定符
1.2.3继承基类成员访问方式的变化
二、基类和派生类对象赋值转换
2.1 子类可以赋值给父类
2.2 父类不可以赋值给子类
2.3 父类赋值给子类的特殊情况
三、继承中的作用域
3.1 隐藏的概念
3.2 如何解决呢?
3.3 注意事项
四、派生类的默认成员函数
4.1 构造函数
4.2 拷贝构造函数
4.3 赋值重载
4.4 析构函数
4.5 总结
4.6 练习
五、继承与友元
六、继承与静态成员
七、复杂的菱形继承及菱形虚拟继承
7.1 继承的分类及概念
7.2 菱形继承存在的问题
7.3 虚拟继承
7.4 虚拟继承解决数据冗余和二义性的原理
八、继承的总结和反思
8.1理解
8.2. 继承和组合
九、笔试面试题
9.1 C++的缺陷是什么
9.2 什么是菱形继承?菱形继承的问题是什么?
一、继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
class Person
{public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};class Student : public Person //学生类继承自Person类
{protected:int _stuid; // 学号 (新增的属于自己类成员变量)
};class Teacher : public Person //老师类继承自Person类
{protected:int _jobid; // 工号 (新增的属于自己类成员变量)
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
继承带来的作用:
子类/派生类会具有父类/基类的成员变量和成员函数,当然也可以有属于自己类的成员变量和函数。
1.2 继承定义
1.2.1定义格式
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类
1.2.2继承关系和访问限定符
1.2.3继承基类成员访问方式的变化
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中(内存空间会有这个成员变量),但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。protected访问限定符和private访问限定符在当前类中没有区别,他们是一样的,类外都不能访问,区别在于继承的派生类,private成员无论什么继承方式,在派生类中都不能访问,但是protected就不一样了!
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class声明的类(指的是派生类)时默认的继承方式(派生类不写继承方式)是private,使用struct声明的类(指的是派生类)默认的继承方式(派生类不写继承方式)是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
// 实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{public :void Print (){cout<<_name <<endl;}protected :string _name ; // 姓名private :int _age ; // 年龄
};//class Student : protected Person
//class Student : private Person
class Student : public Person
{protected :int _stunum ; // 学号
};
二、基类和派生类对象赋值转换
2.1 子类可以赋值给父类
1、派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
2.2 父类不可以赋值给子类
2、基类对象不能赋值给派生类对象。
2.3 父类赋值给子类的特殊情况
3、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后 面再讲解,这里先了解一下)
class Person
{protected :string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{public :int _No ; // 学号
};void Test ()
{Person p;Student s ;子类和父类之间的赋值兼容规则:// 1.子类对象可以赋值给父类的对象/的指针/的引用,叫做切片Person p = s ;Person* ptr = &s;Person& rp = s;//2.父类对象不能赋值给子类对象(父给子是不可以的!反过来是不可以的!)s = p; 坚决不可以!// 3.基类的指针可以通过强制类型转换赋值给派生类的指针ptr = &sStudent* ps1 = (Student*)ptr;//这种情况转换时可以的,因为这个父类的指针有时是指向子类对象的ps1->_No = 10;ptr = &p;Student* ps2 = (Student*)ptr; //这种情况转换时虽然可以,但是会存在越界访问的问
题ps2->_No = 10;
}
三、继承中的作用域
3.1 隐藏的概念
在继承体系中基类和派生类都有独立的作用域。当父类和子类同时有同名成员变量或者成员函数时,子类就会隐藏父类的同名成员变量或者成员函数。子类和父类中有同名成员,子类成员将屏蔽对父类同名成员(成员变量或者成员函数)的直接访问,这种情况叫隐藏, 也叫重定义。
3.2 如何解决呢?
(如何解决呢?在子类成员函数中,可以使用 基类::基类成员 显示访问)
3.3 注意事项
注意在实际中在继承体系里面最好不要定义同名的成员变量或者同名的成员函数。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{protected :string _name = "小李子"; // 姓名int _num = 111; // 身份证号
};class Student : public Person
{public:void Print(){cout<<" 姓名:"<<_name<< endl;cout<<" 身份证号:"<<Person::_num<< endl; //必须显示的指定基类才可使用,否则它用的是派生类自己的成员变量cout<<" 学号:"<<_num<<endl;}protected:int _num = 999; // 学号
};void Test()
{Student s1;s1.Print();
};
B中的fun和A中的fun不是构成重载,因为不是在同一作用域!!!
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。class A
{public:void fun(){cout << "func()" << endl;}
};class B : public A
{public:void fun(int i){A::fun();cout << "func(int i)->" <<i<<endl;}
};void Test()
{B b;b.fun(10);b.A::fun(); //必须要显示的指定基类,才可以调用父类的这个同名的隐藏函数
};
四、派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
4.1 构造函数
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :protected :int _num ; //学号
};int main()
{Student s;return 0;
}
基本原则:
派生类的初始化和析构会分别自动调用基类的构造函数初始化基类的那一部分成员和自动调用析构函数,然后还会调用自己的构造函数和析构函数。也就是说他把父类和基类分的很清楚!
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): _name(name) 这里编译器不允许这样初始化!不写这个,他会编译通过,因为他会自动调用父类的构造函数初始化, _num(num){cout<<"Student()" <<endl;}protected :int _num ; //学号
};int main()
{Student s("peter",1);return 0;
}
派生类继承父类,对于父类那一部分,调用父类的构造函数进行初始化! 不可以在初始化列表中以初始化自己的成员变量的方式进行初始化!如果我们不对父类的成员变量进行初始化,他会自动的调用父类的构造函数进行初始化,如果想要在派生类中显示的初始化这个父类的成员变量,就必须以父类的构造函数的方式显示初始化,如下所示:
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name) 显示调用父类构造函数初始化!, _num(num){cout<<"Student()" <<endl;}protected :int _num ; //学号
};int main()
{Student s("peter",1);return 0;
}
总结1:
派生类的构造函数包含两个部分:第一部分是父类继承的,不能自己去初始化父类继承的那一部分,必须要调用父类的构造函数进行初始化(或者你不调,他会去调用父类默认的那个构造函数:编译器默认生成的构造函数、全缺省的构造函数、无参的构造函数,进行初始化)。第二部分是派生类自己的成员变量和之前普通的类没有什么区别。
4.2 拷贝构造函数
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}protected :int _num ; //学号
};int main()
{Student s1("jack", 18);Student s2(s1); //拷贝构造}
派生类不实现自己的拷贝构造函数,编译器会自动生成一个拷贝构造函数,进行拷贝,对于父类的那一部分,他会自动的调用父类的拷贝构造函数。
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}Student(const Student& s)//: _name(s._name) 这里是不可以的!: Person(s) 这样做!把子类对象给父类的引用,切片!!!, _num(s ._num){cout<<"Student(const Student& s)" <<endl ;}protected :int _num ; //学号
};int main()
{Student s1("jack", 18);Student s2(s1); //拷贝构造}
总结2:
派生类的拷贝构造函数也同样包含两个部分:第一部分是父类继承的,不能自己去拷贝父类继承的那一部分,必须要调用父类的拷贝构造函数进行拷贝,第二部分是派生类自己的成员变量和之前普通的类没有什么区别,这一部分也会调用自己的拷贝构造函数进行拷贝。
4.3 赋值重载
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}Student(const Student& s): Person(s) , _num(s ._num){cout<<"Student(const Student& s)" <<endl ;}Student& operator = (const Student& s ){cout<<"Student& operator= (const Student& s)"<< endl;if (this != &s){operator =(s); 这样显示调用赋值重载(this->operator=(s);),这里也有切片_num = s ._num;cout<<"Student& operator= (const Student& s)"<< endl;}return *this ;} protected :int _num ; //学号
};int main()
{Student s1("jack", 18);Student s2(s1); //拷贝构造Student s3("rose", 20);s1 = s3;}
为什么会发生栈溢出??
因为派生类调用operator=与基类的operator=构成隐藏了(同名函数),子类和父类中有同名成员函数,那么子类成员函数将屏蔽对父类同名成员函数的直接访问!!!也就是说,没办法调用基类的operator=。解决办法:指定基类!
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person& operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}Student(const Student& s): Person(s) , _num(s ._num){cout<<"Student(const Student& s)" <<endl ;}Student& operator = (const Student& s ){if (this != &s){Person::operator =(s); 指定基类的,并显示的调用基类的赋值重载_num = s ._num;cout<<"Student& operator= (const Student& s)"<< endl;}return *this ;} protected :int _num ; //学号
};int main()
{Student s1("jack", 18);Student s2(s1); //拷贝构造Student s3("rose", 20);s1 = s3;}
4.4 析构函数
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person& operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){if (this != &s){Person::operator =(s); //指定基类的,并显示的调用基类的赋值重载_num = s._num;cout << "Student& operator= (const Student& s)" << endl;}return *this;}~Student() //子类的析构函数和父类的析构函数构成隐藏!!!因为他们的名字会被编译器统一处理成: destructor(跟多态相关){//~Person(); //不能这样直接调用基类的析构函数,因为它们构成隐藏,基类无法访问父类的析构函数,解决办法:指定基类Person::~Person();cout << "~Student()" << endl;}protected:int _num; //学号
};int main()
{Student s1("jack", 18);return 0;}
第一个Person析构应该去掉!基类的析构函数不需要我们显示的去调用,他会在派生类析构函数调用后,自动的去调用基类的析构函数!!修改如下:
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person& operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){if (this != &s){Person::operator =(s); //指定基类的,并显示的调用基类的赋值重载_num = s._num;cout << "Student& operator= (const Student& s)" << endl;}return *this;}~Student() //子类的析构函数和父类的析构函数构成隐藏!!!因为他们的名字会被编译器统一处理成: destructor(跟多态相关){//Person::~Person();cout << "~Student()" << endl;}protected:int _num; //学号
};int main()
{Student s1("jack", 18);return 0;}
4.5 总结
4.6 练习
请设计一个类,不能被继承
只需要将父类的构造函数的访问限定符设置成私有的,这样子类无论以什么方式继承,父类的构造函数在子类中都不可见,那么我们在创建子类对象时,它必须首先去调用父类的构造函数进行初始化,但是,发现父类的构造函数此时不可见,那他就不能调用了,那么子类对象就创建失败了!也就是说,这个父类不能被继承。
class A
{
private:A(){}
};class B:public A
{B(){}
};
五、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class Person
{public:friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}class Student : public Person
{protected:int _stuNum; // 学号
};int main()
{Person p;Student s;Display(p, s);return 0;
}
六、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。
class Person
{public:Person () {++ _count;}protected :string _name ; // 姓名public :static int _count; // 统计人的个数。
};int Person :: _count = 0;class Student : public Person
{protected :int _stuNum ; // 学号
};class Graduate : public Student
{protected :string _seminarCourse ; // 研究科目
};void main()
{Student s1 ;Student s2 ;Student s3 ;Graduate s4 ;cout <<" 人数 :"<< Person ::_count << endl;Student ::_count = 0;cout <<" 人数 :"<< Person ::_count << endl;
}
七、复杂的菱形继承及菱形虚拟继承
7.1 继承的分类及概念
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
7.2 菱形继承存在的问题
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在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";
}
7.3 虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在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";
}
7.4 虚拟继承解决数据冗余和二义性的原理
正常的菱形继承:
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;
};int main()
{D d;cout<<sizeof(d)<<endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余
虚拟菱形继承:
class A
{public:int _a;
};class B : virtual public A
{public:int _b;
};class C : virtual public A
{public:int _c;
};class D : public B, public C
{public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成 员的模型。
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下 面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A。
有童鞋会有疑问为什么D中B和C部分要去找属于自己的A?那么大家看看当下面的赋值发生时,d是
不是要去找出B/C成员中的A才能赋值过去?D d;
B b = d;
C c = d;
下面是上面的Person关系菱形虚拟继承的原理解释:
八、继承的总结和反思
8.1理解
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
8.2. 继承和组合
九、笔试面试题
9.1 C++的缺陷是什么
多继承就是C++的一个问题,多继承中的菱形继承存在数据冗余和二义性的问题,解决它的方法是虚拟继承,它的底层结构的对象模型非常复杂,且有一定的效率损失。
9.2 什么是菱形继承?菱形继承的问题是什么?
菱形继承(diamond inheritance)是C++中多重继承的一种特殊情况,其继承结构形成一个菱形,因此得名。这种继承方式通常涉及一个基类、两个从这个基类继承的中间类以及一个从这两个中间类继承的派生类。
菱形继承的问题
重复继承(重复基类): 当
D
继承自B
和C
时,由于B
和C
都继承自A
,导致D
将包含两份A
的成员,这会造成数据冗余和不一致性问题。二义性(Ambiguity): 如果在类
D
中调用基类A
的成员,例如函数或变量,由于D
包含两份A
的成员,编译器无法确定应该调用哪一份,会导致二义性错误。
至此,这一讲内容介绍完毕,内容简单,星光不问赶路人,加油吧,感谢阅读,如果对此专栏感兴趣,点赞加关注!