目录
类中的默认函数
在C++中,一个类有八个默认函数:
- 默认构造函数;
- 默认析构函数;
- 默认复制构造函数;
- 默认重载赋值运算符函数;
- 默认重载取址运算符函数;
- 默认重载取址运算符const函数;
- 默认移动构造函数(C++11);
- 默认重载移动赋值操作符函数(C++11)。
假设我们现在有如下一个学生类。
- string类型的名字变量
- int类型的年龄
- int类型的科目数量
- 指针类型的成绩
- 还有一个成员函数show(),可以输出这个学生的基本信息。
class stu{
public:
void show(); //输出学生的基本信息
private:
string name; //姓名
int age; //年龄
int num; //科目数量
int *src; //num科的成绩分别为src[i]
};
void stu::show(){
cout<<name<<" "<<age<<endl;
cout<<num<<" : ";
for (int i=0; i<num; i++)
cout<<src[i]<<" ";
cout<<endl;
}
我们用这个例子来手动实现类中的默认函数。
1. 构造函数
- 构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户调用它,在建立对象时自动执行。
- 构造函数的名字与类名相同,没有类型,没有返回值。构造函数一般声明为public。
- 构造函数可以重载,提倡用带默认参数的构造函数,比较省事。
构造函数的主要作用是初始化,我们为学生类定义如下构造函数:其含有默认参数,并且对src进行内存空间的动态分配。
stu(const char* a="std",int b=20, int n=3){//默认名字是std 默认20岁 科目数量为3
name=a;
age=b;
num=n;
src=new int[n];
for (int i=0; i<n; i++) src[i]=100; //这里假设每科成绩都为100(当然也可以自己写入)
}
2. 析构函数
析构函数和构造函数一样:没有返回值,且不带参数。在类名的函数名前加一个取反运算符~即可。
析构函数可以重载吗?
- 不能,因为无参无返回值无类型
什么时候需要析构函数?
- 当我们的构造函数中有动态内存分配时,就一定要自己实现析构函数来释放这些空间,否则会造成内存泄漏。
- 文件占用
- 网络断开
在继承中,如果子类有申请内存,父类无法调用子类的析构函数,这时父类需要使用虚析构。(有的人也习惯把所有的析构函数写为虚函数)
哪些函数不可以成为虚函数?构造函数和析构函数可以是虚函数吗?
在本例中,因为src是动态分配的内存,所以必须实现析构函数来释放申请的空间。
~stu(){
if (src!=NULL)
delete []src;
src=NULL;
}
注意:delete指针之后,要把指针设为NULL。因为我们在删除一个指针之后,编译器只会释放该指针所指向的内存空间,而不会删除这个指针本身。空间被回收再分配,会出现两个指针指向同一地址的情况。
解析C++中 new和delete 以及 malloc和free 的使用方法和区别
3. 复制构造函数
复制构造函数的调用分以下三种情况:
- 一个对象以值传递的方式传入函数体
- 一个对象以值传递的方式从函数返回
- 一个对象需要通过另外一个对象进行初始化。
默认的复制构造函数是浅拷贝的,它把对象里的值完全复制给另一个对象。
在本例中,浅拷贝不能实现我们的需求,因为src是一个指针,如果仅仅是拷贝地址的话,会让两个对象指向同一堆中的内存。在析构时,会产生错误。
所以我们需要自己写复制构造函数来实现深拷贝。
深拷贝和浅拷贝 默认拷贝构造函数和自定义拷贝构造函数【有代码实例】
stu(const stu& A){
name=A.name;
age=A.age;
num=A.num;
src=new int[num];
for (int i=0; i<num; i++)
src[i]=A.src[i];
}
这里说明一下复制构造函数的参数问题。
为什么要用引用?
可能你的第一反应用引用是为了减少一次内存拷贝。其实不然。
我们都知道参数的值传递需要用到复制构造函数,试想如果复制构造函数的参数传递也是值传递方式,那么就会无限递归下去,所以必须使用地址传递(也就是引用)。
学会了引用,再也不用担心令人烦恼的C++指针了!!!为什么要用const?
不加const,编译器也不会报错。但是为了防止对引用类型参数值的意外修改,一般都加上const。
4. 赋值操作符重载函数
如果自定义了复制构造函数,那也必须重载赋值操作符。
复制构造函数是用已存在的对象来初始化新对象。而赋值运算符是使用已存在的对象赋值给相同类型的已存在对象。
其流程是:
- 判断是否为自赋值
- 释放掉旧的堆空间
- 开辟新的空间
- 将要赋值的内容拷贝到被赋值的对象中
其参数为const引用,防止修改实参;返回值也为引用,目的是实现链式表达式。
stu& operator=(const stu& A){
if (this!=&A){
name=A.name;
age=A.age;
num=A.num;
if (src!=NULL) delete src; //释放旧的堆空间
src=new int[num];
for (int i=0; i<num; i++)
src[i]=A.src[i];
}
return *this;
}
5和6. 重载取址运算符函数和重载取址运算符const函数
重载取址运算符函数没有参数;
如果没有显式定义,编译器会自动生成默认的重载取址运算符函数,函数内部直接return this
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载(比如想让别人获取到指定的内容)。
stu* operator&(){//重载取址运算符函数
return this ;
}
const stu* operator&()const{//重载取址运算符const函数
return this ;
}
7和8. 移动构造函数和重载移动赋值操作符函数
1.C++11 新增move语义:源对象资源的控制权全部交给目标对象,可以将原对象移动到新对象, 用于a初始化b后,就将a析构的情况;
2.移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用;
3.临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候就可以使用移动构造。移动构造可以减少不必要的复制,带来性能上的提升。
样例代码全部
#include<iostream>
using namespace std;
class stu{
public:
void show();
//构造函数
stu(const char* a="std",int b=20, int n=3){
name=a;
age=b;
num=n;
src=new int[n];
for (int i=0; i<n; i++) src[i]=100;
}
//析构函数
~stu(){
if (src!=NULL)
delete []src;
src=NULL;
}
//复制构造函数
stu(const stu& A){
name=A.name;
age=A.age;
num=A.num;
src=new int[num];
for (int i=0; i<num; i++)
src[i]=A.src[i];
}
//重载赋值操作符函数
stu& operator=(const stu& A){
if (this!=&A){
name=A.name;
age=A.age;
num=A.num;
if (src!=NULL) delete src;
src=new int[num];
for (int i=0; i<num; i++)
src[i]=A.src[i];
}
return *this;
}
stu* operator&(){//重载取址运算符函数
return this ;
}
const stu* operator&()const{//重载取址运算符const函数
return this ;
}
private:
string name;
int age;
int num;
int *src;
};
void stu::show(){
cout<<name<<" "<<age<<endl;
cout<<num<<" : ";
for (int i=0; i<num; i++)
cout<<src[i]<<" ";
cout<<endl;
}
int main(){
stu A("Sam",16,10); //构造函数
A.show();
stu B=A; //复制构造函数
B.show();
stu C; //构造函数
C.show();
C=B=A; //赋值操作符重载函数
B.show();
//对象消亡自动执行析构函数
return 0;
}