跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

C++ 继承基础:概念定义、访问权限与默认成员函数

C++ 继承是面向对象编程中代码复用的核心机制。阐述了继承的概念定义与格式,重点解析了公有、保护、私有三种继承方式下成员访问权限的变化规则。内容涵盖基类与派生类的对象转换逻辑,解释了子类对象可隐式转换为父类指针或引用的原理及切片现象。同时详细说明了同名变量与函数的隐藏规则,强调作用域限定符的使用。最后深入讲解了派生类默认成员函数的执行顺序,包括构造时先父后子、析构时先子后父的原则,以及实现不可继承类的方法。

心动瞬间发布于 2026/3/21更新于 2026/6/2126 浏览
C++ 继承基础:概念定义、访问权限与默认成员函数

前言

写代码时总遇到'重复定义'的问题?比如 Student 和 Teacher 类都要写姓名、地址、身份认证函数,改一处就要两处同步改 —— 这就是没用到 C++ 的'继承'机制。继承是面向对象复用代码的核心,能让子类直接'继承'父类的成员和方法,再扩展自己的专属功能。这篇文章就从'继承的概念定义''基派生类转换''作用域隐藏''默认成员函数'四个核心板块入手,手把手带你搞懂继承的基础逻辑,避开常见的坑。

一、继承的概念与定义:怎么让类'复用'代码?

先想一个场景:Student 和 Teacher 都需要'姓名、地址、身份认证',但 Student 有学号、Teacher 有职称。如果各自写一遍,代码会很冗余 —— 继承就是把'公共部分'抽成父类 (基类),子类 (派生类) 直接复用。

本篇博客代码示例中所需头文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
#include <list>
using namespace std;

1.1 继承的核心概念

  • 父类(基类):存放公共成员的类,比如 Person 类(包含姓名、地址、identity 身份认证函数)。
  • 子类(派生类):继承父类并扩展专属成员的类,比如 Student(加学号)、Teacher(加职称)。

本质:子类是父类的'扩展',能直接用父类的公共/保护成员,不用重复定义。

1.2 继承的定义格式

关键是'继承方式 + 父类名',比如 class Student : public Person。注意两点:

  • class默认私有继承,struct默认公有继承,推荐显式写继承方式(比如 public)。
  • 继承方式会影响父类成员在子类中的访问权限(后面会讲)。

有了上面的知识储备后,我们来看一段代码示例来加深理解(注意看注释):

// 基类/父类
class Person {
    // 公共成员:子类和类外都能访问
public:
        // 进入校园/图书馆/实验室刷二维码等身份认证
    void identity() { cout << "void identity():" << _name << endl; }
    void func() { cout << _age << endl; }

    
:
    string _name = ; 
    string _address;       
    string _tel;           

    
:
     _age = ; 
};




  :  Person {
:
    
    {
        
        
        
        
        ();
    }
:
    
     _stuid; 
};


  :  Person {
:
    
    {
        
    }
:
    string title; 
};


{
    Student s;
    Teacher t;
    s.(); 
    s.();    
     ;
}
// 保护成员:子类能访问,类外不能访问(专门为继承设计)
protected
"赵四"
// 姓名
// 地址
// 电话
// 私有成员:子类和类外都不能直接访问(像'爸爸的私房钱')
private
int
18
// 年龄
// 子类 Student:公有继承 Person
// class 的话不写默认是私有继承,struct 是公有继承
// class Student:Person
class
Student
public
public
// 学习
void study()
// ……
// 基类私有成员 (爸爸的私房钱),派生类 中不可见,语法限制上不能直接使用
// cout << _age << endl;
// 父类公有函数能间接访问私有成员
func
protected
// 在继承中保护用的比较多
int
// 学号
// 子类 Teacher:公有继承 Person
class
Teacher
public
public
// 授课
void teaching()
// …………
protected
// 职称
// 测试:子类能直接用父类的函数
int main()
identity
// 用父类的 identity,输出'赵四'
study
// 用子类的 study,调用父类的 func,输出了 18
return
0

1.3 继承方式与成员访问权限

父类成员在子类中的访问权限,取决于'父类的访问限定符'和'继承方式',核心规则是:访问权限 = 两者中更严格 (可以理解为 Min) 的那个(public > protected > private)。

我们用表格总结一下(重点记 public 继承,实际开发最常用):

父类成员类型public 继承(推荐)protected 继承private 继承
父类 public 成员子类中为 public子类中为 protected子类中为 private
父类 protected 成员子类中为 protected子类中为 protected子类中为 private
父类 private 成员不可见(不可访问)不可见(不可访问)不可见(不可访问)

关键提醒:

  • 父类 private 成员无论怎么继承都'不可见'(但是实际上是存在的)—— 子类想访问,只能通过父类的公有函数(比如上面的 func())。
  • protected 是为继承设计的:既不让类外访问,又能让子类用。
  • 在实际运用中一般都还是 public 继承,几乎很少使用 protected/private 继承。也不提倡使用后两者,因为它们继承下来的成员实际中扩展维护性不强,受到的限制比公有继承多。

我们这里就拿我们之前的 Stack 来看,可以使用继承来实现 (注意看注释):

namespace Demo {
template <class T>
class Stack : public vector<T> {
public:
    void push(const T& x) {
        // 基类是类模板时,需要指定⼀下类域,
        // 否则编译报错:error C3861: 'push_back': 找不到标识符
        // 因为 stack<int>实例化时,也实例化 vector<int>了
        // 但是模版是按需实例化,调用了那个成员函数就实例化那个,push_back 等成员函数未实例化,所以找不到 vector<T>::push_back(x);
        vector<T>::push_back(x);
    }
    void pop() {
        vector<T>::pop_back();
    }
    const T& top() {
        return vector<T>::back();
    }
    bool empty() {
        return vector<T>::empty();
    }
};
}

int main() {
    Demo::Stack<int> st;
    st.push(1);
    st.push(2);
    st.push(3);
    while (!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }
    // 但是模版是按需实例化,调用了哪个成员函数,就实例化哪个
    // 构造/析构/push_back,其他成员函数就不会实例化
    // vector<int> v;
    // v.push_back(1);
    return 0;
}

二、基类与派生类的转换:子类对象能当父类用吗?

这是继承的核心特性之一,简单说:子类对象能隐式转换成父类对象 / 指针 / 引用,反之不行。

  • public 继承的 派生类对象 可以赋值给 基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
  • 基类对象不能赋值给派生类对象
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须基类的指针是指向派生类对象时才是安全的。这里如果基类的多态类型,可以使用 RTTI(Run-Time-Type Information) 的 dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下)

代码示例 (注意看注释):

class Person {
protected:
    string _name; // 姓名
    string _sex;  // 性别
    int _age;     // 年龄
};

class Student : public Person {
public:
    int _No; // 学号
};

int main() {
    int i = 1;
    double d = i;
    const double& rd = i;
    string s1 = "1111";
    const string& rs = "111111";
    // 上面的转换在之前类和对象以及其它的一些地方都讲到过,这里就不说了,主要是对比

    Student sobj;
    // 1. 子类对象 → 父类指针/引用(隐式转换,安全)
    Person* pp = &sobj;      // 父类指针指向子类对象的'父类部分'
    Person& rp = sobj;       // 父类引用引用子类对象的'父类部分'

    // 2. 子类对象 → 父类对象(调用父类拷贝构造,只拷贝父类部分)
    // 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷⻉构造完成的
    Person pobj = sobj;

    // 3. 父类对象 → 子类对象(编译报错,不安全)
    // sobj = pobj;            // 错误:父类没有子类的成员(比如学号)
    // sobj = (Student)pobj;   // 强制转换也不行
    return 0;
}

为什么?
子类包含 '父类部分 + 自己的部分',把子类当父类用,只会用到'父类部分',不会越界;但父类没有子类的成员,强行转子类会访问不存在的内容(比如学号),所以禁止。

三、继承中的作用域:同名成员会冲突吗?

父类和子类有独立的作用域,如果出现同名成员(变量或函数),子类会'隐藏'父类的同名成员 ——— 这就是 '隐藏规则',很容易踩坑。

  • 在继承体系中基类和派生类都有独立的作用域
  • 派生类和基类有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫做隐藏。(在派生类和成员函数中,可以使用基类::基类成员 显示访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏 (参数什么的不重要)。

避坑提醒:继承体系中,尽量不要定义同名成员—— 如果必须同名,访问时一定要加父类作用域。

3.1 变量隐藏:同名变量只认子类的

代码示例 (注意看注释):

class Person {
protected:
    string _name = "小李子"; // 姓名
    int _num = 111;          // 父类的_num:身份证号
};

class Student : public Person {
public:
    void Print() {
        // 同名变量:默认访问子类的_num(学号)
        // 同名成员构成隐藏,只在乎是否同名,参数什么的都不用管
        cout << "子类的_num:" << _num << endl; // 输出 999
        // 想访问父类的_num:必须加'父类::'
        cout << "父类的_num:" << Person::_num << endl; // 输出 111
    }
protected:
    int _num = 999; // 学号
};

int main() {
    Student s;
    s.Print();
}

规则:不管变量类型、参数,只要同名,子类就隐藏父类的 —— 想访问父类的,必须用 父类名::成员名。

3.2 函数隐藏:同名函数只认子类的

比变量隐藏更坑:只要函数名相同,不管参数列表,子类就隐藏父类的函数。

答案:B,A 具体解析注意看下面代码的注释 (附如何修改):

int main() {
    Student s;
    s.Print();
}

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); // 调用子类的 fun(int),输出'func(int i)10'
    // b.fun(); // 编译报错:父类的 fun()被隐藏了,不能直接调用
    b.A::fun(); // 想调用父类的 fun():加'父类::',输出'func()'
    return 0;
}

四、派生类的默认成员函数:构造、拷贝、析构怎么写?

子类和普通类一样,有 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),但子类的默认成员函数必须先处理父类的部分。

前置说明:继承的基成员变量 (整体对象)+自己的成员变量 (遵循普通的规则,跟类和对象部分一样)。默认生成的构造,派生类自己的成员,内置类型不确定,自定义类型调用默认构造,基类部分调用默认构造本质可以把派生类当做一个自定义成员变量 (基类) 的普通类总,跟普通类原则基本一样派生类一般要自己实现构造,不需要显示写析构,构造函数,赋值重载,除非派生类有深拷贝的资源需要处理

核心规则:子类的成员函数 = 父类成员的处理 + 子类成员的处理。
注意:下面的代码为了便于理解会分模板展示,大家要是需要自己实现的话全部综合一下就行。

4.1 构造函数:先调用父类构造,再初始化子类成员

子类构造时,会先自动调用父类的'默认构造'(无参,编译器默认生成的或全缺省);如果父类没有默认构造,必须在子类构造的初始化列表中显式调用父类构造。

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

代码示例 (注意看注释):

class Person {
public:
    // 父类带参构造(无默认构造)
    Person(const char* name) : _name(name) // 初始化父类的_name
    {
        cout << "Person()" << endl;
    }
protected:
    string _name; // 姓名
};

class Student : public Person {
public:
    // 子类构造:必须在初始化列表显式调用父类构造
    Student(const char* name = "赵四", int num = 18, const char* address = "西安")
        // 这里可以显示写一下 (不能直接用_name),其实就把他当成一个自定义类型成员变量就可以了。基类的成员当成一个整体。
        : Person(name) // 先初始化父类(必须写在前面)
        , _num(num)    // 再初始化子类自己的成员
        , _address(address)
    {
        cout << "Student()" << endl;
    }
protected:
    int _num;       // 学号
    string _address;// 地址
};

int main() {
    // 构造顺序:先调用 Person(name),再调用 Student()
    Student s1("张三", 20, "北京");
    return 0;
}

关键顺序:构造时 '先父后子'—— 父类先初始化,子类才能用父类的成员。

4.2 拷贝构造:先拷贝父类,再拷贝子类

子类拷贝构造时,会先调用父类的拷贝构造,拷贝父类部分的成员;再拷贝子类自己的成员。

  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

代码示例 (注意看注释):

class Person {
public:
    // 父类拷贝构造
    Person(const Person& p) // 传的是派生类的话也可以转换,前面讲过
    {
        _name = p._name;
        cout << "Person(const Person& p)" << endl;
    }
protected:
    string _name; // 姓名
};

class Student : public Person {
public:
    // 子类拷贝构造
    Student(const Student& s) : Person(s) // 调用父类拷贝构造,拷贝父类部分(s 是子类,能隐式转父类)
        , _num(s._num)                     // 拷贝子类自己的学号
        , _address(s._address)             // 拷贝子类自己的地址
    {
        // 如果有深拷贝资源(比如 int*),这里要手动处理
    }
protected:
    int _num;       // 学号
    string _address;// 地址
};

4.3 赋值重载:先赋值父类,再赋值子类

子类赋值重载会'隐藏'父类的赋值重载,所以要显式调用父类的赋值重载(Person::operator=),避免父类部分没赋值。

  • 派生类的 operator=必须调用基类的 operator=完成基类的复制。需要注意的是派生类的 operator=隐藏了基类的 operator=,所以显示调用基类的 operator=,需要指定基类作用域。

代码示例 (注意看注释):

class Person {
public:
    Person& operator=(const Person& p) {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p) _name = p._name;
        return *this;
    }
protected:
    string _name; // 姓名
};

class Student : public Person {
public:
    Student& operator=(const Student& s) {
        if (this != &s) // 防止自己赋值自己
        {
            // 先赋值父类部分
            Person::operator=(s);
            // 再赋值子类自己的成员
            _num = s._num;
            _address = s._address;
            // 如果有深拷贝资源在这里处理
        }
        return *this;
    }
protected:
    int _num;       // 学号
    string _address;// 地址
};

4.4 析构函数:先析构子类,再自动析构父类

子类析构时,会先执行自己的析构逻辑,结束后编译器自动调用父类的析构—— 不用显式调用,否则会析构两次。

  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 因为在多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同 (这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成 destructor(),所以基类析构函数不加 virtual 的情况下,派生类析构函数和基类析构函数构成隐藏关系。不过我们这里不显示调用,只是补充一下这个知识点。

代码示例 (注意看注释):

class Person {
public:
    ~Person() {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名
};

class Student : public Person {
public:
    ~Student() {
        // Person::~Person(); // 前面也需要 Person::,有一定历史原因,有提过
        // 但是这里不能写,否则无法保证析构先子后父的原则,会析构两次。
        // 先执行子类析构逻辑(比如释放子类的资源)
        cout << "~Student()" << endl;
        // 编译器自动调用父类析构:~Person()
    }
protected:
    int _num;
};

int main() {
    // 构造顺序:先 Person(), 再 Student()
    // 先父后子:我们联想一下之前初始化列表按声明顺序来的原理
    // 析构顺序:先~Student(),再~Person()
    // 先子后父:我们可以想一下如果先析构父类,那么子类的成员如果需要访问父类就出问题了
    Student s1;
    return 0;
}

为什么先子后父?
如果先析构父类,子类的成员可能还需要访问父类资源,会出问题(比如子类析构时要打印父类的姓名,父类先析构就没了)。

4.5 实现一个不能被继承的类

方法一:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

// 方法 1:C++98
class Base {
public:
    void func5() {
        cout << "Base::func5" << endl;
    }
protected:
    int a = 1;
private:
    // C++98 的方法:构造函数私有的类不能被继承
    Base() {}
};

class Derive : Base {};

int main() {
    // Base b;
    // Derive d;
    return 0;
}

方法二:C++11 新增了⼀个 final 关键字,final 修改基类,派生类就不能继承了。

class Base final {
public:
    void func5() {
        cout << "Base::func5" << endl;
    }
protected:
    int a = 1;
};

class Derive : Base {};

int main() {
    // Base b;
    // Derive d;
    return 0;
}

总结:
继承的核心是'复用',但不是万能的 —— 掌握这四个板块,就能应对大部分基础场景:

  • 用 public 继承实现代码复用,区分 public/protected/private 的访问权限;
  • 记住 '子类能转父类,父类不能转子类',避免不安全转换;
  • 同名成员会隐藏,访问时加父类作用域;
  • 子类默认成员函数要 先处理父类部分,尤其是构造和赋值重载。
  • 构造顺序:先父后子,析构顺序:先子后父。

后续还会讲 '多继承''虚继承' 这些进阶内容,但基础阶段先把这些吃透,写继承类时就不会踩常见的坑了。

结语

继承的核心是'复用'与'扩展',吃透单继承的权限规则、对象转换、同名隐藏及默认函数逻辑,就能避开多数基础陷阱。不必急于进阶多继承,先写好'父稳子清'的继承体系,便是掌握了面向对象复用的精髓。

目录

  1. 前言
  2. 一、继承的概念与定义:怎么让类“复用”代码?
  3. 1.1 继承的核心概念
  4. 1.2 继承的定义格式
  5. 1.3 继承方式与成员访问权限
  6. 二、基类与派生类的转换:子类对象能当父类用吗?
  7. 三、继承中的作用域:同名成员会冲突吗?
  8. 3.1 变量隐藏:同名变量只认子类的
  9. 3.2 函数隐藏:同名函数只认子类的
  10. 四、派生类的默认成员函数:构造、拷贝、析构怎么写?
  11. 4.1 构造函数:先调用父类构造,再初始化子类成员
  12. 4.2 拷贝构造:先拷贝父类,再拷贝子类
  13. 4.3 赋值重载:先赋值父类,再赋值子类
  14. 4.4 析构函数:先析构子类,再自动析构父类
  15. 4.5 实现一个不能被继承的类
  16. 结语
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 数电设计步骤与 FPGA 实现的本质区别
  • LLM 架构解析:为何主流大模型偏好 Decoder-Only 设计
  • 用 Vibe Coding 实现 AI 全自动代码生成与交付
  • Python 基础语法完全指南:变量、类型、运算符与字符串
  • PyTorch Checkpoint 机制原理与源码解析
  • C++ 图论实战:深入理解三种最短路径算法
  • OpenSpec 实战:用规范驱动开发破解 AI 编程协作难题
  • HTTP 请求方式详解:GET、POST 与常用方法实战
  • 文心一言与通义千问大模型能力对比评测
  • GTC 2026 前瞻:Rubin 平台与 AI 工厂化趋势
  • Cursor 实战:Web 版背单词应用开发全流程
  • 二分查找实战:山峰数组峰顶索引与寻找峰值
  • OpenCode 本地 AI 模型配置指南
  • Visual Studio 使用 GitHub Copilot 与 IntelliCode 辅助编码
  • Mac 系统安装 Python 详细教程
  • 基于 FastGPT 与 MCP 协议构建工具增强型 AI Agent
  • JavaScript 基础语法与 jQuery 快速入门
  • AIGC 工具全解析:文本、图像、代码与视频生成指南
  • MySQL 数据类型详解:从数值到字符串的实战指南
  • OpenClaw 多飞书机器人与多 Agent 团队实战复盘

相关免费在线工具

  • 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

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online