类和对象(友元、运算符重载、继承、多态)---C++

发布时间:2024年01月24日

在这里插入图片描述

4.友元

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术;

友元的目的就是让一个函数或者类访问另一个类中私有成员
友元的关键字为 friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

4.1全局函数做友元

class Building
{
	//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
	friend void goodGay(Building* building);

public:

	Building()
	{
		m_SittingRoom = "客厅";
		m_BedRoom = "卧室";
	}

public:
	//公共权限
	string m_SittingRoom; //客厅

private:
	//私有权限
	string m_BedRoom; //卧室
};

//全局函数
void goodGay(Building* building)
{
	cout << "好朋友正在访问: " << building->m_SittingRoom << endl;
	cout << "好朋友正在访问: " << building->m_BedRoom << endl;
}

void test01()
{
	Building b;
	goodGay(&b);
}

在这里插入图片描述
friend void goodGay(Building* building);

  • 告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容

可见,调用test01函数,当全局函数做友元时,该全局函数可以访问私有权限内容。

4.2类做友元

class Building;//先声明,防止goodGay类出错
class goodGay
{
public:
	//类内声明,类外实现
	goodGay();
	void visitor();
	~goodGay();

private:
	Building* building;
};

class Building
{
	//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
	friend class goodGay;

public:
	Building();

public:
	string m_SittingRoom; //客厅
private:
	string m_BedRoom;//卧室
};

//类外实现成员函数(注意加上所在类空间)
Building::Building()//Building类构造函数
{
	this->m_SittingRoom = "客厅";
	this->m_BedRoom = "卧室";
}
goodGay::goodGay()//goodGay类构造函数
{
	//堆区开辟空间,注意要释放(析构函数释放,delete)
	building = new Building;
}
goodGay::~goodGay()
{
	if(building!=NULL)
	{
		delete building;
		building=NULL;
	}
}
void goodGay::visitor()
{
	cout << "好朋友正在访问" << building->m_SittingRoom << endl;
	cout << "好朋友正在访问" << building->m_BedRoom << endl;
}

void test01()
{
	goodGay gg;
	gg.visitor();

}

在这里插入图片描述
== friend class goodGay;==

  • 告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容

可见,调用test01函数后,当类做友元时,该类内可以访问另一个类内的私有权限内容。

4.3成员函数做友元

class Building;//先声明,防止goodGay类出错
class goodGay
{
public:

	goodGay();
	void visitor1(); //只让visitor1函数作为Building的好朋友,可以发访问Building中私有内容
	void visitor2();

private:
	Building* building;
};

class Building
{
	//告诉编译器  goodGay类中的visitor1成员函数 是Building好朋友,可以访问私有内容
	friend void goodGay::visitor1();

public:
	Building();

public:
	string m_SittingRoom; //客厅
private:
	string m_BedRoom;//卧室
};

Building::Building()
{
	this->m_SittingRoom = "客厅";
	this->m_BedRoom = "卧室";
}
goodGay::goodGay()
{
	building = new Building;
}

void goodGay::visitor1()
{
	cout << "好朋友1正在访问" << building->m_SittingRoom << endl;
	cout << "好朋友1正在访问" << building->m_BedRoom << endl;
}
void goodGay::visitor2()
{
	cout << "好朋友2正在访问" << building->m_SittingRoom << endl;
	//cout << "好朋友正在访问" << building->m_BedRoom << endl;//无法访问
}

void test01()
{
	goodGay  gg;
	gg.visitor1();
	gg.visitor2();

}

在这里插入图片描述
friend void goodGay::visitor1();

  • 告诉编译器 goodGay类中的visitor1成员函数 是Building好朋友,可以访问私有内容

可见,调用test01函数后,当一个类内的成员函数做另一个类的友元时,该类内的成员函数可以访问另一个类内的私有权限内容。

5.运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

5.1 加号运算符重载

作用:实现两个自定义数据类型相加的运算。

对于内置数据类型,编译器知道如何进行运算:
例如两个整型数据的相加,可以直接应用,但对于一些非内置数据类型,比如两个对象的相加,编译器内部没有相关运算方式,故需要我们自己对运算符进行补充。

如果不进行运算符重载,就会出现下面类似的错误:
在这里插入图片描述

5.1.1成员函数实现运算符重载

实例:实现两个对象的相加

class Person {
public:
	Person() {};//为了提供对无参构造函数的调用
	//若无,自己定义了有参构造函数,系统默认无无参构造函数
	//则Person temp;无法成立
	Person(int a, int b)//有参构造
	{
		this->m_A = a;
		this->m_B = b;
	}
	//成员函数实现 + 号运算符重载
	//函数名operator+系统默认,可以实现简化调用
	//自己定义函数名也可,但无法实现下面的简化调用
	Person operator+(const Person& p) {
		Person temp;//无参构造
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;//值返回(重新创建新对象)
	}

public:
	int m_A;
	int m_B;
};

//运算符重载 可以发生函数重载 
//同一函数名表示不同运算
Person operator+(const Person& p2, int val)
{
	Person temp;
	temp.m_A = p2.m_A + val;
	temp.m_B = p2.m_B + val;
	return temp;
}

//测试函数
void test() {

	Person p1(10, 10);
	Person p2(20, 20);

	//成员函数方式
	//本质调用为:
	//Person p3=p2.operaor+(p1)
	Person p3 = p2 + p1; 
	cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;

	//本质调用为:
	//Person p4=operaor+(p3,10)
	Person p4 = p3 + 10; 
	cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;

}

在这里插入图片描述

5.1.2全局函数实现运算符重载

示例:

class Person {
public:
	Person(int a, int b)//有参构造
	{
		this->m_A = a;
		this->m_B = b;
	}

public:
	int m_A;
	int m_B;
};

//全局函数实现 + 号运算符重载
//对象+对象
Person operator+(const Person & p1, const Person & p2) {
	Person temp(0, 0);
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}
//运算符重载 可以发生函数重载 
//对象+int
Person operator+(const Person& p2, int val)
{
	Person temp(0,0);
	temp.m_A = p2.m_A + val;
	temp.m_B = p2.m_B + val;
	return temp;
}

void test() {

	Person p1(10, 10);
	Person p2(20, 20);

	//成员函数方式
	//本质实现:
	Person p3 = operator+ (p1, p2);
	//Person p3 = p2 + p1;  
	cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;

	//本质实现:Person p4 = operator+ (p3, 10);
	Person p4 = p3 + 10; 
	cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}

在这里插入图片描述

总结1:对于内置的数据类型的表达式的的运算符是不可能改变的;
总结2:不要滥用运算符重载。(即不可命名为加号运算符重载,实现用减法)

5.2 左移运算符重载

作用:可以输出自定义数据类型。

5.2.1全局函数实现运算符重载

示例:

class Person {
	//友元:实现对私有权限成员的访问
	friend ostream& operator<<(ostream& out, Person& p);

public:

	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}

	

private:
	int m_A;
	int m_B;
};

//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {
	out << "a:" << p.m_A << " b:" << p.m_B;
	return out;
}

void test() {

	Person p1(10, 20);
	cout << p1 << "hello world" << endl; //链式编程
}

在这里插入图片描述

5.2.2成员函数实现运算符重载

示例:

  • 1.对象本身做形参
class Person {

public:

	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}

	//成员函数 实现左移运算符重载,可以实现,但不是我们想要的效果
	ostream& operator<<(Person& p){
		cout << "a:" << p.m_A << " b:" << p.m_B;
		return cout;//链式编程
	}	
private:
	int m_A;
	int m_B;
};

void test() {

	Person p1(10, 20);
	p1.operator<<(p1)<<endl;
	//简化
	p1 << p1 << " hello world" <<endl;//与内置函数实现不一致
}

在这里插入图片描述

  • 2.标准输出流做形参

示例:

class Person {

public:

	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}

	//成员函数 实现左移运算符重载,可以实现,但不是我们想要的效果
	ostream& operator<<(ostream &out) {
		out << "a:" << this->m_A << " b:" << this->m_B;;
		return cout;//链式编程
	}

private:
	int m_A;
	int m_B;
};

void test() {

	Person p1(10, 20);
	//本质实现:
	//p1.operator<<(cout);
	p1 << cout << " hello world" << endl;//可见与内置函数输出不一致
}

利用成员函数重载左移运算符,无法实现与内置输出一致的顺序(即cout<<p,cout在左侧),故不会利用成员函数重载<<运算符。

总结:重载左移运算符配合友元可以实现输出自定义数据类型

5.3 递增/递减运算符重载

作用: 通过重载递增运算符,实现自己的整型数据

5.3.1 前置++

5.3.1.1成员函数实现运算符重载

示例:

class MyInteger {

	friend ostream& operator<<(ostream& out, MyInteger myint);

public:
	MyInteger() {
		m_Num = 0;
	}
	//前置++
	//局部函数实现
	//返回引用
	MyInteger& operator++() {
		//先++
		m_Num++;
		//再返回
		return *this;//返回对象本身(引用),实现对一直对一个对象进行递增操作
	}

private:
	int m_Num;
};

//左移运算符重载:全局函数
ostream& operator<<(ostream& out, MyInteger myint) {
	out << myint.m_Num;
	return out;
}

//前置++ 先++ 再返回
void test01() {
	MyInteger myInt;
	cout << ++(++myInt) << endl;
	cout << myInt << endl;
//本质实现:
//operator<<(cout, myInt.operator++())<<endl;//相当于cout << ++myInt << endl;
//cout << myInt.operator++().operator++() << endl;//相当于cout << ++(++myInt) << endl;
}

分析:++myInt:m_Num=1(返回对象本身);
++(++myInt)(对同一个对象++) :m_Num=2(返回对象本身);

在这里插入图片描述

  • 如果将返回值改为值返回:即
//值返回
MyInteger operator++() {
	//先++
	m_Num++;
	//再返回
	return *this;//拷贝一个新对象
}

分析:++myInt(第一次:对象本身++):m_Num=1(创建新对象);
++(++myInt)(对新对象++):m_Num=2(返回新对象);

  • 即cout << ++(++myInt) << endl中的++(++myInt)不再是原对象,而是创建的第二个新对象(其内容和返回对象本身结束的时候一样);
    cout << myInt << endl;输出对象本身。

在这里插入图片描述
可见,返回类型不同,最后结果不同。

  • 返回对象本身(引用):实现对一直对一个对象进行递增操作,每一次++都对同一个进行运算。
  • 值返回方式:第一次调用++是对对象本身进行,但之后回利用拷贝构造函数创建一个新的对象,就形成了每次调用都会形成一个新对象,无法实现如同第一种(返回引用)的对同一个对象持续累加。
5.3.1.2全局函数实现运算符重载
MyInteger& operator++(MyInteger &myInt) {
	//先++
	myInt.m_Num++;
	//再返回
	return myInt;//返回对象本身(引用)
}

注:全局函数下,要实现链式访问只能返回对象本身(返回引用);

5.3.2 后置++

在这里插入图片描述
对于后置++,无法实现链式编程。故对于后置++的重载也无法实现链式编程。
报错原因:表达式必须是可修改的左值。

5.3.2.1成员函数实现运算符重载

示例:

class MyInteger {

	friend ostream& operator<<(ostream& out, MyInteger myint);
public:
	MyInteger() {
		m_Num = 0;
	}
	
	//后置++
	MyInteger operator++(int) {
		//先返回
		MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
		m_Num++;
		return temp;
	}

private:
	int m_Num;
};
//左移运算符重载:全局函数
ostream& operator<<(ostream& out, MyInteger myint) {
	out << myint.m_Num;
	return out;
}

//后置++ 先返回 再++
void test02() {

	MyInteger myInt;
	cout << myInt++ << endl;
	cout << myInt << endl;
	//本质实现:
	//operator<<(cout, myInt.operator++(0))<<endl;
	//operator<<(cout, myInt)<<endl;
}

在这里插入图片描述

MyInteger operator++(int):int-占位参数

5.3.2.2全局函数实现运算符重载

示例:

//int为占位参数,为了和前置++区分;调用时需补占位参数(任意数都可)
MyInteger operator++(MyInteger& myInt,int) {
	//先返回
	MyInteger temp = myInt; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
	myInt.m_Num++;
	return temp;
}

//后置++ 先返回 再++
void test02() {

	MyInteger myInt;
	cout << myInt++ << endl;
	cout << myInt << endl;
	// 
	//本质实现:
	//operator<<(cout, operator++(myInt,0)) << endl;
	//operator<<(cout, myInt)<<endl;

}

5.3.3 前置–

5.3.3.1成员函数实现运算符重载

参考前置++:(前置–可以实现链式编程,只列写返回对象本身)

MyInteger& operator--() {
	//先--
	m_Num--;
	//再返回
	return *this;//返回对象本身(引用),实现对一直对一个对象进行递增操作
}
5.3.3.2全局函数实现运算符重载
MyInteger& operator--(MyInteger& myInt) {
	//先++
	myInt.m_Num--;
	//再返回
	return myInt;//返回对象本身(引用)
}

5.3.4 后置–

参见后置++:

5.3.4.1成员函数实现运算符重载
//后置--
MyInteger& operator--(int) {
	//先返回
	MyInteger temp = *this; //记录当前本身的值,然后让本身的值减1,但是返回的是以前的值,达到先返回后--;
	m_Num--;
	return temp;
}
5.3.4.2全局函数实现运算符重载
//int为占位参数,为了和前置--区分;调用时需补占位参数(任意数都可)
MyInteger operator--(MyInteger& myInt,int) {
	//先返回
	MyInteger temp = myInt; //记录当前本身的值,然后让本身的值减1,但是返回的是以前的值,达到先返回后--;
	myInt.m_Num--;
	return temp;
}

递增/递减运算符重载总结: 前置递增返回引用,后置递增返回值.

5.4 赋值运算符重载

c++编译器至少给一个类添加4个函数:

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=, 对属性进行值拷贝

前三个之前介绍过,此处着重介绍第四个:
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题 。
深浅拷贝

示例:

class Person
{
public:

	Person(int age)
	{
		//将年龄数据开辟到堆区
		m_Age = new int(age);
	}

	//重载赋值运算符 
	Person& operator=(Person& p)
	{
		//判断是否有属性在堆区,若有先释放干净,再进行深拷贝
		if (m_Age != NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
		//编译器提供的代码是浅拷贝
		//m_Age = p.m_Age;

		//提供深拷贝 解决浅拷贝的问题
		m_Age = new int(*p.m_Age);

		//返回自身
		return *this;//链式编程
	}


	~Person()
	{
		//释放堆区空间
		if (m_Age != NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
	}

	//年龄的指针
	int* m_Age;

};
void test01()
{
	Person p1(18);
	Person p2(20);
	Person p3(30);

	p3 = p2 = p1; //赋值操作:链式编程

	cout << "p1的年龄为:" << *p1.m_Age << endl;
	cout << "p2的年龄为:" << *p2.m_Age << endl;
	cout << "p3的年龄为:" << *p3.m_Age << endl;
}

在这里插入图片描述
附:
参照内置类型赋值的链式程序。

int a = 10;
int b = 20;
int c = 30;

c = b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;

在这里插入图片描述

5.5 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作。

5.5.1 关系运算符(==)重载

示例:

class Person
{
public:
	Person(string name, int age)
	{
		this->m_Name = name;
		this->m_Age = age;
	};

	bool operator==(Person& p)
	{
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	string m_Name;
	int m_Age;
};

void test01()
{

	Person a("Tom", 18);
	Person b("Tom", 25);

	if (a == b)
	{
		cout << "a和b相等" << endl;
	}
	else
	{
		cout << "a和b不相等" << endl;
	}

}

在这里插入图片描述

5.5.2 关系运算符(>)重载

示例:

class Person
{
public:
	Person(string name, int age)
	{
		this->m_Name = name;
		this->m_Age = age;
	};

	int operator>(Person& p)
	{
	//按字母进行比较
		return this->m_Name.compare(p.m_Name);
		//compare按照每一个字母的ASCII值进行比较,根据结果返回0,大于0,小于0
	}

	string m_Name;
	int m_Age;
};

void test01()
{

	Person a("Tom", 18);
	Person b("Marry", 25);

	if ((a > b) == 0)
	{
		cout << "a==b" << endl;
	}
	else if ((a > b) > 0)
	{
		cout << "a>b" << endl;
	}
	else
		cout << "a<b" << endl;
}

在这里插入图片描述

5.6 函数调用运算符重载

  • 函数调用运算符 () 也可以重载;
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数;
  • 仿函数没有固定写法,非常灵活。

示例:

class MyAdd
{
public:
	int operator()(int v1, int v2)
	{
		return v1 + v2;
	}
};

void test01()
{
	MyAdd add;
	int ret = add(10, 10);//相当于:int ret = add.operator()(10, 10)
	cout << "ret = " << ret << endl;

	//匿名对象调用  
	cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}

在这里插入图片描述

6.继承

继承是面向对象三大特性之一

有些类与类之间存在特殊的关系,例如下图中:
在这里插入图片描述
猫和狗都具备动物的属性,同时其又有很多品种(自己的属性)。

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码

6.1 继承的基本语法

继承的语法:class 子类 : 继承方式 父类
借助下面的事例,介绍继承的优势和语法:

例如:
我们看到很多网站(以某网站编程培训为例)中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同;接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处:

普通实现:

//Java页面
class Java 
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	
	void content()
	{
		cout << "JAVA学科视频" << endl;
	}
};
//Python页面
class Python
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	
	void content()
	{
		cout << "Python学科视频" << endl;
	}
};
//C++页面
class CPP 
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	
	void content()
	{
		cout << "C++学科视频" << endl;
	}
};

//测试函数
void test01()
{
	//Java页面
	cout << "Java下载视频页面如下: " << endl;
	Java ja;
	ja.header();
	ja.footer();
	ja.content();
	cout << "--------------------" << endl;

	//Python页面
	cout << "Python下载视频页面如下: " << endl;
	Python py;
	py.header();
	py.footer();
	py.content();
	cout << "--------------------" << endl;

	//C++页面
	cout << "C++下载视频页面如下: " << endl;
	CPP cp;
	cp.header();
	cp.footer();
	cp.content();

}

可见,对于上述代码,有一部分代码多次重复引用,虽然结构清晰,但会造成代码冗余,内存浪费。
借助继承的特性可以实现简化:

继承实现:

//公共页面
class BasePage
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}

	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}

};

//Java页面
class Java : public BasePage
{
public:
	void content()
	{
		cout << "JAVA学科视频" << endl;
	}
};
//Python页面
class Python : public BasePage
{
public:
	void content()
	{
		cout << "Python学科视频" << endl;
	}
};
//C++页面
class CPP : public BasePage
{
public:
	void content()
	{
		cout << "C++学科视频" << endl;
	}
};

在这里插入图片描述
将普通实现的每个类内容替换,测试函数不变,会实现如上的结果。

总结:

继承的好处:可以减少重复的代码
class A : public B
A 类称为子类 或 派生类
B 类称为父类 或 基类

派生类中的成员,包含两大部分

一类是从基类继承过来的,一类是自己增加的成员
从基类继承过过来的表现其共性,而新增的成员体现了其个性

6.2继承的方式

继承的语法:class 子类 : 继承方式 父类

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承
    不同继承方式下,对于父类的不同权限的内容的访问条件,可以用下面的图进行说明。
    在这里插入图片描述
    可联系,封装权限中的保护权限和私有权限的区别:
  • protected 保护权限 :类内可以访问 类外不可以访问(例如:儿子可以访问到父亲中的保护内容)
  • private 私有权限 :类内可以访问 类外不可以访问(例如:儿子不可以访问到父亲中的私有内容)

6.3继承中的对象模型

问题:
从父类继承过来的成员,哪些属于子类对象中?
或者说子类的大小对父类中的继承权限有无关系?

示例:

class Base
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C; //私有成员只是被隐藏了,但是还是会继承下去
};

//公共继承
class Son :public Base
//对于父类中的公共权限和保护权限,可访问,不可访问私有权限
{
public:
	int m_D;
};

void test01()
{
	cout << "sizeof Son = " << sizeof(Son) << endl;
}

在这里插入图片描述
可见,虽然子类无法访问父类中的私有权限内容,但子类大小是包含父类中的私有权限的。
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:

在这里插入图片描述
由上图可知,对于Son类中,包含父类的全部内容(三种权限内容都被继承下来)和自己的特有内容,虽然对于公共继承而言,父类中私有权限无法访问,但其也被子类继承,只是被编译器隐藏。

6.4继承中的构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数;
问题:父类和子类的构造和析构顺序是谁先谁后?

  • 继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
    示例:
class Base
{
public:
	Base()
	{
		cout << "Base构造函数!" << endl;
	}
	~Base()
	{
		cout << "Base析构函数!" << endl;
	}
};

class Son : public Base
{
public:
	Son()
	{
		cout << "Son构造函数!" << endl;
	}
	~Son()
	{
		cout << "Son析构函数!" << endl;
	}

};

void test01()
{
	Son s;
}

在这里插入图片描述

总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
(可对比类对象作为类成员的构造和析构函数调用顺序)2.7类对象作为类成员

6.5继承同名成员处理方式

6.5.1继承非静态同名成员处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员 直接访问即可;
  • 访问父类同名成员 需要加作用域
6.5.1.1非静态同名成员变量处理方式

示例:

//父类
class Base {
public:
	Base()
	{
		m_A = 100;
	}
	
public:
	int m_A;
};

//子类
class Son : public Base {
public:
	Son()
	{
		m_A = 200;
	}
public:
	int m_A;
};

void test01()
{
	//子类和父类都有m_A成员变量
	Son s;
	cout << "Son类下m_A:" << s.m_A << endl;

	cout << "Base类下m_A:" << s.m_A << endl;
	cout << "Base类下m_A:" << s.Base::m_A << endl;
}

在这里插入图片描述
可见,子类和父类中有同名成员变量时,如要访问父类中成员变量需要加上父类所在作用域。

6.5.1.2非静态同名成员函数处理方式

当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数;
如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域.

示例:

//父类
class Base {
public:
	
	void func()
	{
		cout << "Base - func()调用" << endl;
	}

	void func(int a)
	{
		cout << "Base - func(int a)调用" << endl;
	}

public:
	int m_A;
};

//子类
class Son : public Base {
public:
	void func()
	{
		cout << "Son - func()调用" << endl;
	}
public:
	int m_B;
};

void test02()
{
	Son s;

	s.func();
	//s.func(10);//报错
	s.Base::func();
	s.Base::func(10);
}

在这里插入图片描述
总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

6.5.2继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员 直接访问即可;
  • 访问父类同名成员 需要加作用域
6.5.2.1同名静态成员变量处理方式

示例:

class Base {
public:
	static int m_A;
};

//类内声明,类外初始化
int Base::m_A = 100;

class Son : public Base {
public:
	static int m_A;
};
//类内声明,类外初始化
int Son::m_A = 200;

//同名成员属性
void test01()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	cout << "Son  下 m_A = " << s.m_A << endl;
	cout << "Base 下 m_A = " << s.Base::m_A << endl;

	//通过类名访问
	cout << "通过类名访问: " << endl;
	cout << "Son  下 m_A = " << Son::m_A << endl;
	cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}

在这里插入图片描述
对于静态同名成员变量,子类和父类中成员变量的访问有两种方式:

  • 通过对象访问:子类对象直接访问子类同名成员,父类成员访问需要子类对象加上父类作用域;
  • 通过类名访问:子类对象可以直接在子类类名直接访问,父类成员需要在子类对象类名的基础上加上父类的作用域。
    • Son::Base::m_A 中第一个::代表通过类名方式访问;第二个::代表访问父类作用域下。
6.5.2.2同名静态成员函数处理方式

同同名静态成员变量访问一样。
示例:

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

	static int m_A;
};

class Son : public Base {
public:
	static void func()
	{
		cout << "Son - static void func()" << endl;
	}
	static int m_A;
};

//同名成员函数
void test02()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	s.func();
	s.Base::func();
	s.Base::func(10);

	//通过类名访问
	cout << "通过类名访问: " << endl;
	Son::func();
	Son::Base::func();
	//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
	Son::Base::func(100);
}

在这里插入图片描述

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)

大总结:

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

6.6多继承语法

C++允许一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分。
注:C++实际开发中不建议用多继承

示例:

//父类1
class Base1 {
public:
	Base1()
	{
		m_A = 100;
	}
public:
	int m_A;
};

//父类2
class Base2 {
public:
	Base2()
	{
		m_A = 200;  
	}
public:
	//同名成员变量
	int m_A;
};

//语法:class 子类:继承方式 父类1 ,继承方式 父类2 
class Son : public Base2, public Base1
{
public:
	Son()
	{
		m_C = 300;
		m_D = 400;
	}
public:
	int m_C;
	int m_D;
};

//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
	Son s;
	cout << "sizeof Son = " << sizeof(s) << endl;
	cout << s.Base1::m_A << endl;
	cout << s.Base2::m_A << endl;
}

在这里插入图片描述
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
在这里插入图片描述

总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域

6.7菱形继承

菱形继承概念:
? 两个派生类继承同一个基类;
? 又有某个类同时继承者两个派生类;
? 这种继承被称为菱形继承,或者钻石继承。
典型的菱形继承案例:
在这里插入图片描述
在上图中,羊和驼都继承了动物中的属性,同时羊驼又分别继承两者的属性,就会造成羊驼中重复包含动物属性,造成浪费。即:

菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性(不知道继承谁的)。
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

普通示例:

class Animal
{
public:
	int m_Age;
};

class Sheep :  public Animal {};
class camel :  public Animal {};//camel-骆驼
class alpaca : public Sheep, public camel {};//alpaca-羊驼

void test01()
{
	alpaca st;
	st.Sheep::m_Age = 100;
	st.camel::m_Age = 200;

	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
	cout << "st.camel::m_Age = " << st.camel::m_Age << endl;
}

在这里插入图片描述
对于,羊和驼中都含有动物中年龄成员变量,羊驼中就会含有两份年龄成员变量,就会造成以哪个为准呢?
虽然我们可通过添加作用域进行区分,但仍会造成内存浪费,毕竟羊驼只需要一份就可。这个问题可以通过下面的方式进行解决:

优化示例:

虚继承:

  • 继承前加virtual关键字后,变为虚继承
  • 此时公共的父类Animal称为虚基类
class Animal
{
public:
	int m_Age;
};

class Sheep : virtual public Animal {};
class camel : virtual public Animal {};//camel-骆驼
class alpaca : public Sheep, public camel {};//alpaca-羊驼

void test01()
{
	alpaca st;
	st.Sheep::m_Age = 100;
	st.camel::m_Age = 200;

	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
	cout << "st.camel::m_Age = " << st.camel::m_Age << endl;
	cout << "st.m_Age = " << st.m_Age << endl;

	st.Sheep::m_Age = 300;
	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
	cout << "st.camel::m_Age = " << st.camel::m_Age << endl;
	cout << "st.m_Age = " << st.m_Age << endl;
}

在这里插入图片描述
可以看出,采用虚继承方式,羊驼继承动物的属性就变成了一份,无论改变羊和驼中哪个属性,三者中的属性变量就都改变了。
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:

  • 普通示例:
    在这里插入图片描述
  • 优化示例:
    在这里插入图片描述
    对比两者,我们可以方向,参与虚继承方式下,动物类中属性(m_Age)只含有一份。
    虚继承下,羊和驼类中包含一个指针(vbptr-virtual base pointer虚基类指针)指向vbtable(虚基类表-图中1和2的位置),羊和驼中的虚基类表包含各自的偏移量,用于找到动物类中的属性(m_Age),羊和驼就不再分别基础动物类中的属性了,只继承一个指针用于找到动物类的属性,减少空间浪费。

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

7.多态

在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口
多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上

7.1 多态的基本应用

7.1.1 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类:

  • 静态多态: 函数重载和运算符重载属于静态多态,复用函数名;
  • 动态多态: 派生类(子类)和虚函数实现运行时多态。

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

非多态示例:

class Animal
{
public:
	void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat :public Animal
{
public:
	void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

class Dog :public Animal
{
public:

	void speak()
	{
		cout << "小狗在说话" << endl;
	}

};

//希望实现每次调用函数,给什么形参就调用谁的函数
//提供一个公共接口,否则就需要多个函数才可实现对每个动物叫的实现
void DoSpeak(Animal& animal)
{
	animal.speak();
}

void test01()
{
	Cat cat;
	DoSpeak(cat);

	Dog dog;
	DoSpeak(dog);
}

在这里插入图片描述
但由输出来看与我们的想法不同,每次都调用动物类的说话函数(speak)。

原因:
由于doSpeak函数在编译阶段就确定了函数地址,即地址早绑定——静态多态

对于此种问题可以采用多态技术进行解决:

要实现调用谁,谁执行就需要使得doSpeak函数地址在运行阶段进行绑定,实现地址晚绑定——动态多态。

多态示例:
只需要在父类speak函数前添加关键字-virtual,形成虚函数。

class Animal
{
public:
	//Speak函数就是虚函数
	//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编

在这里插入图片描述

多态满足条件
1、有继承关系(继承是多态实现的基础);
2、子类重写父类中的虚函数:

  • 也就是与父类虚函数一样(关键字virtual可加可不加),返回值类型 函数名 形参列表完全一致

多态使用: 父类指针或引用指向子类对象,即:
void DoSpeak(Animal& animal)-父类引用;
void DoSpeak(Animal* animal)-父类指针。

7.1.2 多态的基本原理

class Animal
{
public:
	//Speak函数就是虚函数
	//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat :public Animal
{
public:
	void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

class Dog :public Animal
{
public:

	void speak()
	{
		cout << "小狗在说话" << endl;
	}

};

//希望实现每次调用函数,给什么形参就调用谁的函数
//提供一个公共接口,否则就需要多个函数才可实现对每个动物叫的实现
void DoSpeak(Animal& animal)
{
	animal.speak();
}

void test01()
{
	Cat cat;
	DoSpeak(cat);
}

对于多态来说,需要满足两个条件,继承和重写虚函数。

  • 在子类未重写父类的虚函数时:
    在这里插入图片描述
    子类(猫类)由于继承父类,因此在子类未重写父类虚函数时,子类中将父类中的虚函数完全复制一份。
    (图中,父类存储一个指针(即vfptr),其指向vftable-虚函数表,表中放着虚函数的地址)
    下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
    在这里插入图片描述
    在这里插入图片描述

  • 子类重写父类虚函数
    在这里插入图片描述
    子类虚函数表内部就替换成子类的(虚)函数地址,子类就有了自己的(虚)函数,从而可以进行调用自己的函数。
    下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
    在这里插入图片描述

  • 父类指针或引用指向子类对象

void DoSpeak(Animal& animal)
{
	animal.speak();
}
//父类函数传入子类对象:DoSpeak(cat);
//Animal& animal=Cat;
//再进行调用父类函数:animal.speak();
//由于Animal& animal=Cat,指向子类对象(Cat),编译器就会在子类的虚函数表中去找内部的函数地址,从而调用子类函数。

7.2 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象;
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
    示例:
class Base
{
public:
	//纯虚函数
	//类中只要有一个纯虚函数就称为抽象类
	virtual void func() = 0;
};

class Son :public Base
{
public:
	//子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func()
	{
		cout << "func调用" << endl;
	};
};

void test01()
{
	Base* base = NULL;
	//base = new Base; // 错误,抽象类无法实例化对象
	//父类指针指向子类对象
	base = new Son;
	base->func();//通过父类指针调用子类函数
	delete base;//记得销毁
}

7.3 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
问题示例:

class Animal {
public:

	Animal()
	{
		cout << "Animal 构造函数调用!" << endl;
	}
	virtual void Speak() = 0;

	~Animal()
	{
		cout << "Animal析构函数调用!" << endl;
	}
};

class Cat : public Animal {
public:
	Cat(string name)
	{
		cout << "Cat构造函数调用!" << endl;
		m_Name = new string(name);
	}
	virtual void Speak()
	{
		cout << *m_Name << "小猫在说话!" << endl;
	}
	~Cat()
	{
		cout << "Cat析构函数调用!" << endl;
		if (this->m_Name != NULL) {
			delete m_Name;
			m_Name = NULL;
		}
	}

public:
	string* m_Name;
};

void test01()
{
	Animal* animal = new Cat("Tom");//父类指针指向子类对象
	animal->Speak();
	delete animal;//释放
}

在这里插入图片描述

由此可知,对于子类在堆区开辟的空间,对父类指针释放未能对子类空间释放,造成内存泄漏。(未能调用子类析构函数)

解决方式
将父类中的析构函数改为虚析构或者纯虚析构

  • 虚析构和纯虚析构共性

    • 可以解决父类指针释放子类对象;
    • 都需要有具体的函数实现
  • 虚析构和纯虚析构区别:

    • 如果是纯虚析构,该类属于抽象类,无法实例化对象。

虚析构语法:

virtual ~类名(){}

纯虚析构语法:(类内声明,类外实现

类内声明: virtual ~类名() = 0;

类外实现:类名::~类名(){}
示例:

  • 虚析构函数
class Animal {
public:

	Animal()
	{
		cout << "Animal 构造函数调用!" << endl;
	}
	//纯虚函数
	virtual void Speak() = 0;

	法一:析构函数加上virtual关键字,变成虚析构函数
	virtual~Animal()
	{
		cout << "Animal虚析构函数调用!" << endl;
	}
};

在这里插入图片描述

  • 纯虚析构函数
class Animal {
public:

	Animal()
	{
		cout << "Animal 构造函数调用!" << endl;
	}
	//纯虚函数
	virtual void Speak() = 0;

	//法二:类内声明:纯虚析构函数
	//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
	virtual ~Animal() = 0;
};

//法二:类外实现:
Animal::~Animal()
{
	cout << "Animal 纯虚析构函数调用!" << endl;
}

在这里插入图片描述
注意:纯虚析构函数不要忘了类外实现,否则会出现下面的错误;
在这里插入图片描述

总结:

? 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象;

? 2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构;

? 3. 拥有纯虚析构函数的类也属于抽象类。

文章来源:https://blog.csdn.net/weixin_63685622/article/details/135679573
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。