纵有疾风起,人生不言弃。本文篇幅较长,如有错误请不吝赐教,感谢支持。
💬文章目录
- 一.类和对象的概念
- ①什么是对象?
- ②抽象和类
- 1.类的基本概念
- 2.类的声明与定义:
- 3.对象的创建与使用
- 二.类的封装
- ①为什么有封装?
- ②封装和访问权限
- ③尽量把属性设置为私有权限
- ④C++中结构体(struct)和类(class)的区别:
- 三.对象的构造函数和析构函数
- ①对象的初始化和清理
- ②构造函数和析构函数
- 1.构造函数和析构函数作用和语法
- 2.构造函数点和析构函数的注意事项
- ③构造函数的分类
- 1.构造拷贝函数:
- 2.拷贝构造函数中形参要用引用
- 3.拷贝构造函数调用时机
- ④构造函数调用规则
- 四.多个对象的构造函数和析构函数
- ①构造函数的初始化列表
- 1.初始化列表的定义
- 2.作用
- 3.注意事项
- ②对象成员的构造函数和析构函数调用顺序
- 五.对象的深浅拷贝
- ①浅拷贝
- ②深拷贝
- 六.动态对象创建
- ① C动态分配内存方法
- ②new操作符
- ③delete操作符
- ④用于数组的new和delete
- 1.new创建基础类型数组
- 2.new创建对象数组
- 3.delete void*可能出错,不会调用对象的析构函数
- 4.C和C++的申请和释放堆区空间不要混用
- 七.const修饰类的成员
- ①const修饰成员变量—常成员变量
- ②const修饰成员函数—常成员函数
- 八.static修饰类的成员
- ①static修饰成员变量-静态成员变量
- ②static修饰成员函数-静态成员函数
- ③const静态成员属性
- 九.C++的面向对象模型初探
- ①成员变量和函数的存储
- ②this指针
- 1.this指针工作原理
- 2.this指针的使用
- 十.有元
- ①什么是有元?
- 1.普通函数作为友元函数
- 2.其他类的成员函数作为友元函数
- 3.友元类
- ③友元的注意事项
- 十一.单例模式
- ①单例模式简介
- ②单例模式的作用?
- ③实现单例模式的思路
- ④案例
- ________________________
- 一.类的继承
- ①继承的概念
- ②派生类定义
- ③继承方式:
- 1.公有继承 (public)
- 2.保护继承(protected)
- 3.私有继承 (privated)
- 二、派生类
- ①派生类的构造函数与析构函数
- 1.继承中的构造函数和析构函数的调用顺序
- ②继承中同名成员的处理方法
- ③继承中的静态成员特性
- ④查看对象模型
- 三、多继承
- ①多继承概念
- ②菱形继承
- ③虚继承
- ④🔥虚继承实现原理🔥
- 四、派生类和基类的特殊关系
- ①类型的兼容性
- ②C++中的类型兼容情况
- ③拓展:向上和向下强制类型转换
- ____________________________________________________
- 一、多态
- ①什么是多态?
- ②一个问题?
- 二、问题解决思路
- ①静态联编与动态联编
- ②虚函数
- 三、多态小结
- ①多态作用
- ②多态发生的三个条件
- 四、🔥多态(虚函数)的实现原理🔥
- ①示例代码
- ②💬原理详细分析
- 五、纯虚函数和抽象类
- 六、虚析构函数纯虚析构函数
- ①一个问题
- ②虚析构函数
- ③纯虚析构函数
一.类和对象的概念
①什么是对象?
生活中有各种各样的事物,如人、动物、植物等在C++中将这些称为对象。
对象多种多样,各种对象的属性也不相同。
例如狗的品种,毛色,年龄等
各个对象都有自己的行为,例如球的弹跳和缩小;婴儿的唏哭睡眠走路和眨眼,汽车的加速、刹车和转弯,同时各个对象可能也有些共同之处,至少它们都是现实世界的组成部分。
人们是通过研究对象的属性和观察它们的行为而认识对象的。可以把对象分成很多类,每一大类中又可分成若干小类,也就是说,类是可以分层的。同一类的对象具有许多相同的属性和行为,
不同类的对象可能具有相同的属性和类似的行为。例如,婴儿和成人、小汽车和卡车等都有共同之处。
总之,对象是指一个具有属性和行为的实体,它在计算机程序中扮演着重要的角色,使得程序更加灵活、可维护和可扩展。
②抽象和类
类是对对象的抽象,生活中的对象充满复杂性,处理复杂性的方法之一就是简化和抽象。
1.类的基本概念
在C++中,用类来描述对象。
类是一种将抽象转换为用户定义类型的C++工具,它将对象的数据表示和操纵数据的方法组合成一个整洁的包。
1.数据(相当于对象的属性)—>存储数据的变量
2.操纵数据的方法(相当于对象的行为)—>函数
1.类(class)是自定义数据类型,是C语言的结构体进化而成的,类中可以包含两类成员――数据成员和成员函数,就是对象的两个部分。
2.对象是类实例化出的,用类这个自定义数据类型定义一个变量,这个变量就是对象。
2.类的声明与定义:
C++中类的定义包括类的声明和类的实现。
💠类的声明相当于函数声明,其中对类中的数据成员和成员函数进行了说明。
💠类的实现相当于函数实现,其中包括对数据成员进行的操作以及完成一些成员函数的实现。
类定义语法:
class 类名
{
权限控制符:成员函数/成员变量
};
⚠️类定义具体介绍:
关于类定义格式的具体介绍如下。
- class是定义类的关键字。
- 类名是类的标识符,其命名遵循标识符的命名规范。
- 类名后面的一对大括号,用于包含类的成员,类的所有成员要在这一对大括号中声明。类中可以定义成员变量(也称为属性)和成员函数(也称为方法),成员变量用于描述对象的属性,成员函数用于描述对象的行为。
- 声明类的成员时,通常需要使用权限控制符限定成员的访问规则,权限控制符包括public、protected和private,这三种权限控制符的权限依次递减。
- 类的定义和声明本质上也属于一条语句,因此在类定义结束的花括号后面带有一个分号( ; ),不加分号会引起编译错误。
通常情况下,类的成员函数在类中声明,在类外实现。在类外实现成员函数,必须在返回值之后、函数名之前加上所属的类作用域,即“类名::”,表示函数属于哪个类。在类外实现成员函数的格式如下所示:
示例1:
我们抽象出狗🐶这个类。
class Dog//类的关键字是class,
//用类这个关键字创建一个叫做Dog的自定义数据类型
{
public:
//公有权限,任何地方都可以访问
int a;//成员属性(成员变量),狗的年龄
//函数的声明
void bark();//声明狗叫成员行为(成员函数)
void eat();//声明狗吃饭的成员函数
};
void Dog::bark()//类外实现
{
cout << "狗汪汪叫" << endl;
}
void Dog:: eat()
{
cout << "狗吃狗粮" << endl;
}
3.对象的创建与使用
定义了类,就相当于定义了一个数据类型。类与int、char等数据类型的使用方法是一样的,可以定义变量,使用类定义的变量通常称为该类的对象。
对象的定义格式如下所示:
类名 对象名;
下面创建一个表示狗类Dog的对象,示例代码如下所示:
Dog jinmao;
Dog erha;
上述代码中,创建了类的对象jinmao和erha之后,系统就要为对象分配内存空间,用于存储对象成员。每个对象都有成员变量和成员函数两部分内容。成员变量标识对象的属性,比如创建两个Dog类对象jinmao和erha,由于两个🐶的品种、性别、年龄都不同,因此在创建对象时应当为每个对象分配独立的内存空间存储成员变量的值。
成员函数描述的是对象的行为,每个对象的行为都相同,比如🐶对象jinmao和erha都具有叫、吃饭行为。如果为每个对象的成员函数也分配不同的空间,则必然造成浪费。因此,C++用同一块空间存放同类对象的成员函数代码,每个对象调用同一段代码。
对象与成员之间的内存分配示意图:
对象的成员变量和成员函数的访问和C语言的结构体一致,可以通过“.”或指针–>访问其格式如下所示:
对象名.成员变量(成员函数)
p(指向要访问的对象)->成员变量(成员函数)
void test()
{
Dog jinmao;//m就是对象,用Maker这一自定义数据类型创建
jinmao.bark();//对象的成员就是类规定的成员
jinmao.eat();
Dog erha;
Dog*p = &erha;
p->bark();
p->eat();
//其访问方法和C语言的结构体一致。
}
二.类的封装
①为什么有封装?
我们编写程序的目的是为了解决现实中的问题,而这些问题的构成都是由各种对象组成,我们在计算机中要解决这种问题:首先要做就是要将这个问题的参与者:对象抽象到计算机程序中,也就是用程序语言表示现实的对象。那么现在问题是如何用程序语言来表示对象?
🖲比如我们要抽象出人和动物这两个对象,在C语言中,我们可以这么表示:
typedef struct Person
{
char name[64];//人的姓名
int age;//人的年龄
//以上都是人这一对象的属性
}Person;
void PersonEat(Person* person)
{
printf("%s在吃人吃的饭!\n",person->name);
//以上是人这一对象的行为-吃饭
}
typedef struct Aninal
{
char name[64];//动物名字
int age;//动物年龄
int type; //动物种类
//以上是动物这一对象的属性
}Ainmal;
void AnimalEat(Ainmal* animal)
{
printf("%s在吃动物吃的饭!\n", animal->name);
//以上是动物这一对象的行为-吃饭
}
int main()
{
Person person;
strcpy(person.name, "小明");
person.age = 30;
AnimalEat(&person);//行为和属性匹配错误
return 0;
}
「属性和行为不匹配,解决方法就是封装。」
定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的行为,这样我们就表示出来一个事物,在C语言中,行为和属性是分开的,也就是说吃饭这个属性不属于某类对象,而属于所有的共同的数据,所以不单单是PeopleEat可以调用Person数据,AnimalEat也可以调用Person数据,那么万一调用错误,将会导致问题发生。
从这个案例我们应该可以体会到,对象的属性和行为应该放在一起,一起表示一个具有属性和行为的对象。
假如某对象的某项属性不想被外界获知,比如说漂亮女孩的年龄不想被其他人知道,那么年龄这条属性应该作为女孩自己知道的属性;或者女孩的某些行为不想让外界知道,只需要自己知道就可以。那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。
②封装和访问权限
💟封装
1.把变量(属性)和函数(行为或操作)合成一个整体,封装在一个类中 。
2.访问控制,现实事物本身有些属性和行为是不对外开放,所以要对变量(属性)和函数(行为或操作)进行访问控制。
💟访问权限
1.在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问
2.在类的外部(作用域范围外),访问权限才有意义:public,private,protected在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时,private和protected是同等级的,外部不允许访问。
在C++用类的方法实现实现人这一事物:
#include <iostream>
using namespace std;
class Person
{
public:
//公有权限,任何地方都可以访问
char name[20];//人的属性-名字
void behavior()
{
cout << "我勤奋刻苦,长得高大帅气" << endl;
}
private:
//私有权限,外部不可以访问
int age;//人的属性-年龄-不想外人知道
int password;//密码-不想外人知道
protected:
//保护权限,外部访问不了,但在继承中,子类可以访问
int money;//个人财产
};
//继承
class Son :public Maker
{
void func()
{
a = 20;//子类的类内可以方法父类的保护权限的成员
}
};
int main()
{
Person one;
strcpy(one.name, "小明");
//公有权限的成员可以在类外修改
cout << "one.name=" << one.name << endl;
//one.money 保护成员外部无法访问
//one.age one.password私有,外部成员外部无法访问
one.behavior();
return 0;
}
)
③尽量把属性设置为私有权限
1.可以控制属性的读写权限
如果我们让所有的成员变量为public,类外任何区域都可以读写它,如果把成员变量全部设置成private,类外不能直接访问,我们只能通过在类外访问类内公有权限的成员函数,那么我们就可以实现“不准访问”、“只读访问”、“读写访问”“只写访问”的各类成员函数,借助函数实现更精细的优化,
2.可赋予客户端访问数据的一致性
如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。
3.可以保护属性的合法性
④C++中结构体(struct)和类(class)的区别:
类(class)不加权限修饰符,默认访问权限为private(私有)
struct默认访问权限为public(公有)
☮️:
class A
{
int Age;
void func()
{
cout<<"Age="<<Age<<endl;
}
};
struct B
{
int Age;
void func()
{
cout<<"Age="<<Age<<endl;
}
};
void test()
{
A a1;
a1.Age=18;
a1.func;
B b1;
b1.Age=25;
b1.func;
}
三.对象的构造函数和析构函数
①对象的初始化和清理
引入:
我们大家在购买一台电脑或者手机,或者其他的产品,这些产品都有一个初始设置,也就是这些产品对被创建的时候会有一个基础属性值。那么随着我们使用手机和电脑的时间越来越久,那么电脑和手机会慢慢被我们手动创建很多文件数据,某一天我们不用手机或电脑了,那么我们应该将电脑或手机中我们增加的数据删除掉,保护自己的信息数据。
从这样的过程中,我们体会一下,所有的事物在起初的时候都应该有个初始状态,当这个事物完成其使命时,应该及时清除外界作用于上面的一些信息数据。
C++也是来源于现实,是对现实事物的抽象模拟,具体来说,当我们创建对象的时候,这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。
对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。
C++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。
无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。
☯️为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现。
②构造函数和析构函数
1.构造函数和析构函数作用和语法
✅什么是构造函数?
构造函数是类的特殊成员函数,用于初始化对象。构造函数在创建对象时由编译器自动调用。C++中的每个类至少要有一个构造函数,如果类中没有定义构造函数,系统会提供一个默认的无参构造函数,默认的无参构造函数体也为空,不具有实际的初始化意义。因此,在C++程序中要显示定义构造函数。
💠构造函数语法:
构造函数是类的特殊成员函数,C++编译器严格规定了构造函数的接口形式,定义格式如下所示:
⚠️关于构造函数定义,有以下几个注意事项:
- 构造函数名必须与类名相同。
- 构造函数名的前面不需要设置返回值类型。
- 构造函数中无返回值,不能使用return返回。
- 构造函数的成员权限控制符一般设置为public。
✅什么是析构函数?
创建对象时,系统会为对象分配所需要的内存空间等资源,当程序结束或对象被释放时,系统为对象分配的资源也需要回收,以便可以重新分配给其他对象使用。在C++中,对象资源的释放通过析构函数完成。析构函数的作用是在对象被释放之前完成一些清理工作。析构函数调用完成之后,对象占用的资源也被释放。
💠析构函数语法:
与构造函数一样,析构函数也是类的一个特殊成员函数,其定义格式如下所示:
⚠️关于析构函数的定义,有以下注意事项:
- 析构函数的名称与类名相同,在析构函数名称前添加“~”符号。
- 析构函数没有参数。因为没有参数,所以析构函数不能重载,一个类中只有一个析构函数。
- 析构函数没有返回值,不能在析构函数名称前添加任何返回值类型。在析构函数内部,也不能通过return返回任何值。
📝构造函数和析构函数示例:
class Maker
{
public:
//构造函数的作用是初始化成员变量,是编译器去调用的
Maker()
{
a = 10;
cout << "构造函数被调用" << endl;
}
//析构函数,在对象销毁前,编译器调用析构函数
~Maker()
{
cout << "析构函数被调用" << endl;
}
public:
int a;
};
void test01()
{
//实例化对象,内部做了两件事,1.分配空间,2.调用构造函数进行初始化
Maker m;
//如果构造函数被编译器调用a就被初始化为10
//那么b的值为10
int b = m.a;
cout <<"b="<< b << endl;
cout<<"test01函数调用结束"<<endl;
//test01函数结束,析构函数被调用
}
2.构造函数点和析构函数的注意事项
1.构造函数和析构函数的权限必须是公有的,私有实例不了对象。
class Maker
{
private:
//私有权限
//构造函数的作用是初始化成员变量,是编译器去调用的
Maker()
{
cout << "构造函数被调用" << endl;
}
//析构函数,在对象销毁前,编译器调用析构函数
~Maker()
{
cout << "析构函数被调用" << endl;
}
};;
void test02()
{
Maker m;
}
2.构造函数没有返回值,不能用void,可以有参数,析构函数没有返回值,不能用void,没有参数。
3.如果构造函数存在参数,当创建对象时需要在对象后面对照构造函数的参数列表添加参数。构造函数是无参数,创建对象后面不需要加( )。
class Maker
{
public:
//构造函数的作用是初始化成员变量,是编译器去调用的
Maker()
{
a = 99;
cout << "无参构造函数被调用" << endl;
}
Maker(int x)
{
a = x;
cout << "有参构造函数被调用" << endl;
}
//析构函数,在对象销毁前,编译器调用析构函数
~Maker()
{
cout << "析构函数被调用" << endl;
}
void Makerprintf()
{
cout<<"a="<<a<<endl;
}
private:
int a;
};
void test03()
{
//调用无参构造函数
Maker m1;
m1.Makerprintf();
//调用有参构造函数
Maker m2(100);
m2.Makerprintf();
}
4.构造函数可以重载。
class Maker
{
public:
Maker()
{
a = 100;
b = 66;
cout << "无参构造函数1被调用" << endl;
}
Maker(int x)
{
a = x;
b = 66;
cout << "有参构造函数2被调用" << endl;
}
Maker(int x, int y)
{
a = x;
b = y;
cout << "有参构造函数3被调用" << endl;
}
void Mprintf()
{
cout << "a=" << a << "b=" << b << endl;
}
~Maker()
{
cout << "析构函数被调用" << endl;
}
private:
int a;
int b;
};
void test()
{
Maker m1;
m1.Mprintf();
Maker m2(100);
m2.Mprintf();
Maker m3(100, 66);
m3.Mprintf();
}
5.有对象产生必然会调用构造函数,有对象销毁必然会调用析构函数。有多少个对象产生就会调用多少次构造函数,有多少个对象销毁就会调用多少次析构函数
③构造函数的分类
参数类型:分为无参构造函数和有参构造函数
按类型分类:普通构造函数和拷贝构造函数(复制构造函数)
这些分类本质上就是函数重载,函数名相同都是用类自定义的数据类型名,区别就在参数部分。
1.构造拷贝函数:
首先对于普通类型的对象来说,它们之间的拷贝是很简单的,例如:
int a = 66;
int b = a;
//用变量a去初始化b
//就是a赋值给b,这就是普通类型对象的拷贝
//拷贝构造函数也是这个功能
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量,当类的对象需要拷贝(赋值)时,通常会在类内定义一个函数,这个函数对需要拷贝(赋值)的成员变量进行操作,这个函数叫做拷贝构造函数。
拷贝构造函数的定义格式如下所示:
举例:
用一个已有的对象去初始化另一个对象。
(同类型的对象)
class Maker
{
public:
Maker()
{
cout << "无参构造函数" << endl;
a = 20;
}
//拷贝构造函数
Maker(const Maker &m)
//参数这等价于const Maker&m=m1
//这是赋值操作,也就是类与类之间的拷贝
{
cout << "拷贝构造函数" << endl;
a = m.a;
}
//打印函数
void printMaker()
{
cout << "a=" << a << endl;
}
private:
int a;
};
void test05()
{
Maker m1;//调用无参构造函数
m1.printMaker();
//用一个已有的对象去初始化另一个对象
Maker m2(m1);//调用拷贝构造函数
//也可以写成Maker m2=m1;
m2.printMaker();
}
2.拷贝构造函数中形参要用引用
class Maker3
{
public:
Maker3(int Ma)
{
cout << "有参构造函数" << endl;
ma = Ma;
}
Maker3(const Maker3 m)
//形参我们不加引用,看看会产生什么后果
{
cout << "拷贝构造函数" << endl;
}
private:
int ma;
};
void test03()
{
Maker3 m1(10);//调用有参构造
Maker3 m2(m1);//调用拷贝构造函数
}
解释:
如果拷贝构造函数中的形参不是引用时
Maker3(const Maker3 m)
{
cout << "拷贝构造函数" << endl;
}
Maker3 m1(10);//调用有参构造
Maker3 m2(m1);//调用拷贝构造函数
//函数接受实参时
1.const Maker m=m1;
//这是赋值操作,也就是类与类之间的拷贝
//编译器会再次调用拷贝构造函数
2.const Maker3 m(m1);
//紧接着又进行传参,又变为
3.const Maker3 m=m1;
//编译器将这句话变为
4.const Maker3 m(m1);
5.进入死循环
3.拷贝构造函数调用时机
①对象以值方式给函数参数
举例:
当对象作为实参传给形参时,形参在接受时就要使用拷贝构造函数去初始化对象
class Maker
{
public:
Maker()
{
cout << "无参构造函数" << endl;
}
Maker(int a)
{
cout << "有参构造函数" << endl;
}
Maker(const Maker &maker)
{
cout << "拷贝构造函数" << endl;
}
~Maker()
{
cout << "析构函数" << endl;
}
};
//1.对象以值方式给函数参数
void func(Maker m)//Maker m=m1;等价于Maker m(m1),调用拷贝构造函数
{
}
void test04()
{
Maker m1;
func(m1);
}
2.用已有的对象去初始化另一个对象
void test05()
{
Maker m1;
Maker m2(m1);
//用m1初始化m2
}
3.函数的局部对象以值的方式从函数返回,vs Debug(调试)模式下,会调用拷贝构造,vs Release(发行)模式下不会调用拷贝构造,qt也不调用。
Maker func2()
{
//局部对象
Maker m;//调用无参构造函数
cout << "局部对象的地址:" << &m << endl;
return m;
}
void test()
{
Maker m1 = func2();
//先调用func2函数,然后func2函数返了一个局部对象m
//所以等价于Maker m1 = m;然后调用拷贝构造函数。
cout << "m1对象的地址:" << &m1 << endl;
}
debug版本:函数的局部对象以值的方式从函数返回,vs Debug(调试)模式下,会调用拷贝构造,
release版本:vs Release(发行)模式下不会调用拷贝构造,qt也不调用。
④构造函数调用规则
❤️默认情况下,C++编译器至少为我们写的类增加3个函数如果我们在定义类的时候没有写构造函数和析构函数,编译器不会报错,默认情况下,C++编译器至少为我们写的类增加3个函数,分别是:
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
class Maker
{
public:
//编译器提供的默认构造函数
Maker()
{
}
//编译器提供的默认的析构函数
~Maker()
{
}
};
3.默认拷贝构造函数,对类中非静态成员属性简单值拷贝
class Maker2
{
public:
Maker2()
{
cout << "无参构造函数" << endl;
a = 20;
}
//编译器提供了默认的拷贝构造函数
//Maker2(const Maker2 &m)
//{
// //默认拷贝构造函数进行了成员变量的简单拷贝
// a = m.a;
//}
//打印函数
void printMaker()
{
cout << "a=" << a << endl;
}
private:
int a;
};
void test()
{
Maker2 m1;//无参构造函数
m1.printMaker();
Maker2 m2(m1);//编译器调用默认拷贝构造函数
m2.printMaker();
}
❤️如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数
❤️如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造函数。
四.多个对象的构造函数和析构函数
①构造函数的初始化列表
1.初始化列表的定义
与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。
2.作用
我们之前讲的构造函数是对创建的对象进行初始化操作,而初始化列表是对创建的对象的成员进行初始化操作。
初始化对象的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员。
C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,C++编译器必须确保调用了所有子对象的构造函数。如果所有的成员对象有默认无参构造函数,编译器可以自动调用他们。但是如果成员对象定义了有参构造函数,那么编译器就不会再提供默认构造函数,或者想指定调用某个构造函数怎么办?那么是否可以在类的构造函数直接调用成员对象类的成员变量完成初始化呢?但是如果成员对象类的成员变量的访问权限是私有的,我们是没有办法访问并完成初始化的。
解决办法非常简单:对于子类调用构造函数,C++为此提供了专门的语法,即构造函数初始化列表。
3.注意事项
1.初始化列表是干什么用的,初始化对象的成员。
2.初始化列表只能写在构造函数后面。
3.如果使用了初始化列表,那么所有的构造函数都要写初始化列表。
4.如果有多个对象的成员需要初始化,用逗号隔开。
5.可以使用初始化列表传递数值给对象的成员。
举例:
class Maker1
{
public:
Maker1(int a)
{
cout << "Maker1有参构造函数" << a << endl;
}
~Maker1()
{
cout << "Maker1析构函数" << endl;
}
int a;
};
class Maker2
{
public:
Maker2(int b, int c)
{
cout << "Maker2构造函数" << "b=" << b << "c" << c << endl;
}
~Maker2()
{
cout << "Maker2析构函数" << endl;
}
private:
int b;
int c;
};
class Maker3
{
public:
//初始化列表
//注意1:初始化列表只能写在构造函数后面,并加个:
//如果有多个成员需要初始化,用逗号隔开
//使用初始化列表给对象成员传参
Maker3(int a, int b, int c, int d) :m1(a), m2(b, c), d(d)
//m1,m2,d都是Maker3的成员,abcd的值依次和后面abcd一一对应,并分别赋值给m1,m2,和d
//在初始化Maker3创建的对象,要传4个参数
{
cout << "Maker3有参构造函数" << endl;
}
//注意2:如果使用了初始化列表,那么所有的构造函数都要写初始化列表
Maker3(const Maker3 &m2) :m1(40), m2(10, 20), d(100)//拷贝构造函数
{
//
}
~Maker3()
{
cout << "Maker3析构函数" << endl;
}
private:
Maker1 m1;//成员对象
Maker2 m2;//成员对象
int d;//成员变量
};
//初始化列表是调用成员对象的指定构造函数
void test()
{
Maker3 m3(30, 10, 20, 100);
//调用Maker3的有参构造函数
//100初始化给m3中的d,30调用m1的有参构造函数,并赋值给a
//10,20调用m2的有参构造函数,并赋值给b,c
}
②对象成员的构造函数和析构函数调用顺序
1.如果类有成员对象,那么先调用成员对象的构造函数,再调用本身的构造函数,析构函数的调用顺序反之。
2.成员对象的构造函数调用和定义顺序一样3.注意,如果有成员对象,那么实例化对象时,必须保证成员对象的构造和析构能被调用。
💞举例:
class Maker1
{
public:
Maker1()
{
cout << "Maker1无参构造函数" << endl;
}
~Maker1()
{
cout << "Maker1析构函数" << endl;
}
};
class Maker2
{
public:
Maker2()
{
cout << "Maker2无参构造函数" << endl;
}
~Maker2()
{
cout << "Maker2析构函数" << endl;
}
};
class Maker3
{
public:
Maker3()
{
cout << "Maker3无参构造函数" << endl;
}
~Maker3()
{
cout << "Maker3析构函数" << endl;
}
private:
Maker1 m1;//成员对象
Maker2 m2;//成员对象
};
void test()
{
Maker3 m3;
}
五.对象的深浅拷贝
①浅拷贝
同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝.
一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
注:编译器提供的默认构造函数就是浅拷贝
例如:
class Person
{
public:
//提供了有参构造函数,根据规则编译器会提供默认拷贝构造函数,默认拷贝构造函数就是对类内成员的简单赋值
Person(const char *name, int age)
{
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName, name);
Age = age;
}
~Person()
{
cout << "析构函数" <<endl;
if (pName != NULL)
{
free(pName);
pName = NULL;
}
}
public:
char *pName;
int Age;
};
void test()
{
//调用有参构造函数
Person p1("Edward",30);
//调用编译器提供的默认构造函数
Person p2(p1);
cout << "p1 Name=" << p1.pName << " p1 age=" << p1.Age << endl;
cout <<"p2 Name=" << p2.pName << " p2 age=" << p2.Age << endl;
}
问题:pName指针指向的是在堆区开辟的空间,进行默认拷贝后s1和s2的pName相同,那么当s1和s2的析构函数都被调用时,pName被重复释放
②深拷贝
当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。
这样我们可以借助深拷贝解决浅拷贝的问题。
class Person
{
public:
Person(char* name,int age)
{
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName,name);
Age = age;
}
//增加拷贝构造函数
Person(const Person& person)
{
pName =(char*)malloc(strlen(person.pName) + 1);
strcpy(pName, person.pName);
Age = person.Age;
}
~Person()
{
if (pName != NULL)
{
free(pName);
}
}
public:
char* pName;
int Age;
};
void test()
{
//调用有参构造函数
Person p1("Edward",30);
//调用拷贝构造函数
Person p2(p1);
cout << "p1 Name=" << p1.pName << " p1 age=" << p1.Age << endl;
cout <<"p2 Name=" << p2.pName << " p2 age=" << p2.Age << endl;
}
六.动态对象创建
① C动态分配内存方法
当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。所以动态的意思意味着不确定性。
c语言为了解决这个问题,提出了一系列动态内存函数。
例如:malloc以及它的变种calloc和realloc,释放内存的free。
然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
举例用c语言的思想创建动态类:
class Maker
{
public:
Maker()
{
cout << "Maker构造函数被调用" << endl;
}
Maker(int a)
{
cout << "Maker有参函数被调用" << endl;
}
~Maker()
{
cout << "Maker析构函数被调用" << endl;
}
};
void test()
{
//用C语言的方式申请堆区空间,不会调用构造函数
Maker *m = (Maker*)malloc(sizeof(Maker));
if(m == NULL)//判断内存是否申请成功
{
return 0;
}
//对象释放时也不会调用析构函数。
free(m);
}
还有一些其他问题:
⚛️程序员必须确定对象的长度,sizeof(类名)
⚛️malloc返回一个void指针,c++不允许将void赋值给其他任何指针,必须强转。
⚛️malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
⚛️用C语言的方式申请堆区空间,不会调用构造函数对象释放时也不会调用析构函数,我们需要手动进行初始化。
C的动态内存分配函数太复杂,容易令人混淆,是不可接受的,C++中我们推荐使用运算符new 和 delete.
②new操作符
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
new返回的是指针,是你申请内存类型的指针。
Maker *m=new Maker;
//相当于:
Maker *m = (Maker*)malloc(sizeof(Maker));
if(m == NULL)
{
return 0;
}
free(m);
new操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。
现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。
③delete操作符
new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
delete只适用于由new创建的对象。如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。
因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。
如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。
class Maker
{
public:
Maker()
{
cout << "Maker构造函数被调用" << endl;
}
Maker(int a)
{
cout << "Maker有参函数被调用" << endl;
}
~Maker()
{
cout << "Maker析构函数被调用" << endl;
}
};
void test()
{
//用new方式申请堆区空间,会调用类的构造函数
Maker *m1=new Maker;
//调用有参构造函数
Maker *m2=new Maker(1000);
//释放堆区空间,会调用类的析构函数
delete m1;
delete m2;
m1=NULL;
m2=NULL;
}
④用于数组的new和delete
1.new创建基础类型数组
void test()
{
//申请基础数据类型的数组
int *pInt = new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};//不推荐这样初始化数组
for (int i = 0; i < 10; i++)
{
pInt[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
cout << pInt[i] << " ";
}
cout << endl;
//字符数组
char *pChar = new char[64];
memset(pChar, 0, 64);
strcpy(pChar, "小话");
cout << pChar << endl;
//其他数据类型都是这样申请
//注意:如果new时有中括号,那么delete时也要有中括号
delete[] pInt;
delete[] pChar;
}
2.new创建对象数组
class Maker
{
public:
Maker()
{
cout << "构造函数" << endl;
}
Maker(int a)
{
cout << "有参构造函数" << endl;
}
~Maker()
{
cout << "析构函数" << endl;
}
};
void test()
{
Maker *ms = new Maker[2];//调用无参构造
delete[] ms;
//大部分编译器不支持这种写法,(聚合初始化)
//Maker *ms2 = new Maker[2]{Maker(10), Maker(20)};
}
3.delete void*可能出错,不会调用对象的析构函数
void test()
{
void *m = new Maker;
//如果用void*来接new的对象,那么delete时不会调用析构函数
delete m;
//在编译阶段,那么编译器就确定好了函数的调用地址,
//C++编译器不认识void*,不知道void*指向那个函数,所以不会调用析构函数
//这种编译方式叫静态联编
}
4.C和C++的申请和释放堆区空间不要混用
malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗?
void test()
{
Maker *m=new Maker;
free(m);
}
不会调用析构函数。
七.const修饰类的成员
前面学习的类中,成员变量都是我们比较熟悉的简单类型,比如int、double等,但很多时候为描述比较复杂的情况,例如,只允许类的成员函数读取成员变量的值,但不允许在成员函数内部修改成员变量的值,此时就需要使用const关键字修饰成员函数。同样,在程序设计中有些数据也不希望被改变,只允许读取。对于不希望被改变的数据,也可以使用const关键字修饰。在类中,const既可以修饰类的成员变量,也可以修饰类的成员函数。下面对这两种情况分别进行讲解。
①const修饰成员变量—常成员变量
使用const修饰的成员变量称为常成员变量。对于常成员变量,仅仅可以读取第一次初始化的数据,之后是不能修改的。常成员变量通常使用有参构造函数进行初始化。
例如:
class Student
{
public:
const string Name;//成员变量声明
const int Age;
Student(string name,int age);//构造函数声明
~Student();//析构函数声明
};
Student::Student(string name,int age):Name(name),Age(age)//类外实现
{
cout<<"初始化const修饰的成员变量"<<endl;
cout<<"name:"<<Name<<endl;
cout<<"age:"<<Age<<endl;
}
Student::~Student(){}
void test()
{
Student s1("强风吹拂king",18);
s1.age=20;//err
//在创建对象时完成了三个常成员变量的初始化。
//这是创建对象后初始化常成员变量的唯一机会
//常成员变量一旦初始化就不能再改变。
}
②const修饰成员函数—常成员函数
使用const修饰的成员函数称为常成员函数。与修饰成员变量不同的是,修饰成员函数时,const位于成员函数的后面,其格式如下:
返回值类型 函数名()const;
- 在常成员函数内部,只能访问类的成员变量,而不能修改类的成员变量。
- 常成员函数只能调用类的常成员函数,而不能调用类的非常成员函数。
- 常对象(const修饰的对象)可以调用常函数,但不能调用普通成员函数,普通对象可以调用常函数也可以调用普通函数,但类中定义的成员函数若与常成员函数名相同则构成重载,则该常成员函数只能由常对象(const修饰的对象)进行访问。
- mutable修饰的成员变量在常函数中可以被修改
案例:
class Student
{
public:
string Add;//学生家庭住址
Student(string add,string name,int age,string id);//构造函数声明
void ShowInform();//普通成员函数,展示学生信息
void ShowInform()const;//const修饰成员函数,与普通成员函数构成重载
void ShowAdd()const;//const修饰的成员函数,但不构成重载
~Student();//析构函数声明
private:
const string Name;//学生姓名
const int Age;//学生年龄
mutable string ID;//学生学号
};
Student::Student(string add,string name,int age,string id):Add(add),Name(name),Age(age),ID(id)
{
cout << "成员变量已全部初始化完毕,构造函数执行完成" << endl;
}
void Student::ShowInform()//普通成员函数,展示学生信息
{
cout << "调用被重载普通成员函数ShowInform展示学生信息" << endl;
cout << "姓名:" <<Name << endl;
cout << "学号:" <<ID << endl;
cout << "年龄:" <<Age << endl;
cout << "家庭住址:" <<Add << endl;
}
void Student::ShowInform()const
{
cout << "调用被重载const修饰成员函数ShowInform展示学生信息" << endl;
//Age=20;错误
//在常成员函数内部,只能访问类的成员变量,而不能修改类的成员变量。
cout << "姓名:" <<Name << endl;
ID="2203120220";//mutable修饰的成员变量在常函数中可以被修改
cout << "学号:" <<ID << endl;
cout << "年龄:" <<Age << endl;
ShowAdd();
//ShowInform()错误
//常成员函数只能调用类的常成员函数,而不能调用类的非常成员函数。
}
void Student::ShowAdd()const//未被重载的常成员函数
{
cout << "调用const修饰成员函数ShowAdd展示学生家庭住址" << endl;
cout << "家庭住址:" <<Add << endl;
}
Student::~Student(){};
void test()
{
Student s1("山东省","强风吹拂king",19,"2203120201");
//普通对象可以调用常函数也可以调用普通函数,
s1.ShowInform();//调用被重载的普通函数,调用不了被重载的常函数
s1.ShowAdd();//调用常函数
//但类中定义的成员函数若与常成员函数名相同则构成重载
//则该重载常成员函数只能由常对象(const修饰的对象)进行访问。
const Student s2("山东省","我的好基友",19,"2203120000");//常对象
s2.ShowInform();//调用的是重载的常函数,常对象调用不了普通函数
}
八.static修饰类的成员
类中的成员变量,在某些时候被多个类的对象共享,实现对象行为的协调作用。共享数据通过static实现,用static修饰成员后,创建的对象都共享一个静态成员。
①static修饰成员变量-静态成员变量
在一个类中,若将一个成员变量声明为static,这种成员称为静态成员变量。
静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
注意事项:
♋️静态成员变量必须在类中声明,在类外定义。
♋️静态成员变量不属于某个对象,属于类,在为对象分配空间中不包括静态成员所占空间,也就是说我创建100对象,也只创建一份静态成员变量,归根结底是为了节省空间。
♋️静态成员变量是所有对象共享,都可以进行访问。
♋️静态成员变量可以用对象访问,也可以用类访问,(即在对象没有创建前,也可通过类名调用)
举例:
class Maker
{
public:
Maker()//构造函数
{
}
public:
//1.静态成员变量的生命周期是整个程序,作用域在类内
static int a;
static int b;
};
//2.静态成员变量要在类内声明,类外初始化,初始化不加static
int Maker::a = 100;
int Maker::b = 101;
void test01()
{
//3.静态成员变量属于类,不属于对象,是所有对象共享
Maker m1;
Maker m2;
//m1和m2对象都可以进行访问
cout << m1.a <<m2.b<< endl;
cout << m2.a <<m2.b<<endl;
//4.静态成员变量可以用类访问,也可以用对象访问,
//即使没有创建对象m1,m2也可以通过类名访问。
cout << Maker::a <<Maker::b<<endl;
}
②static修饰成员函数-静态成员函数
类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样,同样在对象没有创建前,即可通过类名调用。
静态成员函数主要为了访问静态变量,但是,不能访问普通成员变量。
静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态成员变量,完成对静态成员变量的封装。
注意事项:
⚛️1.静态成员函数只能访问静态成员变量,不能访问普通成员变量。
⚛️2.静态成员也有权限,如果为私有,类外也不可以访问。
⚛️3.静态成员函数可以用对象访问,也可以用类访问,(即在对象没有创建前,也可通过类名调用)
⚛️4.普通成员函数可访问静态成员变量、也可以访问非静态成员变量
举例:
class Maker
{
public:
//普通成员函数可访问静态成员变量、也可以访问非静态成员变量
void func1(int a)
{
x=a;
y=a;
cout<<"普通成员变量x="<<x<<endl;
cout<<"静态成员变量y="<<y<<endl;
}
//静态成员函数只能访问静态成员变量,不能访问普通成员变量。
static void func2(int a)
{
//x=a;错误改不了普通成员变量。
y=a;
cout<<"静态成员变量y="<<y<<endl;
}
private:
//静态成员也有权限,如果为私有,类外也不可以访问。
static void func3(int a)
{
//x=a;//错误
y=a;
}
int x;
static int y;
};
//类外初始化静态成员变量
int Maker::y=100;
void test03()
{
//静态成员函数可以用对象访问,也可以用类访问,(即在对象没有创建前,也可通过类名调用)
Maker::func2(200);
Maker m1;
m1.func1(300);
//m1.fun3,错误,func3为私有,类外不能访问。
}
③const静态成员属性
如果一个类的成员,既要实现共享,又要实现不可改变,那就用 static和const 修饰。
定义静态const数据成员时,最好在类内部初始化。
//const修饰的静态成员变量最好在类内初始化
class Maker4
{
public:
const static int a = 20;
const static int b;
};
//类外也可以初始化,但尽量类内初始化。
const int Maker4::b = 30;
九.C++的面向对象模型初探
①成员变量和函数的存储
1.空类的大小是1,不是0。
class Maker
{
};
void test()
{
cout<<sizeof(Maker)<<endl;
//为什么是1,不是0
Maker *m1=new Maker[100];
//便于编译器处理,如果是0怎么创建数组
}
2.类的成员函数不占用类的大小,静态成员变量不占用类的大小,静态成员函数不占用类的大小
class Maker
{
public:
void func1()
{
}
static int a;
static void func2()
{
}
};
int Maker::a = 10;
void test()
{
cout << sizeof(Maker) << endl;
}
3.普通成员变量占用类的大小
class Maker
{
public:
int b;
};
void test()
{
cout<<sizeof(Maker)<<endl;
}
②this指针
1.this指针工作原理
通过对象模型的②和③,我们可以知道,C++的数据和操作也是分开存储,也就是成员变量和成员函数分开存储,并且每一个非内联成员函数(non-inline member function)只会诞生一份函数实例,也就是说多个同类型的对象会共用一块函数代码。
那么问题是:这一块公用代码是如何区分是哪个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针,解决上述问题。This指针指向被调用的成员函数所属的对象。
🔯C++规定,this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址,也就是说虽然我们没有写上this指针,编译器在编译的时候也是会加上的。因此this也称为“指向本对象的指针”,this指针并不是对象的一部分,不会影响sizeof(对象)的结果。
🔯this指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,在外部看来,每一个对象都拥有自己的函数成员。一般情况下,并不写this,而是让系统进行默认设置。
🔯成员函数通过this指针即可知道操作的是那个对象的数据。 This指针是一种隐含指针,它隐含于每个类的非静态成员函数中。This指针无需定义,直接使用即可。
总结:
1.每个对象都有一个隐藏的this指针,但不属于对象,是编译器添加的
2.编译器会把this指针传入成员函数内
3.this指针指向对象的存储空间
注意:静态成员函数内部没有this指针,所以静态成员函数不能操作非静态成员变量。
c++编译器对普通成员函数的内部处理
2.this指针的使用
💝当形参和成员变量同名时,可用this指针来区分
💝在类的非静态成员函数中返回对象本身,可使用return *this.
class Maker2
{
public:
//1.当形参名和成员变量名相同时,用this指针区分
Maker2(int id)
{
//id=id;err
this->id = id;
}
//2.返回对象的本身
Maker2 &getMaker2()
{
return *this;//运算符重载时有用
}
public:
int id;
};
十.有元
①什么是有元?
类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。
但是,有时候需要在类的外部访问类的私有成员,怎么办?解决方法是使用友元函数,友元函数是一种特权函数,C++允许这个特权函数访问私有成员。
这一点从现实生活中也可以很好的理解:比如你的家,有客厅,有你的卧室,那么你的客厅是Public的,所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是呢,你也可以允许你的闺蜜好基友进去。
程序员可以把一个普通函数、某个类中的成员函数、甚至整个类声明为友元。
1.普通函数作为友元函数
将普通函数作为类的友元函数,在类中使用friend关键字声明该普通函数就可以实现,友元函数可以在类中任意位置声明。普通函数作为类的友元函数的声明格式如下所示:
2.其他类的成员函数作为友元函数
其他类中的成员函数作为本类的友元函数时,需要在本类中表明该函数的作用域,并添加友元函数所在类的前向声明,其语法格式如下:
class Building;//添加友元函数所在类的前向声明
class MyFriend
{
public:
//友元成员函数声明
void LookAtBedRoom(Building& building);
void PlayInBedRoom(Building& building);
};
class Building
{
//声明普通函数做友元函数
friend void CleanBedRoom(Building& building);
//声明另一个类的成员函数做友元函数
friend void MyFriend::LookAtBedRoom(Building& building);
friend void MyFriend::PlayInBedRoom(Building& building);
public:
Building();
public:
string mSittingRoom;
private:
string mBedroom;
};
//友元成员函数类外实现
void MyFriend::LookAtBedRoom(Building& building)
{
cout << "(另一个类的成员函数)我的朋友参观" << building.mBedroom << endl;
}
void MyFriend::PlayInBedRoom(Building& building)
{
cout << "(另一个类的成员函数)我的朋友玩耍在" << building.mBedroom << endl;
}
//友元普通函数实现
void CleanBedRoom(Building& building)
{
cout << "(友元普通函数)我的家人在" << building.mBedroom << endl;
}
Building::Building()
{
this->mSittingRoom = "客厅";
this->mBedroom = "卧室";
}
void test()
{
Building building;
MyFriend myfriend;
CleanBedRoom(building);
myfriend.LookAtBedRoom(building);
myfriend.PlayInBedRoom(building);
}
3.友元类
除了可以声明函数为类的友元函数,还可以将一个类声明为友元类,友元类可以声明在类中任意位置。声明友元类之后,友元类中的所有成员函数都是该类的友元函数,能够访问该类的所有成员。
与声明友元函数类似,友元类也是使用关键字friend声明,其语法格式如下:
①通过给友元类传入参数,来访问类的私有变量
class Building
{
//声明MyFriend类成为Building类的友元类
friend class MyFriend;
public:
//构造函数初始化
Building()
{
livingroom = "客厅";
bedroom = "卧室";
}
public:
string livingroom;
private:
string bedroom;
};
class MyFriend//友元类
{
public:
void GoodGay(Building &bd)
{
cout << "好基友访问我的:" << bd.livingroom << endl;
cout << "好基友访问我的:" << bd.bedroom << endl;
}
};
void test()
{
Building myhouse;
MyFriend myfriend;
myfriend.GoodGay(myhouse);
}
②通过类内指针来访问类的私有变量
class Building
{
//声明MyFriend类成为Building类的友元类
friend class MyFriend;
public:
//构造函数初始化
Building()
{
livingroom = "客厅";
bedroom = "卧室";
}
public:
string livingroom;
private:
string bedroom;
};
class MyFriend
{
public:
MyFriend()
{
cout << "无参构造函数" << endl;
pbu = new Building;
}
void GoodGay()
{
cout << "访问:" << pbu->livingroom << endl;
cout << "访问:" << pbu->bedroom << endl;//通过成员指针访问私有成员
}
//拷贝构造函数 ,防止对象拷贝时,动态内存地址被拷贝
//那样析构函数调用两次,动态内存被重复释放。
MyFriend(const MyFriend &f2)
{
cout << "拷贝构造函数" << endl;
//1.申请空间
pbu = new Building;
}
~MyFriend()
{
cout << "析构函数" << endl;
if (pbu != NULL)
{
cout << "pbu销毁" << endl;
delete pbu;
}
}
public:
Building *pbu;
};
void test()
{
MyFriend myfriend;
myfriend.GoodGay();
MyFriend friend2 = myfriend;
}
③友元的注意事项
♋️友元关系不能被继承,你爹的朋友不是你的朋友。
♋️友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友。
♋️友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友。
十一.单例模式
①单例模式简介
单例模式是设计模式中最简单的形式之一,单例模式目的:让一个类只能实例化出一个对象。
②单例模式的作用?
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。
③实现单例模式的思路
要实现单例模式,需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有能生成多个对象的方法。
1.把无参构造函数和拷贝构造函数私有化
2.定义一个类内的静态成员指针
3.在类外初始化时,new一个对象
4.把指针的权限设置为私有,然后提供一个静态成员函数让外面获取这个指针
//单例模式是一个类只能实例化一个对象
class Maker
{
//1.把构造函数私有化
private:
Maker()
{
}
Maker(const Maker &m)
{
}
public:
//静态成员函数,访问静态成员变量
//4.把指针的权限设置为私有,然后提供一个静态成员函数让外面获取这个指针
static Maker* getMaker()
{
return pMaker;
}
private:
//2.定义一个类内的静态成员指针
static Maker *pMaker;
};
//3.在类外初始化时,new一个对象
Maker* Maker::pMaker = new Maker;//这里可以new是因为在Maker::作用域,编译器把它当成在类内
void test()
{
//类外获取这个对象指针
Maker* m = Maker::getMaker();
Maker* m2 = Maker::getMaker();
cout << "m=" << m << endl;
cout << "m2=" << m2 << endl;
//Maker m3 = *m;//调用拷贝构造,这里会产生新对象,所以要把拷贝构造也私有化
//cout << "m3=" << &m3 << endl;
}
④案例
单例模式案例(需求:获取打印机打印次数)
思路:创建一个打印机类,用这个类创建一个打印机并会记录使用次数,只要使用打印机,都是使用的这一台。
//需求,获取打印机使用的次数
class Printer
{
private:
//1.把无参构造和拷贝构造私有化
Printer()
{
mcount = 0;
}
Printer(const Printer &p)
{
}
public:
static Printer *getPrinter()
{
return p;
}
void printPrinter(string name)
{
cout << name << ":打印" << endl;
mcount++;
}
int getCount()
{
return mcount;
}
private:
int mcount;//记录打印打印的次数
//2.定义静态成员指针
static Printer *p;
};
//3.类外进行初始化,new对象
Printer *Printer::p = new Printer;
void test()
{
//销售部
Printer *p1 = Printer::getPrinter();//p指针
p1->printPrinter("销售部");
//技术部
Printer *p2 = Printer::getPrinter();//p指针
p2->printPrinter("技术部");
//公关部
Printer *p3 = Printer::getPrinter();//p指针
p3->printPrinter("公关部");
Printer *p4 = Printer::getPrinter();//p指针
cout << "打印使用的次数:"<<p4->getCount() << endl;
}
________________________
一.类的继承
①继承的概念
C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。一个B类继承于A类,或称从类A派生类B。
这样的话,类A成为基类(父类), 类B成为派生类(子类)。
派生类中的成员,包含两大部分:
- 一类是从基类继承过来的,一类是自己增加的成员。
- 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
②派生类定义
在类的继承中,有以下几点需要注意:
- ①基类的构造函数与析构函数不能被继承。
- ②派生类对基类成员的继承没有选择权,不能选择继承或不继承某些成员。
- ③派生类中可以增加新的成员,用于实现新功能,保证派生类的功能在基类基础上有所扩展。
- ④一个基类可以派生出多个派生类;一个派生类也可以继承自多个基类。
③继承方式:
在继承中,派生类会继承基类除构造函数、析构函数之外的全部成员。从基类继承的成员,其访问属性除了成员自身的访问属性,还受继承方式的影响。类的继承方式主要有三种:public(公有继承)、protected(保护继承)和private(私有继承)。不同的继承方式会影响基类成员在派生类中的访问权限。
1.公有继承 (public)
采用公有继承方式时,基类的公有成员和保护成员在派生类中仍然是公有成员和保护成员,其访问属性不变,可以使用派生类的对象访问基类公有成员。但是,基类的私有成员在派生类中变成了不可访问成员。如果基类中有从上层基类继承过来的不可访问成员,则基类的不可访问成员在它的派生类中同样是不可访问的。
公有继承对派生类继承成员的访问控制权限影响图:
✅注意:
不可访问成员是指无论在类内还是在类外均不可访问的成员。它与私有成员的区别是,私有成员在类外不可访问,只能通过类的成员进行访问。不可访问成员完全是由类的派生形成的。对于顶层类,不存在不可访问成员,但是通过继承,基类的私有成员在派生类内就成为不可访问成员。
2.保护继承(protected)
采用保护继承方式时,基类的公有成员和保护成员在派生类中全部变成保护成员,派生类的其他成员可以直接访问它们,在派生类外无法访问。基类的私有成员和不可访问成员在派生类中的访问属性是不可访问。
保护继承对派生类继承成员的访问控制权限影响图:
3.私有继承 (privated)
采用私有继承方式时,基类的公有成员和保护成员在派生类中全部变成私有成员,派生类的其他成员可以直接访问它们,在派生类外无法访问。基类的私有成员和不可访问成员在派生类中的访问属性是不可访问。
私有继承对派生类继承成员的访问控制权限影响图:
二、派生类
①派生类的构造函数与析构函数
派生类的成员变量包括从基类继承的成员变量和新增的成员变量,因此,派生类的构造函数除了要初始化派生类中新增的成员变量,还要初始化基类的成员变量,即派生类的构造函数要负责调用基类的构造函数。派生类的构造函数定义格式如下所示:
由上述格式可知,在定义派生类构造函数时,通过“:”运算符在后面完成基类构造函数的调用。基类构造函数的参数从派生类构造函数的参数列表中获取。
1.继承中的构造函数和析构函数的调用顺序
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。
继承中的构造函数和析构函数的调用顺序:
✅派生类对象在创建时会首先调用基类的构造函数,基类构造函数执行完毕后,才会调用派生类类的构造函数。
✅当基类构造函数有参数时,需要在派生类初始化列表(参数列表)中显示调用基类构造函数。
✅析构函数调用顺序和构造函数相反。
当派生类含有成员对象时,派生类构造函数除了负责基类成员变量的初始化和本类新增成员变量的初始化,还要负责成员对象的初始化,其定义格式如下所示:
此时的调用顺序为:
先调用父类的构造,然后调用成员对象的构造,最后调用本身的构造,析构函数调用顺序反之。
继承中的构造函数和析构函数的调用顺序案例:
class Base//基类
{
public:
Base(string name,int age);//基类构造函数声明
~Base();//基类析构函数声明
private:
string Name;//成员变量声明
int Age;
};
Base::Base(string name,int age)
{
cout<<"基类构造函数被调用"<<endl;
Name=name;
Age=age;
cout<<"基类Name:"<<Name<<"基类Age:"<<Age<<endl;
}
Base::~Base()
{
cout<<"基类析构函数被调用"<<endl;
}
//成员对象类
class Maker
{
public:
//构造函数和析构函数声明
Maker(int x);
~Maker();
private:
int x;
};
Maker::Maker(int m)
{
cout<<"成员对象类构造函数被调用"<<endl;
x=m;
}
Maker ::~Maker()
{
cout<<"成员对象类析构函数被调用"<<endl;
}
class Derived:public Base
{
public:
Derived(string name,int age,int x,string add,string tele);//派生类构造函数声明
~Derived();
private:
Maker m;
string Add;
string Tele;
};
//类外实现构造函数,后面使用“:”运算符调用成员对象构造函数、基类构造函数
Derived::Derived(string name,int age,int x,string add,string tele):Base(name,age),m(x)
{
cout<<"派生类构造函数被调用"<<endl;
Add=add;
Tele=tele;
cout<<"派生类Add: "<<Add<<" 派生类Tele:"<<Tele<<endl;
}
Derived::~Derived()
{
cout<<"派生类析构函数被调用"<<endl;
}
void test()
{
Derived d("强风吹拂king",19,666,"山东省","15606333333");
}
②继承中同名成员的处理方法
在派生类中重新定义基类同名函数,基类同名函数在派生类中被隐藏,通过派生类对象调用同名函数时,调用的是改写后的派生类成员函数,基类同名函数不会被调用,如果想通过派生类对象调用基类的同名函数,需要使用作用域限定符“::”指定要调用的函数,或者根据类型兼容规则,通过基类指针调用同名成员函数。
⚠️注意:
需要注意的是,只要是同名函数,无论参数列表和返回值类型是否相同,基类同名函数都会被隐藏。若基类中有多个重载函数,派生类中有同名函数,则基类中所有同名函数在派生类中都会被隐藏。
③继承中的静态成员特性
1.静态成员可以被继承。
2.继承中的静态成员变量一样会被同名的子类成员变量隐藏。
3.继承中的静态成员函数中,当子类有和父类同名静态函数时,父类的所有同名重载静态函数都会被隐藏。
4.改变从基类继承过来的静态函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数。
5.静态成员函数不能是虚函数。
6.从父类继承过来的静态成员变量是父类的静态成员变量。
class Father
{
public:
static int sNum;
static void func()
{
cout << "Father static func" << endl;
}
static void func(int a)
{
cout << "Father static func(int a)" << endl;
}
static void func(int a,int b)
{
cout << "Father static func(int a,int b)" << endl;
}
};
int Father::sNum = 10;
class Son : public Father
{
public:
//定义一个与父类重名的静态成员变量
//基类静态成员属性将被隐藏
static int sNum;
//定义一个与父类重名的函数
//父类所以同名函数全部被隐藏
static void func()
{
cout << "Son static func" << endl;
}
//改变从基类继承过来的静态函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数。
};
int Son::sNum = 20;
void test()
{
Son s;
cout<<"s.sNum="<<s.sNum<<endl;
//当子类和父类有同名静态成员时,子类的静态同名成员会隐藏父类的同名成员
cout<<"s.Father::sNum="<<s.Father::sNum<<endl;
s.func();
//同名覆盖,访问的是子类的func
//想要访问父类需要加作用域
s.Father::func();
s.Father::func(20,30);
}
④查看对象模型
在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:
class Aclass
{
public:
int mA;
int mB;
};
class Bclass : public Aclass
{
public:
int mC;
};
class Cclass : public Bclass
{
public:
int mad;
};
void test()
{
cout << "A size:" << sizeof(Aclass) << endl;
cout << "B size:" << sizeof(Bclass) << endl;
cout << "C size:" << sizeof(Cclass) << endl;
}
查看类继承的内部模型步骤:(VS2013为例)
-
1.找到Visual Studio Tools文件夹,打开,找到VS2013开发人员命令提示程序(一般在:C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts),打开
-
2.然后复制你工程路径,窗口输入命令:cd 路径,回车,进入你工程文件夹中(如果工程不在C盘在E盘的话,要再E:下)。
-
3.输入命令:cl /d1 reportSingleClassLayout类名 文件名全称如:cl /d1 reportSingleClassLayoutSon test.cpp
-
三、多继承
①多继承概念
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。通俗说一个儿子好几个爹。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
多继承是单继承的扩展,在多继承中,派生类的定义与单继承类似,其语法格式如下所示:
例如:
class Base1
{
public:
void func1()
{
cout << "Base1::func1" << endl;
}
};
class Base2
{
public:
void func1()
{
cout << "Base2::func1" << endl;
}void func2()
{
cout << "Base2::func2" << endl;
}
};
//派生类继承Base1、Base2
class Derived : public Base1, public Base2
{
};
void test()
{
Derived derived;
//func1是从Base1继承来的还是从Base2继承来的?
//derived.func1();
derived.func2();
//解决歧义:显示指定调用那个基类func1
derived.Base1::func1();
derived.Base2::func1();
}
通过多继承,派生类会从多个基类中继承成员。在定义派生类对象时,派生类对象中成员变量的排列规则是:按照基类的继承顺序,将基类成员依次排列,然后再存放派生类中的新增成员。
多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?解决方法就是显示指定调用那个基类的版本。
②菱形继承
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。
这种继承所带来的问题:
🌿羊🐏继承了动物的数据和函数,驼🐫同样继承了动物的数据和函数,而羊驼继承了羊和驼,当羊驼🦙调用函数或者数据时,就会产生二义性问题。解决方法:虚继承
🌿羊驼🦙继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
举例:
class Anmial
{
public:
Anmial()
{
ma=10;
}
void func()
{
cout << "动物类" << endl;
}
int ma;
};
class Sheep:public Anmial{};
class Camel:public Anmial{};
//羊驼类继承羊类、驼类
class SheepCamel : public Sheep, public Camel{};
void test()
{
SheepCamel sc;
//sc.func1;错误,二义性,
//因为羊类和驼类都继承了动物类的func
//解决二义性方法,指定调用基类的方式解决
sc.Sheep::func();
//重复继承问题
cout << "SheepCamel size:" << sizeof(SheepCamel) << endl; //重复继承,是8
}
上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚继承的方法。
③虚继承
在程序设计过程中,通常希望间接基类的成员变量在底层派生类中只有一份拷贝,从而避免成员访问的二义性。通过虚继承可以达到这样的目的,虚继承就是在派生类继承基类时,在权限控制符前加上virtual关键字,其格式如下所示:
在上述格式中,在权限控制符前面添加了virtual关键字,就表明派生类虚继承了基类。被虚继承的基类通常称为虚基类,虚基类只是针对虚继承,而不是针对基类本身。在普通继承中,该基类并不称为虚基类。
class Sheep : virtual public Anmial {…};
class Camel : virtual public Anmial {…};
然后,可以将SheepCamel类定义为:
class SheepCamel: public Sheep, public Camel{.….};
现在,SheepCamel对象将只包含Anmial对象的一个副本。从本质上说,继承的Sheep和Camel对象共享一个Anmial对象,而不是各自引入自己的Anmial对象副本。
class Anmial
{
public:
Anmial()
{
ma=10;
}
void func()
{
cout << "动物类" << endl;
}
int ma;
};
class Sheep:virtual public Anmial{};
class Camel:virtual public Anmial{};
//羊驼类继承羊类、驼类
class SheepCamel : public Sheep, public Camel{};
void test()
{
SheepCamel sc;
//二义性问题解决
sc.func();
//重复继承问题解决
cout << "SheepCamel size:" << sizeof(SheepCamel) << endl;
}
但是虚基类是如何解决二义性的呢?并且SheepCamel大小为12字节,这是怎么回事?
④🔥虚继承实现原理🔥
class BigBase
{
public:
BigBase()
{
mParam = 0;
}
void func()
{
cout << "BigBase::func" << endl;
}
public:
int mParam;
};
//虚继承
class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
//普通继承
class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};
我们通过查看对象模型观察到底有什么区别?
通过对象布局图,我们发现普通继承和虚继承的对象内存图是不一样的。我们也可以猜测到编译器肯定对我们编写的程序做了一些手脚。
✅BigBase 菱形最顶层的类,内存布局图没有发生改变。
✅Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个虚指针vbptr (virtual base pointer),在虚继承中,每个虚继承的派生类都会增加一个虚基类指针vbptr,该指针位于派生类对象的顶部。vbptr指针指向一个虚基类表vbtable(不占对象内存),虚基类表中记录了基类成员变量相对于vbptr指针的偏移量,根据偏移量就可以找到虚基类成员变量。
✅当虚基类的派生类被当作基类继承时,虚基类指针vbptr也会被继承,因此底层派生类对象中成员变量的排列方式与普通继承有所不同,Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类成员变量的偏移量。
由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。现在模型就变成了Base1和 Base2 、Derived三个类对象共享了一份BigBase数据。
四、派生类和基类的特殊关系
①类型的兼容性
不同类型的数据在一定条件下可以进行转换,比如int n=‘a’,是将字符’a’赋值给整型变量n,在赋值过程中发生了隐式类型转换,字符类型的数据转换为整型数据。这种现象称为类型转换,也称为类型兼容。
在C++中,基类与派生类之间也存在类型兼容。通过公有继承,派生类获得了基类除构造函数、析构函数之外的所有成员。公有派生类实际上就继承了基类所有公有成员。因此,在语法上,公有派生类对象总是可以充当基类对象,即可以将公有派生类对象赋值给基类对象,在用到基类对象的地方可以用其公有派生类对象代替。
②C++中的类型兼容情况
- 1.使用公有派生类对象为基类对象赋值。(本质上是用一个对象去初始化另一个对象,调用基类的拷贝构造函数)
- 2.使用公有派生类对象为基类对象的引用或基类指针赋值。(本质上发生隐式向上类型转换)
- 3.如果函数的参数是基类对象、基类对象的引用、基类指针,则函数在调用时,可以使用公有派生类对象、公有派生类对象的地址作为实参。等同于第二条,因为函数在接受实参时会发生赋值。(本质上发生隐式向上类型转换)
- ⚠️注意:
派生类对象指针或引用赋值给基类对象的指针或引用后,通过基类指针或引用只能访问基类的成员,无法访问派生类的新增成员。
(本质:发生隐式向上类型转换。)
class Derived;
class Base
{
public:
Base()
{
cout << "基类Base的无参构造函数调用:" << endl;
Basename = "基类成员变量";
}
void Basefunc()
{
cout << "基类Base的Basefunc调用:" << Basename <<endl;
}
//不定义析构函数,用编译器默认的就行
protected:
string Basename;
};
class Derived :public Base
{
public:
Derived()
{
cout << "派生类Derived的无参构造函数调用" << endl;
Basename = "派生类成员变量";
}
void Derivedfunc()
{
cout << "派生类Derived的Derivedfunc调用" <<Basename << endl;
}
};
//定义普通函数func(),参数为基类指针
void func(Base* pbase)
{
pbase->Basefunc();
}
void test()
{
//创建派生类Derive类对象derive
Derived derive;
//使用对象derive为Base类对象base赋值,实际上就是拿一个派生类去初始化基类
Base base = derive;
//通过Base类对象调用Basefunc函数
base.Basefunc();
//使用公有派生类对象为基类对象的引用或基类指针赋值。(本质上发生隐式向上类型转换)
Base &qbase = derive;
Base *pbase = &derive;
//通过Base类对象的引用或指针调用Basefunc函数
//只能调用基类Base的Basefunc函数,因为已经向上类型转换为基类了
qbase.Basefunc();
pbase->Basefunc();
//qbase.Derivedfunc();pbase->Derivedfunc();//编译器报错
func(&derive); //取对象derive的地址作为func()函数的参数
}
③拓展:向上和向下强制类型转换
将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这种情况可以是隐式的,所以才会出现类型兼容的情况。
相反的过程:将基类指针或引用转换为派生类指针或引用——称为向下强制转(downcasting)。如果不使用显示类型转换,则向下类型转换是不被允许的。派生类可以新增数据成员,因此使用这些数据成员的类成员函数不能应用于基类。
____________________________________________________
一、多态
多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
①什么是多态?
之前的继承中派生类对象使用基类的方法,而未做任何修改。然而,可能会遇到这样的情况,即希望同一个方法在派生类和基类中的行为是不同的。换句话来说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态–具有多种形态,即同一个方法的行为随上下文而异。
②一个问题?
代码:
class Animal
{
public:void speak()
{
cout << "动物在唱歌..." << endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout << "小狗在唱歌..." << endl;
}
};
void DoBussiness(Animal& animal)
{
animal.speak();//speak函数调用,那它调用的是哪个对象的speak呢?
}
void test()
{
Dog dog;
DoBussiness(dog);
}
运行结果: 动物在唱歌
**❓问题抛出: **
我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak,那么为什么会出现这种现象呢?
🔧如何解决?
多态,多态的目的就是实现方法的调用取决于该方法的对象,我们传入了dog对象,就应该调用dog的speak函数。
实现多态的重要机制:
- 使用虚函数。
二、问题解决思路
①静态联编与动态联编
程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题。
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。
✅①静态联编
在C语言中,这非常简单,因为每个函数名都对应一个不同的函数。在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定调用哪个函数。然而C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(staticbinding),又称为早期联编(earlybinding)。
我们之前在类型兼容的时候讲过派生类对象的引用或指针赋值给基类对象的引用或指针后,基类对象的引用和指针只能调用基类方法而调用不了派生类方法,本质就是静态联编,编译器早早的就根据指针类型确定了调用函数。
✅②动态联编
使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding) 。也叫晚期联编(late binding)。
上面的问题就是由于静态联编引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,传过来的实参是Dog类型,anmial被隐式向上类型转换为Anmial类型,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。
解决方法就是动态联编(晚期联编,late binding),意味着绑定要根据对象的实际类型,发生在运行。
C++语言要实现这种动态联编,必须有某种机制来确定运行时对象的类型并调用合适的成员函数——虚函数机制。
②虚函数
C++动态多态性是通过虚函数来实现的,虚函数允许派生类重新定义基类成员函数,而派生类重新定义基类虚函数的做法称为覆盖(override),或者称为重写。
虚函数的声明方式是在成员函数的返回值类型前添加virtual关键字,格式如下所示:
声明虚函数时,有以下3点需要注意。
- (1)构造函数不能声明为虚函数,但析构函数可以声明为虚函数。
- (2)虚函数不能是静态成员函数。
- (3)友元函数不能声明为虚函数,但虚函数可以作为另一个类的友元函数。
对于实现动态联编,C++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.
使用虚函数的主要目的是实现动态多态性。在程序运行时,若类中声明了虚函数,并且派生类重新定义了虚函数,当使用基类指针或基类引用操作派生类对象调用虚函数时,系统会自动调用派生类中的虚函数代替基类虚函数。
这样就可以实现在运行时根据对象的类型来调用对应的函数(方法的行为应取决于调用该方法的对象),从而实现多态性。
问题解决:
class Animal
{
public:
virtual void speak()
{
cout << "动物在唱歌..." << endl;
}
};
class Dog : public Animal
{
public:
virtual void speak()
{
cout << "小狗在唱歌..." << endl;
}
};
void DoBussiness(Animal& animal)
{
animal.speak();//speak函数调用,那它调用的是哪个对象的speak呢?
}
void test()
{
Dog dog;
DoBussiness(dog);
}
三、多态小结
①多态作用
1.可以解决项目中的紧耦合问题,提供程序的可扩展性
2.应用程序不必为每一个子类的功能调用编写代码,只需要对抽象的父类进行处理。
②多态发生的三个条件
- 1.有继承。
- 2.重写父类的虚函数。
✝️返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
✝️子类中virtual关键字可写可不写,建议写 - 3.基类指针指向派生类对象(类型兼容)
例如:
有一个Hero类,该类有attack(普通攻击)这个操作函数,又有三个类继承Hero类,分别是吕布,干将莫邪,百里守约,它们这三个子类对父类Hero的attack函数有不同的解释,有不同的效果,子类不需要单独写出不同的解释,只需对父类的attack函数进行处理。
如若出新英雄,只需增加新代码,不需要对就英雄代码进行更改,提供程序的可扩展性
class Hero
{
public:
//虚函数
virtual void attack()
{
}
};
//1.有继承
class lvbu :public Hero
{
public:
//2.重写父类的虚函数
virtual void attack()
{
cout << "近战普通攻击" << endl;
}
};
class ganjiangmoye :public Hero
{
public:
//重写父类的虚函数
virtual void attack()
{
cout << "近战法术攻击" << endl;
}
};
class bailishouyue :public Hero
{
public:
//重写父类的虚函数
virtual void attack()
{
cout << "远程攻击且不会暴击" << endl;
}
};
//同一个操作,且参数部分,父类指针指向子类对象
void doLogin(Hero *hero)
{
hero->attack();//产生不同的效果
}
void test()
{
Hero *hero = NULL;
//3.父类指针指向子类对象
hero = new lvbu;
doLogin(hero);//不同的对象
delete hero;
hero = new ganjiangmoye;
doLogin(hero);//不同的对象
delete hero;
hero = new bailishouyue;
doLogin(hero);//不同的对象
delete hero;
}
四、🔥多态(虚函数)的实现原理🔥
①示例代码
class Animal
{
public:
virtual void speak()
{
cout << "Animal Speak " << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "狗汪汪叫" << endl;
}
};
void test()
{
Anmial *anmial = new Dog;
anmial->speak;
}
②💬原理详细分析
步骤 | 漫画演示 | 对象模型图和代码图 |
---|---|---|
1️⃣ | 编译器发现基类Anmial有虚函数,会为Anmial添加一个vfptr指针,指向虚函数表vtbl(virtual function table),虚函数表中存储了在类对象中进行声明的虚函数的地址。 | |
2️⃣ | 派生类继承基类,派生类Dog会继承基类Anmial的虚函数表指针,这时的虚函数表指针指向基类,然后派生类在初始化调用构造函数时使其指向自己的虚函数表,如果派生类不改变基类的虚函数并且不新定义虚函数,那么虚函数表中的内容还是基类的虚函数地址。 | |
3️⃣ | 派生类Dog重写基类Anmial的虚函数Speak,Dog的虚函数表原先Anmial的speak被重写的Dog的speak覆盖。 | |
4️⃣ | 调用虚函数时,如果使用指向基类对象Anmial的引用或指针来调用虚函数speak,程序将通过指针或引用找到那份内存空间中(Dog)的虚函数表指针进而找到虚函数表,然后再找到虚函数speak。 |
五、纯虚函数和抽象类
在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对派生类进行向上类型转换为基类类型,使用基类的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。
做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).
🟣纯虚函数使用关键字virtual,并在其后面加上=0。
如果试图去实例化一个抽象类,编译器则会阻止这种操作。
🟣抽象类只能作为基类派生新类,不能创建抽象类的对象,但可以定义抽象类的指针或引用,通过指针或引用操作派生类对象,
当派生类继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
🟣Virtual void fun() = 0;//纯虚函数声明告诉编译器在(虚函数表)vtable中为函数保留一个位置,但在这个特定位置不放地址。
六、虚析构函数纯虚析构函数
①一个问题
下面代码中delete animal;将调用谁的析构函数
class Animal
{
public:
Animal()
{
cout << "Animal的构造" << endl;
}
~Animal()
{
cout << "Animal的析构" << endl;
}
};
class Son :public Animal
{
public:
Son()
{
cout << "Son的构造" << endl;
pName = new char[64];
memset(pName, 0, 64);
strcpy(pName, "如花");
}
~Son()
{
cout << "Son的析构" << endl;
if (pName != NULL)
{
delete[] pName;
pName = NULL;
}
}
public:
char *pName;
};
void test()
{
Animal *animal = new Son;
delete animal;
}
上述现象将会出现一个问题,派生类Son中的指针没有被释放。
②虚析构函数
虚析构函数的声明是在“~”符号前添加virtual关键字,格式如下所示:
基类中声明虚析构函数之后,基类的所有派生类的析构函数都自动成为虚析构函数。
在基类声明虚析构函数之后,使用基类指针或引用操作派生类对象,在析构派生类对象时,编译器会先调用派生类的析构函数释放派生类对象资源,然后再调用基类析构函数。如果基类没有声明虚析构函数,在析构派生类对象时,编译器只会调用基类析构函数,不会调用派生类析构函数,导致派生类对象申请的资源不能正确释放。
问题解决:
只需要将需要将基类Anmial的析构函数声明为虚函数,即可解决Son类中指针不能正确释放的问题。
③纯虚析构函数
纯虚析构函数,有纯虚析构函数的类是抽象类,不能实例化对象。
注意:有纯虚析构函数的类是抽象类,那么我们想要派生类,需要在派生类重写父类的纯虚析构函数,但这是不可能的,所以纯虚析构函数需要在类外实现。
class Animal
{
public:
Animal()
{
cout << "Animal的构造" << endl;
}
//虚析构函数会调用子类的析构函数
/*virtual ~Animal()
{
cout << "Animal的析构" << endl;
}*/
//纯虚析构函数,纯虚析构函数需要在类外实现
virtual ~Animal() = 0;
};
//在类外实现纯虚析构函数
Animal::~Animal()
{
cout << "Animal的纯虚析构" << endl;
}