默认函数是指如果设计者不提供,系统会默认提供,如果提供了,系统就不提供了。主要有6个:
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符的重载函数
- 取地址操作符的重载函数
- const修饰的取地址操作符的重载函数
这篇博文的全部函数讲解,主要使用商品类进行示例讲解:
//商品类
class CGoods
{
public:
void show()
{
std::cout<<"商品名称"<<mname;
std::cout<<"商品价格"<<mprice;
std::cout<<"商品数量"<<mamount<<std::endl;
}
private:
char* name;//商品名称
double mprice;//商品价格
int mamount;//商品数量
};
商品类中有一个char*指针,指向保存商品名称的内存,那么就会存在字符串拷贝函数,我们介绍常用的两种,第一种strcpy函数不安全,第二种strcpy_s是一种安全的拷贝函数:
- strcpy()
char *strcpy(char *dst,const char *src)
将以src为首地址的字符串复制到以dst为首地址的字符串,包括’\0’结束符,返回dst地址。
要求:src和dst所指内存区域不可以重叠且dst必须有足够的空间来容纳src的字符串,若dst空间不足,编译时并不会报错,但执行时因系统不同会出现不同的结果,如错误地改变了程序中其他部分的内存的数据,导致程序数据错误,非法内存访问抛出异常。故这个函数不安全。
- strcpy_s()
strcpy_s( char *dst, size_t num, const char *src )
功能:同strcpy()函数功能相同,不同之处在于参数中多了个size_t类型的参数,该参数为字符串dst的长度,当存在缓存区溢出的问题时(即src的长度大于dst的长度),strcpy_s()会抛出异常。是一种安全的拷贝函数。故建议使用第二种。
一、构造函数
(一)构造函数的作用:初始化对象所占的内存空间,将属性进行初始化,即赋予对象所占的内存空间资源。所以当构造函数没有调用完成,我们说这个对象是不完整的,半成品对象。
(二)主要用法:
【1.格式】
函数名和类名一样,无返回值,形参自己设计。我们设计商品类的构造函数,将定义的成员变量进行初始化,代码如下:
CGoods(char* n,double p,int a)
{
mname=new char[strlen(n)+1]();
strcpy_s(mname,strlen(n)+1,n);
mprice=p;
mamount=a;
std::cout<<"CGoods"<<std::endl;
}
【2.使用】
- 生成对象,自动调用构造函数
- 系统为一个对象调用一次来分配资源,不可能存在一个对象调用两次构造函数
【3.系统提供的构造函数】
-
格式:不带参数,无函数体,【类名(){} 】,如商品类的默认构造函数:
CGoods() {}
-
如何调动默认的构造函数:
(1)错误调用:加(),是一个函数声明,并不是调用默认的构造函数
(2)正确的是不加(),即不加形参列表CGoods good2;//调用默认的构造函数
-
如果我们提供了构造函数,那么系统就不会提供默认的构造函数了。
CGoods good1("面包",5.5,10); //调用自己写的三个参数的构造函数,生成对象并初始化成员变量
将系统的构造函数称为默认的构造函数。
【4.构造函数可以重载吗?】
可以,因为对每一个对象的初始化可以不一样
(三)生成对象有几步:
- 开辟对象所占的内存空间。
- 调用构造函数(初始化对象所占的内存空间)
(四)什么是对象
了解了构造函数后,可以更清晰的给对象下定义:对象=空间+资源。
- 定义变量,可以只开辟空间,不赋予资源;
- 实例化对象,开辟空间并赋予资源
二、析构函数
(一)构造函数的作用:释放对象的资源,一旦开始调度析构,那么对象就不完整了因为对象是空间+资源。
(二)主要用法:
【1.格式】
【~类名(){}】,无返回值,商品类的析构函数如下:
~CGoods()
{
delete[] mname;
mname=NULL;
std::cout<<"~CGoods"<<std::endl;
}
【2.使用】
- 析构函数在什么时候被调用:程序结束时调用。在销毁局部变量时,局部变量在栈上存放,系统可以自己清理,但是其他资源需要析构释放,所以在return 0后进行内存的释放后调用析构函数。
- 可以人为调用,但是人为调用后,系统还是会在最后调用,那么可能出现一块内存被释放两次的状况,直接报错。
【3.系统提供的构造函数】
~CGoods(){}
系统自动调用。
【4.析构函数可以重载吗?】
不可以,因为对象消亡的方式都一样。只有一个原型,那就是不带参数,无返回值的析构函数。
(三)对象的销毁有几步:
-
调用析构函数,释放对象所占的其他资源,如堆,那么系统函数中调用delete即可。
-
系统释放对象所占的空间,释放栈上内存。
三、构造和析构的区别?
【1. 构造函数】
- 初始化对象
- 可以重载,生而不同
- 有this指针,虽然此时对象不完整,但是对象空间已经存在了,指向对象空间。
- 构造函数不可以人为调用,必须系统进行调用,生成第一个对象。因为:构造函数遵循thiscall调用约定,依赖对象调用,调用构造函数时,对象还没有生成。表示此时不能人为【对象.构造函数】调用构造函数,这时就会死循环,你要调用构造函数,需要对象,但是构造函数调用了才有对象。所以构造函数不可以认为调用,必须系统进行调用,生成第一个对象。
- 不可以成为虚函数,因为此时没有生成。
【2. 析构函数】
- 释放对象所占的资源
- 不可重载,亡而单一
- 有this指针
- 可以人为调用,因为在调用前,对象是完整的,调用析构函数后对象才开始慢慢消亡。但是会退化为普通函数调用,你自己调用了析构函数后,系统在return 0结束后,还会再调用一次。
- 可以成为虚函数,因为此时对象存在且可以取地址。
【3.构造函数和析构函数的顺序:】
对象空间在栈上开辟空间,故根据栈先进后出的特性,故 先构造的后析构。
四、拷贝构造函数
(一)拷贝构造函数的作用:
拷贝构造函数是一个特殊的构造函数,用一个已存在的对象生成相同类型的新对象。
(二)系统提供的拷贝构造函数- - 浅拷贝:
现在我们写了两个构造函数:无参,3个参数的构造函数。运行下面的代码:
CGoods good1("面包",5.5,10);
CGoods good2=good1;
用good1对象生成good2对象,运行出错但编译正确。good2生成对象时,需要调用构造函数,但是此时我们并没有写合适的构造函数,但是编译却正确了,表示系统中为我们提供了拷贝构造函数,运行错误说明,系统提供的拷贝构造函数处理CGoods时存在缺陷。
我们可以推测系统提供的拷贝构造函数的原型:
- 本质上还是构造函数,所以函数名和类名一样,无返回值。
- 形参:用good1对象生成good2,那么拷贝构造函数的形参是good1对象,因为赋值不会改变值,所以加const,加引用(下面讲解)
那我们就可以写出系统拷贝函数的代码,并进行分析,如下图:
可以看到 系统默认的拷贝构造函数是一种浅拷贝,即简单的赋值操作,两个指针指向相同的地址。那么释放的时候,会对同一块堆空间面包释放两次,故运行失败。所以当需要开辟其他内存资源时,如成员变量有指针类型来动态开辟堆,就应该考虑实现深拷贝。
(三)拷贝构造函数的原型:
原型为:【类名(const 类名& 变量名)】,如商品类的拷贝构造函数:
CGoods(const CGoods& temp)
为什么拷贝构造函数形参一定要是引用?
传递一个对象进去,那么表示方式无非就三种:
【1. 传入类名】
用good1生成good2对象,调用拷贝构造函数,那么调用过程如下所示:
如果没有引用,那么形参传实参的过程,也是一个初始化生成对象的过程,系统自动调用拷贝构造。const CGoods temp=good1,那么这又是一个用已生成对象初始化另一个对象,又要去调用拷贝构造函数,这样就形成了一个递归,那么递归调用生成形参对象,形参永远不能生成,就这样不断调用,直至栈溢出,程序出错。
【2. 使用指针】
使用指针,需要传递地址进去,那么和拷贝构造函数的概念违背,拷贝构造函数是拿对象生成对象,不是拿对象地址生成对象。所以不能用指针。
【3.使用引用】
加了引用就表示给good1对象起别名temp,在对temp操作时,就是对good1操作,就是利用good1来生成新对象,并不需要赋值这一步,所以就不会形成递归调用拷贝构造来实参初始化形参。
所以拷贝构造函数的形参一定需要用&修饰。
(四)根据情况实现拷贝构造函数- -深拷贝:
系统默认的拷贝构造函数是浅拷贝,当需要开辟其他内存资源时,如成员变量有指针类型来动态开辟堆,释放时就会出错,这时就应该考虑实现深拷贝,开辟内存,让每个对象拥有自己的其他资源,如堆。那么商品类的深拷贝如下:
这样就不会出现一个内存块被释放两次,出现错误的情况。
五、赋值运算符重载函数
(一)赋值运算符重载函数的作用:
用已存在的对象赋给相同类型的已存在对象
(二)系统提供的赋值运算符重载函数 --浅拷贝
两个对象进行赋值操作,代码如下:
CGoods good1("面包",5.5,10);
CGoods good2("牛奶",6.6,20);
good2=good1;
都是已经存在的对象,所以称为赋值。这段代码,运行出错,表示系统知道如何将good1对象赋给good2,代表系统提供了赋值运算符重载函数,只是使用系统提供的函数不能处理商品类,所以运行出错。
那么我们可以推测赋值运算符重载函数的函数原型:
- operator是重载运算符的关键字,将=重载为可以处理类类型的函数。
- 形参需要传入一个对象引用。
- this指向左操作数,形参指向右操作数
- 返回值是一个对象引用(下面讲了原因)
系统提供的运算符重载函数也是一个浅拷贝,只是将goo1的值赋值给good2,这样就会出错,因为直接将good2的指针指向good1的堆内存,那么就会导致good2存放牛奶的堆内存无人管理,造成内存泄露,面包这块堆内存被两个对象指向,最后析构两次,释放两次,出错。
(三)赋值运算符重载函数的原型:
原型:类名& operate=(const 类名& 变量名,……),如商品类的赋值运算符重载函数:
CGoods& operator=(const CGoods& rhs)
【1. 参数加const的作用:】
- 防止形参修改实参的值
- 接收隐式生成的临时量,因为隐式生成的临时量是常量,只有将const和&结合为常引用才可以引用常量,我们在const章节讲过这种引用。
【2. 返回值:【类名&】的原因】
为了处理连续赋值的情况,如
从右至左进行处理,先进行good2=good1的赋值,然后用good2对象对good3赋值,所以在第一次调用赋值函数之后得到good2本身,那么就需要给good2起一个别名,那么就需要返回good2一样的类型,然后加上&即可。
(四)根据情况实现赋值运算符重载函数- -深拷贝:
系统提供的赋值运算符重载函数出错原因是没有给每个对象分配独立的堆空间,释放时出错,那么深拷贝函数就需要让对象拥有自己的空间,实现步骤是:
- 判断是否是自赋值,即是一个对象,那么没有必要赋值。
- 先释放原来旧的内存(除了栈以外的),然后根据新对象的数据大小开辟新内存,防止出现旧内存不能完全存储新对象的数据,出现内存溢出。
- 将新对象的内容赋值到旧对象开辟的内存上
那么商品类的赋值运算符重载函数为:
CGoods& operator=(const CGoods& rhs)
{
if(this != &rhs)
{
delete[] mname;
mname=new char [strlen(rhs.mname)+1]();
strcpy_s(mname,strlen(rhs.mname)+1,rhs.mname);
mprice=rhs.mprice;
mamount=rhs.mamount;
}
std::cout<<"CGoods ="<<std::endl;
return *this;
}
六、取地址操作符的重载函数、const修饰的取地址操作符的重载函数(未讲解)
这两个函数使用的很少,几乎不用,后面用到的时候再讲解。
七、商品类的实现
我们将商品类补充完整:
# include<iostream>
# include<cstring>
class CGoods
{
public:
CGoods(char* n,double p,int a)
{
mname=new char[strlen(n)+1]();
strcpy_s(mname,strlen(n)+1,n);
mprice=p;
mamount=a;
std::cout<<"CGoods"<<std::endl;
}
CGoods(const CGoods& temp)
{
mname=new char [strlen(temp.mname)+1]();
strcpy_s(mname,strlen(temp.mname)+1,temp.mname);
mprice=temp.mprice;
mamount=temp.mamount;
std::cout<<"CGoods& "<<std::endl;
}
CGoods& operator=(const CGoods& rhs)
{
if(this != &rhs)
{
delete[] mname;
mname=new char [strlen(rhs.mname)+1]();
strcpy_s(mname,strlen(rhs.mname)+1,rhs.mname);
mprice=rhs.mprice;
mamount=rhs.mamount;
}
std::cout<<"CGoods ="<<std::endl;
return *this;
}
~CGoods()
{
delete[] mname;
mname=NULL;
std::cout<<"~CGoods"<<std::endl;
}
void show()
{
std::cout<<"商品名称"<<mname;
std::cout<<"商品价格"<<mprice;
std::cout<<"商品数量"<<mamount<<std::endl;
}
private:
char* mname;//商品名称
double mprice;//商品价格
int mamount;//商品数量
};
int main()
{
CGoods good1("面包",5.5,10);
CGoods good2=good1;//调用拷贝构造函数
CGoods good3("牛奶",6.6,20);
good3=good2;//调用赋值运算符重载函数
good1.show();
good2.show();
good3.show();
}
加油哦!🥡。