一、默认成员函数
前言:所有代码都是在 VSCode下执行
默认成员函数的作用
假设我们定义了一个类,但不对这个类做任何的操作,我们称为空类
class A{};
但是空类中真的什么都没有吗?并非如此,事实上,编译器会自动生成 6 个默认成员函数。
什么是默认成员函数?在我们没有显示实现下,编译器自动生成的成员函数就称为默认成员函数。
1.1 六个默认成员函数
六个默认成员函数与它们各自的工作
1.1.1 构造函数
构造函数的概念:构造函数是一种特殊的成员函数,这个函数的名字与类名相同。当创建一个类类型的对象时,编译器会自动调用这个成员函数。以确保每个成员变量都有一个合适的初始值,且**在对象的整个生命周期中只会调用一次。**我们从上面的图中可以看到,构造函数的主要任务是完成初始化的工作,而不是开空间来创造对象。
构造函数的特性
- 函数名与类名相同
- 没有返回值
- 实例化对象时,编译器会自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,但如果在类中有显示定义构造函数,则编译器就不会生成
无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
结论就是不传实参就可以调用的构造就叫默认构造
此外,编译器自动生成的构造对于内置类型的成员变量初始化是没有特别要求的,也就是说,编译器自动生成的构造函数是否会初始化成员变量是取决于编译器的。而如果是自定义类型的变量,就会特别要求去调用该变量的构造函数来对此变量进行初始化,如果该变量没有默认构造函数的话,程序就会报错,此时就会需要利用初始化列表来解决这个问题。
关于初始化列表会在类和对象(下)进行讨论
再来我们会一一说明构造函数的特性与细节
// 我们会以日期类来进行说明
#include <iostream>
using namespace std;
class Date{
// 第一种构造函数:无参构造函数
public:
Date(){
cout << "我是无参的构造函数" << endl;
_year = 1;
_month = 1;
_day = 1;
}
// 带参的构造函数(这个不是默认构造)
Date(int year, int month, int day){
cout << "我是带参的构造函数" << endl;
_year = year;
_month = month;
_day = day;
}
// 第二种构造函数:全缺省构造函数
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;
return 0;
};
情况1:只保留带参构造函数
输出结果
报的错是没有相配的构造函数,因为我们已经自己写了构造函数,此时编译器就不会在自动生成默认构造函数
情况2:变量是自定义变量
#include <iostream>
using namespace std;
class Stack{
public:
Stack(){
cout << "调用Stack()"<< endl;
_a = (int*)malloc(sizeof(int)*4);
if(_a == NULL){
perror("malloc fail");
exit(-1);
}
_capacity = 4;
_top = 0;
}
private:
int* _a;
int _capacity;
int _top;
}
class Queue{
// ...
private:
Stack pushst;
Stack popst;
};
int main(){
Queue q1;
return 0;
}
输出结果
这里也印证了前面说的,如果成员变量是自定义类型,则会调用该自定义类型的默认构造函数。如果没有默认构造函数会怎样?
输出结果(这里是把Stack的构造函数改成带参构造函数)
这个错误就是在说Queue类的成员变量没有默认的构造函数来初始化。
解决方法有两种:
- 提供默认构造函数
- 初始化列表
1.1.2 析构函数
析构函数的概念:析构函数在做的事情和构造函数刚好相反,需要注意的是,析构函数不是对对象进行销毁,局部对象是存在栈帧中,当函数结束后,对象就会随着栈帧销毁而销毁。析构函数是在对象销毁时自动被调用,用来完成对象中资源的清理释放。
析构函数的特性
- 析构函数名是在类名前加上字符 ~
- 无参数无返回值 (和构造类似,也不需要加void)
- 一个类只能有一个析构函数。如果没有显式定义,系统会自动生成默认的析构函数
- 对象生命周期结束时,系统会自动调用析构函数
- 跟构造函数类似,我们不写的话编译器会自动生成析构函数,但不会对内置类型成员做处理。而自定义类型成员会去调用该自定义类型的析构函数
- 需要注意的是就算我们显示写了析构函数,对于自定义类型成员也会调用他的析构,也就是说,自定义类型成员无论什么情况都会自动调用析构函数
- 后定义的对象会先被析构
那什么时候我们才要自己写析构?
如果我们没有在类中申请资源的话(如前面的Date类),就可以不写,直接使用编译器默认生成的析构函数;但如果我们有在类中申请资源的话(如前面的Stack类)就要自己写析构函数,假设我们申请了资源又不自己写析构函数,就会造成资源的泄漏
Stack 析构函数实现
class Stack{
public:
Stack(){
cout << "调用Stack()"<< endl;
_a = (int*)malloc(sizeof(int)*4);
if(_a == NULL){
perror("malloc fail");
exit(-1);
}
_capacity = 4;
_top = 0;
}
// Stack 析构
~Stack(){
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
int* _a;
int _capacity;
int _top;
}
1.1.3 拷贝构造
拷贝构造的概念:拷贝构造函数是一个构造函数,其特点是函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值
拷贝构造的特性
拷贝构造是构造函数的一种重载
拷贝构造函数的第一个参数==一定要是自身类类型的引用==,如果是使用传值传递,在语法逻辑上会导致无限递归而编译错误。而拷贝构造函数中的可以传入很多参数,但都必须给默认值
C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造完成
如果没有显示实现拷贝构造则编译器默认会生成一个拷贝构造函数,自动生成的拷贝构造会对成员变量进行潜拷贝/值拷贝
(以一个字节一个字节的方式进行拷贝)。而对于自定义类型,则会调用该自定义类型的拷贝构造
对于什么时候需要自己实现拷贝构造,有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要
传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
class Date
{
public:
Date(int year = 2025, int month = 1, int day = 1) : _year(year), _month(month), _day(day)
{
}
// 拷贝构造函数
// 如果写的是 Date(Date d) --> 编译报错 : error C2652: “Date”: 非法的复制构造函数: 第一个参数不应是“Date”
Date(Date &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 也可以使用指针来实现拷贝(但此时就只是普通的构造函数而不是拷贝构造函数)
Date(Date *d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)
{
cout << &d << endl;
d.Print();
}
// Date &Func2()
// {
// Date tmp;
// tmp.Print();
// // 这样可能会导致野引用,因为tmp 是一个局部变量
// return tmp;
// }
int main()
{
Date d1(2025, 3, 14);
Func1(d1);
/*
C++规定定义对类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝
所以这里的d1传值传参给d要调用拷贝构造完成拷贝,传引用传参可以省去这里的拷贝
*/
Date d2(&d1); //这里传入的是 d1 的地址 不是引用!
d2.Print();
d1.Print();
Date d3(d1); // 这样写才是调用拷贝构造函数
d3.Print();
return 0;
}
在 VScode 中,Func2函数是不能编译通过的,其他编译器可能可以,但还是要避免返回局部变量的引用
class Stack
{
public:
Stack(int n = 4)
{
cout << "调用 Stack()" << endl;
_a = (int *)malloc(sizeof(int) * n);
if (_a == NULL)
{
perror("malloc fail");
exit(-1);
}
_capacity = n;
_top = 0;
}
Stack(const Stack &st)
{
_a = (int *)malloc(sizeof(int) * st._top);
if (nullptr == _a)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_capacity = st._capacity;
_top = st._top;
}
private:
int *_a;
int _capacity;
int _top;
};
对于有申请资源,我们自己实现的拷贝构造函数进行的是深拷贝,对于深/浅拷贝的内容会在之后详细说明
1.1.4 运算符重载
前言:在这部分会提及各种运算符的重载,但要记得只有赋值重载才是默认成员函数
运算符重载的概念:在C++中规定,当自定义类型对象要使用运算符时,需要转换成调用对应的运算符重载,如果没有能与其对应的运算符重载,则编译器会报错
运算符重载的特性
运算符重载的函数名比较特别,其由operator + 要重载的运算符组成。和一般函数一样具有返回类型、参数列表和函数体
重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数
如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,所以运算符重载作为成员函数时,参数比运算对象少一个
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致
可以重载C++中有的运算符,但不能自己创建运算符,但要记得有五个运算符是不能重载的==( . :: sizeof ?: .* )==
// 介绍.* 运算符 class A { public: void func() { cout << "A::func()" << endl; } }; typedef void(A::*PF)(); //成员函数指针类型 int main() { // C++规定成员函数要加&才能取到函数指针 PF pf = &A::func; A obj; // 对象调用成员函数指针时,使用.*运算符 (obj.*pf)();
重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义。 如: int operator+(int x, int y)
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。因此,C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置,第二个形参位置当类类型对象就可以实现cout<<
对于那些运算符应该要重载取决于哪些运算符重载之后具有意义,举例来说,Date类实现 + - > < 等运算符就具有意义,但如果是 * / 等就没有意义,因此在实现时会去实现+ - > < 等运算符而不会实现 * / 等运算符
赋值运算符重载:==赋值运算符重载==是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象
赋值运算符重载的特点
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值的情况
- 没有显式实现时,编译器会自动生成一个默认的赋值运算符重载,默认运算符重载的行为与拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数
与拷贝构造一样,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要
// Date类完整实现(会在这部分顺带说明各种运算符重载)
// Date.h
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
// 因为如果 << >> 作为成员函数重载会不符合我们期望的结果,因此把这两个运算符重载在全局
// 但重载在全局会面临无法访问私有成员的问题
/*
// 重载为全局 面临对象访问私有成员的问题有几种方法
1、成员放公有
2、Date提供getxxx函数来访问私有成员
3、友元函数
4、重载为成员函数(在 << 和 >> 这个方法是不行的)
*/
friend ostream &operator<<(ostream &out, Date &d);
friend istream &operator>>(istream &in, Date &d);
public:
Date(int year = 2025, int month = 3, int day = 15);
void Print() const;
// 这里的const 作用是什么? Print函数应该是 void Print(Date* const this);
// 如果在Print()后加上const就会变成 void Print(const Date* const this); 因为不期望在Print函数中修改传入的类对象的值
// 关于 const 修饰,和引用一样 只能是权限的平移或缩小,不能有权限的放大
// 检查日期
bool CheckDate();
// 获取每个月的天数
int GetMonthDay(int year, int month);
// 运算符重载
bool operator<(const Date &d) const;
bool operator==(const Date &d) const;
// 实现了 < 和 == 后就可以直接利用这个两个函数重载其他比较运算符
bool operator<=(const Date &d) const;
bool operator>(const Date &d) const;
bool operator>=(const Date &d) const;
bool operator!=(const Date &d) const;
// day天后是什么时候
Date operator+(int day) const;
// day天前是什么时候
Date operator-(int day) const;
Date &operator+=(int day);
Date &operator-=(int day);
// 前置++
Date &operator++();
// 后置++
Date operator++(int);
// 前置--
Date &operator--();
// 后置--
Date operator--(int);
// d1 - d2
Date operator-(const Date &d) const;
private:
int _year;
int _month;
int _day;
};
// Date.cpp
#include "../include/Date.h"
// 构造函数的缺省值,声明和定义择一写就好
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
{
cout << "非法日期" << endl;
}
}
void Date::Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
// 检查日期,避免非法输入
bool Date::CheckDate()
{
if ((_month < 1 || _month > 12) || (_day < 1 || _day > GetMonthDay(_year, _month)))
{
return false;
}
return true;
}
// 获取每个月的天数
int Date::GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int Day[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 判断是否是闰年
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 29;
}
return Day[month];
}
bool Date::operator<(const Date &d) const
{
{
if (_year < d._year)
{
return true;
}
if (_year == d._year && _month < d._month)
{
return true;
}
if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}
}
bool Date::operator==(const Date &d) const
{
return (_year == d._year) && (_month == d._month) && (_day == d._day);
}
bool Date::operator<=(const Date &d) const
{
return *this < d || *this == d;
}
bool Date::operator>(const Date &d) const
{
return !(*this <= d);
}
bool Date::operator>=(const Date &d) const
{
return !(*this < d);
}
bool Date::operator!=(const Date &d) const
{
return !(*this == d);
}
Date &Date::operator+=(int day)
{
if (day < 0)
{
_day -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
Date Date::operator+(int day) const
{
Date tmp(*this); // 把*this 「拷贝」给tmp 注意是拷贝不是赋值
tmp += day;
return tmp;
}
Date &Date::operator-=(int day)
{
// 2025 3 15
//- 30
// 2025 3 -15
// 2025 3
if (day < 0)
{
_day += -day;
}
_day -= day;
while (_day < 0)
{
--_month;
if (_month < 1)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
Date Date::operator-(const Date &d) const
{
// 两个日期要相减,要先考虑哪一个人日期比较大
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
Date &Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date &Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
ostream &operator<<(ostream &out, Date &d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
}
istream &operator>>(istream &in, Date &d)
{
cout << "请输入日期:";
in >> d._year >> d._month >> d._day;
if (!(d.CheckDate()))
{
cout << "非法日期" << endl;
}
}
1.1.5 取地址重载
取地址运算符重载分为普通取地址运算符重载和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
};