【C++】继承

面向对象三大特性,封装、继承和多态。通过前面文章的学习,我们已经对封装有所了解,比如类是一种封装,迭代器也是一种封装等。

在平时写代码过程中,函数复用可以帮助我们减少许多工作量,那么类可以复用吗?

类当然可以复用,比如前面所学习到的类模板,它就是一种类的复用,那还有没有其它复用类的方法呢?接下来进入本篇核心内容:继承。

一、继承的概念

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

接下来先看一段代码:

cpp 复制代码
class Student
{
public:
	void identity() //身份验证 
	{}
	
	void study() //学习 
	{}

protected:
	string _name = "心怀花木";   //姓名 
	string _address;            //地址 
	string _tel;                //电话 

	int _stuid;                 //学号 
private:
	int _age = 18;              //年龄 
};

class Teacher
{
public:
	void identity() //身份验证 
	{}
	
	void teaching() //授课 
	{}

protected:
	string _name = "明灯";     //姓名 
	string _address;          //地址 
	string _tel;              //电话 

	string _title;            //职称 
private:
	int _age = 28;            //年龄 
};    

通过观察Student和Teacher这两个类,不难发现它们中的代码有许多的重复的部分:Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面是冗余的。当然它们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。

要解决冗余问题,可以使用继承,将Person和Teacher类中的公共成员都放到Person类中,Student类和teacher类都继承Person类,就可以复用Person类中的成员,就不需要重复定义了,省去了许多麻烦,具体代码如下(代码中具体语法细节暂且忽略):

cpp 复制代码
class Person
{
public:
	void identity() //身份验证 
	{
		cout << "void identity()" << _name << endl;
	}

protected:
	string _name = "xxx";  //姓名 
	string _address;       //地址 
	string _tel;           //电话 
private:
    int _age = 18;         //年龄 
};

class Student : public Person
{
public:
	void study() //学习 
	{}

protected:
	int _stuid; //学号 
};

class Teacher : public Person
{
public:
	void teaching() //授课 
	{}

protected:
	string title; //职称 
};

Student类中的对象可以调用Person类中的成员函数,Teacher类中的对象也可以调用Person类中的成员函数,这样就减少了代码冗余。

二、继承的定义

上面我们看到的Person类是父类,也被称为基类。Student和Teacher是子类,也被称为是派生类。

以Student为例:

下面单独讲解一下继承方式:

继承方式和访问修饰符一样,有3种,分别是公有、私有、保护。

对应下面一张表:

这张表所表达的信息就是,父类中被访问限定符修饰的成员,分别通过三种继承方式后,在子类中的访问权限。

下面进行详细的介绍:

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

它。

举个例子:

cpp 复制代码
class Person
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}

protected:
	string _name = "xxx";   
	string _address;       
	string _tel;          
private:
	int _age = 18;
};
class Student : public Person
{
public:
	void study()
	{
		cout << _age << endl; //err
        //因为_age在Person中是private成员,所以不管以什么方式继承下来,对于子类来说都是不可见的,也就是不能直接使用。
        //但_age的确是被继承下来的,也就是说Student类的对象有_age这个属性,只是不能访问

        cout << _name << endl; //因为_name是protected成员,在子类中可以直接访问,是可见的,这也是protected和private的主要区别

		//private和protected修饰的成员在类外都是不可访问的
	}

protected:
	int _stuid; 
};
int main()
{
	Student s;
	return 0;
}

通过调试,我们可以看到,_age的确是被继承下来的:

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

如果在子类中想要去访问到父类中的private成员,也是有办法的:可以间接访问,可以先在父类中写一个获取的函数:

cpp 复制代码
int GetAge()
{
	return _age;
}

再在子类中去调用:

cpp 复制代码
cout << GetAge() << endl;

这样就可以间接获得了。

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

4、在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用

protetced/private继承,因为protetced/private继承下来的成员都只能在子类的类里面使用,实际中扩展维护性不强。

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

三、继承类模板

在某种程度上,继承类模板也可以达到适配器的效果,比如:

cpp 复制代码
namespace blue //加上命名空间是为了和库中的stack发生命名冲突
{
	template<class T>
	class stack : public std::vector<T>  //继承类模板
	{
	public:
		void push(const T& x)
		{
			push_back(x);
		}

		void pop()
		{
			pop_back();
		}

		const T& top()
		{
			return back();
		}

		bool empty()
		{
			return empty();
		}
	};
}

int main()
{
	blue::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}

	return 0;
}

运行代码时会报错:

出现这种原因是和按需实例化有关,按需实例化针对的是模板,它的意思是,即使函数模板实例化了,函数模板中的成员函数如果不调用,成员函数依旧不会实例化。对于上述代码,定义了st对象,它继承vector,vector虽然实例化了,但此时它里面的成员函数并没有调用,所以也不会实例化,stack中的push函数直接调用push_back,push_back此时并没有实例化,所以调用时找不到,就会报这样的错误。

解决方法如下:

cpp 复制代码
namespace blue //加上命名空间是为了和库中的stack发生命名冲突
{
	template<class T>
	class stack : public std::vector<T>  //继承类模板
	{
	public:
		//当父类是类模板时,需要指明类域
		void push(const T& x)
		{
			std::vector<T>::push_back(x); //先在子类中找,再到父类中找
		}

		void pop()
		{
			std::vector<T>::pop_back();
		}

		const T& top()
		{
			std::return vector<T>::back();
		}

		bool empty()
		{
			std::return vector<T>::empty();
		}
	};
}

int main()
{
	blue::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}

	return 0;
}

运行结果:

我们还可以对代码整体进一步改进:

cpp 复制代码
#define CONTAINER std::vector
//#define CONTAINER std::list
//#define CONTAINER std::deque

namespace blue //加上命名空间是为了和库中的stack发生命名冲突
{
	template<class T>
	class stack : public CONTAINER<T>  //继承类模板
	{
	public:
		//当父类是类模板时,需要指明类域
		void push(const T& x)
		{
			CONTAINER<T>::push_back(x); //先在子类中找,再到父类中找
		}

		void pop()
		{
			CONTAINER<T>::pop_back();
		}

		const T& top()
		{
			return CONTAINER<T>::back();
		}

		bool empty()
		{
			return CONTAINER<T>::empty();
		}
	};
}

可以用宏来控制继承不同的类模板,使代码更加灵活多变。

四、父类和子类对象赋值兼容转换

public继承的子类对象可以赋值给父类的对象/父类的指针/父类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切开赋值过去。

通俗来讲,就是将子类中的父类的那一部分,赋值给父类,且中间不发生类型转换。

代码说明如下:

cpp 复制代码
class Person
{
public:
	string _name;

protected:
	string _sex;
	int _age;
};

class Student : public Person
{
public:
	int _No;
};

int main()
{
	Student sobj; //子类对象

	//子类对象赋值给父类对象/指针/引用 
	Person pobj = sobj;
	Person* pp = &sobj; //这里的指针只管理子类中父类的那一部分,引用也是如此
	Person& rp = sobj; //注意:这里没有产生临时对象,所以不加const,可以理解为C++做了特殊处理

	return 0;
}

注意:父类对象不能赋值给子类,强制转换也不行。但父类的指针或引用有时可以给子类的指针或引用(父类对象指针指向的是子类对象时可以强制转换赋给子类对象的指针)。

五、继承中的作用域

在继承体系中父类和子类都有独立的作用域。

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用父类::父类成员显示访问)

代码说明:

cpp 复制代码
class Person
{
protected:
	string _name = "张三"; 
	int _num = 1; 
};

class Student : public Person
{
public:
	void Print()
	{
		//Student的_num和Person的_num构成隐藏关系
        //Student类中的_num将Person类中的_num隐藏起来了

		//子类中有,默认先访问子类,若子类中没有而父类中有,则访问父类
		cout << _num << endl;

		//若子类和父类同时存在,要想访问父类要加上作用域
		cout << Person::_num << endl;
	}

protected:
	int _num = 9; 
};

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

代码说明:

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();  //err,B中的func将A中的func隐藏了,所以不能调用
    b.A::func(); //指定作用域就可以调用
	return 0;
};

B中的func与A中的func不构成函数重载,因为函数重载的前提是在同一作用域下。

在实际中在继承体系里面最好不要定义同名的成员。

六、子类的默认成员函数

注意要把默认成员函数和默认构造函数分开,概念不要搞混淆。

默认成员函数是我们不写,编译器会默认生成(它包括默认构造,拷贝构造,赋值重载,析构函数)。默认构造函数是我们不需要传参就可以调用的构造函数。

那么在子类中,这几个成员函数是如何生成的呢?

1、构造函数

子类的构造函数必须调用父类的构造函数初始化父类的那⼀部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用,否则编译报错。

默认生成的构造函数的行为:

1.内置类型:不确定,取决于编译器

2.自定义类型:调用它的默认构造

3.继承的父类成员看作一个整体对象,要求调用父类的默认构造(相比普通的类多出的部分)

代码说明:

cpp 复制代码
class Person
{
public:
	Person(const char* name = "zhangsan")
		:_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:
	//默认生成的构造函数的行为
	//1.内置类型:不确定,取决于编译器
	//2.自定义类型:调用它的默认构造
	//3.继承的父类成员看作一个整体对象,要求调用父类的默认构造(相比普通的类多出的部分)

private:
	int _num;
	string _address;
};

int main()
{
	Student s;
	return 0;
}

经过调试:

调试结果正如上述所说的"默认生成的构造函数的行为"那样进行。我们也可以在成员变量的位置上给缺省值,它会走初始化列表进行初始化。

如果父类中没有默认构造函数,则必须在子类构造函数的初始化列表阶段显示调用,否则编译报错。

代码说明:

cpp 复制代码
class Person
{
public:
	//这里,Person类没有默认构造
	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:
	//默认生成的构造函数的行为
	//1.内置类型:不确定,取决于编译器
	//2.自定义类型:调用它的默认构造
	//3.继承的父类成员看作一个整体对象,要求调用父类的默认构造(相比普通的类多出的部分)


    //在子类构造函数的初始化列表中显示调用,如果不写就会报错
	Student(const char* name, int num, const char* address)
		:Person(name)   //不能写成这样:_name(name),子类不能直接初始化父类成员
		,_num(num)
		,_address(address)
	{}

private:
	int _num;
	string _address;
};

int main()
{
	Student s("zhangsan",1,"China");
	return 0;
}

经过调试:

2、拷贝构造

子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。

默认生成的拷贝构造的行为:

1.内置类型:浅拷贝 / 值拷贝

2.自定义类型:调用它的拷贝构造

3.继承的父类成员看作一个整体对象,要求调用父类的拷贝构造(相比普通的类多出的部分)

只有子类中的成员变量有资源申请,需要深拷贝时我们才会单独写拷贝构造,如果不需要深拷贝,那么编译器提供的默认的拷贝构造就够用了。

如果要写拷贝构造,那我们可以这样去写:

cpp 复制代码
Student(const Student& s)
	:Person(s)  //赋值兼容转换,如果不写这一句,且Person中没有默认构造函数,就会报错,如果Person中有默认构造函数,则不会报错,但可能会不满足需求
	,_num(s._num)
	,_address(s._address)
{
	//完成深拷贝工作
	//...
}

3、赋值重载

默认生成的赋值重载的行为:

1.内置类型:浅拷贝 / 值拷贝

2.自定义类型:调用它的赋值重载

3.继承的父类成员看作一个整体对象,要求调用父类的赋值重载(相比普通的类多出的部分)

只有子类中的成员变量有资源申请,需要深拷贝时我们才会单独写赋值重载,如果不需要深拷贝,那么编译器提供的默认的赋值重载就够用了。

如果要写赋值重载,那我们可以这样去写:

cpp 复制代码
Student& operator=(const Student& s)
{
	if (this != &s)
	{
        //子类的operator=必须要调用父类的operator=完成父类的赋值。
        //需要注意的是子类的operator=隐藏了父类的operator=,所以显示调用父类的operator=,需要指定父类作用域。
		Person::operator=(s); //赋值兼容转换,必须指明作用域
		_num = s._num;
		_address = s._address;
	}

	return *this;
}

4、析构函数

默认生成的析构函数的行为:

1.内置类型:不需要析构

2.自定义类型:调用它的析构函数

3.继承的父类成员看作一个整体对象,要求调用父类的析构函数(相比普通的类多出的部分)

只有当子类中的成员变量有资源申请,且需要主动释放时,才需要我们主动写析构函数。

如果要写析构函数,那我们可以这样去写:

cpp 复制代码
~Student()
{
	//这里需要注意:父类的析构和子类的析构构成隐藏关系,其实它们名为destructor()
	//规定:这里不需要显式调用,子类析构函数之后,会自动调用父类析构
	//如果这里显示调用,那么父类析构会被调用两次,如果有资源申请,那就会导致释放两次,从而发生错误
	//Person::~Person(); //必须指明作用域

	//释放申请的资源
	//...
}

子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员,要保证"先子后父"的顺序。

因为子类对象初始化时先调用父类构造再调子类构造, 所以子类对象析构时**先调用子类析构再调父类的析构,**遵循后定义的先析构。

父类的析构和子类的析构构成隐藏关系:

因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

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

每个类都可以被继承吗?答案是否定的,有的类不能被继承。

方法一:将父类的构造函数私有,子类对象的构造必须调用父类的构造函数,但是父类的构造函数私有化以后,子类看不见就不能调用,那么子类就无法实例化出对象。

代码说明:

cpp 复制代码
class Base
{
public:
	void func1() { cout << "Base::func1" << endl; }

protected:
	int a = 1;

private:
	//C++98的方法,用private修饰父类中的构造函数
	Base(){}
};

class Derive :public Base //这里不会报错
{
	void func2() { cout << "Derive::func2" << endl; }

protected:
	int b = 2;
};
int main()
{
	//父类不能定义对象,子类也不能定义对象
	Base b;  //err
	Derive d; //err

	return 0;
}

但有一点是需要注意:如果不定义对象的话,代码是不会报错的。

方法二:在类名后面加上一个final,任何类都不能继承此类。(C++引入的关键字final)

代码说明:

cpp 复制代码
class Base final  //C++11的方法
{
public:
	void func1() { cout << "Base::func1" << endl; }

protected:
	int a = 1;
};

class Derive :public Base //无法被继承,语法上过不去,会直接报错
{
	void func2() { cout << "Derive::func2" << endl; }

protected:
	int b = 2;
};

这种方式更加直观。

八、继承与友元

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

代码说明:

cpp 复制代码
//前置声明
class Student;

class Person
{
	//友元关系不能被继承
	friend void Display(const Person& p, const Student& s); //在编译时,编译器不认识Student是什么,向上查找也找不到,所以这里要添加前置声明
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; //友元关系不能被继承,所以要想访问子类中的成员变量,必须也要在子类中添加友元声明
}

int main()
{
	Person p;
	Student s;
	Display(p, s);

	return 0;
}

九、继承与静态成员

父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。简单来说,如果父类中有一个普通成员_name,那么子类在继承父类后也会有一个_name,这两个_name是不同的,如果父类中有一个静态成员_count,那么子类在继承父类后也会有一个_count,这两个_count是相同的。

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;
    
    Person::_count++;

	//公有情况下,也可以用"对象.静态成员"的方式进行访问
	cout << p._count << endl;
	cout << s._count << endl;

	return 0;
}

运行结果:

十、多继承及其菱形继承

1、继承模型

(1)单继承

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

(2)多继承

一个子类有两个或两个以上直接父类 时称这个继承关系为多继承,多继承对象在内存中的模型

是,先继承的父类在前面,后面继承的父类在后面,子类成员在放到最后面。

(3)菱形继承

菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余二义性的问题,在Assistant的对象中Person成员会有两份。

代码说明:

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

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 = "zhangsan"; //err,编译报错,此时Teacher中有_name,Student中也有_name,_name的访问不明确

	//需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "小张";
	a.Teacher::_name = "老张";
	//一个人可以有两个名字这很正常,但Person中的_sex只能有一个吧,一个人不可能有两个性别,那这种情况下就出现了数据冗余。

	return 0;
}

所以,菱形继承是不好的,支持多继承就可能会存在菱形继承,像Java就不直接支持多继承,这样就规避掉了二义性和数据冗余这些问题,所以在实践中不建议设计出菱形继承这样的模型。

有一种方式也可以解决菱形继承带来的问题,那就是虚继承 。它的用法就是在继承方式前面加上一个关键字virtual

代码说明:

cpp 复制代码
class Person
{
public:
	string _name; 
};

//使用虚继承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()
{
	//使用虚继承,可以解决数据冗余和二义性,共用一份_name
	Assistant a;
	a._name = "zhangsan"; //Teacher中的_name是"zhangsan",Student中的_name也是"zhangsan",Person中的_name也是"zhangsan",它们共用一份_name,所以不会有数据冗余,也不会有二义性,也不会报错

	return 0;
}

通过调试可以有更深的理解:

这里还要注意一个点:在菱形继承中,不是任何类在继承时都要加virtual,而是哪个类产生数据冗余,继承时才用虚继承。

举个例子:

2、多继承指针偏移问题

现有一段代码,请判断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,p2,p3它们三者之间的关系。

如图所示:

所以,p1 == p3 != p2,准确来说是p1 == p3 < p2。栈是向下增长的,上面地址低,下面地址高。

十一、继承和组合

1、public继承是一种is-a的关系。也就是说每个子类对象都一个父类对象。

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

打个比方,组合就是stack中的容器,我们知道stack本质上是用另外一个容器实现的,假设这个容器是vector,stack中有一个vector,这就是一种组合。

继承就是每个子类对象都一个父类对象。

3、继承允许你根据父类的实现来定义子类的实现(父类中的非私有成员或函数在子类中都可以"观察"的很清楚)。这种通过生成子类的复用通常被称为白箱复用。术语"白箱"是相对可视性而言:在继承方式中,父类的内部细节对子类可见。 继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度高。

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

这里简单说一下耦合度的概念:

如果两个模块关联度特别强,假设分别是模块一和模块二,模块一提供给模块二100个接口,如果模块一时不时修改这100个接口,那模块二可就"惨"了,它要时刻关注模块一这100个接口的变化,它发现这100个接口发生变化了,就必须立马跟着改,否则就会出问题,这就是两个模块间耦合度高的表现,这是不好的。

如果两个模块关联度不强,假设分别是模块一和模块二,模块二要求模块一只提供10个接口,剩下的接口,模块一独自在内部处理不展示给模块二,那么模块二就只需要关注这10个接口即可,剩下的接口不管,这样模块一修改剩余的90个接口的时候,模块二不需要管,这两个模块之间的依赖性就弱,这就是两个模块间耦合度低的表现,这是比较好的。

所以,耦合度低点会更好。简单说一下高内聚,高内聚就是每个模块内部的联系要紧凑,关联性要强一点。耦合度是两个模块之间。

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

下面用代码介绍一下什么时候用is-a或has-a:

cpp 复制代码
//Tire(轮胎)和Car(车)更符合has-a的关系 
//只能说车有轮胎,不能说车是轮胎,所以这里用has-a,组合
class Tire
{
protected:
	string _brand = "xxx"; //品牌 
	size_t _size = 17;          //尺寸 
};

class Car
{
protected:
	string _colour = "黑色";  //颜色 
	string _num = "xxxxx"; //车牌号 
	Tire _t1;                 //轮胎1
	Tire _t2;                 //轮胎2
	Tire _t3;                 //轮胎3
	Tire _t4;                 //轮胎4
};

//Car和BMW/Benz更符合is-a的关系
//只能说BMW(宝马)是车,不能说BMW有车,所以这里用is-a,继承
class BMW : public Car
{
public:
	void Drive()
	{
		cout << "好开-操控" << endl;
	}
};
class Benz : public Car
{
public:
	void Drive()
	{
		cout << "好坐-舒适" << endl;
	}
};

/****************************************************/

template<class T>
class vector
{};

//stack和vector的关系,既符合is-a,也符合has-a。
//当既符合is-a也符合has-a,推荐用has-a,因为它的耦合度低

//is-a
template<class T>
class stack : public vector<T>
{};

//has-a
template<class T>
class stack
{
public:
	vector<T> _v;
};

十二、结语

本篇到这里就结束了,主要讲了继承这一面向对象特性的基本使用和相关问题,希望对大家有所帮助,祝生活愉快,我们下一篇再见!

相关推荐
染指11102 小时前
50.第二阶段x86游戏实战2-lua获取本地寻路,跨地图寻路和获取当前地图id
c++·windows·lua·游戏安全·反游戏外挂·游戏逆向·luastudio
Code out the future2 小时前
【C++——临时对象,const T&】
开发语言·c++
sam-zy2 小时前
MFC用List Control 和Picture控件实现界面切换效果
c++·mfc
aaasssdddd963 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
发呆小天才O.oᯅ3 小时前
YOLOv8目标检测——详细记录使用OpenCV的DNN模块进行推理部署C++实现
c++·图像处理·人工智能·opencv·yolo·目标检测·dnn
qincjun4 小时前
文件I/O操作:C++
开发语言·c++
星语心愿.4 小时前
D4——贪心练习
c++·算法·贪心算法
汉克老师4 小时前
2023年厦门市第30届小学生C++信息学竞赛复赛上机操作题(三、2023C. 太空旅行(travel))
开发语言·c++
single5944 小时前
【c++笔试强训】(第四十一篇)
java·c++·算法·深度优先·图论·牛客
yuanbenshidiaos4 小时前
C++-----函数与库
开发语言·c++·算法