类中6个默认函数,如果在定义类时没有定义这6个函数的某几个,编译器会提供相应的默认函数,
如果定义了这6个函数的某几个,编译器则不会提供相应的默认函数,
系统提供的这6个默认函数都是公有的,都是内联的
构造函数
析构函数
拷贝构造函数
赋值运算符重载函数
取地址操作符重载函数
const修饰的取地址操作符重载函数
构造函数
1.构造函数与类名相同,并且没有返回值
2.构造函数只能系统调用,不能手动调用构造函数
3.构造函数有this指针
4.构造函数可以重载
析构函数
1.析构函数的函数名是~+类名(例:class Student{};析构函数:~Student()),并且没有返回值
2.析构函数可以手动调用,但手动调用相当于调用普通成员函数,并且在对象的生命周期结束后系统会隐式调用析构函数。
注意:如果类的成员变量中有指针类型的变量,并且析构函数在释放掉对象的资源后没有将指针置为NULL,手动调用析构函数运行时会崩溃 。
例:
class Node{
public:
Node(char *str=NULL)
{
this->str=new char[strlen(str)+1]();
strcpy(this->str,str);
}
~Node()
{
delete [] this->str;
}
private:
char *str;
};
int main()
{
Node N("Hello");
N.~Node();
return 0;
}
因为手动调用析构函数释放掉对象所占的的资源,当没有将指针置为NULL时,这个指针就成为了野指针,当对象的生命周期结束后系统会隐式调用析构函数,此时释放野指针指向的内存,程序发生崩溃。
上面代码在析构函数中释放了对象所占资源后,将this->str=NULL程序就能正常运行
所以若要手动调用析构函数,当类的成员变量中有指针类型的变量,在析构函数中释放掉对象所占的资源后必须将指针置为NULL
3.析构函数有this指针
4.析构函数不能重载
构造函数与析构函数的调用顺序:
例:
#include<iostream>
class Node{
public:
Node(char *str=NULL)
{
this->str=new char[strlen(str)+1]();
strcpy(this->str,str);
std::cout<<this->str<<std::endl;
}
~Node()
{
std::cout<<this->str<<std::endl;
delete [] this->str;
this->str=NULL;
}
private:
char *str;
};
int main()
{
Node n1("n1");
Node n2("n2");
Node n3("n3");
return 0;
}
从上面的结果可以看出构造函数是和代码的运行顺序相同,析构函数刚好相反
简单来说,先构造的后析构,后构造的先析构
这是由于这些对象都是在栈上开辟内存生成的,栈的特点是先进后出
区别:
如果对象是在堆上开辟内存生成的,由于堆内存上开辟的空间不会随着函数的结束而释放,所以对象的生命周期不会结束,系统也不会隐式调用析构函数释放对象所占的资源,此时会造成内存泄漏(包括对象所占内存和他所占资源的内存)。
我们必须手动释放对象的堆内存,此时对象的生命周期结束,系统会隐式调用析构函数释放掉对象所占资源,然后再释放掉对象所占的堆内存,保证不会发生内存泄露。
此时对象调用析构函数的顺序取决于我们释放对象的堆内存顺序。
拷贝构造函数
拷贝构造函数是用以存在的对象去生成一个新对象
系统默认的拷贝构造函数是一个浅拷贝,它只将对象在栈上的数据进行拷贝,并没有将对象在堆上面的数据进行拷贝。
如果对象在堆上占有资源,我们必须手动实现深拷贝构造函数,将对象在堆上的数据也进行拷贝,
如果对象仅仅只在栈上占有资源,我们可以不手动实现拷贝构造函数而调用系统提供的拷贝构造函数
1.拷贝构造函数的函数名与类名相同,没有返回值
2.拷贝构造函数的参数必须为对象的引用,为了防止在拷贝构造函数中将已存在对象的数据修改,我们通常加上const
例:
#include<iostream>
class Node{
public:
Node(char *str=NULL)
{
this->str=new char[strlen(str)+1]();
strcpy(this->str,str);
std::cout<<this<<" "<<this->str<<std::endl;
}
~Node()
{
delete [] this->str;
this->str=NULL;
}
Node(const Node& rhs)
{
if( this!=&rhs)
{
this->str=new char[strlen(rhs.str)+1]();
strcpy(this->str,rhs.str);
}
}
void Show()
{
std::cout<<this<<" "<<this->str<<std::endl;
}
private:
char *str;
};
int main()
{
Node n1("n1");
Node n2("n2");
Node n3("n3");
Node n(n1);
n.Show();
return 0;
}
那我们能否把拷贝构造函数的参数改为对象传递,而不是一个对象的引用呢?
即将拷贝构造的函数名改为 Node(const Node rhs)
我们修改代码后编译发现程序报错,这是为什么呢?
我们可以看出,当拷贝构造函数的形参改为对象传递时,在调用拷贝构造时会一直递归构造形参对象而导致栈溢出,最终程序崩溃
赋值运算符重载函数
含义:把一个已存在对象赋值给相同类型的已存在对象
关键字:operator 函数原型 类名& operator=(const 类名&rhs)
返回值为对象引用,形参为const 修饰的对象引用
例:
#include<iostream>
class Node{
public:
Node(char *str=NULL)
{
this->str=new char[strlen(str)+1]();
strcpy(this->str,str);
std::cout<<"Node(char *str=NULL)"<<std::endl;
}
~Node()
{
delete [] this->str;
this->str=NULL;
std::cout<<"~Node()"<<std::endl;
}
Node(const Node& rhs)
{
if( this!=&rhs)
{
this->str=new char[strlen(rhs.str)+1]();
strcpy(this->str,rhs.str);
}
std::cout<<"Node(const Node& rhs)"<<std::endl;
}
Node& operator=(const Node& rhs)
{
if(this!=&rhs)
{
Node TmpNode=rhs;
char *tmp=this->str;
this->str=TmpNode.str;
TmpNode.str=tmp;
}
return *this;
}
void Show()
{
std::cout<<"show() "<<this->str<<std::endl;
}
private:
char *str;
};
int main()
{
Node n1("Hello");
Node n2("world");
n1=n2;
n1.Show();
return 0;
}
首先在函数中判断this(当前对象)!=&rhs
这是避免自赋值的情况,即 n1=n1;
在if语句中我们首先使用rhs生成了一个中间对象TmpNode
然后将中间对象的数据成员str的指向与当前对象的str指向进行交换
为什么不直接释放掉当前对象占有的资源呢?
这是为了避免在内存不足时,直接释放掉当前对象占有的资源,而在开辟新资源时开辟失败,此时
当前对象的资源就丢失了。
而先使用rhs生成了一个中间对象TmpNode,再交换中间对象的str的指向与当前对象的str指向,
当程序运行处if的外面时,中间对象出作用域,会调用析构函数释放掉资源,而此时中间对象的资源是当前对象的资源,
就释放掉当前对象的资源。
即使在内存不足时,也仅仅只是使用rhs生成了一个中间对象TmpNode时错误,程序会抛出异常,不会影响到当前对象。
问题1.返回值为什么是对象引用而不是对象?
例:在上面代码中仅仅将赋值运算符函数名改为Node operator=(const Node& rhs)
运行结果:
和未修改的运行结果对比,我们发现拷贝构造函数、析构函数多调用了一次,即多了一个对象产生,
我们知道在非类类型作返回值时,
0<返回值字节数<=4 返回值由eax寄存器带出
4<返回值字节数<=8 返回值由eax、edx寄存器带出
8<返回值字节数 返回值由临时量带出
那么在类作返回值呢?
假设由寄存器带出
在执行
Node n1("Hello");
Node n2("world");
Node n3("C++");
(n1=n2)=n3;
首先 n1.operator=(n2)
返回值由寄存器带出 然后 寄存器.operator=(n3)
我们知道在普通成员函数中有默认参数Node * const this指针指向当前对象,必须要初始化(const修饰)
而寄存器没有地址,没有办法将地址传给this指针
此时程序崩溃,而在运行上面代码时结果:
程序正常运行,比预想的结果多了两个拷贝构造函数、析构函数的的调用,即多了两个对象产生,
而且n1的str指向"world"而并非"C++",这不符合我们的需求
所以,赋值运算符重载函数返回值必须是对象引用,
如果返回对象,会产生临时对象(临时对象在内存中储存),造成结果异常。
问题2.形参是否可以改为 Node& rhs 而非const Node& rhs
例:执行
Node n1("Hello");
Node n2("world");
n1=n2;
n1.Show();
运行结果:
我们发现结果与未修改前一致
再不修改代码情况下执行
Node n1("Hello");
Node n2("world");
std::cout<<"-----------"<<std::endl;
n1="C++";
std::cout<<"------------"<<std::endl;
n1.Show();
按照我们的理解n1="C++"这句代码应该报错,因为左右类型不匹配,
但在执行时却运行成功:
因为在执行n1="C++"编译器会调用形参为一个指针类型的构造函数生成一个隐式的临时对象,即n1=Node(“C++”);
而将赋值运算符函数形参改为 Node& rhs,程序错误,
因为隐式生成的临时对象是常量,即Node &rhs=const Node(“C++”);这有修改常量内存块的风险,所以程序错误,
而如果将n1="C++"改为 n1=Node(“C++”);程序却能正常运行
因为显示生成的临时对象是变量,即Node &rhs=Node(“C++”);
所以:形参不能去掉const,加上const一方面防止修改实参的值,另一方面,可以接收隐式生成的临时对象。
点击查看临时对象详细解释
拷贝构造函数与赋值运算符的区别
当有新对象生成时,调用拷贝构造函数
即 Node n1;
Node n2=n1;
当没有新对象生成时,调用赋值运算符的重载函数
即 Node n1;
Node n2;
n2=n1;
版权声明:本文为CSDN博主「灲咲」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40874221/article/details/84504142