跳到主要内容
C++ 继承机制详解:从基础语法到菱形继承 | 极客日志
C++ 算法
C++ 继承机制详解:从基础语法到菱形继承 C++ 继承机制允许派生类复用基类的成员函数和数据,同时支持访问控制修饰符调整可见性。对象赋值遵循切片原则,子类可隐式转换为父类指针或引用。作用域查找遵循隐藏规则,同名成员需显式指定基类作用域。默认成员函数中,构造函数需调用基类构造,析构函数自动保证先子后父顺序。多继承可能引发菱形继承问题,导致数据冗余和二义性,虚拟继承通过共享基类实例解决此矛盾。实践中优先组合而非继承,仅在需要多态时使用继承。
全栈工匠 发布于 2026/3/21 更新于 2026/4/25 1 浏览继承的核心概念
继承是类设计层次的复用。基类(父类)包含通用逻辑,派生类(子类)在此基础上扩展。
**语法:**Person 是父类,也称作基类。Student 是子类,也称作派生类。
继承关系和访问限定符
继承以后,保护和私有成员的可见性会发生变化。
不可见 :基类的私有成员虽然被继承到了派生类对象中,但语法上限制派生类对象不管在类里面还是类外面,都不能去访问它。基类的私有成员在基类中还是能用,在基类外不能用。
Protected :如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就把基类成员定义为 protected。
访问规则 :基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式等于 Min(成员在基类的访问限定符,继承方式),即 public > protected > private。
基类和派生类对象赋值转换
不同类型对象赋值要类型转换。
int i = 0 ;
double d = i;
定义几个类来演示:
class Person {
public :
void Print () { cout << "name:" << _name << endl; cout << "age:" << _age << endl; }
protected :
string _name = "peter" ;
int _age = 18 ;
};
class Student : Person {
:
_stuid;
};
: Person {
:
_jobid;
};
public
protected
int
class
Teacher
public
protected
int
父类对象不能赋值给子类对象,强转也不行 。否则子类特有的成员变量怎么办,给随机值?
父类对象可以赋值给子类的指针、引用 (后面多态部分会细说)。
int main () {
Person p;
Student s;
p = s;
Person p1 = s;
return 0 ;
}
子类对象可以赋值给父类的对象、指针、引用 。子类赋值给父类对象,语法上特殊处理,没有发生类型转换,而是赋值兼容(切割、切片)。把子类里,父类那部分切出来,拷贝给父类。
继承中的作用域 域都是编译时查找规则的概念。定义了一个类,就有类域。子类和父类有独立的类域。
子类 和父类 中有同名成员 ,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏 ,也叫重定义 。(在子类成员函数中,可以使用 父类::父类成员 显示访问)。
父子类域中,如果是成员函数的隐藏 ,只需要函数名相同 就构成隐藏 。
class Person {
public :
void fun () { cout << "Person::func()" << endl; }
protected :
string _name = "小李子" ;
int _num = 111 ;
};
class Student : public Person {
public :
void fun (int ) { cout << "Student::func(int)" << endl; }
void Print () {
cout << "姓名:" << _name << endl;
cout << _num << endl;
cout << Person::_num << endl;
}
protected :
int _num = 999 ;
};
int main () {
Student s;
s.Print ();
s.fun (1 );
s.Person::fun ();
return 0 ;
}
2 个 func 构成什么关系?
a、隐藏/重定义 b、重载 c、重写/覆盖 d、编译报错
派生类的默认成员函数
1. 构造函数 按以前的理解,子类有 2 个成员:_name 和 _id。
规定:子类的构造函数中,不能在初始化列表显示初始化父类成员。在构造函数体内可以。
class Person {
public :
Person (const char * name = "peter" ) : _name(name) { cout << "Person()" << endl; }
~Person () { cout << "~Person()" << endl; }
protected :
string _name;
};
class Student : public Person {
public :
Student (const char * name = "张三" , int id = 0 ) :_id(0 )
{
}
protected :
int _id;
};
int main () {
Student s;
return 0 ;
}
规定:子类必须调父类的构造函数,初始化父类成员 。默认:在初始化列表自动调用父类的默认构造函数(父类不提供默认构造函数会报错)。若父类不提供默认构造函数:
class Person {
public :
Person (const char * name) : _name(name) { cout << "Person()" << endl; }
~Person () { cout << "~Person()" << endl; }
protected :
string _name;
};
class Student : public Person {
public :
Student (const char * name = "张三" , int id = 0 ) :Person (name)
,_id(0 ) {}
protected :
int _id;
};
int main () {
Student s;
return 0 ;
}
调试发现,在初始化列表先走 Person(name),说明:继承的成员声明在自己的成员之前 。在初始化列表先写 _id 也可以,因为初始化顺序是按声明顺序来的。
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 () { cout << "~Person()" << endl; }
protected :
string _name;
};
class Student : public Person {
public :
Student (const char * name = "张三" , int id = 0 ) :Person (name),_id(0 ) {}
Student (const Student& s)
:Person (s),_id(s._id) {}
protected :
int _id;
};
int main () {
Student s1;
Student s2 (s1) ;
return 0 ;
}
31 行,父类拷贝构造要传父类对象,在这里只有子类对象,可以直接传:子类对象可以传给父类对象的指针/引用 。
如果删去 31 行,不会报错,但有问题:拷贝构造也是构造函数,构造函数在初始化列表不写,默认不会调拷贝构造函数,默认调默认构造函数。
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 id = 0 ) :Person (name),_id(0 ) {}
Student (const Student& s) :Person (s),_id(s._id) {}
Student& operator =(const Student& s) {
if (this != &s) {
Person::operator =(s);
_id = s._id;
}
return *this ;
}
protected :
int _id;
};
int main () {
Student s1;
Student s3 ("李四" , 1 ) ;
s1 = s3;
return 0 ;
}
4. 析构函数 class Person {
public :
protected :
string _name;
};
class Student : public Person {
public :
protected :
int _id;
};
int main () {
Student s1;
Student s3 ("李四" , 1 ) ;
s1 = s3;
return 0 ;
}
为什么 58 行调不到,要指定父类类域?由于后面多态的原因 (具体后面讲),析构函数的函数名被特殊处理了,统一处理成 destructor,此时构成隐藏。
但此时又有新的问题,总共 2 个对象,调了 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; delete _pstr; }
protected :
string _name;
string* _pstr = new string ("111111111" );
};
class Student : public Person {
public :
Student (const char * name = "张三" , int id = 0 ) :Person (name),_id(0 ) {}
Student (const Student& s) :Person (s),_id(s._id) {}
Student& operator =(const Student& s) {
if (this != &s) {
Person::operator =(s);
_id = s._id;
}
return *this ;
}
~Student () {
cout << *_pstr << endl;
}
protected :
int _id;
};
int main () {
Student s1;
Student s3 ("李四" , 1 ) ;
s1 = s3;
return 0 ;
}
如果把 Person::~Person(); 放开:↓
继承与友元 友元关系不能继承,父类友元不能访问子类私有和保护成员 。
class Student ;
class Person {
friend void Display (const Person& p, const Student& s) ;
protected :
string _name;
};
class Student : public Person {
friend void Display (const Person& p, const Student& s) ;
protected :
int _stuNum;
};
void Display (const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main () {
Person p;
Student s;
Display (p, s);
return 0 ;
}
屏蔽掉 11 行会报错:"Student::_stuNum": 无法访问 protected 成员 (在"Student"类中声明)。
继承与静态成员 以前的继承:子类对象中的父类成员,和父类对象成员不是同一个。
父类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 static 成员实例,在子类中不会单独拷贝一份。
e.g.:↓ 统计 Person 及子类创建出多少个对象。
class Person {
public :
Person () { ++_count; }
public :
static int _count;
};
int Person::_count = 0 ;
class Student : public Person {
protected :
int _stuNum;
};
class Graduate : public Student {
protected :
string _seminarCourse;
};
int main () {
Person p;
Student s;
cout << &p._name << endl;
cout << &s._name << endl;
cout << &p._count<< endl;
cout << &s._count << endl;
cout << &Person::_count << endl;
cout << &Student::_count << endl;
return 0 ;
}
多继承、菱形继承、菱形虚拟继承 单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
菱形继承的问题:数据冗余(浪费空间)、二义性(不知道要访问谁) 。
class Person {
public :
string _name;
int _age;
};
class Student : public Person {
protected :
int _num;
};
class Teacher : public Person {
protected :
int _id;
};
class Assistant : public Student, public Teacher {
protected :
string _majorCourse;
};
int main () {
Assistant as;
as._age = 19 ;
return 0 ;
}
int main () {
Assistant as;
as.Student::_age = 18 ;
as.Teacher::_age = 30 ;
return 0 ;
}
虚拟继承解决数据冗余和二义性的原理 class Person {
public :
string _name;
int _age;
};
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;
};
int main () {
Assistant as;
as.Student::_age = 18 ;
as.Teacher::_age = 30 ;
as._age = 19 ;
return 0 ;
}
菱形继承 vs 菱形虚拟继承 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;
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 ;
d._a = 0 ;
D d1;
return 0 ;
}
int main () {
D d;
d._a = 0 ;
B b;
b._a = 2 ;
b._b = 3 ;
B* ptr = &b;
ptr->_a++;
ptr = &d;
ptr->_a++;
return 0 ;
}
先取到偏移量,计算 _a 在对象中的地址,再访问 。
继承和组合 class C {
class D : public C {};
class E { private : C _cc; };
继承:白箱复用 ,父类的内部细节对子类可见;子类对象可以访问父类的 public、protected;父类和子类的耦合度高。改变父类的成员,子类要跟着改。
组合:黑箱复用 ,对象的内部细节不可见;E 类只能用 C 类的 public 和定义了友元的 protected;耦合度低。
实践中多用组合。继承和组合都可以,就用组合;更适合用继承的用继承;实现多态,必须用继承。
public 继承是 is-a 的关系:每个子类对象一定是父类对象。
e.g.:教师、学生都是人。
组合是 has-a 的关系:E 组合了 C,每个 E 对象中都有一个 C 对象。
e.g.:E 是车,C 是轮胎,每个车都有轮胎。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online