默认成员函数
目录
1.默认成员函数概念
2.构造函数
2.1.构造概念
2.2.构造特性
2.3.构造类型
2.3.1.无参构造
2.3.2.全缺省构造
2.3.3.带参构造
2.3.4.拷贝构造
2.4.初始化列表
3.析构函数
3.1.析构概念
3.2.析构特性
3.3.析构实现
4.赋值重载函数
4.1.运算符重载
4.2.赋值运算符重载
4.3.赋值运算符重载的实现
5.取地址运算符重载
5.1.const成员函数
5.2.取地址运算符重载
1.默认成员函数概念
所谓默认,就是我们不写,编译器自动实现的函数,一般来说,当我们实现一个类类型的时候,会自动生成6个默认成员函数,其中最后两个只需要了解即可
| 函数名称 | 语法形式 | 核心作用 |
| 1.构造函数 | 类名() | 创建对象时初始化对象 |
| 2.析构函数 | ~类名() | 销毁对象时清理资源 |
| 3.拷贝构造函数(构造函数的一种) | 类名(const 类名& 源对象) | 用一个已存在的对象 “拷贝” 生成新对象 |
| 4.赋值运算符重载函数 | 类名& operator=(const 类名& 源对象) | 把一个对象的值 “赋值” 给另一个已存在的对象 |
| 5.取地址运算符重载 | 类名* operator&() | 返回对象的地址 |
| 6.const取地址运算符重载 | const 类名*operator() | 返回 const 对象的地址 |
那么既然编译器既然能够自动实现,那么我们还有学习的必要吗?有的有的,事实上编译器实现的默认成员函数大部分都不满足我们的需求
这时候我们就需要去思考:
- 编译器生成的默认成员函数到底适用于哪种情况,哪些情况需要我们自已动手
- 既然需要我们自已动手,那么到底应该怎么实现
2.构造函数
2.1.构造概念
有些同学可能会对构造一词产生误解,认为构造就是创造出一片空间,事实上开辟空间在建立函数栈帧的时候就已经开辟完成,详情请看函数的栈帧和销毁,所以所谓构造函数只是完成初始化工作而已,类似于我们顺序表Init()函数
2.2.构造特性
2.2.1.与普通的初始化函数不同,构造函数不需要手动调用,而是编译器在编译阶段自动调用,从而从根源上解决忘记初始化的问题

可以看到构造函数被调用了
2.2.1.构造函数没有返回值,甚至不需要void,名字与类名字相同,就如同下图
class Stack {
Stack() {
//
}
};
2.2.3.构造函数又有默认构造函数,注意这里的默认构造并不只是编译器自动生成的构造,要与前面的默认成员函数作出区分,事实上,这里的默认构造是"0参"构造,即不传参数的构造,以下的构造函数均被称为默认构造
-
编译器默认生成的构造
-
不传参数的构造
-
全缺省构造函数
需要注意:全缺省与不传参数的构造尽管构成函数重载,但如果不传参数的情况下编译器也无法区分
2.2.4.编译器生成的默认构造是否对内置类型变量完成初始化是不确定的(不同的编译器可能结果不同),可能初始化,也有可能不初始化。
可以看到vs2022下并没有初始化

对类类型变量会调用类类型的相应的默认构造
2.2.5.一旦用户手动完成了默认构造,编译器就不会再生成
2.3.构造类型
到这里大概对上面的思考有一定了解了,可看出编译器生成的默认构造并不好用
下面我们就要手动写构造函数了(以日期类实现为例)
2.3.1.无参构造
Date()
{
//不需要参数
_year = 1;
_month = 1;
_day = 1;
}


2.3.2.全缺省构造
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
2.3.3.带参构造
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
执行:

2.3.4.拷贝构造
与单纯的构造不同,是一个已经初始化好的去初始化另一个还没有初始化的
注:涉及临时变量的都要调用拷贝构造,如隐式类型转换,传值返回,额外需要注意的是函数传值传参也要调用拷贝构造,即实参拷贝给形参
所以拷贝构造的重要性不言而喻,而编译器生成的拷贝构造默认是浅拷贝(一字节一字节地拷贝),对于内置类型(int、double等)足够了,但是如果涉及资源的拷贝就会出现问题,比如栈的拷贝
由于编译器生成的是浅拷贝,就相当于直接复制过来了,这样指向空间就会是同一份指针,空间申请就会被销毁,想象一下,一份空间销毁连续两次会出现什么事?
所以涉及资源(空间)的拷贝就需要我们手动实现
Stack(const Stack& st) {
_capacity = st._capacity;
int* tmp = (int*)malloc(sizeof(int)*_capacity);
if (tmp == nullptr) {
perror("malloc fail");
return;
}
memcpy(tmp, st._array,st._top);
_array = tmp;
_top = st._top;
}
注:这里必须用引用传参,不能用传值传参,因为前面提到传值传参会调用拷贝构造,而拷贝构造又是传值传参,传值传参调用拷贝构造……,就这样无限递归下去
这是我们有拷贝构造的情况

而没有拷贝构造,编译器生成的拷贝构造,可以看到指针_array都是同一份,在销毁的时候就会被销毁两次

2.4.初始化列表
这里是声明的地方
private:
//声明的地方
int* _array;
int _top;
int _capacity;
初始化列表是定义(开空间的地方)并初始化的地方
格式:
- 以一个冒号开始,接着是一个以逗号分隔的数据成员列表
- 每个"成员变量"后面跟一个放在括号中的初始值或表达式
Date()
:_year(1)
,_month(1)
,_day(1)
{}
注意:初始化列表初始化顺序是按照声明的顺序初始化的
Date()
: _month(1)
,_year(_month)
,_day(1)
{}
private:
int _year;
int _month;
int _day;
你可能期望着用_month = 1初始化_year,但事实上会优先初始化_year,但此时的_month还没有初始化,为随机值

建议尽量用初始化列表进行初始化成员变量,因为尽管不写初始化列表,每个构造函数也有初始化列表,编译器也会优先执行初始化列表
3.析构函数
3.1.析构概念
从名字就可以看出析构函数与构造函数是一对函数,析构函数是清理资源的
3.2.析构特性
3.2.1.与构造函数类似,在对象生命周期结束之后,编译器会自动调用析构函数
3.2.2.函数名为~+类名字
3.2.3.析构函数没有返回值,也不用加void
3.2.4.一个类中只能有一个析构函数,自已手动写了显式定义,编译器就不会生成了,可以理解为只能析构一次
3.2.5.析构函数对于内置类型成员类型不处理,对于自定义类型会自动调用自定义类型中的析构函数,还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数
3.2.6.⼀个局部域的多个对象,C++规定后定义的先析构,跟函数的栈帧和销毁有关
注:析构函数并不是释放对象的空间,函数栈帧销毁之后就自动释放了,而是完成对空间的清理。比如栈,指针指向了一片空间,析构函数的目的就是清理这片空间,否则如果直接释放,就会导致内存泄漏
所以对于内置类型且没有指向资源的类型,编译器自动生成的析构函数就已够用了,比如日期类,全是内置类型
3.3.析构实现
对于指向资源的类型,编译器自动生成的析构函数就不够用了,因为此时资源还未释放,会造成内存泄漏的问题,比如栈,所以需要手动实现
~Stack() {
free(_array);
_array = nullptr;
_top = _capacity = 0;
}
4.赋值重载函数
赋值重载函数实际上是特殊的运算符重载函数,我们先了解一下运算符重载函数
4.1.运算符重载
4.1.1.当运算符被用于类类型的对象的时候,c++允许通过运算符重载来指定运算符新的含义,在类类型使用运算符的时候会调用相应的运算符重载,若没有就会报错
4.1.2.运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
4.1.3.重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,规定二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个
4.1.4.重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分
以日期类举例,因为栈++没有意义
Date& Date::operator+=(int day) {
_day += day;
while (_day > GetMonthDay()) {
_day -= GetMonthDay();
_month++;
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) {
//自已不加,所以要临时拷贝一份
Date tmp = *this;
tmp += day;
return tmp;
}
前置++(返回加后)
Date& Date::operator++() {
//复用+
*this = *this + 1;
return *this;
}
//后置++(返回加之前)
Date Date::operator++(int) {
//提前拷贝一份好返回
Date tmp = *this;
//复用+=
*this += 1;
return tmp;
}
4.2.赋值运算符重载
4.2.1.赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数,这样就有this指针,可以直接调用函数,赋值运算重载的参数建议const 引用传参,避免拷贝
4.2.2.建议用引用返回,引用返回可以避免拷贝提高效率,有返回值目的是为了支持连续赋值场景(返回后的对象又可以对另一个对象赋值)
4.2.3.没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成浅拷贝(⼀字节⼀字节的拷贝)对自定义类型成员变量会调用他的赋值重载函数
4.3.赋值运算符重载的实现
Stack& operator=(const Stack& st) {
//坑
if (this == &st) {
return *this;
}
//坑
free(_array);
_capacity = st._capacity;
int* tmp = (int*)malloc(sizeof(int) * _capacity);
if (tmp == nullptr) {
perror("malloc fail");
return;
}
memcpy(tmp, st._array, st._top);
_array = tmp;
_top = st._top;
return *this;
}
坑1:一定要优先释放掉_array指向的空间,因为等会_array会指向另一片空间,导致原空间找不到了,也就是内存泄漏了
坑2:一定要检查,不能给自已赋值,是因为前面已经已经释放掉了自已的内存,如果再拷贝,会导致未定义行为
5.取地址运算符重载
5.1.const成员函数
如果我们期望this指针指向的对象不修改,那么就需要加上const修饰,但是有一个问题:this指针是默认在第一个位置的,我们没法手动修改,所以c++规定在成员函数后面加上const来进行表示
void func() const {
//内容
}
5.2.取地址运算符重载
取地址运算符分为普通的取地址,和const取地址,即返回this,一般来说编译器自动生成的就足够我们使用了,除非是你不希望别人获取this的值


