当前位置: 首页 > news >正文

今日分享C++ ---继承

😎【博主主页:晚云与城——csdn博客】😎

🤔【本文内容:C++ 继承😍                】 🤔

----------------------------------------- 感谢大家的观看,点赞 ,收藏-----------------------------------------------

1.继承的介绍:

1.继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展增加功能,这样产生新的类,称派生类,原有类,称为基类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,承是类设计层次的复用。
2.继承机制的存在,极大简化了开发流程。它能让新类继承原有类的属性与方法,就好比手机的迭代更新,新手机并非完全从零开始研发,而是在继承前代手机已有功能的基础上,再去添加新的特性与功能。

2.继承的定义:

看上图,这里涉及到新的知识 —— 继承方式。之前我们学习类的时候,主要接触的是public(公共)和private(私有)访问修饰符,protected(受保护)的存在感似乎不强。但在继承场景中,protected就发挥出了很大的作用。

1.继承关系和访问限定符:

类成员 / 继承方式public 继承protected 继承private 继承
基类的 public 成员派生类的 public 成员派生类的 protected 成员派生类的 private 成员
基类的 protected 成员派生类的 protected 成员派生类的 protected 成员派生类的 private 成员
基类的 private 成员在派生类中不可见在派生类中不可见在派生类中不可见

下面为大家总结一些关于类继承的关键要点与实用小技巧:

  1. 基类的private成员,派生类无论采用何种继承方式,都无法直接访问。不过要注意,这并非是没有继承,而是派生类 “不可见”,所以用不了。
  2. 若希望成员能在派生类中被访问,却又不想在类外被访问,protected访问限定符就派上用场了。
  3. 观察上面表格内容能发现这样的规律:基类的私有成员子类不可见,其余基类成员在子类中的访问权限,是取 “成员在基类的访问限定符” 与 “继承方式” 两者中的更严格者(
    基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)
    )(权限优先级:publicprotectedprivate)。
  4. class的默认访问限定符是privatestruct的默认访问限定符是public。即便知晓这一规则,为了代码的清晰性,最好还是显式写出继承方式。
  5. 实际开发中,常用的是public继承,protectedprivate继承很少使用,也不推荐。因为通过这两种继承方式得到的成员,只能在派生类内部使用,不利于代码的扩展与维护。

1.public继承方式:

2.protected继承方式:

3.private继承方式:

3.基类和派生类对象复制转换:

  1. 派生类对象向基类的赋值
    派生类对象能为基类的指针、引用、对象赋值。这一过程可形象地称为 “切片”—— 即把派生类里继承自基类的那部分 “切割” 出来,赋值给基类的指针、引用或对象。
  2. 基类对象向派生类对象的直接赋值
    基类对象无法直接给派生类对象赋值。因为派生类包含了基类没有的成员,基类对象缺少这些额外信息,不能直接填充派生类对象的所有部分。
  3. 基类对象向派生类指针、引用的赋值(强制类型转换)
    若要让基类对象给派生类的指针、引用赋值,可通过强制类型转换实现。但为了安全,最好确保基类指针原本指向的就是派生类对象。要是涉及多态场景下的强制转换,我们可以利用运行时类型识别(RTTI)中的dynamic_cast,它能先识别对象的实际类型,再进行安全的转换,有效避免因类型不匹配而导致的错误。

class person
{
//public:
//	void print()
//	{
//		cout << _name << endl;
//	}
protected:string _name = "那笔小新";string _sex ="男";
//private:size_t _age = 3;
};
class student :public person
{
public:int _no;/*using person::print;void print1(){cout << _sex << endl;}
protected:size_t stuid;string school;*/
};
void test()
{student sobj;// 1.子类对象可以赋值给父类对象/指针/引用person pobj = sobj;person* pp = &sobj;person& rp = sobj;//2.基类对象不能赋值给派生类对象sobj = pobj;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;student* ps1 = (student*)pp; // 这种情况转换时可以的。ps1->_no = 10;pp = &pobj;student* ps2 = (student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_no = 10;
}

4.继承的作用域:

  1. 作用域独立性:基类和派生类拥有各自独立的作用域,继承关系并非意味着派生类会无差别继承基类的所有内容。
  2. 同名成员的隐藏:当基类与派生类存在同名成员时,派生类虽会继承基类成员,但会触发隐藏机制,直接访问基类同名成员的操作会被屏蔽。不过,在子类成员函数里,可通过基类::基类成员的方式显式访问基类同名成员。
  3. 同名成员函数的隐藏判定:在派生类中,只要成员函数与基类的成员函数名称相同,就会构成隐藏,无需考虑参数列表等其他因素。
  4. 使用建议:为避免因成员同名引发的隐藏问题,建议在设计类时,从一开始就尽量避免出现同名成员.
class person
{
public:void print(){cout << "person " << _num << endl;}
protected:string _name = "嘻嘻";int _num = 1;
private:
};class student:public person
{
public:void print(int _num){cout <<"student " << _num << endl;}
protected:int _num = 2;
private:
};
int main()
{student s;s.print();
}//运行结果
student 2
  1. 在通过 print 函数进行打印操作时,此处的 _num 属于派生类内部的成员。
  2. 这里的 print 打印函数,并非函数重载。因为函数重载要求多个函数处于同一个命名作用域,而此处不满足该条件。
  3. print 函数形成了函数隐藏。在类的成员函数中,只要函数名称相同,就会构成函数隐藏。

5.派生类的默认成员函数:

6个默认函数是指,我们没写,但编译器会自动给我们写的函数,但在派生类里面,默认函数是怎么回事,是会继承基类里面的默认函数还是会在派生类里面从新生成。

#include<iostream>
#include<string>
using namespace std;
class person
{
public:person(const string& name):_name(name){cout << "person()" << endl;cout <<_name<< endl;}person(const person& p):_name(p._name){cout <<" person(const person & p)" << endl;cout << 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;
private:size_t _age;string _sex;
};class student:public person
{
public:student(const string& name  , const size_t& stuid ):_stuid(stuid), person(name) /*person(name)*/{cout << "student()" << endl;cout <<"_name:" << _name << "\tstuid:" << _stuid << endl;cout << endl;}student(const student& s): person(s), _stuid(s._stuid){cout << "Student(const Student& s)" << endl;}student& operator = (const student& s){cout << "student& operator= (const student& s)" << endl;if (this != &s){person::operator =(s);_stuid = s._stuid;}return *this;}~student(){cout << "~student()" << endl;}
protected:
private:size_t _stuid;
};int main()
{student s("pertr", 100);student s1(s);student s2("haha", 101);s2 = s;return 0;
}

1.派生类构造函数需调用基类构造函数来初始化基类成员。若基类无默认构造函数,就必须在派生类构造函数的初始化列表阶段显式调用基类构造函数

2.派生类的拷贝构造函数要调用基类的拷贝构造函数,以此完成基类部分的拷贝初始化。

3.派生类的 operator= 必须调用基类的 operator=,从而完成基类部分的复制操作。

4.派生类的析构函数在自身调用完成后,会自动调用基类的析构函数来清理基类成员。这样做是为了确保派生类对象先清理派生类自身成员,再清理基类成员的顺序。

5.派生类对象初始化时,会先调用基类构造函数,之后再调用派生类构造函数。

6.派生类对象析构清理时,会先调用派生类析构函数,接着再调用基类的析构函数。

7.后续有些场景中析构函数需要构成重写,而重写的条件之一是函数名相同(后续会讲解)。编译器会对析构函数名进行特殊处理,将其处理为 destructor()。所以,在父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。

6.继承与友元:

友元是不可以被继承的,也就是说基类友元无法访问派生类里面的私人和保护成员。

6. 继承与静态成员:

基类中定义了 static 静态成员,那么在整个继承体系里,该静态成员只有唯一的一个实例。它类似全局变量,存储在静态存储区,无论基类派生出多少个子类,都共享这同一个 static 成员实例。
class person
{
public:person(){++_count;}static int _count;
protected: string _name;
};
int person::_count = 0;
class student:public person
{
protected:int stuid;
};
class graduate:public student
{
protected:string _seminarCourse;
};int main()
{student s0;student s1;student s2;student s3;student s4;graduate s5;cout << " 人数 :" << person::_count << endl;student::_count = 0;cout << " 人数 :" << person::_count << endl;}
//运行结果:
人数:6
人数:0

7.复杂的菱形继承及菱形虚拟继承:

1. 单继承

单继承指的是一个派生类仅拥有一个基类的继承方式。在这种继承关系里,派生类能从唯一的基类那里继承属性与方法,类的层次结构呈现出简单的树形分支特点,例如类 B 继承自类 A,类 C 又继承自类 B 这样的链式继承。

2. 多继承

多继承是指一个派生类具备多个基类的继承形式。通过多继承,派生类可以整合多个不同基类的特性,像类 D 同时继承类 B 和类 C,从而能拥有类 B 与类 C 各自的成员(属性和方法)。不过,多继承也容易引发一些问题,比如命名冲突等。

3. 菱形继承

菱形继承属于多继承里的特殊情形。其继承结构形似菱形,存在一个基类(如类 A),有两个类(类 B 和类 C)都继承自类 A,之后又有一个类(类 D)同时继承类 B 和类 C这种情况下,类 D 会间接包含类 A 的成员两次,极易产生二义性等问题,在很多编程语言中需要通过特定机制(如虚继承)来解决。

代码中出现了菱形继承问题,针对该问题,有两种常见的解决思路:

1.显式访问:通过明确指定要访问的父类成员,来消除二义性。

2.虚继承:在继承时采用虚继承的方式,让派生类只保留一份共同基类的成员,从而避免重复继承导致的问题。

虚继承就是在我们的继承方式前面加上virtual ,它类似于我们的静态变量,但不完全是。

1.未加virtual

class A
{
public:int _a = 10;
};
class B:/*virtual*/ public A
{
public:int _b;
};
class C:/*virtual */public A
{
public:int _c;
};
class D:public B,public C
{
public:/*void print(){cout << num << endl;}*/int _d;};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在 D 类对象中,因包含 BC 类,而 BC 类又都包含 A 类,导致 A 的成员存在两份,既引发访问时的二义性问题,又造成数据冗余。

2.加virtual

class A
{
public:int _a = 10;
};
class B:virtual public A
{
public:int _b;
};
class C:virtual public A
{
public:int _c;
};
class D:public B,public C
{
public:/*void print(){cout << num << endl;}*/int _d;};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

这里的0到D的最后面去了,并且只有一份。类中会包含一个虚基表指针(vbptr),指向对应的虚基表。虚基表内部记录着当前类到共同虚基类(如上述例子中的类 A)的偏移量等信息。借助虚基表,派生类(如类 D)能通过统一的偏移计算,精准且唯一地访问共同虚基类的成员,既消除了访问的二义性,又避免了数据的重复存储,高效处理了菱形继承的缺陷。

8.继承的总结:

在C++里面,继承最大的问题就是他的继承方式(大多数情况下,只会用常用的)以及多继承多继承可以认为是C++的缺陷之一,很多后来的面对对象的语言都没有多继承,如Java。

继承与组合:

1.public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
2.组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

1.继承是一种代码复用方式,属于 “白箱复用”:派生类可基于基类的实现来定义自身逻辑,基类的内部细节对派生类可见。但这会一定程度破坏基类的封装性,基类的改动易对派生类产生较大影响,使得派生类与基类依赖强、耦合度高。

 

2.对象组合是另一种复用选择,属于 “黑箱复用”:通过组装或组合对象来实现更复杂功能,被组合对象只需有良好定义的接口,其内部细节不可见。组合的类之间依赖弱、耦合度低,能更好地保持类的封装性。

 

实际开发中,应优先使用对象组合,因其耦合度低,利于代码维护。不过继承也有适用场景,比如类间存在明确的继承关系,或者需要实现多态(多态的实现依赖继承)时,就适合用继承。当类之间既能用继承又能用组合时,优先选择组合。

#include <iostream>
#include <string>
using namespace std;
// Animal和Cat、Dog构成is - a的关系
class Animal {
protected:string _name = "动物"; // 名字int _age = 1;         // 年龄
};
class Cat : public Animal {
public:void Action() { cout << "会捉老鼠" << endl; }
};
class Dog : public Animal {
public:void Action() { cout << "会看家" << endl; }
};
//“has - a” 关系(汽车与发动机)
class Engine {
protected:string _brand = "某品牌"; // 发动机品牌int _power = 150;        // 发动机功率
};
class Car {
protected:string _color = "黑色"; // 汽车颜色string _model = "SUV";  // 汽车型号Engine _engine;         // 汽车有发动机,体现has - a关系
public:void Start() { cout << "汽车启动,发动机品牌:" << _engine._brand << ",功率:" << _engine._power << endl; }
};

❤️总结

相信坚持下来的你一定有了满满的收获。那么也请老铁们多多支持一下,点点关注,收藏,点赞。❤️

http://www.dtcms.com/a/394204.html

相关文章:

  • TableGPT:浙江大学发布的表格大模型
  • Linux 概述
  • 领码学堂·定时任务新思维[二]——七大替代方案总览:场景、优缺点与快速选型
  • NLP:详解FastText
  • 【力扣】hot100系列(一)哈希部分解析(多解法+时间复杂度分析)
  • 用AI开发HTML双语阅读工具助力英语阅读
  • AI论文速读 | 当大语言模型遇上时间序列:大语言模型能否执行多步时间序列推理与推断
  • 如何使用升腾C92主机搭建本地Linux编译服务器并通过Windows映射访问共享目录
  • 测试DuckDB-rs项目中的示例程序
  • 分布式协议与算法实战-实战篇
  • 【硬件-笔试面试题-105】硬件/电子工程师,笔试面试题(知识点:详细讲讲什么是链表和数组)
  • 【获取地址栏的搜索关键字】功能-总结
  • 关于__sync_bool_compare_and_swap的使用及在多核多线程下使用时的思考
  • 【嵌入式简单外设篇】-433MHz 无线遥控模块
  • 计算机视觉(opencv)实战三十——摄像头实时风格迁移,附多种风格转换
  • 【数据分享】《中国农村统计年鉴》(1985-2024年)全pdf和excel
  • 2025年中国研究生数学建模竞赛“华为杯”C题 围岩裂隙精准识别与三维模型重构完整高质量成品 思路 代码 结果分享!全网首发!
  • [Linux]文件与 fd
  • FFmpeg 深入精讲(二)FFmpeg 初级开发
  • 睡眠脑电技术文章大纲
  • 计算机等级考试Python语言程序设计备考•第二练
  • 【Python】面向对象(一)
  • Jetson 设备监控利器:Jtop 使用方式(安装、性能模式、常用页面)
  • 「数据获取」《商洛统计年鉴》(2001-2024)
  • 链表的探索研究
  • 2025年工程项目管理软件全面测评
  • JAVA算法练习题day17
  • Nacos:服务注册和配置中心
  • Linux 命令行快捷键
  • EasyClick JavaScript Number