多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。为了更为详细的说明多态,此处我们划分为**静态多态 **和 **动态多态 ** 两种状态来讲解
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数可以调用就调,没有的话就会发出警告或者报错 。 该种方式的出现有两处地方: **函数重载 ** 和 泛型编程| 函数模板
int Add(int a, int b){
return a + b;
};
double Add(double a, double b)
{
return a + b;
};
int main()
{
Add(10, 20);
Add(10.0,20.0); //正常代码
return 0;
}
它是在程序运行时根据父类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。
A *a = new A();
Father * f = new Fahter();
Father * f2 = new Child();
动态多态的必须满足两个条件:
- 父类中必须包含虚函数,并且子类中一定要对父类中的虚函数进行重写。
- 通过父类对象的指针或者引用调用虚函数。
通常情况下,如果要把一个引用或者指针绑定到一个对象身上,那么要求引用或者指针必须和对象的类型一致。 不过在继承关系下,父类的引用或指针可以绑定到子类的对象,这种现象具有欺骗性,因为在使用这个引用或者指针的时候,并不清楚它所绑定的具体类型,无法明确是父类的对象还是子类的对象。
int *p = new int(3);
继承关系:
father *p = new father();
child *c = new child();
//继承的时候,有一种特殊情况:
father *p2 = new child():
只有在继承关系下,才需要考虑静态和动态类型,这里仅仅是强调类型而已。所谓的
静态类型
指的是,在编译时就已经知道它的变量声明时对应的类型是什么。而动态类型
则是运行的时候,数据的类型才得以确定。只有在
引用
或者指针
场景下,才需要考虑 静态或者动态类型。因为非引用或者非指针状态下,实际上发生了一次拷贝动作。father *f2 = new son();
//静态类型:不需要运行,编译状态下,即可知晓 a,b的类型。
int a = 3;
string b = "abc";
//动态类型:
//f在编译时,类型是Father ,但在运行时,真正的类型由getObj来决定。目前不能明确getObj返回的是Father的对象还是Child的对象。
Child getObj(){
Child c ;
return c;
};
Father &f = getObj();
父类的引用或指针可以绑定到子类的对象 , 那么在访问同名函数时,常常出现意想不到的效果。
class father{
public:
void show(){
cout << "father show" << endl;
}
};
class children : public father{
public:
void show(){
cout << "children show" << endl;
}
};
int main(){
father f = children();
f.show(); // 打印father show
}
程序调用函数时,到底执行哪一个代码块,由编译器来负责回答这个问题。将源码中的函数调用解释为执行特定的函数代码,称之为
函数名联编
。 在C语言里面,每个函数名都对应一个不同的函数。但是由于C++里面存在重载的缘故,编译器必须查看函数参数以及函数名才能确定使用哪个函数,编译器可以在编译阶段完成这种联编,在编译阶段即可完成联编也被称为:静态联编 | 早期联编
。 程序在运行期间才决定执行哪个函数,这种称之为动态联编 | 晚期联编
class WashMachine{
public:
void wash(){
cout << "洗衣机在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
void wash(){
cout << "智能洗衣机在洗衣服" << endl;
}
};
int main(){
WashMachine *w1= new WashMachine(); //父类指针指向父类对象 打印:洗衣机在洗衣服
w1->wash();
SmartWashMachine *s = new SmartWashMachine(); //子类指针指向子类对象 打印: 智能洗衣机...
s->wash();
WashMachine *w2 = new SmartWashMachine(); //父类指针指向子类对象 打印:洗衣机在洗衣服
w2->wash();
return 0 ;
}
动态联编在处理子类重新定义父类函数的场景下,确实比静态联编好,静态联编只会无脑的执行父类函数。但是不能因此就否定静态联编的作用。动态联编状态下,为了能够让指针顺利访问到子类函数,需要对指针进行跟踪、这需要额外的开销。但是并不是所有的函数都处于继承状态下,那么此时静态联编更优秀些。
编写c++代码时,不能保证全部是继承体系的父类和子类,也不能保证没有继承关系的存在,所以为了囊括两种情况,c++ 才提供了两种方式。
正所谓两害相权取其轻,考虑到大部分的函数都不是处在继承结构中,所以效率更高的静态联编也就成了默认的的选择。
做这样的设计是出于什么目的?
C++中的虚函数的作用主要是实现了多态的机制 , 有了虚函数就可以在父类的指针或者引用指向子类的实例的前提下,然后通过父类的指针或者引用调用实际子类的成员函数。这种技术让父类的指针或引用具备了多种形态。定义虚函数非常简单,只需要在函数声明前,加上
virtual
关键字即可。 在父类的函数上添加 virtual 关键字,可使子类的函数也变成虚函数。如果基类指针指向的是一个基类对象,则基类的虚函数被调用
如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。
class WashMachine{
public:
virtual void wash(){
cout << "洗衣机在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
virtual void wash(){
cout << "智能洗衣机在洗衣服" << endl;
}
};
int main(){
WashMachine *w2 = new SmartWashMachine(); //父类指针指向子类对象 打印..洗衣机在洗衣服
w2->wash();
return 0 ;
}
了解虚函数的工作原理,有助于理解虚函数。
通常情况下,编译器处理虚函数的方法是: 给每一个对象添加一个隐藏指针成员,它指向一个数组,数组里面存放着对象中所有函数的地址。这个数组称之为虚函数表(virtual function table ) 。表中存储着类对象的虚函数地址。
父类对象包含的指针,指向父类的虚函数表地址,子类对象包含的指针,指向子类的虚函数表地址。
如果子类重新定义了父类的函数,那么函数表中存放的是新的地址,如果子类没有重新定义,那么表中存放的是父类的函数地址。
若子类有自己虚函数,则只需要添加到表中即可。
构造函数不能是虚函数。因为虚函数的作用是标注在父子类同名方法上,父类和子类的构造并不是同名方法。并且虚函数是用于控制在父类引用 | 指针指向子类对象时,决定调用父类还是子类的同名函数。
class father{
public:
virtual father(){ //报错!
cout <<"父亲构造函数~!~" << endl;
}
}
在继承体系下, 如果父类的指针可以指向子类对象,这就导致在使用
delete
释放内存时,却是通过父类指针来释放,这会导致父类的析构函数会被执行,而子类的析构函数并不会执行,此举有可能导致程序结果并不是我们想要的。究其原因,是因为静态联编的缘故,在编译时,就知道要执行谁的析构函数。为了解决这个问题,需要把父类的析构函数变成虚拟析构函数,也就是加上
virtual
的定义。一旦父类的析构函数是虚函数,那么子类的析构函数也将自动变成虚函数。概括: 继承关系下,所有人的构造都不能是虚函数,并且所有人的析构函数都必须是虚函数。
只要在父亲的析构函数加上 virtual ,那么所有的析构函数都变成 虚函数
class WashMachine{
public:
virtual ~WashMachine(){
cout << "执行父类析构函数" << endl;
}
};
class SmartWashMachine : public WashMachine{
~SmartWashMachine(){
cout << "执行子类析构函数" << endl;
}
};
int main(){
WashMachine *w = new SmartWashMachine(); //父类指针指向子类对象
delete w; //会执行父类的析构函数
return 0 ;
}
在父类的函数上添加 virtual 关键字,可使子类的函数也变成虚函数。
一旦形成父类引用 或者 指针指向子类对象时,调用同名的虚函数,执行的是子类的虚函数,这是因为存在动态联编。 默认情况下,如果没有virtual关键设置,那么执行的是父类的函数。
如果某个类被声明成父类,那么那些要在子类重新定义的函数,应该声明为虚函数。 基本处理是:只要是同名的函数,都应该加上virtual关键字。
构造函数不能是虚函数,应该把父类的析构函数变成虚函数。
在继承关系下,子类可以重写父类的函数,但是有时候担心程序员在编写时,有可能因为粗心写错代码。所以在C++ 11中,推出了
override
关键字,用于表示,子类的函数就是重写了父类的同名函数 。 不过值得注意的是,override
标记的函数,必须是虚函数。
override
的用意并不会影响程序的执行结果,仅仅是作用于编译阶段,用于检查子类是否真的重写父类函数
class WashMachine{
public:
virtual void wash(){
cout << "洗衣机在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
void wash() override{ //表示重写父类的函数
cout << "智能洗衣机在洗衣服" << endl;
}
};
在c++11 推出了final关键字,其作用有两个: (1)、禁止虚函数被重写;(2)、禁止类被继承。
注意: 只有虚函数才能被标记为final ,其他的普通函数无法标记final。
class Person final{ //表示该类是最终类,无法被继承
};
class Student : public Person{}; //编译错误
//===============================
class Person {
virtual void run() final{ //表示该方法时最终方法,无法被重写
}
};
class Student : public Person{
void run(){ //错误
}
};
纯虚函数是一种特殊的虚函数,C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。C++中的纯虚函数更像是“只提供声明,没有实现”,是对子类的约束。
纯虚函数就是没有函数体,同时在定义的时候,其函数名后面要加上“= 0”。
例如洗衣机具有特征是洗衣服,但洗衣机不会去洗,而是特定洗衣机,例如海尔滚筒式洗衣机才会去洗,而且也必须存在洗衣服的功能(怎么洗,如何洗),所以此时要继承洗衣机这个抽象类,
class WashMachine{
public:
//没有函数体,表示洗衣机能洗衣服,但是具体怎么洗,每个品牌不一样
virtual void wash() = 0;
};
class HaierMachine:public WashMachine{
public :
virtual void wash(){
cout << "海尔牌洗衣机在洗衣服" << endl;
}
};
class LittleSwanMachine:public WashMachine{
public :
virtual void wash(){
cout << "小天鹅洗衣机在洗衣服" << endl;
}
};
int main(){
//WashMachine w; 错误,抽象类无法创建对象
WashMachine *w1 = new HaierMachine() ;
WashMachine *w2 = new LittleSwanMachine() ;
return 0 ;
}
- 如果有一个类当中有纯虚函数,那么这个类就是抽象类
- 抽象类是无法创建对象的,因为一旦能够创建对象,里面的纯虚函数没有函数体,也就不知道要执行什么逻辑了,所以禁止抽象类创建对象。
- 抽象类当中也可以有普通的成员函数,虽然父类不能创建对象,但是子类可以创建,所以这些函数可以由子类访问。
- 如果一个子类继承了一个父类(父类是抽象类),那么子类就必须重写所有的纯虚函数,否则视子类为抽象类,因为继承体系下,等同于子类拥有了和父类一样的代码。
所谓接口,其实就是用于描述行为和功能,并不会给出具体的实现。C++中没有提供类似
interface
这样的关键字来定义接口 , 纯虚函数往往承担起了这部分功能。抽象类可以用来定义一种事物的行为特征,例如:洗衣机: 洗衣服。
class Person{
Person() {}; // 可以用于初始化成员函数
virtual ~IPerson(){} //防止子类析构函数无法被调用问题
//每个人吃什么,做什么都不一样,,即可声明为纯虚函数
virtual void eat() = 0 ;
virtual void work() = 0 ;
...
};
c++ 把内存的控制权对程序员开放,让程序显式的控制内存,这样能够快速的定位到占用的内存,完成释放的工作。但是此举经常会引发一些问题,比如忘记释放内存。由于内存没有得到及时的回收、重复利用,所以在一些c++程序中,常会遇到程序突然退出、占用内存越来越多,最后不得不选择重启来恢复。造成这些现象的原因可以归纳为下面几种情况:
- 野指针: 内存已经被释放、但是指针仍然指向它。这时内存有可能被系统重新分配给程序使用,从而会导致无法估计的错误
- 重复释放:程序试图释放已经释放过的内存,或者释放已经被重新分配过的内存,就会导致重复释放错误.
- 内存泄漏: 不再使用的内存,并没有释放,或者忘记释放,导致内存没有得到回收利用。 忘记调用delete
随着多线程程序的广泛使用,为了避免出现上述问题,c++提供了智能指针,并且c++11对c++98版本的智能指针进行了修改,以应对实际的应用需求。
在98版本提供的
auto_ptr
在 c++11得到删除,原因是拷贝是返回左值、不能调用delete[] 等。 c++11标准改用unique_ptr
|shared_ptr
|weak_ptr
等指针来自动回收堆中分配的内存。智能指针的用法和原始指针用法一样,只是它多了些释放回收的机制罢了。智能指针位于 头文件中,所以要想使用智能指针,还需要导入这个头文件
#include
unique_ptr
是一个独享所有权的智能指针,它提供了严格意义上的所有权。也就是只有这个指针能够访问这片空间,不允许拷贝,但是允许移动(转让所有权)。
unique_ptr p(new int(10)); //指针p无法被复制
unique_ptr p2 = p ; //错误
cout << *p << endl; //依然可以使用 解引用获取数据。
unique_ptr p3 = move(p) ; // 正确,至此,p将不再拥有控制权。
cout << *p3 << endl; // p3 现在是唯一指针
cout << *p << endl; // p 现在已经无法取值了。
p3.reset(); // 可以使用reset 显式释放内存。
p3.reset(new int(6));
p3.get() ; // 可以获取到指针存放的地址值。
shared_ptr
: 允许多个智能指针共享同一块内存,由于并不是唯一指针,所以为了保证最后的释放回收,采用了计数处理,每一次的指向计数 + 1 , 每一次的reset会导致计数 -1 ,直到最终为0 ,内存才会最终被释放掉。 可以使用use_cout
来查看目前的指针个数
shared_ptr s1(new int(3));
shared_ptr s2 = s1;
s1.reset();
s2.reset(); // 至此全部解除指向 计数为0 。
cout << *s1 << endl; //无法取到值
对于引用计数法实现的计数,总是避免不了循环引用(或环形引用)的问题,即我中有你,你中有我,
shared_ptr
也不例外。 下面的例子就是,这是因为f和s内部的智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。
class son;
class father {
public:
father(){cout <<"father 构造" << endl;}
~father(){cout <<"father 析构" << endl;}
void setSon(shared_ptr s) {
son = s;
}
private:
shared_ptr son;
};
class son {
public:
son(){cout <<"son 构造" << endl;}
~son(){cout <<"son 析构" << endl;}
void setFather(shared_ptr f) {
father = f;
}
private:
shared_ptr father;
};
int main(){
shared_ptr f(new father());
shared_ptr s(new son());
f->setSon(s);
s->setFather(f);
}
为了避免
shared_ptr
的环形引用问题,需要引入一个弱指针weak_ptr,它指向一个由
shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个
weak_ptr绑定到一个
shared_ptr不会改变
shared_ptr的引用计数。不论是否有
weak_ptr指向,一旦最后一个指向对象的
shared_ptr被销毁,对象就会被释放。从这个角度看,
weak_ptr更像是
shared_ptr`的一个助手而不是智能指针。
class father {
public:
father(){cout <<"father 构造" << endl;}
~father(){cout <<"father 析构" << endl;}
void setSon(shared_ptr s) {
son = s;
}
private:
shared_ptr son;
};
class son {
public:
son(){cout <<"son 构造" << endl;}
~son(){cout <<"son 析构" << endl;}
void setFather(shared_ptr f) {
father = f;
}
private:
//shared_ptr father;
weak_ptr father; //替换成weak_ptr 即可。
};
int main(){
shared_ptr f(new father());
shared_ptr s(new son());
f->setSon(s);
s->setFather(f);
}
在C++中内存分为5个区,分别是
堆
、栈
、全局/静态存储区
和代码|常量存储区
|共享内存区
。栈区:又叫堆栈,存储非静态局部变量、函数参数、 返回值等,栈是可以向下生长的
共享内存区:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内 存,做进程间通讯
堆区:用于程序运行时动态内存分配,堆是可以向上增长的
静态区:存储全局数据和静态数据
代码区:存储可执行的代码、只读常量
- 从栈上分配:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是栈本身的容量有限
- 从堆上分配:动态成员存储位置,程序在运行的时候用
malloc
或new
申请任意多少的内存空间,程序员自己负责在何时用free
或delete
释放内存.动态内存的生存期由用户决定,使用非常灵活,但问题也最多。- 从静态区上分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量
在 c++ 中 , 如果要在堆内存中申请空间,那么需要借助
new
操作符,释放申请的空间,使用delete
操作 。而c语言使用的是malloc
和free
,实际上new
和delete
的底层实际上就是malloc
和free
。
在c++中, new是一个关键字,同时也是一个操作符,用于在堆区申请开辟内存 , new的操作还具备以下几个特征:
- 内存申请成功后,会返回一个指向该内存的地址。
- 若内存申请失败,则抛出异常,
- 申请成功后,如果是程序员定义的类型,会执行相应的构造函数
int *a = new int();
stu *s = new stu();
//new的背后先创建
在c++中,
delete
和new
是成对出现的,所以就有了no new no delete
的说法。delete
用于释放new
申请的内存空间。delete
的操作具备以下几个特征:
- 如果指针的值是0 ,delete不会执行任何操作,有检测机制
- delete只是释放内存,不会修改指针,指针仍然会指向原来的地址
- 重复delete,有可能出现异常
- 如果是自定义类型,会执行析构函数
int *p = new int(6);
delete p ; // 回收数据
*p = 18 ; //依然可以往里面存值,但是不建议这么做。
malloc
和 free 实际上是C语言 申请内存的语法,在C++ 也得到了保存。只是与 new 和 delete 不同的是, 它们 是函数,而 new 和 delete是作为关键字使用。 若想使用,需要导入#include
- malloc 申请成功之后,返回的是void类型的指针。需要将void*指针转换成我们需要的类型。
- malloc 要求制定申请的内存大小 , 而new由编译器自行计算。
- 申请失败,返回的是NULL , 比如: 内存不足。
- 不会执行自定义类型的构造函数
int *p=(int *)malloc(int); //如果申请失败,返回的是NULL
free 和 malloc是成堆出现的,所以也有了 no malloc no free的说法。 free 用于释放 mallo申请的内存空间。
- 如果是空指针,多次释放没有问题,非空指针,重复释放有问题
- 不会执行对应的析构
- delete的底层执行的是free
free (p);
在申请动态内存时,new有一些灵活上的缺陷,其中一方面是它将内存分配和对象构造绑定在一起,delete将对象的析构和内存释放绑定到一起。如果是单个对象,那么这无可厚非。 但是如果分配一大片内存时,通常都想按需去构造对象,而不是把整个空间都构造了。避免了内存的浪费。
如下:开辟了10个空间,但是只使用了3个位置。
//动态数组:
stu *s = new stu[5]; //分配5个位置,打算存储5个学生对象
s[0] = son()
allocator 是定义在 头文件 中的一个类, 它可以做到将内存分配和对象构造分离出来。它分配的内存是原始的,未构造的。与vector相似,它也是一个模板类,所以在使用的时候,需要制定分配内存的类型是什么类型。它会根据给定对象的类型来确定恰当的内存大小。分配成功后,返回一个指向内存区域的一个指针。
allocator | 定义一个名为a的allocator对象,它可以为类型为T的对象分配内存 |
---|---|
a.allocate(n) | 分配一段连续的为构造的内存,能容纳n个类型为T的对象 |
a.deallocate(p, n) | 释放从指针p中地址开始的内存,这块内存保存了n个类型为T的对象。p必须是以个先前有allocate返回的指针,而且n必须是创建p时所要求的大小。在调用deallocate以前,用户必须对每个在这块内存中创建的对象调用destroy |
a.construct(p, args) | p即分配内存返回的指针,可以通过指针运算进行偏移,args 即构造对象使用的参数,用来在p指向的内存块中构造一个对象。 |
a.destroy§ | p为类型为T的指针,对p指向的对象执行析构函数。 |
//创建allocator对象
allocator sa;
//申请分配一块5个连续的内存地址
son *p = sa.allocate(5);
//往第一个位置构造一个son
sa.construct(p , "张三",18); //第一个位置
sa.construct(p+1 , "李四",19); //第二个位置
sa.construct(p+4 , "王五",19); //第5个位置
//销毁对象,执行析构函数
sa.destroy(p);
sa.destroy(p+1);
sa.destroy(p+4);
//释放内存,标记这些内存可用
sa.deallocate(p , 5);