目录
一、类的默认成员函数
用户没有显示实现,编译器自动生成的成员函数称为默认成员函数。
在 C++ 中,默认成员函数是当类中没有显式定义时,编译器会自动生成的特殊成员函数。这些函数用于对象的创建、初始化、拷贝、移动、销毁等基础操作,具体包括以下 6 个
二、构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。
构造函数的特点:
1、函数名与类名相同;
2、无返回值;
注意:返回值啥都不需要给,也不需要写void,不要纠结,C++的语法就是这么规定的。
3、对象实例化时系统会自动调用对应的构造函数;不需要每个都写init()
4、构造函数可以重载;
5、如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
6、默认构造函数有三种:无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数。注意,这三种函数有且只有一个能存在,不能同时存在。
无参构造函数、全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
不是只有编译器默认生成的叫默认构造,无参构造函数、全缺省构造函数也是默认构造函数,默认构造总结一下就是:不传实参就可以调用的构造。
7、不用我们自己动手写,编译器默认生成的构造对内置类型成员变量的初始化没有要求,即是否初始化是不确定的,这个具体得看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数,就会报错,我们如果要初始化这个成员变量,需要用到初始化列表,初始化列表这个我们之后会介绍。
下面小编通过代码例子来说明第5点()
不显示定义构造,调用编译器自动生成的无参默认构造,编译器默认生成的构造对内置类型成员变量的初始化没有要求,所以要看具体的编译器
#include<iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//对象实例化一定会调用对应的构造,保证了对象实例化出来一定被初始化了
Date d1; // 调用默认构造函数
d1.Print();
return 0;
}
终端显示:

6、默认构造函数有三种:无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数。注意,这三种函数有且只有一个能存在,不能同时存在。
无参构造函数、全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
不是只有编译器默认生成的叫默认构造,无参构造函数、全缺省构造函数也是默认构造函数,默认构造总结一下就是:不传实参就可以调用的构造。
下面通过举例代码来说明(注意注释!!)
#include<iostream>
using namespace std;
// 定义日期类,用于封装日期相关的属性和操作
class Date
{
public:
// 1. 无参构造函数(默认构造函数之一)
// 特点:无需传入参数即可调用,用于初始化默认日期
Date()
{
_year = 1; // 将年份初始化为1
_month = 1; // 将月份初始化为1
_day = 1; // 将日期初始化为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; // 私有成员:存储日期
};
int main()
{
// 对象实例化时一定会调用对应的构造函数,确保对象被正确初始化
Date d1; // 调用无参构造函数(默认构造),初始化默认日期(1/1/1)
Date d2(2025, 1, 1); // 调用带参构造函数,初始化指定日期(2025/1/1)
// 注意:通过无参构造函数创建对象时,对象名后不能加括号
// 如下写法会被编译器误认为是函数声明(函数名为d3,返回值类型为Date)
// Date d3(); // 错误示例:无法区分是对象定义还是函数声明
// 调用Print方法打印日期信息
d1.Print(); // 输出d1的日期:1/1/1
d2.Print(); // 输出d2的日期:2025/1/1
// 默认构造函数说明:
// 1. 包含三种形式:无参构造函数、全缺省构造函数、编译器默认生成的构造函数
// 2. 三种形式有且只能存在一种,否则会导致调用歧义
// 3. 核心特征:不需要传入实参即可调用
return 0;
}
7、不用我们自己动手写,编译器默认生成的构造对内置类型成员变量的初始化没有要求,即是否初始化是不确定的,这个具体得看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数,就会报错,我们如果要初始化这个成员变量,需要用到初始化列表,初始化列表这个我们之后会介绍。
注意注释!!!
#include<iostream>
using namespace std;
// 定义栈的数据类型为int
typedef int STDataType;
// 栈类的定义
class Stack
{
public:
// Stack类的构造函数:带默认参数n=4(全缺省构造函数)
// 全缺省构造函数属于默认构造函数的一种(无需传参即可调用)
Stack(int n = 4)
{
// 为栈申请n个STDataType类型的空间
_a = (STDataType*)malloc(sizeof(STDataType) * n);
// 检查内存申请是否成功
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
// 初始化栈的容量和栈顶指针
_capacity = n; // 容量为n
_top = 0; // 栈顶指针初始化为0(指向栈顶元素的下一个位置)
}
// ... 此处省略栈的其他成员函数(如入栈、出栈等)
private:
STDataType* _a; // 指向栈的底层数组
size_t _capacity; // 栈的容量(最多能存储的元素个数)
size_t _top; // 栈顶指针(标识当前栈中元素的个数)
};
// 用两个栈实现队列的类
class MyQueue
{
public:
// 注意:MyQueue类没有显式定义任何构造函数
// 此时编译器会为MyQueue生成默认构造函数
// 编译器生成的默认构造函数会自动调用其所有自定义类型成员的默认构造函数
// 即会调用pushst.Stack()和popst.Stack()来初始化这两个成员
private:
// 自定义类型成员:两个Stack对象
Stack pushst; // 用于入队的栈
Stack popst; // 用于出队的栈
};
int main()
{
// 实例化MyQueue对象mq
// 这行代码会触发MyQueue的默认构造函数
// MyQueue的默认构造函数会进一步调用pushst和popst的默认构造函数(Stack的全缺省构造)
// 因为Stack有默认构造函数(全缺省),所以初始化成功
MyQueue mq;
return 0;
}
/* 关键原理说明:
1. 自定义类型成员的初始化规则:
当类(如MyQueue)包含自定义类型成员(如Stack对象)时,
编译器生成的默认构造函数会自动调用这些成员的默认构造函数来完成初始化。
2. 默认构造函数的定义:
无需传参即可调用的构造函数,包括:
- 无参构造函数(如Stack())
- 全缺省构造函数(如Stack(int n=4),本代码使用的类型)
- 编译器默认生成的构造函数(当类中没有显式定义任何构造函数时)
3. 错误场景模拟:
若将Stack的构造函数改为"Stack(int n)"(去掉默认参数),
则Stack不再有默认构造函数(必须传参才能构造)。
此时MyQueue的默认构造函数无法调用Stack成员的构造函数(因为需要传参却没传),
编译会报错:"没有合适的默认构造函数可用"。
4. 结论:
包含自定义类型成员的类,其成员的类必须有默认构造函数,
否则需要在该类的构造函数初始化列表中显式为成员传参。
*/
调试结果也是成功调用了stack的默认构造:()

三、析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,就像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
- 1. 析构函数名是在类名前加上字符 ~。
- 2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
- 3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
- 4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
- 5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会 调⽤他的析构函数。
- 6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数。
- 7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要 ⾃⼰写析构,否则会造成资源泄漏,如Stack。
- 8. ⼀个局部域的多个对象,C++规定后定义的先析构
下面通过代码例子来学习(注意注释!!!)
#include<iostream>
using namespace std;
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;
}
// 1. 析构函数名:在类名(Stack)前加 ~
// 2. 无参数无返回值(不需要写void,也不能加参数)
// 3. 一个类只能有一个析构函数,不能重载
~Stack()
{
cout << "~Stack()" << endl;
// 8. 局部域多个对象:后定义的先析构,后续main函数中会验证
// 7. Stack类申请了动态内存(_a指向的空间),必须显式写析构函数释放资源
// 否则会造成内存泄漏,默认析构函数不会处理内置类型(指针_a是内置类型)
free(_a);
_a = nullptr; // 避免野指针
_top = _capacity = 0; // 内置类型成员重置(非必须,仅为规范)
}
private:
STDataType* _a; // 内置类型成员(指针)
size_t _capacity; // 内置类型成员(无符号整数)
size_t _top; // 内置类型成员(无符号整数)
};
// 两个Stack实现队列
class MyQueue
{
public:
// 3. MyQueue未显式定义析构函数,编译器会自动生成默认析构函数
// 5. 编译器默认生成的析构函数:对内置类型成员不做处理,对自定义类型成员调用其析构函数
// 这里pushst和popst是Stack类型(自定义类型),默认析构会调用它们的~Stack()
// 6. 即使显式写MyQueue的析构函数(如下注释代码),也会自动调用自定义类型成员的析构函数
// 无需手动在MyQueue析构中调用pushst.~Stack()或popst.~Stack()
/*~MyQueue()
{}*/
private:
Stack pushst; // 自定义类型成员(Stack类对象)
Stack popst; // 自定义类型成员(Stack类对象)
};
int main()
{
// 定义两个局部对象:先定义st,后定义mq
Stack st; // 局部对象1:生命周期从定义到main函数结束
MyQueue mq; // 局部对象2:生命周期从定义到main函数结束
// 4. 程序运行到main函数末尾,对象生命周期结束,系统自动调用析构函数
// 8. 局部域多个对象析构顺序:后定义的先析构
// 执行顺序:先析构mq(后定义),再析构st(先定义)
// mq析构时,会先调用其内部两个Stack成员的析构:popst先析构(后定义),pushst后析构(先定义)
// 最终控制台输出顺序:~Stack()(popst)→ ~Stack()(pushst)→ ~Stack()(st)
return 0;
}
/* 析构函数核心规则总结(对应代码注释):
1. 命名规则:类名前加~,如Stack的析构函数是~Stack()。
2. 格式要求:无参数、无返回值(不能写void)。
3. 唯一性:一个类只能有一个析构函数,无法重载;未显式定义则编译器生成默认析构。
4. 调用时机:对象生命周期结束时(如局部对象出作用域、动态对象delete时),系统自动调用。
5. 默认析构行为:对内置类型成员(如指针、int)不处理,对自定义类型成员调用其析构函数。
6. 显式析构行为:即使手动写析构函数,自定义类型成员的析构仍会自动调用,无需手动触发。
7. 析构函数必要性:类中无资源申请(如Date类仅存int成员),可不用写析构;有资源申请(如Stack的动态内存),必须显式写析构释放资源,避免泄漏。
8. 局部对象析构顺序:同一局部域中,后定义的对象先析构。
*/
四、拷贝构造
(一)拷贝构造的概念
如果一个构造函数的第一个参数是自身类类型的引用,并且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
(二)拷贝构造的特点

第二点,拷贝构造无穷递归

下面通过代码示例来解释(注意注释!!!)
代码注释演示
代码 1:Date 类拷贝构造核心规则演示
#include<iostream>
using namespace std;
class Date
{
public:
// 普通构造函数(全缺省)
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 1. 拷贝构造函数是构造函数的一个重载(函数名相同,参数不同)
// 2. 第一个参数必须是类类型对象的引用(const修饰避免修改原对象)
// 若写成 Date(Date d) 会编译报错:引发无穷递归(传值传参需调用拷贝构造,拷贝构造又要传值,循环往复)
// 拷贝构造可加其他参数,但后续参数必须有缺省值,例如:Date(const Date& d, int n=0)
Date(const 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;
};
// 3. 自定义类型传值传参:会调用拷贝构造函数完成拷贝
void Func1(Date d) // d是形参,接收实参d1时需拷贝,触发拷贝构造
{
cout << &d << endl; // d是新对象,地址与d1不同
d.Print();
}
// 6. 传引用返回分析:返回局部对象的引用是错误的
// 若写成 Date Func2() 则是传值返回,会生成临时对象并调用拷贝构造
Date& Func2()
{
Date tmp(2024, 7, 5); // tmp是局部对象,函数结束后生命周期结束
tmp.Print();
return tmp; // 错误:返回局部对象的引用,函数结束后tmp销毁,变成野引用
}
int main()
{
Date d1(2024, 7, 5);
Func1(d1); // 传值传参,调用拷贝构造生成d(Func1的形参)
cout << &d1 << endl; // 对比d1和Func1中d的地址,验证是不同对象
// 用指针参数的普通构造初始化,不是拷贝构造
Date d2(&d1);
d1.Print();
d2.Print();
// 拷贝构造的两种调用场景:用同类型对象初始化新对象
Date d3(d1); // 直接调用拷贝构造
Date d4 = d1; // 赋值语法初始化,本质也是调用拷贝构造(不是赋值运算符)
d3.Print();
d4.Print();
// 6. 野引用问题:Func2返回的tmp已销毁,ret引用无效内存
Date ret = Func2();
ret.Print(); // 结果可能乱码或程序异常
// 5. Date类成员都是内置类型,无资源依赖,编译器默认生成的拷贝构造足够用
// 即使我们不写Date(const Date& d),d3、d4的拷贝也能正常完成(值拷贝)
return 0;
}

000000BF31F4FA28和000000BF31F4F878是Func1中形参d和主函数中d1的地址,不同的地址说明发生了拷贝构造,验证了自定义类型传值传参需调用拷贝构造的规则。- 多次输出
2024-7-5是Date类的- 最后一行乱码(
-44227244-32759--44150666)是因为Func2返回了局部对象的引用,导致野引用,访问了已销毁的内存,体现了局部对象引用返回的风险。
代码 2:Stack 与 MyQueue 深拷贝规则演示
#include<iostream>
#include<cstring> // 包含memcpy函数
using namespace std;
typedef int STDataType;
class Stack
{
public:
// 普通构造函数:申请动态内存资源(_a指向堆空间)
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// 5. Stack类必须显式实现拷贝构造(深拷贝)
// 原因:_a指向堆空间资源,编译器默认生成的拷贝构造是浅拷贝(仅拷贝指针地址)
// 浅拷贝会导致两个对象的_a指向同一块内存,析构时会释放两次,程序崩溃
Stack(const Stack& st)
{
// 深拷贝:为新对象重新申请一块与原对象容量相同的内存
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
return;
}
// 拷贝原对象的元素数据(不是拷贝指针地址)
memcpy(_a, st._a, sizeof(STDataType) * st._top);
// 拷贝其他内置类型成员(值拷贝)
_top = st._top;
_capacity = st._capacity;
}
// 入栈操作(辅助演示,用于给栈添加数据)
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
// 析构函数:释放动态内存资源
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a; // 指向堆空间的指针(内置类型,但关联资源)
size_t _capacity; // 内置类型(容量)
size_t _top; // 内置类型(栈顶指针)
};
// 两个Stack实现队列
class MyQueue
{
public:
// 4. 编译器默认生成的拷贝构造:对自定义类型成员调用其拷贝构造
// MyQueue的成员是Stack类型(自定义类型),默认拷贝构造会自动调用Stack的拷贝构造
// 5. 因此MyQueue无需显式实现拷贝构造,依赖Stack的深拷贝即可保证正确性
private:
Stack pushst; // 自定义类型成员
Stack popst; // 自定义类型成员
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// 调用Stack的显式拷贝构造(深拷贝),st1和st2的_a指向不同堆空间
Stack st2 = st1; // 若Stack未显式实现深拷贝,此处会因浅拷贝导致后续析构崩溃
MyQueue mq1;
// 调用MyQueue默认生成的拷贝构造,自动调用pushst和popst的拷贝构造(深拷贝)
MyQueue mq2 = mq1; // 无需显式实现MyQueue的拷贝构造,完全可靠
// 5. 小技巧验证:Stack显式实现了析构(释放资源),因此必须显式实现拷贝构造
// MyQueue未显式实现析构,因此无需显式实现拷贝构造
return 0;
}

- 多次输出
~Stack()是Stack类析构函数的调用提示。
Stack st1和Stack st2各调用一次析构,共 2 次。MyQueue mq1和MyQueue mq2中的pushst和popst各调用一次析构,共 4 次。- 总计 6 次析构调用,且没有内存重复释放的崩溃,验证了Stack 深拷贝的正确性和MyQueue 依赖自定义成员拷贝构造的合理性。
| 规则编号 | 代码体现核心点 |
|---|---|
| 1 | Date 类中Date(const Date& d)与普通构造函数构成重载 |
| 2 | 拷贝构造第一个参数必须是引用,传值会报错;支持带缺省值的额外参数 |
| 3 | Func1 传值传参触发拷贝构造,传值返回(若 Func2 返回 Date 而非引用)也会触发 |
| 4 | MyQueue 默认拷贝构造调用 Stack 的拷贝构造;Date 默认拷贝构造完成内置类型值拷贝 |
| 5 | Date 无需显式拷贝构造(无资源),Stack 必须显式深拷贝(有堆资源),MyQueue 无需显式拷贝构造(依赖自定义成员的拷贝构造) |
| 6 | Func2 返回局部对象引用导致野引用;传引用返回需确保对象生命周期长于函数 |
拷贝构造函数核心规则 - 代码实例对照表
| 规则编号 | 核心规则描述 | 对应代码实例(关键片段 + 说明) |
|---|---|---|
| 1 | 拷贝构造函数是构造函数的一个重载 | Date 类代码:Date(int year = 1, int month = 1, int day = 1)(普通构造)Date(const Date& d)(拷贝构造)→ 函数名相同,参数列表不同,满足重载定义 |
| 2 | 第一个参数必须是类类型对象的引用(传值报错,避免无穷递归);可加带缺省值的额外参数 | Date 类代码:❌ 错误写法(注释掉):Date(Date d)(编译报错:无穷递归)✅ 正确写法:Date(const Date& d)(引用参数)✅ 扩展写法(示例):Date(const Date& d, int n=0)(额外参数带缺省值) |
| 3 | 自定义类型传值传参、传值返回会调用拷贝构造 | Date 类代码:1. 传值传参:void Func1(Date d),调用Func1(d1)时触发拷贝构造(d 是 d1 的拷贝)2. 传值返回(示例):若Func2返回类型改为Date(非引用),返回tmp时会生成临时对象,触发拷贝构造 |
| 4 | 未显式定义时,编译器生成默认拷贝构造:内置类型值拷贝 / 浅拷贝,自定义类型调用其拷贝构造 | MyQueue 类代码:MyQueue 未显式写拷贝构造,编译器默认生成→ 初始化mq2 = mq1时,自动调用pushst和popst(Stack 类型)的拷贝构造→ Stack 的内置类型成员(_a、_capacity等)由 Stack 的拷贝构造处理 |
| 5 | 无需显式拷贝构造的场景:Date(无资源的内置类型成员)、MyQueue(依赖自定义成员拷贝构造);必须显式的场景:Stack(有堆资源,需深拷贝) | 1. Date 类:成员是int(无资源),默认拷贝构造的浅拷贝足够用2. Stack 类:_a指向堆空间,默认浅拷贝导致双次析构崩溃,需显式实现深拷贝(重新申请内存 + 拷贝数据)3. MyQueue 类:成员是 Stack(已实现深拷贝),默认拷贝构造自动调用 Stack 的拷贝构造,无需手动写 |
| 6 | 传引用返回无拷贝,但需确保返回对象生命周期长于函数;局部对象引用返回会产生野引用 | Date 类代码:❌ 错误写法:Date& Func2()返回局部对象tmp,函数结束tmp销毁,ret成为野引用✅ 正确场景(示例):若返回全局对象 / 静态对象的引用,生命周期长于函数,可安全使用传引用返回(减少拷贝) |
【结尾】:
今天的内容就到这里了,希望大家给个三连支持一下,后续会继续更新,感谢阅读!
【封面】:

1180

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



