
大家好,我是你们的小小风呀!上一篇我们讲了类和对象的基础,认识了类的定义、实例化和 this 指针。今天我们继续深入,来聊聊类里最核心的默认成员函数!
很多新手刚学的时候会发现,我明明什么成员函数都没写,创建对象、销毁对象居然都能正常运行?这就是因为编译器偷偷帮我们做了很多事,默认生成了 6 个成员函数!今天我们就把这 6 个函数一个个拆开来,用大白话 + 简单代码,保证你看完就懂!
目录
[示例 1:构造函数的基本使用](#示例 1:构造函数的基本使用)
[示例 2:全缺省的构造函数](#示例 2:全缺省的构造函数)
[3.示例 3:析构函数的基本使用](#3.示例 3:析构函数的基本使用)
[3.示例 4:默认拷贝构造的使用](#3.示例 4:默认拷贝构造的使用)
[4.示例 5:浅拷贝的坑,重复释放崩溃](#4.示例 5:浅拷贝的坑,重复释放崩溃)
[5.示例 6:深拷贝的拷贝构造,解决浅拷贝问题](#5.示例 6:深拷贝的拷贝构造,解决浅拷贝问题)
[4.示例 7:赋值运算符重载的实现](#4.示例 7:赋值运算符重载的实现)
[六、取地址运算符重载和 const 成员函数](#六、取地址运算符重载和 const 成员函数)
[2.示例 8:取地址重载和 const 成员函数](#2.示例 8:取地址重载和 const 成员函数)
一、什么是默认成员函数?
基础概念
默认成员函数就是:你定义一个类的时候,如果你什么成员函数都不写,编译器也会自动给你生成 6 个默认的成员函数,帮你处理对象的创建、销毁、拷贝这些通用的操作,不用你自己写。
这 6 个函数分别是什么呢?我们用一个简单的思维导图来给大家列出来:

是不是很清晰?接下来我们一个个来拆解,每个函数到底是干嘛的,有什么特点!
二、构造函数:对象的初始化
1.基础概念
我们之前写类的时候,创建完对象,还要手动调用一个**Init函数** 来初始化成员变量,比如之前的 Student 类,要手动set_name、set_age,太麻烦了!
而构造函数,就是专门用来初始化对象的!创建对象的时候,编译器会自动调用它,帮你把成员变量初始化好,不用你手动调用了,是不是很方便?
2.构造函数的特点
它有 7 个非常重要的特点,新手一定要记牢:
-
函数名必须和类名一模一样 :比如类叫
Date,构造函数就必须叫Date,不能改名字。 -
没有返回值!连 void 都不能写:因为它是创建对象的时候自动调用的,不需要返回什么东西给你。
-
可以重载!:你可以写多个构造函数,只要参数不同就行,就像我们之前学的函数重载一样。
-
可以带参数,也支持缺省参数:你可以给参数加默认值,这样不传参也能初始化。
-
创建对象的时候,编译器自动调用:你不用手动去调用它,定义对象或者 new 对象的时候,它自己就跑了。
-
如果你没写,编译器会自动生成一个空的无参构造:就是你什么都不写,编译器也会给你一个空的构造函数,帮你创建对象。
-
默认构造函数指的是 "不用传参就能调用的构造":无参的、全缺省的都算,一个类只能有一个默认构造函数,不然编译器不知道调用哪个。
示例 1:构造函数的基本使用
这个例子会展示怎么写不同的构造函数,看看它们是怎么自动调用的。
cpp
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
// 1. 无参构造函数
Date()
{
_year = 2024;
_month = 1;
_day = 1;
cout << "无参构造函数调用了!" << endl;
}
// 2. 带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << "带参构造函数调用了!" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
// 调用无参构造,注意!这里不能写Date d1(); 那会变成函数声明!
Date d1;
d1.Print();
// 调用带参构造
Date d2(2024, 4, 26);
d2.Print();
return 0;
}
运行结果:
cpp
无参构造函数调用了!
2024-1-1
带参构造函数调用了!
2024-4-26
新手踩坑提醒:定义无参对象的时候,千万不要写
Date d1();!这会被编译器当成一个返回值是 Date、没有参数的函数声明,而不是对象!
示例 2:全缺省的构造函数
这个例子会展示全缺省的构造函数,它既可以不传参,也可以传参,非常灵活。
cpp
#include<iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
// 全缺省构造,所有参数都有默认值
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
cout << "全缺省构造调用了!" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
// 不传参,用默认值
Date d1;
d1.Print();
// 传一个参数
Date d2(2025);
d2.Print();
// 传三个参数
Date d3(2024, 4, 26);
d3.Print();
return 0;
}
运行结果:
cpp
全缺省构造调用了!
2024-1-1
全缺省构造调用了!
2025-1-1
全缺省构造调用了!
2024-4-26
你看,一个构造函数就搞定了所有情况,是不是很方便?而且它就是我们说的默认构造函数,因为不用传参就能调用。
三、析构函数:对象的销毁
1.基础概念
析构函数和构造函数正好反过来!构造是对象出生的时候,帮你初始化、申请资源;析构就是对象死亡的时候,帮你清理资源!
比如你在对象里申请了堆内存、打开了文件、申请了锁,这些系统不会自动帮你释放,你就要在析构函数里自己写,对象销毁的时候,编译器会自动调用析构,帮你把这些资源释放掉,不然就会造成内存泄漏!
2.析构函数的特点
它有 6 个重要的特点:
-
函数名是~加类名 :比如类叫
Date,析构就是~Date,前面加个波浪号,很好记。 -
没有返回值,也没有任何参数:因为它是自动调用的,不需要你传东西,也不需要返回东西。
-
不能重载!:因为没有参数,所以一个类只能有一个析构函数,你没法写多个不同的。
-
对象销毁的时候,编译器自动调用:比如局部对象,出了作用域就自动调用析构,不用你手动调用。
-
如果你没写,编译器会自动生成一个空的析构:如果你的类里没有什么需要清理的资源,比如只有 int、string 这些,编译器生成的空析构就够了。
-
主要用来释放对象自己申请的资源:比如堆内存、文件句柄、锁这些,这些系统不会自动帮你释放,需要你在析构里自己写。
3.示例 3:析构函数的基本使用
这个例子会展示析构函数怎么帮我们释放堆内存,避免内存泄漏。
cpp
#include <iostream>
using namespace std;
class Stack
{
private:
int* _a;
int _top;
int _capacity;
public:
//构造函数
Stack(int capacity = 4)
{
// 构造里申请堆内存
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
cout << "构造函数调用了,申请了内存" << endl;
}
// 析构函数,释放资源
~Stack() {
free(_a);
_a = nullptr;
cout << "析构函数调用了,释放了内存" << endl;
}
void Push(int x)
{ }
};
int main()
{
// 我们用一个大括号来做作用域
{
Stack s;
// 在这里s还是活着的
}
// 出了这个作用域,s就销毁了,自动调用析构!
cout << "出了作用域了" << endl;
return 0;
}
运行结果:
cpp
构造函数调用了,申请了内存
析构函数调用了,释放了内存
出了作用域了
你看,是不是很神奇?我们什么都没做,出了作用域,析构就自动跑了,把我们申请的内存释放了,这样就不会有内存泄漏了!
四、拷贝构造函数:对象的克隆
1.基础概念
拷贝构造,顾名思义,就是拷贝一个已有的对象,来创建一个一模一样的新对象,就像克隆一样!你已经有了一个对象 d1,现在要创建一个新对象 d2,和 d1 完全一样,这时候就会调用拷贝构造函数。
2.拷贝构造函数的特点
它有 6 个重要的特点:
-
它是构造函数的一种,专门用来初始化新对象:注意哦,它是初始化,不是赋值!是创建新对象的时候才用的。
-
只有一个参数,就是当前类的 const 引用:必须是引用,不然会无限递归!因为传值的话,又要调用拷贝构造来拷贝参数,就死循环了。
-
如果你没写,编译器会自动生成,默认是浅拷贝:就是把原来对象的成员变量,一个个的值拷贝过来,但是如果有指针的话,就会出问题。
-
用一个对象初始化另一个对象的时候,自动调用 :比如
Date d2(d1);或者Date d2 = d1;,这时候都会调用拷贝构造。 -
传值传参的时候,会自动调用拷贝构造 :比如你函数的参数是
Date类型,传值的话,就会把实参拷贝给形参,调用拷贝构造。 -
值返回的时候,也会自动调用拷贝构造:函数返回值是值类型的话,会把返回的对象拷贝到外面,调用拷贝构造。
3.示例 4:默认拷贝构造的使用
这个例子会展示默认的拷贝构造,对于普通的成员变量,它是完全没问题的。
cpp
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
int main()
{
//初始化d1
Date d1(2024, 4, 26);
// 用d1初始化d2,调用编译器自动生成的默认拷贝构造
Date d2 = d1;
d1.Print();
d2.Print();
return 0;
}
运行结果:
cpp
2024-4-26
2024-4-26
你看,两个对象的内容完全一样,因为都是 int 成员,浅拷贝没问题,两个对象互不影响。
4.示例 5:浅拷贝的坑,重复释放崩溃
但是如果你的类里有指针成员,默认的浅拷贝就会出大问题!这个例子会给你展示这个坑。
cpp
#include <iostream>
using namespace std;
class Stack
{
private:
int* _a;
int _top;
int _capacity;
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
cout << "构造调用" << endl;
}
//析构函数
~Stack()
{
free(_a);
_a = NULL;
cout << "析构调用" << endl;
}
void Push(int x)
{ }
};
int main()
{
Stack s1;
// 用s1拷贝构造s2,默认的浅拷贝
Stack s2 = s1;
return 0;
}
这个代码运行之后会直接崩溃!为什么?因为默认的浅拷贝,只是把s1的_a指针的值拷贝给了s2,也就是说,两个对象的指针指向了同一块堆内存!
然后对象销毁的时候,s2 先析构,把这块内存 free 了,然后 s1 析构的时候,又 free 了一次!重复释放同一块内存,程序就直接崩了!这就是浅拷贝的大问题!
5.示例 6:深拷贝的拷贝构造,解决浅拷贝问题
那怎么解决呢?我们自己写拷贝构造,做深拷贝!就是不仅拷贝指针的值,还要把指针指向的内存也拷贝一份,让两个对象的指针指向不同的内存!
cpp
#include <iostream>
#include <string.h>
using namespace std;
class Stack
{
private:
int* _a;
int _top;
int _capacity;
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
cout << "构造调用" << endl;
}
// 自己写的拷贝构造,深拷贝!
Stack(const Stack& s)
{
// 先拷贝普通的成员
_top = s._top;
_capacity = s._capacity;
// 重点!自己申请新的内存,把数据拷贝过来!
_a = (int*)malloc(sizeof(int) * _capacity);
memcpy(_a, s._a, sizeof(int) * _top);
cout << "拷贝构造(深拷贝)调用" << endl;
}
~Stack()
{
free(_a);
cout << "析构调用" << endl;
}
void Push(int x)
{ }
};
int main()
{
Stack s1;
// 现在调用我们自己写的深拷贝构造
Stack s2 = s1;
return 0;
}
运行结果:
cpp
构造调用
拷贝构造(深拷贝)调用
析构调用
析构调用
你看,现在就正常了!两个对象各自有自己的内存,析构的时候各自释放,就不会重复释放了!
五、赋值运算符重载:对象的赋值
1.基础概念
首先我们来聊聊什么是运算符重载,我们之前学了函数重载,运算符重载其实就是,把运算符当成一个特殊的函数,你可以自己定义这个运算符对你的类怎么用!
比如我们之前学的+,只能给 int、double 这些用,现在你可以重载它,让它能给你的 Date 类用,比如d1 + 10,就是日期加 10 天,这就是运算符重载!
2.注意点:
-
不能改变运算符的优先级,比如
*本来比+高,你重载之后还是一样。 -
不能创造新的运算符,比如你不能创造一个
**的运算符,只能重载 C++ 已经有的。 -
不能改变运算符的操作数个数,比如
+本来是两个操作数,你不能改成一个。
而我们今天要讲的赋值运算符重载 ,就是重载=这个运算符,用来把一个对象赋值给另一个已经存在的对象!
注意哦,和拷贝构造不一样!拷贝构造是创建新对象的时候用的,赋值是两个对象都已经创建好了,把一个的内容给另一个!
3.赋值运算符的重载
它有 4 个重要的特点:
-
参数是当前类的 const 引用:和拷贝构造一样,不能传值,不然会递归调用。
-
返回值是当前类的引用 :这样才能支持连续赋值,比如
d1 = d2 = d3;,不然返回值的话就做不到了。 -
必须检查自赋值 :就是不能自己给自己赋值,比如
d1 = d1;,不然你把自己的资源释放了,就凉了。 -
如果你没写,编译器会自动生成,默认也是浅拷贝:和拷贝构造一样,默认的赋值也是值拷贝,有指针的话也会出问题。
4.示例 7:赋值运算符重载的实现
这个例子会展示我们怎么自己实现赋值运算符的深拷贝,解决浅拷贝的问题。
cpp
#include <iostream>
#include <string.h>
using namespace std;
class Stack
{
private:
int* _a;
int _top;
int _capacity;
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
}
// 拷贝构造我们之前写过了
Stack(const Stack& s)
{
_top = s._top;
_capacity = s._capacity;
_a = (int*)malloc(sizeof(int) * _capacity);
memcpy(_a, s._a, sizeof(int) * _top);
}
// 赋值运算符重载!
Stack& operator=(const Stack& s)
{
// 1. 第一步,检查自赋值!
if (this == &s)
{
// 自己给自己赋值,直接返回,啥也不用干
return *this;
}
// 2. 第二步,释放自己原来的旧资源
free(_a);
// 3. 第三步,拷贝新的资源,和拷贝构造差不多
_top = s._top;
_capacity = s._capacity;
_a = (int*)malloc(sizeof(int) * _capacity);
memcpy(_a, s._a, sizeof(int) * _top);
// 4. 第四步,返回*this,支持连续赋值
return *this;
}
~Stack()
{
free(_a);
}
void Push(int x)
{
_a[_top] = x;
_top++;
}
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
Stack s2;
// 赋值,调用我们自己写的赋值重载
s2 = s1;
// 连续赋值!因为返回了引用,所以可以这么写
Stack s3;
s3 = s2 = s1;
return 0;
}
这样,我们就搞定了赋值的问题,两个对象的资源是独立的,不会再有浅拷贝的坑了!
六、取地址运算符重载和 const 成员函数
1.基础概念
最后我们来看两个比较简单的默认成员函数,首先是const 成员函数:
什么是 const 成员函数?就是成员函数后面加个const,比如void Print() const;,它的意思是:这个函数里,this 指针是 const 的,也就是说,你不能在这个函数里修改成员变量,相当于给这个函数加了个只读的保护!
这样有什么好处呢?const 对象也能调用这个函数!因为 const 对象的 this 是 const 的,普通的成员函数的 this 是非 const 的,const 对象不能调用,但是 const 成员函数就可以!
然后是取地址运算符重载 ,就是重载&这个运算符,默认的&运算符,对一个对象用,就是返回这个对象的地址,但是你也可以自己重载它,比如你想隐藏对象的真实地址,或者返回别的东西,这时候就可以重载。
而且,你还要写一个 const 版本的,因为 const 对象调用&的时候,会调用 const 的重载版本,这就是为什么默认成员函数里有两个取地址的重载!
2.示例 8:取地址重载和 const 成员函数
这个例子会展示这两个特性的用法。
cpp
#include <iostream>
using namespace std;
class Date {
private:
int _year;
int _month;
int _day;
public:
Date(int year, int month, int day)
//初始化列表:后面会进一步学习,先做了解
: _year(year), _month(month), _day(day)
{
}
// 普通的取地址重载,给普通对象用
Date* operator&()
{
cout << "普通的取地址重载调用了" << endl;
// 这里我们可以返回别的,比如隐藏真实地址,这里就返回this演示
return this;
}
// const的取地址重载,给const对象用的
const Date* operator&() const
{
cout << "const的取地址重载调用了" << endl;
return this;
}
// const成员函数,不能修改成员变量
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
// 这里不能修改_year,不然会报错,因为this是const的
// _year = 2025; 这行编译会报错!
}
};
int main()
{
Date d1(2024, 4, 26);
// 普通对象调用,调用普通的取地址
cout << &d1 << endl;
const Date d2(2024, 1, 1);
// const对象调用,调用const的取地址
cout << &d2 << endl;
// const对象调用const成员函数
d2.Print();
return 0;
}
运行结果:
cpp
普通的取地址重载调用了
0x7ffd8b7c5a4c
const的取地址重载调用了
0x7ffd8b7c5a48
2024-1-1
你看,是不是很清晰?普通对象和 const 对象,分别调用了对应的重载版本,const 成员函数也保护了成员变量不会被修改。
总结:
今天我们把 6 个默认成员函数全部讲完了!这几个函数是类和对象的核心,很多新手刚学的时候会搞混拷贝构造和赋值,搞不清浅拷贝和深拷贝,没关系,把今天的例子自己敲一遍,动手实操比看十遍都有用!
如果有任何不懂的地方,欢迎在评论区留言,我会一一回复!下一篇我们会讲类和对象的进阶内容,别忘了点赞收藏关注,我们下期再见~