【C++ 基础入门】C++ 继承(上):概念、切片、隐藏、默认成员函数与友元

👋 欢迎阅读

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容: 《C++学习》

📑 目录


一、前置讲解

在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。

🧠 前置知识一:继承(Inheritance)

类设计层面的复用:在保持原有类特性的基础上扩展,复用成员变量 + 成员函数,形成层次结构。

✂️ 前置知识二:切片(Slicing)

派生类对象赋给基类的对象 / 指针 / 引用 时,只把属于基类的那一部分"切"出来。

🙈 前置知识三:隐藏(重定义)

基类与派生类有同名成员 时,派生类成员屏蔽 基类成员;靠 基类::成员 显式访问。

🏗️ 前置知识四:派生类默认成员函数

构造、拷贝构造、赋值重载都要显式处理基类那部分 ;析构顺序先子后父。

🔗 前置知识五:友元与静态成员的继承规则

友元关系不能继承 ;而静态成员整个继承体系只有一份。


二、继承的概念及定义

2.1 继承的概念

继承机制 是面向对象程序设计使代码可以复用的最重要的手段 :它允许程序员在保持原有类特性 的基础上进行扩展、增加新功能,由此产生的新类称为派生类(子类)。

继承体现了面向对象程序设计的层次结构 ,反映了从简单到复杂的认知过程。之前我们接触的复用主要是函数复用 ,而继承是类设计层面的复用。

cpp 复制代码
class Person
{
public:
    void Print()
    {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
protected:
    string _name = "peter";   // 姓名
    int _age = 18;            // 年龄
};
​
// 继承后父类 Person 的成员(成员函数 + 成员变量)都会变成子类的一部分。
// 这里体现出了 Student 和 Teacher 复用了 Person 的成员。
​
class Student : public Person
{
protected:
    int _stuid;   // 学号
};
​
class Teacher : public Person
{
protected:
    int _jobid;   // 工号
};

2.2 继承定义

Person 是父类,也称基类;Student 是子类,也称派生类。

继承方式与访问限定符共同决定:基类成员在派生类中的访问权限。

基类成员 \ 继承方式 public 继承 protected 继承 private 继承
public 成员 public protected private
protected 成员 protected protected private
private 成员 不可见 不可见 不可见

总结规则:

序号 规则
1 基类的 private 成员 在派生类中无论以什么方式继承,都是不可见的 。这里的"不可见"是指:基类的私有成员确实被继承到了派生类对象中 ,但语法上限制了派生类对象(不管类内还是类外)都不能访问它 ------并不是说它不存在,这一点需要牢记
2 基类的 private 成员在派生类中不能访问。如果基类成员不想在类外被直接访问、但又需要在派生类中能被访问,就应该定义为 protected ------可以看出,protected 在很大程度上是因继承才出现的
3 结合表格总结:基类的私有成员在派生类中始终不可见 ;基类其他成员在派生类中的访问方式,可以归纳为 访问权限 = min(基类中该成员的访问限定符, 继承方式) ,权限大小关系为 public > protected > private
4 用 class 关键字时默认继承方式是 private ;用 struct 时默认继承方式是 public 。不过最好总是显式写出继承方式,避免混淆
5 实际开发中一般都用 public 继承 ,几乎很少用 protected / private 继承 ,也不提倡:因为这两种继承下来的成员只能在派生类内部使用

三、基类和派生类对象赋值转换(切片)

派生类对象可以赋值给基类的对象、基类的指针或基类的引用。 这里有个形象的说法叫**"切片"或"切割"** ,寓意是将派生类对象中属于基类的那一部分"切"出来,然后进行赋值或指向操作。

操作 能否 说明
派生类对象 → 基类对象 可以 切片:只拷贝基类那部分,派生类新增成员被丢掉
派生类对象 → 基类指针 / 引用 可以 指针 / 引用指向的是派生类对象中的基类部分
基类对象 → 派生类对象 不可以 基类对象里没有派生类新增的成员,无法"凭空造出来"
基类指针 / 引用 → 派生类指针 / 引用 强制转换后可以 但只有该基类指针实际指向的是派生类对象时,这种转换才是安全的
cpp 复制代码
class Person   // 父类
{
protected:
    string _name;   // 姓名
    string _sex;    // 性别
    int _age;       // 年龄
};
​
class Student : public Person   // 子类
{
public:
    int _No;   // 学号
};
​
void Test()
{
    Student sobj;
​
    // 1. 子类对象可以赋值给父类对象/指针/引用
    Person pobj = sobj;
    Person* pp = &sobj;
    Person& rp = sobj;
​
    // 2. 基类对象不能赋值给派生类对象
    sobj = pobj;    // 编译报错
​
    // 3. 基类的指针可以通过强制类型转换赋值给派生类的指针
    //    但必须是基类的指针【指向派生类对象】时才是安全的
    pp = &sobj;                     // pp 指向的是子类
    Student* ps1 = (Student*)pp;    // 这种情况转换是可以的
    ps1->_No = 10;
​
    pp = &pobj;                     // pp 指向的是父类
    Student* ps2 = (Student*)pp;    // 这种情况转换虽然可以,但会存在越界访问的问题
    ps2->_No = 10;
}

运行结果:

复制代码
sobj = pobj;        编译报错:无法从 "Person" 转换为 "Student"
​
ps2->_No = 10;      运行时越界访问(pp 指向的 Person 对象里没有 _No 这块空间)

💡 判断安全性的口诀:"指针指向谁,就只能按谁的大小用" 。pp 实际指向 Person 对象时,把它当 Student* 用就等于按大房子去住小房子 ------_No 落在了对象外面,属于未定义行为。


四、继承中的作用域(隐藏)

序号 规则
1 在继承体系中,基类和派生类都有各自独立的作用域
2 当子类和父类有同名成员 时,子类成员会屏蔽父类对同名成员的直接访问 ,这叫隐藏(也称重定义) 。在子类成员函数中,可以通过 基类::基类成员 的方式显式访问被隐藏的基类成员
3 注意:如果是成员函数的隐藏,只需要函数名相同即可构成隐藏 ,与参数列表无关 。这一点要和重载 区分:重载要求同一作用域内且函数参数不同 ,而隐藏发生在基类和派生类之间,函数名相同即触发
4 实际开发中,继承体系里最好不要定义同名成员,避免混淆和误用
cpp 复制代码
// Student 的 _num 和 Person 的 _num 构成隐藏关系
// 可以看出这样代码虽然能跑,但非常容易混淆
class Person
{
protected:
    string _name = "小李子";   // 姓名
    int _num = 111;            // 身份证号
};
​
class Student : public Person
{
public:
    void Print()
    {
        cout << " 姓名:" << _name << endl;
        cout << " 身份证号:" << Person::_num << endl;   // 显式访问被隐藏的基类成员
        cout << " 学号:" << _num << endl;                // 默认访问自己的 _num
    }
protected:
    int _num = 999;   // 学号
};
​
void Test()
{
    Student s1;
    s1.Print();
}

运行结果:

复制代码
姓名:小李子
身份证号:111
学号:999

重载和隐藏的区分:

cpp 复制代码
class A
{
public:
    void fun()
    {
        cout << "func()" << endl;
    }
};
​
class B : public A
{
public:
    // B 中的 fun 和 A 中的 fun 不是重载,因为不在同一作用域
    // B 中的 fun 和 A 中的 fun 构成隐藏:成员函数只要函数名相同就隐藏
    void fun(int i)
    {
        A::fun();                        // 显式调用被隐藏的基类版本
        cout << "func(int i)->" << i << endl;
    }
};
​
int main()
{
    B b;
    b.fun(10);
    // b.fun();      // 编译报错:基类的 fun() 被隐藏了
    b.A::fun();      // 显式访问
    return 0;
}

运行结果:

复制代码
func()
func(int i)->10
func()
对比项 重载(Overload) 隐藏(重定义)
发生位置 同一作用域内 基类和派生类之间(不同作用域)
条件 函数名相同 + 参数列表不同 函数名相同 即可(与参数无关)
调用 编译器按参数自动匹配 默认访问派生类版本,基类版本要用 基类:: 显式调
本质 同一层级里"多个版本" 内层作用域屏蔽外层同名名字

💡 隐藏最坑的地方:基类有 fun() 和 fun(int) 两个重载,派生类只写了一个 fun()------结果基类的 fun(int) 也被一起隐藏了 ,b.fun(10) 直接编不过。这跟"参数无关"那条规则是同一个道理。


五、派生类的默认成员函数

6 个默认成员函数 ,"默认"的意思是如果我们不写,编译器会自动生成一个。那么在派生类中,这几个函数是如何生成的呢?

默认成员函数 派生类中的规则
构造函数 必须调用基类的构造函数 来初始化基类那部分成员;如果基类没有默认构造函数 ,必须在派生类构造函数的初始化列表阶段显式调用
拷贝构造函数 必须显式或隐式地调用基类的拷贝构造函数,完成基类部分的拷贝初始化
赋值运算符重载 operator= 必须显式或隐式地调用基类的 operator=,完成基类部分的复制
析构函数 派生类析构执行完后,自动调用基类的析构函数清理基类成员

构造顺序 :先调用基类构造函数 初始化基类部分,再调用派生类自己构造函数初始化派生类新增成员(因为派生类初始化可能依赖基类成员,所以必须先完成基类构造)。

构造函数示例(基类没有默认构造函数):

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;
​
// 基类:没有默认构造函数
class Base
{
public:
    Base(const string& n)
        : name(n)
    {}
​
private:
    string name;
};
​
// 派生类
class Derived : public Base
{
public:
    // 必须在初始化列表中显式调用 Base 的构造函数
    Derived(const string& baseName, int v)
        : Base(baseName)     // 显式调用基类构造
        , value(v)
    {}
​
private:
    int value;
};
​
int main()
{
    Derived d("example", 42);   // 正确:提供了基类所需的参数
    return 0;
}

拷贝构造函数示例:

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Base
{
public:
    Base(const string& n)
        : name(n)
    {}

    Base(const Base& other)
        : name(other.name)
    {
        cout << "Base 拷贝构造" << endl;
    }

private:
    string name;
};

class Derived : public Base
{
public:
    // 普通构造函数
    Derived(const string& baseName, int v)
        : Base(baseName)
        , value(v)
    {}

    // 拷贝构造函数:必须显式调用 Base(const Base&)
    Derived(const Derived& other)
        : Base(other)        // 显式调用基类拷贝构造
        , value(other.value)
    {}

private:
    int value;
};

int main()
{
    Derived d1("example", 42);
    Derived d2 = d1;   // 触发拷贝构造
    return 0;
}

运行结果:

复制代码
Base 拷贝构造

拷贝构造的两种情况(重要):

情况 编译器行为 结果
一、没有自定义派生类拷贝构造 自动生成默认版本,会自动调用基类的拷贝构造函数拷贝基类部分 不用手写代码,但"调用基类拷贝构造"这个行为依然发生了
二、自定义了派生类拷贝构造 如果不在初始化列表显式调用基类拷贝构造 ,编译器会默认调用基类的默认构造函数(无参构造) ,而不是拷贝构造 通常导致逻辑错误 :基类部分没有被"拷贝",而是被"默认构造"了,与预期拷贝行为不一致

析构顺序:

阶段 顺序
构造 基类构造 → 派生类构造
析构 派生类析构 → 基类析构

💡 为什么先子后父 ?因为子类对象可能会用到父类的成员 ------如果先析构父类,子类析构时再访问父类成员就是访问已销毁对象。所以析构顺序必须和构造顺序相反。


六、继承与友元

友元关系不能继承 ------也就是说,基类友元不能访问子类私有和保护成员。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Student;   // 前向声明:Person 的友元声明里用到了 Student

class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name = "peter";   // 姓名
};

class Student : public Person
{
protected:
    int _stuNum = 1001;   // 学号
};

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;
}

运行结果:

复制代码
编译报错:无法访问 protected 成员(Student::_stuNum)

💡 一句话记:"爸爸的朋友不是我的朋友" 。友元是针对某个类 单独授予的权限,它不随继承关系传递------所以基类的友元只能碰基类的私有/保护成员。


七、继承与静态成员

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。 无论派生出多少个子类,都只有一个 static 成员实例。

cpp 复制代码
class Person   // 父类
{
public:
    Person() { ++_count; }
protected:
    string _name;   // 姓名
public:
    static int _count;   // 统计人的个数
};

int Person::_count = 0;

class Student : public Person   // 子类
{
protected:
    int _stuNum;   // 学号
};

class Graduate : public Student   // 子类
{
protected:
    string _seminarCourse;   // 研究科目
};

void TestPerson()
{
    Student s1;
    Student s2;
    Student s3;
    Graduate s4;
    cout << " 人数 :" << Person::_count << endl;   // 4
    Student::_count = 0;
    cout << " 人数 :" << Person::_count << endl;   // 0
}

int main()
{
    TestPerson();
    return 0;
}

运行结果:

复制代码
 人数 :4
 人数 :0
要点 说明
只有一份 静态成员属于类 ,整个继承体系共享同一份实例
访问方式 Person::_count、Student::_count、Graduate::_count 都能改到同一份
印证 把 Student::_count 改成 0,Person::_count 跟着变 0

💡 输出里的 4 来自 4 个对象(3 个 Student + 1 个 Graduate,Graduate 也间接继承了 Person 的构造),而把 Student::_count 置 0 后 Person::_count 也变成 0------这正说明 Student::_count 并不是一个独立变量,只是同一份数据的另一个名字。

那么今天这部分内容就先讲到这里。目前的重点是一、二、四:一是类继承的重要概念,二、四则是子类和父类的联系,要理解透彻。下一章讲类继承的难点,可以期待一下!


八、复盘(附答案)

💡 思考题

  1. 继承的概念及定义 :public、protected、private 三种继承方式分别对基类成员在派生类中的访问权限 产生什么影响?如何选择继承方式?

  2. 基类和派生类对象赋值转换 :什么是切片 ?基类指针指向派生类对象 和派生类指针指向基类对象分别有什么注意事项?

  3. 继承中的作用域 :派生类和基类同名成员 会发生什么?如何显式访问 基类中被隐藏的成员?隐藏和重载的区别是什么?

  4. 派生类的默认成员函数 :派生类的构造函数 为什么必须在初始化列表中显式调用基类构造函数 ?拷贝构造 和赋值重载需要注意什么?

  5. 继承与友元 :基类的友元函数能否访问派生类的私有成员?为什么?

  6. 继承与静态成员 :基类中定义的静态成员 在继承体系中有多少份实例?派生类能否访问基类的静态成员?

📝 答案

  1. public 继承 保持基类成员原有访问权限;protected 继承 将基类 public 成员降为 protected ;private 继承 将所有基类成员降为 private 。一般使用 public 继承 ,protected 和 private 继承较少使用,主要用于实现细节隐藏 。记住口诀:访问权限 = min(成员原权限, 继承方式) ,而 private 成员永远不可见。

  2. 切片 指派生类对象赋给基类对象 / 指针 / 引用时,切掉派生类部分、只保留基类部分 。基类指针指向派生类对象 时,通过它只能访问基类成员 (安全);基类对象赋给派生类对象 是不允许的,派生类指针指向基类对象 是不安全的 ------强制转换后访问派生类成员会越界(未定义行为)。

  3. 派生类和基类同名成员会构成隐藏 :派生类默认访问自己的成员,要访问基类被隐藏的成员需用 基类::成员名 。隐藏 发生在不同作用域 (基类和派生类),只要函数名相同 就触发;重载 必须发生在同一作用域 ,且参数列表不同。

  4. 派生类构造函数必须在初始化列表 中显式调用基类构造函数,因为基类成员只能通过基类构造函数初始化 。拷贝构造 也需要显式调用基类拷贝构造(Base(other)),否则编译器会调基类的默认构造 而不是拷贝构造,导致基类部分没被拷贝;赋值重载 需显式调用基类赋值(Base::operator=(other));析构函数 在派生类析构后自动调用 基类析构,顺序是先子后父。

  5. 不能 。友元关系不能继承 ------基类的友元函数只能访问基类的私有/保护成员,无法访问派生类的私有和保护成员 ,因为友元是基于具体类授予的权限,不是继承关系的一部分。

  6. 基类中的静态成员在整个继承体系中只有一份 ,所有派生类共享同一个实例 。派生类可以通过 基类::静态成员 或 派生类::静态成员 访问,而且改的是同一份数据。


结语

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充

你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

相关推荐
Godspeed Zhao11 分钟前
从零开始学AI24——无监督学习之K均值
学习·机器学习
叼烟扛炮24 分钟前
[LangChain] 学习笔记(05):提示词模板(Prompt)与少样本注入(Few-Shot)实战
linux·人工智能·笔记·学习·langchain·prompt
apcipot_rain1 小时前
C++转Java日记(05)
java·开发语言·c++
apcipot_rain10 小时前
C++转Java日记(04)
java·开发语言·c++
WL学习笔记12 小时前
vector模拟实现
c++
La Pulga13 小时前
【FreeRTOS源码学习笔记】任务延时列表
笔记·mcu·学习·嵌入式·freertos·实时操作系统
蜗牛一步步往上爬14 小时前
第十六章 C++语言在深度学习中的应用
开发语言·c++·深度学习
楚山之石14 小时前
【JCO】mpMRI 先行替代 TURBT 初始分期:BladderPath 随机试验让 MIBC 患者提前 45 天得到正确治疗
人工智能·学习·mri·医学影像
Peter-OK18 小时前
PostgreSQL学习笔记(入门-进阶-精通说明)
笔记·学习·postgresql