面向对象的三大特性

一、封装性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(私有权限)。三种权限的控制规则如下:

公共权限public: 类内可以访问 类外可以访问保护权限protected:类内可以访问,类外除子类不可以访问私有权限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 二义性

问题:羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。解决方式:通过使用作用域对数据进行区分。

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 资源浪费

问题:草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。解决方式:通过使用虚继承的方式可以解决此问题。让羊和驼都以虚继承的方式来继承动物类,羊驼以正常的继承方式继承羊类和驼类。此时,动物类成为虚基类。虚继承同样可以解决二义性的问题。

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++中的多态分为两类:

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

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

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

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

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;

}

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

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

2.3 个人理解

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

3. 纯虚函数和抽象类

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

3.1 纯虚函数语法

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

3.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 虚析构和纯虚析构异同

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

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

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 父类使用虚析构函数

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

相关文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。