第十讲 | 继承

继承

面向对象三大特性(不只是C++有,Java、python也有的。不止有三个特性,类比四大名著,但是中国还有很多名著):

封装:例如类、适配器、迭代器的封装

继承

多态

一、继承的概念及定义

1、继承(inheritance)的概念

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

下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,这样设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。

cpp 复制代码
class Student
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 学习
	void study()
	{
		// ...
	}
protected:
	string _name = "peter"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
	int _stuid; // 学号
};
class Teacher
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _name = "张三"; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};
int main()
{
	return 0;
}

把公共的成员都放到Person类中,Student和Teacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。

继承就是把公共设计的成员提取出来放在一个类里,让下面的类直接去继承它。所以继承是类设计层次的复用。

cpp 复制代码
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "张三"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
};
class Student : public Person
{
public:
	// 学习
	void study()
	{
		// ...
	}
protected:
	int _stuid; // 学号
};
class Teacher : public Person
{
public:
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string title; // 职称
};
int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

2、继承的定义

(1)、定义格式

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

(2)、继承基类成员访问方式的变化

访问方式:该成员受什么访问限定符限制。

父类的成员到派生类中的访问方式是什么?

父类成员的访问方式与继承方式排列组合一共有9种:

父类公有的成员在派生类中公有继承的访问方式是什么?

父类公有的成员在派生类中保护继承的访问方式是什么?

以此类推还有公有的成员私有继承、保护的成员公有继承、保护的成员私有继承......

9种组合一归类就是下表:

对这个表格的记忆借助下列1、3条:

  1. 基类private成员在派生类中无论以什么方式继承都是不能被访问(不可见)的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

基类的私有成员在派生类中继承是一定不可见吗?严格说派生类对象不能直接用,但是父类能间接用。例如,想对_age进行++,在派生类中不能++,但是在父类中可以++。父类中进行++的函数定义成公有就可以在类外访问了。

  1. 如果基类成员不想在类外直接被访问,但需要在派生类中能访问,基类成员就在基类中定义为protected。可以看出保护(protected)成员限定符是因继承才出现的。(补充:基类的public成员在类里面和类外面都能被访问)

学习继承前,认为private与protected是一样的。private与protected的区别在继承中体现(上面1、2条)。

  1. 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符和继承方式中选择权限最小的作为该成员在派生类中受到的访问限定符),权限大小:public > protected >private。
  2. 不写继承方式也是可以的。使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  • 纵向看基类成员继承到派生类里的访问方式:基类的private成员无论在派生类里面还是外面都不能被访问,因此在实际中父类/基类中定义private成员是不多的,因为继承的意义是让派生类完成复用,基类的private成员显然是在派生类中不可用的,达不到复用的目的;基类的protected成员在派生类里面能被访问,但是在类外面不能被访问;基类的public成员在类里面和类外面都能被访问。
  • 横向看继承方式:在实际运用中一般都是使用public继承,很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

(3)、继承类模板

我们之前实现栈是用适配器实现的。适配器实际上是一种组合关系,后面章节讲解。栈里面包含一个Container,Container由模板类型决定。这种是组合,不是继承,本质也是一种复用:

现在我们使用继承的方式实现一个栈。(当然实践中肯定不会用这种,适配器的方式更好。这就要提到对这两种实现方式的比较了)

cpp 复制代码
namespace bit
{
	// 继承
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错:error C3861: "push_back": 找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
			vector<T>::push_back(x);
			//push_back(x);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};
}
int main()
{
	bit::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

push_back(x);派生类里不能直接这么写。找push_back会在当前作用域找,找不到会在vector< int >域里找,但是在vector< int >域里是找不到的,原因是这个push_back没有实例化。指定在这个实例化的类里调用vector<T>::push_back(x);

模板的按需实例化:一个类模板实例化时调用哪一个函数就实例化哪一个函数。比如,一个类模板实例化出一个类,不会把整个类模板实例化的,调用哪一个函数就实例化哪个函数。

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

  • public继承派生类对象 可以赋值给 基类的指针 / 基类的引用,这个过程有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或基类引用指向的是派生类中切出来的基类那部分。(派生类对象也可以赋值给基类对象,后面会讲解。)

public继承派生类对象 可以赋值给 基类的指针 / 基类的引用 ,有些地方也叫赋值兼容转换 。赋值兼容转换是一种特殊处理,中间不产生临时对象,而在以前的规则中不同类型的对象之间赋值是会产生临时对象的。

  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下)
cpp 复制代码
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;
}

三、继承中的作用域

1、隐藏规则

  1. 在继承体系中基类和派生类都有独立的作用域。(因为不同的作用域可以定义同名变量和函数,所以基类和子类可以有同名变量和函数)
  2. 派生类和基类中有同名成员,同名派生类成员将屏蔽基类对同名成员的直接访问(局部和全局的同名变量同时存在,优先访问局部),这种情况叫隐藏派生类和基类有同名的成员变量是隐藏
    (在派生类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
cpp 复制代码
// 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;// 隐藏了父类中的同名成员。在派生类中访问的是派生类中的_num,局部和全局的同时存在,优先访问局部
	}
protected:
	int _num = 999; // 学号
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
};

2、考察继承作用域相关选择题

同一个代码下的两道题:

  • A和B类中的两个func构成什么关系()

    A. 重载 B. 隐藏 C.没关系

  • 下面程序的编译运行结果是什么()

    A. 编译报错 B. 运行报错 C. 正常运行

cpp 复制代码
class A
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void func(int i)
	{
		cout << "func(int i)" << i << endl;
	}
};
int main()
{
	B b;
	b.func(10);
	b.func();
	return 0;
};

第一题答案:B

为什么不选A呢?函数名相同参数不同确实构成函数重载,但是函数重载还有一点要求:要求在同一个作用域。

第二题答案:A

B类隐藏了A类中的同名函数,所以是调用不到A类中的同名函数的(只有在派生类中查找不到才会到父类中查找)。而派生类对象调用同名函数的查找规则就是先在派生类中查找,语法逻辑没有问题但是还是查找不到的话再去父类中查找。而这里是语法逻辑就先有问题,根本到不了去父类查找这一步。

那么怎么调用到父类中的同名函数呢?指定作用域调用:

cpp 复制代码
b.fun(10);
b.A::fun();// 注意函数调用时指定作用域的位置

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

基类的默认成员函数与之前学习的普通类是一样的,就把基类当成普通类来学习。而派生类就与普通类不一样了,它是继承了基类的所有成员,所以我们就来学习派生类的默认成员函数。

1、4个常见默认成员函数

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

派生类中的成员分类(可以把在派生类中继承的所有基类成员当成一个整体。在派生类中的基类成员和自定义类型成员的初始化逻辑是一样的,例如都可以调用各自的默认构造。在派生类中继承的所有基类成员当成一个整体以及类和对象的知识串联一起,这一块的知识点就没有问题啦):

  1. 基类成员
  2. 派生类成员:内置/基本类型、自定义类型
  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用,否则会报错,调用的方式很像"定义匿名对象"的写法。(派生类中剩下的成员的初始化就跟之前学习的是一样的)

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

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

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

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

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

  7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。



cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
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; // 姓名
};
  • 派生类的构造函数:

实践中大部分的类的构造函数都需要我们自己写,但是拷贝构造、赋值、析构函数一般默认生成的就够用。不过我们从学习的角度还是都要学习一下是怎么写的。

  • 派生类的拷贝构造函数:

上面写好的Student类内部没有申请的资源,不用自己写拷贝、赋值、析构,默认的就够用。对于基类成员(一个整体)调用自己的拷贝构造;对于派生类中的自定义类型成员调用自己的默认拷贝构造,对于int内置类型成员,值拷贝:

但是有这样的成员,就需要写深拷贝相关的函数:

我们自己写一个派生类的拷贝构造函数:

  • 派生类的赋值运算符重载:

同理,Student类没有资源申请不需要自己写赋值。若需要自己写赋值,那么怎么写呢?

正确写法:

cpp 复制代码
Student& operator=(const Student& s)
{
	if (this != &s)
	{
		Person::operator=(s);
		_address = s._address;
		_num = s._num;
	}
	return *this;
}
  • 派生类的析构函数:

同理,Student类内部没有资源需要释放,不需要自己写析构函数。若有资源释放,怎么写呢?

析构可以显示调用,但是编译错误了:

一些场景下析构函数构成多态的原因,需要基类和派生类的构造函数同名,编译后,底层统一将析构函数名统一处理成destructordestructor意思为析构函数。所以上面编译报错的原因就是因为编译后,~Student()~Person()析构函数名统一为destructor,构成隐藏,若想调用基类的析构函数,需要指定类域。

正确写法,但是会出现二次析构:

但是为什么会二次析构呢?当前没有资源,所以没有崩;若有资源,二次析构会崩溃的。这里二次析构的原因:在派生类的析构函数中显示调用基类的析构函数后,会在派生类的析构函数调用完成后自动调用基类的析构函数。

派生类对象初始化时先初始化基类成员,再初始化派生类成员;而派生类对象析构时先清理派生类成员再清理基类成员(可以借助"后定义的先析构"理解记忆)。若在派生类的析构函数中,析构顺序是先父后子,但是先显示调用父类的析构函数后再用父类成员是会出现问题的。保证析构顺序是先子后父是不会出现问题的:在派生类的析构函数中,子类成员先析构了,再用父类成员,派生类的析构函数被调用完成后,自动调用基类的析构函数。

解决二次析构的问题,本质就是保证析构顺序是先子后父。子类析构结束时会自动调用父类的析构函数,所以不需要我们显示调用。

总结:派生类中可以显示调用基类的构造、拷贝构造、赋值;析构不需要显示调用,会自动调用,目的就是保证析构顺序是先子后父。

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
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, const char* address, int num)
		:_num(num)
		,_address(address)
		,Person(name)
	{}
	Student(const Student& s)
		:Person(s)
		,_address(s._address)
		,_num(s._num)
	{}
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_address = s._address;
			_num = s._num;
		}
		return *this;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num = 1; //学号
	string _address = "111";
};
int main()
{
	Student s1("张三", "李四", 5);
	//Student s2(s1);
	//Student s3("111", "222", 9);
	//s1 = s3;
	return 0;
}

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

方法1:C++98,基类的构造函数私有,派生类的构造函数必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。(这种方法不够直观,C++11给出了新的方式)

方法2:C++11新增了一个final关键字,final修饰的类叫做最终类,最终类不能被继承。final修饰基类,派生类就不能继承了。

cpp 复制代码
// C++11的⽅法
class Base final
{
public:
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
private:
	//C++98的⽅法
	/*Base()
	{}*/
};
class Derive :public Base
{
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

五、继承和友元

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

六、继承与静态成员

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

基类和派生类都可以用类域、对象访问基类的公有静态成员。对象访问成员,这个成员不一定是在对象中的,例如静态成员;就像对象调用成员函数一样,成员函数就不在类对象中。两种访问方式都可以,但是静态成员一般都是用类域去访问的。

cpp 复制代码
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;
	cout << &Person::_count << endl;
	cout << &Student::_count << endl;
	return 0;
}

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

1、继承模型

单继承:一个派生类只有一个直接基类,称这个继承关系为单继承。(Student和Person都是PostGraduate的基类,但是直接基类只有Student一个)

多继承:一个派生类有两个或两个以上直接基类,称这个继承关系为多继承。多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员放到最后面。(多继承没问题,但是多继承可能会导致菱形继承)

菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,数据冗余体现在Assistant的对象中Person中的成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。(二义性:Person中有_name成员,Assistant的对象访问_name时不知道要访问哪一个,会报错)

多继承辅助理解:一个类具有多个类的特征,把多个类型都继承。例如,学校里有的人既是学生,也是老师,既是博士生又是辅导员;再比如,两个类分别是水果和蔬菜,有一个类既是水果也是蔬菜,圣女果等。

虽然使用多继承可能会出现菱形继承的问题,但是不能把多继承的语法彻底删除不用了。因为更新都是向前兼容,若彻底删除不用,之前用多继承写的代码维护起来就会很麻烦。

cpp 复制代码
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()
{
	// 编译报错:error C2385: 对"_name"的访问不明确
	Assistant a;
	//a._name = "peter";

	// 指定类域可以解决⼆义性问题,但是数据冗余问题⽆法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

不过,虚继承可以解决数据冗余和二义性。

2、虚继承

虚继承要加一个关键字virtual,加在菱形虚拟继承中中间的两个类中。

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java。

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
	/*int _tel;
	int _age;
	string _gender;
	string _address;*/
	// ...
};
// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:
	int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
	int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	// 使⽤虚继承,可以解决数据冗余和⼆义性
	Assistant a;
	// 指定类域访问或者不指定类域访问都可以,因为只有一份
	a._name = "peter";
	a.Student::_name = "张三";
	a.Student::_name = "王五";
	return 0;
}

监视窗口看到的有时候是不真实的。例如,这里像是有3个_name一样,实际上只有1份_name。没有把Person放在Student和Teacher里面,而是单独放在对象里,一般是放在对象的最后面或最前面,vs里是把Person放在对象里的最后面。监视窗口不真实的例子还有链表,链表会在监视窗口中按照数组一样展示,这是编译器特殊处理过的。


我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,无论是使用还是底层都会复杂很多。当然有多继承语法支持,就一定存在会设计出菱形继承,像Java是不支持多继承的,就避开了菱形继承。

菱形继承的麻烦之处,例如,写出了下列类的构造函数,前三个类正常写,但是Assistant类的构造函数麻烦:

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}
	string _name; // 姓名
};
class Student : virtual public Person
{
public:
	Student(const char* name, int num)
		:Person(name)
		,_num(num)
	{}
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
public:
	Teacher(const char* name, int id)
		:Person(name)
		,_id(id)
	{}
protected:
	int _id; // 职⼯编号
};
// 不要去玩菱形继承
class Assistant : public Student, public Teacher
{
public:
	Assistant(const char* name1, const char* name2, const char* name3)
		:Student(name1, 1)
		,Teacher(name2, 2)
		,Person(name3)
	{}
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	// 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个?
	Assistant a("张三", "李四", "王五");
	return 0;
}

cpp 复制代码
// 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个?
Assistant a("张三", "李四", "王五");

这样的继承方式也是菱形继承,若是虚继承,virtual加在BD上:

菱形继承本质就是继承的两个类有共同的基类。


实际中菱形继承是很少的,菱形继承的示例:IO库中的菱形虚拟继承

3、多继承中指针偏移问题?

下面说法正确的是( )

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

cpp 复制代码
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;
}

底层内存存储也叫做对象模型,多继承的对象模型是先被继承的类对象排在内存前面,先继承对应着先声明。声明中,首先父类在派生类的前面,其次,若有多个父类,先被继承的在前面。

选c

4、IO库中的菱形虚拟继承

cpp 复制代码
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};

八、继承和组合

继承和组合本质都是一种复用,组合优于继承。组合的耦合度低,继承的耦合度高,所以组合更好。

耦合度:模块与模块之间,这里指类与类之间的关系紧密程度。关系越松散越好,即低耦合;关系越紧密,即高耦合。

  • public继承(在派生类中权限不变)是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。(例如,Student继承Person,那么Student就是一个特殊的人)

  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。(我里面有个你)

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

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

黑与白在软件工程中分别指看不见与看得见。继承中是看得见的,基类对派生类是透明的,派生类对象都可以使用,都可以使用就会导致基类与派生类的耦合度高。

  • 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

若是继承,B能使用A中的公有成员和保护成员;若是组合,B只能使用A中的公有成员,A的保护成员用不了。一个类设计时要多设计保护成员,而不是多设计公有成员。若A有100个成员,其中70个是保护成员,30个是公有成员,B能用A的一个成员关联/耦合度就是1,那么继承关联/耦合度就是100,组合的关联/耦合度是30。

cpp 复制代码
// Tire(轮胎)和Car(⻋)更符合has-a的关系(车里面有轮胎)
class Tire {
protected:
	string _brand = "Michelin"; // 品牌
	size_t _size = 17; // 尺⼨
};
class Car {
protected:
	string _colour = "⽩⾊"; // 颜⾊
	string _num = "陕ABIT00"; // ⻋牌号
	Tire _t1; // 轮胎
	Tire _t2; // 轮胎
	Tire _t3; // 轮胎
	Tire _t4; // 轮胎
};
class BMW : public Car {
public:
	void Drive() { cout << "好开-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};
template<class T>
class vector
{
};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{
};
template<class T>
class stack
{
public:
	vector<T> _v;
};
int main()
{
	return 0;
}
相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_841495645 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr6 小时前
2607C++,soui与安卓
c++·soui
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
克里斯蒂亚诺更新6 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117769 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
fqbqrr9 小时前
2607C++,使用微软detours勾挂工具
c++