一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

C++继承从入门到踩坑:这些隐藏规则很多人都会搞混

时间:2026-08-11 11:29:49 编辑:袖梨 来源:一聚教程网

C++继承从入门到踩坑:这些隐藏规则很多人都会搞混需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。

一、继承基础:先搭个架子

继承的概念

C++继承从入门到踩坑:这些隐藏规则很多人都会搞混

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用

最基础的继承,就是一个 Person 类作为基类,StudentTeacher 分别派生:

#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 成员在子类中
publicpublicprotected
protectedprotectedprotected
privateprivateprivate

日常开发里 90% 以上都是 public 继承,后两种知道就行,实际用得很少。

二、模板 + 继承:用容器适配一个栈

下面这个例子把继承和模板揉在一起——用 vectorlist 作为底层容器,通过继承来适配一个简易栈:

#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 从哪来,直接报错。这是模板继承里最容易踩的坑。

第二,通过宏切换 vectorlist。 用宏控制底层容器,同一个栈模板可以适配不同数据结构,灵活性一下就上来了。但宏只是编译期文本替换,没有类型检查,生产环境建议用模板参数来指定容器类型。

三、切片赋值:子类对象能赋给父类,反过来不行

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) 这行很有意思——xconst Student& 类型,但 Person 的拷贝构造接收 const Person&。编译器在这里做了一次切片:只取 Student 对象中属于 Person 的那部分来调用父类拷贝构造。

6.3 赋值运算符重载

子类和父类的 operator= 构成隐藏关系。所以子类内部想调用父类的赋值重载,必须写成:

Person::operator=(s);

直接写 operator=(s) 会递归调用自己,直接栈溢出。

6.4 析构函数

同样构成隐藏。子类析构里显式调 Person::~Person() 来释放父类资源。不过这里要注意:真正的 C++ 运行时会自动调用父类析构,子类析构执行完后,父类析构会被自动触发。显式调用反而可能导致父类析构被执行两次,引发问题。但因为这里只是打印日志做演示,所以无伤大雅——实际写代码时,不要显式调用父类析构函数

七个关键结论(速查表)

场景结论
子类调用父类非虚函数父类名::函数名() 显式调用
子类对象赋给父类指针/引用✅ 合法(切片)
父类对象赋给子类对象❌ 编译报错
同名成员变量子类隐藏父类,访问父类需加作用域
同名成员函数(参数不同)隐藏,不是重载!
赋值重载在父子间构成隐藏,调用需加 父类名::operator=()
模板类继承模板基类访问基类成员必须加 基类<T>:: 限定

写在最后

C++ 的继承机制,初看就是"子类能访问父类的东西",但真到了同名隐藏、切片赋值、模板继承这些细节上,稍不注意就会写出 bug。我的建议是:别光看,一定要动手敲。 把上面这些例子复制到 VS 里跑一遍,逐行设断点看调用栈,理解会深很多。

热门栏目