C++ 继承入门 1/2:访问权限、切片、隐藏与子类默认成员函数一次理清
哈喽大家好,STL 系列之外,咱们补一篇面向对象的基础——继承。
继承是 C++ 面向对象三大特性之一,也是后面多态的地基。这篇用 Person(人)和 Student(学生)两个类贯穿始终,把继承格式、三种访问方式、切片、函数隐藏,以及子类那几个"默认成员函数到底怎么调父类"的坑一次讲清。口语化讲解,代码可直接编译,放心食用~
目录
- 一、什么是继承:在已有类上扩展
- 二、先补一个访问限定符:protected
- 三、继承格式与三种继承方式
- 3.1 继承格式:冒号是标志
- 3.2 权限怎么变:Min 规则
- 3.3 private 成员:被继承了,但看不见
- 3.4 不写继承方式时的默认值
- 四、切片:子类可以赋给父类,反过来不行
- 五、函数隐藏:同名就自动屏蔽父类
- 六、子类的默认成员函数:怎么调父类
- 6.1 默认构造函数
- 6.2 拷贝构造函数
- 6.3 赋值运算符重载
- 6.4 析构函数:最特殊的一个
- 七、类模板继承:别忘了指定类域
- 八、is-a 与 has-a:留个小悬念
- 九、总结
一、什么是继承:在已有类上扩展
继承允许我们在保持原有类特性的基础上进行扩展,增加自己的方法(成员函数)和属性(成员变量),从而复用代码、建立类与类之间的层次关系。
被继承的一方叫父类(基类),继承的一方叫子类(派生类)。本篇用这两个角色贯穿:
Person(人):有姓名、年龄,会打印信息;Student(学生):是一种人,额外有学号。
学生"是一种"人,所以让 Student 继承 Person,就能白拿 Person 的姓名、年龄和打印能力,只需要补上学号。
二、先补一个访问限定符:protected
在封装阶段我们只学过public和private,继承里多出一个新成员:protected。
它的权限强度介于两者之间:
| 限定符 | 类内部 | 类外部 | 派生类内部 |
|---|---|---|---|
public | 可访问 | 可访问 | 可访问 |
protected | 可访问 | 不可访问 | 可访问 |
private | 可访问 | 不可访问 | 不可访问 |
一句话记住:protected 对类外保密,但对自己的子类开放。它就是专门为继承准备的"家族内部可用、外人免进"权限。
三、继承格式与三种继承方式
3.1 继承格式:冒号是标志
classPerson{public:voidPrint(){cout<<_name<<endl;}protected:string _name;private:int_age;};// class Student : protected Person // 保护继承// class Student : private Person // 私有继承classStudent:publicPerson// 公有继承(最常用){protected:int_stunum;// 学号,子类自己新增的属性};看到类名后面的冒号了吗?冒号就是继承的标志,冒号后面写父类名和继承方式。
3.2 权限怎么变:Min 规则
继承方式决定了:父类的成员,在子类里变成什么访问权限,也决定了子类对象、子类派生链能不能访问这些成员。
除了 private 成员单独处理外,父类其他成员在派生类中的最终访问权限,等于取两者的较小值:
派生类中的访问权限 = Min(成员在基类的访问限定符, 继承方式)
把 public 看作 3、protected 看作 2、private 看作 1,谁小取谁:
| 基类成员 \ 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可见 | 不可见 | 不可见 |
可以看到:public 继承最"宽松",成员权限原样保留;private 继承最"严",父类成员到子类全部降级为 private。
3.3 private 成员:被继承了,但看不见
有个容易误解的点:基类的 private 成员,无论以什么方式继承,在派生类中都是"不可见"的。
注意"不可见"不等于"不存在"——父类的私有成员仍然被继承进了子类对象的内存里,只是语法上限制:不管在子类类内还是类外,都不能直接访问它。子类想用,得通过父类提供的 public/protected 接口间接拿。
3.4 不写继承方式时的默认值
有时能看到省略继承方式的写法:
classStudent:Person{};// class 默认 private 继承structStudent:Person{};// struct 默认 public 继承规则和声明类本身的默认权限一致:用class默认是 private 继承,用struct默认是 public 继承。
但默认值只是编译器兜底,实际开发中最好显式写出继承方式,一眼看清意图,也避免误读。
四、切片:子类可以赋给父类,反过来不行
在公有继承(public)前提下,发生"子类赋值给父类"时会出现切片(切割),支持三种载体:对象、指针、引用。
Student s;Person p;p=s;// 对象切片:把子类对象赋给父类对象Person*ptr=&s;// 指针切片:父类指针指向子类对象Person&ref=s;// 引用切片:父类引用绑定子类对象普通赋值会产生临时变量再拷贝,而切片相当于直接把子类中属于父类的那一部分切出来把父类的部分拷贝再赋给父类,多余的子类部分(比如学号)被"切掉"丢弃。
两条铁律:
- ✅子类可以赋值给父类(对象 / 指针 / 引用都行);
- ❌父类不能赋值给子类——父类里压根没有子类新增的成员,强行转过去就是非法访问。
切片是后面多态的基础:父类指针/引用能指向子类对象,再配合虚函数就能表现出不同形态。这里先记住"子赋父可以,父赋子不行"。
五、函数隐藏:同名就自动屏蔽父类
当子类的成员函数和父类成员函数同名(不管参数是否相同),子类会自动屏蔽父类的同名函数,这就叫隐藏(name hiding)。
classPerson{public:voidPrint(){cout<<"Person"<<endl;}};classStudent:publicPerson{public:voidPrint(){cout<<"Student"<<endl;}// 隐藏了父类的 Print};intmain(){Student s;s.Print();// 调的是子类的 Print,父类的被隐藏了s.Person::Print();// 想调父类的,必须显式写父类类域}想调用被隐藏的父类函数,要显式写出父类名加类域s.Person::Print()。
实用建议:尽量不要在子类里定义和父类同名的成员函数,否则容易踩隐藏的坑,也降低可读性。真要重写逻辑,那是多态里"虚函数重写"的话题,和这里的隐藏不是一回事,别混淆。
六、子类的默认成员函数:怎么调父类
类有六个默认成员函数,继承后它们和父类的关系是面试高频,逐个说。
6.1 默认构造函数
子类的默认构造函数里,继承自父类的成员被视为一个整体,会要求调用父类的默认构造函数来初始化这一部分。
- 如果父类有默认构造,编译器自动帮你调;
- 如果父类没有默认构造,就必须在子类的初始化列表里显式声明调用父类构造:
classPerson{public:Person(conststring&name):_name(name){}// 没有默认构造private:string _name;};classStudent:publicPerson{public:// 父类无默认构造,必须在初始化列表显式调用Student(conststring&name,intstunum):Person(name)// 先构造父类部分,_stunum(stunum){}private:int_stunum;};6.2 拷贝构造函数
规则和默认构造一致:子类拷贝构造时,继承自父类的部分要调用父类的拷贝构造。
显式写子类拷贝构造时,把子类对象传给父类拷贝构造,传参过程中会自动发生切片,只拷贝父类那部分:
Student(constStudent&s):Person(s)// 传子类对象,父类拷贝构造自动切片,_stunum(s._stunum){}只有当类里需要深拷贝资源时才需要自己写拷贝构造;普通浅拷贝用编译器默认生成的就够。
6.3 赋值运算符重载
规则和拷贝构造基本一样:显式写子类operator=时,要显式调用父类的赋值运算符来处理父类部分。
注意:子类和父类的operator=也构成隐藏,所以调用父类版本必须显式写父类类域:
Student&operator=(constStudent&s){if(this!=&s){Person::operator=(s);// 显式调父类赋值,s 在此切片_stunum=s._stunum;}return*this;}6.4 析构函数:最特殊的一个
析构和前面几个规则都不同,这里有个常见误区:
// 错误示范:不能这样手动调父类析构~Student(){~Person();// 语法报错,也调不动父类析构}正确规则:子类析构不需要也不能显式调用父类析构。子类析构执行完后,编译器会自动调用父类析构,保证析构顺序是先子后父(构造是先父后子,析构正好反过来)。
父子析构函数同样构成隐藏关系,但因为有自动调用机制,我们不用手动管。至于为什么析构常常要写成虚函数,那是多态篇的内容,先埋个伏笔。
七、类模板继承:别忘了指定类域
继承不仅能继承普通类,也能继承类模板。这里有个编译坑。
比如想用继承实现一个 stack,让它继承std::vector:
template<classT>classstack:publicstd::vector<T>{public:voidpush(constT&x){// 必须指定类域 vector<T>::,否则报 C3861 找不到标识符vector<T>::push_back(x);// push_back(x); // 这样写会报错}voidpop(){vector<T>::pop_back();}constT&top(){returnvector<T>::back();}};为什么直接写push_back(x)会报 C3861"找不到标识符"?
因为基类是类模板,模板是按需实例化的:stack<int>实例化时虽然也实例化了vector<int>,但push_back这些成员函数在没被用到时并不会实例化。编译器在 stack 类域里直接找push_back,找不到就报错。
解决办法:显式指定基类类域vector<T>::push_back(x),告诉编译器这个函数来自基类模板。
八、is-a 与 has-a:留个小悬念
上面用继承实现 stack,其实和之前讲过的"容器适配器(组合/包装 vector)"在功能上很像——都是在底层调用 vector 的成员函数。
那继承和组合到底怎么选?这里先给两个概念:
- is-a(是一个):子类"是一种"父类,比如 Student 是一种 Person,适合用继承;
- has-a(有一个):类里"包含一个"别的对象作为成员,比如 stack 内部持有一个 vector,适合用组合。
stack 和 vector 的关系更接近 has-a,所以标准库的 stack 用的是组合(容器适配器)而不是公开继承。这个区别后面讲适配器和多态时再展开,感兴趣可以先自己琢磨。
九、总结
- 继承:在父类基础上扩展,父类=基类,子类=派生类;
protected对类外保密、对子类开放。 - 三种继承方式:public / protected / private,成员最终权限取 Min(基类权限, 继承方式);private 成员始终不可见但仍在对象里;class 默认 private、struct 默认 public,建议显式写。
- 切片:public 继承下子类可赋给父类(对象/指针/引用),父类不能赋给子类。
- 函数隐藏:子类与父类同名函数自动屏蔽父类,调用父类需显式写类域,尽量避免同名。
- 默认成员函数:构造/拷贝构造/赋值重载都要在初始化列表显式调父类对应版本;析构特殊,自动先子后父,不能手动调。
- 类模板继承:基类成员要加
vector<T>::类域,避免 C3861。 - 选型上分清is-a(继承)和has-a(组合)。
把 Person/Student 这套例子自己敲一遍,再故意写一次切片和隐藏观察结果,继承这关就稳了。
如果这篇对你有帮助,别忘了点赞收藏关注三连~ 咱们下篇见!
参考:C++ 继承体系、访问控制、对象切片、名称隐藏与派生类默认成员函数规则