跳到主要内容 C++ 继承机制详解 | 极客日志
Python
C++ 继承机制详解 继承的概念及定义 1.1 继承的概念 在社会关系中,一个人往往拥有不同的身份,基于不同身份拥有不同的信息。例如作为学生有学号,作为工人有工号,但基于'人'这一根本身份,他们都拥有唯一的姓名、性别等属性。 在面向对象程序设计中,若要模拟这种不同身份并存储对应信息,难道在实现'人''学生''工人'这些类时,每个类都要重复定义姓名、年龄、性别等成员吗?显然,这些通用信息应基于'人'这一基础类进行复用,而…
Tesfly 发布于 2026/3/30 更新于 2026/4/12 83K 浏览1. 继承的概念及定义
1.1 继承的概念
在社会关系中,一个人往往拥有不同的身份,基于不同身份拥有不同的信息。例如作为学生有学号,作为工人有工号,但基于'人'这一根本身份,他们都拥有唯一的姓名、性别等属性。
在面向对象程序设计中,若要模拟这种不同身份并存储对应信息,难道在实现'人''学生''工人'这些类时,每个类都要重复定义姓名、年龄、性别等成员吗?显然,这些通用信息应基于'人'这一基础类进行复用,而学号、工号等个性化信息则在具体类中定义。C++ 的继承机制正是为了解决此类类层次的代码复用问题。
继承(inheritance) 是面向对象程序设计中实现代码复用 的重要手段。它允许我们在保持原有类特性的基础上进行扩展,增加成员函数和成员变量 ,从而产生新的类,称为派生类 (子类),被继承的类称为基类 (父类)。继承体现了面向对象程序设计的层次结构,反映了由简单到复杂的认知过程。
以往我们接触的多是函数层次 的代码复用,而继承属于类设计层次 的复用。父类的相关数据与行为特性会像生物学中的遗传一样被子类继承,同时子类可定义自身的特性。
在未使用继承时,设计 Student 和 Teacher 类会导致大量冗余代码:
class Student {
public :
void identity () { }
void study () { }
protected :
std::string _name = "peter" ;
std::string _address;
std::string _tel;
int _age = 18 ;
int _stuid;
};
class Teacher {
public :
void identity () { }
void teaching () { }
protected :
std::string _name = "张三" ;
int _age = 18 ;
std::string _address;
std::string _tel;
std::string _title;
};
通过继承,将公共成员提取至 Person 基类中,Student 和 Teacher 继承 Person 即可实现复用:
class Person {
public :
void identity () { std::cout << "void identity() " << _name << std::endl; }
protected :
std::string _name = "张三" ;
std::string _address;
std::string _tel;
int _age = 18 ;
};
class Student : public Person {
public :
void study () { }
protected :
int _stuid;
};
class Teacher : public Person {
public :
void teaching () { }
protected :
std::string _title;
};
1.2 继承定义
1.2.1 定义格式 派生类定义格式为:class 派生类名 : 继承方式 基类名。继承方式与访问限定符一致,分为 public(公有)、protected(保护)和 private(私有)。
1.2.2 继承基类成员访问方式的变化
基类的 private 成员在派生类中无论以何种方式继承均不可见 。不可见是指基类的私有成员仍被继承到派生类对象中,但语法上限制派生类在类内或类外直接访问它。
若基类成员不希望被类外直接访问,但需在派生类中访问,应定义为 protected。protected 限定符正是因继承需求而引入的。
总结:基类的 private 成员在派生类中均不可见。其他成员的访问权限为 Min(基类成员访问限定符, 继承方式)。权限大小关系为:public > protected > private。
使用 class 关键字时默认继承方式为 private,使用 struct 时默认为 public。建议显式写出继承方式以提升代码可维护性。
实际开发中几乎只使用 public 继承 。protected/private 继承会限制成员仅在派生类内部使用,扩展性与维护性较差,不推荐使用。
基类成员访问限定符 public 继承 protected 继承 private 继承 public public protected private protected protected protected private private 不可见 不可见 不可见
class Person {
public :
void Print () { std::cout << _name << std::endl; }
protected :
std::string _name;
private :
int _age;
};
class Student : public Person {
protected :
int _stunum;
};
1.3 继承类模板 namespace zlr {
template <class T >
class stack : public std::vector<T> {
public :
void push (const T& x) {
std::vector<T>::push_back (x);
}
void pop () { std::vector<T>::pop_back (); }
const T& top () { return std::vector<T>::back (); }
bool empty () { return std::vector<T>::empty (); }
};
}
int main () {
zlr::stack<int > st;
st.push (1 );
st.push (2 );
st.push (3 );
while (!st.empty ()) {
std::cout << st.top () << " " ;
st.pop ();
}
return 0 ;
}
说明:虽然语法上子类继承了父类对象,但在编译查找时仍需到父类作用域中查找。由于类模板的按需实例化 机制,未调用的函数不会被实例化,直接调用 push_back() 会报错。必须通过 基类<T>::函数名() 显式调用,编译器才会实例化对应函数。构造与析构函数由编译器特殊处理并自动调用,因此无需显式指定。
2. 基类和派生类间的转换
派生类对象可赋值给基类指针/引用 (仅限 public 继承)。此过程称为切片(Slicing) ,即截取派生类对象中属于基类的部分,基类指针或引用指向该切片部分。
基类对象不能赋值给派生类对象 ,因为基类不包含派生类新增的成员数据。
基类指针/引用可通过强制类型转换赋值给派生类指针/引用 。仅当基类指针实际指向派生类对象时,该转换才是安全的。若基类为多态类型,可使用 RTTI 的 dynamic_cast 进行安全转换。
class Person {
public :
std::string _name;
std::string _sex;
int _age;
};
class Student : public Person {
public :
int _No;
};
int main () {
Student sobj;
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
rp._name = "张三" ;
Student* ps1 = dynamic_cast <Student*>(pp);
std::cout << ps1 << std::endl;
pp = &pobj;
Student* ps2 = dynamic_cast <Student*>(pp);
std::cout << ps2 << std::endl;
return 0 ;
}
注:赋值过程中未发生类型转换产生临时对象,编译器对切片操作进行了特殊处理。
3. 继承中的作用域
3.1 隐藏规则
继承体系中,基类与派生类拥有独立的作用域 。
若派生类与基类存在同名成员 ,派生类成员将屏蔽 基类同名成员的直接访问,此现象称为隐藏 (或重定义)。在派生类中可通过 基类名::成员名 显式访问基类成员。
对于成员函数,只要函数名相同即构成隐藏 ,与参数列表无关。
实际开发中,继承体系内应尽量避免定义同名成员,以防混淆。
class Person {
protected :
std::string _name = "小李子" ;
int _num = 111 ;
};
class Student : public Person {
public :
void Print () {
std::cout << " 姓名:" << _name << std::endl;
std::cout << " 身份证号:" << Person::_num << std::endl;
std::cout << " 学号:" << _num << std::endl;
}
protected :
int _num = 999 ;
};
3.2 作用域相关示例 问题 1: A 和 B 类中的两个 func 构成什么关系?
A. 重载 B. 隐藏 C. 没关系
答案: B. 隐藏。函数重载要求在同一作用域内,而父子类作用域独立,因此构成隐藏。
问题 2: 下面程序的编译运行结果是什么?
A. 编译报错 B. 运行报错 C. 正常运行
class A {
public :
void fun () { std::cout << "func()" << std::endl; }
};
class B : public A {
public :
void fun (int i) { std::cout << "func(int i) " << i << std::endl; }
};
int main () {
B b;
b.fun (10 );
b.fun ();
return 0 ;
}
解析: B::fun 隐藏了 A::fun。调用 b.fun() 时,编译器仅在 B 的作用域中查找,发现 B::fun(int) 需要参数,故编译报错。若需调用基类版本,必须使用 b.A::fun() 显式指定。
4. 派生类的默认成员函数
4.1 常见默认成员函数行为
构造函数 :派生类构造函数必须调用基类构造函数 以初始化基类部分。若基类无默认构造函数,则必须在派生类构造函数的初始化列表中显式调用。
拷贝构造函数 :派生类拷贝构造必须调用基类拷贝构造完成基类部分的拷贝。
赋值运算符重载 (operator=) :派生类 operator= 会隐藏基类的同名函数,需通过 基类::operator= 显式调用以完成基类部分的赋值。
析构函数 :派生类析构函数执行完毕后,会自动调用基类析构函数 。此机制保证了对象清理顺序为'先派生类,后基类'。
初始化与析构顺序 :构造时先基类后派生类;析构时先派生类后基类。
析构函数重写 :为支持多态,编译器会将析构函数名统一处理为 destructor。因此即使基类析构未加 virtual,派生类析构与基类析构在底层也构成隐藏关系。
注:若不涉及深拷贝,且基类提供了默认成员函数,通常可直接使用编译器生成的默认版本,编译器会自动处理基类部分的调用。
class Person {
public :
Person (const char * name = "xxx" ) : _name(name) { std::cout << "Person()" << std::endl; }
Person (const Person& p) : _name(p._name) { std::cout << "Person(const Person&)" << std::endl; }
Person& operator =(const Person& p) {
std::cout << "Person::operator=" << std::endl;
if (this != &p) _name = p._name;
return *this ;
}
~Person () { std::cout << "~Person()" << std::endl; }
protected :
std::string _name;
};
class Student : public Person {
public :
Student (const char * name, int num, const char * addr)
: Person (name), _num(num), _addr(addr) {}
Student (const Student& s) : Person (s), _num(s._num), _addr(s._addr) {}
Student& operator =(const Student& s) {
if (this != &s) {
Person::operator =(s);
_num = s._num;
_addr = s._addr;
}
return *this ;
}
~Student () {
}
protected :
int _num = 1 ;
std::string _addr = "西安市高新区" ;
};
4.2 实现一个不能被继承的类
C++98 方法 :将基类构造函数设为 private。派生类构造必须调用基类构造,但无法访问私有构造函数,从而阻止继承。
C++11 方法 :使用 final 关键字修饰基类,禁止被继承。
class Base final {
public :
void func5 () { std::cout << "Base::func5" << std::endl; }
protected :
int a = 1 ;
};
5. 继承与友元 友元关系不能被继承 。基类的友元函数无法访问派生类的私有或保护成员。若需访问,必须在派生类中重新声明该友元。
class Student ;
class Person {
public :
friend void Display (const Person& p, const Student& s) ;
protected :
std::string _name;
};
class Student : public Person {
protected :
int _stuNum;
};
void Display (const Person& p, const Student& s) {
std::cout << p._name << std::endl;
}
6. 继承与静态成员 若基类定义了 static 静态成员,则整个继承体系中仅存在该成员的一个实例 。无论派生出多少个子类,所有基类与派生类对象共享同一份静态成员。
class Person {
public :
std::string _name;
static int _count;
};
int Person::_count = 0 ;
class Student : public Person {
protected :
int _stuNum;
};
int main () {
Person p;
Student s;
std::cout << &p._name << " " << &s._name << std::endl;
std::cout << &p._count << " " << &s._count << std::endl;
std::cout << Person::_count << " " << Student::_count << std::endl;
return 0 ;
}
7. 多继承及其菱形继承问题
7.1 继承模型
单继承 :派生类仅有一个直接基类。
多继承 :派生类有两个或以上直接基类。内存布局中,先继承的基类成员在前,后继承的在后,派生类自身成员在最后。
菱形继承 :多继承的特殊情况。会导致数据冗余 与二义性 问题。例如 Assistant 继承自 Student 和 Teacher,而两者均继承自 Person,导致 Assistant 中包含两份 Person 成员。现代语言(如 Java)通过禁止多继承规避此问题,C++ 开发中也应尽量避免设计菱形继承。
class Person { public : std::string _name; };
class Student : public Person { protected : int _num; };
class Teacher : public Person { protected : int _id; };
class Assistant : public Student, public Teacher { protected : std::string _majorCourse; };
int main () {
Assistant a;
a.Student::_name = "xxx" ;
a.Teacher::_name = "yyy" ;
return 0 ;
}
7.2 虚继承 为解决菱形继承的数据冗余与二义性,C++ 引入 virtual 关键字实现虚继承 。在中间基类(Student、Teacher)继承顶层基类(Person)时加上 virtual,编译器会将共享的基类部分合并为一份,并单独存放在最终派生类中。
class Person { public : std::string _name; };
class Student : virtual public Person { protected : int _num; };
class Teacher : virtual public Person { protected : int _id; };
class Assistant : public Student, public Teacher { protected : std::string _majorCourse; };
int main () {
Assistant a;
a._name = "peter" ;
return 0 ;
}
注意 :虚继承会引入虚基类指针(vbptr)和虚基类表(vbtable),增加内存开销与访问复杂度。仅在必要时使用。
虚继承下的构造函数调用 :
在菱形虚继承中,最终派生类(Assistant)负责直接初始化虚基类(Person)。中间基类(Student、Teacher)构造函数中对 Person 的调用会被编译器忽略。
class Person {
public :
Person (const char * name) : _name(name) {}
std::string _name;
};
class Student : virtual public Person {
public :
Student (const char * name, int num) : Person (name), _num(num) {}
protected : int _num;
};
class Teacher : virtual public Person {
public :
Teacher (const char * name, int id) : Person (name), _id(id) {}
protected : int _id;
};
class Assistant : public Student, public Teacher {
public :
Assistant (const char * n1, const char * n2, const char * n3)
: Person (n3), Student (n1, 1 ), Teacher (n2, 2 ) {}
protected : std::string _majorCourse;
};
int main () {
Assistant a ("张三" , "李四" , "王五" ) ;
return 0 ;
}
7.3 多继承中的指针偏移 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 指向 Derive 起始地址(即 Base1 部分),p2 指向 Base2 部分起始地址(存在偏移),p3 指向 Derive 起始地址。因此 p1 == p3 != p2。
7.4 IO 库中的菱形虚继承 C++ 标准库的 IO 流体系采用了菱形虚继承结构:
template <class CharT , class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits> {};
template <class CharT , class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits> {};
basic_iostream 同时继承 basic_istream 和 basic_ostream,通过虚继承共享唯一的 basic_ios 基类实例。
8. 继承和组合
8.1 核心区别
继承(is-a) :派生类对象是 一种基类对象。属于白箱复用 ,基类内部细节对派生类可见,耦合度高,基类修改易影响派生类。
组合(has-a) :新类对象包含 另一个类的对象。属于黑箱复用 ,仅通过公开接口交互,内部细节不可见,耦合度低,易于维护。
设计原则 :优先使用组合,而非继承。仅在明确存在 is-a 关系或需要实现多态时使用继承。若两者皆可,优先选择组合。
class Tire {
protected :
std::string _brand = "Michelin" ;
size_t _size = 17 ;
};
class Car {
protected :
std::string _colour = "白色" ;
std::string _num = "陕ABIT00" ;
Tire _t1, _t2, _t3, _t4;
};
class BMW : public Car {
public :
void Drive () { std::cout << "好开-操控" << std::endl; }
};
template <class T >
class Stack {
public :
void push (const T& x) { _v.push_back (x); }
private :
std::vector<T> _v;
};
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,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
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online