C++(第十一篇):继承(基类与派生类、菱形继承和菱形虚拟继承问题)

news2024/11/15 18:05:39

📒博客主页:Morning_Yang丶
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文所属专栏:【C++拒绝从入门到跑路】
🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!

文章目录

    • 一、前言
    • 二、继承的概念
    • 三、继承的方式
    • 四、基类 & 派生类的赋值转换
    • 五、继承中的作用域 & 隐藏
    • 六、派生类的默认成员函数
    • 七、继承与友元函数
    • 八、继承与静态成员
    • 九、菱形继承与菱形虚拟继承
      • 9.1 继承关系
      • 9.2 菱形继承中存在的问题
      • 9.3 通过虚拟继承解决问题
      • 9.4 虚拟继承解决问题的原理
    • 十、继承的总结与反思
    • 十一、知识巩固题

一、前言

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类去继承一个已有的类即可。这个已有的类称为基类(父类),新建的类称为派生类(子类)


二、继承的概念

继承(inheritance)机制是面向对象程序设计中使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

如果我们需要描述一个学校里面的学生和教职工,有姓名、性别、专业/部门、学号/工号等信息,该怎么做呢?把学生和教职工中共有的信息拿出来创建一个 Person 类,新建两个类 Student 和 Teacher,继承 Person 类,它们就拥有了 Person 类中的数据成员和成员函数,实现代码复用。

image-20220126205713461

image-20220809171638843

// 基类
class Person {
	// _name 姓名
	// _age 性别
};

// 派生类
class Student : public Person {
	// _stuID 学号
	// _major 专业
};

// 派生类
class Teacher : public Person {
	// _teaID 工号
	// _department 部门
};

三、继承的方式

继承方式有以下三种,通过不同的访问限定符来指定继承的方式。

但我们几乎不使用 protectedprivate 继承,通常使用 public 继承。

  • 公有继承(public)
  • 保护继承(protected)
  • 私有继承(private)

继承产生了多种组合:

继承方式 / 基类成员基类的 [ 公有 ] 成员基类的 [ 保护 ] 成员基类的 [ 私有 ] 成员
公有继承(public)成为派生类的公有成员成为派生类的保护成员在派生类中不可见
保护继承(protected)成为派生类的保护成员成为派生类的保护成员在派生类中不可见
私有继承(private)成为派生类的私有成员成为派生类的私有成员在派生类中不可见

我们如何给[基类]成员设置合适的访问限定符:

  • 基类成员想让它被所有人访问,就设置成公有
  • 基类成员不想让它在类外被直接访问,但需要在派生类中被访问,就设置成保护
  • 基类成员不想让它在类外被直接访问,也不想让它在派生类中被访问,就设置成私有
  • 但我们使用继承时,很少在基类中设计 private 成员,通常设计 publicprotected 成员。

总结

  1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见并不是说不存在,而是指基类的私有成员还是被继承到了派生类中(派生类对象的内存空间中存在),只是语法上限制派生类对象不管在类里面还是类外面都不能直接去访问它。只能通过调用基类的公有保护成员函数来访问。

    image-20220126215233020

  2. 基类 private 成员在派生类中是不能被访问,如果基类成员不想让它在类外被直接访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的。(我们之前说过,创建类时,不想在类外被访问的成员设置成 private / protected,可私有和保护到底有啥区别?继承这里就正好体现出来了)。

  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符与继承方式比较,取权限更小的),public > protected > private。

  4. 使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,不过最好显示的写出继承方式

  5. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced / private 继承,也不提倡使用 protetced / private 继承,因为 protetced / private 继承下来的成员都只能在派生类的类里面使用,甚至不能使用,实际中扩展维护性不强。


一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

四、基类 & 派生类的赋值转换

派生类继承了基类的所有成员,那么我们可以把派生类对象赋值给基类吗?

注意:==我们这里讨论都是公有继承的情况下!==因为保护和私有继承后,「派生类中的基类数据成员」访问权限可能会发生变化,赋值过去后,基类又可以按照它原来的访问权限来操作「派生类中的基类数据成员」,这是有问题的!!!

  • 派生类对象 可以赋值给 基类对象 / 基类对象指针 / 基类对象引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分数据成员切来赋值过去。(切的是数据成员赋值过去,因为同一个类的各对象中只储存各自的数据成员,成员函数共用一份,单独存放在对象之外的另一段存储空间中)

    image-20220127120335218

  • 基类对象不能赋值给派生类对象。

  • 基类对象指针(或引用)可以通过强制类型转换赋值给派生类对象指针。但是必须是基类指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run-Time Type Information)的 dynamic_cast 来进行识别后进行安全转换。

代码如下:

#include<iostream>
#include<string>
using namespace std;

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

// 派生类
class Student : public Person {
protected:
	int id;  //学号
};

int main() {
	Student stu;
	// 1.派生类对象赋值给基类对象、基类指针、基类引用
	Person per = stu;
	Person* ptr = &stu;
	Person& ref = stu;

	// 2.基类对象不能赋值给派生类对象
	// stu = per;
	
	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针,但最好用dynamic_cast进行转换,这样才是安全的

	return 0;
}

五、继承中的作用域 & 隐藏

  1. 在继承体系中 基类派生类 都有 独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 建议自己实际使用中,在继承体系里面最好不要定义同名的成员

【实例1】如下代码中, Student_num 成员和 Person_num 成员构成隐藏关系

// 当派生类和基类有同名成员时,派生类会隐藏基类成员,可以看出这样代码虽然能跑,但是非常容易混淆

// 基类
class Person {
protected:
	int _num = 10;  //序号
};

// 派生类
class Student : public Person {
public:
	void print() {
		cout << _num << endl;          //打印的是派生类的
		cout << Person::_num << endl;  //打印基类的必须指明类域
	}
protected:
	int _num = 20;  //序号
};

int main() {
	Student stu;
	stu.print();
	return 0;
}

【实例2】如下代码中, Bfun(int i) 成员函数和 Afun() 成员函数构成隐藏关系

// B中的fun和A中的fun不构成函数重载,因为在不同作用域
// B中的fun和A中的fun构成隐藏关系,在继承中,只要函数名相同就构成隐藏

// 基类
class A {
public:
	void fun() { 
		cout << "fun()" << endl; 
	}
};

// 派生类
class B : public A {
public:
	void fun(int i) { 
		cout << "fun(int i)" << endl;
	}
};
	
int main() {
	B b;
	b.A::fun();  // 调用基类的fun()必须指明类域
	b.fun(1);    // 调用派生类的fun()
	return 0;
}

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

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

image-20220127170427527


1)派生类的「构造函数」(没写编译器会自动生成),在初始化列表中,它会做这三件事:

  1. 对于继承的基类成员 ------------> 把它作为一个整体,先自动调用基类的默认构造函数初始化
  2. 对于类中的内置类型成员 ------> 不处理(除非声明时给了缺省值)
  3. 对于类中的自定义类型成员 —> 自动调用它的默认构造函数(不要参数就可以调用的,比如 无参构造函数 或 全缺省构造函数)

image-20220128184442657

2)派生类的「拷贝构造函数」(没写编译器会自动生成),在初始化列表中,它会做这三件事:

  1. 对于继承的基类成员 ------------> 把它作为一个整体,先自动调用基类的拷贝构造函数来完成拷贝初始化
  2. 对于类中的内置类型成员 ------> 值拷贝
  3. 对于类中的自定义类型成员 —> 自动调用它的拷贝构造函数来完成拷贝初始化

image-20220128184024234

3)派生类的「赋值重载函数」(没写编译器会自动生成),它会做这三件事:

  1. 对于继承的基类成员 ------------> 把它作为一个整体,调用基类的拷贝赋值函数来完成赋值初始化
  2. 对于类中的内置类型成员 ------> 值拷贝
  3. 对于类中的自定义类型成员 —> 调用它的赋值重载函数来完成赋值初始化

4)派生类的「析构函数」(没写编译器会自动生成),它会做这三件事:

  1. 对于类中的内置类型成员 ------> 不处理
  2. 对于类中的自定义类型成员 —> 调用它的析构函数完成清理工作
  3. 对于继承的基类成员 ------------> 把它作为一个整体,派生类的析构函数调用完成后,会自动调用基类的析构函数完成清理工作

5)派生类的「取地址重载」(分为普通对象和 const 对象),平时很少自己去实现,了解下就好

  1. 这两个函数里就不用调用基类的,取自己的地址就行了

代码如下

image-20220602142846809

// 基类
class Person {
public:
	// 默认构造函数
	Person(const char* name)
		:_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 id)
		:Person(name)  // 显示调用基类的构造函数
		,_id(id)
	{}

	// 如果我们要自己实现派生类的拷贝构造,就要像下面这样写
	// 但一般编译器默认生成的就可以,如果派生类中存在深拷贝,才需要自己实现
	Student(const Student& s)
		:Person(s)  // 必须显示调用基类的拷贝构造(这里会发生切片)
		,_id(s._id)
	{}

	// 自己实现派生类的赋值重载函数
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			_id = s._id;
			Person::operator=(s);  // 必须显示调用基类的赋值重载(这里会发生切片)
		}
		return *this;
	}

	~Student()
	{
		// 先清理自己的资源……

	}  // 结束后会自动调用父类的析构函数
protected:
	int _id;      //学号
};

int main() {
	Student stu1("张三", 1);   // 调用构造函数
	Student stu2(stu1);       // 调用拷贝构造函数

	Student stu3("李四", 2);
	stu1 = stu3;              // 调用重载赋值函数

	return 0;
}

总结 & 一些要注意的细节

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。(多继承时,按照基类声明顺序(继承顺序)依次初始化)

    构造函数初始化列表阶段会自动调用基类的默认构造函数,如果基类没有不传参的默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用,否则会报错。

  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的那一部分成员的拷贝初始化。

    如果你要自己实现派生类的拷贝构造函数,就必须在实现的派生类拷贝构造函数的初始化列表中显示调用基类的拷贝构造,

    如果你自己不显示调用,初始化列表阶段会自动调用基类的默认构造函数(因为拷贝构造和构造都是构造函数,而编译器只会自动调用不传参的默认构造),所以就不会去调用基类的拷贝构造了,导致无法正常完成拷贝工作。

  3. 派生类的 operator=( ) 必须要显示调用基类的 基类::operator=( ) 完成基类的复制。

  4. 派生类的析构函数会在被调用完成后,会自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员,再清理基类成员的顺序。(要符合先定义的先析构)

    • 派生类的析构函数和基类的析构函数构成隐藏关系。

      – 因为编译期会对析构函数名做特殊处理,所有类的析构函数名都会被处理成统一的名字: destructor()

    • 为什么编译器会做这个处理呢?-- 因为析构函数要构成多态的重写(重写有个要求就是函数名要相同)

    • 子类的析构函数在执行结束后会自动调用父类的析构函数。

      – 因为创建派生类对象时,先创建初始化了基类成员,再创建初始化了派生类成员。所以派生类对象析构清理先调用派生类析构函数清理派生类成员后,再调用基类析构函数清理基类成员。

  5. 派生类对象初始化先调用基类构造再调派生类构造。

image-20220809172103347


核心要点

核心思想是把派生类中继承的基类成员当作一个整体,就像一个自定义类型的变量一样来处理它,所以对于派生类,不管是构造、拷贝构造、赋值重载、析构,都要调用基类的对应函数才成完成相应操作。


七、继承与友元函数

友元关系不能继承,也就是说基类友元不能访问派生类的私有和保护成员。相当于你爹的朋友不一定是你的朋友。

// 声明派生类
class Student;

// 基类
class Person
{
public:
	friend void Display(const Person& p, const Student& s);  // 友元函数
protected:
	string _name;  // 姓名
};

// 派生类
class Student : public Person
{
protected:
	int _id;  // 学号
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;  // 访问基类的保护成员
	cout << s._id << endl;    // 不能访问派生类的保护成员,因为友元关系不能继承下来
}

int main()
{
	Person per;
	Student stu;
	Display(per, stu);
	return 0;
}

八、继承与静态成员

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一
个 static 成员实例。(静态成员不会被继承下来,为该基类和它下面的派生类的所有对象共享)

// 基类
class Person
{
public:
	Person() { ++_count; }
public:
	static int _count;  // 统计人的个数
protected:
	string _name;  // 姓名
};

int Person::_count = 0;  // 定义静态数据成员(必须在类外定义)

// 派生类
class Student : public Person
{
protected:
	int _id;  // 学号
};

int main()
{
	Person p;
	Student s;
	cout << "共创建了" << Person::_count << "个对象" << endl;  // 使用基类域访问
	cout << "共创建了" << Student::_count << "个对象" << endl; // 使用派生类域访问
	cout << "共创建了" << p._count << "个对象" << endl;  // 使用基类对象访问
	cout << "共创建了" << s._count << "个对象" << endl;  // 使用派生类对象访问
	return 0;
}

九、菱形继承与菱形虚拟继承

9.1 继承关系

  • 单继承:一个子类只有一个直接父类时称这个继承关系为单继承

    image-20220201172308204

  • 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

    image-20220201172342517


9.2 菱形继承中存在的问题

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

image-20220201172424885

多继承本身没有什么问题,但因为C++支持多继承,就可能会出现菱形继承,从而引发一些问题,我们在这里会探究下具体问题和解决办法以及解决办法的原理,需要注意的是,实际中几乎不会去设计菱形继承和菱形虚拟继承的。

菱形继承的问题:观察上图,以及从下面的对象成员模型中可以看出菱形继承有数据冗余和二义性的问题Assistant 的对象中会有两份 Person 成员。

image-20220202005226941

如果 Person 成员中内容很多,会造成数据冗余。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; // 主修课程
};

int main()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "张三"; // error:对_name访问不明确

	// 需要显示指定访问的是哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "张三-学生";
	a.Teacher::_name = "张三-老师";

	return 0;
}

9.3 通过虚拟继承解决问题

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 StudentTeacher 继承 Person 时使用 virtul 虚拟继承,即可解决问题。

需要注意的是,实际使用中,很少设计菱形继承,也不建议大家使用菱形继承。

image-20220201221740864

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; // 主修课程
};

int main()
{
	Assistant a; // 虚拟继承后,Assistant的对象中就只有一份Person成员了
    // 下面三种访问方式访问到的都是同一个成员
	a.Student::_name = "张三-学生";
	a.Teacher::_name = "张三-老师";
	a._name = "张三";
	return 0;
}

9.4 虚拟继承解决问题的原理

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员模型。

image-20220201225204957

代码如下:

class A
{
public:
	int _a;
};

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

class C : virtual public A
// class C : 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;
}

分析

通过调试,观察内存窗口可看到:

下图是菱形继承的内存对象成员模型:这里可以看到数据冗余,有两份 A 成员。

image-20220201224951771

下图是菱形 虚拟 继承的内存对象成员模型:这里可以分析出在 D 对象中将「虚基类 A 的成员 _a」放到了对象组成的最下面,这个 A 的成员同时属于 B 和 C,那么 B 和 C 如何去找到公共的 A 呢?这里是通过了 B 和 C 中的两个指针,指向的一张表。这两个指针叫「虚基表指针」,这两个表叫「虚基表」。虚基表中存的「偏移量」。通过虚基表指针加偏移量来计算找到下面的 A 的成员 _a。

image-20220202001604325

菱形 虚拟 继承体系中,新创建的 B 对象或 C 对象的内存对象模型中「虚基类 A 的成员 _a」也是放到了对象组成的最下面,都要通过虚基表指针加偏移量来计算找到下面的 A 的成员 _a。

image-20220202001616515

只有菱形继承体系的腰部 B 和 C 是 virtul 虚拟继承的 A( class B : virtual public Aclass C : virtual public A ),所以只有 B 和 C 才通过偏移量去找虚基类「虚基类 A 的成员 _a」,而底部 D 是不需要通过偏移量去找「虚基类 A 的成员 _a」的。

所以C++弄这个多继承,反而还搞复杂了。


思考

  1. 可能有小伙伴还会有疑问为什么不把偏移量直接存到虚基表指针的那个位置,而是需要通过虚基表指针去找偏移量呢?这是因为虚基表是一个表,不止存放偏移量,还要存其它东西(记录虚基表指针和虚函数指针之间的相对位置,反正这个我是不研究了),后面会有其应用场景。

  2. 可能有小伙伴会有疑问为什么 D 对象中的 B 和 C 部分要去找属于自己的 A?那么大家看看当下面的赋值发生时,d 是不是要去找出 B / C 成员中的 A 才能切片赋值过去?

    D d;
    B b;
    B* p1 = &d; // B对象指针 -> D对象,把D对象切片给B对象指针
    p1->_a; // 指针访问虚基类A的成员_a
    B* p2 = &b; // B对象指针 -> B对象
    p2->_a; // 指针访问虚基类A的成员_a
    
    // 指针是无法识别自己指向的是哪个类的对象,即可能指向自己,也可能指向子类,比如上面代码,B对象和D对象中虚基类成员_a的偏移量是不一样的,所以也只能通过偏移量来计算_a的位置
    

    B或C的对象、对象指针、对象引用访问继承的虚基类 A 的对象中的成员 _a,都要取偏移量计算 _a 的位置。


结论

可以看到,虚继承后,解决了菱形继承,但是同时,对象模型更复杂了,其次访问虚基类成员也付出了一定的效率代价


下面是上面9.3的 Person 关系菱形 虚拟 继承的原理解释:

image-20220202012313441


十、继承的总结与反思

  1. 很多人说 C++ 语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是 C++ 的缺陷之一,很多后来的OO面向对象语言都没有多继承,如 Java。

面向对象系统中功能复用的两种最常用技术是 类继承对象组合

  • public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象(例如,教师是人,学生是人;哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等)。

    class B : public A
    
  • 组合是一种 has-a 的关系。假设 B 组合了 A,每个 B 对象中都有一个 A 对象。

    参考文章:优先使用对象组合,而不是类继承。

    class D // q
    {
    protected:
        A a; // 轮胎
        B b; // 车身
    };
    
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高

  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。因为对象只能通过接口访问,所以我们并不破坏封装性;组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系(is-a)就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系(is-a/has-a)可以用继承,可以用组合,就用组合。

比如:脸和眼睛、车和轮胎,这样的关系就适合用组合;人和司机、人和学生,这样的关系就适合用继承。


耦合内聚

  • 模块(类……)之间的关联度越高,独立性越低,耦合度越高
  • 模块(类……)之间的关联度越低,独立性越高,耦合度越低
  • 软件工程中强调:低耦合,高内聚,意思是模块内部关联度要高,模块之间独立性要高

十一、知识巩固题

黑体选项为易错概念

👉1、下面关于访问权限与继承权限说法不正确的是( )

A.访问权限和继承权限是不同的概念

B.访问权限和继承权限关键字上是一样的,但是出现位置不一样

C.如果是protected继承方式,基类public的成员变量能通过基类对象在类外直接访问

D.基类私有的成员变量在子类中都不能直接访问,因为没有被子类继承了

解答

A.两个权限控制的东西不一样

B.访问权限在类内部,继承权限在类外

C.只要是public成员对象都可以直接访问

D.基类私有成员不能直接访问不是没有被继承,而是权限问题

正确答案是:D


👉2、下面代码输出结果:( )

class A
{
    public:
    void f(){ cout<<"A::f()"<<endl; }
    int a;   
};
class B : public A
{
    public:
    void f(int a){cout<<"B::f()"<<endl;}
    int a;
};

int main()
{
    B b;
    b.f();
    return 0;
}

A.打印A::f()

B.打印B::f()

C.不能通过编译,因为基类和派生类中a的类型以及名称完全相同

D.以上说法都不对

解答

A.错误

B.错误

C.不能通过编译是正确的,不过原因不是因为成员变量a的问题,而是子类同名隐藏了父类方法的原因

D.很显然以上说法都不对

正确答案是:D


👉3、关于派生类构造函数与析构函数说法正确的是( )

A.在派生类对象构造时,先调用基类构造函数,后调用子类构造函数

B.在派生构造函数初始化列表的位置必须显式调用基类构造函数

C.在派生类对象销毁时,先调用基类析构函数,后调用子类析构函数

D.派生类的析构函数只需析构派生类的资源即可

解答

A.先构造父类,在构造子类 故正确

B.不一定,如果父类有默认构造函数就不需要

C.刚好相反,先调用子类,在调用父类

D.派生类的析构函数往往还需要连同父类析构函数一起调用,同时清除父类的资源

正确答案是:A


👉4、下列代码中f函数执行结束后输出( )

class A
{
public:
  A() { cout<<"A::A()"<<endl; }
  ~A() { cout<<"A::~A()"<<endl; }
  int a;
};
class B : public A
{
public:
  B() { cout<<"B::B()"<<endl; }
  ~B() {cout<<"B::~B()"<<endl; }
  int b;
};

void f()
{
  B b;
}

A.B::B() B::~B()

B.B::B() A::A() A::~A() B::B()

C.A::A() B::B() B::~B() A::~A()

D.以上都不对

解答

分析: 子类实例化对象,由于继承的有父类。所以会先构造父类,然后在构造子类,析构顺序完全按照构造的相反顺序进行析构,故答案为 C


👉5、关于基类哪些成员被子类继承说法不正确的是( )

A.静态成员函数

B.所有成员变量

C.基类的友元函数

D.静态成员变量在整个继承体系中只有一份

解答

A.静态成员函数也可以被继承

B.成员变量所有的都会被继承,无论公有私有

C.友元函数不能被继承,相当于你爹的朋友不一定是你的朋友

D.静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份

故答案为 C


👉6、关于基类与派生类对象模型说法正确的是()

A.基类对象中包含了所有基类的成员变量

B.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量

C.子类对象中没有包含基类的私有成员

D.基类的静态成员可以不包含在子类对象中

E.以上说法都不对

解答

A.静态变量就不被包含

B.同理,静态变量就不被包含

C.父类所有成员都要被继承,因此包含了

D.静态成员一定是不被包含在对象中的

E.很显然,以上说法都不正确

正确答案是:E


👉7、下面哪项结果是正确的( )

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 
{ public: int _d; };

int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    return 0;
}

A.p1 == p2 == p3

B.p1 < p2 < p3

C.p1 == p3 != p2

D.p1 != p2 != p3

解答

分析:p1和p2虽然都是其父类,但在子类内存模型中,其位置不同,所以p1和p2所指子类的位置也不相同,因此p1!=p2,

由于p1对象是第一个被继承的父类类型,所有其地址与子类对象的地址p3所指位置都为子类对象的起始位置,因此p1==p3,所以C正确


👉8、关于以下菱形继承说法不正确的是( )

class B {public: int b;};
class C1: public B {public: int c1;};
class C2: public B {public: int c2;};
class D : public C1, public C2 {public: int d;};

A.D总共占了20个字节

B.B中的内容总共在D对象中存储了两份

C.D对象可以直接访问从基类继承的b成员

D.菱形继承存在二义性问题,尽量避免设计菱形继承

解答

A.C1中b和c1共8个字节,C2中c2和b共8个字节,D自身成员d 4个字节,一共20字节

B.由于菱形继承,最终的父类B在D中有两份

C.子类对象不能直接访问最顶层基类B中继承下来的b成员,因为在D对象中,b有两份,一份是从C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可以加类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。

D.菱形继承存在二义性问题,尽量避免设计菱形继承,如果真有需要,一般采用虚拟继承减少数据冗余

答案:C


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

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

相关文章

虚拟机||后续1:使用Virtual Box7.0.4安装Ubuntu20.04图文教程+解决时间显示乱码问题

这个博主超爱碎碎念&#xff1a;&#xff09; 哈喽&#xff0c;四海八方的友友们&#xff0c;我胡汉三又回来啦&#xff08;&#xffe3;︶&#xffe3;&#xff09;↗ 有挺长一段时间没更新了&#xff0c;转眼间12月了&#xff0c;又到年底冲业绩的时候啦ψ(&#xff40;∇)ψ…

【大数据入门核心技术-Hadoop】(七)Hadoop基本Shell命令行

目录 一、 三种shell命令方式 二、常见Shell操作命令 1、创建文件夹 2、查看指定目录下内容 3、上传文件到HDFS指定目录下 4、查看HDFS文件内容 5、下载HDFS文件 6、拷贝HDFS文件 7、追加数据到HDFS文件中 8、HDFS数据移动操作 9、创建文件 10、查看磁盘使用情况 1…

金融信创与云化转型|基金超融合架构转型与场景探索合集

随着数字化经济的蓬勃发展&#xff0c;基金数字化转型作为金融行业发展的重点被提上日程。其中&#xff0c;证监会于 2022 年 4 月发布的《关于加快推进公募基金行业高质量发展的意见》中强调&#xff0c;基金行业应强化行业基础设施建设&#xff0c;以拥抱数字化金融市场&…

MySQL内置函数

MySQL内置函数1 .日期函数2.字符串函数3.数学函数4.其它函数5.综合练习题1 .日期函数 current_date() 当前日期获得年月日 mysql> select current_date(); ---------------- | current_date() | ---------------- | 2022-12-10 | ---------------- 1 row in set (0.0…

C语言基础—程序创建过程和编译过程

C程序的创建过程 4️⃣ C 程序的创建过程有4️⃣个基本步骤&#xff1a; 编写、编译、链接和运行。 编写&#xff1a;用文本编辑器或IDE编写源代码。源代码包含程序所需的所有命令和语句。编译&#xff1a;使用编译器对源代码进行编译&#xff0c;生成可执行文件。 - .o 文件…

gyp ERR find Python 解决方案

命令行报错如下 E:\vue-admin\node_modules\fibers>if not defined npm_config_node_gyp (node "D:\nodejs\node_modules\npm\node_modules\npm-lifecycle\node-gyp-bin\\..\..\node_modules\node-gyp\bin\node-gyp.js" rebuild --releas e ) else (node "…

Java项目:SSM物流快递管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 仓库管理员角色包含以下功能&#xff1a; 仓库管理员操作,入库操作,员工查看,揽收快件,新建员工等功能。 快递员角色包含以下功能&#xff1a; …

Nacos服务注册解析

服务注册就是在微服务启动时自动注册进nacos注册中心&#xff0c;核心逻辑就是在启动时调用nacos-server端的http接口:/nacos/v1/ns/instance&#xff0c;具体参考nacos官方文档。 我们打开nacos源码结构查看 上图为Nacos2.2的源码结构.其中比较核心的就是这几个包&#xff1…

墨者学院 PHP代码分析溯源(第4题) 详解

今天继续给大家介绍CTF通关writeup&#xff0c;本文主要内容是墨者学院 PHP代码分析溯源(第4题)。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严…

尝试使用CubeMX做stm32开发之十四:FatFs的基础知识

一、文件系统概念 文件系统&#xff1a;在存储介质中建立一种组织架构&#xff0c;以更好地储存和管理数据 文件系统一般包含&#xff1a;操作系统引导区、目录、文件等 文件系统中数据以文件的形式存储 文件系统中数据的存取需要遵循特定的格式 与直接操作存储介质相比&a…

[附源码]计算机毕业设计基于SpringBt的演唱会购票系统论文2022Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

C++ MySQL Error 1366 incorrect string value引发的认识

C MySQL Error 1366 incorrect string value引发的认识 前言 在使用MySQL C API编写程序时&#xff0c;由于用到了中文&#xff0c;导致出现了MySQL error 1366 incorrect string value 问题&#xff0c;但令我同样不解的是我用同样的语句在cmd下可以正常执行。&#xff08;M…

【算法】动态规划 ⑤ ( LeetCode 63.不同路径 II | 问题分析 | 动态规划算法设计 | 代码示例 )

文章目录一、问题分析二、动态规划算法设计1、动态规划状态 State2、动态规划初始化 Initialize3、动态规划方程 Function4、动态规划答案 Answer三、代码示例LeetCode 63. 不同路径 II : https://leetcode.cn/problems/unique-paths-ii/ 一个机器人位于一个 m x n 网格的左上…

java初步学习 String(基于小李的课进行自学,初学者)12

初步学习 String 基本概念 String类型即为字符串类型&#xff0c;即“103”&#xff0c;“abc”,"小陈"等字符类型&#xff0c;在工作中可用于加密&#xff0c;替换&#xff0c;截取&#xff0c;查找等工作 不过这种名词显然不能联想到工作的实际样子 例&#xff1a;…

[附源码]Python计算机毕业设计SSM基于框架的旅游订票系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

IIS 之 添加MIME扩展类型及HTTP错误403

IIS 之 添加MIME扩展类型及HTTP错误403 我是艾西今天跟大家分享下IIS添加MIME及HTTP报错403 经常用IIS作为下载服务器的时候有时传上去的文件比如 example.mp4 文件名上传后&#xff0c;但是用http打开的时候确显示为 404 文件不存在。其实是IIS对文件的一种保护&#xff0c;…

基于java+springboot+mybatis+vue+mysql的校园志愿者管理系统

项目介绍 本校园志愿者管理系统以springboot作为框架&#xff0c;前端vue技术&#xff0c;b/s模式以及mysql作为后台运行的数据库&#xff0c;同时使用Tomcat用为系统的服务器。本系统主要包括首页、个人中心、志愿者管理、活动类型管理、活动信息管理、活动报名管理、活动通知…

游戏开发49课 性能优化7

4. 渲染优化 渲染优化的目的是减少Draw Calls&#xff0c;减少渲染状态切换开销&#xff0c;降低显存占用&#xff0c;降低带宽和GPU负担。在讲解渲染优化之前&#xff0c;先了解渲染性能消耗点。 Draw Call数量 Draw Call有些引擎也称为SetPass Call。一个Draw Call就是游戏调…

叫ChatGPT用html+css+js写一个圣诞节代码,看看什么样子?

最近ChatGPT这么火&#xff0c;那就让他给我写点代码吧。 如何注册一个账号&#xff0c;参考&#xff1a;注册ChatGPT详细指南 注册不了的小伙伴们&#xff0c;咱们评论区见&#xff0c;问一个最想问的问题&#xff0c;看到就给你回复&#xff01; 我已经注册好了&#xff0c;…

代码随想录算法训练营第五十九天|503.下一个更大元素II、42. 接雨水

LeetCode 503.下一个更大元素II 链接&#xff1a;503.下一个更大元素II 思路&#xff1a; 本题其实和739. 每日温度更像一点&#xff0c;因为本题只有一个数组&#xff0c;而在下一个更大元素I中有两个&#xff0c;因此必须要一个哈希表来在另一个数组中查找相对应的数字。除…