跳到主要内容
C++ 继承机制详解 | 极客日志
C++
C++ 继承机制详解 C++ 继承涉及基类与派生类的复用关系,包含访问控制、对象切片、作用域隐藏等核心机制。内容涵盖构造、拷贝、赋值及析构函数的调用规则,友元与静态成员在继承中的行为,多继承导致的菱形二义性问题及虚拟继承方案,最后对比了继承与组合的应用差异。
全栈工匠 发布于 2026/3/26 更新于 2026/4/23 2 浏览一、概念
继承是类设计层次的复用。
**语法:**Person 是父类,也称作基类。Student 是子类,也称作派生类。
继承关系和访问限定符
继承以后,保护和私有不一样了:
不可见:基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面,都不能去访问它。基类的私有成员在基类中还是能用,在基类外不能用。
如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就把基类成员定义为 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 : public Person {
protected :
int _stuid;
};
class Teacher : public Person {
protected :
int _jobid;
};
父类对象不能赋值给子类对象,强转也不行 :否则子类特有的成员变量怎么办,给随机值?
父类对象可以赋值给子类的指针、引用(后面说) 。
int main () {
Person p; Student s;
p = s;
Person p1 = s;
;
}
return
0
子类对象可以赋值给父类的对象、指针、引用 。
子类赋值给父类对象,语法上特殊处理,没有发生类型转换,而是赋值兼容(切割、切片) 。把子类里,父类那部分切出来,拷贝给父类。
怎么证明没有发生类型转换?用另一种语法。
p2 变成子类中,父类那一部分的别名。
三、继承中的作用域 域都是编译时,查找规则的概念 。
定义了一个类,就有类域。子类和父类有独立的类域。
子类 和父类 中有同名成员 ,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏 ,也叫重定义 。(在子类成员函数中,可以使用父类::父类成员 显示访问)。
父子类域中,如果是成员函数的隐藏 ,只需要函数名相同 就构成隐藏 。
现实中尽量不要在继承体系里定义同名成员 。
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)
{ }
protected :
int _id;
};
int main () {
Student s;
return 0 ;
}
不加 19 行会报错,19 行的写法很特殊。
调试发现,在初始化列表先走 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 次析构函数。
屏蔽 59 行反而正确了。
构造、拷贝、赋值要显示调用 。
析构函数是个特殊,为保证析构顺序,会自动调 。
子类对象分开来看,分为父类部分和子类部分。
显示调用父类析构,无法保证先子后父的析构顺序 。
所以子类析构函数完成后,自动调用父类析构,这样就保证了先子后父的析构顺序。
为什么要保证先子后父的析构顺序?因为子类中有可能用到父类成员 ,父类不可能用到子类。
eg:先析构父,子再访问父的成员。
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 成员实例,在子类中不会单独拷贝一份。
eg:↓ 统计 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 ;
}
虚拟继承解决数据冗余和二义性的原理 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 的关系:每个子类对象一定是父类对象。
eg:教师、学生都是人。
组合是 has-a 的关系:E 组合了 C,每个 E 对象中都有一个 C 对象。
eg:E 是车,C 是轮胎,每个车都有轮胎。
相关免费在线工具 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
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online