C++ —— 继承

目录

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.1 隐藏规则](#3.1 隐藏规则)

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

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

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

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

5.继承和友元

6.继承和静态成员

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

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

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

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

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

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


1.继承的概念和定义

1.1 继承的概念

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

下面我们看到没有继承之前我们设计的两个类:Sutudent和Teacher,Sutudent和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 = "peter";   //姓名
	string _address;		  //地址
	string _tel;			  //电话
	int _age = 18;   		  //年龄

	string _title;		      //职称

};

可以发现上面的两个类之间是有许多重复的成员变量的还有一个重复的成员函数。将公共的成员都放在Person类中,然而让Teacher和Student都继承Person,就可以复用这些成员,就不要重复的定义了。

cpp 复制代码
//使用继承
class Person 
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "lisi";   //姓名
	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 继承基类成员访问方式的变化:

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

  • 基类private成员在派生类中无论以什么方式继承都是不可见的。 这里的不可见是 指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
cpp 复制代码
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name;
private:
	int _age;
};
class Student : public Person
{
public:
	void func()
	{	//不可见
		cout << _age << endl; //基类private成员在派生类中无论以什么方式继承都是不可见的。所以会报错
	}
protected:
	int _stunum;//学号
};

int main()
{
	return 0;
}

但是确实是被继承下来了,可以通过监视的窗口看见:

  • 基类private成员在派生类中是不能被访问(限制于直接访问,但是可以间接使用) ,如果**基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。**可以看出保护成员限定符是因继承才出现的。(没有继承这个概念,保护和私有是一样的。有了继承的概念,二者才有了区别)
cpp 复制代码
//一定使用不到父类的私有吗?不是的,我们可以间接的使用
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
		cout << _age << endl; //间接的使用
	}
protected:
	string _name="张三";
private:
	int _age=18;
};
class Student : public Person
{
public:
	void func()
	{	//不可见
		//cout << _age << endl; //基类private成员在派生类中无论以什么方式继承都是不可见的。所以会报错
		Print();  //间接的使用
	}
protected:
	int _stunum=1;//学号
};

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

运行结果:

  • 实际上上面的表格,进行一下总结就会发现:基类的私有成员在派生类都是不可见的。基类的其他成员在派生类的访问方式==Min(成员在基类的访问限定符,继承方式),public > protected >private。
  • 使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是public,不过最好显示的写出继承方式。
  • 在实际运用中一般使用的都是public继承,几乎很少用 protected/private继承,也不提倡使用 protected/private继承,因为protected/private继承下来的成员都只能在派生类的类里里面使用。实际中扩展维护性不强。

最常用的组合就是:

基类的publi成员和protected成员通过public方式继承

1.3 继承类模板

可以用继承的方式来实现栈,就是一个类模板的继承:

cpp 复制代码
namespace ysy
{
	template<class T>
	class stack :public std::vector<T>
		//                   vector<T>没有实例化  
	{
	public:
		void push(const T& x)
		{
			vector<T>::push_back(x);  // 直接调用push_back的话是找不到的,所以要加上vector<T>::(进行指定)
    
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};
}

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

运行结果:

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

  • public继承的派生类(子类)对象 可以赋值给 基类(父类)的指针/基类(父类)的引用 。这里有个形象的说法叫切片或者切割。寓意把派生类(子类)中基类(父类)的那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
cpp 复制代码
class Person
{
public:
protected:
	string _name = "张三";

};
struct Student :public Person
{
public:
protected:
	int _stunum = 1;//学号
};

int main()
{
	Student s;
	Person* ptr = &s;  //按道理是需要强转的,但是赋值兼容转换,就不需要了
	Person& ref = s;   //引用的不是临时对象,引用的是派生类中切出来的父类的那一部分,可以理解为并没有产生临时对象或者是一种特殊处理

	//上面的引用是比较明显的,举一个例子:
	int i = 1;
	const double& rd = i;  //i赋值给double会产生临时对象,临时对象是具有常性的,所以要加上const


	return 0;
}
  • 基类对象不能赋值给派生类对象。(基类里面的内容相对于派生类里面的内容就会少一点)
  • 基类的指针 或者引用可以通过强制类型转换赋值给赋值给派生类的指针或者引用。**(基类有可能指向基类有可能指向派生类)**但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast(dynamic_cast 会去识别基类指向基类返回空指针,基类指向派生类对象允许转换) 来进⾏识别后进⾏安全转换。(后面会提到)
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;
}

3.继承中的作用域

C++中有几个域:

**全局域:**在main函数之外的地方叫做全局域

**局部域:**由花括号括起来的大多都是局部域,函数里面有循环,也是一个局部域

命名空间域:不管生命周期,他是做名字的隔离

**类域:**跟命名空间域有点像,是一个定义出新的类型,命名空间域不是定义出新的类型,仅仅是将这里面定义出来的进行隔离。而类域不一样,他定义了一个类,同时进行名字隔离,类和类之间同名的成员同名的函数就不会发生冲突。

3.1 隐藏规则

  • 在继承体系中基类和派生类都有独立的作用域。
  • 派生类和基类的中有同名成员 ,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用 基类::基类成员 显示访问)
cpp 复制代码
class Person
{
public:
protected:
	string _name = "张三";
	int _id = 11;

};
struct Student :public Person
{
public:
	void func()
	{
		cout << Person::_id << endl;  //指定作用域,访问的就是父类的_id
		cout << _id << endl;
	}
protected:
	int _stunum = 1;//学号
	int _id = 22;
};

int main()
{
	Student s;
	s.func(); //派生类和基类的中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,
			  //但是也是可以对基类同名成员的访问,通过指定一下作用域也是可以的

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

解析:

直接指定才能通过:

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

4.1 4个常见默认成员函数

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

派生类(一部分是基类的,一部分是自己的):

1.我们不写默认生成的行为是什么

2.需要显式写,怎么写

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类的构造函数的初始化列表阶段显示调用。

派生类要生成默认构造是要要求基类要有默认构造的,否则生成不了

cpp 复制代码
class Person
{
public:
	//万一这里没有默认构造
	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:
protected:
	int _num;  //学号
};

int main()

{
	Student s1;

	return 0;
}

报错:

想要显示的初始化,或者没有默认构造,此时就要显示的写默认构造函数了:

cpp 复制代码
class Person
{
public:
	//Person(const char* name = "peter")
	//万一这里没有默认构造
	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:
	//报错
	//Student(const char* name,int num)
	//	:_name(name)
	//	,_num(num)
	//{ }

	Student(const char* name, int num)
		:Person(name)  //初始化父类那一部分
		, _num(num)
	{
	}
protected:
	int _num;  //学号
};

int main()

{
	Student s1("jack",18);

	return 0;
}
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
cpp 复制代码
int main()

{
	Student s1("jack",18);
	Student s2(s1); //拷贝构造,不写,编译器会生成默认的拷贝构造,默认的拷贝构造对内置类型会完成值拷贝
	                //对于自定义类型会调用它的拷贝构造,不能将父类看成一个一个的个体,应该看成一个整体
	                //对象,作为一个自定义类型的一个整体的成员去看待

	return 0;
}

一般拷贝构造,父类的拷贝构造怎么调用的,它就怎么拷贝,内置类型完成值拷贝。一般情况下,拷贝构造自己生成就可以了。构造还是要我们自己写。但是,当我们有指针的话,指向了一个数组,涉及深拷贝,此时就要自己写拷贝构造了

cpp 复制代码
	Student(const Student& s)
		//拷贝构造需要传父类对象,这里没有父类对象,将s传过去就搞定了
		:Person(s) //传子类对象给给父类的引用或者指针,会进行切割或者是切片把子类中的父类那部分切出来,引用的是子类当中父类的那一部分
		,_num(s._num)
	{
		//深拷贝
		//
	}
  • 派生类的operator=必须要调用基类的operator=完成基类的赋值。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类operator=,需要指定基类作用域

赋值重载也是拷贝,拷贝构造也是拷贝,只是说赋值重载用于两个已经存在的对象之间的拷贝,对于内置类型,完成值拷贝,对于自定义类型调用它的赋值重载,对于父类去调用父类的赋值重载

假设有深拷贝,就要我们自己写

cpp 复制代码
Student& operator=(const Student& s)
{
	//深拷贝 需要自己写,否则默认生成的就足够了
	if (this != &s)
	{
		Person::operator = (s);  //调用父类的,走切割切片,调用父类的得指定父类的,否则构成隐藏,调用不到
		_num = s._num;
	}

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

析构同样也是,默认不写对内置类型不做处理,对自定义类型调用它的析构,但是要注意子类中父类成员变量当成整体对象,作为子类自定义类型成员看待,所以就要调用Person类的析构

此时就要显示的写析构了:

但是这里为什么会报错呢?(编译器的报错原因也不能完全的相信)

报错是因为派生类的析构和父类的析构构成隐藏,但是两个析构函数名又不一样,为什么呢?

是因为让析构完成重写构成一个多态,父类子类析构函数不构成多态的话,又会有内存泄漏的问题,所以多态就要求函数名相同,编译器统一都处理为destructor(),所以就构成了隐藏的关系,所以得指定类域

运行结果:

怎么析构会调用两次呢?这个地方是不用调用的

析构这个部分是会自动调用的,会在派生类析构结束的时候自动调用父类的析构,这个是因为:构造要保持先父后子(先定义的先初始化),析构是先定义的后析构,先子后父,派生类先析构,再调用基类的。

如果按照上面写的显式调用的话,就会变为先父后子,但是析构要求是先子后父,所以就不用自己写析构函数。

  • 派生类对象初始化先调用基类构造再调派生类构造。
  • 派生类对象析构清理先调用派生类析构再调基类的析构。
  • 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(后面会提到)。那么编译器会对析构函数名机型特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

总结:

派生类和之前普通的类是差不多的,就是唯一多了父类继承的那一部分,把父类继承的那一部分当成一个整体的对象。

  1. 不论是显式写还是默认的函数的调用都是要调用父类的那一部分,调用拷贝构造的时候就像匿名对象一样,并且这里还有切片;
  2. 赋值重载显式调用那里有一个隐藏需要指定类域;
  3. 析构那里要保证先子后父,并且会在派生类调用析构之后自动调用父类的析构,所以不需要显式调用。
  4. 构造一般需要自己写,析构、拷贝构造和赋值就只有深拷贝的场景需要自己写,否则默认生成的就够了

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

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

cpp 复制代码
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()
{

	Derive d;
	return 0;
}

方法二:C++11新增了一个final关键字,final修改基类,派生类就不能继承了。(不生成对象也会报错)

cpp 复制代码
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()
{

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

报错:

解决方法:Display也变成Student 的友元即可

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;   //派生类里面的父类
	cout << &p._name << endl;
	cout << &s._name << endl;


	return 0;
}

静态成员是会被继承下来的,但是Student中的_count 和 Person 中的 _name是同一个吗?------不是的!

运行结果:

_count 是同一个 _count 吗?------是同一个_count,只是受类域的限制

cpp 复制代码
int main()
{
	Person p;
	Student s;

	//_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。

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:
	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.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

加上virtual的解决方法大概就是Person就不放在Student和Teacher里面了,合成一份放在开始或者是最后(看编译器),此时就没有数据冗余和二义性。

有菱形继承就要用虚继承。

尽量不要去用菱形继承,会很麻烦:

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是"张三", "李四", "王五"中的哪⼀个?
	Assistant a("张三", "李四", "王五");  // "王五" ,编译的时候只会走Person里面的构造,不会走Student和Teacher里面对Person的构造
	return 0;
}

初始化的顺序是先声明的先初始化,先声明的是父类,父类有多个,先继承的先初始化,

只走Person里面的name,不走Student和Teacher里面对应的Person的构造,虽然看似是解决了数据冗余和二义性,但是会很混淆

不走Student和Teacher里面对应的Person的构造的话是不是可以不用写,但是在Student和Teacher里面不写Person的话,编译会不通过,因为有可能Teacher会单独定义一个类,例如:Teacher t("xxxxx",1); 所以还是有意义的。

7.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

改变一下题目的话:

class Base1 { public: int _b1 = 1; };
class Base2 { public: int _b2 = 2; };
class Derive : public Base2, public Base1 { public: int _d = 3; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}

解析:

先继承的是Base2,Base2就在前面,谁先被继承,谁就先声明

2就在前面,因为先继承的是base2,再是bese1

所以是:p1 != p2 == p3

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)也适合用组合(has-a),就用组合。
相关推荐
佛系豪豪吖2 小时前
OpenClaw(龙虾)彻底卸载教程|Windows+Mac通用,3步无残留
开发语言
AMoon丶2 小时前
C++基础-类、对象
java·linux·服务器·c语言·开发语言·jvm·c++
为搬砖记录2 小时前
杰理AC695N soundbox 3.1.2打开ble宏的编译bug
c语言·开发语言·单片机·bug
17(无规则自律)2 小时前
Leetcode第二题:用 C++ 解决字母异位词分组
c++·leetcode·哈希算法
样例过了就是过了2 小时前
LeetCode热题100 子集
数据结构·c++·算法·leetcode·dfs
y = xⁿ2 小时前
【Java八股锁机制的认识】synchronized和reentrantlock区分,锁升级机制
java·开发语言·后端
Fruit_Caller2 小时前
GmSSL 编译与 Qt 项目集成问题排查记录(-lssl-1_1-x64 -lcrypto-1_1-x64)
开发语言·qt
free-elcmacom2 小时前
C++三种参数传递方式:从交换函数看值、指针与引用的区别
开发语言·c++
bubiyoushang8882 小时前
基于PSO的列车速度优化MATLAB实现
开发语言·人工智能·matlab