最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
C++继承从入门到踩坑:这些隐藏规则很多人都会搞混
时间:2026-08-11 11:29:49 编辑:袖梨 来源:一聚教程网
C++继承从入门到踩坑:这些隐藏规则很多人都会搞混需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。
一、继承基础:先搭个架子
继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用
最基础的继承,就是一个 Person 类作为基类,Student 和 Teacher 分别派生:
#include <iostream>#include <string>using namespace std;class Person{public: void identity() { cout << "void identity()" << _name << endl; }protected: string _name; // 姓名 string _address; // 地址 string _tel; // 电话};class Student : public Person{public: void study() { }protected: int _stuid; // 学号};class Teacher : public Person{public: void teaching() { }protected: string title; // 职称};int main(){ Student s; Teacher t; s.identity(); // 子类对象调用父类方法 t.identity(); return 0;}这个例子很直观:子类自动继承了父类的公有和保护成员,identity() 也能正常调用。但这里有一个容易被忽略的点——访问权限。
| 继承方式 | 父类 public 成员在子类中 | 父类 protected 成员在子类中 |
|---|---|---|
| public | public | protected |
| protected | protected | protected |
| private | private | private |
日常开发里 90% 以上都是 public 继承,后两种知道就行,实际用得很少。
二、模板 + 继承:用容器适配一个栈
下面这个例子把继承和模板揉在一起——用 vector 或 list 作为底层容器,通过继承来适配一个简易栈:
#include <iostream>#include <vector>#include <list>using namespace std;// 通过宏切换底层容器:vector 或 list// #define container std::vector#define container std::listtemplate<class T>class stack : public container<T>{public: void push(const T& x) { // 必须显式指定父类作用域,否则编译器找不到 push_back container<T>::push_back(x); } void pop() { container<T>::pop_back(); } const T& top() { return container<T>::back(); } bool empty() { return container<T>::empty(); }};int main(){ box::stack<int> st; // 假设 stack 定义在 box 命名空间内 st.push(1); st.push(2); st.push(3); while (!st.empty()) { cout << st.top() << " "; st.pop(); } // 输出:3 2 1 return 0;}这里有两个关键点:
第一,为什么要写 container<T>::push_back(x) 而不是直接 push_back(x)?
这跟 C++ 的"按需实例化"机制有关。编译器在解析模板时,不会立刻去父类(依赖模板参数的基类)里查找名字。你不加作用域限定,编译器就不知道 push_back 从哪来,直接报错。这是模板继承里最容易踩的坑。
第二,通过宏切换 vector 和 list。 用宏控制底层容器,同一个栈模板可以适配不同数据结构,灵活性一下就上来了。但宏只是编译期文本替换,没有类型检查,生产环境建议用模板参数来指定容器类型。
三、切片赋值:子类对象能赋给父类,反过来不行
class Person{protected: string _name; string _sex; int _age;};class Student : public Person{public: int _No; // 学号};int main(){ Student sobj; // ✅ 子类对象可以赋值给基类的指针或引用 Person* pp = &sobj; Person& rp = sobj; // ✅ 子类对象赋值给基类对象——通过拷贝构造完成 Person pobj = sobj; // ❌ 基类对象不能赋值给子类对象,编译直接报错 // sobj = pobj; return 0;}这段代码展示了 C++ 继承里一个非常重要的特性——切片(slicing)。
当你把 Student 对象赋给 Person 对象时,编译器只拷贝 Person 的那部分成员,Student 独有的成员(比如 _No)会被"切掉"丢弃。这在 C++98 里是完全合法的,但也是很多隐蔽 bug 的根源——你以为传的是子类对象,实际上函数只拿到了父类的那一半。
反过来就不行了:sobj = pobj 会编译报错,因为基类对象天然缺了子类特有的那一部分,编译器不知道怎么填补。
四、同名成员隐藏:子类变量遮住了父类的
class Person{protected: string _name; int _num; // 身份证号(父类)};class Student : public Person{public: void Print() { cout << _name << endl; // 没问题,_name 只在父类定义 cout << Person::_num << endl; // 访问父类的 _num cout << _num << endl; // 访问子类的 _num }protected: int _num; // 学号(子类),和父类的 _num 同名};int main(){ Student s1; s1.Print(); return 0;}输出结果:
xiaoming
111
999
子类和父类有同名成员变量(_num),子类会隐藏父类的同名成员。你直接写 _num,编译器默认找的是子类自己的那个。想访问父类的版本,必须显式加作用域:Person::_num。
这个规则也适用于成员函数,下面的例子会进一步说明。
五、函数隐藏 vs 重载:90% 的人都会搞混
来看一个经典的面试题:
class A{public: void fun() { cout << "func()" << endl; }};class B : public A{public: void fun(int i) { cout << "func(int i)" << i << endl; }};int main(){ B b; b.fun(10); // ✅ 调用 B::fun(int) // b.fun(); // ❌ 编译报错!父类的 fun() 被隐藏了 return 0;}问:A 和 B 中的两个 fun 构成什么关系?
很多人第一反应是"重载"——毕竟参数列表不同嘛。但答案是:隐藏(hide)。
为什么不是重载?因为重载要求两个函数在同一个作用域里。而父类和子类分属不同作用域,子类的 fun(int) 把父类的 fun() 给隐藏了。编译器在 B 的作用域里找到了 fun 这个名字,就不再往父类里找了,哪怕参数不匹配也不会回退。
想调用父类的版本,同样需要显式指定:
int main(){ B b; b.A::fun(); // ✅ 显式调用父类的 fun() return 0;}一句话总结:只要函数名相同,子类就会隐藏父类的所有同名函数,管你参数一不一样。
六、六大默认成员函数:子类到底继承了什么?
这是 C++ 继承里最核心的一块——构造函数、拷贝构造、赋值重载、析构函数在继承链里到底怎么运作?
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 num, const char* address) : Person(name) // 显式调用父类构造函数 , _num(num) , _address(address) { _ptr = new int(666); } // 拷贝构造 Student(const Student& x) : Person(x) // 切片:把子类对象当父类引用传进去 , _num(x._num) , _address(x._address) { _ptr = x._ptr; // 浅拷贝(演示用,实际需要深拷贝) } // 赋值重载 Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s); // 必须显式调用,否则被隐藏 _num = s._num; _address = s._address; } return *this; } // 析构函数 ~Student() { Person::~Person(); // 显式调用父类析构 }protected: int _num; string _address; int* _ptr;};int main(){ Student s("xiaoming", 12, "chengdu"); Student s2(s); // 拷贝构造 Student s3("lisi", 12, "meiguo"); s = s3; // 赋值重载 return 0;}这段代码里藏了很多细节,逐个拆开看:
6.1 构造函数
子类构造函数必须在初始化列表中调用父类构造函数。如果不写,编译器会偷偷帮你调父类的默认构造函数——但如果父类没有默认构造函数,编译直接报错。
Student 的构造里写的是 : Person(name),这就是显式调用父类的带参构造。
6.2 拷贝构造
初始化列表里 : Person(x) 这行很有意思——x 是 const Student& 类型,但 Person 的拷贝构造接收 const Person&。编译器在这里做了一次切片:只取 Student 对象中属于 Person 的那部分来调用父类拷贝构造。
6.3 赋值运算符重载
子类和父类的 operator= 构成隐藏关系。所以子类内部想调用父类的赋值重载,必须写成:
Person::operator=(s);
直接写 operator=(s) 会递归调用自己,直接栈溢出。
6.4 析构函数
同样构成隐藏。子类析构里显式调 Person::~Person() 来释放父类资源。不过这里要注意:真正的 C++ 运行时会自动调用父类析构,子类析构执行完后,父类析构会被自动触发。显式调用反而可能导致父类析构被执行两次,引发问题。但因为这里只是打印日志做演示,所以无伤大雅——实际写代码时,不要显式调用父类析构函数。
七个关键结论(速查表)
| 场景 | 结论 |
|---|---|
| 子类调用父类非虚函数 | 用 父类名::函数名() 显式调用 |
| 子类对象赋给父类指针/引用 | ✅ 合法(切片) |
| 父类对象赋给子类对象 | ❌ 编译报错 |
| 同名成员变量 | 子类隐藏父类,访问父类需加作用域 |
| 同名成员函数(参数不同) | 隐藏,不是重载! |
| 赋值重载在父子间 | 构成隐藏,调用需加 父类名::operator=() |
| 模板类继承模板基类 | 访问基类成员必须加 基类<T>:: 限定 |
写在最后
C++ 的继承机制,初看就是"子类能访问父类的东西",但真到了同名隐藏、切片赋值、模板继承这些细节上,稍不注意就会写出 bug。我的建议是:别光看,一定要动手敲。 把上面这些例子复制到 VS 里跑一遍,逐行设断点看调用栈,理解会深很多。
相关文章
- 如何高效撰写意识形态工作汇报?一份详细的范文与提示词供你参考! 08-11
- AI图表,如何颠覆数据展示的未来 08-11
- 《逐出》第八关山顶介绍 08-11
- 如何将TXT数据转化为AI视觉的秘密武器 08-11
- AI仿写工具如何样助力企业在竞争中取得优势 08-11
- Zibrary中文官网入口 08-11