C++继承 ---- 继承是面向对象三大特性之一【好处:可以减少重复的代码】
目录
一、前言:继承技术的引入
二、继承的基本语法
2.1. 基本语法
2.2. 继承示例
三、三种继承方式【公共继承、保护继承、私有继承】
2.1 基本概念
2.2 三种继承方式的影响
2.3 为什么通常使用 public 继承?
2.4 class的默认继承方式:private继承
四、继承中的对象模型
📌 继承成员的归属情况
1️⃣ 属于子类对象的成员
2️⃣ 不属于子类对象的成员
🔬 代码验证
🔎 结果分析
💡 结论
✅ 属于子类对象的:
❌ 不属于子类对象的:
五、继承中的构造和析构顺序
六、继承同名成员处理方式
6.1 示例:非静态成员变量情况下
6.2 示例:非静态成员函数情况下
6.3 示例:静态成员变量情况下
6.3.1 通过对象访问
6.3.2 通过类名来访问
6.4 示例:静态成员函数情况下
6.4.1 通过对象访问
6.4.2 通过类名来访问
6.5 总结
七、多继承语法【可以认多个爹】
7.1 多继承的基本语法
🚨 7.2. 多继承的命名冲突【作用域解析符 :: 解决命名冲突】
💢 7.3. 菱形继承问题(Diamond Problem)【虚继承 virtual 解决菱形继承】
7.3.1 菱形继承示例
7.3.2 解决方案:使用虚继承
7.3.3 虚继承解决菱形继承的底层原理
八、Python中的 super() 等效于 Base::method() 调用
一、前言:继承技术的引入
有些类与类之间存在特殊的关系,如下图中所示:
从这幅图可以直观地看出继承在面向对象编程(OOP)中的应用。
- 最顶层是“动物”类(基类):所有猫和狗的共同特性(如生命体、能移动等)都可以定义在这个类中。
- 第二层是“猫”和“狗”类(派生类):它们继承了“动物”的共性,同时可以添加各自独特的行为,比如猫会“喵喵叫”,狗会“汪汪叫”。
- 第三层是具体的猫和狗品种:比如加菲猫、布偶猫、哈士奇、德牧等。它们不仅拥有“猫”或“狗”的特性,还可以进一步细化,比如加菲猫的扁脸、哈士奇的“二哈”性格等。
结合继承来看:
- 代码复用:比如所有猫科动物的行为可以定义在“猫”类中,所有狗的行为可以定义在“狗”类中,而不用为每个品种重复编写。
- 层级清晰:继承关系清晰地表达了从一般到具体的层次关系,使代码更易维护和拓展。
- 便于扩展:如果未来需要新增动物类别(比如“鸟”),只需继承“动物”类,而不影响现有结构。
这幅图很好地体现了继承的本质:将共性上移,个性下放,减少重复,提高代码的组织性和可维护性。 😊
对上述内容的同等专业语言描述:
在定义类时,我们常常会遇到这样的情况:下级别的成员不仅继承了上一级的共性,还具备自身的特性。此时,我们可以借助继承这一技术,减少重复代码,提高代码的复用性。
继承是面向对象编程(OOP)的核心概念之一,它使子类(派生类)能够直接复用父类(基类)的属性和方法,同时可以根据自身需求进行扩展或重写。这不仅有助于提升代码的组织性和可维护性,还能让程序结构更加清晰,便于后续扩展和优化。
再举一个实际项目的继承例子:
在网站开发中,通常会有公共的头部、底部、侧边栏,这些部分在多个页面中是相同的,而页面的主要内容(中心区域)是不同的。我们可以使用继承来避免重复代码,让子类只关注自己的“独特内容”。
例如我们打开网页:人工智能开发基础,人工智能开发教程,人工智能开发学习就来黑马程序员
公共的头部分:
公共的侧边栏部分:
公共的底部分:
不同的中心区域:
- 人工智能开发:
- C/C++:
我们可以使用继承来避免重复代码,让子类只关注自己的“独特内容”。
- 基类(父类):
WebPage
负责定义通用部分(头部、底部、侧边栏)。 - 子类(派生类):
HomePage
、AboutPage
、ContactPage
只需要专注于中心内容,而公共部分由父类提供。
派生类中的成员包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过来的表现其共性,而新增的成员体现了其个性。
二、继承的基本语法
在 C++ 中,继承(Inheritance) 允许子类(派生类)继承父类(基类)的成员变量和方法,从而提高代码复用性和可维护性。
2.1. 基本语法
class 父类名 {
public:父类名(参数) {// 构造函数}void 方法() {cout << "这是父类的方法" << endl;}
};// 子类继承父类
class 子类名 : public 父类名 {
public:子类名(参数) : 父类名(参数) {} // 调用父类构造函数
};
2.2. 继承示例
#include <iostream>
using namespace std;// 定义父类 Animal
class Animal {
public:string name;// 构造函数Animal(string name) {this->name = name;}// 父类方法void makeSound() {cout << "动物在发出声音" << endl;}
};// 子类 Dog 继承 Animal
class Dog : public Animal {
public:// 构造函数,调用父类构造函数Dog(string name) : Animal(name) {}// 重写父类方法void makeSound() {cout << name << ":汪汪!" << endl;}
};int main() {Dog dog("哈士奇");dog.makeSound(); // 输出:哈士奇:汪汪!return 0;
}
说明
Dog
继承了Animal
的属性name
,并重写了makeSound()
方法。- 子类构造函数
Dog(string name) : Animal(name)
显式调用了父类构造函数。
三、三种继承方式【公共继承、保护继承、私有继承】
2.1 基本概念
在 C++ 中,class Child : [继承方式] Parent
指定了子类继承父类的方式,[继承方式] 决定了 父类成员在子类中的访问权限。
C++ 提供三种继承方式:
public
继承(常用)protected
继承private
继承
这三种继承方式会影响 父类的 public
和 protected
成员 在 子类中的访问权限。
继承方式 | 父类的 public 变成 | 父类的 protected 变成 | 父类的 private |
---|---|---|---|
public 继承 | 仍然是 public | 仍然是 protected | 无法访问 |
protected 继承 | 变成 protected | 仍然是 protected | 无法访问 |
private 继承 | 变成 private | 变成 private | 无法访问 |
向高权限兼容!!
2.2 三种继承方式的影响
#include <iostream>
using namespace std;class Parent {
public:int pubVar; // 公有成员
protected:int protVar; // 受保护成员
private:int privVar; // 私有成员(子类不可访问)
};// 1. public 继承
class ChildPublic : public Parent {
public:void show() {pubVar=10; // 父类中的公共权限成员 到子类中依然是公共权限protVar=10; // 父类中的受保护权限成员 到子类中依然是受保护权限// privVar=10; // ❌ 子类无法访问父类的 private 成员}
};// 2. protected 继承
class ChildProtected : protected Parent {public:void show() {pubVar=10; // 父类中的公共权限成员 到子类中为受保护权限protVar=10; // 父类中的受保护权限成员 到子类中依然是受保护权限// privVar=10; // ❌ 子类无法访问父类的 private 成员}
};// 3. private 继承
class ChildPrivate : private Parent {
public:void show() {pubVar=10; // 父类中的公共权限成员 到子类中是私有权限protVar=10; // 父类中的受保护权限成员 到子类中是私有权限// privVar=10; // ❌ 子类无法访问父类的 private 成员}
};int main() {ChildPublic obj1;cout << obj1.pubVar << endl; // ✅ `public` 继承,外部可以访问 pubVar// cout << obj1.protVar; // ❌ protected 成员仍然不可外部访问ChildProtected obj2;// cout << obj2.pubVar; // ❌ `protected` 继承后,pubVar 变成 protected,外部无法访问ChildPrivate obj3;// cout << obj3.pubVar; // ❌ `private` 继承后,pubVar 变成 private,外部无法访问return 0;
}
2.3 为什么通常使用 public
继承?
✅ public
继承遵循 “is-a” 关系,即 子类“是一种”父类,适用于 面向对象编程。
✅ public
继承后,子类对象 仍然可以使用父类的 public
成员,符合直觉。
✅ private
或 protected
继承会 改变访问权限,通常用于特殊情况,比如 代码封装 或 限制子类访问。
一般来说,如果子类应该 完全继承 父类的行为,使用 public
继承 是最佳选择。
2.4 class的默认继承方式:private继承
#include <iostream>
using namespace std;class Base {public:int m_A; // 基类中的公有成员变量
};class Derived : Base { // 省略了继承方式,默认是 private 继承public:// m_A=10; // 不是构造函数或成员函数中的赋值,而是非法的语句,不能这样写。void func(){m_A=10; // Derived 以 ptivate 继承方式继承了 Base,m_A 变量现在变成私有成员,外部无法访问}
};int main() {Base b;b.m_A; // 可以访问Derived d;d.m_A; // 编译❌:member "Base::m_A" (declared at line 6) is inaccessibleC/C++(265)return 0;
}
❌ 错误原因:
Derived
默认是 private 继承Base
,导致Base::m_A
变成private
,main()
中无法访问它。
四、继承中的对象模型
问题:从父类继承过来的成员,哪些属于子类对象中?
“在 C++ 中,从父类继承过来的非静态成员(变量/函数)会成为子类对象的一部分,而静态成员(变量/函数)、构造函数和析构函数并不属于子类对象。”
属于子类对象的:
- 基类的非静态成员变量(无论是
public
、protected
还是private
,都存在于子类对象的内存中)- 基类的非静态成员函数(但
private
成员函数不能在子类中访问)- 基类的
protected
和private
成员虽然不能被子类访问,但仍然在子类对象的内存布局中不属于子类对象的:
- 基类的静态成员变量(静态成员属于类,而不属于任何对象)
- 基类的构造函数和析构函数(它们只是控制对象的构造和销毁,不属于对象本身)
- 基类的静态成员函数(因为它们不依赖于具体对象)
💡 所以,继承只是让子类“拥有”了基类的非静态成员,但不意味着所有成员都属于子类对象! 🚀
📌 继承成员的归属情况
1️⃣ 属于子类对象的成员
- 所有的非静态成员变量
- 所有的非静态成员函数
💡 无论继承方式(public
/ protected
/ private
),基类的非静态成员都会作为子类对象的一部分,静态成员不会。
2️⃣ 不属于子类对象的成员
-
静态成员变量(Static Members)
- 静态成员属于类本身,而不是对象。
- 子类可以访问基类的静态成员,但静态成员不存储在子类对象中。
-
基类的构造函数 & 析构函数
- 构造函数不属于子类对象,但会在创建子类对象时执行。
- 析构函数也不属于子类对象,但在销毁子类对象时会自动调用。
-
私有继承和保护继承时的基类成员
- 在
private
或protected
继承时,基类的public
和protected
成员的访问级别发生变化,但它们仍然存在于子类对象中。
- 在
🔬 代码验证
验证: 从父类继承过来的静态成员(变量/函数)不属于子类对象。
#include <iostream>
using namespace std;class Base {
public:int a; // 非静态成员变量 -> 属于子类对象static int b; // 静态成员变量 -> 不属于任何对象
};int Base::b = 10; // 初始化静态成员变量class Derived : public Base { // public 继承
public:int c; // 子类自己的成员变量
};int main() {Derived d;cout << "Size of Derived object: " << sizeof(d) << endl;cout << "Base::b (Static): " << Base::b << endl;return 0;
}// 输出:
// Size of Derived object: 8
// Base::b (Static): 10
🔎 结果分析
sizeof(d)
计算Derived
对象的大小,包括了Base
的非静态成员a
。Base::b
是静态成员,不属于d
,而是共享的。
验证:基类中的public、protected、private成员是否被继承到子类中。
#include<iostream>class Base{public:int a;protected:int b;private:int c;
};class Derived:public Base{public:int d;
};int main(){// 打印子类实例大小【注意:类本身是不占用内存的,sizeof(Derived)本质是sizeof(Derived类的实例)】std::cout<<"size of Derived:"<<sizeof(Derived)<<std::endl; // 输出:size of Derived:16return 0;
}
💡 结论
✅ 属于子类对象的:
- 基类的非静态成员变量
- 基类的非静态成员函数
- 基类的
protected
/private
继承时仍然保留的成员变量
❌ 不属于子类对象的:
- 静态成员变量
- 基类的构造函数 & 析构函数
- 基类的私有成员(子类无法访问,但仍然存储在对象中)
🎯 记住:继承会让基类的非静态成员成为子类对象的一部分,但静态成员和构造函数不会! 🚀
五、继承中的构造和析构顺序
子类继承父类后,当创建子类对象时,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
答案:父类构造-->子类构造-->子类析构-->父类析构
我们以父类与子类为例来说明构造顺序和析构顺序 ,示例如下:
#include<iostream>class Base{public:// 基类中的构造函数Base(){std::cout<<"调用基类中的构造函数"<<std::endl;}// 基类中的析构函数~Base(){std::cout<<"调用基类中的析构函数"<<std::endl;}};class Derived:public Base{public:Derived(){std::cout<<"调用子类中的构造函数"<<std::endl;}// 基类中的析构函数~Derived(){std::cout<<"调用子类中的析构函数"<<std::endl;}
};int main(){Derived d1; // 子类实例化return 0;
}
// 输出:(先有父亲再有儿子)
// 调用基类中的构造函数
// 调用子类中的构造函数
// 调用子类中的析构函数
// 调用基类中的析构函数
我们再增添一个孙子类来看看构造与析构顺序。
#include<iostream>
// 父亲类
class Base{public:// 基类中的构造函数Base(){std::cout<<"调用Base类中的构造函数"<<std::endl;}// 基类中的析构函数~Base(){std::cout<<"调用Base类中的析构函数"<<std::endl;}
};
// 儿子类
class Derived:public Base{public:Derived(){std::cout<<"调用Derived类中的构造函数"<<std::endl;}// 基类中的析构函数~Derived(){std::cout<<"调用Derived类中的析构函数"<<std::endl;}
};// 孙子类
class grandDerived:public Derived{public:grandDerived(){std::cout<<"调用grandDerived类中的构造函数"<<std::endl;}// 基类中的析构函数~grandDerived(){std::cout<<"调用grandDerived类中的析构函数"<<std::endl;}
};int main(){grandDerived gd1; // 孙子类实例化return 0;
}
// 输出:(构造顺序:先有父亲再有儿子再有孙子,析构顺序:白发人送黑发人)
// 调用Base类中的构造函数
// 调用Derived类中的构造函数
// 调用grandDerived类中的构造函数
// 调用grandDerived类中的析构函数
// 调用Derived类中的析构函数
// 调用Base类中的析构函数
六、继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员直接访问即可
- 访问父类同名成员需要加作用域
- 当子类与父类拥有同名的成员,子类会隐藏父类中同名成员,加父类作用域可以访问到父类中的成员。
6.1 示例:非静态成员变量情况下
当子类中出现和父类同名的成员变量,子类的同名成员变量会隐藏掉父类中所有同名成员变量,如果想要通过子类对象访问到父类中被隐藏的同名成员变量,需要加父类作用域。
#include<iostream>
// 父类
class Base{public:int m_A;Base(){ // 父类构造函数m_A=100;}
};
// 子类
class Derived:public Base{public:int m_A;Derived(){ // 子类构造函数m_A=200;}
};int main(){Derived d1; // 子类实例化std::cout<<"Derived 类中的 m_A = "<<d1.m_A<<std::endl; // 输出:Derived 类中的 m_A = 200 访问自身数据// 如果通过子类对象 访问到父类中与子类同名的成员,需要加父类作用域std::cout<<"Base 类中的 m_A = "<<d1.Base::m_A<<std::endl; // 输出:Base 类中的 m_A = 100return 0;
}
当子类中没有出现和父类同名的成员变量,可通过子类对象直接访问到父类中的成员变量。
#include<iostream>
// 父类
class Base{public:int m_A=100;
};
// 子类
class Derived:public Base{public:
};int main(){Derived d1; // 子类实例化std::cout<<d1.m_A<<std::endl; // 输出:m_A = 100 return 0;
}
6.2 示例:非静态成员函数情况下
当子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有同名成员函数,如果想要通过子类对象访问到父类中被隐藏的同名成员函数,需要加父类作用域。
#include<iostream>
// 父类
class Base{public:void func(){std::cout<<"Base-func调用~"<<std::endl;}
};
// 子类
class Derived:public Base{public:void func(){std::cout<<"Derived-func调用~"<<std::endl;}
};int main(){Derived d1; // 子类实例化d1.func(); // 访问自身 Derived-func调用~d1.Base::func(); // Base-func调用~return 0;
}
当子类中没有出现和父类同名的成员函数,可通过子类对象直接访问到父类中的成员函数。
#include<iostream>
// 父类
class Base{public:void func(){std::cout<<"Base-func调用~"<<std::endl;}
};
// 子类
class Derived:public Base{public:
};int main(){Derived d1; // 子类实例化d1.func(); // Base-func调用~return 0;
}
6.3 示例:静态成员变量情况下
6.3.1 通过对象访问
当子类中出现和父类同名的静态成员变量,子类的同名静态成员变量会隐藏掉父类中所有同名静态成员变量,如果想要通过子类对象访问到父类中被隐藏的同名静态成员变量,需要加父类作用域。
#include<iostream>
// 父类
class Base{public:static int a; // 静态成员变量类内声明
};
int Base::a=10; // 静态成员变量类外初始化// 子类
class Derived:public Base{public:static int a; // 静态成员变量类内声明
};
int Derived::a=20; // 静态成员变量类外初始化int main(){Derived d1; // 子类实例化std::cout<<"Derived-a:"<<d1.a<<std::endl; // Derived-a:20std::cout<<"Base-a:"<<d1.Base::a<<std::endl; // Base-a:10return 0;
}
当子类中没有出现和父类同名的静态成员变量,可通过子类对象直接访问到父类中的静态成员变量。
#include<iostream>
// 父类
class Base{public:static int a; // 静态成员变量类内声明
};
int Base::a=10; // 静态成员变量类外初始化// 子类
class Derived:public Base{
};int main(){Derived d1; // 子类实例化std::cout<<d1.a<<std::endl; // 10return 0;
}
6.3.2 通过类名来访问
#include<iostream>
// 父类
class Base{public:static int a; // 静态成员变量类内声明
};
int Base::a=10; // 静态成员变量类外初始化// 子类
class Derived:public Base{public:static int a; // 静态成员变量类内声明
};
int Derived::a=20; // 静态成员变量类外初始化int main(){// 通过类名方式访问静态成员变量std::cout<<"Derived-a:"<<Derived::a<<std::endl; // Derived-a:20// 第一个::表示通过类名方式访问 第二个::表示访问父类作用域下std::cout<<"Base-a:"<<Derived::Base::a<<std::endl; // Base-a:10return 0;
}
6.4 示例:静态成员函数情况下
6.4.1 通过对象访问
当子类中出现和父类同名的静态成员函数,子类的同名静态成员函数会隐藏掉父类中所有同名静态成员函数,如果想要通过子类对象访问到父类中被隐藏的同名静态成员函数,需要加父类作用域。
#include<iostream>
// 父类
class Base{public:static void func(){std::cout<<"Base-func调用~"<<std::endl;}
};
// 子类
class Derived:public Base{public:static void func(){std::cout<<"Derived-func调用~"<<std::endl;}
};int main(){Derived d1; // 子类实例化d1.func(); // 访问自身 Derived-func调用~d1.Base::func(); // Base-func调用~return 0;
}
当子类中没有出现和父类同名的静态成员函数,可通过子类对象直接访问到父类中的静态成员函数。
#include<iostream>
// 父类
class Base{public:static void func(){std::cout<<"Base-func调用~"<<std::endl;}
};
// 子类
class Derived:public Base{
};int main(){Derived d1; // 子类实例化d1.func(); // Base-func调用~return 0;
}
6.4.2 通过类名来访问
#include<iostream>
// 父类
class Base{public:static void func(){std::cout<<"Base-func调用~"<<std::endl;}
};
// 子类
class Derived:public Base{public:static void func(){std::cout<<"Derived-func调用~"<<std::endl;}
};int main(){// 通过类名访问静态成员函数Derived::func(); // Derived-func调用~Derived::Base::func(); // Base-func调用~return 0;
}
6.5 总结
无论是静态成员还是非静态成员(包括成员变量和成员函数):
- 若子类中没有同名成员,则可通过子类实例直接访问父类成员
- 若子类中有和父类相同的成员,则需要加父类作用域才能通过子类实例访问父类成员
注意:静态成员属于类本身,不属于某个具体的对象,它们是共享的(相对于对象来说是共享的)。因此也可以通过类名直接访问它们。
七、多继承语法【可以认多个爹】
C++ 允许多继承(Multiple Inheritance),但它会带来一些复杂性,特别是命名冲突和菱形继承(Diamond Problem),所以在实际开发中通常不推荐使用多继承,而是更倾向于组合(Composition)或虚继承(Virtual Inheritance)来解决相关问题。
7.1 多继承的基本语法
class A {
public:int valueA;
};class B {
public:int valueB;
};class C : public A, public B { // C 继承自 A 和 B
public:int valueC;
};
C
继承了 A
和 B
,因此 C
对象中同时包含 valueA
和 valueB
。
🚨 7.2. 多继承的命名冲突【作用域解析符 ::
解决命名冲突】
当多个基类中存在同名成员,子类需要通过作用域解析符(::
)来区分,否则会产生二义性错误。
⚠️ 命名冲突示例
#include <iostream>
using namespace std;class A {
public:int value = 10;
};class B {
public:int value = 20;
};class C : public A, public B { };int main() {C obj;// cout << obj.value; // ❌ 编译错误:value 有二义性cout << "A::value = " << obj.A::value << endl; // ✅ 访问 A 的 valuecout << "B::value = " << obj.B::value << endl; // ✅ 访问 B 的 valuereturn 0;
}
编译错误:
🔹 解释:
obj.value
直接访问会产生二义性错误,因为A
和B
都有value
。- 需要使用
obj.A::value
和obj.B::value
明确指定要访问哪个基类的value
。
💢 7.3. 菱形继承问题(Diamond Problem)【虚继承 virtual
解决菱形继承】
当一个类间接继承自同一个基类时,就可能导致菱形继承问题。即:两个派生类继承同一个基类,又有某个类同时继承着这两个派生类,这种继承被称为菱形继承,或者钻石继承。
7.3.1 菱形继承示例
class A {
public:int value = 10;
};class B : public A { }; // B 继承 A
class C : public A { }; // C 继承 A
class D : public B, public C { }; // D 继承 B 和 C
💥 菱形继承所带来的问题
D
继承了B
和C
,但B
和C
都继承了A
,导致D
有两份A::value
,这会造成数据冗余和二义性。
7.3.2 解决方案:使用虚继承
#include<iostream>
class A { // 虚基类public:int value = 10;};class B : virtual public A { }; // 虚继承
class C : virtual public A { }; // 虚继承
class D : public B, public C { };int main() {D obj;std::cout << obj.value << std::endl; // ✅ 只有一份 A::valuereturn 0;
}
🔹 virtual
继承 确保 D
只有一份 A::value
,避免数据冗余。
7.3.3 虚继承解决菱形继承的底层原理
虚继承解决菱形继承的底层原理请看这篇博文:C++中的菱形继承问题【使用虚继承方法来解决】-CSDN博客!!!!
八、Python中的 super()
等效于 Base::method()
调用
在 C++ 中,子类可以使用 Base::method()
调用父类的方法,类似于 Python 的 super()
:
#include<iostream>class Animal {public:void makeSound() {std::cout << "动物发出声音" << std::endl;}};class Dog : public Animal {
public:void makeSound() {Animal::makeSound(); // 调用父类方法std::cout << "汪汪!" << std::endl;}
};
int main(){Dog hashiqi; // 哈士奇hashiqi.makeSound();return 0;
}// 输出:
// 动物发出声音
// 汪汪!
C++ 继承机制强大,同时支持多重继承和虚继承,如果有更复杂的需求,可以进一步探索 虚函数(virtual functions) 和 抽象类(abstract classes)。🚀