C++:继承

目录

一:继承的概念

1.1 继承的定义

1.2 继承方式

 1.3 可见性区别

公有方式

私有方式

保护方式

1.4 一般规则

二、继承中的隐藏规则

三、基类和派生类间的转换

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

实现一个不能被继承的类

继承与友元

五、继承与静态成员

六、多继承及其菱形继承问题

虚拟继承解决菱形继承问题

虚拟继承的底层机制

1. 虚基类指针(vbptr)

2.内存布局实例

3. 虚拟继承的代价

内存开销

访问性能

复杂性

七、继承和组合


一:继承的概念

在C++中,继承是一种面向对象编程的特性,它允许我们定义一个类(称为子类或派生类)继承另一个类(称为基类或父类)的属性和方法。通过继承,我们可以复用代码,实现代码的共享和重用,同时还可以扩展基类的功能。

通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。

1.1 继承的定义

在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。

单继承的定义格式如下:

class<派生类名>:<继承方式><基类名>
{<派生类新定义成员>
};

其中,class是关键词,<派生类名>是新定义的一个类的名字,它是从<基类名>中派生的,并且按指定的<继承方式>派生的。

多继承的定义格式如下:

class<派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{<派生类新定义成员>
};

可见,多继承与单继承的区别从定义格式上看,主要是多继承的基类多于一个。

如果省略继承方式,对'class'将采用私有继承,对'struct'将采用公有继承。

class Base1
{//
};
struct Base2
{//
};
class Base3 : Base1, Base2
{//
};

那么,Base3类将私有继承Base1,公有继承Base2,相当于:

class Base3 : private Base1, public Base2
{//
};

1.2 继承方式

公有继承、私有继承、保护继承是常用的三种继承方式。

公有继承

公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。

私有继承

私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。

保护继承

保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员或友元访问,基类的私有成员仍然是私有的。

 

public

protected

private

公有继承

public

protected

不可见

私有继承

private

private

不可见

保护继承

protected

protected

不可见

 1.3 可见性区别

公有方式

(1) 基类成员对其对象的可见性:公有成员可见,其他不可见。这里保护成员同于私有成员。

(2) 基类成员对派生类的可见性:公有成员和保护成员可见,而私有成员不可见。这里保护成员同于公有成员。

(3) 基类成员对派生类对象的可见性:公有成员可见,其他成员不可见。

所以,在公有继承时,派生类的对象可以访问基类中的公有成员;派生类的成员函数可以访问基类中的公有成员和保护成员。这里,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的。

私有方式

(1) 基类成员对其对象的可见性:公有成员可见,其他成员不可见。

(2) 基类成员对派生类的可见性:公有成员和保护成员是可见的,而私有成员是不可见的。

(3) 基类成员对派生类对象的可见性:所有成员都是不可见的。

所以,在私有继承时,基类的成员只能由派生类中的成员函数访问,而且无法再往下继承。

保护方式

这种继承方式与私有继承方式的情况相同。两者的区别仅在于对派生类的成员而言,对基类成员有不同的可见性。

上述所说的可见性也就是可访问性。关于可访问性还有另的一种说法。这种规则中,称派生类的对象对基类访问为水平访问,称派生类的派生类对基类的访问为垂直访问。

1.4 一般规则

公有继承时,水平访问和垂直访问对基类中的公有成员不受限制;

私有继承时,水平访问和垂直访问对基类中的公有成员也不能访问;

保护继承时,对于垂直访问同于公有继承,对于水平访问同于私有继承。

对于基类中的私有成员,只能被基类中的成员函数和友元函数所访问,不能被其他的函数访问。

基类与派生类的关系

任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,基类和派生类是相对而言的。

二、继承中的隐藏规则

在继承体系中,基类和派生类都有独立的作用域。派生类和基类中有同名成员,派生类成员将屏蔽基类对同名函数的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用 基类::基类成员 显示访问)

需要注意的是 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

class Person
{
protected:int _num = 111;
};class Student : public Person
{
public:void Print(){//隐藏了基类对成员的直接访问cout << "学号:" << _num << endl;//显示调用cout << "学号:" << Person::_num << endl;}
protected:int _num = 99;
};int main()
{Student s1;s1.Print();return 0;
}

我们可以看到只有使用了 基类::基类成员 的方法显示访问了以后才能输出我们想要的数据。明明继承了基类,但因为成员重名,所以隐藏了对基类成员的直接访问。

再来看下面的例子:

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);//b.fun();return 0;
};

这里是调用了B类中的fun成员函数。 然后我们放出  b.fun(); 本意是让它调用基类的成员函数,结果出现了报错。

这里也发生了隐藏,基类中的成员函数被隐藏。

这里基类成员变量和成员函数被隐藏实际上就是局部优先规则,在子类中有的话就用子类的,子类没有才去父类中寻找。

三、基类和派生类间的转换

public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

基类对象不能赋值给派⽣类对象。类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。

class Person
{
protected:string _name; // 姓名string _sex;  // 性别int _age;  // 年龄
};
class Student : public Person
{
public:int _No; // 学号
};
int main()
{Student sobj;// 1.派⽣类对象可以赋值给基类的指针Person * pp = &sobj;Person& rp = sobj;Person pobj = sobj;// 派⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错//sobj = pobj;return 0;
}

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

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

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

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

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派 生类对象先清理派生类成员再清理基类成员的顺序。

派生类对象初始化先调⽤基类构造再调派生类构造。

派生类对象析构清理先调用派生类析构再调基类的析构。

class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()构造函数" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)拷贝构造函数" << 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; //姓名
};
class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()构造函数" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)拷贝构造函数" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){//构成隐藏,所以需要显⽰调⽤Person::operator =(s);_num = s._num;}return *this;}~Student(){cout << "~Student()析构函数" << endl;}
protected:int _num; //    学号};
int main()
{Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;return 0;
}

实现一个不能被继承的类

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

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

继承与友元

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

五、继承与静态成员

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

class Person
{
public:string _name;static int _count;
};//静态成员要在类外初始化
int Person::_count = 0;class Student : public Person
{
protected:int _stuNum;
};
int main()
{Person p;Student s;// 这⾥的运⾏结果可以看到⾮静态成员	_name的地址是不⼀样的// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员 _count的地址是⼀样的// 说明派⽣类和基类共⽤同⼀份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}

六、多继承及其菱形继承问题

单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承

多继承:⼀个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后⾯。

菱形继承:菱形继承是多继承的⼀种特殊情况。

假设有下面的继承关系

class Person
{
public:int age;
};
class Student1 : public Person
{ };
class Student2 : public Person
{ };
class Bat : public Student1, public Student2 //虚继承
{ };

此时,Bat对象将包含两个Person子对象。 这会导致 数据冗余 Bat中有两份age 二义性:直接访问age需要明确路径

虚拟继承解决菱形继承问题

通过virtual 关键字声明虚拟继承,使共享基类(Animal)仅保留一个实例:

class Animal{};class Mammal : public virtual Animal{}; // 虚拟继承
class Bird : public virtual Animal{}; //虚拟继承class Bat : public Mammal, public Bird{};

此时,Bat对象中仅存在一个Animal对象,所有通过Mammal 或Bird的访问均指向该共享实例。

虚拟继承的底层机制

1. 虚基类指针(vbptr)

每个虚拟继承的派生类(如Mammal 和Bird)会包含一个虚基类指针(vbptr)。

vbptr指向一个虚基类表(vbtable),表中存储虚基类子对象相当于当前对象的偏移量。

2.内存布局实例

对于Bat对象:

Mammal和Bird的vbptr均指向各自的虚基类表,表中记录如何找到共享的Animal子对象。

构造顺序优先级:虚基类→非虚基类→成员变量→派生类自身

析构顺序与构造顺序严格相反:派生类自身→成员变量→非虚基类→虚基类。

3. 虚拟继承的代价
内存开销

每个虚拟继承的类需额外存储vbptr,增加对象大小

虚基类表占用额外内存

访问性能

访问虚基类成员需通过vbptr间接寻址,比直接访问多一步指针跳转。

复杂性

构造顺序需显示管理,尤其是存在多个虚基类时。

调试困难,内存布局更复杂。

七、继承和组合

(1)public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
(2)组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
(3)优先使用对象组合,而不是类继承 。
(4)继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
(5)对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
(6)实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

class A
{//....
};
class B
{//...
protected:A _a;
};

像上面的这种定义形式就是组合,即在B类中会有A类的对象,并且会使用A类内的部分成员函数或成员变量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/131.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

十二种存储器综合对比——《器件手册--存储器》

存储器 名称 特点 用途 EEPROM 可电擦除可编程只读存储器&#xff0c;支持按字节擦除和写入操作&#xff0c;具有非易失性&#xff0c;断电后数据不丢失。 常用于存储少量需要频繁更新的数据&#xff0c;如设备配置参数、用户设置等。 NOR FLASH 支持按字节随机访问&…

C语言高频面试题——malloc 和 calloc区别

在 C 语言中&#xff0c;malloc 和 calloc 都是用于动态内存分配的函数&#xff0c;但它们在 内存初始化、参数形式 和 使用场景 上有显著区别。以下是详细的对比分析&#xff1a; 1. 函数原型 malloc void* malloc(size_t size);功能&#xff1a;分配 未初始化 的连续内存块…

Qt -对象树

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【暂无】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 目录 前言构造QObject::QObjectQObjectPrivate::setParent_helper 析构提醒 #mermaid-svg-FTUpJmKG24FY3dZY {font-family:"trebuchet ms",verdana,arial,sans-s…

JavaScript与TypeScript

TypeScript 和 JavaScript 都是用于构建 Web 应用的编程语言&#xff0c;但它们有着不同的设计目标和特性。 一、JavaScript 1. 定义与特点 动态脚本语言&#xff1a;由 Brendan Eich 在 1995 年创建&#xff0c;最初用于浏览器端的交互逻辑。弱类型/动态类型&#xff1a;变量…

教育行业网络安全:守护学校终端安全,筑牢教育行业网络安全防线!

教育行业面临的终端安全问题日益突出&#xff0c;主要源于教育信息化进程的加速、终端设备多样化以及网络环境的开放性。 以下是教育行业终端安全面临的主要挑战&#xff1a; 1、设备类型复杂化 问题&#xff1a;教育机构使用的终端设备包括PC、服务器等&#xff0c;操作系统…

Linux常见指令介绍中(入门级)

1. man 在Linux中&#xff0c;man命令是用于查看命令手册页的工具&#xff0c;它可以帮助用户了解各种命令、函数、系统调用等的详细使用方法和相关信息。 用法&#xff1a;在终端中输入man加上要查询的命令或工具名称&#xff0c;例如man ls&#xff0c;就会显示ls命令的手册…

linux安装mysql数据库

1.判断系统是多少位的 file /sbin/init2.下载linux安装包 5.7.25.64位安装包链接&#xff1a;https://pan.baidu.com/s/13vFuRikwJaI96K0AmUQXzg提取码&#xff1a;ga7h其他版本安装 去官网下载&#xff1a;https://dev.mysql.com/downloads/mysql/3.创建mysql文件夹 mkdir /…

基于超启发鲸鱼优化算法的混合神经网络多输入单输出回归预测模型 HHWOA-CNN-LSTM-Attention

基于超启发鲸鱼优化算法的混合神经网络多输入单输出回归预测模型 HHWOA-CNN-LSTM-Attention 随着人工智能技术的飞速发展&#xff0c;回归预测任务在很多领域得到了广泛的应用。尤其在金融、气象、医疗等领域&#xff0c;精确的回归预测模型能够为决策者提供宝贵的参考信息。为…

深度解析算法之位运算

33.常见位运算 1.基础位运算 << 左移操作符 > >右移操作符号 ~取反 &按位与&#xff1a;有0就是0 |按位或&#xff1a;有1就是1 ^按位异或&#xff1a;相同为0&#xff0c;不用的话就是1 /无进位相加 0 1 0 0 1 1 0 1 0 按位与结果 0 1 1 按位或结果 0 0 1 …

python生成项目依赖文件requirements.txt

文章目录 通过pip freeze去生成通过pipreqs去生成 通过pip freeze去生成 pip freeze > requirements.txt会将整个python的Interceptor的环境下lib包下所有的依赖都生成到这个文件当中&#xff0c;取决于我们使用的python的版本下所有的安装包。不建议使用这种方式&#xff…

C++11特性补充

目录 lambda表达式 定义 捕捉的方式 可变模板参数 递归函数方式展开参数包 数组展开参数包 移动构造和移动赋值 包装器 绑定bind 智能指针 RAII auto_ptr unique_ptr shared_ptr 循环引用 weak_ptr 补充 总结 特殊类的设计 不能被拷贝的类 只能在堆上创建…

C语言之高校学生信息快速查询系统的实现

&#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 C语言之高校学生信息快速查询系统的实现 目录 任务陈述与分析 问题陈述问题分析 数据结构设…