前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++】继承

【C++】继承

作者头像
野猪佩奇`
发布2023-03-28 14:35:50
8760
发布2023-03-28 14:35:50
举报
文章被收录于专栏:C/C++ 后台开发学习路线

文章目录

一、继承的概念及定义

1、继承的概念

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

比如我们要设计一个学校人员管理系统,那么我们可能需要为老师、学生、食堂阿姨等群体分别设计一个类,但是这些类中有很多的成员属性都是相同的,如下:

代码语言:javascript
复制
class Teacher {  //教师类
public:
	//作为人的共有行为
	void eating();  //吃饭
	void sleeping();  //睡觉
	//老师的单独行为
	void teaching();  //教学

protected:
	//人的共有属性
	string _name;  //姓名
	string _tele;  //电话
	string _addr;  //住址
	//老师的特有属性
	int _jobid;  //工号
	int _salary;  //薪水
};

class Student {  //学生类
public:
	//作为人的共有行为
	void eating();  //吃饭
	void sleeping();  //睡觉
	//学生的单独行为
	void attendclass();  //上课

protected:
	//人的共有属性
	string _name;  //姓名
	string _tele;  //电话
	string _addr;  //住址
	//学生的特有属性
	int _stuid;  //学号
	int _score;  //学分
};

//其他职工类...

可以看到,在学校人员管理系统中,由于人的许多行为是相同的,所以这些类中就会存在大量相同的成员,此时,我们就可以将这些公共的属性抽取出来,单独设计成一个类作为父类,然后让其他类作为子类来继承父类,从而实现代码复用:

代码语言:javascript
复制
class Person {  //父类
public:
	void eating();
	void sleeping();
	
protected:
	string _name;  
	string _tele;  
	string _addr;  
};

class Teacher : public Person {  //教师类继承person类
public:
	void teaching();  
protected:
	int _jobid; 
	int _salary;  
};

class Student : public Person {  //学生类继承person类
public:
	void attendclass(); 
protected:
	int _stuid;  
	int _score;  
};

//其他职工类...

如上,我们可以通过继承使得子类对象中拥有父类所有的成员方法和成员变量,实现代码复用。

2、继承的定义

定义格式

继承的定义格式如下:其中 Person 类是父类/基类,Student 类是子类/派生类,public 是继承方法;

继承方法

C++中继承一共有三种方式 – public 继承、protected 继承 和 private 继承:

基类成员被继承后在子类对象中的访问权限变化如下:

-基类成员\继承方式

-public继承

protected继承

private继承

基类的public成员

派生类的public成员

派生类的protected成员

派生类的private成员

基类的protected成员

派生类的protected成员

派生类的protected成员

派生类的private成员

基类的private成员

在派生类中不可见

在派生类中不可见

在派生类中不可见

表格内容可以概述为:基类的私有成员在子类都不可见,基类的其他成员在子类的访问方式等于成员在基类中的访问权限与继承方式取较小值,其中访问权限大小为 public > protected > private。

继承总结

1、基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它,而不是派生类没有继承该成员。 2、基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出 protected 限定符是因继承才出现的。(没有关系继承时,protected 和 private 没有区别) 3、基类的其他成员在子类的访问方式等于成员在基类中的访问权限与继承方式取较小值,其中访问权限大小为 public > protected > private。 4、使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,不过最好显示的写出继承方式。 5、在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

注意:以后我们说继承只要不特别指明是 protected 继承或 private 继承,则都是指 public 继承。


二、基类和派生类对象的赋值

在继承关系中,派生类对象可以直接赋值给基类的对象/基类的指针/基类的引用,而不产生类型转换。这个赋值的过程也被形象的叫做切片或者切割,寓意把派生类中父类那部分切来赋值过去。

如图所示:派生类对象赋值给基类对象时是直接将派生类中属于基类那一部分切割给基类,引用和指针也是一样,基类的引用是派生类中属于基类那一部分成员的别名,基类的指针指向派生类中属于基类的那一部分。

什么是类型转换

C++ 常引用 以及 类和对象下 部分其实我们已经学习了类型转换了,这里我们再来回顾一下 – 类型转换分为显示类型转换和隐式类型转换,显示类型转换即当两个不同类型的对象进行操作 (比较/赋值等) 时,我们手动将其中一个对象的类型转换为另一个对象的类型,使得二者能够顺利进行比较;

隐式类型转换则是指当两个不同类型的变量之间进行运算时,编译器会自动将其中一个变量的类型转换为另一个变量的类型。在一般情况下,隐式类型转换没有什么要特别注意的地方,但是当涉及到引用或者指针时,我们就需要特别注意了。

代码语言:javascript
复制
void test1(){
	int a = 0;
	double b = a;
	const double& rb = a;
    const int& c = 1;
}

对于上面这段代码,如果我们不将 rb 和 c 定义为 const 引用,而定义为普通引用,那么编译器会报错,原因如下:

如上,如果我们将 int 的 a 赋值给 double 的 b,因为二者类型不同,所以 a 不会被直接赋值给 b,而是编译器会先根据 a 创建一个 double 类型的临时变量 tmp,然后将 tmp 赋值给 b; 对于 rb 来说也是一样的,但是 rb 是引用类型,因为对于引用和指针来说,权限只能放大和平移,而不能缩小,所以用引用类型的变量 rb 去引用 a 生成的临时变量 tmp 需要使用 const 修饰 (临时变量具有常性); 对于最后一条语句来说也是一样 – 数字 1 只存在于指令中,在内存中并不占用空间,所以当我们对其进行引用时,1会先赋给一个临时变量,然后我们再对这个临时变量进行引用;同时由于临时变量具有常性,所以我们需要使用 const 修饰;

派生类对象赋值给基类对象不存在类型转换

现在,我们就能真正理解 “派生类对象可以直接赋值给基类的对象/基类的指针/基类的引用,而不产生类型转换” 是什么意思了 – 派生类对象赋给基类对象时中间不会参数临时变量,所以基类对象可以直接引用/指向派生类对象,而不需要使用 const 修饰

代码语言:javascript
复制
class Person {
public:
	void Print() {
		cout << _name << endl;
	}
protected:
	string _name; 
};

class Student : public Person {
protected:
	int _stunum; 
};

注意事项:

1、基类对象不能赋值给派生类对象;(即只能向上转,不能向下转)

2、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的;同时,这里基类如果是多态类型,可以使用 RTTI (Run-Time Type Information) 的 dynamic_cast 来进行识别后进行安全转换。(多态我们将在下一节讲解,这里大家先做了解即可)

代码语言:javascript
复制
void test3() {
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

	//2.基类对象不能赋值给派生类对象
	sobj = pobj;

	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj;
	Student* ps1 = (Student*)pp; //这种情况转换时可以的。

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

特别提醒:切片在继承和多态中起着至关重要的作用,继承和多态中大部分的重难点知识都和切片有关,特别是子类对象赋值给父类指针/引用。


三、继承中的作用域

1、继承中的作用域 (隐藏)

在继承体系中基类和派生类都有各自独立的作用域,所以我们可以在子类中定义与父类同名的成员变量和成员函数 – 如果子类和父类中有同名成员,那么子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏/重定义;如果我们想要访问父类成员,就必须指定基类的类域。

注意:对于成员函数来说,只要子类成员函数与父类成员函数的函数名相同就构成隐藏

代码语言:javascript
复制
class Person {
public:
	void func() {
		cout << "Person::func()" << endl;
	}
	string _name = "小李子";
	int _num = 111;
};

class Student : public Person {
public:
	void func(int a) {
		cout << "Student::func(int a)" << endl;
	}
	int _num = 999; // 学号
};

可以看到:

1、虽然子类中的 func 函数和父类中的 func 函数参数不同,但是它们仍然构成了隐藏,因为它们的函数名相同; 2、由于子类中的成员造成了父类同名成员的隐藏,所以默认调用同名成员时默认调用子类成员,如需调用父类成员需要指定父类的作用域。

2、经典面试题

下面两个 func 是什么关系、运行结果是什么:A. 重载 B. 重写 C.重定义/隐藏 D. 编译报错

代码语言:javascript
复制
class A {
public:
	void fun() {
		cout << "A::func()" << endl;
	}
};

class B : public A {
public:
	void fun(int i) {
		cout << "B::func(int i)->" << i << endl;
	}
};

void Test()
{
	B b;
	b.fun(10);
};

虽然 A 类中的 func 函数和 B 类中的 func 函数同名且参数不同,但是它们不构成重载,因为它们的作用域不同,重载函数一定是在同一个作用域中的; 所以两个 func 的关系是隐藏,因为 B 继承自 A,且两个函数同名;并且编译结果是 B::func(int i)->,因为父类成员被隐藏,默认调用子类成员,如果要调用父类成员需要指定父类作用域。 注:关于重写的知识我们在下一节多态学习。

代码语言:javascript
复制
class A {
public:
	void fun() {
		cout << "A::func()" << endl;
	}
};

class B : public A {
public:
	void fun(int i)
	{
		cout << "B::func(int i)->" << i << endl;
	}
};

void Test() {
	B b;
	b.fun();
};

和上面一样,两个 func 构成隐藏,但是这里会编译报错,因为默认调用子类的 func 函数,但是子类 func 函数需要一个参数 i,而调用时未传递此参数。


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

普通类的默认成员函数

在学习派生类的默认成员函数之前,我们先来回顾一下普通类的默认成员函数:C++中成员变量一共可以分为两类 – 内置类型和自定义类型,各个默认成员函数对它们的处理可以用下面两句话概括:

注:由于取地址重载和 const 取地址重载这两个默认成员函数我们一般使用编译器自动生成的即可,所以在这里我们不考虑它们。

注:如果对类的默认成员函数的知识有遗忘或者细节不清楚的可以回头看看我们之前的博客 类和对象中篇

派生类的默认成员函数

派生类的默认成员函数的规则如下:

1、派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用基类的构造函数。 2、派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。 3、派生类的operator=必须要调用基类的operator=完成基类的复制。 4、 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。 5、派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构。 6、子类析构和父类析构构成隐藏关系。(由于多态关系需求,所有的析构函数的函数名都会被编译器处理为 destructor)

上面这些文字看起来太复杂了,其实总结起来就两条:

1、派生类的成员变量分为三类 – 内置类型、自定义类型以及父类成员变量,其中派生类成员函数对内置类型和自定义类型的处理和普通类的成员函数一样,但是父类成员变量必须由父类成员函数来处理; 2、派生类的析构函数非常特殊,它不需要我们显式调用父类的析构函数,而是会在子类析构函数调用完毕后自动调用父类的析构函数,这样做是为了保证子类成员先被析构,父类成员后被析构 (如果我们显式调用父类析构,那么父类成员变量一定先于子类成员变量析构)。同时,子类析构和父类析构构成隐藏

代码语言:javascript
复制
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; //学号
};

(1)父类没有提供默认构造,所以我们需要在子类构造函数的初始化列表处显式调用父类构造来完成父类成员的初始化;

(2)子类的拷贝构造必须调用父类的拷贝构造完成对父类成员的拷贝,同时这里还存在子类对象赋值给父类对象 (切片) 的问题,s 是 Student 的对象,而父类拷贝构造需要的参数是 Person 对象,但是这里我们能够直接将 s 作为父类拷贝构造参数,且中间并不存在类型转换;

(3)子类的赋值重载必须调用父类的赋值重载完成父类成员的赋值,这里需要特别注意,由于子类父类赋值重载函数的函数名相同,构成隐藏,所以我们调用父类赋值重载时必须指定父类作用域,否则会无线递归调用子类赋值重载;同时,这里也存在子类对象赋值给父类对象的问题;

(4)子类析构会在被调用完成后自动调用父类的析构函数清理父类成员,同时子类析构和父类析构函数名都会被处理成 destructor,构成隐藏;


五、继承与友元

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

代码语言:javascript
复制
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;
}

六、继承与静态成员

1、继承与静态成员

类和对象下篇 中我们介绍了类的静态成员变量具有如下特性:

  • 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
  • 静态成员变量必须在类外定义,定义时不添加 static 关键字,类中只是声明;
  • 静态成员变量的访问受类域与访问限定符的约束。

在继承中,如果父类定义了 static 静态成员,则该静态成员也属于所有派生类及其对象;即整个继承体系里面只有一个这样的成员,并且无论派生出多少个子类,都只有一个 static 成员实例;

代码语言:javascript
复制
class Person {
public:
	Person() { ++_count; }

	void Print() {
		cout << this << endl;
	}

public:
	string _name; // 姓名
	static int _count; // 统计人的个数。
};
int Person::_count = 0;  //在类外对静态成员进行定义初始化

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

可以看到,父类和子类在操作时操作的都是同一个静态成员实例,因为父类和子类中静态成员是同一个地址;所以,静态成员属于所有父类及其对象,以及所有派生类及其对象,且只有一份。

2、经典面试题

下面程序的运行结果分别是什么,并解释原因:

代码语言:javascript
复制
class Person {
public:
	Person() { ++_count; }

	void Print() {
		cout << this << endl;
	}

public:
	string _name; // 姓名
	static int _count; // 统计人的个数。
};
int Person::_count = 0;  //在类外对静态成员进行定义初始化

int main()
{
	Person* ptr = nullptr; 
	cout << ptr->_name << endl;   //1
	ptr->Print();                 //2
	cout << ptr->_count << endl;  //3
	
	(*ptr).Print();                //4
	cout << (*ptr)._count << endl; //5

	return 0;
}

1、编译错误:name 是 Person 类中的普通成员变量,存在于对象里面,当我们访问 name 时,编译器会到 ptr 指向的 Person 对象中去取 name,此时发生空指针解引用问题;

2、正确:Print() 是 Person 类中的成员函数,成员函数存在于代码段,不存在对象里面,调用成员函数需要传递 this 指针,刚好 ptr 也是 Person 类型的指针,所以 ptr 的值 0x0000 会被当作 this 指针传递给 Print() 函数;同时,Print() 函数内部并没有访问普通成员变量,所以不会发生空指针解引用问题;

3、正确:count 是静态成员变量,存在于静态区,不存在对象里面,访问 count 时编译器会直接到静态区取数据,所以不会对 ptr 解引用,此处 ptr 的作用是指明类域,等价于 Person::_count;

4、5:正确:虽然这里看起来对 ptr 进行了解引用,但其实并没有,这两句代码经过编译器处理后等价于 2、3:

代码语言:javascript
复制
Person::Print();
Person::_count();

七、C++11 中的 final

在 C++ 中,如果我们希望一个类不被继承该如何做到呢?传统的做法是将类的构造私有,因为子类对象在进行构造时必须调用父类的构造函数完成父类成员的初始化工作,同时父类的私有成员在子类中是不可访问的,所以子类无法调用父类构造,自然也就无法创建子类对象。

上面这种是 C++98 给出的做法,它虽然阻止了子类创建对象,但是构造私有化也使得它本身也不能创建对象,因为创建对象需要调用构造函数。

所以 C++11 提供了另外一种方式 – 使用 final 关键字来修饰,被 final 修饰的类不能被继承:


八、多继承和菱形继承

1、多继承的概念

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

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

多继承使得子类可以同时继承多个直接父类的成员,进一步实现了代码复用。

2、菱形继承

菱形继承是多继承的一种特殊情况,它的表现形式如下:

菱形继承存在数据冗余和二义性的问题,如下,由于 Student 和 Teacher 都继承了 Person 的成员,而 Assistant 又同时继承了 Student 和 Teacher 的成员,所以 Person 的成员在 Assistant 中就会存在两份,造成数据冗余以及调用时二义性的问题;

代码语言:javascript
复制
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; // 主修课程
};

如上,虽然我们可以通过指定类域来解决二义性的问题,但数据冗余仍然存在。

3、菱形虚拟继承

为了解决菱形继承数据冗余和二义性的问题,C++引入了虚拟继承 – 虚拟继承可以解决菱形继承的二义性和数据冗余的问题,如上面的继承关系,在 Student 和 Teacher 继承 Person 时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

特别注意:虚拟继承是使用在产生菱形继承的地方,即菱形的腰部,而不是使用在最后出现问题的地方,即菱形的尾部。

代码语言:javascript
复制
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; // 主修课程
};

虚拟继承解决数据冗余和二义性的底层原理

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

我们首先来观察不进行虚拟继承时菱形继承的内存情况:

代码语言:javascript
复制
class A {
public:
	int _a;
};

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

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

class D : public B, public C {
public:
	int _d;
};

可以看到,在没有进行虚拟继承时,D 对象中存在着三部分成员 – 从 B 继承来的成员、从 C 继承来的成员以及 D 自身的成员;同时,由于 B 和 C 同时继承自 A,所以 D 对象中存在两份 A 的成员,从而造成数据冗余和二义性。

现在我们来观察进行虚拟继承后的内存情况:

代码语言:javascript
复制
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;
};

通过观察我们发现,进行虚拟继承后内存窗口变得非常奇怪:

1、整个 D 对象中只有一份 A 的成员了,并且位于最下面,B 和 C 对象中不再有 A 的成员; 2、但是 B 和 C 对象中多了一个指针,并且当我们查看该指针指向的内存窗口时,我们发现该窗口第二个整形的值恰好为 B/C 对象的起始地址与 A 对象的起始地址的偏移量。 (0x14 = 20B, 0x0c = 12B)

这些现象其实是合理的:

1、整个 D 对象中只有一份 A 对象成员,并且位于最下面,解决了数据冗余和二义性; 2、B 和 C 对象中多出来的那个指针指向的内容被称为虚基表,虚基表中前四个字节的内容与多态有关,我们下一节介绍,后四个字节的内容存放的是对象的起始地址与虚基类起始地址的偏移量

那么为什么进行虚继承的类对象中要记录距离虚基类的偏移量呢?其实是为了满足切片的场景:

可以看到,虽然 1 和 2 都是在 B 对象中去访问 A 成员变量 a,但是 A 的在内存中的位置是不同的,如果此时我们仍然到最下面去访问 _a,那么 2 访问的结果就会是 C 的虚基表指针;但是按照偏移量访问就则不会出现这种问题。

注:有的同学可能会说,上面虚继承并没有解决数据冗余的问题,反而还多浪费了空间,这是由于为了方便理解,我们将上面的类故意设计的很小,只要类稍微大一点大家就会发现虚继承确实节省了空间。

所以上面的 Person 关系菱形虚拟继承的原理解释如下:

总结-- 虚继承是如何解决菱形继承数据冗余和二义性的问题的:

1、在对象模型上,虚继承将虚基类放在了模型的最下面,使得虚基类在对象模型中只存在一份; 2、同时,为了在切片场景下也能够找到虚基类,虚继承的类对象中会存储一个虚基表指针,虚基表里面存储了虚继承对象的存储地址与虚基类地址的偏移量;虚继承类对象可以根据这个偏移量来找到虚基类。


九、继承的总结和反思

很多人说 C++ 语法复杂,其实多继承就是一个体现;有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,这会使得底层实现变得非常复杂;所以一般不建议设计出多继承,一定不要设计出菱形继承,否则在复杂度及性能上都有问题。

同时,多继承可以认为是 C++ 的缺陷之一,很多后来的面向对象语言都吸取了 C++ 的经验,将多继承给去除了,比如 Java。

注:在实际工作中,菱形继承基本上不会遇到,所以也就不存在虚继承的问题,但是在校招中比较喜欢考察虚继承,以此来检测同学们对 C++ 的理解。


十、继承与组合

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象;而组合是一种 has-a 的关系,假设 B 组合了 A,则每个 B 对象中都有一个 A 对象。

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

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

所以如果既能用继承,也能用组合,优先使用组合,因为组合耦合度低,代码维护性好-- 对于继承来说,父类的任何一个非私有成员修改都可能会影响子类,而对于组合,只有公有成员修改才可能会影响;但在实际开发中基本上不会出现全部都是公有成员的类,所以优先使用组合。

不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态也必须要继承,只是说当类之间的关系即可以用继承,可以用组合时,优先使用组合。

代码语言:javascript
复制
// 车和宝马、奔驰构成is-a的关系,使用继承
class Car {
protected:
	string _colour = "白色"; // 颜色
	string _num = "陕ABIT00"; // 车牌号
};

class BMW : public Car {
public:
	void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

// 轮胎和车构成has-a的关系,使用组合
class Tire {
protected:
	string _brand = "Michelin";  // 品牌
	size_t _size = 17;     // 尺寸
};

class Car {
protected:
	string _colour = "白色"; // 颜色
	string _num = "陕ABIT00"; // 车牌号
	Tire _t; // 轮胎
};

十一、笔试面试题

  1. 派生类的默认成员函数有哪些特点?
  2. 什么是菱形继承?菱形继承的问题是什么?
  3. 什么是菱形虚拟继承?菱形虚拟继承是如何解决菱形继承数据冗余和二义性的?
  4. 继承和组合的区别是什么?什么时候用继承?什么时候用组合?

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-03-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 一、继承的概念及定义
    • 1、继承的概念
      • 2、继承的定义
      • 二、基类和派生类对象的赋值
      • 三、继承中的作用域
        • 1、继承中的作用域 (隐藏)
          • 2、经典面试题
          • 四、派生类的默认成员函数
          • 五、继承与友元
          • 六、继承与静态成员
            • 1、继承与静态成员
              • 2、经典面试题
              • 七、C++11 中的 final
              • 八、多继承和菱形继承
                • 1、多继承的概念
                  • 2、菱形继承
                    • 3、菱形虚拟继承
                    • 九、继承的总结和反思
                    • 十、继承与组合
                    • 十一、笔试面试题
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档