类的默认成员函数
编译器会自动生成一些成员函数,称为默认成员函数。在 C++11 之前,一个类如果不显式定义,编译器会默认生成以下 6 个函数;C++11 之后增加了移动构造函数和移动赋值函数。
我们主要关注两点:
- 不写时,编译器生成的行为是否符合需求?
- 如果不符合,如何自己实现?
通常编译器生成的默认版本并不完全满足业务逻辑,特别是涉及资源管理时,我们需要手动干预。
构造函数
构造函数的核心任务是对象实例化时的初始化。就像以前写栈或队列需要单独调用 Stack Init(),有了构造函数就不需要这一步了。
关键特性:
- 函数名与类名相同,无返回值(连 void 都不能写)。
- 对象实例化时系统自动调用。
- 支持重载。
- 若未显式定义,编译器生成一个无参的默认构造函数。一旦用户定义了任意构造函数,编译器将不再自动生成无参版本。
- 注意: 无参构造函数、全缺省构造函数以及编译器默认生成的构造函数,统称为'默认构造函数'。它们有且只有一个存在,不能同时存在。虽然无参和全缺省构成重载,但调用时若无实参会有歧义。
对于内置类型成员变量,编译器默认生成的构造函数不会初始化,值是不确定的。对于自定义类型成员变量,则会调用其默认构造函数。如果该成员没有默认构造函数,编译会报错,此时必须使用初始化列表。
基本用法
class Date {
public:
// 无参构造函数
Date() { _year = 1; _month = 1; _day = 1; }
// 带参构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 全缺省构造函数
// Date(int year = 1, int month = 1, int day = 1) { ... }
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1; // 调用默认构造函数
d1.Print(); // 输出:1/1/1
Date d2(2025, 11, 25); // 调用带参构造函数
d2.Print();
return 0;
}
特殊场景:容器中的对象
当类中包含其他自定义类型成员时,构造顺序很重要。编译器会先调用成员的构造函数,再执行当前类的构造函数体。
typedef int STDataType;
class Stack {
public:
// 默认构造函数,若改成 int n 则不是默认构造函数
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
public:
// 编译器默认生成 MyQueue 的构造函数,调用了 Stack 的构造
// 完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main() {
MyQueue mq;
return 0;
}
析构函数
析构函数与构造函数功能相反,用于对象生命周期结束时清理资源。C++ 规定对象销毁时会自动调用析构函数。
关键特性:
- 函数名是类名前加
~,如~Stack()。 - 无参数、无返回值。
- 一个类只能有一个析构函数。
- 若未显式定义,系统生成默认析构函数。
- 对象生命周期结束(如离开作用域)时自动调用。
对于内置类型成员,默认析构不做处理。自定义类型成员会调用其析构函数。重点: 如果类中申请了动态资源(如 malloc),必须显式编写析构函数释放内存,否则会造成泄漏。
运用示例
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a) {
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
~Stack() {
free(_a);
_a = nullptr; // 修正原代码笔误
_capacity = _top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue {
private:
// 自定义类型成员,析构时会递归调用 Stack 的析构
Stack pushst;
Stack popst;
};
int main() {
MyQueue mq;
return 0;
}
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且额外参数都有默认值,则称为拷贝构造函数。它是构造函数的重载形式。
核心规则:
- 第一个参数必须是类类型对象的引用(传值会导致无穷递归)。
- 若未显式定义,编译器生成默认的拷贝构造函数。
- 默认拷贝对内置类型是浅拷贝(字节级复制),对自定义类型成员调用其拷贝构造。
何时需要自己实现?
- 像
Date这样全是内置类型且无资源,默认即可。 - 像
Stack这样有指针指向堆资源,默认浅拷贝会导致析构时重复释放,需实现深拷贝。 - 像
MyQueue内部是自定义类型,默认会调用成员的拷贝构造,通常无需重写。
经验法则: 如果类显式实现了析构并释放资源,那么通常需要显式实现拷贝构造和赋值运算符。
常见误区:无穷递归
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year; _month = month; _day = day;
}
// 错误写法:传值会导致递归调用自身
// Date(const Date d) { ... }
// 正确写法:必须加引用 const
Date(const 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;
};
void Func1(const Date& d) {
cout << &d << endl;
d.Print();
}
int main() {
Date d1(2025, 11, 16);
d1.Print();
Func1(d1);
Date d2(d1); // 调用拷贝构造
d2.Print();
return 0;
}
浅拷贝与深拷贝
问题:同一空间被释放两次
#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;
}
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;
};
int main() {
Stack st1;
st1.Push(1);
st1.Push(2);
// 未实现拷贝构造,默认浅拷贝
// st1 和 st2 的 _a 指向同一块内存,析构时 crash
Stack st2(st1);
return 0;
}
解决:深拷贝
// 在 Stack 类中添加拷贝构造
Stack(const Stack& st) {
cout << "Stack(const Stack& st)" << endl;
_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;
}
引用返回与效率优化
传值返回会产生临时对象调用拷贝构造,效率低。可以使用引用返回减少拷贝,但要注意对象生命周期。
// 减少拷贝消耗
Stack& func2() {
static Stack st; // 静态存储期,避免返回局部对象野引用
return st;
}
int main() {
Stack ret = func2();
return 0;
}
赋值运算符重载
赋值运算符用于两个已存在对象之间的赋值,区别于拷贝构造(初始化新对象)。
特点:
- 必须重载为成员函数。
- 参数建议为
const 当前类类型引用。 - 返回值建议为
当前类类型引用,以支持连续赋值(如a = b = c)。 - 默认行为同默认拷贝构造(浅拷贝)。
规则总结: 如果显示了析构释放资源,就需要实现拷贝构造和赋值重载。
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year; _month = month; _day = day;
}
// 赋值运算符重载
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this; // 支持连续赋值
}
void Print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2025, 11, 26);
Date d2(2025, 11, 27);
d1 = d2; // 调用赋值运算符
Date d3 = d2; // 调用拷贝构造
return 0;
}
运算符重载
C++ 允许通过运算符重载为类类型指定新的含义。重载后的优先级和结合性与内置类型一致。
比较运算符
比较运算符通常重载为成员函数,this 指针指向左侧对象。
bool operator==(const Date& d) {
return _year == d._year && _month == d._month && _day == d._day;
}
IO 流重载
<< 和 >> 重载为全局函数,因为左侧运算对象通常是 ostream/istream,无法作为成员函数的 this。
friend ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "/" << d._month << "/" << d._day;
return out;
}
friend istream& operator>>(istream& in, Date& d) {
in >> d._year >> d._month >> d._day;
return in;
}
前置与后置 ++/--
后置重载需要一个 int 形参来区分。
Date operator++(int) { // 后置
Date tmp = *this;
*this += 1;
return tmp;
}
Date& operator++() { // 前置
*this += 1;
return *this;
}
日期类完整实现
下面是一个综合了上述知识点的 Date 类实现,包含日期校验、加减运算及比较。
头文件 (Date.h)
#pragma once
#include <assert.h>
#include <iostream>
using namespace std;
class Date {
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1990, int month = 1, int day = 1) {
_year = year; _month = month; _day = day;
if (!CheckDate()) {
cout << "非法日期";
Print();
}
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
inline int GetMonthDay(int year, int month) {
assert(month > 0 && month < 13);
static int MontDayArray[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 30, 31, 30, 31, 30};
if ((month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))) {
return 29;
}
return MontDayArray[month];
}
bool CheckDate();
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
Date operator++(int);
Date& operator++();
Date operator--(int);
Date& operator--();
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
实现文件 (Date.cpp)
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
bool Date::CheckDate() {
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month)) {
return false;
}
return true;
}
Date& Date::operator+=(int day) {
if (day < 0) return this->operator-=(-day);
_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) {
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date::operator-=(int day) {
if (day < 0) return this->operator+=(-day);
_day -= day;
while (_day <= 0) {
_month--;
if (_month == 0) {
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) {
Date tmp = *this;
tmp -= day;
return tmp;
}
bool Date::operator<(const Date& d) {
if (_year < d._year) return true;
else if (_year == d._year) {
if (_month < d._month) return true;
else if (_month == d._month) return _day < d._day;
}
return false;
}
bool Date::operator<=(const Date& d) { return *this < d || *this == d; }
bool Date::operator>(const Date& d) { return !(*this <= d); }
bool Date::operator>=(const Date& d) { return !(*this < d); }
bool Date::operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; }
bool Date::operator!=(const Date& d) { return !(*this == d); }
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;
}
Date& Date::operator--() {
*this -= 1;
return *this;
}
int Date::operator-(const Date& d) {
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d) {
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max) {
++min;
++n;
}
return n * flag;
}
ostream& operator<<(ostream& out, const Date& d) {
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d) {
cout << "请输入年月日:";
in >> d._year >> d._month >> d._day;
return in;
}
测试用例
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
void Test01() {
Date d1(2025, 11, 28);
d1.Print();
Date d2 = d1 - 100;
d2.Print();
}
void Test02() {
Date d1(2024, 7, 13);
Date ret1 = d1++;
ret1.Print();
d1.Print();
Date d2(2024, 7, 13);
Date ret2 = ++d2;
ret2.Print();
d2.Print();
}
int main() {
Test01();
Test02();
return 0;
}
取地址运算符重载与 const 成员函数
const 成员函数
在成员函数参数列表后加 const,修饰隐含的 this 指针,表明函数内不修改成员变量。这允许在 const 对象上调用该函数。
class Date {
public:
void Print() const {
cout << _year << "/" << _month << "/" << _day << endl;
}
};
void Test() {
const Date d1(2025, 11, 30);
d1.Print(); // 合法
}
取地址运算符重载
一般不需要显式实现,编译器生成的足够用。但在特殊场景(如单例模式或禁止取址)下可自定义。
class Date {
public:
Date* operator&() {
return this;
}
const Date* operator&() const {
return this;
}
};


