C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑

C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一. 继承的概念与定义

  1、继承的核心概念

  2、继承的定义格式

  3、继承方式与成员访问权限

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

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

  1、变量隐藏

  2、函数隐藏

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

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

  2、拷贝构造:先拷贝父类,再拷贝子类

  3、 赋值重载:先赋值父类,再赋值子类

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

  5、实现一个不能被继承的类

结束语


前言

      实现多个类时总遇到 “重复定义” 的问题?比如 Student 和 Teacher 类都要写姓名、地址、身份认证函数,改一处就要两处同步改 —— 这就是没用到 C++ 的 “继承” 机制。继承是面向对象复用代码的核心,能让子类直接 “继承” 父类的成员和方法,再扩展自己的专属功能。

一. 继承的概念与定义

      继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类

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

  1、继承的核心概念

  • 父类(基类):存放公共成员的类,比如 person 类(包含姓名、地址、identity 身份认证函数)。
  • 子类(派生类):继承父类并扩展专属成员的类,比如 Student (加学号)、Teacher (加职称)。
  • 本质:子类是父类的 “扩展”,能直接用父类的公共 / 保护成员,不用重复定义。

  2、继承的定义格式

      关键是 “继承方式 + 父类名”,比如 class Student : public Person。

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

#include<iostream> using namespace std; //基类/父类 class Person { // 公共成员:子类和类外都能访问 public: //进入校园等场合需要的身份认证 void identity() { cout << "void identity()" << _name << endl; } void age() { cout << _age << endl; } // 保护成员:子类能访问,类外不能访问(专门为继承设计) protected: string _name = "张三"; //姓名 string _address; //地址 string _tel = "123456"; //电话 // 私有成员:子类和类外都不能直接访问 private: int _age = 18; //年龄 }; // 子类Student:公有继承Person // class的话不写默认是私有继承,struct是公有继承 // class Student:Person (私有继承) class Student : public Person { public: void study() { cout << "void study() " << _tel << endl; //通过父类公有函数能间接访问私有成员: age(); } protected: int _stuid; //学号 }; // 子类Teacher:公有继承Person class Teacher : public Person { public: void teaching() { cout << "void teaching() " << _tel << endl; } protected: string title; //职称 }; int main() { // 测试:子类能直接用父类的函数 Student s; Teacher t; s.identity(); t.identity(); s.study(); // 用子类的 study,调用父类的 age(),输出了18 t.teaching(); return 0; }

  3、继承方式与成员访问权限

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

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

父类成员类型public继承(推荐)protected继承private继承
父类public成员子类中为public子类中为protected子类中为private
父类protected成员子类中为protected子类中为protected子类中为private
父类private成员不可见(不可访问)不可见(不可访问)不可见(不可访问)
  • 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  • 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的
  • 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  • 使用关键字 class 默认的继承方式是 private,使用 struct 默认的继承方式是 public,不过最好显示的写出继承方式。
  • 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

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

#include<iostream> #include<vector> #include<list> #include<deque> using namespace std; #define CONTAINER vector //#define CONTAINER list //#define CONTAINER deque namespace MyStack { template<class T> class stack : public std::CONTAINER<T> { public: void push(const T& x) { //push_back(x); // 基类是类模板时,需要指定⼀下类域, // 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了 // 但是模版是按需实例化,调用哪个成员函数就实例化哪个, // 由于 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(); } }; } void Test2() { MyStack::stack<int> st; st.push(1); st.push(2); st.push(3); while (!st.empty()) { cout << st.top() << " "; st.pop(); } } int main() { Test2(); 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; // 学号 }; void Test3() { //基类与派生类的转换 Student st; // 1.派生类对象可以赋值给基类的指针/引用 // 子类对象 → 父类指针 / 引用(隐式转换,安全) Person* ppe = &st; //父类指针 指向子类对象的“父类部分” Person& rpe = st; //父类引用 引用子类对象的“父类部分” //这里会有人误以为就是前面所学的简单的隐式类型转换 //但其实不是,从第三个引用来看就知道了: int i = 1; double d = i; //double& rd = i; const double& rd = i; //如果是隐式类型转换的话会产生临时对象,临时对象显常性,如果不用const修饰就会报错 //但是上面的转换并没有const修饰也不会报错, //所以和之前学习的隐式类型转换是有区别的,可以理解为是C++规定的特例 // 2.子类对象 → 父类对象(调用父类拷贝构造,只拷贝父类部分) // 生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造完成的 Person pe = st; // 3.基类对象不能赋值给派生类对象,这里会编译报错 // 父类对象 → 子类对象(编译报错,不安全) //st = pe; //st = (Student)pe;// 强制转换也不行 } int main() { Test3(); return 0; }
为什么?
      子类包含“父类部分 + 自己的部分”,把子类当父类用,只会用到 “父类部分”不会越界;但父类没有子类的成员,强行转换成子类会访问不存在的内容(比如学号),所以禁止。

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

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

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

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

  1、变量隐藏

代码示例(注意看注释)

//继承中的作用域 class Person { protected: string _name = "张三"; // 姓名 int _num = 123456; // 父类的_num:身份证号 }; class Student : public Person { public: void Print() { // 同名变量:默认访问子类的_num(学号) // 同名成员构成隐藏 //如果是成员函数的隐藏,只需要函数名相同就构成隐藏 cout << "子类的_num:" << _num << endl;//输出111 // 想访问父类的_num:必须加“父类::”,即“基类::基类成员”显式访问 cout << "父类的_num:" << Person::_num << endl;//输出123456 } protected: int _num = 111; // 学号 }; int main() { Student s; s.Print(); }

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

  2、函数隐藏

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

我们看一下继承作用域的相关选择题:

答案:B,A

具体分析可以看下面代码注释(附如何修改)

//继承作用域相关选择题 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 个默认成员函数(构造、拷贝构造、赋值重载、析构等),但子类的默认成员函数必须先处理父类的部分

核心规则:子类的成员函数 = 父类成员的处理 + 子类成员的处理。

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

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

      基类有默认构造函数的情况:

class Person { public: // 父类全缺省构造(默认构造) Person(const char* name = "张三") :_name(name) // 初始化父类的_name { cout << "Person()" << endl; } protected: string _name;// 姓名 }; class Student :public Person { public: //默认生成的构造函数行为: //1、内置类型——> 不确定 //2、自定义类型——> 调用对应的默认构造 //3、继承父类成员看作一个整体对象,要求调用父亲的默认构造 protected: int _num; // 学号 string _address; // 地址 }; int main() { Student s1; return 0; }

      基类没有默认构造函数的情况:(则必须在子类构造的初始化列表中显式调用父类构造)

class Person { public: // 父类带参构造(无默认构造) Person(const char* name) :_name(name) // 初始化父类的_name { cout << "Person()" << endl; } protected: string _name;// 姓名 //int _age; //年龄 //string _gender; //性别 //string _title; //职业 }; class Student :public Person { public: // 子类构造:必须在初始化列表显式调用父类构造 Student(const char* name = "张三", int num = 18, const char* address = "北京") //这里可以显式写一下(不能直接用_name),其实就把他当成一个自定义类型成员变量就可以了 //: _name(name) //error C2614: “Student”: 非法的成员初始化:“_name”不是基或成员 :Person(name) // 先初始化父类(必须写在前面) //这样要求也是变相的为我们节省构造的实现,如果父类的成员变量非常多, //难道子类为了实现构造要把每个父类的成员变量都初始化一遍吗?显然太麻烦了 //所以初始化列表通过调用父类构造就可以直接对父类把部分的成员变量全部进行初始化 , _num(num) // 再初始化子类自己的成员 , _address(address) { cout << "Student()" << _name << endl; } protected: int _num; // 学号 string _address; // 地址 }; int main() { // 构造顺序:先调用Person(name),再调用Student() Student s1("李四", 20, "北京"); return 0; }

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

  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; // 地址 }; int main() { // 构造顺序:先调用Person(name),再调用Student() Student s1("李四", 20, "北京"); //拷贝构造 Student s2(s1); return 0; }

  3、 赋值重载:先赋值父类,再赋值子类

      派生类的 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)//一定要注意子类的同名函数 operator= 与父类构成隐藏 { if (this != &s) { //operator=(s); //不能直接调用operator=否则就是调用子类本身的赋值重载导致死循环而栈溢出 Person::operator=(s); _num = s._num; _address = s._address; } return *this; } protected: int _num; // 学号 string _address; // 地址 }; int main() { // 构造顺序:先调用Person(name),再调用Student() Student s1("李四", 20, "北京"); //拷贝构造 Student s2(s1); //赋值重载 Student s3("王五", 18, "上海"); s1 = s3; return 0; }

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

      派生类的析构函数会在被调用完成自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
      因为在多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成 destructor(),所以基类析构函数不加 virtual 的情况下,派生类析构函数和基类析构函数构成隐藏关系。不过我们这里不显示调用,只是补充一下这个知识点。
class Person { public: //父类的析构 ~Person() { cout << "~Person()" << endl; } protected: string _name;// 姓名 }; class Student :public Person { public: //子类的析构 ~Student() { //~Person(); //error //由于析构函数都会被特殊处理成destructor() //所以子类的析构和父类的析构也是会构成隐藏关系(虽然表面上不是同名的) //规定:不需要显式调用父类析构,子类析构结束后,编译器会自动调用父类析构 //Person::~Person(); //这样写如果存在有动态开辟的空间反而会导致对同一块空间析构两次而程序崩溃 cout << "~Student()" << endl; } protected: int _num; // 学号 string _address; // 地址 }; int main() { // 构造顺序:先调用Person(name),再调用Student() Student s1("李四", 20, "北京"); // 构造顺序:先Person(),再Student() // 先父后子:我们联想一下之前初始化列表按声明顺序来的原理 // 析构顺序:先~Student(),再~Person() // 先子后父:我们可以想一下如果先析构父类,那么子类的成员如果需要访问父类就出问题了 return 0; }

  5、实现一个不能被继承的类

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

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

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

//方法二:添加 final 关键字(C++11) class Base final { public: void func5() { cout << "Base::func5" << endl; } protected: int a = 1; }; class Derive : Base { }; int main() { Derive d; return 0; }

结束语

      继承的核心是 “复用” 与 “扩展”,吃透单继承的权限规则、对象转换、同名隐藏及默认函数逻辑,就能避开多数基础陷阱。

      总结为以下五点:

(1) 用 public 继承实现代码复用,区分 public/protected/private 的访问权限;

(2) 记住 “子类能转父类,父类不能转子类”,避免不安全转换;

(3) 同名成员会隐藏,访问时加父类作用域;

(4) 子类默认成员函数要先处理父类部分,尤其是构造和赋值重载

(5) 构造顺序:先父后子析构顺序:先子后父

希望对大家学习C++能有所收获!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

Read more

2025年AI领域年度深度总结:始于DeepSeek R1开源发布,终于Manus天价出海

2025年AI领域年度深度总结:始于DeepSeek R1开源发布,终于Manus天价出海

2025年AI领域年度深度总结:始于DeepSeek R1开源发布,终于Manus天价出海 摘要 站在2025年12月31日的终章回望,吴恩达曾说过:“2025年,是AI工业时代的黎明。”在经历了2023-2024年的“大炼模型”狂热后,2025年,AI终于从“概率模仿”跃向了“逻辑推理”的新阶段,从“对话框”到“行动流”的转折也逐渐显现。这一年,AI技术与产业的演进不仅仅是技术迭代那么简单,而是一场深刻的变革,清晰的产业蓝图开始显现:始于DeepSeek R1的开源突破,终于Manus的数十亿美元收购,验证了Agent商业化的巨大潜力。 2025年,AI不再是实验室中的抽象概念,而是逐步嵌入日常生产生活,以更加务实的姿态和广泛的应用场景,真正走向了社会的主流。从年初DeepSeek R1的开源发布到年末Manus的天价收购,这两件大事为2025年的AI发展定下了基调:开源与闭源的博弈,技术与商业的融合,模型与应用的深度对接,无疑为AI的未来铺设了一条发展道路。技术突破和产业落地不断交织,AI的角色正在悄然发生深刻的转变——从“辅助工具”走向了“自主执行者”。 文章目录

By Ne0inhk
最新版 Kimi K2.5 进阶实战全攻略:从开源部署到 Agent 集群搭建(视频理解 + 多模态开发 + 高并发调优)

最新版 Kimi K2.5 进阶实战全攻略:从开源部署到 Agent 集群搭建(视频理解 + 多模态开发 + 高并发调优)

1 技术背景与核心架构原理 1.1 技术定位与版本说明 Kimi K2.5 是月之暗面于2026年初发布的开源多模态大语言模型,聚焦长上下文理解、原生多模态交互、Agent 原生支持三大核心能力,针对工业级落地场景完成了全链路优化。本次实战覆盖的开源版本包括: * kimi-k2.5-chat-70b:基础对话版,支持2000K token 上下文窗口,原生适配工具调用 * kimi-k2.5-multimodal-70b:多模态完整版,新增图像、长视频时序理解能力,支持最长10小时连续视频输入 * kimi-k2.5-agent-70b:Agent 优化版,强化多轮工具链执行、分布式状态同步能力,适配集群化部署 * 量化衍生版本:AWQ 4bit/8bit、FP8 量化版,适配低显存硬件环境,精度损失控制在1%以内 1.2 核心架构与技术亮点 1.2.1

By Ne0inhk
一文读懂VR/AR/MR:小白也能分清的虚实交互技术

一文读懂VR/AR/MR:小白也能分清的虚实交互技术

目录 * 前言 * 一、逐个击破 —— 三种技术的 “大白话” 解读 * 1.1 VR(虚拟现实):钻进 “虚拟世界” 不出来 * 1.2 AR(增强现实):给 “现实世界” 加层 “滤镜” * 1.3 MR(混合现实):在 “现实里” 玩 “虚拟物件” * 二、核心区别大对比 —— 一张表 + 一张图看懂 * 2.1 对比表格 * 2.2 可视化对比图(核心区别一目了然) * 三、避坑指南 —— 小白最容易混淆的 2 个误区 * 3.1 误区 1:

By Ne0inhk
开源AI编程工具对决:Superpowers技能库与OpenSpec规范驱动,谁更胜一筹?

开源AI编程工具对决:Superpowers技能库与OpenSpec规范驱动,谁更胜一筹?

文章概要 在AI辅助编程领域,Obra/superpowers库与Fission-AI/OpenSpec库代表了两种截然不同的技术路径。前者致力于构建可复用的AI编程技能库,后者则倡导以规范(Spec)为核心的驱动开发模式。本文将深入对比两者在核心理念、工作流程及适用场景上的核心差异,探讨它们如何分别解决AI开发中的效率与一致性难题,并分析在项目演进中应如何取舍。 前几天在咖啡店,我无意中听到邻桌两位程序员在激烈争论。一位坚持说:“AI编程助手最大的价值就是帮我快速写出新代码,我需要的是更多‘技能’。”另一位则反驳:“不对,AI最该解决的是代码一致性,我们团队现在最缺的是‘规范’。”这让我立刻想到了最近在GitHub上观察到的两个项目:Obra的superpowers技能库和Fission-AI的OpenSpec规范驱动框架。它们恰好代表了这两种截然不同的思路。 我打开superpowers的仓库,第一印象是它像一个为AI助手精心打造的“瑞士军刀”工具箱。它的核心理念非常直接:将常见的、复杂的编程任务封装成一个个可复用的“技能”(Skill)。这就像给AI安装了一个插件商店,当需要

By Ne0inhk