一、类的默认成员函数
类的默认成员函数的意思是即使自己不写编译器也会自动生成的成员函数(用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数)。一个类,我们不写的情况下编译器会默认生成以下6个成员函数,这六个中最重要的是前四个,最后两个取地址重载不重要,我们稍微了解一下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
- 我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
- 编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?

二、构造函数
构造函数是 C++ 类中特殊的成员函数,虽然叫构造函数,但是这个函数并不开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了)。它的核心任务是:初始化对象,在创建类的对象时自动被调用。本质是代替了之前我们创建栈时写的Init函数。
构造函数的特点:
- 函数名与类名完全相同(大小写也必须一致)。
- 无返回值:不需要写返回值类型,连void也不能写,这是 C++ 的硬性规定。
- 自动调用:在对象实例化时,系统会自动匹配并调用对应的构造函数。
- 支持重载:一个类可以有多个构造函数,通过参数列表的不同来区分。
- 默认生成规则:如果类中没有显式定义任何构造函数,编译器会自动生成一个无参的默认构造函数;但只要用户显式定义了任意构造函数,编译器就不再自动生成。
- 默认构造函数的定义:
1.无参构造函数、全缺省构造函数、编译器自动生成的构造函数,都属于默认构造函数。
2.这三者有且只有一个存在,不能同时出现,否则调用时会产生歧义。
3.简单来说:不传实参就能调用的构造函数,就是默认构造函数。 - 编译器默认构造的行为:
1…对于内置类型成员(如int、char、指针等):初始化行为不确定,取决于编译器,可能是随机值。
2…对于自定义类型成员(如class或struct对象):会自动调用其默认构造函数进行初始化;如果该成员没有默认构造函数,编译会报错,此时需要用初始化列表(之后再说)来解决。
总结:大多数的类都需要我们自己写构造函数,确定初始化方式。少数可以用默认生成构造函数,比如有类Stack实现栈,有类MyQuee用两个栈实现队列,也就是:
class MyQuee
{
private:
Stack _pushst;
Stack _popst;
};
这种就不用写构造函数,会调用类Stack里面的。
三、析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能。但有的类不需要Destroy功能,也就不需要析构函数。
析构函数的特点:
- 命名规则:函数名是在类名前加上波浪号 ~,例如 ~Stack()。
- 无参无返回值:没有参数,也没有返回值类型(连void都不能写),因此不支持重载。
- 唯一性:一个类只能有一个析构函数。如果用户没有显式定义,编译器会自动生成一个默认的析构函数。
- 自动调用:当对象生命周期结束时(如局部对象离开作用域、delete动态对象),系统会自动调用析构函数。
- 默认析构的行为:
1…对于内置类型成员(如int、指针等):编译器生成的默认析构函数不做任何处理,不会自动释放指针指向的内存。
2…对于自定义类型成员(如class或struct对象):会自动调用其析构函数,完成该成员的清理工作。 - 自定义析构的行为:即使我们显式定义了析构函数,对于自定义类型的成员,编译器依然会自动调用它们的析构函数,这是自动发生的。
- 是否需要手动编写:
1…不需要:如果类中没有申请任何资源(如动态内存、文件句柄等),使用编译器生成的默认析构函数即可,例如Date、MyQueue。
2…必须手动编写:如果类中申请了资源(如Stack类中动态分配的数组),就必须手动实现析构函数来释放这些资源,否则会造成资源泄漏。 - 调用顺序:在一个局部作用域中创建多个对象时,C++ 规定:后定义的对象先被析构,即遵循 “后进先出” 的栈式规则。
四、拷贝构造函数
拷贝构造函数是一种特殊的构造函数,用于用一个已存在的同类型对象来初始化一个新对象。
形式:第一个参数必须是自身类型的引用,其他参数(如果有)必须有默认值。
本质:是构造函数的一种重载,专门处理 “对象复制” 的场景。
拷贝构造函数的特点:
- 必须是引用参数,拷贝构造函数的第一个参数必须是类类型的引用,如果使用值传递,编译器会直接报错。这是因为值传递本身就会触发一次拷贝构造,导致无穷递归调用。
- 自定义类型的拷贝必须调用它,在 C++ 中,只要是对自定义类型对象进行值传递或值返回,都会调用拷贝构造函数完成复制。
- 默认生成规则
如果没有显式定义拷贝构造函数,编译器会自动生成一个默认版本:
1…对内置类型成员:完成逐字节的浅拷贝(值拷贝)。
2…对自定义类型成员:自动调用其拷贝构造函数。 - 何时需要手动实现:
不需要手动实现:当类中没有指向堆内存的指针等资源时(如Date(日期)类),默认的浅拷贝即可满足需求。
必须手动实现:当类中管理了动态资源(如Stack类中的_a指针指向堆内存),默认的浅拷贝会导致多个对象共享同一块内存,造成 double free 等问题,此时需要手动实现深拷贝(比如拷贝一个栈构造函数,虽然拷贝了但地址还是同一个地址,需要深拷贝解决,之后讲)。
小技巧:如果一个类显式实现了析构函数并释放了资源,那么它通常也需要显式实现拷贝构造函数。 - 传值返回会产生拷贝,当函数返回一个局部对象时,会调用拷贝构造函数生成一个临时对象。如果使用引用返回,则不会产生拷贝,但要注意返回的引用不能指向一个已经销毁的局部对象。
五、赋值运算符重载
运算符重载
我们都知道运算符,比如1 + 1 = 2。但是类类型怎么用运算符呢?比如有一个日期的类,我们想实现日期的加减,C++语言允许我们通过运算符重载的形式指定运算符新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
- 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
比如下面的Date类,实现日期的加法:
//Date.h
class Date//日期类
{
public:
Date(int year = 1,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int GetMonthDay(int year, int month) const //确认是不是润年
{
static int MonthDayArray[13] = { -1,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;
}
else
{
return MonthDayArray[month];
}
}
Date& operator+=(int day);//在类里面的声明,除了小的或者使用频率高的函数都放类外面
private:
int _year;
int _month;
int _day;
};
//Date.cpp
Date& Date::operator+=(int day)//返回的是Date的引用是为了少拷贝构造一次
{
//第一个参数是隐藏的this指针,存放D1
//下面写定义的+=运算符
_day += day;
while (_day > GetMonthDay(_year,_month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
- 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
- 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
- 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
- “ .* ”、“ :: ”、“ sizeof ”、“ ?: ”、“ . ”。这 5 个运算符无法重载。
- 重载函数至少有一个类类型参数(不能仅重载内置类型)。
- 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator*就没有意义。
- 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。比如:operator++(int),int随便填,主要是区分用的。
- 重载 << 和 >> 时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。 比如:
ostream& operator<<(ostream& os, const Date& d) {
os << d.year << "-" << d.month << "-" << d.day;
return os; // 返回os以支持连续输出(如cout << d1 << d2)
}
赋值运算符重载
赋值运算符重载(operator=)是 C++ 类的默认成员函数,核心作用是完成两个已存在对象间的拷贝赋值,需注意与拷贝构造区分:
- 拷贝构造:用于新对象由已有对象拷贝初始化(对象创建阶段)。
- 赋值重载:用于已有对象间的拷贝赋值(对象创建后)。
比如:
D1 = D2;//赋值重载
Date D1 = D2;//拷贝构造
- 必须作为成员函数重载。语法强制规定,无法重载为全局函数。
- 建议写成const 类名&(const 修饰的当前类类型引用),避免值传递带来的额外拷贝,提升效率;const 保证不修改传入的源对象。
- 建议返回类名&(当前类类型引用)。引用返回避免拷贝,提升效率;支持连续赋值(如a = b = c)。
- 未显式实现时,编译器自动生成默认版本,行为与默认拷贝构造类似:内置类型逐字节浅拷贝;自定义类型调用其赋值重载。
- 是否需要手动写赋值运算符重载,核心看类是否管理动态资源(如堆内存、文件句柄等):
1… 无需显式实现:类成员全是内置类型(如 Date 类:仅 int 年 / 月 / 日),或类内自定义类型成员已实现深拷贝赋值重载(如 MyQueue 类包含 Stack 成员,且 Stack 已写好深拷贝赋值);
2…必须显式实现:类持有动态资源(如 Stack 类的指针_a指向堆内存),编译器默认的浅拷贝会导致“浅拷贝问题”(如多个对象共用同一块资源、析构时重复释放);
六、取地址运算符重载
const成员函数
- 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。比如之前Date类型里面的int GetMonthDay(int year, int month) const 。
- const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
只有在极特殊需求下才需要自定义实现,最典型的场景是:不想让外部获取到对象的真实地址。
class Date {
public:
// 1. 普通取地址运算符重载(非const对象调用)
Date* operator&() {
// 默认行为:返回真实地址(this)
return this;
// 特殊需求:返回虚假地址(如禁止外部获取真实地址)
// return nullptr;
}
// 2. const取地址运算符重载(const对象调用)
const Date* operator&() const {
// 默认行为:返回const修饰的真实地址
return this;
// 特殊需求:返回虚假地址
// return nullptr;
}
private:
int _year, _month, _day;
};
7773

被折叠的 条评论
为什么被折叠?



