【C++】继承

目录

[1. 继承的概念及定义](#1. 继承的概念及定义)

[1.1 继承的概念](#1.1 继承的概念)

[1.2 继承定义](#1.2 继承定义)

[1.2.1 定义格式](#1.2.1 定义格式)

[1.2.2 继承基类成员访问方式的变化](#1.2.2 继承基类成员访问方式的变化)

[1.3 继承类模板](#1.3 继承类模板)

[2. 基类和派生类间的转换](#2. 基类和派生类间的转换)

[3. 继承中的作用域](#3. 继承中的作用域)

[3.1 隐藏规则:](#3.1 隐藏规则:)

[3.2 考察继承作用域相关选择题](#3.2 考察继承作用域相关选择题)

[3.2.1 A和B类中的两个func构成什么关系()](#3.2.1 A和B类中的两个func构成什么关系())

[3.2.2 下面程序的编译运行结果是什么()](#3.2.2 下面程序的编译运行结果是什么())

[4. 派生类的默认成员函数](#4. 派生类的默认成员函数)

[4.1 4个常见默认成员函数](#4.1 4个常见默认成员函数)

[4.2 实现一个不能被继承的类](#4.2 实现一个不能被继承的类)

[5. 继承与友元](#5. 继承与友元)

[6. 继承与静态成员](#6. 继承与静态成员)

[7. 多继承及其菱形继承问题](#7. 多继承及其菱形继承问题)

[7.1 继承模型](#7.1 继承模型)

[7.2 虚继承](#7.2 虚继承)

[7.3 多继承中指针偏移问题:下面说法正确的是( ) C](#7.3 多继承中指针偏移问题:下面说法正确的是( ) C)

[7.4 IO库中的菱形虚拟继承](#7.4 IO库中的菱形虚拟继承)

[8. 继承和组合](#8. 继承和组合)

[8.1 继承和组合](#8.1 继承和组合)


1. 继承的概念及定义

1.1 继承的概念

在社会关系中,一个人往往会拥有不同的身份,基于不同身份,有用不同的信息,比如当学生有学号,成为工人有工号等,可能会有不同的外号,但是基于人的最根本的身份,他们都要自己唯一的名称、性别等属性。

面向对象的程序设计中,如果我们想要模拟实现这种一个人的不同身份,并存储对应信息,难道实现"人"、"学生"、"工人"这些模板时,我们每个模板中都有名称、年龄、性别这些信息吗?

仔细思考我们发现像名称、年龄、性别这些是基于人这个最根本的身份出发的,学生、工人等都是基于这个基础上产生的,那么对于这些通用信息,我们能否使用就人这个类的数据呢?对于学号、工人这些个性化的信息,我们就在学生、工人这些类具体存储呢?

C++中继承就提供了这种类层次的代码复用。

继承(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;
}

1.2 继承定义

1.2.1 定义格式

下面我们看到Person是基类 ,也称作父类 。Student是派生类 ,也称作子类 。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类)

子类的类名称后面加上 :继承方式 基类名称,继承方式的符号与访问限定符一致,分为公有、保护、私有。

1.2.2 继承基类成员访问方式的变化

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

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected 。可以看出保护成员限定符是因继承才出现的

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

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

  5. 由于C++出现时间早,没有其他参考,因此设计者一开始考虑了所有的可能情况,因此出现了上表中的9种情况,但是在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

cpp 复制代码
// 实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name; // 姓名
private:
	int _age; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:
	int _stunum; // 学号
};

1.3 继承类模板

cpp 复制代码
namespace zlr
{
	//template<class T>
	//class vector
	//{};
	// stack和vector的关系,既符合is-a,也符合has-a
	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()、pop_back()形式调用对应程序报错,因此我们要这里要指明对应类,显式调用函数接口,编译器才会将对应的父类的函数实例化,程序才不会报错。

对于构造与析构函数等特殊的函数,编译器会做特殊处理,自动调用(下文会说明),因此即使我们不显示调用,也实例化,不会报错

2. 基类和派生类间的转换

public继承 的派生类对象 可以赋值给 基类的指针 / 基类的引用(protected\private继承类外无法访问,因此没有这个概念) 。这里有个形象的说法叫切片或者切割 。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分

基类对象不能赋值给派生类对象。(对于派生类增加的成员,基类对象中也没有值可以赋)

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用 。但是必须是基类的指针是指向派生类对象时才是安全的 。这里基类如果是多态类型 ,可以使用RTTI(Run-Time TypeInformation)的dynamic_cast 来进行识别后进行安全转换。(ps:这个后面笔者多态类型转换再单独专门介绍,这里先提一下)

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

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

int main()
{
	Student sobj;
	// 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造完成的
	Person pobj = sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用,不属于类型转换
	Person* pp = &sobj;
	Person& rp = sobj;
	rp._name = "张三";

	//int i = 1;
	//double d = i;
	//const double& rd = i;//类型转换会产生临时对象

	//2.父类对象不能赋值给子类对象,这里会编译报错
	//sobj = (Student)pobj;

	// 这里简单了解一下
	Student* ps1 = dynamic_cast<Student*>(pp);//
	cout << ps1 << endl;

	pp = &pobj;
	Student* ps2 = dynamic_cast<Student*>(pp);
	cout << ps2 << endl;

	return 0;
}

注:之所以基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,其实也是因为之前提到派生类可以赋值给基类,所以基类指针或者引用也可能指向一个派生类,这种指向派生类的基类的指针或者引用就可以赋值给派生类,指向基类的指针或者引用不可以赋值。

此外,在赋值过程当中虽然基类与派生类的类型不同,但是这里并没有发生类型转换产生临时对象,可以理解为编译器对切片这一操作特殊处理了。

3. 继承中的作用域

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

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

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

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

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

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

cpp 复制代码
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();
    //b.A::fun();要调用A中的函数就需要显式调用
	return 0;
};

首先,需要明确的是函数重载要求的是在同一域内的同名函数,父类与子类有独立的域,因此A与B中的同名函数不构成重载,而是B的fun对A的fun构成隐藏。

因此,我们以B.fun()形式不管穿不传参都是调用B中的fun,而B中的fun是要传参的,如果我们不传参,程序就会报错。

如果我们想要调用A中的fun,我们必须显式调用。

4. 派生类的默认成员函数

4.1 4个常见默认成员函数

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

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员( 在之前类文章中,我们了解到编译器默认生成的函数,对于内置类型的行为是不确定的,对于自定义类型是调用它的默认函数。而对于继承的父类成员可以视作一个整体对象,对于这个整体对象要求调用父类的默认函数 ,编译器会自动调用**)** 。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。由于拷贝构造是一种特殊的构造,一但构造函数显示写了,拷贝构造我们也必须显示写。

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

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

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

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

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

注:一般情况下,如果我们的程序不涉及深拷贝,如果父类有对应的默认函数,那么我们仅使用编译器默认生成的就可以了,相关继承的父类成员,编译器会自动调用父类成员的默认函数进行处理。

对于子类与父类都是模板的情况,需要注意的是像构造、析构等函数,编译器默认生成时,在汇编层面实际上会自动调用,所以相关的父类函数会实例化,因此即使我们没有显式调用也不会报错。

继承自父类的成员在子类内部实际上声明在子类成员前面,因此先声明的成员在初始化列表先初始化,所以在初始化列表,父类成员作为整体先初始化。而在析构环节,为了保持栈上数据后进先出的特性,子类是先析构,父类后析构。

cpp 复制代码
class Person
{
public:
	Person(const char* name = "xxx")
		: _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, const char* addrss)
		:Person(name)
		, _num(num)
		, _addrss(addrss)
	{}

	// 严格说Student拷贝构造默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	Student(const Student& s)
		:Person(s)//显式调用时,这里需要传一个父类的对象,我们上文提过子类对象可以传给父类的
                   //引用(切片),因此,这里我们直接传子类引用就可以
		, _num(s._num)
		, _addrss(s._addrss)
	{
		// 深拷贝
	}

	// 严格说Student赋值重载默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			// 父类和子类的operator=构成隐藏关系
			Person::operator=(s);

			_num = s._num;
			_addrss = s._addrss;
		}

		return *this;
	}

	// 严格说Student析构默认生成的就够用了
	// 如果有需要显示释放的资源,才需要自己实现
	// 析构函数都会被特殊处理成destructor() 
	~Student()
	{
		// 子类的析构和父类析构函数也构成隐藏关系
		// 规定:不需要显示调用,子类析构函数之后,会自动调用父类析构
		// 这样保证析构顺序,先子后父,显示调用取决于实现的人,不能保证
		// 先子后父
		//Person::~Person();
		//delete _ptr;
	}
protected:
	int _num = 1; //学号
	string _addrss = "西安市高新区";

	int* _ptr = new int[10];
};

int main()
{
	Student s1("张三", 1, "西安市");
	Student s2(s1);

	Student s3("李四", 2, "咸阳市");
	s1 = s3;

	/*Person* ptr = new Person;
	delete ptr;*/

	return 0;
}

注:对于析构函数来说,无论父类析构函数是否显示的调用,编译器都会自动调用,因此,在上段代码中,我们写析构函数不能显示调用父类析构,否则容易造成对已释放空间析构,造成崩溃。

4.2 实现一个不能被继承的类

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

方法2:C++11新增了一个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;
}

5. 继承与友元

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

cpp 复制代码
class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
int main()
{
	Person p;
	Student s;
	// 编译报错:error C2248: "Student::_stuNum": 无法访问 protected 成员
	// 解决方案:Display也变成Student 的友元即可
	Display(p, s);
	return 0;
}

6. 继承与静态成员

基类定义了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;
	return 0;
}

7. 多继承及其菱形继承问题

7.1 继承模型

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

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

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

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;
}

7.2 虚继承

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

为了处理数据冗余,C++设计了一个关键词virtual来解决,Assisant的冗余数据主要是Person有两份,一份来自Student,一份来自Teacher,那们我们就在Student与Teacher继承的Person类名称前加上virtual,那么Assisant在继承Student与Teacher的Person数据时,就相当于两份数据合并成一份,之后这份数据单独存放在Assisant中,不在存放在Student与Teacher内。

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";
	return 0;
}

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

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)
		:Person(name3)
		, Student(name1, 1)
		, Teacher(name2, 2)
	{}
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	// 思考一下这里a对象中_name是"张三", "李四", "王五"中的哪一个?
    //编译器特殊处理,使用virtual这里Student与Teacher的构造函数都不会再
    //调用Person的构造,所以只有Person(name3)起作用,结果是王五
	Assistant a("张三", "李四", "王五");
	return 0;
}

对于virtual的使用,以上图中第一种情况为例,E中的冗余数据是来自B与C继承的A的部分,因此我们需要对B、C使用virtual(D中的A部分是严格上属于B的,继承的B的部分)

以第二中情况为例,当我们不涉及菱形继承是时,我们就不要使用virtual,避免额外的开销(当然能不用菱形继承就不要使用菱形继承)

7.3 多继承中指针偏移问题:下面说法正确的是( ) C

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;
					}

这里一题设计就是前文所提到子类赋值给父类指针或引用的切片问题,这里赋值给p1时将对应的Derive中Base1部分切片给p1,赋值给p2时就将对应的Derive中Base2部分切片给p2,因此,这里就发生了指针偏移的现象,p1指向Base1部分开始的地方,p2指向Base2部分开始的地方。p3就是指向Derive开始的地方,因此p1==p3!=p2,选C

7.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>
{};

8. 继承和组合

8.1 继承和组合

• public继承是一种is-a 的关系。也就是说每个派生类对象都是一个基类对象

• 组合是一种has-a 的关系。假设B组合了A,每个B对象中都有一个A对象。(组合就是新类的接口是通过封装已实现类的接口实现的)

在日常中,我们会说大象是动物,大象这个概念属于动物这个概念,这个就是is-a,而我们不会说大象是动物组成的。继承就是子类的特性是继承父类的特性,并在其上拓展的,有所属关系,这个就是is-a.

而我们会说汽车由4个轮胎组成,我们不会说汽车是轮胎,这种概念本身没有所属关系,但是在具体事务上构成的关系我们叫做组合。如之前实现stack我们是在vector接口基础上封装出来的,stack是有vector构成的,我们把这就叫做组合。

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

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

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

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>//stack是通过封装已实现的vector实现的,这就是组合
class stack : public vector<T>
{};
template<class T>
class stack
{
public:
	vector<T> _v;
};
int main()
{
	return 0;
}
相关推荐
PluviophileDD1 小时前
【笔记】C语言转C++
c语言·c++
獨枭2 小时前
MFC 自定义编辑框:打造灵活的数据输入控件
c++·mfc
逆袭之路6662 小时前
浅谈右值引用 移动语义 完美转发 std::move std::forward,窥探模板元编程的一角
c++
安於宿命2 小时前
【Linux】文件系统
linux·服务器·c++
oioihoii2 小时前
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
开发语言·c++
OTWOL3 小时前
预处理基础指南
开发语言·数据结构·c++·算法
老猿讲编程3 小时前
【零成本抽象】汽车嵌入式软件开发中零成本抽象的功能安全考量与应对策略
c++·安全·汽车·零成本抽象
ZPILOTE3 小时前
日志基础示例python和c++
c++·python·日志·log·logger·glog
codears4 小时前
Qt编写的文件传输工具
c++·qt