
🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
在C++面向对象学习中,类的默认成员函数是绕不开的核心知识点,也是夯实代码基础的关键。很多初学者会困惑:为什么编译器能自动生成函数,我们还需要手动实现?这些函数各自承担什么角色,又有哪些容易踩坑的细节?本文就从默认成员函数的基本概念出发,逐一拆解构造、析构、拷贝构造等六大函数的规则与用法,结合具体代码案例帮大家理清逻辑。最后还会通过完整的日期类实现,让理论落地实践,助力大家真正吃透这部分内容。

既然编译器能生成这些函数,那程序员在使用这些功能的时候还需要去写他们吗? 主要是因为一些编译器生成的函数不能满足我们的需求,这个时候就需要我们自己去实现,下面我们来一次看一下这些函数具体的规则。
构造函数是做初始化工作的,它有以下几个特点
定义方面:
#include<iostream>
using namespace std;
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;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};使用方面:
如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉 然后运行
Date d1;// 调⽤默认构造函数会导致编译报错:error C2512: “Date”: 没有合适的默认构造函数可⽤ 因为这里的Date d1是没有传值的,使用默认构造,但是三个默认构造类里面一个都没有
int main()
{
Date d1; // 调⽤默认构造函数
Date d2(2025, 1, 1); // 调⽤带参的构造函数
// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法
// 区分这⾥是函数声明还是实例化对象
// warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?)
Date d3();
d1.Print();
d2.Print();
return 0;
}析构函数是 “销毁函数” ,析构函数和栈和队列里面用的Destroy()函数类似,是用来销毁(释放)申请内存资源的函数,举个例子说上方的Date类并不许需要析构函数。
要销毁动态申请内存而不用销毁内置类型的原因是,动态申请内存在堆上,而函数内的内置类型在栈上,函数调用完会销毁栈帧
析构函数也有以下几个特点:
定义方面:
Date;如果存在申请资源,那就必须要写析构函数,如Stack;但是如果类里面有包含其他类型,而且其他类型全部有可用的析构函数,如MyQueue。
这里引入MyQueue的类定义代码:
typedef int STDataType;
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内部的资源
// 就算在这里显⽰写析构,return时候也会⾃动调⽤Stack的析构
/*
~MyQueue()
{}
*/
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}值得注意的是,如果前面
Stack没有显示定义析构函数的话,MyQueue会生成析构函数的,但是调用stack的默认析构函数时并没有处理申请的内存资源,也就是说会造成内存泄漏,注意这里并不会报错(并非编译错误而是内存泄漏)使用方面:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}拷贝构造函数实际上是构造函数的重载,也就是说,拷贝构造函数是一个特殊的构造函数
拷贝构造函数的第一个参数是自身类类型的引用,并且其他参数都有默认值
拷贝构造函数是用来将一个类直接赋值给另一个类的,下面有关特点展开叙述:
那么如果是传值传参的话会如何呢? 首先需要说明的一点是:
c++规定传值传参会引发拷贝构造:这是为了处理C语言中类似memcpy函数直接浅拷贝(一个字节一个字节拷贝),而如果拷贝的是含有指针的类类型或者是结构体类型的话,会造成拷贝出来的结果中也指向了同一块空间,只是变量名不同;而这又会导致析构时候连续释放两块相同空间会造成程序崩溃。

如果一个非拷贝构造函数去传值传参,他会在拷贝构造里去拷贝一个数据:
void Copy(Date d) { // 重点:传值传参
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
cout << "void Copy(Date d):普通拷贝函数调用(传值参数)" << endl;
}//这会很正常的调用但是如果拷贝构造函数去传值传参会导致如图无限递归:

3. 和其他默认成员函数一样,如果拷贝构造没有显式定义,那么会自动生成拷贝函数,自动生成的拷贝构造对内置类型变量会浅拷贝,对自定义类型会调用他的拷贝构造(myqueue调用stack)
data、stack、myqueue;
data内只有内置类型,不需要写拷贝构造、stack需要程序员深拷贝、myqueue如果存在stack就不需要自己定义
小技巧:析构和拷贝构造函数的显式定义总是一起出现拷贝构造使用时候有两种写法:
//写法一
Date d3(d1);
d2.Print();
//写法二
Date d4 = d1;
d2.Print();C++中有函数重载,同样允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转变成调用对应运算符重载,如果没有对应运算符重载,则会编译错误。
运算重载符是具有特殊名字的函数,名字由 函数返回类型 operator[运算符号](参数) 组成
重载运算符函数的参数个数必须和原符号的参数个数一样多,对于二元运算符来说第一个位于运算符左边,第二个参数位于运算符右边
由于成员函数第一个参数是隐式的this指针,重载运算符函数是成员函数的时候,参数少一个
重载运算符以后新运算符的优先性和结合性不变,并且也不能重载非法的符号
.*→成员函数的解引用 ::→作用域限定符 sizeof→计算变量的字节数 ?:→三目操作符 .→成员访问操作符这五个运算符不能重载,而且重载操作符必须有一个是类类型
这里介绍一下
.*符号、函数指针和函数指针typedef函数指针和typedef函数指针使用
#include <iostream>
using namespace std;
// 1. 回调函数1:求和
int sum(const int arr[], int len) {
int total = 0;
for (int i = 0; i < len; ++i) {
total += arr[i];
}
return total;
}
// 2. 回调函数2:找最大值
int maxVal(const int arr[], int len) {
int m = arr[0];
for (int i = 1; i < len; ++i) {
m = (arr[i] > m) ? arr[i] : m;
}
return m;
}
// 3. 数组处理函数:参数直接写函数指针类型(无别名,冗长)
void processArray(const int arr[], int len, int (*callback)(const int[], int)) {
int result = callback(arr, len);
cout << "结果:" << result << endl;
}
int main() {
int arr[] = {1, 3, 5, 7, 9};
int len = sizeof(arr) / sizeof(arr[0]);
// 传递回调函数(函数名隐式转换为函数指针)
processArray(arr, len, sum); // 求和:25
processArray(arr, len, maxVal); // 找最大值:9
return 0;
}typedef 给函数指针起别名#include <iostream>
using namespace std;
// 方式1:typedef 定义函数指针别名(兼容C)
// typedef int (*Callback)(const int[], int);
// 方式2:using 定义别名(C++11+推荐,语法更清晰)
using Callback = int (*)(const int[], int);
// 1. 回调函数1:求和(与无别名版一致)
int sum(const int arr[], int len) {
int total = 0;
for (int i = 0; i < len; ++i) {
total += arr[i];
}
return total;
}
// 2. 回调函数2:找最大值(与无别名版一致)
int maxVal(const int arr[], int len) {
int m = arr[0];
for (int i = 1; i < len; ++i) {
m = (arr[i] > m) ? arr[i] : m;
}
return m;
}
// 3. 数组处理函数:用别名替代冗长的函数指针类型
void processArray(const int arr[], int len, Callback callback) {
int result = callback(arr, len);
cout << "结果:" << result << endl;
}
int main() {
int arr[] = {1, 3, 5, 7, 9};
int len = sizeof(arr) / sizeof(arr[0]);
// 传递回调函数(用法不变,可读性更高)
processArray(arr, len, sum); // 求和:25
processArray(arr, len, maxVal); // 找最大值:9
// 额外优势:用别名直接定义函数指针变量
Callback funcPtr = sum;
cout << "单独调用回调(sum):" << funcPtr(arr, len) << endl; // 25
funcPtr = maxVal;
cout << "单独调用回调(maxVal):" << funcPtr(arr, len) << endl; // 9
return 0;
}以上两个样例由AI生成
对于普通函数正常去指针
PF pf = func,因为函数名本来就是指针 对于成员函数,c++规定前面要加&:PF pf = &func普通函数指针的使用有两种方式:pf(参数)、(*pf)(参数)而成员函数就需要解引用:d1.*pf这里用到了.*,这个符号可以理解为成员函数的解引用
int形参,跟前置++做区分。
d1 - d2有意义,是时间的差,而d1 + d2就没有意义,可以根据实际情况来赋值
this指针,而<<左操作数是cout,>>左操作数是cin,不符合操作逻辑,所以放在外面。
注意重载操作符的最后一个点:如果重载操作符函数要在类域外定义的话会有访问私有成员变量的问题,解决方案一般是把重载操作符函数放在类域内部,当然也有.get()等方法,但是不推荐。对于实在无法放在类内部的函数如<</>>,可以使用友元函数,关于友元函数我们下一篇文章详细讲解。
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象 拷⻉初始化 给另⼀个要创建的对象,即拷贝重载是携带初始化的,而赋值运算符重载必须在使用前初始化对象。
Date d1(2024, 7, 5);
Date d2(d1);
//以上是拷贝构造
//以下是赋值运算符重载
Date d3(2024, 7, 6);
d1 = d3;data、stack、myqueue;
data内只有内置类型,不需要写、stack需要程序员深拷贝、myqueue如果存在stack就不需要自己定义
小技巧:析构和赋值重载的显式定义总是一起出现将const修饰的成员函数称为const成员函数,const修饰成员函数放到成员函数的参数列表的后面。
const修饰的实际上是该成员函数隐含的this指针,表明在成员函数中不能对类的任何成员对象做出更改。
在成员函数中,以Print为例:void Print(Date* const this),实际内部是这个样子的,其中的 const是修饰this本身的,而如果用const修饰Print函数:void Print(Date* const this) const,其实际上是这样子的:void Print(const Date* const this),新增添的这个const是修饰*this指针的。

取地址运算符重载分为普通的取地址运算符重载和const取地址运算符重载,一般情况下编译器默认生成的取地址运算符就可以使用,对于一些特殊情况:例如不想让使用者得到实际的地址,这个时候我们可以在取地址操作符重载函数内部去返回空值或者随便返回一个地址。
下面基于以上六种默认成员函数和之前了解过的知识点写一个日期类
Date.h#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<cassert>
using namespace std;
class Date
{
//友元函数声明
friend ostream& operator<<(ostream&, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print() const;
//获取月份对应天数,同时声明放在函数内部,默认inline
int getmonthday(int year, int month)
{
static int day_array[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;
}
return day_array[month];
}
bool CheckDate();
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;
// d1 += 天数
Date& operator+=(int day);
Date operator+(int day);
// d1 -= 天数
Date& operator-=(int day);
Date operator-(int day);
// d1 - d2
int operator-(const Date& d);
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
private:
int _year;
int _month;
int _day;
};Date.cpp#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include"Date.h"
//打印函数
void Date::Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
//检查日期
bool Date::CheckDate()
{
if (_month < 1 || _month>12 || _day< 1 || _day>getmonthday(_year,_month))
{
return 0;
}
else
{
return 1;
}
}
//重载 <
bool Date::operator<(const Date& d) const
{
if(* this == d)
{
return false;
}
if (_year != d._year)
{
return _year < d._year;
}
if (_month != d._month)
{
return _month < d._month;
}
if (_day != d._day)
{
return _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 _year == d._year
&& _month == d._month
&& _day == d._day;
}
//重载 !=
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
//构造函数
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
{
cout << "输入非法" << endl;
}
}
//重载 +=
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day; // 负天数转成减正数
}
_day += day;
while (_day > Date::getmonthday(_year, _month))
{
_day -= Date::getmonthday(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
//重载 +
//可以复用+=来实现+,也可以复用+来实现+=,但是用+实现+=多了三次拷贝构造
Date Date::operator+(int day)
{
Date tmp = *this; //拷贝构造,将d1的日期拷贝到tmp上去
tmp += day;
return tmp;
}
//重载 -=
Date& Date::operator-=(int day)
{
if (day > 0)
{
_day -= day;
while (_day < 1)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += getmonthday(_year, _month);
}
return *this;
}
else
{
day = -day;
*this += day;
return *this;
}
}
//重载 -
Date Date::operator-(int day)
{
Date tmp = *this; //拷贝构造
tmp -= day;
return tmp;
}
// d1 - d2
//int Date::operator-(const Date& d)
//{
// if (*this < d)
// {
// assert("用小日期减大日期无意义");
// return -1;
// }
// int count = 0;//闰年个数
// for (int year = d._year;year < _year;year++)
// {
// if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
// {
// count++;
// }
// }
// int nums_day = 365*(_year - d._year) + count;//天数
// int month = _month - 1;
// int day = _day - 1;
// for (int i = 1; i <= month; i++)
// {
// day += getmonthday(_year, i);
// }
// month = d._month - 1;
// day -= (d._day - 1);
// for (int i = 1; i <= month; i++)
// {
// day -= getmonthday(d._year, i);
// }
// return nums_day + day;
//}
//重载 d1 - d2
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
flag = -1;
max = d;
min = *this;
}
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, const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
//重载流提取
istream& operator>>(istream& in, Date& d)
{
cout << "请输入年月日:";
cin >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "⽇期⾮法" << endl;
}
return in;
}