一、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。但空类中并不是什么都没有,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
cpp
class Date
{};

注:默认是指我们不写编译器会自动生成。
二、构造函数
2.1 概念
对于一个日期类
cpp
class Date
{
public:
//初始化函数
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2023,7,20);
return 0;
}
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 d1(2023,7,20);
return 0;
}
构造函数的功能和Init函数功能类似,是用于保证每个数据成员都能有一个合适的初始值。但构造函数是一个特殊的成员函数,名字和类名相同,没有返回值,也不用写void,实例化对象时编译器会自动调用,并且在对象的整个生命周期都只会调用一次。
2.2 特性
构造函数是特殊的成员函数,但需要注意的是,虽然名字叫构造,但构造函数的主要任务并不是开空间创建对象,而是初始化对象。
1.构造函数的函数名和类名相同。
2.构造函数没有返回值,也不需要写void。
3.在对象实例化时编译器会自动调用对应的构造函数。
4.构造函数可以重载。
cpp
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2222);
Date d3(2023, 7, 20);
return 0;
}
注意:在调用无参的构造函数时对象后面不能接括号。
cpp
错误的无参构造函数的调用方法
Date d();
因为编译器无法确定这段代码是定义一个对象还是一个函数的声明
Date func(void); //返回值为Date类型,没有参数,函数名是func
5.如果类中没有显示定义构造函数,那么C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义了构造函数编译器将不再自动生成。如果需要自己定义一个构造函数,那么只需要定义一个全缺省的构造函数就够了。
cpp
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
6.编译器自动生成的默认构造函数对内置类型和自定义类型有不同的处理方式。
7.默认构造函数对内置类型(int、double、int*、char...)成员不会进行处理,一般情况下使用默认的构造函数打印出的结果是一些随机值,不过C++11支持在声明处给缺省值。声明处给了缺省值后默认构造函数会自动使用缺省值来进行初始化。
cpp
class Date
{
public:
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class Date1
{
public:
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
//C++11后能支持在声明处给成员变量一个缺省值
int _year=1;
double _month=1;
float _day=1;
};
int main()
{
Date d;
d.Print(); //-858993460 -858993460 -858993460
Date1 d1;
d1.Print(); //1 1 1
return 0;
}
注:int*是内置类型,同样Date*也是内置类型,因为Date*是指针,只要是指针就属于内置类型。
8.默认构造函数对自定义类型(struct、class...)的成员会自动处理,会去调用该自定义类型成员的默认构造函数。
例如:使用两个栈实现一个队列
cpp
class Stack
{
public:
Stack(int n=4)
{
if (n == 0)
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * n);
_top = 0;
_capacity = n;
}
}
private:
int* _a;
int _top;
int _capacity;
};
class Queue
{
private:
Stack pushst;
Stack popst;
};
int main()
{
Queue q;
return 0;
}

8.无参的构造函数和全缺省的构造函数以及编译器自动生成的构造函数都可以被称为默认构造函数。并且默认构造函数有且只能存在一个(如果存在多个默认构造函数会有调用二义性)。特点:不传参就能调用的构造函数就是默认构造函数。
cpp
//都是默认构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
9.必须要传参的构造函数不是默认构造函数,例如半缺省构造函数,带参构造函数等。非默认构造函数可以存在多个。
cpp
//不是默认构造函数
Date(int year, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
注:没有默认构造函数时使用不传参实例化会报错。
三、析构函数
3.1 概念
析构函数和构造函数的功能相反,析构函数不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
1.析构函数的函数名是在类名前面加~。
cpp
~Date()
{}
2.析构函数没有参数也没有返回值。因此析构函数无法构成重载。
3.一个类中只能有一个析构函数,若未显示定义析构函数,则编译器会自动生成一个默认析构函数。
4.在对象生命周期结束时,会自动调用析构函数完成对象内部的资源清理工作,类似Destroy()的功能。
cpp
class Stack
{
public:
Stack(int n=4)
{
cout << "Stack()" << endl;
if (n == 0)
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * n);
_top = 0;
_capacity = n;
}
}
~Stack()
{
cout << "~Stack()" << endl;
if (_a)
{
free(_a);
}
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
//自动调用构造和析构
//Stack()
//~Stack()
return 0;
}
5.默认析构函数对内置类型和自定义类型的处理方式不同。
6.编译器自动生成的默认析构函数对内置类型(int、double、int*、Date*...)不会进行处理。
7.对自定义类型会自动去调用该自定义类型成员的析构函数。
8.有动态申请内存空间的类一定要显示写对应的析构函数,完成对动态内存空间的释放等资源清理工作,否则可能会导致内存空间的泄漏。
例如:Stack类
系统生成的默认析构函数

显示写的析构函数

9.先return再调用析构函数。

10.构造函数的调用顺序是先定义的先构造。析构函数调用的顺序是后定义的先析构。
例如:有Date和Stack这两个类
cpp
class Date
{
public:
Date()
{
cout << "Date()" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
Stack(int n=4)
{
cout << "Stack()" << endl;
if (n == 0)
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * n);
_top = 0;
_capacity = n;
}
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
构造顺序:

析构顺序:

四、拷贝构造函数
4.1 概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。

同样,在创建对象时,也可以创建一个和已经存在的对象一模一样的新对象。
拷贝构造函数:除隐含this指针外只有单个形参,该形参是对本类类型对象的引用(一般会用const进行修饰),在用已存在的类类型对象创建新对象时编译器会自动调用。
4.2 特性
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用(且一般会用const进行修饰)。使用传值方式做形参时编译器会直接报错,因为会引发无穷递归调用。因为传值传参本身就是一种拷贝构造。
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
cout << "Date()" << endl;
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
//错误的写法,编译器会直接报错,error C2652: "Date": 非法的复制构造函数: 第一个参数不应是"Date"
//Date(const Date d)
//正确的写法
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};

3.若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数会按照字节序完成拷贝,这种拷贝叫浅拷贝或值拷贝。
系统生成的默认拷贝构造函数对于没有动态申请内存空间的类:

系统生成的默认拷贝构造函数对于有动态申请内存空间的类:

4.对于有动态申请内存空间的类,需要显示写一个深拷贝的拷贝构造函数,否则在调用析构函数时会出现野指针和对一块空间释放两次的问题。

栈的拷贝构造函数:
cpp
Stack(const Stack& st)
{
//申请空间
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
//拷贝数据
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}

5.编译器默认生成的拷贝构造对内置类型和自定义类型的成员变量的处理
对内置类型:会进行值拷贝/浅拷贝。
对自定义类型:会调用自定义类型成员变量的拷贝构造函数。
6.使用拷贝构造时的两种写法:
cpp
Date d2(d1);
Date d3 = d1;
7.拷贝构造函数的几种经典使用场景:
cpp
1.函数参数类型为类类型对象
2.函数返回值类型为类类型对象
3.使用已经存在的类类型对象拷贝构造新对象
Date func(Date d)
{
Date tmp(d);
return tmp;
}
因此,为了提高效率一般对象传参时,尽量使用引用类型,返回时根据实际场景能用引用返回就用引用返回。
五、赋值运算符重载
5.1 运算符重载
对于一个日期类:
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
cout << "Date()" << endl;
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
~Date()
{
cout << "~Date()" << endl;
}
//private:
int _year;
int _month;
int _day;
};
bool DateLess(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
else
{
return false;
}
}
int main()
{
Date d1(2023, 7, 21);
Date d2(2024, 5, 8);
bool ret = DateLess(d1, d2);
cout << ret << endl;
return 0;
}
为了比较两个日期的大小,不仅要放开私有成员的访问权限,而且写出的比较大小的函数名称还可能会产生误解。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名称的函数,也具有其返回值类型,函数名称和参数列表,其返回值类型和参数列表和普通函数类似。
函数名称:关键字operator后面接需要进行重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)。
cpp
bool operator<(const Date& x1,const Date& x2);
使用方式
bool ret = d1<d2;
显示调用
ret = operator<(d1,d2);
运算符重载的几个特性:
1.不能通过连接其他符号来创建新的操作符,例如operator@等。
2.重载操作符必须要有一个类类型参数。
3.对于内置类型的操作符,其含义不能被改变,例如:内置的整型+,重载后依然要保持+的功能。
4.不能改变操作符的操作数个数,操作符有几个操作数那么重载后的操作符就必须要有相对应数量的参数。
5.操作符重载可以作为类的成员函数,其形参看起来会比实际操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。
例如小于操作符重载为日期类的成员函数时:
cpp
bool operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
else
{
return false;
}
}
使用方式:
cpp
Date d1(2023, 7, 21);
Date d2(2024, 5, 8);
直接使用
bool ret = d1<d2;
cout << ret << endl;
显示调用
ret = d1.operator<(d2);
cout << ret << endl;
cout << (d1 < d2) << endl;
注意:直接打印d1<d2时要加括号。
6.有5种运算符无法被重载:
cpp
:: 域作用符
sizeof 计算字节大小的操作符
?: 三目操作符
. 点操作符
.* 点星操作符
注: * (解引用操作符)可以被重载。
7.运算符重载有多个参数时,参数的顺序是从左向右固定的。
5.2 赋值运算符重载
以日期类为例,类里面并没有显示重载赋值运算符:
cpp
Date d1(2020, 8, 5);
Date d2(2023, 7, 27);
d1 = d2;
赋值运算符有两个参数d1和d2,当有多个参数时,参数的顺序是从左向右固定的。因此赋值运算符重载应该是:
cpp
void operator=(const Date& d)
{}
或
void operator=(const Date d)
{}
赋值运算符重载和拷贝构造函数的功能很相似,不过会有一些区别,对于拷贝构造函数来说参数列表只能使用引用传参,否则会造成无穷递归的情况,但对于赋值运算符重载来说,可以使用引用传参,同时也可以直接使用传值传参,并不会导致无穷递归的情况出现。但平时还是会使用引用作为参数,可以提高传参时的效率。

注:一般引用传参都会加const,可以避免参数在函数内部被修改。
拷贝构造和赋值重载的区别:
拷贝构造:一个已经存在的对象去初始化另一个要创建的新对象。
赋值重载:两个已经存在的对象之间进行拷贝。
cpp
拷贝构造:一个已经存在的对象去初始化另一个要创建的新对象
赋值重载:两个已经存在的对象之间进行拷贝
Date d1(2020, 8, 5);
Date d2(2023, 7, 27);
//拷贝构造
Date d3(d1);
//赋值重载
d1 = d2;
赋值运算符重载的两点优化。
优化1:编译器内置的赋值运算符是支持连续赋值的。
cpp
int i, j;
i = j = 10; //此处j=10的返回值是j的大小,然后赋值给i

为了复合内置赋值运算符的特性,所以类的赋值运算符重载也需要有返回值,且返回值的类型需要和类的类型相同。出了作用域后*this还在,生命周期并没有结束,因此可以使用引用返回,且引用返回还可以降低返回的消耗。
cpp
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
优化2:检查是否有自己给自己赋值的情况。
cpp
Date d1(2020, 8, 5);
//自己给自己赋值
d1 = d1;
因此为了优化自己给自己赋值的情况可以增加一个if判断
if(this != &d)
{
//...
}
实际上赋值运算符重载也是类里面的一个默认成员函数,也就是说当不显示写的时候,编译器会自动生成一个默认的赋值运算符重载函数,且该默认赋值重载函数对不同类型的成员变量会进行不同的处理方式:
cpp
对内置类型的成员变量会进行值拷贝/浅拷贝。
对自定义类型的成员变量会调用其本身的默认赋值重载函数。
如果类中未涉及到资源管理,赋值运算符可实现可不实现;一旦涉及到资源管理则必须实现对应的赋值运算符。
总结:
Date类的赋值运算符重载函数:
cpp
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Stack类的赋值运算符重载函数:
cpp
Stack& operator=(const Stack& st)
{
if (this != &st)
{
//重新申请空间
_a = (int*)realloc(_a, sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
//拷贝数据
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
5.3 前置++和后置++重载
前置++和后置++都是一元运算符,且运算符符号都是++,祖师爷为了区分实现这两个运算符,实现方式如下:
cpp
//前置++,++d
Date& operator++()
{
}
//后置++,d++
Date operator++(int)
{
}
Date d1;
++d1;
前置++显示调用:d1.operator++()
d1++;
后置++显示调用:d1.operator++(0)
注:后置++重载里面加了一个int参数,进行占位,跟前置++构成函数重载进行区分,本质上后置++调用时编译器会自动进行特殊处理。后置++显示调用时只要保证参数是自变量整型即可,只是为了保持和前置++的区分。
日期类中前置++和后置++的实现:
cpp
//前置++,++d
Date& operator++()
{
*this += 1;
return *this;
}
//后置++,d++
Date operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
int GetMonthDay(int year, int month)
{
//增加一个static可以避免每次使用GetMonthDay时都需要创建数组的消耗
static int array[] = { 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 array[month];
}
Date& operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
注:前置++和后置++都是运算符重载,同时前置++和后置++也构成了函数重载。
六、const成员
cpp
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
const Date d;
//error C2662: "void Date::Print(void)": 不能将"this"指针从"const Date"转换为"Date &"
d.Print();
return 0;
}
当使用const限制的类实例化对象去调用普通的成员函数时会出现权限放大的情况。

将const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际上修饰的是该成员函数的隐含this指针,表明该成员函数中不能对类的任何成员变量进行修改。
修饰后的成员函数:
cpp
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
不同类型的对象调用不同类型的成员变量:

不同类型的成员函数互相调用:

const位于函数的不同位置时会有不同的作用:

const成员函数的意义:
const成员函数和同名非const成员函数可以同时存在,并且它们会互相构成函数重载。因为编译器会将const Date*和Date*默认为不同的类型。
cpp
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
注:编译器对于const对象和非const对象去匹配使用const成员函数和非const成员函数会采取最优匹配原则。
cpp
对于const对象:只能匹配const成员函数
对于非const对象:如果同时存在const成员函数和非const成员函数,则会优先使用非const成员函数。
如果没有非const成员函数则也会去使用const成员函数。

因此我们可以利用const成员匹配的优先级顺序来控制不同对象读写数据的权限。
总结:只读函数都可以加const,内部不涉及修改成员的都是只读函数。例如日期类里面的大小比较函数、打印函数、日期-日期函数、+和-的运算符重载函数等都不涉及对成员的修改,可以都加上const,用于防止在函数内部不小心对函数成员进行修改操作。
七、取地址及const取地址操作符重载
取地址运算符重载函数是默认成员函数,不显示写编译器会自动生成。
以日期类为例
普通版本:
cpp
Date* operator&()
{
return this;
}
const版本:
cpp
const Date* operator&() const
{
return this;
}
用处:
期望普通对象无法正常取到地址,但const对象可以取到地址。
cpp
class Date
{
public:
Date* operator&()
{
return nullptr;
}
const Date* operator&() const
{
return this;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
cout << &d1 << endl; //00000000
const Date d2;
cout << &d2 << endl; //003DFE5C
return 0;
}