资讯详情

深入解析C++继承体系:从访问控制到虚继承的全面指南

📅 2026/10/28 18:40:21 | 华诺云谱 👁 阅读
深入解析C++继承体系:从访问控制到虚继承的全面指南
目录一、什么是继承1.1 继承的概念和定义1.2 9种组合方式1.3 继承模板二、继承中基类和派生类的转换2.1 派生类转换为基类2.2 基类不能直接转换为派生类三、继承中的作用域3.1 隐藏规则3.2 隐藏和重载的区别四、继承中默认成员函数五、实现不能继承的类六、继承中多继承6.1 单继承和多继承6.2 菱形继承6.3 虚继承6.4 虚继承和菱形继承的区别总结七、继承和组合7.1 继承的使用7.2 组合的使用7.3 区别八、总结继承的作用一、什么是继承1.1 继承的概念和定义继承是面向对象编程的核心特性之一它允许一个类派生类/子类获取另一个类基类/父类的属性和行为。派生类能继承基类中的所有成员变量和成员函数但是友元关系不能继承就像你把的朋友不能继承他家财产这里就需要自己在继承内加入友元关系。这里需要友元两个类而编译器会往上找所以这里还需要声明另一个类继承z中需要调用函数还需要声明一些函数这里的函数是上面寻找不到的也就是在此类后面定义的类如果需要使用后面定义的类的函数就得声明使用别人类之前是友元。class Student;//由于这里友元了函数有Student这个类编译器向上寻找Student这个类定义没找到只能直接前置声明 class Person { //友元 friend void DisplayDate(const Person p, const Student s); public: private: string _name; string _address; string _tel; int _age; }; //Student是派生类,Person是基类这里public是继承方式 class Student: public Person { public: friend void DisplayDate(const Person p, const Student s); private: string _uid; }; void DisplayDate(const Person p, const Student s) { cout p._name endl; cout s._uid endl; cout s._name endl; }注意这里继承对于基类中的静态成员变量是不算继承的静态成员变量在整个继承体系中只有一份可以这么理解继承就是共享而不是复制基类的静态成员一定不包含在基类/子类中它是属于栈外的不属于某一个类。子类不一定比基类大可能相同。1.2 9种组合方式类成员/继承方式public继承protected继承private继承基类的public成员可以访问基类public成员不可访问不可访问基类的protected成员可以访问基类protected成员可以访问基类protected成员不可访问基类的private成员不可访问抛开私有成员不能访问public继承是可以访问private成员但是这里受到private限制符限制不能被其他类访问不可访问同理不可访问(同理)注意派生类在继承基类时不一定需要明确指定继承方式会默认为私有继承。在类中封装protected和private是一样的都是限制访问不能在类外访问只能类内访问但是在继承中就能看出区别了基类private成员是不能以任何继承方式被派生类访问而基类protected成员是可以被派生类访问这个在基类protected限制在派生类中也能被使用就是针对继承需求写的语法。这里派生类访问基类受到继承方式和基类中限制符继承方式中public最大protect较大private最小啥也不能访问只是继承所有成员变量/函数这里public派生类可以访问基类public和protected而protected只能访问到基类的protected。一句话总结不管继承方式是什么在内存中派生类都已经把基类中的所有成员和函数都继承下来了只是继承方式对派生类访问/调用基类成员/函数限制这个过程只受基类它给的限制符限制。1.3 继承模板类模板的继承直接调用类模板中的函数会导致编译错误它这找不到识别标志但是在一个类模板函数中实例化过程也会调用另一个类的实例化这个过程的函数并未实例化所以编译器找不到所以在调用基类模板函数时需要显示实例化并指定该函数类域才能调用该函数并完成实例化。#includevector templateclass T class stack : public vectorT { public: void push(const T value) { vectorT::push_back(value);//继承中组合模板类时需要调用模板类中的函数时需要实例化stack实例化过程会带动vector实例化但是vector的函数并未实例化 //push_back(value);//一般来说这样已经够了因为是public继承vectorT中函数也是public继承后可以直接调用但是这里是模板类函数未实例化 } void pop() { vectorT::pop_back(); } T top() { return vectorT::back(); } const T top()const { return vectorT::back(); } bool empty()const { return vectorT::empty(); //return empty();//这里也不能这样编译器找不到这里也存在隐藏就是在基类中出现empty然后再派生类也出现了empty会先调用派生类的 } };二、继承中基类和派生类的转换2.1 派生类转换为基类直接转换基类 派生类子类的值给父类这里实际上是派生类进行切片转换为父类中这个很好理解本来子类就是利用的父类继承下来的类型当然也相同直接转换即可不需要产生临时对象也就不会在引用过程中担心权限问题。也可以这样理解父亲赚到的钱也会成为他儿子的钱这样父亲的行为会影响儿子的结果所以基类可以用引用/指针去指向派生类这里也是多态的形成条件之一。class Person { public: Person(string name xxxx, string address xxxxxxx, string tel181XXX, int age 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; class Student: public Person { public: Student(string name xxxx, string address xxxxxxx, string tel 181XXX, int age 0,string uid 2403XXX) :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; }; int main() { Person p(张三, 翻斗花园1号, 110, 18); Student s(李四, 翻斗花园2号, 120, 20,240312345); Person P s;//直接切片 Person* P s;//直接切片 return 0; }2.2 基类不能直接转换为派生类在c中基类对象不能强制赋值给派生类对象派生类 基类这是面向对象设计的基本原则。所以这里对于派生类去引用和指针指向基类对象是不行的(这里基类切片没意义而且还会导致派生类中定义的私有成员未定义情况)。但是基类对象也不是不能转换为派生类只是这里需要使用写构造函数/赋值构造来转换而对于引用/指针就需要使用dynamic_cast来强制转换。总结三、继承中的作用域3.1 隐藏规则1.在继承体系中基类父和派生类子都有独立的作用域2.在派生类和基类中有同名成员派生类成员将屏蔽基类同名成员并直接访问派生类种的成员这种叫隐藏可以使用类域的一样去强调这个属于哪个域的3.成员函数的隐藏只要函数名相同且基类和派生类中都有就构成隐藏。class A { public: void fun() { cout func() endl; } }; class B : public A { public: void fun(int i) { cout func(int i) iendl; } };这里也同样构成隐藏满足函数名相同并且在派生类和基类都有相同函数名这里和有无参数无关系只要相同函数即可所以当我们创建一个B的对象时候然后调用fun函数如果是没参数传过去会编译报错它并不会去匹配A中的函数因为被隐藏了。3.2 隐藏和重载的区别先从概念入手隐藏的概念在继承中派生类的函数会隐藏基类中同名的函数只要函数名相同不管参数列表是否相同基类的函数都会被隐藏除非使用using声明引入基类函数。重载概念在同一作用域内函数名相同参数列表不同参数类型、个数、顺序返回类型可以相同也可以不同。从概念我们进行总结它们俩区别特性重载隐藏作用域必须在同一个作用域内在不同类域中派生类隐藏基类函数函数名必须相同必须相同参数列表必须不同没有要求有无都不影响隐藏返回类型没有要求没有要求virtual关键字没有要求没有要求继承关系不需要继承必须存在继承对于隐藏的导致不能使用基类函数我们还是在继承中最好不要用同名/明确使用基类中隐藏的函数时候加入类域去访问该隐藏的函数去显式调用。四、继承中默认成员函数两份部分分开处理各自处理各自。构造函数/拷贝构造/赋值1.基类父成员调用基类自己的构造/拷贝/赋值2.派生类子成员调用自己的也就是和之前学的类和对象一样。在声明顺序是基类先于派生类在调用初始化列表时会先调用基类成员这里又因为我们是把基类看成整体所以我们在给基类成员初始化/拷贝/赋值时需要像匿名对象调用一样把值传给基类从而实现在派生类统一初始化/拷贝/赋值当然前提是基类有这种构造函数/拷贝才能初始化/拷贝/赋值对于析构函数析构函数名字实际上不是~类名编译器会自动处理为Distractor所以这里就涉及到隐藏调用到相同名字所以在调用基类析构时需要指明基类中的调用。class Student;//由于这里友元了函数有Student这个类编译器向上寻找Student这个类定义没找到只能直接前置声明 class Person { //友元 friend void DisplayDate(const Person p, const Student s); public: Person(string name xxxx, string address xxxxxxx, string tel181XXX, int age 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } ~Person() { cout ~Person() endl; } private: string _name; string _address; string _tel; int _age; }; //Student是派生类,Person是基类这里public是继承方式 class Student: public Person { friend void DisplayDate(const Person p, const Student s); public: Student(string name xxxx, string address xxxxxxx, string tel 181XXX, int age 0,string uid 2403XXX) :Person(name,address,tel,age)//先调用基类构造只能这样调用基类构造来初始化派生类 ,_uid(uid) {} ~Student() { Person::~Person();//这里编译器处理后析构函数统一为destractor所以这里涉及隐藏必须显式调用 cout ~Student() endl; } private: string _uid; };左是没在派生类调用~Person()编译器自动释放的右是在派生类析构中调用~Person(),会导致先初始化先析构的情况当然这里派生类中不用写基类的析构函数编译器为了避免内存泄漏和调用顺序是先析构派生类在析构基类这里因为基类先初始化并且怕后续需要基类而提前释放空间对派生类的影响的混乱会等待派生类析构后自动调用基类的析构函数。五、实现不能继承的类方法一可以把基类的构造函数私有化因为派生类实例化必须先调用基类的构造函数。class Person { private://把构造私有化任何继承都不能访问私有成员变现限制继承 Person(string name xxxx, string address xxxxxxx, string tel181XXX, int age 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; class Student: public Person { public: Student(string name xxxx, string address xxxxxxx, string tel 181XXX, int age 0,string uid 2403XXX) :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; };方法二实现一个类不能继承可以在基类中类名加final。class Person final//不能被继承 { public: Person(string name xxxx, string address xxxxxxx, string tel181XXX, int age 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; class Student: public Person { public: Student(string name xxxx, string address xxxxxxx, string tel 181XXX, int age 0,string uid 2403XXX) :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; };六、继承中多继承6.1 单继承和多继承单继承 是指一个派生类继承一个直接基类和基类中包含继承没关系多继承就是一个派生类只有两个或以上直接基类。左边就是单继承右边是多继承6.2 菱形继承菱形继承是多继承的⼀种特殊情况。菱形继承的问题从下⾯的对象成员模型构造可以看出菱形继承有数据冗余和⼆义性的问题。class Person { public: Person(string name xxxx, string address xxxxxxx, string tel181XXX, int age 0) :_name(name) ,_address(address) ,_tel(tel) ,_age(age) { } private: string _name; string _address; string _tel; int _age; }; //Student是派生类,Person是基类这里public是继承方式 class Student: public Person { public: Student(string name xxxx, string address xxxxxxx, string tel 181XXX, int age 0,string uid 2403XXX) :Person(name,address,tel,age) ,_uid(uid) {} private: string _uid; }; class Teacher : public Person { public: Teacher(string name xxxx, string address xxxxxxx, string tel 181XXX, int age 0, string office 一教302) :Person(name, address, tel, age) ,_office(office) {} private: string _office; }; //菱形继承 class School :public Student, public Teacher { public: School(string name xxxx, string address xxxxxxx, string tel 181XXX, int age 0, string uid 2403XXX, string office 一教302) :Student(name, address, tel, age, uid) , Teacher(name, address, tel, age, office) {} private: string _school; };这就是一个典型的菱形继承导致数据冗余从而导致二义性问题我们这里需要的是这两个继承下来的Person中的成员中的一个即可不需要继承两个同样的Person成员。这里我们只需要解决其中一个数据冗余问题这两个问题就能解决了。6.3 虚继承根据多继承可能出现的菱形继承就一定会出现数据冗余和二义性问题会导致内存浪费和数据不一致这里数据不一致是不同路径的数据可能不同步还有构造/析构开销这里展开了一个虚继承它是会吧这两个继承同一个基类的放到外面然后通过指针等一些列操作去访问同一个该变量从而避免了数据冗余和内存浪费。使用关键词virtualclass Person1 { public: Person1(const char* name) :_name(name) {} string _name; // 姓名 }; class Student1 : virtual public Person1 { public: Student1(const char* name, int num) :Person1(name) , _num(num) {} protected: int _num; //学号 }; class Teacher1 : virtual public Person1 { public: Teacher1(const char* name, int id) :Person1(name) , _id(id) {} protected: int _id; // 职工编号 }; class Assistant : public Student1, public Teacher1 { public: Assistant(const char* name1, const char* name2, const char* name3) :Person1(name1) , Student1(name2, 2) , Teacher1(name3, 3) , _majorCourse(xxxx) {} protected: string _majorCourse; // 主修课程 };注意在菱形继承中的初始化过程它会以菱形继承中的根基类以上面的代码为例就是Person1的初始化为最终的初始化结果这个结果只是影响根基类中的变量结果但是不影响其他派生类中特有的变量结果6.4 虚继承和菱形继承的区别总结关键区别普通菱形继承的二义性原因是存在多个不同的 Base对象编译器不知道你要操作哪个对象必须明确指定访问路径虚继承的路径调用所有路径指向同一个共享的 Base 对象不同路径只是访问方式不同操作的是同一数据可以直接访问也可以选择路径但效果相同本质差异普通菱形继承你有两个名字相同的亲戚需要指定是哪一个虚继承你有一个亲戚可以用不同昵称称呼他七、继承和组合7.1 继承的使用#includevector templateclass T class stack:public vectorT { public: void push(const T value) { vectorT::push_back(value);//继承中组合模板类时需要调用模板类中的函数时需要实例化stack实例化过程会带动vector实例化但是vector的函数并未实例化 } void pop() { vectorT::pop_back(); } T top() { return vectorT::back(); } const T top()const { return vectorT::back(); } bool empty()const { return vectorT::empty(); } };7.2 组合的使用#includevector templateclass T class stack { public: void push(const T value) { _v.push_back(value); } void pop() { _v.pop_back(); } T top() { return _v.back(); } const T top()const { return _v.back(); } bool empty()const { return _v.empty(); } private: vectorT _v; };7.3 区别public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。组合是⼀种has-a的关系。假设B组合了A每个B对象中都有⼀个A对象。继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔在继承⽅式中基类的内部细节对派⽣类可见 。继承⼀定程度破坏了基类的封装基类的改变对派⽣类有很⼤的影响。派⽣类和基类间的依继承缺点赖关系很强耦合度⾼。对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤风格被称为⿊箱复⽤(black-box reuse)因为对象的内部细节是不可见的。对象只以“⿊箱”的形式出现。组合优点组合类之间没有很强的依赖关系耦合度低。优先使⽤对象组合有助于你保持每个类被封装。优先使⽤组合⽽不是继承。实际尽量多去⽤组合组合的耦合度低代码维护性好。不过也不太那么绝对类之间的关系就适合继承(is-a)那就⽤继承另外要实现多态也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a)就⽤组合。#includevector templateclass T class stack : public vectorT//如果没有继承vector这里是可以正常实现stack栈的功能的只是这里组合和继承一起了还调用函数也混淆了才会导致栈的功能不正常也就是数据混乱 { public: void push(const T value) { _v.push_back(value);//插入到_v成员变量的内部去了而不是stack这个栈中了 vectorT::push_back(value);//正确调用这才能插入到stack这个类中而不是私有成员_v } void pop() { _v.pop_back(); } T top() { return _v.back(); } const T top()const { return _v.back(); } bool empty()const { return _v.empty(); } private: vectorT _v; };在日常中最好还是不要写上面这种代码虽然也是可以的但是容易混淆就像这里混淆了把_v当作stack的主体了进行使用但是这里stack又继承就会导致_v没意义了它内部是以继承中的vectorT为主体包括_size,_capacity,_arr这些当像上面的代码这样写插入时候会插到_v中去了不在stack主体上了(可以理解成隐藏了但是不是隐藏只是可以当成隐藏理解)而是它的成员内部去了如果想用也是可以的只不过这里需要把插入这些逻辑变成继承中的。八、总结继承的作用继承就是一个复用可以使我们代码提供更简便。我们要理解继承方式和类中限定符的区别protected用处是在继承中替代private的作用不仅如此还要注意派生类和基类的转换派生类可以直接赋值给基类也就能够是得基类直接引用和指针指向派生类。继承中的隐藏也是一个很重要的知识点这里又涉及到了和重载的区别。继承中的多继承是一个继承的多样化但是我们需要注意多继承和单继承区别还有就是出现多继承必然可能会出现菱形继承在实践中我们应该避免菱形继承这种菱形继承会涉及到虚继承虚表从而解决数据冗余和二义性问题。在写代码的途中我也发现了继承和组合这两个一起时候如果使用不当会造成实现结果不符合自己预期要么就是避免两个相同类一个组合一个继承同时使用要么就是逻辑需要以继承为主避免以组合为主导致我上面写出的栈功能不对。当然在既能使用组合和继承这两复用时候我们可以首先选择组合因为组合依赖性低耦合度低。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑