C++--继承

继承

1. 继承的概念及定义

1.1 继承的概念

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

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前接触的复用都是函数复用,而继承便是类设计层次的复用。

具体示例:

下面在没有学习到继承之前设计了两个类 StudentTeacherStudentTeacher 都有姓名、年龄等成员变量,都有 Print 这个成员函数,然而这些内容在这两个类中是重复出现的,设计到两个类里面就是冗余的。

当让这两个类中也有一些不同的成员变量,比如 Teacher 中独有的成员变量是工号,Student 中独有的成员变量是学号。

//Student类
class Student
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "张三";   //姓名
	int _age = 18;  		//年龄
    
    int _stuid;   			//学号
};

//Teacher类
class Teacher
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "张三";   //姓名
	int _age = 18;  		//年龄
    
	int _jobid;   			//工号
};

在这里插入图片描述

下面是使用了继承,将公有的成员都放到了 Person 中,StudentTeacher 都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多麻烦。

继承后,父类 Person 的成员,包括成员函数和成员变量,都会变成子类的一部分,也就是说,子类 StudentTeacher 复用了父类 Person 的成员。

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

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

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

在这里插入图片描述

1.2 继承的定义

1.2.1 定义格式

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

在这里插入图片描述

1.2.2 继承方式和访问限定符

访问限定符有以下三种:

  1. public访问
  2. protected访问
  3. private访问

继承的方式也有类似的三种:

  1. public继承
  2. protected继承
  3. private继承

在这里插入图片描述

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

基类当中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化。

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

**总结:**可以认为三种访问限定符的权限大小为:public > protected > private,可以以这个为基准去理解表中的结果。

1.2.3.1 基类成员访问方式的变化规则
  1. 在基类当中的访问方式为 publicprotected 的成员,在派生类当中的访问方式变为:Min(成员在基类的访问方式,继承方式)
  2. 在基类当中的访问方式为 private 的成员,在派生类当中都是不可见的。

如何去理解基类的private成员在派生类当中不可见

这句话的意思是,无法在派生类当中访问基类的 private 成员。

例如,虽然 Student 类继承了 Person 类,但是无法在 Student 类当中访问 Person 类当中的 private 成员 _name。

//基类
class Person
{
private:
	string _name = "张三"; //姓名
};
//派生类
class Student : public Person
{
public:
	void Print()
	{
		//在派生类当中访问基类的private成员,报错!
		cout << _name << endl; 
	}
protected:
	int _stuid;   //学号
};

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

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

**注意:**在实际运用中一般使用的都是 public 继承,几乎很少使用 protectedprivate 继承,也不提倡使用 protectedprivate 继承,因为使用 protectedprivate 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

1.2.3.2 默认继承方式

在使用继承的时候也可以不指定继承方式,使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public

在关键字为class的派生类当中,所继承的基类成员_name的访问方式变为private。

//基类
class Person
{
public:
	string _name = "张三"; //姓名
};

//派生类
class Student : Person //默认为private继承
{
protected:
	int _stuid;   //学号
};

在关键字为struct的派生类当中,所继承的基类成员_name的访问方式仍为public。

//基类
class Person
{
public:
	string _name = "张三"; //姓名
};

//派生类
struct Student : Person //默认为public继承
{
protected:
	int _stuid;   //学号
};

注意: 虽然继承时可以不指定继承方式而采用默认的继承方式,但还是最好显示的写出继承方式。

1.3 继承类模版

下面是利用继承,通过 vector<int> 作为基类继承给了 stack 从而达到快速开发的目的。

namespace bit
{
 	//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;
}

**补充:**第18行,使用了类模版中还未实例化的成员函数一定要指定类域才可以调用。

2. 基类和派生类的转化

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

在这里插入图片描述

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

具体示例:

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

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

int main()
{
 	Student s;
	Person p = s;     //派生类对象赋值给基类对象,可以
	Person* ptr = &s; //派生类对象赋值给基类指针,可以
	Person& ref = s;  //派生类对象赋值给基类引用,可以
    
    s = p;	//基类对象不可以赋值给派生类,这里会编译错误
    
    return 0;
}

派生类对象赋值给基类指针图示:

在这里插入图片描述

派生类对象赋值给基类引用图示:

在这里插入图片描述

**注意:**基类对象不能赋值给派生类对象,基类的指针可以通过强制类型转换赋值给派生类的指针,但是此时基类的指针必须是指向派生类的对象才是安全的。

3. 继承中的作用域

3.1 隐藏

在继承体系中的基类和派生类都有独立的作用域。若派生类和基类中有同名成员(成员变量、成员函数),派生类成员将屏蔽基类对自己作用域中同名成员的直接访问,这种情况叫隐藏

具体示例:

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

//父类
class Person
{
protected:
	int _num = 111;
};

//子类
class Student : public Person
{
public:
	void fun()
	{
		cout << _num << endl;
	}
protected:
	int _num = 999;
};

int main()
{
	Student s;
	s.fun(); 
	return 0;
}
//运行结果:999

对于以上代码,访问成员函数 fun 会打印子类中的成员变量 _num ,但是父类和子类中均有成员变量 _num ,但是这里会访问子类中的 _num,也就是打印 999

补充:

  • 若此时就是要访问父类当中的 _num 成员,可以使用作用域限定符进行指定访问

    void fun()
    {
    	cout << Person::_num << endl; //指定访问父类当中的_num成员
    }
    

3.2 经典面试题

题目描述:

**问题1:**下面两个 func 是什么关系?A. 重载 B. 重写 C.没关系

**问题2:**下面这段程序编译运行的结果是什么?A. 编译报错 B. 运行报错 C.正常运行

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.fun();
 
 return 0;
}

问题1:

虽然 A 类中的 func 函数和 B 类中的 func 函数同名且参数不同,但是它们不构成重载,因为它们的作用域不同,重载函数一定是在同一个作用域中的。并且根据隐藏的规则,成员函数的隐藏,只需要函数名相同就构成隐藏,可知,这两个函数构成隐藏,选择A。

问题2:

所以两个 func 的关系是隐藏,因为 B 继承自 A,这里通过实例化 B 的对象 b 来调用 fun 函数,但是 B 中的 fun 函数需要参数,所以这里的语法出现问题,会编译报错。选择A。

如果这里想调用父类中的 fun 函数,需要在指定父类作用域。

总结:

  • 针对成员变量,派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类 :: 基类成员显示访问)
  • 如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中在继承体系里面最好不要定义同名的成员。

4. 派生的默认成员函数

4.1 普通类的默认成员函数

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

在这里插入图片描述

在这里插入图片描述

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

4.2 派生类的默认成员函数

和普通类的默认成员函数一样,这里只讨论构造函数、析构函数、拷贝构造函数和赋值重载函数这四个成员函数。

4.2.1 派生类的构造函数

在均使用默认构造的前提下对于派生类的成员变量中的内置类型(有缺省值就用,没有就由编译器初始化)、自定义类型(使用默认构造)和父类成员(调用父类默认构造)。

若要自行实现构造函数,如果基类有默认的构造函数,派生类的构造函数无须调用基类的构造函数直接初始化子类的成员变量即可。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用基类的构造函数,再对剩余派生类中的成员进行构造。

//父类
class Person 
{
public:
    //父类构造函数
	Person(const char* name)//非默认构造
		: _name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name; // 姓名
}

//子类
class Student : public Person 
{
public:
    //自行实现的子类构造函数
	Student(const char* name, int num)
		: Person(name)  //显示调用父类构造
		, _num(num)
	{
		cout << "Student()" << endl;
	}
    
protected:
	int _num; //学号
}

补充:上述代码中的父类中没有构造函数,所以其子类 Student 的构造函数就必须先显示调用父类 Person 的构造函数再初始化子类中的成员变量。如果父类 Person 中的构造函数有默认构造那么子类的构造函数在默认情况下就无需显式调用父类的构造函数,只需要对子类的成员函数进行初始化即可。

4.2.2 派生类的拷贝构造函数

在均使用默认拷贝构造的前提下对于派生类的成员变量中的内置类型(浅拷贝)、自定义类型(此类型的拷贝构造)和父类成员(调用父类的拷贝构造)。

如果要自行实现拷贝构造函数,派生类的拷贝构造函数必须先调用基类的拷贝构造完成基类的拷贝初始化。再对子类中的成员变量进行拷贝构造。

//父类
class Person 
{
public:
    //父类拷贝构造函数
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
protected:
	string _name; // 姓名
}

//子类
class Student : public Person 
{
public:
    //自行实现的子类拷贝构造函数
	Student(const Student& s)
		: Person(s)  //显示调用父类拷贝构造
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
    
protected:
	int _num; //学号
}

**补充:**针对拷贝构造无论父类如何,子类若要自行实现拷贝构造函数必须在初始化列表中显示调用父类的拷贝构造函数。

并且这里调用父类的拷贝构造时传递的参数直接传递子类的变量名即可,因为以指针的形式接收,父类的拷贝构造函数接受后会对其进行切割,这里涉及基类和派生类对象的赋值中的知识点。

4.2.3 派生类的赋值重载函数

在均使用默认赋值重载构造的前提下对于派生类的成员变量中的内置类型(浅拷贝)、自定义类型(此类型的拷贝构造)和父类成员(调用父类的拷贝构造)。与拷贝构造相同。

如果要自行实现赋值重载函数,其要求也与自行实现拷贝构造函数相同,必须要调用基类的operator=完成基类的复制。

//父类
class Person 
{
public:
    //父类赋值重载函数
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}
protected:
	string _name; // 姓名
}

//子类
class Student : public Person 
{
public:
    //自行实现的子类赋值重载函数
	Student& operator = (const Student& s) {
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);  //父类赋值重载
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num; //学号
}

**补充:**针对赋值重载函数无论父类如何,子类若要自行实现赋值重载函数必须在初始化列表中显示调用父类的赋值重载函数。

同样这里的赋值重载函数也需要使用传子类的变量名作为参数给父类的赋值重载函数,因为因为以指针的形式接收,父类的赋值重载函数接受后会对其进行切割,使其变成子类对象中父类那部分的别名。

并且这里调用 operator = 时需要指定父类类域,因为如果不指定这里的 operator = 由于子类和父类中的函数名相同构成隐藏,就会一直调用子类的 operator = 最后造成栈溢出

4.2.4 派生类的析构函数
//父类
class Person 
{
public:
    //父类析构函数
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
}

//子类
class Student : public Person 
{
public:
    //自行实现的子类赋值重载函数
	~Student() 
    {
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
}

补充:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。所以在自行实现子类析构函时不需要显示调用父类的析构函数。

如果在平时代码中需要在子类中调用父类的析构函数需要使用类域指定析构函数,Person : ~Person()。这是因为派生类的析构和基类的析构构成隐藏关系。(由于多态关系需求,所有的析构函数的函数名都会被编译器处理为 destructor,因为函数名相同所以构成隐藏。)

4.2.5 派生类的4个成员函数的总结
  1. 派生类的成员变量分为三类:内置类型、自定义类型以及父类成员变量。其中派生类成员函数对内置类型和自定义类型的处理和普通类的成员函数一样,但是父类成员变量必须由父类成员函数来处理

  2. 派生类的析构函数非常特殊,它不需要我们显式调用父类的析构函数,而是会在子类析构函数调用完毕后自动调用父类的析构函数,这样做是为了保证子类成员先被析构,父类成员后被析构 (如果我们显式调用父类析构,那么父类成员变量一定先于子类成员变量析构)。同时,子类析构和父类析构构成隐藏在这里插入图片描述

  3. 派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构。

    并且派生类对象的析构函数在被调用完之后会自动调用基类的析构函数清理基类成员。因为只有这样才能保证派生类对象先清理派生类成员在清理基类成员的顺序。在这里插入图片描述

4.3 面试题:实现一个不能被继承的类

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

class Base 
{
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;
}

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

**方法2:**C++11新增了一个 final 关键字,final 修改基类,派生类就不能继承了。

//C++11的方法
class Base final
{
public:
 	void func5() { cout << "Base::func5" << endl; }
protected:
 	int a = 1;
private:
};

class Derive :public Base 
{
 	void func4() { cout << "Derive::func4" << endl; }
protected:
 	int b = 2;
};
	int main()
{
 	Base b;
 	Derive d;
 	return 0;
}

5. 继承和友元

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

**简记:**你父亲的朋友并不是你的朋友。

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

class Student : public Person 
{
    friend void Display(const Person& p, const Student& s);  //友元函数
protected:
	int _stuNum; // 学号
};

void Display(const Person& p, const Student& s) 
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}

补充:这里的 Dispaly 函数分别调用了基类和派生类中的成员变量,如果需要访问的话需要再基类和派生类中都加上友元声明。

6. 继承和静态成员

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

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

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

class Person
{
public:
 	string _name;
 	static int _count;	//静态成员类内声明
};
int Person::_count = 0;	//静态成员类外定义

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

在这里插入图片描述

7. 多继承和菱形继承问题

7.1 继承模型

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

从菱形继承的模型构造就可以看出,菱形继承的继承方式存在数据冗余二义性的问题。

例如,对于以上菱形继承的模型,当实例化出一个 Assistant 对象后,访问成员时就会出现二义性问题。

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 = "peter"; //二义性:无法明确知道要访问哪一个_name,产生报错
	return 0;
}

补充: Assistant 对象是多继承的 StudentTeacher ,而 StudentTeacher 当中都继承了 Person ,因此 StudentTeacher 当中都有 _name 成员,若是直接访问 Assistant 对象的 _name 成员会出现访问不明确的报错。

如果想要访问 _name 中的数据,可以具体指定是哪个类域的 _name

//显示指定访问哪个父类的成员
a.Student::_name = "张同学";
a.Teacher::_name = "张老师";

虽然该方法可以解决二义性的问题,但仍然不能解决数据冗余的问题。因为在 Assistant的对象在 Person 成员始终会存在两份。

在这里插入图片描述

7.2 虚继承

为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承。如前面说到的菱形继承关系,在Student和Teacher继承Person是使用虚拟继承,即可解决问题

虚拟继承代码如下:

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;
	a._name = "peter"; //无二义性
	return 0;
}

此时就可以直接访问 Assistant 对象的 _name 成员了,并且之后就算指定访问 AssistantStudent 父类和 Teacher 父类的 _name 成员,访问到的都是同一个结果,解决了二义性的问题。而打印 AssistantStudent 父类和 Teacher 父类的 _name 成员的地址时,显示的也是同一个地址,解决了数据冗余的问题。

cout << a.Student::_name << endl; //运行结果:peter
cout << a.Teacher::_name << endl; //运行结果:peter

cout << &a.Student::_name << endl; //运行结果:0136F74C
cout << &a.Teacher::_name << endl; //运行结果:0136F74C

7.3 虚拟继承的原理

7.4 多继承面试题

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

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

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

解答:

在这里插入图片描述

首先创建一个 Derive 类的对象 d ,因为C++中规定在内存中先继承的存储在前面,所以这里的 Base1Base2 呈上图存储方式排列。

所以 p3 理所当然指向这块空间的起始地址,然而对于 p1p2 因为其类型为 Derive 的父类,所以在赋值的时候,需要进行切片,指向子类中父类那一块所属的空间。

p1 指向 Base1 的起始地址也就是和 p3 指向的空间一样,但是需要注意 p1p3 的含义并不一样,如果对 p3 解引用其空间包含 d 的整块空间,如果对 p1 解引用其空间则只包含 Base1 那一块。p2与以上类似,指向 Base2 的起始地址。

最后根据图示的地址大小可以得出答案为C。

8. 继承和组合

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象,本质是子类对象是一种特殊的父类对象

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

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

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

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

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

**eg1:**车类和宝马类就是is-a的关系,它们之间适合使用继承。

class Car
{
protected:
	string _colour; //颜色
	string _num; //车牌号
};

class BMW : public Car	//BWM是车,继承关系
{
public:
	void Drive()
	{
		cout << "this is BMW" << endl;
	}
};

**eg2:**车和轮胎之间就是has-a的关系,它们之间则适合使用组合。

class Tire
{
protected:
	string _brand; //品牌
	size_t _size; //尺寸
};

class Car	//车有轮胎,组合关系
{
protected:
	string _colour; //颜色
	string _num; //车牌号
	Tire _t; //轮胎
};
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值