文章目录
- 类的6个默认成员函数
- 构造函数
- 总结构造函数
- 析构函数
- 总结析构函数
- 拷贝构造函数
- 总结拷贝构造函数
- 赋值运算符重载
- 取地址重载和const取地址重载
类的6个默认成员函数
一个什么都不写的类我们称之为“空类”
class Test
{}
我们什么都没写,这里看着空空的,可是它真的什么都没有吗?
所以今天我们来探究一下,类里面都有那些好玩的
构造函数
什么叫做构造函数,说白了构造->初始化。就这么理解,构造函数的作用就是初始化函数且在对象整个生命周期内只会调用一次构造函数,那么它是如何实现呢,我们看看(用的是日期)
class Date
{
public:
//这就是一个构造函数的雏形Date(){}
private:int _year;int _month;int _day;
};
它是由类名当作名称的一个函数,那么怎样体现它的初始化呢?
class Date
{
public:
//这就是一个构造函数的雏形Date(int year,int month,int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{
//对一个对象进行实例化Date p1(2024,5,1);return 0;
}
这就是最基础能体现构造函数方法的一串代码
结果如下:
我们发现我们并没有显示的去调用这个函数,就是说我们并没有在
Data p1(2024,5,1)这行代码下面去写Date()我们没有调用是编译器自动帮我们去调用的,这样一看还挺智能奥,这样就有效的阻止我们有时候忘记初始化的问题。
问题又来了,那么为什么说它是我们类中的一个默认成员函数呢?我们不写构造函数,这个类里真的还有默认构造函数吗?我们来看这样一个测试
class Date
{
private:int _year;int _month;int _day;
};
int main()
{Date p1;return 0;
}
我们发现了什么?这代码编译居然能过?说明我们这里产生了默认构造这个默认的构造函数是无参的,也就是说你别乱写给个参数那指定报错,但是值是随机值有兴趣可以自己调试看看。
有人问了,它给个随机值,也没啥用,那么这个
默认的构造函数有什么用呢?
我们知道C++将类型分为内置类型和自定义类型,什么是内置类型?(int,char,float…)什么是自定义类型?显而易见就是我们的类或者结构体
那么如果是这种情景呢?
class Date
{
public://无参的构造函数Date(){cout << "我是不带参的" << endl;}//带参的构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;cout << "我是带参的" << endl;}
private:int _year;int _month;int _day;
};class T
{
private:
int hour;
int minute;
int second;
Date p;
};
这就有意思了我们一个类中的成员变量是另一个自定义类型的对象,那么当我们在这里不显示的写构造函数的时候我们去调用会发现,这里的自定义类型的对象会去调用它的构造函数(不是默认构造),对于我们的内置类型却不做任何处理,也就是和之前的默认构造一样但是对于自定义类型会去调用它的构造函数,这很有意思。
看见了嘛,这里是我不是带参的这句话输出了两次,这可以说明默认构造函数的价值还是很大的,它对内置类型不做处理,对于自定义类型会去走自定义类型的默认构造
回来再看如果我们放开前面写的构造函数看看。
这里就会报错,但是报错的原因是什么呢?没有合适的默认构造可用,这说明当我们显示的写了一个构造函数之后编译器就不会生成那个默认的构造函数,这就导致了如果我们写了构造函数我们就需要传参,实例化一个完整的对象
注意:
构造函数也可也重载
我们来举个例子
这里就能看出来,两个不同的对象调用了不同的构造函数
总结构造函数
用途:初始化
特性:不显示的写编译器可以默认生成一个
无返回值
函数名就是类名
可以进行重载
析构函数
刚说完构造函数的作用就相当于初始化,那么析构函数就是它的死对头,它是用来销毁的,在我们以前手写栈的时候我们会用到自己写的Destroy()函数用来释放我们动态内存开辟的空间,但是当我们有了析构函数我们就不需要再去显示的调用这个Destroy()函数了,编译器会自己调用。
我们先写个栈的雏形,但是我们不实现奥,要看栈的完整实现,可以点这里用的是C语言
我们来继续讨论这里的析构函数的写法以及特点
class Stack
{
public:Stack(){int* tmp;tmp = (int*)malloc(sizeof(int) * 4);if (tmp == nullptr)exit(-1);_arry = tmp;_size = 0;_capacity = 4;cout << "Stack()" << endl;}
//这就是析构函数---我们的Destroy~Stack(){free(_arry);_size = 0;_capacity;cout << "~Stack()" << endl;}void Stack_Push(int x){////......//}private:int* _arry;int _size;int _capacity;
};int main()
{Stack st;return 0;
}
我们编译一下
如果说构造函数你有把握你不会忘记init()也就是你不会忘记初始化,但是当程序写的七七八八的时候你是否会忘记要释放内存呢?如果导致内存泄漏了呢?这些都是问题,但是如果你写了析构函数那么,我们就不用担心遗忘的问题,编译器会自动调用这里的析构函数。
那么我们如何能看到编译器自带的默认析构函数呢?
class Date
{
public:Date(){}Date(int year, int month, int day){_year = year;_month = month;_day = day;}~Date(){cout<<"~Date()"<<endl;}
private:int _year;int _month;int _day;
};class T
{
private:
int hour;
int minute;
int second;
Date p;
};
有意思吧,我们没有去调用这里的析构函数,但是会有存在在T这个类的默认析构函数的调用,这里的默认析构函数和构造函数一样对于自定义类型会去找其析构函数,对于自定义类型由于内置类型销毁时不需要资源的清理,OS直接回收内存就行所以对于内置类型不做处理,这里是T的默认析构,调用了Date的析构函数,也就是说默认析构是存在的
那么析构函数可以重载吗??
当然是不可以的
析构函数只能有一个
总结析构函数
作用:Destroy,进行内存的回收或者是资源的清理
特性:
函数名就是类名前面加一个~取反的那个符号或者是说小波浪号
不可以重载析构函数
没有返回值
在对象生命周期结束的时候自动调用
拷贝构造函数
这可是个大头,很多人都可能不太能理解什么是拷贝构造函数,为此我讲这个也想了很久该怎么叙述这个概念
什么是拷贝构造函数,它和构造函数有啥区别?
拷贝构造是构造函数的一种重载形式
那么为什么要有拷贝构造构造函数不够我们用的吗?
我们先来看看写法
class Date
{
public:Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;cout << "Date(const& )" << endl;}Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};int main()
{Date p1;Date p2(p1);return 0;
}
当我们用一个自定义类型的对象作为参数去初始化另一个自定义类型的对象的时候我们就会调用我们的拷贝构造,它本质作用起始也是初始化,但是它的用处有很多可以作为返回值,参数。。。。
刚刚是我们主动写的拷贝构造,来我们看看编译器自主实现的默认拷贝构造
可以看到我们利用p1去初始化p2是可以成功的,但是我们并没有显示的写出来拷贝构造,这也能说明默认拷贝构造的存在
但是
这里能看出来他们的地址并不指向同一块空间,这说明编译器自带的默认拷贝构造是一种浅拷贝也就是值的拷贝,这种默认拷贝构造可以完成很大多数的情况,但是我们来看看这种
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}
既然说过了默认拷贝构造是将值进行拷贝,那么通过s1去初始化s2的时候,两个array都指向了同一块空间,那么我们曾经写过的析构函数登场了,既然你有两个对象,那么我就要析构两次没问题吧?s2先销毁,那么析构函数第一次就把array这块儿空间给销毁了,那么在销毁s1的时候,析构函数它又来了,又得销毁一次,同一块空间可以被销毁两次吗??所以这就能说明在没有进行动态内存申请的时候我们利用默认的拷贝构造是可以完成大部分事情的,但是当涉及到动态内存申请的时候我们就必须得去显示的写出拷贝构造。
那么还有一个问题我们需要解决那就是为什么拷贝构造的参数是一个同类类型的引用了
拷贝构造的参数只能是同类类型的引用,第一点是因为你只能拿同类型的去初始化一个同类型的对象这是第一点,当你用传值的方式去初始化的时候那么就会引发一种无穷递归的情况。
这是为什么呢?
当你的参数是同类型的对象的时候,形参是实参的一份临时拷贝,你得先拷贝到这个临时对象上才能进行操作
也就是
实参 ——>拷贝——>形参
这么一个过程但是你拷贝的时候需要调用拷贝构造因为你在用一个同类型去初始化一个同类型的对象,那么好了你拷贝的时候又建立了一个拷贝构造的对象,这个拷贝构造的对象作为实参又要去拷贝一个对象当作形参,在这个中途就会引发无穷递归根本到不了形参这一步,所以我们用的是传引用当作参数,直接让这个形参就是实参的别名,这样就不用经过拷贝这一步就不会引发无穷递归
总结拷贝构造函数
作用:仍然是初始化,但是和构造函数有不同,作用点也不一样
特性:
是构造函数的一种重载
当用同类型的对象去初始化另一个同类型的对象的时候会调用
同类型的对象作为返回值的时候会调用
同类型的对象做参数的时候会调用
这里还有很多好玩的比如说编译器的自我优化也会提到构造函数和拷贝构造目前就讲这些,后面会提及
赋值运算符重载
之前我们写过运算符重载的篇章可以运算符重载
点击这里可以看到,但是我们还是要单拿赋值运算符重载来说一下,因为它是编译器默认生成的一个成员函数,我们可以测试一下看能否看见这一现象
我们可以发现我们没有写赋值操作符,但是我们去直接使用的时候居然编译通过了,说明在这个类中有一个不显示写的赋值运算符,但是它也是浅拷贝,也就是把值赋值给了另一个对象,但是大部分操作的时候这个功能就够用了
所以我们知道编译器帮助我们实现了一个赋值运算符的重载就好还有两个重载分别是取地址重载和const取地址重载
取地址重载和const取地址重载
其实这两个部分都没什么好讲的,编译器自主实现的这两个运算符重载就足够满足大多数的情况了,就是取这个对象的地址的时候就可以调用
class Date
{
public :Date* operator&(){return this ;}const Date* operator&()const{return this ;}
private :int _year ; // 年int _month ; // 月int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!