【C++】深层次了解继承,从基础概念到复杂菱形继承问题(文章结尾有菱形继承常见面试题)

news2025/4/4 5:11:41

1.继承的概念及定义

继承的概念

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

继承的概念并不是固定的,只要能够通过自己的语言组织起来,再结合一些常见实例解释就可以了。

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18;			//年龄
};

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


class Teacher : public Person
{
protected:
	int _jobid; // 工号
};

int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}

继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了

Student和Teacher复用了Person的成员; 下面我们使用visual studio监视窗口查看Student和Teacher对象,可以看到变量的复用,调用Print可以看出成员函数的复用。

继承的定义

定义格式

以学生(Student)类为例,继承Person类;

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

三种继承方式和三种访问限定符

子类的三种继承方式:public、protected、private

父类中三种访问限定符:public、protected、private

下面这张表是继承方式、访问限定符的变化对子类成员的影响:

示例:

(1).父类中的public成员,公有继承

class Person
{
public:
    void show()
    {
        cout << "name:" << name << " " << "age:" << age << endl;
    }
    string _name;
    int _age;
};
class Student : public Person
{};
int main()
{
    Student s1;
    s1.name = "阿飞";
    s1.age = 19;
    s1.show();
    return 0;
}

Student中没有任何成员,只有从Person类中继承下来的name和age。

(2).父类的protected成员,公有继承

同样使用Person类,只是把成员变量name和age改为了protected:

class Person
{
public:
    void show()
    {
        cout << "name:" << name << " " << "age:" << age << endl;
    }
protected:
    string _name;
    int _age;
};

注意:子类继承之后成员属性为protected,不能在类外进行访问。

protected属性的成员在类内是可以访问的,可以在类内设置接口进行访问。

class Student : public Person
{
public:
    void Set(string m_name, int m_age)
    {
        name = m_name;
        age = m_age;
    }
};
int main()
{
    Student s1;
    s1.Set("阿飞", 19);
    return 0;
}

类外无法访问类内的protected/private成员,但是可以设置公有的接口对类内的protected/private成员进行访问。

(3)父类的private成员,公有继承

上面提到,父类的private成员在子类中是不可见的,那么这个不可见是什么含义呢?

class Person
{
public:
    void show()
    {
        cout << "name:" << name << " " << "age:" << age << endl;
    }
private:
    string name;
    int age;
};

在子类Student中设置公有的属性去访问父类中的private是否可行?

不可见: 子类继承父类的成员在类内和类外都无法进行访问。

一般的话,我们不会设置父类的成员为private,除非不想被子类继承的成员。

总结:

  1. 父类中的private成员在子类中无论以什么方式继承都是不可见的(语法上限制子类对象不管是在类内还是类外都不能访问)
  2. 如果子类成员不想在类外被访问,但需要在类内访问的,父类中就可以定义为protected。
  3. 父类其他成员在子类中的访问方式 为继承方式和访问限定符中权限小的一个(public > protected > private)
  4. 使用class定义类时默认的继承方式是private,使用struct默认继承方式为public,不过最好显示写出继承方式。
  5. 实际运用中一般使用public继承,很少用到protected和private继承。

2.父类和子类对象赋值转换

  • 子类对象可以赋值给父类对象/父类指针/父类的引用,这种也叫切片或者切割;寓意把子类中父类那部分切来赋值过去
  • 父类对象不能赋值给子类对象
  • 父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用,但是必须是父类的指针指向子类对象时才安全
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;
	Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;

	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
	ps2->_No = 10;

    //父类的引用赋值给子类的引用
    rp = sobj;
	Student& ps3 = (Student&)rp;
}

3.继承中的作用域

每一个变量都有其对应的作用域,类中也有属于自己的类域;而且不同的类有不同的类域;

父类和子类中的成员在不同的类域中。

  1. 在继承体系中父类和子类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类中的成员, 对子类中同名成员直接访问; 这种情况叫隐藏, 也叫重定义(在子类成员函数中,可以使用 基类::基类成员 直接访问)
  3. 需要注意的是,如果是成员函数的隐藏,只需要函数名相同即可
  4. 注意在实际的继承体系中,尽量不要出现同名的成员

示例1:

// 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;
    }
protected:
    int _num = 999; // 学号
};
void Test()
{
    Student s1;
    s1.Print();
};

示例2:

// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
    void fun()
    {
        cout << "func()" << endl;
    }
};
class B : public A
{
public:
    void fun(int i)
    {
        A::fun();
        cout << "func(int i)->" <<i<<endl;
    }
};
void Test()
{
    B b;
    b.fun(10);
};

注意:构成隐藏的成员函数,在进行调用时一定要满足参数的匹配,否则会出现调用错误。

4.子类的默认成员函数

6个默认成员函数,“默认”的意思就是我们不写,编译器会自动生成;那么在子类中,这几个成员函数是如何生成的呢?

  • 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员,如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用; 子类的部分调用子类的构造函数(内置类型不做处理,自定义类型调用自定义类型的默认构造)

下面的代码对应的就是父类中没有默认构造,只有有参构造时的情况:

Student(const char* name, int num)
		: Person(name)
		, _num(num)
{
    cout << "Student()" << endl;
}
  • 子类的拷贝构造必须调用父类的拷贝构造完成父类成员的初始化,子类成员调用子类构造函数完成初始化
  • 子类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; //学号
};


void Test()
{
	Student s1("jack", 18);
}

5.继承与友元

友元函数的概念

某些虽然不是类中的成员却能够访问类的所有成员(包括protected和private)的函数称为友元函数

class Person
{
	friend void Display(const Person& p);//Display是Person类的友元函数
public:
    void SetName(const string& name)
    {
        _name = name;
    }
protected:
	string _name; // 姓名
};
void Display(const Person& p)
{
    cout << p._name << endl;
}
int main()
{
    Person p;
    p.SetName("阿飞");
    Display(p);
    return 0;
}


继承中的友元函数

友元关系不能继承,也就是说父类的友元函数不能访问子类中的protected和private成员。

class Student;
class Person
{
public:
	friend void Display1(const Person& p);
	friend void Display2(const Student& s);
protected:
	string _name= "阿飞"; // 姓名
};

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

void Display1(const Person& p)
{
	cout << p._name << endl;
}

void Display2(const Student& s)
{
	cout << s._stuNum << endl;

}
int main()
{
	Person p;
	Display1(p); //正常运行

	Student s;
	Display2(s); //编译报错
	return 0;
}

注意:如果需要访问子类中的protected/private成员,可以把函数声明为子类的友元。

class Student : public Person
{
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; 
};

6.继承与静态成员

一般成员在子类和父类中都是单独的一份,而静态成员在父类和子类中是同一份。

示例:

class Person
{
public:
	static int _count; 
};
int Person::_count = 0;	//static类内声明,类外初始化
class Student : public Person
{};

int main()
{
    Person p;
    Student s;
    cout << s._count << endl;
    Person::_count++;
    cout << s._count << endl;
    //打印一下父类和子类中静态成员的地址
    cout << &Person::_count << endl;
    cout << &Student::_count << endl;
    return 0;
}

注意:子类对象和父类对象中的静态成员_count是同一份,改变父类对象中的_count,子类对象中的_count也会随之改变。

7.菱形继承和菱形虚拟继承

菱形继承的概念

在提出菱形继承的概念之前,首先看一下单继承和多继承;

单继承:只有一个直接父类。

多继承:有两个或者两个以上的直接父类。

菱形继承其实是多继承的一种特殊情况:

菱形继承存在的问题:

如图,可以看出菱形继承有数据冗余二义性的问题(在Assistant对象中Person成员有两份)

示例:

class Person
{
public:
	string _name; // 姓名
};

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

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	//a._name = "peter";//error C2385: 对“_name”的访问不明确
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余问题,但是在其他地方不要随便使用虚拟继承。

class Person
{
public :
	string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
	int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
	int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
	string _majorCourse ; // 主修课程
};
void Test ()
{
    Assistant a ;
    a._name = "peter";
}

为了研究虚拟继承的原理,我们给出一个简化的菱形继承模型,再借助visual studio的调试内存窗口来观察一下:

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	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;
}

非虚拟继承时,继承时未使用virtual

继承体系中B中和C中都有一份A,导致数据冗余;下面看一下虚拟继承时如何解决这个问题的。

虚拟继承,继承时加上virtual关键字

下图是虚拟继承的内存对象成员模型:_a同时属于B和C,那么B和C如何去找公共的_a呢?

这里通过了B和C中的两个指针,指向的一张表;这两个指针叫虚基表指针,这两个表叫虚基表;虚基表中存的是偏移量,通过偏移量可以找到A中的成员_a。

虚拟继承中的切片情况:

int main()
{
    D d;
    B b = d;
    B* pb = &d;
    pb->_a = 10;
    return 0;
}

由于_a在B和C中是公共的,在切片时是不能直接放在B或者C中的,所以在这里访问_a还是需要虚基表指针来找到偏移量,然后偏移量加上当前对象的地址,来访问_a。

8.继承的总结和反思

  1. C++语法虽然支持多继承,但是一般是不建议设计出多继承的,因为多继承问题比较复杂,而且多继承问题可能引发菱形继承的问题,导致我们所研究的问题更复杂。
  2. 多继承可以认识是C++的缺陷之一,很多的语言是不支持多继承的,如我们所熟悉的Java。

继承和组合

  • public继承是一种is-a的关系;例如 Student is-a Person
  • 组合是一种has-a的关系;例如 车has-a轮胎(a只是一个量词,实际不一定是一个)
  • 如果两个物体之间的关系既可以是is-a,也可以使has-a,那么优先使用has-a的组合,在编程中,我们追求的是一种“高内聚,低耦合”:因为组合中耦合度减低
  • 当继承和组合都可以解决问题时,优先使用组合

9.笔试面试题

1.什么是菱形继承?菱形继承的问题时什么?

两个子类同时继承一个父类,而且又有一个子类同时继承这两个类,这就是我们通常所说的菱形继承;

菱形继承的问题在于数据冗余和二义性。

2.什么是菱形虚拟继承?如何解决数据冗余和二义性的?

两个子类同样同时继承一个父类,但是不同于菱形继承的是,继承时是一种虚拟继承(使用virtual关键字完成);同样有一个子类继承同时继承这两个类。

继承最开始的那个父类,它的成员在它的子类中不会存储多份,而是只存储了一份,在它的子类中各有一个指针(这个指针也叫虚基表指针),这个指针指向一个虚基表,虚基表中存储的有父类成员的偏移量,通过这个偏移量加上自身对象的地址,找到的就是父类中的那个成员;因为这里子类继承之后父类的成员只有一份,所以就解决了数据冗余和二义性。

3.继承和组合的区别?什么时候用继承?什么时候用组合?

继承是一种is a的关系,像我们常说的Person和Student类,Student is a Person,而组合是一种has a的关系,像一辆汽车has a 油箱,这里的has a 的a是一个量词,并不是一个,像一辆小汽车是可以有四个轮胎的。

继承中父类的protected成员在子类中public继承在类内是可以访问到的,增加了代码的耦合性;而组合中是类中包含这个自定义类型,像一个car类中包含这个轮胎类,轮胎类中有其他的一些成员,这种耦合度很低;

我们的程序设计追求的是一种“高内聚,低耦合”,所以如果继承和组合都可以使用的时候,要尽量的使用组合,当组合不能实现时,再考虑使用继承来完成。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅聊webpack的工作原理

参考文献: https://webpack.docschina.org/concepts/ 简述一下 WebPack 是一个模块打包工具&#xff0c;可以使用 WebPack 管理模块。在 webpack 看来&#xff0c;项目里所有资源皆模块&#xff0c;分析模块间的依赖关系&#xff0c;最终编绎输出模块为 HTML、JavaScript、CS…

C++OpenCV(5):图像模糊操作(四种滤波方法)

&#x1f506; 文章首发于我的个人博客&#xff1a;欢迎大佬们来逛逛 &#x1f506; OpenCV项目地址及源代码&#xff1a;点击这里 文章目录 图像模糊操作均值滤波高斯滤波中值滤波双边滤波 图像模糊操作 关于图片的噪声&#xff1a;指的是图片中存在的不必要或者多余的干扰数…

MySQL-多表查询-案例1

案例 根据需求完成多表查询的SQL语句的编写将资料汇中准备好的数据的SQL脚本导入到数据库中准备数据中各表的关系如下 具体代码 -- 分类表 create table category(id int unsigned primary key auto_increment comment 主键ID,name varchar(20) not null unique comment 分类名…

ARM练习

通过汇编语言完成LED1-3循环点亮练习 .text .global _start _start: /**********LED1点灯**************/ /*初始化RCC*/ RCC_INIT:LDR R0,0X50000A28LDR R1,[R0]ORR R1,R1,#(0X1<<4)ORR R2,R1,#(0x1<<5)STR R1,[R0]STR R2,[R0]LED1_INIT:设置输出模式LDR R0,0X5…

Spring(10) 生成和替换Banner启动图案

目录 1.背景2.推荐网站3.如何集成到spring项目中4.效果展示 1.背景 我们在启动 Spring 项目的时候经常会看到一个 Spring 字样的启动图案。如下所示&#xff1a; 如果我们也想根据我们的内容生成这样的图案&#xff0c;应该怎么操作呢&#xff1f; 2.推荐网站 可以生成这种图…

Docker 制作镜像

自定义制作镜像 我们学习了Dockerfile语法,那么如何应用Dockerfile制作自定义的镜像呢?那今天我们就来实战一下,以主流的微服务Jar 为例子,开启我们自定义制作镜像之旅。 建立简单Springboot项目,并打包成jar 简历demo项目(访问路径 /start/springboot)配置端口,以及利…

P2196 [NOIP1996 提高组] 挖地雷

[NOIP1996 提高组] 挖地雷 题目描述 在一个地图上有 N ( N ≤ 20 ) N\ (N \le 20) N (N≤20) 个地窖&#xff0c;每个地窖中埋有一定数量的地雷。同时&#xff0c;给出地窖之间的连接路径。当地窖及其连接的数据给出之后&#xff0c;某人可以从任一处开始挖地雷&#xff0c;…

飞行动力学 - 第14节-飞机的配平 之 基础点摘要

飞行动力学 - 第14节-飞机的配平 之 基础点摘要 1. 最大上偏配平角2. 重心前限3. 配平曲线4. 空气压缩性影响 & 配平曲线5. 马赫速配平曲线6. 地面效应7. 地效的影响8. 参考资料 1. 最大上偏配平角 升降舵下偏为正从操纵性的角度&#xff0c;重心应该位于【重心前限】 X c…

投个 3D 冰壶,上班玩一玩

本篇文章将介绍如何使用物理引擎和图扑 3D 可视化技术来呈现冰壶运动的模拟。 Oimo.js 物理引擎 Oimo.js 是一个轻量级的物理引擎&#xff0c;它使用 JavaScript 语言编写&#xff0c;并且基于 OimoPhysics 引擎进行了改进和优化。Oimo.js 核心库只有 150K &#xff0c;专门用…

408计算机考研-101-数据结构-基本概念

数据结构 数据结构(Data Structure)是计算机存储、组织数据的方式。 数据结构分为逻辑结构和物理结构(存储结构) 逻辑结构 逻辑结构是指数据之间的相互关系和组织方式。 按照数据元素之间的关系不同&#xff0c;可以分为以下4种&#xff1a; 集合结构线性接口树结构图结构 …

华为OD机试真题 Java 实现【数字游戏】【2023 B卷 100分】,附详细解题思路

TOC 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

操作系统练习:进程间通信(共享内存方式)

说明 本文是《操作系统概念(第九版)》3.4节“进程间通信”的练习。 进程间通信主要由两种模型&#xff1a; 共享内存消息传递 本文使用共享内存的方式实现进程间的通信 创建消息生产者 创建生产者的主要操作包括&#xff1a; 定义共享内存的大小、名称&#xff0c;以及通…

SJA1000的简单调试

文章目录 基本过程SJA1000波特率计算公式验收滤波器使用其他关于CLKOUT测试寄存器初始化过程中会产生中断扩展帧、标准帧的区分计算器 基本过程 SJA1000的接口连接到FPGA上&#xff0c;采用软核进行CAN数据的收发。调试花了1天多的时间&#xff0c;有点波折&#xff0c;下面按…

最新版edge浏览器中安装xpath插件

最近在跟着尚硅谷进行爬虫的学习&#xff0c;老师给出了在谷歌浏览器安装Xpath插件的方法&#xff0c;由于电脑上面未安装谷歌浏览器&#xff0c;所以在网上搜索了在edge上安装Xpath插件的方法&#xff0c;安装完成以后发现使用快捷键 CTRL SHIFTX不能够打开&#xff0c;以为是…

[java安全]CommonsCollections2

文章目录 【java安全】CommonsCollections2前言Commons-Collections4版本中能否调用cc6等链子&#xff1f;PriorityQueue利用链PriorityQueueTransformingComparator**使用了phithon的总结&#xff1a;**POC 进阶POCPOC分析调用链 【java安全】CommonsCollections2 前言 Apac…

【Flutter问题记录】Android Studio不显示(右上角main.dart左边)设备栏

记录一下今天遇到的情况&#xff1a; 用android studio打开项目&#xff0c;我检查了&#xff0c;已经配置了flutter sdk和android sdk&#xff0c;但是右上角main.dart左边的设备栏就是不显示。 解决方法&#xff1a; 恢复如初&#xff1a;

【密码学】三、分组密码概述

分组密码 1.分组密码简介2.分组密码的基本原理2.1代换2.2扩散2.3混淆 3.分组密码的结构3.1Feistel网络3.1.1平衡Feistel网络3.1.2不平衡Feistel网络 3.2SP网络 4.分组密码的设计 分组密码算法是将输入数据划分成固定长度的组进行加密和解密的一类对称密码算法。本章主要介绍分组…

零基础深度学习——学习笔记1 (逻辑回归)

前言 因为各种各样的原因要开始学习深度学习了&#xff0c;跟着吴恩达老师的深度学习视频&#xff0c;自己总结一些知识点&#xff0c;以及学习中遇到的一些问题&#xff0c;以便记录学习轨迹以及以后复习使用&#xff0c;为了便于自己理解&#xff0c;我会将一些知识点用以个…

MySQL 中NULL和空值的区别

MySQL 中NULL和空值的区别&#xff1f; 简介NULL也就是在字段中存储NULL值&#xff0c;空值也就是字段中存储空字符(’’)。区别 1、空值不占空间&#xff0c;NULL值占空间。当字段不为NULL时&#xff0c;也可以插入空值。 2、当使用 IS NOT NULL 或者 IS NULL 时&#xff0…

了解 3DS MAX 3D摄像机跟踪设置:第 4 部分

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 1. 项目设置 步骤 1 打开“后效”。 打开后效果 步骤 2 转到合成>新合成以创建新合成。 将“宽度”和“高度”值分别设置为 1280 和 720。将帧速率设置为 25&#xff0c;将持续时间设置为 12 秒。单…