【C++】继承

目录

前言

一.继承的概念

二.子类对象的存储模型

三.继承中的权限

四.继承中的切片

五.继承中的隐藏

1.普通成员变量构成隐藏

2.普通成员函数构成隐藏

3.静态成员构成隐藏

六.多继承

1.多继承的语法

2.多继承中子类对象的存储模型

3.多继承中的父类成员同名问题

4.多继承中的子类父类赋值转换

七.继承与友元

八.继承与静态成员

九.派生类中的默认成员函数

1.子类中的构造函数

2.子类中的拷贝构造

3.子类中的operator=

4.子类中的析构函数

5.多继承中的父子类对象的构造顺序和析构顺序

十.C++11新增关键字 - final

十一.菱形继承的缺陷以及如何解决

1.什么是菱形继承

2.用一个现实生活中的例子, 去很好的理解菱形继承以及菱形继承的缺陷

3.菱形继承的两大问题

4.虚继承解决菱形继承问题

5.虚继承中的子类父类赋值转换问题 

十二.继承和组合的对比

1.继承关系

2.组合关系

3.是继承关系, 也是组合关系

4.用继承? 还是用组合?


前言

面向对象语言的三大特性: 封装, 继承, 多态

在学习继承之前, 先简单阐述一下我个人对于封装的理解:

将一类事物描述为一个类, 类中包含该事物的方法和属性, 也就是可以将方法和属性合并到一起, 再通过类中权限控制, 从而达到"高内聚, 低耦合"的目的, 再到更深层次的理解, 容器适配器 - stack,queue,priority_queue, 底层都是封装的某个容器, 基于某个容器进行了进一步的封装, 迭代器适配器 - 反向迭代器(reverse_iterator), 同理, 底层封装的是正向迭代器, 本质也可以理解为基于某一种结构的复用

一.继承的概念

子类继承父类, 本质可以理解为是子类基于父类上的一层扩展, 是子类复用了父类, 继承的思想也是复用的思想

子类也可以称为派生类, 父类也可以称为基类, 是两种不同的叫法

学生, 老师, 员工, 都有Person的属性, 说白了他们都是人, 所以分别继承了Person类, 本质上是对于Person类的复用

继承是"is-a"的关系

学生 是 人, 老师 是 人, 职工 是 人

这一点在后面会和组合有区分, 在本篇文章的最后会详细说明, 继承与组合的区别

继承的语法

class Person
{
public:
	//...
protected:
	//...
};

class Student : public Person
{
public:
	//...
private:
	//...
};

二.子类对象的存储模型

类对象内部只有成员变量, 并不包含成员函数以及静态成员

1.对于子类实例化出的子类对象而言, 内部可以理解为是由两部分构成

子类对象 = 父类对象 + 子类成员

子类继承父类时, 并非是将父类拷贝到子类, 而更像是子类复用了父类, 或者说是共享了父类

在子类中可以直接使用父类的方法和成员

为什么呢? 是什么让他可以这么做的? (一些额外扩展)

在类外调用类中的方法或者成员, 对于成员函数实际是将函数作用域放开给子类并且调用方(子类对象)的地址传给this指针, 这时会对子类对象指针进行切片, 让一个子类对象指针(this)赋值给一个父类对象指针(this), 对于成员变量, 当子类对象在访问继承下来的父类成员变量时, 实际就是在访问自己的成员,  创建子类对象时, 会给子类从父类继承的成员变量开空间, 从而可以直接访问
 

在类内的成员函数中调用父类的方法或成员, 实际是通过this指针去调用, 例如我要在成员函数中调用一个父类的成员函数, 和上面同理, 也是对this进行了一个切片赋值, 从子类类型转换为父类类型, 从而达到可以调用父类的方法和成员, 并且如果是静态成员, 是不属于子类和父类的对象中的, 而是在全局区域中是属于父类的, 所以在底层父类的静态成员的域是对子类可见, 从而让子类也可以去访问父类的静态成员

 

2.对于子类继承的父类的成员函数而言, 父类的成员函数是在父类定义的, 在哪里定义它的作用域就在哪里, 所以子类继承下来的父类成员函数的作用域是父类域

对于成员函数, 子类和父类只有一份, 存在于代码区中, 父类通过给子类放开域的限制, 让子类可以访问到继承下来的成员函数

总的来说, 子类和父类共同使用成员函数, 每个成员函数只有一份存在于代码区, 且在父类作用域中对子类可见

三.继承中的权限

C++作为最早面向对象语言, 难免会踩到一些坑, 在继承权限设计上有一些复杂

在C++中, 有三种继承方式: 公有继承, 保护继承, 私有继承

子类中继承的父类部分的权限取决于两部分: 1.父类原有权限 2.子类继承方式

综合一下, 可以总结为: 最终权限选取父类原有权限和子类继承方式中最小的那个权限

如何理解这句话:

如果此时是公有继承, 那么父类是什么权限, 子类继承下来就是什么权限

如果此时是保护/私有继承, 那么选取最小的那个权限

举例:

如果父类是公有, 而子类是私有继承, 那么继承下来的权限就是私有

如果父类是公有, 而子类是保护继承, 那么继承下来的权限就是保护

但是绝大多数(99.9%)的情况都是使用的公有继承, 在JAVA中, 则只有公有继承, 没有保护/私有继承

当子类是公有继承时

父类public权限可以在子类使用, 可以在类外使用

父类protected权限可以在子类使用, 不可以在类外使用

父类private权限在子类中不可见, 且不可以在类外使用

关于父类private权限在子类中不可见, 那么是否可以被子类继承呢?

答案是: 子类会继承父类的private权限的成员, 只是在子类中不可见而已

这里对于不可见的理解: 通过子类对象无法访问该成员, 但是子类对象中却存储了这样的一个值, 所以对于子类对象的大小是有影响的

通过以下特殊手段, 可以将子类中继承下来的父类不可见的成员提取出来

 

注: 使用class关键字的默认继承方式是private, 使用struct关键字的默认继承方式是public, 但最好显式写出继承方式

四.继承中的切片

派生类对象可以赋值给基类对象(值,指针,引用都可以赋值), 而基类对象不可以赋值给派生类对象

派生类对象赋值给基类对象并不是一个类似于int a = 10; double b = a这样的隐式类型转换, 而是可以理解为派生类对象就是一个特殊的基类对象

赋值原理: 切片(底层原理会去调用一个函数, 这里暂时不谈)

在派生类对象赋值给基类对象时会发生切片, 在赋值的时候, 子类对象将自己的基类对象那一部分切出来后赋值给基类

基类对象不可以赋值给派生类

指向基类对象的指针(或者基类的引用), 不可以赋值给指向派生类对象的指针(或者派生类对象的引用), 虽然在vs下能编译通过(以强制类型转换的方式赋值), 但是会发生越界访问

代码

class Person
{
public:
	int a;
	int b;
};

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

int main()
{
	Student sobj;
	Person pobj = sobj;
	Person* pobj = &sobj;
	Person& pobj = sobj;//这里可以看出这并不是隐式类型转换
    //底层发生切片, 可以理解为子类对象就是一个特殊的父类对象
    //因为如果这是隐式类型转换的话,这里会报错,因为这是引用赋值且没有const
    //参考:
    //int a = 10;
    //const double& b = a;这才是隐式类型转换
 	return 0;
}

五.继承中的隐藏

在继承体系中, 父类和子类各自的作用域是相对独立的, 各自都有各自的作用域

如果子类和父类有同名成员或者同名成员函数, 处理方式为 --- 隐藏掉父类的同名成员函数或成员变量

隐藏并不是没有, 而是它还在只是不指定作用域时访问不到

1.普通成员变量构成隐藏

2.普通成员函数构成隐藏

函数名和参数都相同, 这样构成隐藏很容易理解

那么,以下子类和父类中的func成员函数是构成函数重载, 函数构成隐藏关系?

class Person
{
public:
	void func(int a) { cout << "Person :: func()" << endl; }
};

class Student : public Person
{
public:
	void func() { cout << "Student :: func()" << endl; }
};

答案: 这也是隐藏, 虽然看上去函数名相同, 参数不同, 很像函数重载, 但并不是!

因为子类和父类是两个独立的作用域, 而构成函数重载的条件之一是要在相同作用域下, 作用域不同, 谈何函数重载呢?

所以, func()依旧构成隐藏, 总结: 只要函数名相同, 就构成隐藏

进一步解释, 因为在编译器眼中, 如果子类对象调用func函数, 就是要调用子类中的func函数, 如果你传参不符, 就会报错

如果你指定作用域去调用父类的func函数, 就要按照父类的func函数的参数去进行传参

3.静态成员构成隐藏

以静态成员变量举例, 静态成员函数同理

六.多继承

1.多继承的语法

Student继承了Person1和Person2

class Person1
{
public:
    //..
};
class Person2
{
public:
    //..
};
class Student : public Person1,public Person2
{
public:
	//..
};

2.多继承中子类对象的存储模型

3.多继承中的父类成员同名问题

如图, 以成员变量举例, 这就直接出现了二义性的问题, 子类对象调用时, 不知道要调用Person1还是Person2, 所以需要我们手动声明作用域

正确的调用方式: sobj.Person1::a或者sobj.Person2::b

4.多继承中的子类父类赋值转换

多继承的类对象赋值转换, 与单继承是相同的, 只不过继承的父类不止一个

class Base1
{
public:
	int _num1 = 10;
};
class Base2
{
public:
	int _num2 = 20;
};

class Child : public Base1, public Base2
{
public:
	int _num3 = 30;
};

int main()
{
	Child c;
	Base1 b1 = c;
	Base2 b2 = c;
}

如果是指针赋值, 指针大小如何对比呢?

例如

class Base1
{
public:
	int _num1 = 10;
};
class Base2
{
public:
	int _num2 = 20;
};

class Child : public Base1, public Base2
{
public:
	int _num3 = 30;
};

int main()
{
	Child c;
	Base1* b1 = &c;
	Base2* b2 = &c;
}

 &c, b1, b2的大小关系又是如何呢? 直接看图

七.继承与友元

友元不能被继承

基类友元不可以访问子类的保护或者私有成员

(友元尽量少用, 因为它一定程度上破环了类的封装性)

class Student;
class Person
{
public:
    //友元函数, Display可以访问父类的保护或私有成员
	friend void Display(const Person& p, const Student& s);
protected:
	int _a = 10;
};

class Student :public Person
{
protected:
	int _b = 20;
};

void Display(const Person& p, const Student& s)
{
    //可以访问父类对象p中的保护或私有成员
	cout << p._a << endl;//输出10
    //可以访问子类对象s中的继承的父类的保护或私有成员
	cout << s._a << endl;//输出10
    //不可以访问子类对象s中属于子类对象自己的保护或私有成员
	cout << s._b << endl;//报错, 子类私有/保护成员类外无权限访问
}

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

八.继承与静态成员

需要明确的概念: 静态成员不在任何类内, 是存在于全局数据区的, 如果是父类的静态成员, 那么该静态成员的作用域就是父类的作用域

子类继承父类的静态成员, 与子类继承父类成员函数的形式非常像, 静态成员既不在父类对象内也不在子类对象内, 它是存在于全局数据区, 子类和父类中该静态成员只有一个, 该静态成员的作用域是父类的且对子类可见, 子类和父类可以共同使用这个静态成员, 无论创建多少个子类对象, 创建多少个父类对象, 该父类静态成员变量有且仅有一份

可以通过以下几种方式访问父类的静态成员变量

class Person
{
public:
	static int _aa;
};
int Person::_aa = 99;

class Student :public Person
{
public:
};

int main()
{
	Person p;
	Student s;
    //-------------------------
	p._aa;
	s.Person::_aa;
	s._aa;
	Person::_aa;
	Student::_aa;
    //-------------------------
    return 0;
}

一但在子类对象中修改了继承下来的父类的静态成员变量, 父类对象中的静态成员变量也会发生相应的改变

class Person
{
public:
	static int _aa;
};
int Person::_aa = 99;

class Student :public Person
{
public:
};

int main()
{
	Person p;
	Student s;

	cout << "修改_aa之前,通过父类对象访问父类静态成员变量: " << p._aa << endl;
	s._aa += 10;
	cout << "通过子类修改_aa之后,通过子类对象访问父类静态成员变量: " << s._aa << endl;

	return 0;
}

九.派生类中的默认成员函数

在C++ Primer书中提出: 子类中的默认成员函数属于一种合成版本

1.子类中的构造函数

规则

1.对于子类自己的成员, 与类和对象一样, 内置类型直接初始化, 自定义类型去调用他自己的构造函数

2.对于继承下来的父类成员, 要去调用父类的构造函数

首先第一个问题就是, 子类是如何初始化成员变量的呢? 以及调用顺序是什么?

在初始化子类对象时, 是先调用父类构造函数, 再调用子类构造函数

子类对象通过哪种方式去调用父类的构造函数呢?

子类在实例化对象时, 会去调用子类的构造函数, 在调用子类构造函数时, 会在子类构造函数的初始化列表处去调用父类的构造函数, 至于去调用父类的哪一种构造, 取决于你是否显式的在初始化列表去写, 如果没有写, 编译器默认去调用父类的默认构造函数(默认构造函数: 无参或者全部参数都带缺省值)

如何在初始化列表显式去写, 我要调用父类的哪一种构造

在初始化列表以类似"匿名对象的方式", 只是很像匿名对象, 但并不是, 只是语法规定

父类中实现的构造函数

Person(string name, int age)
    :_name(name), _age(age)
{
    cout << "Person(string name, int age)" << endl;
}

子类中写构造函数的方式: 在初始化列表处显式显出要调用的父类构造函数

Student(string name, int age, int id)
    :Person(name, age), _id(id)
{
    cout << "Student(string name, int age, int id)" << endl;
}

或者你想要调用其他的父类构造, 但前提是父类中要有对应的构造的实现!

class Person
{
public:
	Person()
	{
		cout << "Person()" << endl;
	}
	Person(string name, int age)
		:_name(name), _age(age)
	{
		cout << "Person(string name, int age)" << endl;
	}
protected:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student()//调用Person()
	{
		cout << "Student()" << endl;
	}
	Student(string name, int age, int id)
		:Person(name, age), _id(id)
	{
		cout << "Student(string name, int age, int id)" << endl;
	}
	//显式Student信息
	void Print()
	{
		cout << "-------------------------------------" << endl;
		cout << _name << ", " << _age << ", " << _id << endl;
	}
protected:
	int _id;//学号
};

int main()
{
	Student s("张三", 18, 100001);
	s.Print();
	return 0;
}

2.子类中的拷贝构造

规则

1.对于子类自己的成员, 与类和对象一样, 内置类型直接值拷贝, 自定义类型去调用他自己的拷贝构造

2.对于继承下来的父类成员, 要去调用父类的拷贝构造

编译器默认生成的子类拷贝构造, 会去隐式调用父类拷贝构造

如果遇到一些深拷贝问题需要我们自己写子类的拷贝构造

子类中的拷贝构造必须要去显式的调用父类的拷贝构造, 否则编译器会默认去调用无参构造, 会出现意外情况, 未初始化或者赋值错误等情况

显式调用父类拷贝构造的形式与上面所讲构造函数一致, 拷贝构造也是构造函数的一种

父类的拷贝构造需要一个父类对象, Person(Person& p), 我子类在显式去调用父类拷贝构造时, 怎么才可以传入一个父类对象呢?

切片的作用在这里有所体现, 直接传入子类对象, 底层会发生切片, 将子类对象赋值转换给父类对象p的引用, 也就是直接在初始化列表写Person(s)即可

class Person
{
public:
	Person()
	{
		//cout << "Person()" << endl;
	}
	Person(string name, int age)
		:_name(name), _age(age)
	{
		//cout << "Person(string name, int age)" << endl;
	}
	//-----------------------------------------------------
	//拷贝构造, 这个父类拷贝构造不写也是可以的, 编译器会默认生成
	Person(Person& p)
		:_name(p._name), _age(p._age)
	{
		cout << "Person(Person& p)" << endl;
	}
	//-----------------------------------------------------
protected:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student()//调用Person()
	{
		//cout << "Student()" << endl;
	}
	Student(string name, int age, int id)
		:Person(name, age), _id(id)
	{
		//cout << "Student(string name, int age, int id)" << endl;
	}

	//-----------------------------------------------------
	//拷贝构造
	Student(Student& s)
		:Person(s), _id(s._id)
	{
		cout << "Student(Student& s)" << endl;
	}
	//-----------------------------------------------------
	// 
	//显式Student信息
	void Print()
	{
		//cout << "-------------------------------------" << endl;
		cout << _name << ", " << _age << ", " << _id << endl;
	}
protected:
	int _id;//学号
};

int main()
{
	Student s("张三", 18, 100001);
	s.Print();

	Student s2(s);
	s2.Print();

	return 0;
}

3.子类中的operator=

规则

1.对于子类自己的成员, 与类和对象一样, 内置类型直接值赋值, 自定义类型去调用他自己的operator=

2.对于继承下来的父类成员, 要去调用父类的operator=

对于operator=而言, 先赋值父类还是先赋值子类没有强制规定, 但一般都是先赋值父类

Student& operator=(Student& s)
{
	if (this != &s)
	{
        //一定要这么写, 因为operator=与父类的operator=函数同名, 构成隐藏
		Person::operator=(s);
		_id = s._id;
	}
	return *this;
}

4.子类中的析构函数

规则

1.对于子类自己的成员, 与类和对象一样, 内置类型不处理, 自定义类型去调用他自己的析构

2.对于继承下来的父类成员, 要去调用父类的析构

析构是最特殊的, 体现在:

1.子类的析构函数和父类的析构函数构成隐藏, 需要指定作用域

但是~Student()和~Person()函数名不同, 为何会构成隐藏呢?

由于多态的需要, 析构函数的名字会统一处理成destructtor(), 就会构成隐藏了

class Person
{
public:
	Person()
	{
		//cout << "Person()" << endl;
	}
	Person(string name, int age)
		:_name(name), _age(age)
	{
		//cout << "Person(string name, int age)" << endl;
	}
	//-----------------------------------------------------
	//拷贝构造
	Person(Person& p)
		:_name(p._name), _age(p._age)
	{
		//cout << "Person(Person& p)" << endl;
	}
	//-----------------------------------------------------

	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student()//调用Person()
	{
		//cout << "Student()" << endl;
	}
	Student(string name, int age, int id)
		:Person(name, age), _id(id)
	{
		//cout << "Student(string name, int age, int id)" << endl;
	}

	//-----------------------------------------------------
	//拷贝构造
	Student(Student& s)
		:Person(s), _id(s._id)
	{
		//cout << "Student(Student& s)" << endl;
	}
	//-----------------------------------------------------
	// 
	Student& operator=(Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_id = s._id;
		}
		return *this;
	}

	//显式Student信息
	void Print()
	{
		//cout << "-------------------------------------" << endl;
		cout << _name << ", " << _age << ", " << _id << endl;
	}

	~Student()
	{
		Person::~Person();
		cout << "~Student()" << endl;
	}
protected:
	int _id;//学号
};

int main()
{
	Student s("张三", 18, 100001);
	//s.Print();

	Student s2(s);
	//s2.Print();

	Student s3;
	s3 = s2;
	//s3.Print();

	return 0;
}

可是, 我构造了3个子类对象, 也就是说需要调用3次子类析构, 3次父类析构, 但是为什么结果的父类析构次数却翻倍了!

2.子类对象的析构函数会自动调用父类的析构, 不能显式写

这么做的目的是为了保证析构顺序问题

如果可以显式写, 那么析构顺序就全乱了!

在C++类和对象中的一条规则: 先构造的后析构, 后构造的先析构

也就是先构造父类, 再构造子类, 先析构子类, 后析构父类

所以编译器会在~Student()结束之后, 自动调用~Person(), 不能显式写!

总结: 子类的析构只对子类成员做处理即可, 不能显式去调父类析构, 编译器会在子类析构结束后自动调用父类析构

5.多继承中的父子类对象的构造顺序和析构顺序

Student先继承Person1, 后继承Person2

我们在写Student构造函数时, 在初始化列表的地方先调用的Person2构造, 在调用Person1构造

打印出的结果是, 先构造父类, 再构造子类, 构造父类的顺序又是, 先构造Person1, 后构造Person2

也就是说, 对于父类构造的顺序是依据继承顺序的, 与初始化列表中的顺序无关

且在析构时, 析构顺序始终遵守: 先构造的后析构, 后构造的先析构

总结:

构造顺序先父后子, 且若是多继承父类构造顺序取决于继承顺序

析构顺序取决于构造顺序, 析构顺序是构造的逆序

class Person1
{
public:
	Person1()
	{
		cout << "Person1()" << endl;
	}
	Person1(string name, int age)
		:_name(name), _age(age)
	{
		cout << "Person1(string name, int age)" << endl;
	}
	~Person1()
	{
		cout << "~Person1()" << endl;
	}
protected:
	string _name;
	int _age;
};

class Person2
{
public:
	Person2()
	{
		cout << "Person2()" << endl;
	}
	Person2(string name, int age)
		:_name(name), _age(age)
	{
		cout << "Person2(string name, int age)" << endl;
	}
	~Person2()
	{
		cout << "~Person2()" << endl;
	}
protected:
	string _name;
	int _age;
};

class Student : public Person1, public Person2
{
public:
	Student()//调用Person()
	{
		cout << "Student()" << endl;
	}
	Student(string name, int age, int id)
		:Person2(name, age)
		,Person1(name, age)
		,_id(id)
	{
		cout << "Student(string name, int age, int id)" << endl;
	}

	~Student()
	{
        cout << "~Student()" << endl;
		//不需要显式调用父类析构, 编译器会自动处理
		//Person1::~Person1();
		//cout << "~Student()" << endl;
	}
protected:
	int _id;//学号
};

int main()
{
	Student s("张三", 18, 100001);
	return 0;
}

十.C++11新增关键字 - final

思考: 如何去定义一个不能被继承的类呢?

在C++98中, 我们可以通过将父类的构造函数私有, 这样子类在继承父类实例化对象, 调用父类构造时, 由于权限不够, 就无法构造, 这是一种间接达成目的的方法

但是这种方法不科学, 因为如果父类构造函数设为私有, 不仅仅是子类继承父类后, 无法实例化对象, 就连父类本身, 也无法实例化对象了!

所以, 在C++11中, 明确新增了一个关键字: final

只需要在父类后面加上final关键字, 父类既可以实例化对象, 子类又无法继承父类

这时直接就是编译器级别的报错, 直接明确禁止了A类不可以继承, 这是一种直接方式 

十一.菱形继承的缺陷以及如何解决

1.什么是菱形继承

菱形继承是一个非常复杂的问题, 只有支持多继承的语言才会有菱形继承问题

(来看以下两张图)

代码

典型的菱形继承 

class Base
{
public:

};
class Base2: public Base
{
public:

};
class Base3: public Base
{
public:

};
class Base4 : public Base2, public Base3
{
public:

};
int main()
{

	return 0;
}

2.用一个现实生活中的例子, 去很好的理解菱形继承以及菱形继承的缺陷

假设, 现在有一个Person类, 这个类抽象出人的信息: 姓名, 年龄...

有一个学生管理员类, 这个类抽象出学生管理员信息: 班级, 年级...

有一个图书管理员类, 这个类抽象出图书管理员信息: 书籍...

有一个管理员类, 这个类抽象出管理员信息: 管理的东西...

现在有这样一个场景, 有一个管理员, 首先他是一个人, 这个管理员既管理学生, 又管理图书

学生管理员类 和 图书管理员类 继承了 Person类

管理员类 继承了 学生管理员类 和 图书管理员类

此时创建一个管理员对象, 其中会存储两份Person类, 我们是不需要两份Person信息的, 因为一个人他有两个身份, 但是只有一个年龄, 一个名字(通常情况下)

这时就构成了数据冗余, 同时也有二义性的问题

这时使用虚继承去解决, 便可以解决了

3.菱形继承的两大问题

class Base
{
public:
	Base() { cout << "Base()" << endl; }
	Base(int b)
	{
		_b = b;
	}
	int _b = 10;
};
class Base2: public Base
{
public:
	Base2() { cout << "Base2()" << endl; }
	Base2(int b)
		:Base(b)
	{}
	int _b2 = 20;
};
class Base3: public Base
{
public:
	Base3() { cout << "Base3()" << endl; }
	Base3(int b)
		:Base(b)
	{}
	int _b3 = 30;
};
class Base4 : public Base2, public Base3
{
public:
	Base4() { cout << "Base4()" << endl; }
	Base4(int b1, int b2)
		:Base2(b1)
		,Base3(b2)
	{}
	int _b4 = 40;
};
int main()
{
	//Base4 b;
	//cout << &b.Base::_b << endl;	
	//cout << &b.Base2::_b << endl;
	//cout << &b.Base3::_b << endl;

	Base4 b2(19, 91);
	cout << b2.Base::_b << endl;
	cout << b2.Base2::_b << endl;
	cout << b2.Base3::_b << endl;
	cout << "---------------" << endl;
	cout << &b2.Base::_b << endl;
	cout << &b2.Base2::_b << endl;
	cout << &b2.Base3::_b << endl;
	return 0;
}

  

(1).二义性

Base4对象b在调用父类的父类中的_b时, 就会有二义性, 此时编译器不知道要调用Base2继承的Base还是Base3继承的Base

必须显式指定作用域去调用继承下来的Base的内容

如果指定Base2域或者Base3域那么结果就是Base2或Base3继承下来的Base成员变量值

如果指定Base域, 结果却和指定Base2域一致, 且地址也一样

若我们把继承顺序颠倒一下, 先继承Base3后继承Base2, 会发生什么呢?

这时如果指定Base域访问_b结果却和指定Base3域一致了 

导致这一结果的原因就是: 我继承顺序颠倒了, 先继承Base3, 也就是先构造Base3, 那么b4对象的存储模型Base3继承下来的成员变量就在b4对象的最低地址处

由此可以得出这样的结论: 如果指定Base域, 看似好像也会发生歧义? 因为指定Base域还是没有说明到底要Base2还是Base3的? 但其实没有, 编译器直接去找了最低地址处(也就是最先构造的那一部分的地址)并且将其继承的Base成员变量找到了

(2).数据冗余

如果Base2和Base3都继承了Base, 且其中继承的Base成员变量相同, 那就是继承了两份相同的Base成员变量, 这就会造成数据冗余, 导致严重的空间浪费

其次, 构造函数要调用两次, 拷贝构造要调用两次, 也会造成资源浪费

4.虚继承解决菱形继承问题

虽然指定作用域可以解决二义性问题, 但是数据冗余仍然是一个很严重的问题

为了解决菱形继承问题, C++便有了虚继承 - 关键字"virtual"

(注: 这里的virtual与多态中的虚函数virtual关键字是两个概念, 只是用了同一个关键字)

class Base
{
public:

};
class Base2: virtual public Base
{
public:

};
class Base3: virtual public Base
{
public:

};
class Base4 : public Base2, public Base3
{
public:

};
int main()
{

	return 0;
}

5.虚继承中的子类父类赋值转换问题 

虚继承解决菱形继承问题也付出一定的代价, 但是在如今的计算机上这些代价几乎可以忽略不计了

代价: 子类访问父类数据时, 中间需要多一步计算, 那就是通过指针找到指向的空间, 再将空间中的值取出, 根据这个偏移量去找到真实数据

同时需要注意的是, 32位下虚继承的存储多个一个4字节指针

十二.继承和组合的对比

1.继承关系

继承更像是一个"is a"的关系, 例如: 学生是人

具体表现形式:

class A
{
    //...
};
class B : public A
{
    //...
};

2.组合关系

组合更像是一个"has a"的关系, 例如: 汽车有轮胎

具体表现形式:

class A
{
    //...
};
class B
{
    //...
    A _a;
};

3.是继承关系, 也是组合关系

有没有继承和组合都适用的关系? 也就是既是"is a" 又是 "has a"的关系

在STL中, stack, queue(容器适配器)就是这样的一个关系

stack, queue既是特殊的deque等容器, 而他的底层实现又有deque这样的容器

在stack, queue中使用的是组合的方式

4.用继承? 还是用组合?

继承和组合的本质都是复用

继承是一种"白箱复用", 子类继承父类, 对于子类而言, 父类的实现接口是完全暴露给子类的, 一定程度上的破坏了父类的封装性, 父类的改变对子类有很大影响, 耦合度高

组合是一种"黑箱复用", A类仅仅只需要给B类使用B类所需要的接口, 对于A类的封装性上有一定的保护, 耦合度低

如果在实际使用中, 可以用继承也可以用组合则推荐使用组合, 保证每个类尽可能被"封装"

有些关系更适合继承, 就使用继承, 例如: 多态, 就需要使用继承这种关系来完成重写, 从而达到多态的目的

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值