【C++】继承机制全解析
1.继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段。它允许一个类(派生类)继承另一个类(基类)的属性(成员变量)和方法(成员函数),同时可以在此基础上添加新的属性和方法,或重写基类的方法,从而实现代码复用和扩展。
class 派生类名 : 继承方式 基类名 {// 派生类新增的成员
};
2.继承的方式
C++通过继承方式控制基类成员在派生类中的访问权限,有三种继承方式,继承方式对应的访问权限如下表所示:
类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结一下,基类的私有成员不管什么继承方式在派生类中都是不可见;基类的其他成员在派生类的访问方式就是派生类的继承方式。在使用class关键字时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。在实际使用当中,一般使用public继承,几乎很少使用protetced/private继承,也不提倡使用,因为protetced/private继承下来的成员都只能在派生类的类中进行使用,实际中扩展维护性不强。
3.继承类模板
类模板是一种通用的类定义,它允许在创建类的时候使用类型参数。继承类模板指的是一个类模板从另一个类模板继承属性和方法,或者一个普通类继承类模板,以及类模板继承普通类等情况。这里就不详细展开讲解了,内容比较好理解,如果需要更深入的学习可在网上搜索。
4.基类和派生类间的转换
当派生类以public方式继承基类时,派生类对象可以赋值给基类指针或绑定到基类引用。这就是多态的基础!通过基类指针/引用统一操作派生类对象的“基类部分”,配合虚函数可实现“运行时多态”。
#include <iostream>
#include <string>
using namespace std;// 基类 Person
class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};// 派生类 Student,public 继承 Person
class Student : public Person
{
public:int _No; // 学号
};int main()
{Student sobj; // 1. 派生类对象可以赋值给基类的指针/引用Person* pp = &sobj;Person& rp = sobj;// 派生类对象可以赋值给基类的对象(通过基类的拷贝构造)Person pobj = sobj;// 2. 基类对象不能赋值给派生类对象,此处会编译报错// sobj = pobj; return 0;
}
上述代码就演示了:派生类对象向基类指针/引用的隐式转换;派生类对象向基类对象的拷贝构造(切片操作);基类对象无法直接赋值给派生类对象的限制。值得注意的是并非不能支持sobj=pobj的操作,如果需要可以为Student类自定义赋值运算符重载函数来实现显式转换逻辑。
5.继承的作用域
5.1继承机制的隐藏规则
在继承体系中基类和派生类都有独立的作用域;派生类和基类如果有同名函数,派生类成员将屏蔽对基类同名成员的直接访问,这种情况叫做隐藏。
class Base {
public:void func(int x) { cout << "Base::func(int)" << endl; }
};class Derived : public Base {
public:// 隐藏基类的 func(int),即使参数不同!void func(double x) { cout << "Derived::func(double)" << endl; }
};int main() {Derived d;d.func(10); // 调用 Derived::func(double)(int 被隐式转 double)// d.func(10) 本想调用基类的 func(int),但被派生类的 func(double) 隐藏了!// 若要调用基类函数,需显式:d.Base::func(10);return 0;
}
上面的这段代码说明了派生类只要声明同名函数,就会隐藏基类的所有同名函数,必须显式调用才能访问基类版本。
总结一下继承的“隐藏规则”:
1)派生类同名成员(变量 / 函数)会隐藏基类的同名成员,默认访问派生类自己的。
2)隐藏是作用域层级的覆盖,而非 “删除” 基类成员,需显式用(基类名::)访问被隐藏的基类成员。
3)实际开发中,应尽量避免同名成员,减少代码混淆风险;若无法避免,务必显式指定作用域。
6.派生类的默认成员函数
派生类默认成员函数的行为与基类的默认成员函数密切相关,其核心规则是派生类必须先初始化 / 清理基类部分,再处理自身成员。
6.1生成和调用规则
派生类的默认成员函数会自动调用基类对应的成员函数,以保证基类部分的正确初始化,拷贝或清理。
1)派生类构造函数:编译器生成的默认构造函数会先调用基类的默认构造函数(无参构造),在初始化派生类自己的成员变量(内置类型不初始化,自定义类型调用其默认构造)。注意:若基类没有默认构造函数(如用户定义了带参构造,编译器不在自动生成),派生类必须显式在初始化列表中调用基类的某个构造函数,否则会编译报错。
2)派生类析构函数:编译器生成的默认析构函数会先清理派生类自己的成员,再自动调用基类的析构函数(与构造函数顺序相反)。注意:基类析构函数建议声明为virtual(虚析构函数),确保删除派生类对象时,基类部分能被正确析构(避免内存泄露),虚函数在后面的多态会详细讲解。
3)派生类拷贝构造函数:编译器生成的默认拷贝构造函数会自动调用基类的拷贝构造函数(完成基类部分的拷贝),再拷贝派生类自己的成员变量。注意:若用户显式定义派生类拷贝构造,必须在初始化列表中显式调用基类的拷贝构造,否则会默认调用基类的默认构造函数(导致基类部分未正确拷贝)。
4)派生类赋值运算符重载:同理,会先调用基类的赋值运算符,再对派生类自己的成员变量赋值。跟拷贝构造同理,若用户显式定义派生类赋值运算符,必须在函数体内显式调用基类的赋值运算符,否则基类部分不会被赋值。
上面让注意必须显式调用的时候,其实原因就是我们上一个话题所说的继承的作用域隐藏问题。
7.继承注意
1)基类的构造函数私有,派生类构造必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就不能调用了,派生类就无法实例化对象。C++11新增一个fonal,final修改基类,派生类就能继承该基类了。
2)注意基类的友元关系不能被继承,也就是基类友元不能访问派生类私有和保护成员。
3)基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论现在派生出多少个派生类,都只有一个static实例。原因时静态成员在本质上属于类而非类对象,在继承中,派生类会继承基类的成员,对于静态成员也不例外,虽然派生类继承了基类的静态成员,但本质时共享基类在静态存储区中已经分配好的那个静态成员实例,而不是创建一个新的静态成员。
8.多继承问题
一个派生类有两个或以上继承基类时称为多继承。虽然能提高代码复用性,当随之而来会带来一些问题。
8.1会出现的问题
1)数据冗余:当类A是基类,类B和类C都继承自A,类D同时继承B和C,此时D会间接拥有两份A的成员(分别来自B和C),A的成员在D中被存储了两次,浪费内存。
2)访问二义性:场景就是数据冗余的那个场景,当通过D访问A的成员时,编译器无法确定来自B还是C的继承路径,导致编译错误。
3)成员名冲突:当多个基类中存在同名成员(变量或函数),即使没有菱形结构,也会导致访问二义性。(想要解决就显式指定基类)
为了解决菱形继承相关问题:C++引入了虚继承机制,让派生类间接继承同一基类时,只保留一份基类成员,原理是通过一个间接指针(虚表类表指针)指向唯一的基类实例,确保派生类(D)中只存在一份A的成员。例如:
class A { public: int x; };// B和C虚继承A(关键:在中间类添加virtual)
class B : virtual public A {};
class C : virtual public A {}; class D : public B, public C {}; // D继承B和C
D中只会保留一份A的成员x,访问d.x不再有歧义。
虚继承的底层原理实现是通过虚基类表与偏移量:虚继承的中间类(B和C)不再直接存储基类A的成员,将虚基类表存储在程序的只读数据段中,它是在编译期生成的静态只读数据,然后在对象中增加一个虚基类表指针指向虚基类表;虚基类表中存储了当前类对象到顶层基类(A)成员的偏移量,通过偏移量可找到唯一的A成员实例。
不过我们要注意虚继承仅用于解决菱形继承问题,非菱形结构的多继承无需使用,否则会增加内存开销(虚基类表指针)和代码复杂度。
我们要注意避免过多使用多继承,优先使用单继承+接口:通过接口(纯虚函数)实现多态,减少多继承带来的复杂度;用组合替代继承:当需要复用多个类的功能时,可在派生类中定义其他类的对象(组合),而非继承它们,更灵活且降低耦合。
最后总结一下多继承问题,多继承的核心问题就是二义性和结构复杂性,通过虚继承、显式指定基类等方式可缓解,但是更推荐在设计中优先考虑单继承和组合,减少多继承的使用场景。
指针自动偏移机制:
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
上述代码中三个指针的关系是啥?答案是p1==p3!=p2。三个指针明明都指向d,为什么不相同呢?这是因为Base2指针其实指向的是d对象的Base2子对象的起始位置,而Base1作为主基类,指针地址和派生类地址相同。这就是在多继承问题中指针自动偏移机制
本篇文章我们主要学习了C++中关于继承机制的知识,本文系作者本人根据自己学习笔记所作,如有错误,希望你能帮忙指出!万分感谢!希望我们共同进步!