今天继续学习类的相关知识;
之前学习了类的一些基本知识,比如类的定义,类的内存计算,this指针之类的;
今天我们就来学习一些深入的知识——类的默认成员函数;
接下来我们就来深入了解一下这几个函数吧。
目录
构造函数
构造函数并非创建一个变量,而是将对象初始化;
每一个默认成员函数都有自己独特的声明方式;
构造函数也有自己独特的规则;
构造函数的声明规则
1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器 自动调用 对应的构造函数。4. 构造函数可以重载。5. 若是没有显式创建,编译器会自己创建,显示创建了就不会创建了;
为了更好的理解各个函数,我这里创建一个类类型 —— Date;
class Date{
private:
int _year;
int _month;
int _day;
};
按照规则,类类型的构造函数应该怎么写?
按照规则来说,应该这样写:
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
但是,实际上这样会报一个错误;
就像这样,这是为什么呢?
这是因为,d1被声明的时候就会调用构造函数,而我们这里没有给出初始化值;
而没有初始化值,那么编译器就会调用默认构造函数;
但是我们没有默认构造函数,因此就出错了;
而默认构造函数是什么呢?
默认构造函数定义
无参构造函数;全缺省构造函数;编译器默认生成的构造函数;
那么想要解决这个问题应该怎么办呢?
1:给出初始值;
2:创建一个默认构造函数;
3: 给变量一个默认值;
给了初始值
创建默认构造函数
给了默认值
就像上面一样,有各种方法能够解决问题;
不过这里最推荐的还是创建一个默认构造函数;
而且这个默认构造函数一定要是全缺省的;
因为我们编译器的默认构造函数实际上只会给一个随机值;
这些都是针对内置类型的构造函数,而若是我们的类的成员变量也是类类型的呢?
构造函数会怎样做?
我们看到,Birth类里面的Date类型d的初始化已经完成了;
实际上,编译器对于自定义的类型,并不会随便就取初始化;
而是根据自定义类型里面的默认构造函数来初始化对象;
析构函数
析构函数虽然听上去像是销毁变量,实际上它只是将对象内部的资源清理干净罢了;
析构函数的声明规则
1. 析构函数名是在类名前加上字符 ~ 。2. 无参数无返回值类型。3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数。
但是实际上,析构函数对于我们的Date类并没有作用;
就像上面说的,析构函数是将对象内部的资源清理掉而已;
而销毁对象这类事情实际上在对象的作用域结束后就自动清理了;
那么析构函数针对的资源是哪些呢?
析构函数针对的资源
1. 动态开辟的空间;
2. 文件流(fopen,fclose)
因此这里用Date类不好演示,最好还是用Stack类来演示;
class Stack {
private:
int* _arr;
int _capacity;
int _top;
public:
Stack(int capacity = 4)
{
_capacity = 4;
_arr = (int*)malloc(sizeof(int) * _capacity);
if (_arr == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
_top = 0;
}
~Stack()
{
free(_arr);
_arr = nullptr;
_capacity = 0;
_top = 0;
}
void Push(int data)
{
_arr[_top++] = data;
if (_top == _capacity)
{
_capacity *= 2;
int* tmp = (int*)realloc(_arr,sizeof(int) * _capacity);
if (tmp == nullptr)
{
perror("realloc fail");
exit(-1);
}
_arr = tmp;
tmp = nullptr;
}
}
};
我们创建好这个类后,在里面插入几个数据,接下来看看析构函数会怎么做吧;
我们可以看到,析构函数将对应的_arr的空间给释放了,并且将指针置空;
这样就不会有内存泄漏的问题了;
而析构函数还有一个特性,和构造函数一样;
若是类类型内部有一个其它类类型的变量;
那么编译器只会调用其它对应类的析构函数来处理空间;
而析构函数其实并不是全部类都一定要写;
一般只有像栈这种需要动态开辟空间之类的才会需要;
拷贝构造函数
拷贝构造函数实际上是构造函数的重载;
在对象初始化的时候用另一个对象的数据构造;
拷贝构造函数的声明规则
1. 拷贝构造函数 是构造函数的一个重载形式 。2. 拷贝构造函数的 参数只有一个 且 必须是类类型对象的引用 ,使用 传值方式编译器直接报错 , 因为会引发无穷递归调用。3. 若是未显式定义,则编译器会默认生成一个拷贝函数,按字节序拷贝,这种拷贝方式又叫浅拷贝或者值拷贝。
拷贝构造函数的定义
用Date类直接实现是这样的:
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
我们可以看到d2成功的复制了d1的值;
但是这只是浅复制,并且实际上我们不写这个拷贝构造函数编译器也能完成;
但是既然编译器能够完成这个工作,那么显式构造的意义何在?
我们先用Stack类来实践一下;
我们发现,编译器默认构造的虽然完成了工作,但是p2中的指针的地址却是一样的;
因此显式构造一定是有必要的,若是类似这种需要开辟空间的类;
那么显式创建一个拷贝构造函数是必要的;
Stack(Stack& d)
{
_capacity = d._capacity;
_top = d._top;
_arr = (int*)malloc(sizeof(int) * _capacity);
int i = 0;
for (i = 0; i < _top; i++)
{
_arr[i] = d._arr[i];
}
}
此外,既然拷贝构造函数是构造函数的一种重载,那么拷贝构造函数应该和构造函数有一样的特性
那就是类中有类变量的时候,拷贝构造函数会不会调用对应类的拷贝构造函数呢?
答案是肯定的,但是此处的知识点现在不好说明,只能先卖个关子了;
拷贝构造函数使用场景
使用已存在对象创建新对象函数参数类型为类类型对象函数返回值类型为类类型对象
实际上当我们用类类型变量作为实参传给函数;
或者说返回值为类类型的时候,编译器都会调用该函数;
因此这个函数还是比较重要的;
运算符重载
在了解复制重载之前我们需要先了解运算符重载;
我们直到,在初始化对象的时候,我们可以调用拷贝构造函数来实现复制一个对象的操作;
但是考虑到代码可读性,c++又引进了运算符重载;
实际上运算符重载算是一种特殊函数,其函数名是各种符号;
那么运算符重载该怎么做呢?
运算符重载定义
<返回值类型> operator <操作符> (参数列表)
按照上面的定义,我们可以重载不一样的运算符;
但是运算符重载也是有自己独特的规则需要遵守;
运算符重载规则
1.不能通过连接其他符号来创建新的操作符:比如 operator@2.重载操作符必须有一个类类型参数3.用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义4.作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的this5. .* :: sizeof ?: . 注意以上 5个运算符不能重载。
虽然我们知道了以上规则,但是还有几个需要注意的点;
运算符重载注意点
1. 赋值运算符重载格式参数类型 : const T& ,传递引用可以提高传参效率返回值类型 : T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回 *this :要复合连续赋值的含义2. 赋值运算符只能重载成类的成员函数不能重载成全局函数3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝 。
而拷贝构造也是有显式和隐式之分,其中编译器自己定义的是逐字节的拷贝;
因此当我们给Stack之类的类对象使用运算符重载时,就需要自己定义了;
此处我们先直接看看Date类的运算符中的拷贝复制;
而若是Stack类则需要自己定义了;
Stack& operator = (Stack& d)
{
_capacity = d._capacity;
_top = d._top;
int* tmp = (int*)malloc(sizeof(int) * _capacity);
if (tmp == nullptr)
{
perror("malloc failed!");
exit(-1);
}
_arr = tmp;
for (int i = 0; i < _top; i++)
{
_arr[i] = d._arr[i];
}
}
我们看到这样d1和d2的_arr指针就不会互相干扰;
而我们运算符重载并不是只有拷贝复制这一操作;
我们可以通过运算符重载,实现类对象的+,-,*,÷等操作;
不过那都是后话了;