本篇博客来梳理C++中重要的默认成员函数
一、类的默认成员函数
用户没有显式实现,编译器自动生成的成员函数,一旦显式实现,编译器不会自动生成
1.学习时应该考虑的两个问题
(1)我们不写时,编译器默认生成的函数行为是什么,是否满足需求
(2)若不满足需求,就需要自己实现,如何自己实现?
2.6个默认成员函数
二、构造函数
1.主要任务
对象实例化时初始化对象,类似Stack中的Init函数
2.特点
(1)函数名与类名相同
(2)无返回值(不用给返回值,也不用写void)
(3)对象实例化时系统会自动调用对应的构造函数
(4)构造函数可以重载
(5)若类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦用户显式定义则编译器不再生成
(6)总结一下:不传实参就可以调用的构造就是默认构造
class Date
{
public:
// 1.⽆参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.全缺省构造函数
/*Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调⽤默认构造函数 ,此时1和3只能存在一个
Date d2(2025, 1, 1); // 调⽤带参的构造函数
return 0;
}
注意:1.无参构造函数和3.全缺省构造函数构成函数重载,如果直接写Date d3(),会产生调用歧义
// 两个Stack实现队列 ,假设Stack的默认构造已经自己写好
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
(7)编译器默认生成的构造函数
①对内置类型成员变量:初始化无要求,看编译器
②对自定义类型成员变量:要求调用这个成员变量的默认构造函数初始化,若该成员没有默认构造函数就会报错,要初始化此成员变量需用到初始化列表,后续进行解析
三、析构函数
1.主要任务
完成对象中资源的清理释放工作,类似Stack中的Destroy函数
2.特点
(1)函数名:在类名之前加上~
(2)无参无返回值(跟构造函数类似)
(3)一个类只能有一个析构函数(不支持重载),若未显式定义,系统会自动生成默认的析构函数
(4)对象生命周期结束时,系统自动调用析构函数
(5)编译器默认生成的析构函数
①对内置类型成员:不做处理
②对自定义类型成员:会调用它对应的析构函数
(6)我们自己写析构,只需要管自定义类型中申请了资源的
(7)对自定义类型成员,无论什么情况(比如自己显式写了)都会自动调用析构函数(比如下面两个栈实现队列)
(8)一个局部域的多个对象,C++规定后定义的先析构
class Stack
{
public:
Stack(int n = 4) //构造函数
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack() //析构函数
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源
// 显式写析构,也会自动调用Stack的析构
/*~MyQueue()
{
//...
}*/
private:
Stack pushst;
Stack popst;
};
注:在vs中调试代码时,在类里面想看成员变量,可以直接输入this
四、拷贝构造函数
1.定义
(1)是一个特殊的构造函数
(2)第一个参数是自身类类型的引用,且任何额外的参数都有默认值
2.特点
(1)拷贝构造函数是构造函数的一个重载
(2)拷贝构造函数的第一个参数必须是类类型对象的引用,若使用传值会引发无穷递归调用,编译器直接报错
C++规定:传值传参(函数的传值调用)和传值返回要调用拷贝构造函数
正常的拷贝构造调用:
如果拷贝构造不写传引用调用,采用传值调用,那么就会产生无穷递归
(3)C++规定:自定义类型对象进行拷贝行为必须调用拷贝构造,因此函数调用时,自定义类型传值调用和传值返回都会调用拷贝构造=>自定义类型建议传引用调用
(4)编译器默认生成的拷贝构造函数:
①对内置类型成员:完成值拷贝/浅拷贝(一个字节一个字节的拷贝)
②对自定义类型成员:会调用它对应的拷贝构造
(5)我们自己写拷贝构造:关注类有没有申请资源,如果类显式实现了析构并释放资源,就需要显式写拷贝构造
Date这样的类,成员变量全是内置类型=>不用自己写拷贝构造
Stack这样的类,_a指向了资源,编译器默认生成的拷贝构造不符合需求,因为程序只对栈实现了浅拷贝,会导致st1和st2两个对象指向同一块数组空间,那么在这两个对象生命周期结束的时候,析构函数调用两次就会出现程序崩溃。所以Stack这样的类需要自己写拷贝构造函数,先申请另一块空间再进行拷贝
MyQueue(两个栈实现队列)这样的类,内部主要是自定义类型Stack,编译器默认生成的拷贝构造会调用栈的拷贝构造,也不需要自己显式写,是“躺赢玩家”
(6)关于函数的传值返回:会产生一个临时对象,因此会调用拷贝构造,返回的是值拷贝。对传引用返回,要小心返回“野引用”
五、赋值运算符重载函数
运算符重载可以给运算符指定新的含义
1.运算符重载(跟函数重载无关)
(1)C++规定:类类型对象使用运算符,必须调用对应的运算符重载,如果没有则编译报错
(2)运算符重载函数是名字比较特殊的函数,函数名:operator运算符
如:
bool operator==(const Date& d1, const Date& d2)
(3)运算符重载函数的参数个数=运算符作用的运算对象数量,一元运算符有一个参数,二元运算符有两个参数,二元运算符左侧算子传给第一个参数,右侧算子传给第二个参数
(4)当运算符重载函数是成员函数:第一个运算对象默认传给隐式的this指针=>这种情况下函数参数比运算对象少一个
(5)不能用语法中没有的符号来创建新的运算符,如operator@
(6).* :: sizeof ?: .这五个运算符不能重载
(7)重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义
如:
int operator+(int x, int y),//参数全是内置类型,不能重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
注释:
- 成员放公有:把private注释掉即可
- 友元函数:在类里面添加友元声明
friend bool operator==(const Date& d1, const Date& d2);
(8)前置++和后置++重载:后置++重载时增加一个int形参,跟前置++构成函数重载,方便区分
(9)重载<<和>>时,需要重载为全局函数
内置类型的流插入和流提取已经重载好了,需要自己实现的是自定义类型的流插入和流提取
① 重载流插入<<
如果重载为成员函数,this指针默认抢占了第一个形参的位置,第一个形参是对象,第二个形参才是cout,写起来就是对象<<cout,简直倒反天罡,不符合使用习惯和可读性,因此应该重载为全局函数,把ostream/istream放到第一个形参的位置,第二个形参的位置放对象
友元声明的优势:不用改变成员变量的private性质,也可以让全局函数访问到对象的成员变量
② 重载流提取>>
2.赋值运算符重载函数(跟函数重载无关)
(1)主要任务:完成两个已经存在的对象的直接拷贝赋值(与拷贝构造不同,拷贝构造用来拷贝初始化给另一个要创建的对象)
(2)特点
①规定必须重载为成员函数,参数建议写成const的类类型引用,减少拷贝同时防止实参被改
②有返回值,且建议写成类类型引用,有返回值的目的是为了支持连续赋值
③编译器默认生成的赋值运算符重载函数:对内置类型完成值拷贝/浅拷贝,对自定义类型成员变量会调用他的赋值重载
④我们自己写赋值运算符重载函数:关注类有没有申请资源,如果类显式实现了析构并释放资源,就需要显式写赋值运算符重载函数
六、取地址运算符重载函数
1.const成员函数
(1)定义:用const修饰的成员函数,const修饰成员函数放到成员函数参数列表的后面
(2)const实际修饰:隐含的this指针,表明在这个成员函数中不能对类的任何成员进行修改
像这样写编译器就会报错
正确的写法
void Date::Print() const
{
cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
写成const成员函数才能顺利访问const成员对象,对于不需要修改成员变量的成员函数都建议加上const
2.取地址运算符重载函数
分为普通取地址运算符重载和const取地址运算符重载,一般编译器实现的足够用,很少自己实现
class Date
{
public :
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
// return nullptr;
}
private :
int _year ; // 年
int _month ; // ⽉
int _day ; // ⽇
};