C/C++ (3. 类与对象)

柏拉图说: 如果不幸福, 不快乐, 那就放手吧.
人生最遗憾的莫过于, 轻易地放弃了不改放弃的, 固执地坚持了不该坚持的.

目录

3. 类与对象

3.1 面向对象程序设计方法概述

3.1.1 什么是面向对象的程序设计
  • (1)它以类或对象作为组织代码的基本单元,并将抽象、继承、封装、多态这四个特性,作为代码设计和实现的基石。
  • (2)C语言是面向过程的,关注的是过程,分析求解问题的步骤,通过函数调用逐步解决问题。
  • (3)C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
  • (4)任何一个对象都应该具备两个要素,即属性(attribute)和行为(behavior),对象由一组属性和一组行为构成的。
3.1.2 面向对象编程语言(OOPL)Object Oriented Programming Language
  • 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
3.1.3 面向对象的发展史
  • 类和对象这两个概念最早出现在1960年,在Simula这种编程语言中第一次使用。
  • 面向对象编程这个概念第一次被使用是在Smalltalk这种编程语言中,Smalltalk被认为是第一个真正意义上的面向对象编程语言。
  • 1980年左右,C++的出现,带动了面向对象编程的流行,也使得面向对象编程被越来越多地人认可。

一般来讲, 面向对象编程都是通过使用面向对象编程语言来进行的,但是,不用面向对象编程语言,我们照样可以进行面向对象编程。反过来讲,即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。

3.1.4 如何判定某编程语言是否是面向对象编程语言?
  • 实际上,我觉得只要某种编程语言支持类或者对象的语法概念,并且以此作为组织代码的基本单元,那就可以被粗略地认为它就是面向对象编程语言了。
  • 至于是否有现成的语法机制,完全地支持了面向对象编程的四大特性、是否对四大特性有取舍和优化,可以不作为判定的标准。
  • 按照严格的定义,很多语言都不能算得上面向对象编程语言,但按照不严格的定义来讲,现在流行的大部分编程语言都是面向对象编程语言。
3.1.3 面向对象的软件开发
  • 面向对象分析(Objecet Reiented Analysis,简称OOA)
  • 面向对象设计(Objecet Reiented Design,简称OOD)
  • 面向对象编程(Objecet Reiented Programming,简称OOP)
  • 面向对象测试(Objecet Reiented Test,简称OOT)
  • 面向对象维护(Objecet Reiented Soft Maintenance,简称OOSM)

3.2 类的认识

3.2.1 类的初步认识

在这里插入图片描述

3.2.2 类的引入
struct Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
 }
 
	void PrintStudentInfo()
	{
		cout<<_name<<" "<<_gender<<" "<<_age<<endl;
	}
 
	 char _name[20];
	 char _gender[3];
	 int _age;
};

int main()
{
	Student s;
	s.SetStudentInfo("Peter", "男", 18);
	return 0;
}
  • C语言中,结构体中只能定义变量,在C++ 中,结构体不仅可以定义变量,也可以定义函数。
  • C++更喜欢使用 class 来代替 struct。
3.2.3 类的定义声明和对象的定义
3.2.3.1 类是对象的抽象,而对象时类的具体事例。
3.2.3.2 声明类类型
class classname
{
	// 类体:由成员函数和成员变量组成
	...
}
  • class 为定义类的关键字,ClassName 为类的名字,{}中为类的主体。
  • 类中的元素称为类的成员:类中的数据称为类的属性或者成员变量;类中的函数称为类的方法或者成员函数;
3.2.3.1 类的两种定义方式
  • 1.声明和定义全部放在类体中,注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
  • 2.声明放在 .h 文件中,类的定义放在 .cpp文件中。
3.2.4 类的访问限定符
3.2.4.1 访问限定符说明
  • 1.public 修饰的成员在类外可以直接被访问;
  • 2.protected 和 private 修饰的成员在类外不能直接访问;
  • 3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止;
  • 4.class 的默认访问权限为 private,struct 的默认访问权限位 public(struct 要兼容C);
  • 注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
3.2.5 类的作用域
  • 类定义了一个新的作用域,类的所有成员都在类的作用域中。
  • 在类外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};

// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout<<_name<<" "<<_gender<<" "<<_age<<endl;
}
3.2.6 类的实例化
3.2.6.1 定义
  • 用类类型创建对象的过程,称为类的实例化。
3.2.6.2 说明
  • 1.类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
  • 2.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
3.2.7 结构体内存对齐规则
  • 1.第一个成员在与结构体偏移量为 0 的地址处;
  • 2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值,VS 中默认的对齐数为 8,gcc 中默认的对齐数为4;
  • 3.结构体的总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍;
  • 4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
3.2.8 this指针
3.2.8.1 定义
  • C++编译器给每个“成员函数“增加了一个隐藏的指针参数,让该指 针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
3.2.8.2 特性
  • 1.this指针的类型:类类型* const;
  • 2.只能在成员函数内部使用;
  • 3.this 指针本质上其实是一个成员函数的形参,是对象调用该成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存在 this 指针;
  • 4.this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。

3.2 类的6个默认的成员函数

3.2.1 构造函数(constructor)
3.2.1.1 定义
  • C++提供了构造函数来处理对象的初始化,构造函数是一种特殊的成员函数,名字必须与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
3.2.1.2 特性
  • 构造函数是特殊的成员函数,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
  • 1.函数名与类名相同;
  • 2.无返回值;
  • 3.对象实例化时编译器自动调用对应的构造函数;
  • 4.构造函数可以重载;
  • 5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成;
  • 6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数,我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
  • 7.C++ 把类型分成内置类型(基本类型)和自定义类型;内置类型是语法已经定义好的类型:如 int/char…,自定义类型就是我们使用 class/struct/union 自己定义的类型,编译器生成默认的构造函数会对自定类型成员调用的它的默认成员函数;
3.2.1.3 构造函数的表达形式

(1)构造函数首部的一般形式

  • 构造函数名(类型1 形参1,类型2,形参2,…)

(2)定义对象的一般形式

  • 类名 对象名(实参1,实参2,…)

(3)带有参数初始化表的构造函数的一般形式

类名::构造函数名([参数表][:成员初始化表]
{
	[构造函数体]
}
3.2.1.4 构造函数体赋值
  • 1.在创建对象时,编译器通过调用构造函数,给对象中的每个成员变量一个合适的初始值;
  • 2.构造函数体中的语句只能将其作为赋初值,而不能称作初始化;因为初始化只能初始化一次,而构造函数体内可以多次赋值。
3.2.1.5 初始化列表

定义

  • 1.以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟着一个放在括号中的初始值或表达式。

注意

  • 1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次);
  • 2.类中包含以下成员,必须放在初始化列表位置进行初始化(引用成员变量;const 成员变量;类类型成员);
  • 3.尽量使用初始化列表初始化;
  • 4.成员变量在类中的声明次序就是初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
3.2.2 拷贝构造函数
3.2.2.1 定义
  • 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
3.2.2.2 特性
  • 1.拷贝构造函数时特殊的成员函数;
  • 2.拷贝构造函数时构造函数的一个重载形式;
  • 3.拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用;
  • 4.若未显示定义,系统生成默认的拷贝构造函数;默认的拷贝构造函数对象按字节序完成拷贝,这种拷贝方式我们叫做浅拷贝,或者值拷贝。
3.2.3 析构函数
3.2.3.1 定义
  • 与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的;而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
3.2.3.2 特性
  • 1.析构函数时特殊的成员函数;
  • 2.析构函数名是在类名前加上字符 ~;
  • 3.无参数无返回值;
  • 4.一个类有且只有一个析构函数;若未显示定义,系统会自动生成默认的析构函数;
  • 5.对象生命周期结束时,C++编译系统自动调用析构函数;
  • 6.关于编译器自动生成的析构函数,会对自定义类型成员调用它的析构函数。
3.2.3.3 调用构造函数和析构函数的顺序
  • 先构造的后析构,后构造的先析构,相当于一个栈,先进后出。
3.2.4 赋值运算符重载
3.2.4.1 定义
  • C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类 型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
  • 函数名字:关键字 operator 后面按需要重载的运算符符号;
  • 函数原型:返回值类型 operator操作符(参数列表);
3.2.4.2 注意
  • 1.不能通过连接其他符号来创建新的操作符:比如operator@ ;
  • 2.重载操作符必须有一个类类型或者枚举类型的操作数;
  • 3.用于内置类型的操作符其含义不能改变,例如:内置的整型+,不能改变其含义 ;
  • 4.作为类成员的重载函数时,其形参看起来比操作数目少1成员函数的操作符有一个默认的形参 this,限定为第一个形参;
  • 5.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
  • 6.检测自己是否给自己赋值;
  • 7.返回 *this 指针;
  • 8.一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
3.2.5 取地址操作符重载
3.2.6 const取地址操作符重载
3.2.6.1 说明
  • 这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
3.2.6.2 代码
class Date
{ 
public :
	Date* operator&()
	{
		return this ;
	}	

	const Date* operator&()const
	{
		return this ;
	}
private :
	int _year ; // 年
	int _month ; // 月
	int _day ; // 日
}

3.3 友元

  • 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
  • 在 C++ 中,关系以关键字 friend(友元)声明,友元可以访问与其好友关系的类的私有成员。
3.3.1 友元类
3.3.1.1 定义
  • 将一个类(例如 B 类)声明为另一个类(例如 A 类)的朋友,这时 B 类就是 A 类的友元类,友元类 B 中所有的函数就是 A 类的友元函数,可以访问 A 类中的所有成员。
3.3.1.2 声明友元类的一般形式
  • friend 类名;
3.3.1.3 说明
  • 1.友元的关系是单向的而不是双向的,如果声明了 B 类是 A 类的友元类,不等于 A 类是 B 类的友元类,A 类中的成员函数不能访问 B 类中的私有成员;
  • 2.友元的关系不能传递,如果 B 类是 A 类的友元类,C 类是 B 类的友元类,不等于 C 类是 A 类的友元类,如果想让 C 类是 A类的友元类,应该在 A 类中另外说明。
3.3.1.3
3.3.2 友元函数
3.3.2.1 定义
  • 如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,有可以是其他类的成员函数),在类体中用 friend 对其进行声明,此函数被称为本类的友元函数。
3.3.2.2 说明
  • 1.友元函数可以访问类的私有成员,但不是类的成员函数;
  • 2.友元函数不能用 const 修饰;
  • 3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
  • 4.一个函数可以是多个类的友元函数;
  • 5.友元函数的调用与普通函数的调用和原理相同。

3.4 封装(Encapsulation)

3.4.1 封装的定义
  • 封装也叫叫做信息隐藏或者数据访问保护。
  • 类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
3.4.2 封装的意义是什么?它能解决什么问题?
  • 1.限制类的属性,灵活且可控(如果我们对类中的属性不做限制,那任何代码都可以访问、修改类中的属性,虽然再这样看起来更加灵活,但是过度灵活意味着不可控制,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性)。
  • 2.可以调高类的易用性(类仅仅通过有限的方法暴露必要的操作,如果我们把类的属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务有足够的了解,而这对于调用者来说是一种负担。相反,如果我们将类的属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多)。
  • 3.这就好比,如果一个冰箱有很多按钮,你就要研究很长时间,还不一定能操作正确。相反,如果只有几个必要的按钮,比如开、停、调节温度,一眼就能知道该如何来操作,而且操作出错的概率也会降低很多。

3.5 抽象(Abstraction)

3.5.1 抽象的定义
  • 抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
3.5.2 抽象的意义是什么?它能解决什么问题?
  • 从另外一个层面考虑的话,抽象及前面讲到的封装都是人类处理复杂性的有效手段,在处理复杂系统的时候,人脑承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节,抽象作为一种只关注功能点而不关注实现的设计思路。
  • 抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用。

3.6 继承(Inheritance)

3.6.1 继承的定义

例子: 诺基亚手机—>半智能手机—>智能手机

  • 继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。
  • 从继承关系上讲,继承可以分为单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,如果猫是哺乳动物,也是爬行动物。
  • 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类
3.6.2 继承的代码示意
#include <iostream>
using namespace std;
#include <string>

//基类 || 父类
class Person{
public:
	void SetPerson(const string& name, const string& gender, int age, double money){
		_name = name;
		_gender = gender;
		_age = age;
		_money = money;
	}
public:
	string _name;
	string _gender;
protected:
	int _age;
private:
	double _money;
};

//派生类 || 子类
class Student :public Person{
public:
	void SetStudent(int stuID){
		_stuID = stuID;
	}
	void Study(){
		cout << "努力学习!" << endl;
	}	
private:
	int _stuID;
};

int main(){
	cout << sizeof(Student) << endl;
	Student s;
	s.SetPerson("小白", "男", 25, 8000.00);
	s.SetStudent(201493144175);
	s.Study();
	return 0;
}
3.6.3 继承的定义方式
3.6.3.1 定义格式

继承定义格式

3.6.3.2 继承关系和访问限定符

在这里插入图片描述

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

在这里插入图片描述

// 派生类/子类---一定要对基类进行扩展
// public继承方式:基类中public和protected访问权限修饰的成员在子类中的权限不变
//               基类中private修饰的成员在子类中不能直接访问 || 不可见---已经被继承到子类中
class Derived : public Base
{
public:
	void SetDerived(int pubD, int proD, int priD)
	{
		_pub = pubD;

		// 基类中protected修饰的成员,在子类中的权限:protected
		_pro = proD;

		// public继承方式,基类中private成员在派生类中不能直接访问---不可见
		//_pri = priD;
	}

public:
	int _pubD;
protected:
	int _proD;
private:
	int _priD;
};

class D : public Derived
{
public:
	void TestFunc()
	{
		_pro = 10;
	}
};


int main()
{
	cout << sizeof(Derived) << endl;

	Derived d;
	d._pub = 10;
	//d._pro = 20;
	return 0;
}
#endif

#if 0
// public
// 基类/父类
class Base
{
public:
	void SetBase(int pub, int pro, int pri)
	{
		_pub = pub;
		_pro = pro;
		_pri = pri;
	}

	void PrintBase()
	{
		cout << _pub << " " << _pro << " " << _pri << endl;
	}

public:
	int _pub;
protected:
	int _pro;
private:
	int _pri;
};


// 派生类/子类---一定要对基类进行扩展
// protected继承方式:基类中被public修饰的成员在子类中的权限降为protected
//                  基类中被protected修饰的成员在子类中的权限不变
//                  基类中private修饰的成员在子类中不能直接访问 || 不可见---已经被继承到子类中
class Derived : protected Base
{
public:
	void SetDerived(int pubD, int proD, int priD)
	{
		_pub = pubD;
		_pro = proD;

		//_pri = priD;
	}
};

class D : public Derived
{
public:
	void TestFunc()
	{
		_pro = 10;
		_pub = 20;
	}
};


int main()
{
	Derived d;
	//d._pub = 10;
	return 0;
}
#endif

#if 0
class Base
{
public:
	void SetBase(int pub, int pro, int pri)
	{
		_pub = pub;
		_pro = pro;
		_pri = pri;
	}

	void PrintBase()
	{
		cout << _pub << " " << _pro << " " << _pri << endl;
	}

public:
	int _pub;
protected:
	int _pro;
private:
	int _pri;
};


// 派生类/子类---一定要对基类进行扩展
// private继承方式:基类中被public和protected修饰的成员在子类中的权限降为private
//                  基类中private修饰的成员在子类中不能直接访问 || 不可见---已经被继承到子类中
class Derived : private Base
{
public:
	void SetDerived(int pubD, int proD, int priD)
	{
		_pub = pubD;
		_pro = proD;

		//_pri = priD;
	}
};

class D : public Derived
{
public:
	void TestFunc()
	{
		//_pub = 10;
		//_pro = 10;
	}
};
#endif
3.6.3.4 总结
  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
3.6.4 基类和派生类对象赋值转换 (赋值兼容规则)

前提条件: 公有的继承方式

3.6.4.1 特性
  • 1.派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去;(孩子像父亲)
  • 2.基类对象不能赋值给派生类对象;
  • 3.基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
3.5.4.2 对象模型及代码示意

在这里插入图片描述

class Base
{
public:
	void SetBase(int b)
	{
		_b = b;
	}

	void PrintBase()
	{
		cout << _b << endl;
	}

protected:
	int _b;
};


/*
public继承方式: 基类中成员在派生类中访问权限不会发生改变
*/
struct Derived : public Base
{
public:
	void SetDerived(int b, int d)
	{
		_b = b;
		_d = d;
	}


	int _d;
};

// 如果是public的继承方式,子类与基类的关系: is-a
// is-a: 可以将子类对象看成是一个基类对象
//       函数调用(在类外):在类外所有用到基类对象的位置,都可以使用派生类对象代替
//       对象模型:对象中各个成员变量在内存中的布局方式


// 1. 可以将子类对象直接赋值给基类对象
// 2. 可以让基类的指针或者引用直接指向子类的对象
int main()
{
	Base b;
	b.SetBase(10);

	Derived d;
	d.SetDerived(20, 30);

	/*b.SetBase(40);
	b.PrintBase();*/

	d.SetBase(40);
	d.PrintBase();

	b = d;

	//d = b;

	Base* pb = &b;
	pb = &d;

	Base& rb = d;

	Derived* pd = &d;
	pd = (Derived*)&b;
	pd->_d = 30;
	return 0;
}
3.6.5 继承中的作用域
3.6.5.1 作用域
  • 1.在继承体系中基类和派生类都有独立的作用域。
  • 2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  • 3.需要注意的是如果是成员函数的影藏,只需要函数名相同就构成隐藏。
  • 4.注意在实际中,在继承体系里面最好不要定义同名的成员。
3.6.5.2 同名隐藏(重定义)
  • 基类与派生类中具有相同名称的成员(成员变量、成员函数)时:
  • 1.如果通过派生类对象调用相同名称的成员时,优先调用派生类自己;
  • 2.如果是成员变量同名,与成员变量的类型是否相同无关;
  • 3.如果是成员函数同名,与成员函数的原型是否相同无关, 都优先调用派生类自己的同名成员;
  • 4.如果想要调用基类的同名成员,只需在同名成员前增加基类的名称以及作用域限定符, (在子类成员函数中,可以使用 基类::基类成员 显示访问);
3.6.5.3 代码示意
class Base
{
public:
	void SetBase(int b)
	{
		_b = b;
	}

	void TestFunc(int)
	{}

	void PrintBase()
	{
		cout << _b << endl;
	}

	int _b;
};


struct Derived : public Base
{
public:
	void SetDerived(int b, int d)
	{
		_b = b;
	}

	void TestFunc()
	{}

	char _b;
};

int main()
{
	cout << sizeof(Derived) << endl;

	Derived d;
	d._b = 1;
	d.Base::_b = 2;

	d.Base::TestFunc(10);
	return 0;
}
3.6.6 派生类的默认成员函数
3.6.6.1 派生类行为执行顺序图

在这里插入图片描述

3.6.6.2 派生类成员函数说明
  • 如果一个类没有显示定义任何构造函数,编译器将会生成一个无参的默认构造函数;如果基类具有带参数的构造函数时,派生类必须显示定义自己的构造函数,并且在其初始化列表的位置显示调用基类的构造函数。
  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
3.6.6.3 代码示意
#if 0
// 如果基类没有定义构造函数 或者 基类具有默认的构造函数(无参和全缺省的构造函数)
// 1. 派生类可以不用定义
// 2. 如果派生类需要做其他事情,将自己的构造函数显式给出
class Base
{
public:
	Base(int b = 10)
		: _b(b)
	{}

	void SetBase(int b)
	{
		_b = b;
	}

	void TestFunc(int)
	{}

	void PrintBase()
	{
		cout << _b << endl;
	}

	int _b;
};


struct Derived : public Base
{
public:
	// 编译器给派生类生成的默认构造函数
	//Derived()
	//	: Base()
	//{}

	void SetDerived(int b, int d)
	{
		_b = b;
		_d = d;
	}

	void TestFunc()
	{}

	char _d;
};
#endif

#if 0
class Base
{
public:
	Base(int b)
		: _b(b)
	{}

	Base(const Base& b)
		: _b(b._b)
	{}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	void SetBase(int b)
	{
		_b = b;
	}

	void TestFunc(int)
	{}

	void PrintBase()
	{
		cout << _b << endl;
	}

	~Base()
	{
		cout << "Base::~Base()" << endl;
	}

	int _b;
};


// 如果基类具有无参的构造函数时,派生类必须显式定义自己的构造函数
// 并且必须在其初始化列表的位置显式调用基类的构造函数完成派生类对象
// 中基类部分成员的初始化
struct Derived : public Base
{
public:
	Derived(int b, int d)
		: Base(b)
		, _d(d)
 	{}

	// 编译器给派生类生成的默认构造函数
	//Derived()
	//	: Base()
	//{}

	// 如果基类没有显式定义自己的拷贝构造函数,派生类是否定义都可以
	// 如果基类显式定义自己的拷贝构造函数,派生类必须显式定义拷贝构造函数
	// 并且需要在其拷贝构造初始化列表的位置显式调用基类的拷贝构造函数
	Derived(const Derived& d)
		: Base(d)
		, _d(d._d)
	{}

	/*
	没有涉及到资源管理时:如果派生类没有显式定义自己的赋值运算符重载,编译器会生成一份默认的,
	该默认的赋值运算符重载可以完整的赋值

	如果类中涉及到资源管理:派生类需要将自己的拷贝构造函数显式给出,必须在其内部显式调用基类的
	赋值运算符重载完成基类部分的赋值,在完成自己特有成员的赋值
	*/
	Derived& operator=(const Derived& d)
	{
		if (this != &d)
		{
			Base::operator=(d);
			_d = d._d;
		}

		return *this;
	}

	void SetDerived(int b, int d)
	{
		_b = b;
		_d = d;
	}

	void TestFunc()
	{}

	// 派生类对象:基类部分 + 派生类特有
	~Derived()
	{
		// 派生类清理自己的资源
		cout << "Derived::~Derived()" << endl;

		// 编译器在拍摄类最后一条语句之后插入一条调用基类析构函数的汇编语句
		// 清理派生类对象中属于基类部分的资源
		// call Base::~Base();
	}

	char _d;
};

void TestDerived()
{
	Derived d1(10, 20);
	Derived d2(30, 40);

	d1 = d2;
}

int main()
{
	TestDerived();
	return 0;
}
#endif

#if 0
class Base
{
public:
	Base(int b)
		: _b(b)
	{
		cout << "Base::Base()" << endl;
	}

	~Base()
	{
		cout << "Base::~Base()" << endl;
	}

	int _b;
};

struct Derived : public Base
{
public:
	Derived(int b, int d)
		: Base(b)
		, _d(d)
	{
		cout << "Derived::Derived()" << endl;
	}

	// 派生类对象:基类部分 + 派生类特有
	~Derived()
	{
		// 派生类清理自己的资源
		cout << "Derived::~Derived()" << endl;

		// 编译器在拍摄类最后一条语句之后插入一条调用基类析构函数的汇编语句
		// 清理派生类对象中属于基类部分的资源
		// call Base::~Base();
	}

	char _d;
};

// 
/*
1. 打印结果
    Base::Base()
	Derived::Derived()
	Derived::~Derived()
	Base::~Base()
*/
// 2. 函数调用次序
/*
      基类构造
	  派生类构造
	  派生类析构
	  基类析构

	  创建那个类的对象,调用那个类的构造函数
	  析构那个类的对象,调用那个类的析构函数

	  Derived  d(10, 20);
	  调用派生类的构造函数
	      : 调用基类的构造函数
	  {}

	  调用派生类的析构函数
	  {
	      // 析构派生类自己的资源
		  // call 基类的析构函数
	  }
*/
3.6.7 继承和友元
  • 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
#if 0
class B
{
	friend void TestFunc();
public:

protected:
	int _b;
};

class D : public B
{
protected:
	int _d;
};


void TestFunc()
{
	// 该函数是基类的友元函数
	// 可以在该函数中访问基类保护或者私有的成员
	B b;
	b._b = 10;

	// 不能访问子类中私有或者保护的成员
	D d;
	d._d = 10;

	// 结论:友元关系 不能被继承
	// 继承:子类一定继承的是基类中的成员
	// 例子:你爸爸的王哥家的财产--与你无关
}
#endif
3.6.8 复杂的菱形继以菱形虚拟继承
3.6.8.1 单继承

定义

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

单继承关系图表示
在这里插入图片描述

3.6.8.2 多继承

定义

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

多继承关系图表示
在这里插入图片描述
多继承代码示意

#if 0
// 不同继承方式下,派生类的对象模型
// B <--- D    单继承:一个类只有一个基类


// 多继承
class B1
{
public:
	int _b1;
};

class B2
{
public:
	int _b2;
};

// 派生类将两个基类中的成员都继承到子类中
// 在派生类对象模型中:先继承那个基类,该基类中的成员就在对象模型的最上方
class D : public B1, public B2
{
public:
	int _d;
};


int main()
{
	cout << sizeof(D) << endl;

	D d;
	d._b1 = 1;
	d._b2 = 2;
	d._d = 3;
	return 0;
}
#endif
3.6.8.3 菱形继承

定义

  • 菱形继承是多继承的一种特殊情况(单继承+多继承)。

菱形继承示意图
在这里插入图片描述
菱形继承对象模型
在这里插入图片描述
菱形继承代码

// 菱形继承:单继承 + 多继承
class B
{
public:
	void TestFunc()
	{}

public:
	int _b;
};

class C1 : public B
{
public:
	int _c1;
};

class C2 : public B
{
public:
	int _c2;
};


class D : public C1, public C2
{
public:
	int _d;
};

int main()
{
	cout << sizeof(D) << endl;

	D  d;

	// C1从B中继承一个_b, C2从B中继承一个_b
	// D: 从C1中继承一个_b, 从C2中继承一个_b
	// D中就有两份_b
	// 如果直接通过派生类对象访问_b,编译器就不知道访问那个_b
	// 编译时报错:访问不明确
	// 以上就是菱形继承缺陷---菱形继承的二义性问题
	//d._b = 1;
	//d.TestFunc();

	// 代码通过编译:让访问明确
	// 1. 在_b前增加基类的名称
	d.C1::_b = 1;
	d._c1 = 2;

	d.C2::_b = 3;
	d._c2 = 4;

	d._d = 5;

	d.C1::TestFunc();
	d.C2::TestFunc();

	// 2. 能否让最顶层基类成员(B)在D类中只保存一份
	// 可以---菱形虚拟继承
	return 0;
}
#endif

菱形继承存在的问题:数据冗余和二义性问题。

3.6.8.4 虚拟继承

在这里插入图片描述
代码示意

#if 0
// 虚拟继承方式
class B
{
public:
	int _b;
};

class D : virtual public B
{
public:
	int _d;
};

// 虚拟继承和普通的单继承有什么区别?

int main()
{
	cout << sizeof(D) << endl;

	D d;
	d._b = 1;
	d._d = 2;
	return 0;
}
#endif
3.6.8.5 菱形虚拟继承

菱形虚拟继承对象模型
在这里插入图片描述
代码示意

#if 0
// 菱形虚拟继承
class B
{
public:
	void TestFunc()
	{}

public:
	int _b;
};

class C1 : virtual public B
{
public:
	int _c1;
};

class C2 : virtual public B
{
public:
	int _c2;
};


class D : public C1, public C2
{
public:
	int _d;
};

int main()
{
	cout << sizeof(D) << endl;

	D  d;

	// C1从B中继承一个_b, C2从B中继承一个_b
	// D: 从C1中继承一个_b, 从C2中继承一个_b
	// D中就有两份_b
	// 如果直接通过派生类对象访问_b,编译器就不知道访问那个_b
	// 编译时报错:访问不明确
	// 以上就是菱形继承缺陷---菱形继承的二义性问题
	//d._b = 1;
	//d.TestFunc();

	// 代码通过编译:让访问明确
	// 1. 在_b前增加基类的名称
	d.C1::_b = 1;
	d._c1 = 2;

	d.C2::_b = 3;
	d._c2 = 4;

	d._d = 5;

	d.C1::TestFunc();
	d.C2::TestFunc();

	// 2. 能否让最顶层基类成员(B)在D类中只保存一份
	// 可以---菱形虚拟继承
	d._b = 0;
	d.TestFunc();
	return 0;
}
#endif
#if 0
// 菱形虚拟继承
class B
{
public:
	void TestFunc()
	{}

public:
	int _b;
};

class C1 : virtual public B
{
public:
	int _c1;
};

class C2 : virtual public B
{
public:
	int _c2;
};


class D : public C1, public C2
{
public:
	int _d;
};

int main()
{
	// 24
	cout << sizeof(D) << endl;

	D  d;
	d._b = 1;

	d._c1 = 2;
	d._c2 = 3;
	d._d = 4;

	C1& c1 = d;
	c1._b = 5;

	C2& c2 = d;
	c2._b = 6;

	return 0;
}
#endif
3.6.9 继承存在的意义是什么?它能解决什么问题?
  • 实现代码复用(利用继承或者组合关系都可以实现代码复用)。
  • 过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性差。

3.7 多态(Polymorphism)

3.7.1 多态的定义
  • 多态是指子类可以替换父类,在实际代码运行过程中,调用子类的实现方法。
  • 通俗来说,就是多种形态,具体点来说就是去完成某个行为,当不同的对象去完成时会产生不同的状态。
  • 举个例子:买票这个行为,当普通人买票时,是全价票;学生买票时,是半价票;军人买票时,是优先买票。
3.7.2 多态的实现
3.7.2.1 多态的构成条件
  • 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。Person对象买票全价,Student对象买票半价。
    在继承体系中多态实现的两个条件
  • 1.必须通过基类的指针或者引用调用虚函数;
  • 2.被调用函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
3.7.2.2 虚函数
  • 即被virtuual修饰的类成员函数称为虚函数。
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
3.7.2.3 代码示意
#if 0
// 普通人
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价票" << endl;
	}

protected:
	string _name;
	string _gender;
	string _job;
	int _age;
};

class Student : public Person
{
public:
	void BuyTicket()
	{
		cout << "半价票" << endl;
	}

protected:
	int _stuId;
	double _score;
};

class soldier : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "军人优先" << endl;
	}
};


// 在程序运行时,根据基类引用实际引用的对象,选择调用对应类的虚函数
void TestBuyTicket(Person& p)
{
	p.BuyTicket();
}

// 如果多态的实现条件没有完全满足,全部调用基类的虚函数

int main()
{
	Person p;
	Student st;
	soldier so;

	TestBuyTicket(p);
	TestBuyTicket(st);
	TestBuyTicket(so);
	return 0;
}
#endif
3.7.2.4 虚函数的重写

定义

  • 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

虚函数重写的两个例外

  • 1.协变(基类与派生类虚函数返回值类型不同)
  • 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
// 协变
#if 0
class Base
{
public:
	virtual Base* TestFunc()
	{
		cout << "Base::TestFunc()" << endl;
		return this;
	}
};

class Derived : public Base
{
public:
	virtual Derived* TestFunc()
	{
		cout << "Derived::TestFunc()" << endl;
		return this;
	}
};

void TestVirtualFunc(Base* pd)
{
	pd->TestFunc();
}

#endif
  • 2.析构函数的重写(基类与派生类析构函数的名字不同)
  • 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数名称统一处理成 destructor。

代码示意

class Person {
public:
	virtual ~Person() {cout << "~Person()" << endl;}
};

class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成
多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main(){
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}
3.7.3 C++11 中的 override 和 final
3.7.3.1 override

定义

  • 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译器报错。

代码示意

#if 0
// C++98--编译器不能帮助用户进行重写的检测
// C++11----override: 让编译器帮助用户检测派生类是否对基类中的某个虚函数进行重写
//                    是:编译成功
//                    否:编译报错
// 注意:override只能修饰子类的虚函数
class Base
{
public:
	virtual void TestFunc1()
	{
		cout << "Base::TestFunc1()" << endl;
	}

	//void TestFunc2()  编译报错:基类中TestFunc2不是虚函数
	virtual void TestFunc2() // OK
	{
		cout << "Base::TestFunc2()" << endl;
	}

	virtual void TestFunc3()
	{
		cout << "Base::TestFunc3()" << endl;
	}
};

class Derived : public Base
{
public:
	// 与基类的函数名字不同
	//virtual void TetsFunc1()override  // 报错:函数名不同,没有构成重写
	virtual void TestFunc1()override    // OK
	{
		cout << "Derived::TestFunc1()" << endl;
	}

	// 基类TestFunc2不是虚函数
	virtual void TestFunc2()override
	{
		cout << "Derived::TestFunc2()" << endl;
	}

	// 与基类虚函数的参数列表不同
	// virtual void TestFunc3(int)override  编译报错:与基类中TestFunc3虚函数参数列表不同
	virtual void TestFunc3()override
	{
		cout << "Derived::TestFunc3()" << endl;
	}
};

void TestFunc(Base& b)
{
	b.TestFunc1();
	b.TestFunc2();
	b.TestFunc3();
}

int main()
{
	Base b;
	Derived d;
	TestFunc(b);
	TestFunc(d);
	return 0;
}
#endif
3.7.3.2 final

定义

  • 修饰虚函数,表示该虚函数不能再被继承。

代码示意

```cpp
// final如果修饰虚函数:表明该虚函数不能被派生类重写
class Base
{
public:
	virtual void TestFunc1()
	{
		cout << "Base::TestFunc1()" << endl;
	}
};

class Derived : public Base
{
public:
	virtual void TestFunc1()final
	{
		cout << "Derived::TestFunc1()" << endl;
	}
};

class D : public Derived
{
	virtual void TestFunc1()
	{
		cout << "Derived::TestFunc1()" << endl;
	}
};

void TestFunc(Base& b)
{
	b.TestFunc1();
}

int main()
{
	Base b;
	Derived d;
	D dd;
	TestFunc(b);
	TestFunc(d);
	TestFunc(dd);
	return 0;
}

3.7.4 重载,覆盖(重写)和隐藏(重定义)的对比

在这里插入图片描述

3.7.5 抽象类(不完整的类)
3.7.5.1 定义
  • 在虚函数的后面写上 = 0,则这个函数为纯虚函数。包含有纯虚函的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
3.7.5.2 代码
class Car{
public:
	virtual void Drive() = 0;
};

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

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

void Test(){
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

3.7.6 多态的原理
3.7.6.1 多态的分类(静态多态和动态多态)
  • 1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态绑定,比如:函数重载,函数模板。
  • 2.动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态绑定。
3.7.6.2 虚函数表的引入
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
{
	cout << "Func1()" << endl;
}
private:
	int _b = 1;
};
  • 通过观察测试发现 b 对象时 8 bytes,除了 _b成员,还多了一个 _vfptr 放在对象的前面(注意有些平台可能会放到对象的后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针。
  • 一个含有虚函数的类至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
3.7.6.2 代码示意
#include <iostream>
using namespace std;

#if 1
class Base
{
public:
	// 	Base()
	// 	{}

	virtual void TestFunc1()
	{
		cout << "Base::TestFunc1()" << endl;
	}

	virtual void TestFunc2()
	{
		cout << "Base::TestFunc2()" << endl;
	}

	virtual void TestFunc3()
	{
		cout << "Base::TestFunc3()" << endl;
	}


	int _b;
};

class Derived : public Base
{
public:
	virtual void TestFunc5()
	{
		cout << "Derived::TestFunc5()" << endl;
	}

	virtual void TestFunc1()
	{
		cout << "Derived::TestFunc1()" << endl;
	}

	virtual void TestFunc3()
	{
		cout << "Derived::TestFunc3()" << endl;
	}

	virtual void TestFunc4()
	{
		cout << "Derived::TestFunc4()" << endl;
	}
public:
	int _d;
};

// 函数指针变量
//void (*PVFT)();

// 函数指针类型
typedef void(*PVFT)();
// 需要获取虚表的地址:对象前4个字节
//&d;  // 对象的首地址
//(int*)(&d)  // 整形指针--->指向对象的前4个字节
// *(int*)(&d)--->将对象前4个字节中的内容取出--->整形数据《===》虚表地址--
//在数据上相等
void PrintVFT(Base& b)
{
	PVFT* pVFT = (PVFT*)(*(int*)&b);
	while (*pVFT)
	{
		(*pVFT)();
		++pVFT;
	}
	cout << endl;
}

int main()
{
	cout << sizeof(Base) << endl;
	Base b;
	b._b = 1;

	cout << sizeof(Derived) << endl;
	Derived d;
	d._b = 1;
	d._d = 2;

	PrintVFT(d);
	system("pause");
	return 0;
}
#endif
3.7.6.3 虚函数表存放规则

虚函数的存放规则: 按照在类中声明的先后次序进行存储。

基类虚基表的存放规则

  • 按照函数在类中的声明次数将其依次放置在虚表中。
    在这里插入图片描述

派生类虚函数表构建规则

  • 1.先将基类中的虚表内容拷贝一份到派生类虚表中;
  • 2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
  • 3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的租后。
    在这里插入图片描述
3.7.6.4 对虚基表中虚函数的调用过程 (满足多态条件)
  • 1.从对象的前4个字节中获取虚基表的地址;
  • 2.传递 this指针;
  • 3.从虚基表中获取当前调用函数的地址;
  • 4.调用虚函数;
  • 注意:如果未满足多态条件时,则直接进行调用。
3.7.6.5 单继承和多继承中的虚函数表

多继承中的虚函数表

代码示意

#include <iostream>
using namespace std;

#if 1
class B1
{
public:
	virtual void TestFunc1()
	{
		cout << "B1::TestFunc1()" << endl;
	}

	virtual void TestFunc2()
	{
		cout << "B1::TestFunc2()" << endl;
	}

	int _b1;
};

class B2
{
public:
	virtual void TestFunc3()
	{
		cout << "B2::TestFunc3()" << endl;
	}

	virtual void TestFunc4()
	{
		cout << "B2::TestFunc4()" << endl;
	}

	int _b2;
};

class D : public B1, public B2
{
public:
	virtual void TestFunc1()
	{
		cout << "D::TestFunc1()" << endl;
	}

	virtual void TestFunc4()
	{
		cout << "D::TestFunc4()" << endl;
	}

	virtual void TestFunc5()
	{
		cout << "D::TestFunc5()" << endl;
	}

	int _d;
};

typedef void(*PVFT)();

void PrintVFT1(B1& b)
{
	cout << "==========" << endl;
	PVFT* pVFT = (PVFT*)(*(int*)&b);
	while (*pVFT)
	{
		(*pVFT)();
		++pVFT;
	}
	cout << "==========" << endl;
}

void PrintVFT2(B2& b)
{
	cout << "==========" << endl;
	PVFT* pVFT = (PVFT*)(*(int*)&b);
	while (*pVFT)
	{
		(*pVFT)();
		++pVFT;
	}
	cout << "==========" << endl;
}


int main()
{
	cout << sizeof(D) << endl;

	D d;
	d._b1 = 1;
	d._b2 = 2;
	B1& b1 = d;
	PrintVFT1(b1);

	B2& b2 = d;
	PrintVFT2(b2);
	return 0;
}
#endif

对象模型
在这里插入图片描述
在这里插入图片描述
多继承派生类对象模型
在这里插入图片描述

3.7.6.6 菱形继承、菱形虚拟继承
#if 0
// 菱形虚拟继承
// 8
class B
{
public:
	virtual void TestFunc1()
	{
		cout << "B::TestFunc1()" << endl;
	}

	virtual void TestFunc2()
	{
		cout << "B::TestFunc2()" << endl;
	}

	int _b;
};

// 
class C1 : virtual public B
{
public:
	virtual void TestFunc1()
	{
		cout << "C1::TestFunc1()" << endl;
	}

	virtual void TestFunc3()
	{
		cout << "C1::TestFunc3()" << endl;
	}

	int _c1;
};

class C2 : virtual public B
{
public:
	virtual void TestFunc2()
	{
		cout << "C2::TestFunc2()" << endl;
	}

	virtual void TestFunc4()
	{
		cout << "C2::TestFunc4()" << endl;
	}

	int _c2;
};

class D : public C1, public C2
{
public:
	virtual void TestFunc1()
	{
		cout << "D::TestFunc1()" << endl;
	}

	virtual void TestFunc3()
	{
		cout << "D::TestFunc3()" << endl;
	}

	virtual void TestFunc4()
	{
		cout << "D::TestFunc4()" << endl;
	}

	virtual void TestFunc5()
	{
		cout << "D::TestFunc5()" << endl;
	}

	int _d;
};


typedef void(*PVFT)();

void PrintVFT1(C1& b, const string& str)
{
	cout << str << endl;

	PVFT* pVFT = (PVFT*)(*(int*)&b);
	while (*pVFT)
	{
		(*pVFT)();
		++pVFT;
	}
	cout << endl;
}

void PrintVFT2(C2& b, const string& str)
{
	cout << str << endl;

	PVFT* pVFT = (PVFT*)(*(int*)&b);
	while (*pVFT)
	{
		(*pVFT)();
		++pVFT;
	}
	cout << endl;
}

void PrintVFT3(B& b, const string& str)
{
	cout << str << endl;

	PVFT* pVFT = (PVFT*)(*(int*)&b);
	while (*pVFT)
	{
		(*pVFT)();
		++pVFT;
	}
	cout << endl;
}

int main()
{
	cout << sizeof(D) << endl;

	D d;
	d._b = 1;
	d._c1 = 2;
	d._c2 = 3;
	d._d = 4;

	PrintVFT1(d, "override C1");
	PrintVFT2(d, "override C2");
	PrintVFT3(d, "override B");
	return 0;
}
#endif
3.7.4 多态存在的意义是什么?它能解决什么问题?
  • 1.多态特性能提高代码的可扩展性和复用性。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值