【C++核心】面向对象的三大特性

news2024/11/25 3:07:07

面向对象的三大特性

  • 一、封装性
    • 1. 封装性的意义
      • 1.1 表现事物
      • 1.2 权限控制
      • 1.3 成员属性设置为私有
    • 2. 封装性的衍生知识
      • 2.1 struct和class区别
      • 2.2 友元
        • 2.2.1 全局函数做友元
        • 2.2.2 类做友元
        • 2.2.3 成员函数做友元
  • 二、继承性
    • 1. 继承的语法
    • 2. 继承方式
    • 3. 继承中的对象模型
      • 3.1 说明
      • 3.2 验证
    • 4. 继承同名成员处理方式
      • 4.1 同名属性
      • 4.2 同名函数
        • 4.2.1 参数相同
        • 4.2.2 参数不同
    • 4. 继承同名静态成员处理方式
    • 5. 继承中构造和析构顺序
      • 5.1 顺序说明
      • 5.2 个人理解
    • 6. 多继承语法
    • 7. 菱形继承
      • 7.1 菱形继承的概念
      • 7.2 菱形继承的问题与解决
        • 7.2.1 二义性
        • 7.2.2 资源浪费
      • 7.3 虚继承解决菱形继承问题的原理
    • 8. Java和C++在继承性方面易混淆的区别
  • 三、多态性
    • 1. 多态的介绍
    • 2. 多态的实现
      • 2.1 失败的多态
      • 2.2 成功的多态
      • 2.3 个人理解
    • 3. 纯虚函数和抽象类
      • 3.1 纯虚函数语法
      • 3.2 抽象类
    • 4. 虚析构和纯虚析构
      • 4.1 虚析构和纯虚析构语法
      • 4.2 虚析构和纯虚析构异同
      • 4.3 代码示例
      • 4.4 个人理解
        • 4.4.1 父类未使用虚析构函数
        • 4.4.2 父类使用虚析构函数

C++认为万事万物都皆为对象,对象上有其属性和行为。例如:人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…;车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…。

具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类。

C++面向对象的三大特性为:封装、继承、多态。

一、封装性

1. 封装性的意义

封装是C++面向对象三大特性之一。封装性的存在主要有两大意义:①将属性和行为作为一个整体,表现生活中的事物;②将属性和行为加以权限控制。

1.1 表现事物

C++中封装性的声明语法如下:

class 类名{ 
访问权限:
	属性  / 行为  
}

举例:设计圆类,求圆的周长

//圆周率
const double PI = 3.14;

//1、封装的意义
//将属性和行为作为一个整体,用来表现生活中的事物

//封装一个圆类,求圆的周长
//class代表设计一个类,后面跟着的是类名
class Circle
{
public:  //访问权限  公共的权限

	//属性
	int m_r;//半径

	//行为
	//获取到圆的周长
	double calculateZC()
	{
		//2 * pi  * r
		//获取圆的周长
		return  2 * PI * m_r;
	}
};

int main() {

	//通过圆类,创建圆的对象
	// c1就是一个具体的圆
	Circle c1;
	c1.m_r = 10; //给圆对象的半径 进行赋值操作

	//2 * pi * 10 = = 62.8
	cout << "圆的周长为: " << c1.calculateZC() << endl;

	system("pause");

	return 0;
}

1.2 权限控制

类在设计时,可以把属性和行为放在不同的权限下,加以控制。访问权限有三种:public(公共权限 )、protected(保护权限)、private(私有权限)。三种权限的控制规则如下:

  1. 公共权限public: 类内可以访问 类外可以访问
  2. 保护权限protected:类内可以访问,类外除子类不可以访问
  3. 私有权限private: 类内可以访问,类外不可以访问

举例:

//三种权限
//公共权限  public     类内可以访问  类外可以访问
//保护权限  protected  类内可以访问  类外不可以访问
//私有权限  private    类内可以访问  类外不可以访问

class Person
{
	//姓名  公共权限
public:
	string m_Name;

	//汽车  保护权限
protected:
	string m_Car;

	//银行卡密码  私有权限
private:
	int m_Password;

public:
	void func()
	{
		m_Name = "张三";
		m_Car = "拖拉机";
		m_Password = 123456;
	}
};

int main() {

	Person p;
	p.m_Name = "李四";
	//p.m_Car = "奔驰";  //保护权限类外访问不到
	//p.m_Password = 123; //私有权限类外访问不到

	system("pause");

	return 0;
}

1.3 成员属性设置为私有

我们设计类时,可以将所有成员属性设置为私有,可以自己控制读写权限。对于写权限,我们可以检测数据的有效性。

class Person {
public:

	//姓名设置可读可写
	void setName(string name) {
		m_Name = name;
	}
	string getName()
	{
		return m_Name;
	}


	//获取年龄 
	int getAge() {
		return m_Age;
	}
	//设置年龄
	void setAge(int age) {
		if (age < 0 || age > 150) {
			cout << "你个老妖精!" << endl;
			return;
		}
		m_Age = age;
	}

	//情人设置为只写
	void setLover(string lover) {
		m_Lover = lover;
	}

private:
	string m_Name; //可读可写  姓名
	
	int m_Age; //只读  年龄

	string m_Lover; //只写  情人
};


int main() {

	Person p;
	//姓名设置
	p.setName("张三");
	cout << "姓名: " << p.getName() << endl;

	//年龄设置
	p.setAge(50);
	cout << "年龄: " << p.getAge() << endl;

	//情人设置
	p.setLover("苍井");
	//cout << "情人: " << p.m_Lover << endl;  //只写属性,不可以读取

	system("pause");

	return 0;
}

2. 封装性的衍生知识

2.1 struct和class区别

在C++中,struct和class唯一的区别就在于默认的访问权限不同:struct默认权限为公共,class默认权限为私有。

2.2 友元

生活中你的家有客厅(Public),有你的卧室(Private)。客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的好闺蜜好基友进去。

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。换句话说,友元的目的就是让一个函数或者类 访问另一个类中私有成员。友元的关键字为friend

友元的一共有三种实现:① 全局函数做友元 ②类做友元 ③成员函数做友元。

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

public:

	Building()
	{
		this->m_SittingRoom = "客厅";
		this->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);
}

int main(){

	test01();

	system("pause");
	return 0;
}
2.2.2 类做友元
class Building;
class goodGay
{
public:

	goodGay();
	void visit();

private:
	Building *building;
};


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

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::visit()
{
	cout << "好基友正在访问" << building->m_SittingRoom << endl;
	cout << "好基友正在访问" << building->m_BedRoom << endl;
}

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

}

int main(){

	test01();

	system("pause");
	return 0;
}
2.2.3 成员函数做友元

class Building;
class goodGay
{
public:

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

private:
	Building *building;
};


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

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::visit()
{
	cout << "好基友正在访问" << building->m_SittingRoom << endl;
	cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void goodGay::visit2()
{
	cout << "好基友正在访问" << building->m_SittingRoom << endl;
	//cout << "好基友正在访问" << building->m_BedRoom << endl;
}

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

}

int main(){
    
	test01();

	system("pause");
	return 0;
}

二、继承性

继承是面向对象三大特性之一。有些类与类之间存在特殊的关系,例如下图:
在这里插入图片描述
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码。

1. 继承的语法

class A : public B; 

上述代码中,A类称为子类或派生类;B类称为父类或基类。

派生类中的成员,包含两大部分: 一类是从基类继承过来的,一类是自己增加的成员。

从基类继承过过来的表现其共性,而新增的成员体现了其个性。

2. 继承方式

继承方式一共有三种,分别是公共继承、保护继承、私有继承。三种方式的区别如下图所示:在这里插入图片描述
注意,这里的不可访问只是因为权限设置无法直接调用私有成员(包括属性和方法)进行访问,但是实际上子类也继承了父类的私有成员,可以通过其他方式进行访问。这点后面会陆续说到。

class Base1
{
public: 
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

//公共继承
class Son1 :public Base1
{
public:
	void func()
	{
		m_A; //可访问 public权限
		m_B; //可访问 protected权限
		//m_C; //不可访问
	}
};

void myClass()
{
	Son1 s1;
	s1.m_A; //其他类只能访问到公共权限
}

//保护继承
class Base2
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class Son2:protected Base2
{
public:
	void func()
	{
		m_A; //可访问 protected权限
		m_B; //可访问 protected权限
		//m_C; //不可访问
	}
};
void myClass2()
{
	Son2 s;
	//s.m_A; //不可访问
}

//私有继承
class Base3
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class Son3:private Base3
{
public:
	void func()
	{
		m_A; //可访问 private权限
		m_B; //可访问 private权限
		//m_C; //不可访问
	}
};
class GrandSon3 :public Son3
{
public:
	void func()
	{
		//Son3是私有继承,所以继承Son3的属性在GrandSon3中都无法访问到
		//m_A;
		//m_B;
		//m_C;
	}
};

3. 继承中的对象模型

3.1 说明

子类中私有成员只是被隐藏了,无法直接访问到,但是还是会继承下去。

3.2 验证

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

int main() {

	test01();

	system("pause");

	return 0;
}

在这里插入图片描述

利用【开发人员命令提示符工具】查看:打开工具窗口后,定位到当前C++文件的盘符,输入:

cl /d1 reportSingleClassLayout查看的类名   所属文件名

即可查看所需要查看类的内部数据,效果图如下:
在这里插入图片描述

4. 继承同名成员处理方式

4.1 同名属性

当子类定义了与父类同名的属性以后,子类会将父类的同名属性进行隐藏。此时直接访问同名属性时,调用的是子类定义的同名属性。

在子类内部,可以通过属性名子类::属性名的方式对只属于子类的同名属性进行调用;可以通过父类::属性名的方式对从父类继承下来的属于子类的同名属性进行调用。

在子类外部,可以可以通过子类对象.属性名子类对象.子类::属性名的方式对只属于子类的同名属性进行调用;可以通过子类对象.父类::属性名的方式对从父类继承下来的属于子类的同名属性进行调用。

class Base
{
public:
	int m_A = 10;
};

//公共继承
class Son :public Base
{
public:
	int m_A = 100;

public:
	void print_info() {
		//子类内
		cout << Base::m_A << endl;
		cout << Son::m_A << endl;
		cout << m_A << endl;
	}
};

void test01()
{
	Son s;
	s.print_info();
	//子类外
	cout << s.Base::m_A << endl;
	cout << s.Son::m_A << endl;
	cout << s.m_A << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}

4.2 同名函数

当子类定义了与父类同名的函数以后,子类会将父类的同名函数性进行隐藏。细分来说,可以再分为两种情况进行讨论:① 父类中,与子类的同名函数参数和子类相同;②父类中,与子类同名的函数参数和子类不同。

4.2.1 参数相同

在子类内部,可以通过函数名子类::函数名的方式对只属于子类的同名函数进行调用;可以通过父类::函数性名的方式对从父类继承下来的属于子类的同名函数进行调用。

在子类外部,可以通过子类对象.函数名子类对象.子类::函数名的方式对只属于子类的同名函数进行调用;可以通过子类对象.父类::属性名的方式对从父类继承下来的属于子类的同名函数进行调用。

class Base
{
public:
	void print_str() {
		cout << "str1" << endl;
	}
};

//公共继承
class Son :public Base
{
public:
	void print_str() {
		cout << "str2" << endl;
	}

	void print_info() {
		//子类内
		Base::print_str();
		Son::print_str();
		print_str();
	}
};

void test01()
{
	Son s;
	s.print_info();
	cout << "-----------------------"<< endl;
	//子类外
	s.Base::print_str();
	s.Son::print_str();
	s.print_str();
}

int main() {

	test01();

	system("pause");

	return 0;
}
4.2.2 参数不同

在子类内部,由于子类的同名函数把父类所有名称相同的函数,都进行了隐藏,因此,无法通过函数名(参数)的方式对子类的继承的不同参数的同名函数进行调用;可以通过父类::函数名(参数)的方式对从父类继承下来的属于子类的同名函数进行调用。

在子类外部,也同样无法通过子类对象.函数名(参数)的方式对子类的继承的不同参数的同名函数进行调用,只能通过子类对象.父类::函数名(参数)的方式进行调用。

class Base
{
public:
	void print_str() {
		cout << "str1" << endl;
	}

	void print_str(string& str) {
		cout << str << endl;
	}
};

//公共继承
class Base
{
public:
	void print_str() {
		cout << "str1" << endl;
	}

	void print_str(string& str) {
		cout << str << endl;
	}
};

//公共继承
class Son :public Base
{	
public:
	void print_str() {
		cout << "str2" << endl;
	}

	void print_info() {
		string name = "jerry";
		//子类内
		//失败
		//print_str(name);
		Base::print_str(name);
	}
};

void test01()
{
	Son s;
	s.print_info();
	cout << "-----------------------" << endl;
	//子类外
	string str = "hello";
	//失败
	//s.print_str(str);
	s.Base::print_str(str);

}

int main() {

	test01();

	system("pause");

	return 0;
}

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

静态成员和非静态成员出现同名,处理方式一致,只不过有两种访问的方式:通过对象和通过类名。

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

int Base::m_A = 100;

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

	static void call_static() {
		cout << "Son  下 m_A = " << m_A << endl;
		cout << "Son  下 m_A = " << Son::m_A << endl;
		cout << "Base 下 m_A = " << Base::m_A << endl;

		Son::func();
		Son::Son::func();
		Son::Base::func();
		Son::Base::func(100);
	}
};

int Son::m_A = 200;

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

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

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

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

//子类中访问
void test3() 
{
	Son::call_static();
}

int main() {

	test01();
	test02();
	test3();
	system("pause");

	return 0;
}

5. 继承中构造和析构顺序

5.1 顺序说明

子类继承父类后,当创建子类对象,也会调用父类的构造函数。构造函数的调用顺序为:先调用父类构造函数,再调用子类构造函数。析构函数的调用顺序为:先调用子类析构函数,再调用父类析构函数。

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

int main() {

	test01();

	system("pause");

	return 0;
}

5.2 个人理解

c++创建一个子类对象时会调用父类的构造函数,但不会创建另外一个父类对象,只是初始化子类中属于父类的成员。

创建一个对象的时候,发生了两件事情,一是分配对象所需的内存,二是调用构造函数进行初始化。子类对象包含从父类对象继承过来的成员,实现上来说,一般也是子类的内存区域中有一部分就是父类的内存区域。调用父类构造函数的时候,这块父类对象的内存区域就被初始化了。为了避免未初始化的问题,语法强制子类调用父类构造函数。

6. 多继承语法

C++允许一个类继承多个类,语法如下:

class 子类 :继承方式 父类1 , 继承方式 父类2...

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

class Base1 {
public:
	Base1()
	{
		m_A = 100;
	}
public:
	int m_A;
};

class Base2 {
public:
	Base2()
	{
		m_A = 200;  //开始是m_B 不会出问题,但是改为mA就会出现不明确
	}
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;
}

int main() {

	test01();

	system("pause");

	return 0;
}

7. 菱形继承

7.1 菱形继承的概念

在这里插入图片描述
菱形继承:两个派生类继承同一个基类,某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承。

7.2 菱形继承的问题与解决

7.2.1 二义性
  1. 问题:羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
  2. 解决方式:通过使用作用域对数据进行区分。
class Animal
{
public:
	int m_Age;
};

class Sheep : public Animal {};
class Tuo : public Animal {};
class SheepTuo : public Sheep, public Tuo {};

void test01()
{
	SheepTuo st;
	st.Sheep::m_Age = 100;
	st.Tuo::m_Age = 200;

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


int main() {

	test01();

	system("pause");

	return 0;
}
7.2.2 资源浪费
  1. 问题:草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
  2. 解决方式:通过使用虚继承的方式可以解决此问题。让羊和驼都以虚继承的方式来继承动物类,羊驼以正常的继承方式继承羊类和驼类。此时,动物类成为虚基类。虚继承同样可以解决二义性的问题。
class Animal
{
public:
	int m_Age;
};

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

void test01()
{
	SheepTuo st;
	st.Sheep::m_Age = 100;
	st.Tuo::m_Age = 200;

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


int main() {

	test01();

	system("pause");

	return 0;
}

7.3 虚继承解决菱形继承问题的原理

采用正常的继承方式,羊驼类内的结构如图,可以看到羊驼类中有两份m_Age数据,一份来源于羊类,一份来源于驼类。
在这里插入图片描述
采用虚继承的方式,羊驼类内的结构如图,可以发现,此时羊驼类内部只有一个m_Age数据,从羊类和驼类中继承的都是vbptr。vbptr是Vitrual Base Pointer,即虚基类指针的意思,这个指针会指向羊驼类中各自的vbtable。vbtable是Virtual Base Table,即虚基类表格的意思,该表格记录了偏移量。vbptr所在的位置+vbtable中的偏移量=实际存放值的位置,下图中羊类的vbptr位置为4,羊类的vbtable记录的偏移量为8,0+8=8,8就是m_Age的位置。
在这里插入图片描述
可以看出,此时羊驼类中只有一个m_Age。无论通过何种方式访问m_Age,都是在访问同一个数据。由此,也就解决了菱形继承带来的二义性和资源浪费的问题。

8. Java和C++在继承性方面易混淆的区别

JavaC++
Java中只有一种继承方式C++中有三种继承方式
Java中子类会重写父类中同名同参数的方法,而与父类中同名不同参数的方法是重载的关系C++中会隐藏父类中所有同名方法(不管参数相同不相同)

三、多态性

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

1. 多态的介绍

C++中的多态分为两类:

  1. 静态多态: 函数重载和运算符重载属于静态多态,复用函数名。
  2. 动态多态: 派生类和虚函数实现运行时多态。

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

  1. 静态多态的函数地址早绑定:编译阶段确定函数地址。
  2. 动态多态的函数地址晚绑定:运行阶段确定函数地址。

动态多态的本质是父类指针或引用指向子类对象。本部分所介绍的多态,指的都是动态多态。因此,为了方便起见,下文只要说到多态的地方,都代表动态多态。

2. 多态的实现

如果想要实现多态,必须要满足以下两个条件:

  1. 两个类有继承关系
  2. 子类重写父类中的虚函数

重写和重载是两种概念:

  1. 重写:函数返回值类型、函数名、参数列表完全一致称为重写
  2. 重载:函数的参数名相同,而参数列表不同

2.1 失败的多态

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


int main() {

	test01();

	system("pause");

	return 0;
}

此时,屏幕输出的都是【动物在说话】,并没有真正的实现多态。这是因为DoSpeak属于地址早绑定,该函数在编译时已经确定了函数的地址。

2.2 成功的多态

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

class Cat :public Animal
{
public:
	void speak()//也可以加上virtual关键字,不加也没事
	{
		cout << "小猫在说话" << endl;
	}
};

class Dog :public Animal
{
public:

	void speak()//也可以加上virtual关键字,不加也没事
	{
		cout << "小狗在说话" << endl;
	}

};

void DoSpeak(Animal& animal)
{
	animal.speak();
}

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


	Dog dog;
	DoSpeak(dog);
}


int main() {

	test01();

	system("pause");

	return 0;
}

此时,屏幕输出的是【小猫在说话】和【小狗在说话】,实现了真正的实现多态。那么为什么函数定义为虚函数就可以实现多态性呢?
在这里插入图片描述

  1. 当函数只是普通的函数时,类中并不存储该函数。
  2. 当函数是虚函数时,类中会存储一个vfptr(Virtual Function Pointer),即虚函数(表)指针,该指针会指向vftable(Virtual Function Table)虚函数表,表中会记录虚函数的地址&类名::函数(在上述例子中就是&Animal::speak)。
  3. 子类继承该类但并未重写虚方法时,子类也会有一个和父类完全相同的vfptr和vftable。
  4. 子类继承该类并重写虚方法以后,子类会将虚函数表内部存放的地址进行替换,替换成子类的虚函数地址(在上述例子中就是&Cat::speak)。
  5. 当父类的指针或引用指向子类对象的时候,就会去子类对象的虚函数表中找对应的函数,也就发生了多态。

2.3 个人理解

  1. 发生多态,最核心的原因就是地址的晚绑定,即在运行阶段才能真正的确定函数的地址。
  2. 在未采用虚函数时,类中不会存放函数的地址,函数的地址是类外的某个具体内存。此时函数地址的确认不依赖于具体的对象,因此直接能够在编译阶段完成函数地址的确认,所以无法发生多态。
  3. 采用虚函数以后,类中会存放函数的地址指针,地址指针指向虚函数表,虚函数表指向真正运行的函数地址。此时,函数地址的确认要依赖于具体的对象,所以无法在编译阶段完成函数地址的确认,必须要在运行时,根据具体指向的对象找到函数的地址,进行调用,因此才能实现多态。

3. 纯虚函数和抽象类

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

3.1 纯虚函数语法

virtual 返回值类型 函数名 (参数列表)= 0 ;

3.2 抽象类

当类中有了纯虚函数,这个类也称为抽象类。抽象类具有以下特点:

  1. 无法实例化对象。
  2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
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;//记得销毁
}

int main() {

	test01();

	system("pause");

	return 0;
}

4. 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,此时,就需要将父类中的析构函数改为虚析构或者纯虚析构。

4.1 虚析构和纯虚析构语法

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;
类名::~类名(){}//纯虚析构需要实现

4.2 虚析构和纯虚析构异同

虚析构和纯虚析构共性:

  1. 可以解决父类指针释放子类对象。
  2. 都需要有具体的函数实现。

虚析构和纯虚析构区别:

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

4.3 代码示例

class Animal {
public:

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

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


	virtual ~Animal() = 0;
};

Animal::~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;
}

int main() {

	test01();

	system("pause");

	return 0;
}

4.4 个人理解

4.4.1 父类未使用虚析构函数

如果在父类没有使用虚析构函数的时候对父类指针进行了释放,那么会调用父类的析构函数(早绑定),此时子类的空间会被释放(地址指向的就是子类的地址),但不会调用子类的析构函数。在释放子类的空间时,指针自身并不关心它所指向的对象有哪些成员变量,因此子类的成员变量也会被一并释放。然而,如果这个子类有任何动态分配的内存(例如使用 new 创建的数组或者其他对象),那么这些动态分配的内存会在子类对象被释放时不会被自动回收,最终会造成内存泄漏。

4.4.2 父类使用虚析构函数

如果在父类使用虚析构函数的时候对父类指针进行了释放,那么会调用子类的析构函数(晚绑定),此时子类的空间会被释放,如果这个子类有任何动态分配的内存,我们可以在子类空间中人为进行释放来避免内存泄露。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1604866.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

良友:献上今天(打开心窗说亮话)- 情绪的秘密

目录 一 二 三 四 五 六 七 八 九 十 十一 十二 十三

python中的列表、元组、字典、集合(集合篇)

数据类型定义符号访问元素是否可变是否重复是否有序列表 [ ]索引可变可重复有序元组&#xff08;&#xff09;索引不可变可重复有序字典{key&#xff1a;value}键可变可重复无序集合{ }可变不可重复无序 基本概念 python语言中的集合是无序的、可变的容器类对象&#xff0c;所…

名称和命名空间

&#x1f4d5;作者简介&#xff1a; 过去日记&#xff0c;致力于Java、GoLang,Rust等多种编程语言&#xff0c;热爱技术&#xff0c;喜欢游戏的博主。 &#x1f4d8;相关专栏Rust初阶教程、go语言基础系列、spring教程等&#xff0c;大家有兴趣的可以看一看 &#x1f4d9;Jav…

【微信小程序从入门到精通(项目实战)】——微电影小程序

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

java的JDK动态代理

JDK动态代理是指&#xff1a;代理类实例在程序运行时&#xff0c;由JVM根据反射机制动态的生成。也就是说代理类不是用户自己定义的&#xff0c;而是由JVM生成的。 由于其原理是通过Java反射机制实现的&#xff0c;所以在学习前&#xff0c;要对反射机制有一定的了解。传送门&…

网络安全的守护者:防火墙的五个主要功能解析

防火墙是一种网络安全设备&#xff0c;用于保护计算机网络免受未经授权的访问、攻击和恶意软件的侵害。它通过监控、过滤和控制网络流量&#xff0c;实施安全策略&#xff0c;防止不安全的数据包进入或离开受保护的网络。 防火墙的五个主要功能&#xff1a; 1. 访问控制&#…

CleanMyMac破解版官方试用补丁器下载2024最新

CleanMyMac是一款由MacPaw公司研发的Mac清理工具。以下是对CleanMyMac的详细介绍&#xff1a; 一、主要功能 一键智能清理&#xff1a;CleanMyMac能智能扫描Mac磁盘空间中的垃圾文件&#xff0c;这包括但不限于识别重复文件、无用语言安装包、iTunes垃圾、重复照片、邮件附件…

15V转5V3A降压同步WT6019

15V转5V3A降压同步WT6019 WT6019则是一种高效的同步降压转换器。它可以将15V的输入电压稳定转换为5V的输出电压&#xff0c;并保证最大3A的电流输出。这种转换器的核心在于其内部的功率MOSFET&#xff0c;它能够以较低的导通电阻和快速的开关速度&#xff0c;实现高效率的能量…

ETLCloud中多并行分支运行的设计技巧

在大数据处理领域&#xff0c;ETL&#xff08;Extract, Transform, Load&#xff09;流程是至关重要的一环&#xff0c;它涉及数据的提取、转换和加载&#xff0c;以确保数据的质量和可用性。而在ETL流程中&#xff0c;多并行分支的运行设计是一项关键技巧&#xff0c;可以有效…

Facebook隐私保护:用户数据安全的关键挑战

在数字化时代&#xff0c;数据已成为最宝贵的资源之一。社交媒体平台如Facebook为用户提供了便捷的交流和信息分享工具&#xff0c;但同时也面临着如何保护用户数据安全和隐私的挑战。本文将深入探讨Facebook在数据安全方面面临的关键挑战&#xff0c;以及其如何应对这些挑战&a…

电商数据接口开发|淘宝商品接口|天猫商品接口|京东商品接口|拼多多商品接口|API接口申请指南

电商数据接口开发涉及到多个电商平台&#xff0c;包括淘宝、天猫、京东和拼多多等。这些平台都提供了丰富的API接口&#xff0c;以便开发者能够获取商品信息、订单数据等&#xff0c;从而构建出各种电商应用和服务。 1.请求方式&#xff1a;HTTP POST GET &#xff08;复制薇&…

一套java+ spring boot与 vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例

一套java spring boot vue mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例 UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术&#xff0c;它不采用正弦载波&#xff0c;而是利用纳秒级的非正弦波窄脉冲传输数据&#xff0c;因此其所占的频谱范围很宽。一套…

Git学习与码云实战

Git学习与码云实战 Git安装 概述&#xff1a; Git 是一个开源的分布式版本控制系统&#xff0c;可以有效、高速的处理从很小到非常大的项目版本管理&#xff0c;是目前使用范围最广的版本管理工具。 下载安装&#xff1a; 下载地址&#xff1a;https://git-scm.com/ 下载后傻瓜…

diffusion model 简单demo

参考自&#xff1a; Probabilistic Diffusion Model概率扩散模型理论与完整PyTorch代码详细解读 diffusion 简单demo 扩散模型之DDPM 核心公式和逻辑 q_x 计算公式&#xff0c;后面会用到&#xff1a; 推理&#xff1a; 代码 import matplotlib.pyplot as plt import nump…

08-GPtimer

通用定时器 &#xff08;GPTimer&#xff09; 通用定时器简介 通用定时器可用于准确设定时间间隔、在一定间隔后触发&#xff08;周期或非周期的&#xff09;中断或充当硬件时钟。如下图所示&#xff0c;ESP32-S3 包含两个定时器组&#xff0c;即定时器组 0 和定时器组 1。每…

力扣练习题(2024/4/14)

1接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;6 解释&#xff1a;上面是由数组 [0,1,0,2,1,0,1,3,2…

vue3 -- 项目使用自定义字体font-family

在Vue 3项目中使用自定义字体(font-family)的方法与在普通的HTML/CSS项目中类似。可以按照以下步骤进行操作: 引入字体文件: 首先,确保你的字体文件(通常是.woff、.woff2、.ttf等格式)位于项目中的某个目录下,比如src/assets/font/。 在全局样式中定义字体: 在你的全局…

mysql常见语法操作笔记

1. 数据库的基本操作 1.1. MYSQL登录与退出 D:\phpstudy_pro\Extensions\MySQL5.7.26\bin 输入 mysql -uroot -proot -h127.0.0.1 退出的三种方法 mysql > exit; mysql > quit; mysql > \q; 1.2. MYSQL数据库的一些解释 注意&#xff1a;数据库就相当于文件夹 …

IDEA 控制台中文乱码 4 种解决方案

前言 IntelliJ IDEA 如果不进行相关设置&#xff0c;可能会导致控制台中文乱码、配置文件中文乱码等问题&#xff0c;非常影响编码过程中进行问题追踪。本文总结了 IDEA 中常见的中文乱码解决方法&#xff0c;希望能够帮助到大家。 IDEA 中文乱码 解决方案 一、设置字体为支…

挣钱新玩法,一文带你掌握流量卡推广秘诀

手机流量卡推广项目是什么&#xff1f;听名字我相信大家就已经猜出来了&#xff0c;就是三大运营商为了开发新用户&#xff0c;发起的有奖推广活动&#xff0c;也是为了长期黏贴用户。在这个活动中&#xff0c;用户通过我们的渠道&#xff0c;就能免费办理低套餐流量卡&#xf…