
往期《C++初阶》回顾:/------------ 入门基础 ------------/ 【C++的前世今生】 【命名空间 + 输入&输出 + 缺省参数 + 函数重载】 【普通引用 + 常量引用 + 内联函数 + nullptr】 /------------ 类和对象 ------------/ 【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
ヾ(◍°∇°◍)ノ゙亲爱的编程小伙伴们!崭新的一周伴随着阳光🌞 又开启啦~
🌈 本篇博客的内容1.7w字左右,,建议收藏后慢慢消化~,博主详细的介绍了
类的默认成员函数,别看这些函数带着"默认"二字,它们可是理解类与对象深层机制的钥匙。 从对象的诞生、拷贝、赋值到销毁,每一步都有默认成员函数的身影,是扎实掌握类和对象知识体系绕不开的核心基石,接下来就让我们深入挖掘,一起解锁它们的奥秘吧!💪✨
类的默认成员函数:是编译器自动生成的特殊成员函数,用于支持对象的基本操作。 即使程序员没有显式定义这些函数,编译器也会隐式地为类生成它们,这些函数对于对象的生命周期管理至关重要,主要包括以下6种:
默认构造函数(Default Constructor)
析构函数(Destructor)
拷贝构造函数(Copy Constructor)
拷贝赋值运算符(Copy Assignment Operator)
移动构造函数(Move Constructor)(C++11+)
移动赋值运算符(Move Assignment Operator)(C++11+)
正如你所见后面两个默认成员函数是C++11新引入的,现阶段的话我们还接触不到,所以接下来我们着重学习的是前四个默认成员函数。
温馨提示:接下来我们要学习的内容会有一定的难度,并且这块的内容在C++中也是相当的重要的。(不要觉得这些函数都是默认成员函数,也就是说都是编译器生成好的,我们没必要学习) 所以这里先明确一下我们学习这些默认成员函数要达到的标准是什么?
构造函数(Constructor):是 C++ 中一类特殊的成员函数,用于初始化对象。
构造函数的特点:
函数名:与类名完全相同 。
class Person中,其构造函数名称也为Person无返回值类型:既不指定返回类型,也不能用void修饰 。
int、void等返回类型声明自动调用:在创建类的对象时,由编译器自动调用,无需程序员显式调用 。
支持重载:可以定义多个参数列表不同的构造函数,实现不同的初始化逻辑。
class Point
{
public:
Point() { x = 0; y = 0; } // 无参构造函数
Point(int a) { x = a; y = 0; } // 单参数构造函数
Point(int a, int b) { x = a; y = b; } // 双参数构造函数
private:
int x, y;
};默认生成:
初始化列表:可使用初始化列表高效初始化成员变量,尤其适用于const成员或引用成员:
class Example
{
public:
Example(int value) : data(value) {} // 初始化列表
private:
const int data;
};看到这个问题你可能会说:这个我知道,构造函数的作用就是初始对象。 好,不错证明前面的内容确实已经掌握住了,但是呢,这里我要进一步的问你:对象怎么进行初始化的呢? 聪明的你可能会说:初始化对象本质上就是初始化对象的
数据成员真是不戳啊,所以我们明白了:构造函数的本质就是:为对象的数据成员进行赋值
当然问题还没完,我们继续向下思考:对象的数据成员有哪些?
回答:有两种:一种是 内置类型 的数据成员,一种是 自定义类型 的数据成员
现在你一定好奇:为什么说构造函数的怎么扯到了数据成员的类型上去了?
这是因为:
在 C++ 中,如果我们没有显式定义构造函数,编译器会默认生成一个构造函数。
在C++中,构造函数根据其功能和特性可以分为以下几类:
下面的三个构造函数现阶段了解即可,这里我们只着重学习前三个。
默认构造函数(Default Constructor):是指不需要任何参数的构造函数。
特点:无参数或所有参数都有默认值
生成规则:
未定义任何构造函数,编译器自动生成隐式默认构造函数(执行默认初始化)定义了任何构造函数,编译器不再生成默认构造函数,需手动定义 如果用户没有为类定义任何构造函数,编译器会自动生成一个隐式的默认构造函数
它的行为是:
基本类型(如:int、float、指针等)成员变量:不初始化(值是未定义的)类类型 成员变量:调用其自身的默认构造函数默认构造函数的种类:
A(int a = 0, int b = 0))解释:
总结:简单一点说就是不传实参就可以调用的构造就叫默认构造函数 注意:但是这些函数有且只有一个存在,不能同时存在
#include <iostream>
using namespace std;
/*-------------示例1:隐式默认构造函数(用户未定义任何构造函数)-------------*/
/**
* @class A
* @brief 演示编译器自动生成的隐式默认构造函数
*
* 该类没有定义任何构造函数,编译器会自动生成一个隐式默认构造函数
* 注意:自动生成的构造函数不会初始化基本类型成员变量(如:int)
*/
class A
{
private:
int _x; // 基本类型成员变量,隐式默认构造函数不会初始化它(值是未定义的)
};
/*-------------示例2:显式无参构造函数-------------*/
/**
* @class B
* @brief 演示用户显式定义的无参默认构造函数
*
* 该类显式定义了一个无参构造函数,会初始化成员变量_x为0
* 注意:当用户定义了任何构造函数后,编译器不再自动生成默认构造函数
*/
class B
{
private:
int _x;
public:
B()// 显式定义的无参默认构造函数
{
_x = 0; // 显式初始化成员变量
cout << "B: 显式无参构造函数,_x=" << _x << endl;
}
};
/*-------------示例3:全参数带默认值的构造函数-------------*/
/**
* @class C
* @brief 演示带默认参数值的构造函数(可作为默认构造函数使用)
*
* 该类的构造函数所有参数都有默认值,因此它既可以作为普通构造函数使用,
* 也可以作为默认构造函数使用(当不传参数时)
*/
class C
{
private:
int _a;
int _b;
public:
C(int a = 0, int b = 0)// 带默认值的构造函数(所有参数都有默认值)
{
_a = a;
_b = b;
cout << "C: 带默认值的构造函数,_a=" << _a << ", _b=" << _b << endl;
}
};
int main()
{
// 测试A的隐式默认构造函数
A a; // 调用编译器自动生成的隐式默认构造函数
//注意1:不要写成 A a(); 这会被解析为函数声明
//注意2:a._x的值是未定义的(没有初始化)
// 测试B的显式无参构造函数
B b; // 调用显式定义的无参构造函数
// 测试C的带默认值构造函数(无参形式)
C c1; // 调用带默认值的构造函数,使用所有参数的默认值
// 测试C的带默认值构造函数(部分参数)
C c2(10); // 第一个参数使用传入值,第二个参数使用默认值
// 测试C的带默认值构造函数(全参数)
C c3(10, 20); // 两个参数都使用传入值
return 0;
}
在介绍默认构造的时候博主有这么说过一句话:“但是这些默认构造函数有且只有⼀个存在,不能同时存在。” 如果忘记了,将
无参构造函数、全缺省构造函数同时写在了一个类中的话,那么就有了这里的注意事项了。
#include<iostream>
using namespace std;
class Date
{
public:
/********************** 构造函数重载 **********************/
// 构造函数是特殊的成员函数,用于初始化对象
// 特点:函数名与类名相同,无返回值类型
/*------------------- 1. 无参构造函数(默认构造函数) -------------------*/
// 作用:当创建对象时不提供任何参数时调用
// 注意:如果类中没有定义任何构造函数,编译器会自动生成一个默认构造函数
// 但编译器生成的默认构造函数对内置类型不做初始化(保持随机值)
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
/*------------------- 2. 全缺省构造函数 -------------------*/
// 作用:可以通过不同参数个数创建对象
// 注意:如果同时定义无参构造和全缺省构造,会导致构造函数调用歧义
// 因此实际开发中二者只能保留一个
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()
{
/*------------------- 对象创建方式测试 -------------------*/
// 方式1:调用无参构造函数
Date d1; // 正确:创建对象d1并调用无参构造
// 注意:不要写成 Date d1(); 这会被解析为函数声明
// 方式2:C++11统一初始化语法(推荐)
Date d2{}; // 明确表示调用无参构造
// 方式3:调用全缺省构造函数(如果启用)
// Date d3(2023); // 年=2023,月日使用默认值
// Date d4(2023, 5); // 年=2023,月=5,日使用默认值
// 打印测试
d1.Print();
d2.Print();
// d3.Print();
// d4.Print();
return 0;
}
#include<iostream>
using namespace std;
class Date
{
public:
/********************** 构造函数重载 **********************/
// 构造函数是特殊的成员函数,用于初始化对象
// 特点:函数名与类名相同,无返回值类型
/*------------------- 1. 带参构造函数 -------------------*/
// 作用:创建对象时必须提供year, month, day三个参数
// 注意:这里没有参数默认值,必须显式传入所有参数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
/*------------------- 2. 全缺省构造函数 -------------------*/
// 作用:可以通过不同参数个数创建对象
// 特点:所有参数都有默认值,可以无参调用
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(2025, 1, 1);
// 打印测试
d1.Print();
return 0;
}
为什么会发生重定义的问题呢? 本质原因:
带有默认的值的有参构造函数本质上就是一个全缺省函数
带参构造函数(Parameterized Constructor):是指需要接收一个或多个参数的构造函数。
特点:接受一个或多个参数,用于显式初始化对象。
#include <iostream>
using namespace std;
/**
* @class Point
* @brief 表示二维坐标系中的一个点
*
* 该类封装了一个点的x和y坐标,提供了带参数的构造函数用于初始化点坐标。
*/
class Point
{
private:
int _x;
int _y;
public:
/*---------------------带参构造函数---------------------*/
/**
* @brief 带参数的构造函数
* @param x 初始化x坐标的值
* @param y 初始化y坐标的值
*
* 1. 这是该类唯一的构造函数,需要两个int类型参数
* 2. 构造函数会在对象创建时自动调用
* 3. 使用参数初始化成员变量_x和_y
*/
Point(int x, int y)
{
_x = x;
_y = y;
cout << "带参构造函数被调用,创建点(" << _x << ", " << _y << ")" << endl;
}
};
int main()
{
/*----------方式1:直接初始化(最常用、最高效的方式)----------*/
//语法:类名 对象名(参数1, 参数2);
Point p1(10, 20); //调用带参构造函数,创建坐标为(10,20)的点
/*----------方式2:显式调用构造函数(功能等价于方式1,但会多生成一个临时对象)----------*/
//语法:类名 对象名 = 类名(参数1, 参数2);
Point p2 = Point(30, 40); //先创建临时Point对象,再通过拷贝构造初始化p2
return 0;
}
小示例:无参数和有参数的构造函数的调用示例
#include<iostream>
using namespace std;
class Date
{
public:
/*-------------------默认无参构造函数-------------------*/
// 1.无参构造函数(默认构造函数)
// 当创建对象时不提供任何参数时调用
Date()
{
_year = 1; // 初始化年为1
_month = 1; // 初始化月为1
_day = 1; // 初始化日为1
// 注意:这里使用的是赋值,不是初始化列表方式
}
/*-------------------带参构造函数-------------------*/
// 2.带参构造函数
// 创建对象时需要提供year, month, day三个参数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
/*-------------------默认全缺省构造函数-------------------*/
// 3.全缺省构造函数(当前被注释掉)
// 如果取消注释,它会与无参构造函数冲突
// 因为Date()调用可以匹配全缺省构造Date(int=1,int=1,int=1)
/*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()
{
/*-------------------情况1:如果只保留带参构造,注释掉其他构造-------------------*/
// Date d1; // 这样会编译错误,因为没有默认构造函数可用
// 错误信息:error C2512: "Date": 没有合适的默认构造函数可用
/*-------------------情况2:当前代码保留无参构造和带参构造-------------------*/
Date d1; // 调用无参构造函数
Date d2(2025, 1, 1); // 调用带参构造函数
/*-----------特别注意:无参构造函数的调用方式-----------*/
// 错误写法(会被解析为函数声明):
Date d3(); //这实际上是声明了一个返回Date对象的函数d3,不是创建对象!
// 编译器警告:warning C4930: "Date d3(void)": 未调用原型函数
// 正确调用无参构造的方式:
Date d4; // 正确:创建对象d4并调用无参构造
Date d5{}; // C++11统一初始化语法,更推荐
// 打印测试
d1.Print(); // 输出:1/1/1
d2.Print(); // 输出:2025/1/1
return 0;
}
拷贝构造函数(Copy Constructor):使用一个已存在的对象初始化同类型的新对象的构造函数。 拷贝构造函数的声明形式为:ClassName(const ClassName &other);
拷贝构造函数在以下场景被隐式调用:
值传递 方式传参值返回 方式从函数返回显式初始化 另一个对象调用场景 | 示例代码 | 说明 |
|---|---|---|
用一个对象初始化另一个对象 | ClassName obj1; ClassName obj2(obj1); // 显式调用 ClassName obj3 = obj1; // 隐式调用 | 直接使用已存在的对象obj1创建新对象obj2和obj3 |
对象通过值传递方式传参 | void func(ClassName obj) { /* ... */ } func(obj1); // 传递时会复制一份新对象 | 函数参数为对象时,实参传递给形参会调用拷贝构造函数创建新对象 |
对象通过值返回方式从函数返回 | ClassName createObject() { ClassName temp; return temp; // 返回时可能调用拷贝构造函数(某些情况下会被优化) } | 函数返回局部对象时,可能调用拷贝构造函数创建返回值(现代编译器常通过 RVO 优化避免) |
拷贝构造函数的主要特点:
const 类名&,用于通过现有对象创建新对象。
const引用,也可以是非const引用,一般常用const引用 ,因为:这样既能以常量对象作为参数,也能以非常量对象作为参数去初始化其他对象
Date类,其拷贝构造函数形式为Date(const Date& d)
void都没有)
浅拷贝版本
拷贝构造函数的生成规则:
class Vector
{
public:
// 显式定义的拷贝构造函数(深拷贝)
Vector(const Vector& other)
{
size = other.size;
data = new int[size];
for (int i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
}
private:
int size;
int* data;
};
浅拷贝和深拷贝是在对象复制过程中处理资源分配的两种不同方式。 主要区别在于:是否复制动态分配的资源 (如:堆内存、文件句柄等)
浅拷贝(Shallow Copy):只复制对象的成员变量值,不复制动态分配的资源。
浅拷贝的不足:
指针悬空(多个对象析构时重复释放同一块内存)数据不一(修改一个对象影响其他对象)原因: 如果对象包含指针成员,浅拷贝后,新旧对象的指针成员指向同一块内存
浅拷贝触发条件:
代码示例:浅拷贝
#include <iostream>
using namespace std;
/**
* @class ShallowCopy
* @brief 演示浅拷贝行为的类
*
* 该类包含一个指针成员,动态分配内存,
* 但使用默认的拷贝构造函数(浅拷贝),
* 会导致多个对象共享同一块内存。
*/
class ShallowCopy
{
private:
int* _data; // 指针成员,指向动态分配的堆内存
public:
/*---------------------自定义带参构造函数---------------------*/
/**
* @brief 构造函数
* @param value 要存储的整数值
*
* 在堆上分配内存并存储给定的值
*/
ShallowCopy(int value)
{
_data = new int(value); // 动态分配内存并初始化
cout << "构造函数调用,分配内存地址: " << _data << endl;
}
/**
* @brief 默认的拷贝构造函数(浅拷贝)
*
* 注意:这里没有显式定义拷贝构造函数,
* 编译器会生成一个默认的拷贝构造函数,
* 它只会简单地复制指针的值(浅拷贝),
* 而不会复制指针指向的内容。
*/
// 编译器生成的拷贝构造函数相当于:
// ShallowCopy(const ShallowCopy& other)
// {
// _data=other._data;
// }
/**
* @brief 析构函数
*
* 释放动态分配的内存。
* 如果有多个对象共享同一块内存,
* 这会导致重复释放问题。
*/
~ShallowCopy()
{
cout << "析构函数调用,释放内存地址: " << _data << endl;
delete _data; // 释放内存
// 注意:如果多个对象共享同一内存,这里会导致重复释放
}
};
int main()
{
cout << "=== 浅拷贝演示 ===" << endl;
// 创建原始对象
ShallowCopy obj1(10); // 调用构造函数
// 输出示例:构造函数调用,分配内存地址: 0x55a5a5e5deb0
// 使用拷贝构造函数创建新对象(浅拷贝)
ShallowCopy obj2 = obj1; // 调用编译器生成的默认拷贝构造函数(浅拷贝)
// obj1.data 和 obj2.data 现在指向同一块内存
cout << "\nmain函数即将结束..." << endl;
return 0;
// 程序结束时:
// 1. 首先析构obj2,释放共享的内存
// 2. 然后析构obj1,尝试再次释放同一块内存 → 导致未定义行为(通常程序崩溃)
}
深拷贝(Deep Copy):不仅复制对象的成员变量,还为指针成员分配新内存,并拷贝指针指向的实际数据。
深拷贝的好处: 如果对象包含指针成员,深拷贝后,新旧对象的指针成员指向不同的内存块,内容相同但地址不同
代码示例:深拷贝
#include <iostream>
using namespace std;
/**
* @class DeepCopy
* @brief 演示深拷贝实现的类
*
* 该类通过自定义拷贝构造函数实现深拷贝,
* 确保每个对象拥有独立的动态分配内存。
*/
class DeepCopy
{
private:
int* _data; // 指针成员,指向动态分配的整型数据
public:
/*---------------------自定义带参构造函数---------------------*/
/**
* @brief 构造函数
* @param value 初始化值
*
* 动态分配内存并存储给定值。
* 输出内存地址用于演示。
*/
DeepCopy(int value)
{
_data = new int(value); // 在堆上分配内存并初始化
cout << "构造函数调用,分配内存地址: " << _data
<< ",存储值: " << *_data << endl;
}
/*---------------------自定义深拷贝构造函数---------------------*/
/**
* @brief 深拷贝构造函数
* @param other 被拷贝的源对象
*
* 1. 为当前对象分配新内存
* 2. 拷贝源对象指针指向的值(而非指针地址)
* 3. 确保新旧对象内存完全独立
*/
DeepCopy(const DeepCopy& other)
{
_data = new int(*other._data); // 关键点:分配新内存并复制值
cout << "深拷贝构造函数调用,新内存地址: " << _data
<< ",拷贝值: " << *_data << endl;
}
/*---------------------自定义析构函数---------------------*/
/**
* @brief 析构函数
*
* 安全释放对象拥有的内存。
* 由于实现了深拷贝,不会出现重复释放问题。
*/
~DeepCopy()
{
cout << "析构函数调用,释放内存地址: " << _data
<< ",存储值: " << *_data << endl;
delete _data; // 释放独享的内存
}
};
int main()
{
cout << "=== 深拷贝演示 ===" << endl;
// 创建原始对象
DeepCopy obj1(10);
// 通过拷贝构造创建新对象(触发深拷贝)
DeepCopy obj2 = obj1;
cout << "\nmain函数即将结束..." << endl;
return 0;
// 对象析构顺序:
// 1. obj2 先析构,释放其独立内存
// 2. obj1 后析构,释放其原始内存
}
拷贝构造函数的第一个参数必须是该类对象的引用(通常为
const引用),若使用值传递会导致编译错误。 这是因为:值传递需要调用拷贝构造函数来复制实参,但该函数本身正在被调用,从而形成无限递归。 拷贝构造函数允许有多个参数,但必须满足:
class MyClass
{
public:
// 正确:第一个参数为引用,其他参数有默认值
MyClass(const MyClass& other, int value = 0);
// 错误:值传递会导致递归错误
MyClass(MyClass other); // 编译错误
//当调用这个构造函数时,需要先复制实参other,而复制other又需要调用拷贝构造函数,从而导致无限递归。
};
#include <iostream>
using namespace std;
class MyClass
{
private:
int* m_data;
public:
/*---------------------默认构造函数---------------------*/
MyClass(int val = 0)
{
cout << "默认构造函数调用" << endl;
m_data = new int(val);
}
/*---------------------拷贝构造函数(正确版本)---------------------*/
MyClass(const MyClass& other, int value = 0)
{
cout << "拷贝构造函数(正确版本)调用" << endl;
m_data = new int(*other.m_data + value); // 深拷贝
}
/*---------------------错误的拷贝构造函数---------------------*/
//MyClass(MyClass other) // 编译错误:值传递引发无限递归
//{
//m_data = new int(*other.m_data);
//}
/*---------------------析构函数---------------------*/
~MyClass()
{
delete m_data;
cout << "析构函数调用" << endl;
}
// 获取数据值
int getData()
{
return *m_data;
}
};
int main()
{
MyClass obj1(10); // 默认构造
MyClass obj2(obj1); // 拷贝构造(使用默认值)
MyClass obj3(obj1, 5); // 拷贝构造(自定义值)
cout << "obj1: " << obj1.getData() << endl;
cout << "obj2: " << obj2.getData() << endl;
cout << "obj3: " << obj3.getData() << endl;
return 0;
}
代码示例:拷贝构造函数的使用
#include <iostream>
using namespace std;
class Date
{
public:
/*---------------------构造函数---------------------*/
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
cout << "[构造] 创建日期: " << *this << endl;
}
/*---------------------拷贝构造函数---------------------*/
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "[拷贝构造] 从 " << &d << " 复制到 " << this << endl;
}
/*---------------------指针构造函数---------------------*/
Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
cout << "[指针构造] 从指针 " << d << " 创建 " << this << endl;
}
// 设置日期
void set(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 友元函数:重载输出运算符
friend ostream& operator<<(ostream& os, const Date& d)
{
os << d._year << "-" << d._month << "-" << d._day;
return os;
}
private:
int _year;
int _month;
int _day;
};
// 值传递测试
void passByValue(Date d)
{
cout << "[值传递] 函数内对象地址: " << &d << endl;
}
// 返回对象测试
Date returnObject()
{
Date temp(2023, 12, 31);
cout << "[返回对象] 临时对象: " << temp << " 地址: " << &temp << endl;
return temp;
}
// 返回引用测试(危险!)
Date& returnRef()
{
Date temp(2024, 1, 1);
cout << "[返回引用] 临时对象: " << temp << " 地址: " << &temp << endl;
return temp; // 警告:返回局部对象引用
}
int main()
{
/*-----------------------------------------测试1: 构造与拷贝-----------------------------------------*/
cout << "===== 测试1: 构造与拷贝 =====" << endl;
cout << "~~~~~~~~~~~~~~~~~~~~~Date d1(2024, 7, 5);正在执行中~~~~~~~~~~~~~~~~~~~~~" << endl;
Date d1(2024, 7, 5);
cout << "---------------------执行结束---------------------\n" << endl;
cout << "~~~~~~~~~~~~~~~~~~~~~Date d2(d1); 正在执行中~~~~~~~~~~~~~~~~~~~~~" << endl;
Date d2(d1);
cout << "---------------------执行结束---------------------\n" << endl;
cout << "d1: " << d1 << " 地址: " << &d1 << endl;
cout << "d2: " << d2 << " 地址: " << &d2 << endl << endl;
/*-----------------------------------------测试2: 值传递-----------------------------------------*/
cout << "===== 测试2: 值传递 =====" << endl;
cout << "调用前d1地址: " << &d1 << endl;
cout << "~~~~~~~~~~~~~~~~~~~~~passByValue(d1); 正在执行中~~~~~~~~~~~~~~~~~~~~~" << endl;
passByValue(d1);
cout << "---------------------执行结束---------------------\n" << endl;
/*-----------------------------------------测试3: 指针构造-----------------------------------------*/
cout << "===== 测试3: 指针构造 =====" << endl;
cout << "~~~~~~~~~~~~~~~~~~~~~Date* ptr = new Date(2025, 1, 1); 正在执行中~~~~~~~~~~~~~~~~~~~~~" << endl;
Date* ptr = new Date(2025, 1, 1);
cout << "---------------------执行结束---------------------\n" << endl;
cout << "~~~~~~~~~~~~~~~~~~~~~Date d3(ptr)正在执行中~~~~~~~~~~~~~~~~~~~~~" << endl;
Date d3(ptr);
cout << "---------------------执行结束---------------------\n" << endl;
cout << "d3: " << d3 << " 地址: " << &d3 << endl;
delete ptr;
/*-----------------------------------------测试4: 返回对象-----------------------------------------*/
cout << "===== 测试4: 返回对象 =====" << endl;
cout << "~~~~~~~~~~~~~~~~~~~~~Date d4 = returnObject(); 正在执行中~~~~~~~~~~~~~~~~~~~~~" << endl;
Date d4 = returnObject();
cout << "---------------------执行结束---------------------\n" << endl;
cout << "d4: " << d4 << " 地址: " << &d4 << endl << endl;
/*-----------------------------------------测试5: 返回引用(危险)-----------------------------------------*/
cout << "===== 测试5: 返回引用(危险) =====" << endl;
cout << "~~~~~~~~~~~~~~~~~~~~~Date& badRef = returnRef(); 正在执行中~~~~~~~~~~~~~~~~~~~~~" << endl;
Date& badRef = returnRef(); // 取消注释会导致未定义行为
cout << "---------------------执行结束---------------------\n" << endl;
cout << "[警告] 返回局部对象引用会导致悬挂引用!" << endl;
cout << "badRef: " << badRef << " 地址: " << &badRef << endl << endl;
return 0;
}
析构函数(Destructor):是 C++ 中一种特殊的成员函数,用于在对象生命周期结束时自动释放资源
在 C++ 中,析构函数与构造函数的功能相对应。 构造函数用于初始化对象的资源,而析构函数则负责在对象生命周期结束时进行资源的清理工作。 需要强调的是,析构函数并不负责销毁对象本身
C++ 规定:当对象的生命周期结束时,系统会自动调用析构函数,其核心作用是释放对象所占用的资源,而非对象本身的内存空间。
语法特点
~(如:~ClassName())void)public,但也可以声明为 private(用于特殊设计模式)何时调用
delete 显式释放时调用。这里我使用我们在《数据结构初阶》中的写过的一道OJ题“使用双栈实现队列”来演示一下如何使用析构函数:
#include<iostream>
using namespace std;
typedef int STKDataType;
/*------------------------------------------实现“栈”的类------------------------------------------*/
class Stack
{
public:
/*---------------------构造函数,默认初始化容量为4---------------------*/
Stack(int n = 4) //参数n表示初始容量,默认为4
{
cout << "Stack()" << endl;
//动态分配内存空间
_a = (STKDataType*)malloc(sizeof(STKDataType) * n);
if (_a == nullptr)
{
perror("malloc申请空间失败");
return;
}
//初始化成员变量
_capacity = n; // 设置栈容量
_top = 0; // 栈顶指针初始化为0(表示空栈)
}
/*---------------------析构函数---------------------*/
~Stack()
{
cout << "~Stack()" << endl;
//释放动态分配的内存
free(_a);
//将指针置空,防止野指针
_a = nullptr;
//重置容量和栈顶指针
_top = _capacity = 0;
}
private:
STKDataType* _a; // 指向动态数组的指针,存储栈元素
size_t _capacity; // 栈的容量
size_t _top; // 栈顶指针(表示下一个元素要插入的位置)
};
/*------------------------------------------实现“队列”的类------------------------------------------*/
class MyQueue
{
public:
// 如果用户不显式定义析构函数:
// 编译器会生成默认的析构函数,它会自动调用成员变量(pushst和popst)的析构函数
// 因此Stack内部的资源(_a指向的内存)会被正确释放
// 如果用户显式定义析构函数(即使函数体为空):
// 也会自动调用成员变量的析构函数
~MyQueue()
{
}
private:
Stack pushst; // 用于入队操作的栈
Stack popst; // 用于出队操作的栈
};
int main()
{
Stack st; // 创建一个栈对象(调用构造函数)
MyQueue mq; // 创建一个队列对象(包含两个栈成员)
return 0;
// main函数结束时:
// 1. mq的析构被调用,自动调用其成员pushst和popst的析构
// 2. st的析构被调用
// 所有动态分配的内存都会被正确释放
}
注意事项:
禁止抛异常:析构函数中应避免抛出异常,否则可能导致程序崩溃。不可重载:析构函数不能有参数,因此一个类只能有一个析构函数。默认析构函数:若未显式定义,编译器会生成一个空的默认析构函数,但不会释放动态资源。 new分配内存、打开文件、建立网络连接等),那么该类的析构函数可以不必显式编写。后定义的对象先析构:对于同一局部作用域内定义的多个对象,遵循 “后定义的对象先析构” 的规则。 const成员函数:是一种特殊的成员函数,它承诺在调用期间不会修改对象的状态。
const关键字来标识。返回类型 函数名(参数列表) const
{
// 函数体(不能修改成员变量)
}在 C++ 中,const 成员函数的本质是:修饰隐含的 this 指针,从而禁止在函数内部修改对象的任何非 mutable 成员变量。
具体来说:
普通成员函数的 this 指针:
对于非 const 成员函数(如:Date::Print()),this 指针的类型为 Date* const
即:指针本身不可变(不能指向其他对象),但可以通过它修改对象内容。
const 成员函数的 this 指针:
当成员函数被声明为 const(如:Date::Print() const)时,this 指针的类型变为 const Date* const,
即:指针本身和指向的对象均不可变,从而禁止修改对象的任何成员。
示例对比:
class Date
{
public:
//普通成员函数:this 类型为 Date* const
void Print()
{
// 可以修改成员变量(如:this->_year = 2024;)
}
------------------------------------------------------------------------
//const成员函数:this 类型为 const Date* const
void Print() const
{
// 禁止修改成员变量(编译错误)
}
};函数类型 | this 指针类型 | 是否允许修改对象成员? |
|---|---|---|
非 const 函数 | Date* const | ✔️ |
const 函数 | const Date* const | ❌(除非成员为 mutable) |
#include<iostream>
using namespace std;
// 日期类定义
class Date
{
public:
/*--------------构造函数(带默认参数)--------------*/
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
/*--------------const成员函数:打印日期--------------*/
// 函数末尾的const表示这是一个const成员函数
// 编译器实际处理为:void Print(const Date* const this)
// 第一个const表示this指向的对象不可修改 -------> 不能修改所指向对象的内容
// 第二个const表示this指针本身不可修改 ---------> 不能重新指向别的对象
void Print() const
{
// const成员函数内部不能修改成员变量,可以安全地用于const对象
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
/*--------------测试非const对象调用const成员函数--------------*/
// 创建一个非const的Date对象
Date d1(2025, 7, 5);
d1.Print(); //非const对象可以调用const成员函数(权限缩小,安全)
/*--------------测试const对象调用const成员函数--------------*/
// 创建一个const的Date对象
const Date d2(2025, 8, 5);
d2.Print(); //const对象只能调用const成员函数
/*--------------重要说明--------------*/
// 1. const成员函数可以被非const对象调用(权限缩小)
// 2. const对象只能调用const成员函数
// 3. 非const成员函数不能被const对象调用(权限放大,禁止)
return 0;
}
对象类型 | 非 const 成员函数 | const 成员函数 |
|---|---|---|
非 const 对象 | ✔️ 可调用 | ✔️ 可调用 |
const 对象 | ❌ 不可调用 | ✔️ 可调用 |
保证数据的安全性:const成员函数承诺不修改对象的状态,这可以防止在无意之中对对象的数据成员进行修改,尤其是在处理复杂的对象层次结构或大型代码库时,能有效避免因误操作导致的数据不一致或错误。
int getValue() const { return _value; } // 安全读取支持对常量对象的操作:
如果没有const成员函数,常量对象将无法调用任何成员函数,这会极大地限制常量对象的使用场景。
有了const成员函数,常量对象就可以安全地调用这些函数来获取相关信息,而不会有数据被修改的风险。
const MyClass obj;
obj.print(); // 若 print() 非 const 则编译报错函数重载与多态性:const成员函数可以与非const成员函数形成重载。
const来选择合适的函数版本进行调用,从而实现不同的行为。
运算符重载:是对已有的运算符重新进行定义,使其能适用于自定义的数据类型(如:自定义的类或结构体 ),赋予运算符新的功能。
operator关键字 与**需要重载的运算符**组合而成。与普通函数类似,运算符重载函数也包含返回类型、参数列表以及函数体。可重载的运算符:C++ 允许重载大部分运算符,但不能重载以下五个运算符:
::(作用域解析).*(成员指针访问).(成员访问)?:(三目运算符)sizeof#include<iostream>
using namespace std;
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
// 定义成员函数指针类型PF
// 该指针可以指向A类的无参void类型成员函数
typedef void(A::* PF)();
int main()
{
// 获取成员函数func的指针
PF pf = &A::func; //注意:C++规定取成员函数地址时必须显式使用&
A obj; // 创建A类的实例对象obj
// 通过成员函数指针调用函数
// 使用.*运算符(对象.*成员指针)
(obj.*pf)(); // 等价于obj.func()
return 0;
}
运算重载注意事项:
1. 不能改变运算符本质特性:重载后的运算符不能改变其优先级、结合性,也不能改变操作数的个数及语法结构 。
2. 至少有一个自定义类型操作数:重载后的运算符至少有一个操作数是用户自定义的类型 ,避免无意义地改变已有基本类型运算符的含义 。
#include <iostream>
using namespace std;
/*--------------在全局作用域里定义:+运算符的重载函数--------------*/
//错误示例:尝试重载仅作用于内置类型的运算符
int operator+(int a, int b) // 编译错误!必须至少有一个自定义类型参数
{
return a - b; // 试图将加法改为减法,但C++禁止这样做
}
int main()
{
int x = 3 + 4; //仍然使用内置的+运算符,不会调用上面的重载函数
cout << x << endl; //输出7,而非预期的-1
return 0;
}
重载方式:
this 指针)参数数量:
++、!)接受 0 个参数(成员函数)或 1 个参数(友元函数)+、==)接受 1 个参数(成员函数)或 2 个参数(友元函数)成员函数重载:
this指针指向的对象)自动充当一个操作数。+实现两个自定义对象相加 类名 operator+(const 类名& other)other为第二个操作数,第一个操作数由调用该函数的对象提供友元函数重载:
friend关键字将运算符函数声明为类的友元函数。this指针。+实现两个自定义对象相加 friend 类名 operator+(const 类名& a, const 类名& b)a和b分别为两个操作数代码示例:使用全局函数重载==运算符(其实真实情况是我们还应该将其声明为友元函数)
#include<iostream>
using namespace std;
class Date
{
public:
/*--------------构造函数(带默认参数)--------------*/
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
/*--------------打印日期信息--------------*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 注意:这里将成员变量改为public是为了演示全局运算符重载
// 实际开发中不推荐直接公开成员变量 (现实中我们应该将operator==重载函数在Date类内声明为友元函数)
// private: // 注释掉private以便全局运算符能访问成员
int _year;
int _month;
int _day;
};
// 全局重载==运算符(可以在Date类内声明为友元函数)
// 参数:两个Date对象的const引用(避免拷贝,且不修改原对象)
// 问题:如果成员变量是private的,这里无法访问
// 解决方法:
// 1. 将成员改为public(不推荐破坏封装)
// 2. 提供getter函数(如GetYear()等)
// 3. 声明为友元函数(推荐)
// 4. 改为成员函数重载(推荐)
bool operator==(const Date& d1, const Date& d2)
{
// 比较两个Date对象的年月日是否全部相等
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
//创建两个Date对象
Date d1(2025, 7, 5);
Date d2(2025, 7, 6);
//两种调用方式:
// 1. 显式调用运算符重载函数
//bool isEqual1 = operator==(d1, d2); // 直接调用函数形式
// 2. 隐式调用(推荐)
bool isEqual2 = (d1 == d2); // 编译器会转换为operator==(d1, d2)
// 输出比较结果
cout << "d1: ";
d1.Print();
cout << "d2: ";
d2.Print();
cout << "d1 == d2 ? " << boolalpha << isEqual2 << endl;
//boolalpha:设置输出流的格式标志,使后续的布尔值以 true/false 形式输出(默认输出 1/0)
return 0;
}
在 C++ 中重载
++运算符时,存在前置++(如:++a,先自增再返回值)和后置++(如:a++,先返回值再自增)两种不同的运算逻辑。 由于运算符重载函数名均为operator++,仅通过函数名无法直接区分这两种操作。
为解决该问题,C++ 作出明确规定:在重载后置 ++ 运算符时,需额外添加一个 int 类型的形参。
++ 的重载函数与前置 ++ 的重载函数形成函数重载关系。
++ 和后置 ++ 的调用,从而执行对应的运算逻辑。
代码案例:使用成员函数重载前置++和后置++运算符
#include <iostream>
using namespace std;
class Counter
{
private:
int _value;
public:
/*----------------------构造函数----------------------*/
Counter(int value = 0)
{
_value = value;
}
/*----------------------前置++运算符重载(返回引用)----------------------*/
Counter& operator++()
{
++_value; // 先增加
return *this; // 返回自引用
}
/*----------------------后置++运算符重载(返回临时对象)----------------------*/
Counter operator++(int)
{
Counter temp = *this; // 保存当前状态
++(*this); // 调用前置++执行实际增加
return temp; // 返回旧值
}
/*----------------------获取当前值----------------------*/
int getValue() const
{
return _value;
}
};
int main()
{
Counter c(5);
/*----------------------测试前置++----------------------*/
cout << "---------------测试前置++---------------" << endl;
cout << "原始值: " << c.getValue() << endl;
Counter d = ++c;
cout << "前置++后原始值为: " << c.getValue() << endl; // 输出6
cout << "前置++返回值的值: " << d.getValue() << endl; // 输出6(与c相同)
/*----------------------测试后置++----------------------*/
cout << "---------------测试后置++---------------" << endl;
cout << "现在的原始值为:" << c.getValue() << endl;
Counter e = c++;
cout << "后置++后原始值为: " << c.getValue() << endl; // 输出7
cout << "后置++返回值的值: " << e.getValue() << endl; // 输出6(保存旧值)
/*----------------------测试链式调用----------------------*/
cout << "---------------测试链式调用---------------" << endl;
cout << "现在的原始值为:" << c.getValue() << endl;
++(++c);
cout << "连续前置++: " << c.getValue() << endl; // 输出9
// 后置++不能链式调用(因为返回的是临时对象)
// c++++; // 错误:无法修改临时对象
return 0;
}
在 C++ 中重载
<<(输出)和>>(输入)运算符时,通常需要将其定义为全局函数而非类的成员函数。 这是因为成员函数的调用会隐式绑定this指针作为第一个参数,导致运算符左侧的操作数被强制限定为该类的对象,形成类似对象 << cout的调用形式。 这与常规的cout << 对象使用习惯相悖,严重影响代码可读性。一般按照故事的发展,此时应该会有一名有当主角资质的小伙伴,不走寻常路的选择使用:类的成员函数重载<<和>>运算符 代码示例:类的成员函数重载<<和>>运算符
#include <iostream>
using namespace std;
class Point
{
private:
int _x;
int _y;
public:
/*----------------------构造函数----------------------*/
Point(int x = 0, int y = 0)
{
_x = x;
_y = y;
}
// 获取坐标
int getX() const { return _x; }
int getY() const { return _y; }
// 设置坐标
void setX(int newX) { _x = newX; }
void setY(int newY) { _y = newY; }
/*---------------------使用类的成员函数重载<<和>>运算符---------------------*/
// 重载<<运算符(作为成员函数,左侧操作数必须是Point对象)
ostream& operator<<(ostream& os)
{
os << "(" << _x << ", " << _y << ")";
return os;
}
// 重载>>运算符(作为成员函数,左侧操作数必须是Point对象)
istream& operator>>(istream& is)
{
cout << "请输入x坐标: ";
is >> _x;
cout << "请输入y坐标: ";
is >> _y;
return is;
}
};
int main()
{
Point p1(3, 4);
Point p2;
/*---------------------使用重载的<<输出对象(注意:语法不直观!)---------------------*/
p1 << cout << endl; // 成员函数版本:对象在左侧,流在右侧
//错误的演示:如果我们要使用之前的:<<输出的方式
//cout << p1 << endl;
/*---------------------使用重载的>>输入对象(注意:语法不直观!)---------------------*/
p2 >> cin; // 成员函数版本:对象在左侧,流在右侧
p2 << cout << endl; // 输出修改后的p2
//错误的演示:如果我们要是还是使用之前的:>>的输入方式
//cin >> p2;
//cout << p2 << end;
return 0;
}

注意:通过上面的演示,我们能够清晰的观察道:这种实现方式与标准做法不同,会导致使用方式非常的不直观。(非常不建议这么做!!!)
将 <<和 >>重载为全局函数时:
可以显式将ostream&或istream&作为第一个参数,对应运算符左侧的cout或cin
将类对象作为第二个参数,对应右侧要输出或输入的内容,这种设计符合标准库的使用惯例,使代码更自然易读。
// 重载输出运算符
ostream& operator<<(ostream& os, const MyClass& obj)
{
os << obj.value; // 输出对象的具体内容
return os;
}
// 使用示例:
MyClass obj;
cout << obj; // 符合直觉的调用方式代码示例:使用全局函数重载<<和>>运算符,并将其声明Point类的友元函数
#include <iostream>
using namespace std;
/*
* Point类:表示二维坐标系中的一个点
* 包含x和y坐标,提供基本的坐标访问和修改方法
* 重载了输入输出运算符,支持直接使用cin/cout进行输入输出
*/
class Point
{
private:
int _x;
int _y;
public:
/*----------------------构造函数----------------------*/
Point(int x = 0, int y = 0)
{
_x = x;
_y = y;
}
//获取坐标
int getX() const { return _x; }
int getY() const { return _y; }
//设置坐标
void setX(int newX) { _x = newX; }
void setY(int newY) { _y = newY; }
/*---------------------使用全局函数重载<<和>>运算符,并将其声明Point类的友元函数---------------------*/
// 重载<<运算符(输出)
friend ostream& operator<<(ostream& os, const Point& p);
// 重载>>运算符(输入)
friend istream& operator>>(istream& is, Point& p);
};
/*----------------------运算符重载实现----------------------*/
/*
* 重载<<运算符:用于输出Point对象
* @param os: 输出流对象
* @param p: 要输出的Point对象
* @return: 输出流对象,支持链式调用
*/
ostream& operator<<(ostream& os, const Point& p)
{
os << "(" << p._x << ", " << p._y << ")";
return os;
}
/*
* 重载>>运算符:用于输入Point对象
* @param is: 输入流对象
* @param p: 要输入的Point对象
* @return: 输入流对象,支持链式调用
*/
istream& operator>>(istream& is, Point& p)
{
cout << "请输入x坐标: ";
is >> p._x; // 输入x坐标
cout << "请输入y坐标: ";
is >> p._y; // 输入y坐标
return is;
}
/*----------------------主函数----------------------*/
int main()
{
// 创建并初始化Point对象p1
Point p1(3, 4);
// 创建默认Point对象p2(0,0)
Point p2;
// 使用重载的<<运算符输出p1
cout << "p1 = " << p1 << endl;
// 使用重载的>>运算符输入p2
cout << "请输入p2的坐标:" << endl;
cin >> p2;
// 使用重载的<<运算符输出p2
cout << "p2 = " << p2 << endl;
return 0;
}
/*
* 代码执行流程说明:
* 1. 创建两个Point对象p1和p2
* 2. 使用重载的<<运算符输出p1的初始值
* 3. 使用重载的>>运算符从用户输入获取p2的坐标
* 4. 使用重载的<<运算符输出p2的新坐标
*
* 注意事项:
* 1. 运算符重载函数声明为友元函数是为了访问私有成员_x和_y
* 2. 运算符重载函数返回流对象引用是为了支持链式调用
* 3. 输入运算符>>会修改Point对象的状态,因此参数为非const引用
* 4. 输出运算符<<不修改Point对象,因此参数为const引用
*/
赋值运算符重载:允许你自定义类对象在使用赋值符号=时的行为,用于完成两个已存在对象间的拷贝赋值操作。
=)时,编译器会自动调用这个函数。浅拷贝问题:
默认情况下,C++ 为每个类提供一个隐式的赋值运算符,它执行浅拷贝(逐成员复制) 实现深拷贝:
当类管理动态资源时,需要自定义赋值运算符来执行深拷贝(复制资源本身,而非仅复制指针)资源管理:
在赋值时可能需要释放旧资源、分配新资源或执行其他清理操作。必须是成员函数:
赋值运算符 operator= 必须作为类的成员函数实现,不能是全局函数。
这是 C++ 语法的强制要求,确保操作符的左侧为当前类的对象实例。
参数通常是 const 引用:
赋值运算符的参数类型通常为 const MyClass&,使用常量引用避免不必要的对象拷贝,同时保护原对象不被修改。
MyClass& operator=(const MyClass& other); 通常返回当前对象的引用:
赋值运算符应返回 *this 的引用,以支持链式赋值语法(如:a = b = c)
返回类型必须为 MyClass& 以保持一致性。
MyClass& operator=(const MyClass& other)
{
// ... 赋值逻辑 ...
return *this;
} 与拷贝构造函数的区别:
拷贝构造函数:在创建新对象时调用,用于初始化(如:MyClass a = b; 或 MyClass a(b);)
赋值运算符:对已存在的对象进行值覆盖(如:a = b;)
这里需要特别注意:
拷贝构造函数:用于在创建新对象时通过已有对象初始化。
赋值运算符重载:用于在对象创建后将一个对象的值赋给另一个已存在的对象。
class MyClass { /* ... */ };
MyClass a; // 对象a已存在
MyClass b = a; // 拷贝构造(创建新对象b)
b = a; // 赋值运算符重载(b已存在)处理自我赋值:
自赋值(如:a = a)可能导致资源释放后再访问(例如:先释放自身内存,再尝试拷贝已释放的内存)
因此必须通过 this != &other 检查:
if (this != &other)
{
// ... 执行赋值逻辑 ...
} 遵循三 / 五法则:
如果类需要自定义赋值运算符,通常也需要自定义析构函数和拷贝构造函数,以确保资源的正确管理。
在 C++11 及以后,还建议实现移动构造函数和移动赋值运算符,构成 “五法则”。
代码示例:重载赋值运算符
#include <iostream>
using namespace std;
// 日期类定义
class Date
{
public:
/*-----------------------构造函数(带默认参数)-----------------------*/
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
/*-----------------------拷贝构造函数-----------------------*/
// 参数:d-要拷贝的Date对象
// 注意:拷贝构造函数是在创建新对象时用已有对象初始化时调用
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
/*-----------------------赋值运算符重载函数-----------------------*/
// 参数:d-要赋值的Date对象
// 返回:当前对象的引用(支持链式赋值)
// 注意:赋值运算符重载是在两个已存在的对象之间进行赋值时调用
Date& operator=(const Date& d)
{
// 检查自赋值情况(虽然在这个简单例子中不是必须的,但是良好的编程习惯)
// 自赋值检查可以避免不必要的操作,在涉及资源管理时尤为重要
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 返回当前对象的引用,支持链式赋值(如d1 = d2 = d3)
return *this;
}
/*-----------------------打印日期函数-----------------------*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 1. 调用普通构造函数创建d1对象
cout << "创建d1对象:" << endl;
Date d1(2025, 7, 5);
// 2. 调用拷贝构造函数创建d2对象(用d1初始化d2)
cout << "\n创建d2对象(拷贝d1):" << endl;
Date d2(d1); // 这里会打印"Date(const Date& d)"
// 3. 调用普通构造函数创建d3对象
cout << "\n创建d3对象:" << endl;
Date d3(2025, 7, 6);
// 4. 调用赋值运算符重载,将d3赋值给d1
cout << "\n将d3赋值给d1:" << endl;
d1 = d3; // 调用operator=
// 5. 注意:这里调用的是拷贝构造函数,不是赋值运算符重载!
// 虽然使用了=符号,但是这是在对象声明时的初始化,属于拷贝构造
// 等价于 Date d4(d1);
cout << "\n创建d4对象(用d1初始化):" << endl;
Date d4 = d1; // 调用拷贝构造函数
cout << "\n最终所有对象的值:" << endl;
cout << "d1: "; d1.Print();
cout << "d2: "; d2.Print();
cout << "d3: "; d3.Print();
cout << "d4: "; d4.Print();
return 0;
}
/*
* 关键区别总结:
* 1. 拷贝构造函数:
* - 用于创建一个新对象并用已有对象初始化它
* - 在以下情况调用:
* * Date d2(d1);
* * Date d4 = d1; (注意这是初始化,不是赋值)
*
* 2. 赋值运算符重载:
* - 用于两个已经存在的对象之间的赋值
* - 在以下情况调用:
* * d1 = d3;
*/

对于像
Date这样的类,其成员变量全部是内置类型,且不涉及指向动态分配资源(如:堆内存、文件句柄等)的情况。
而像 Stack 这类的类,尽管成员变量也属于内置类型,但其中存在如 _a 这样指向外部资源的指针成员。
Stack 这样的类,我们必须自行实现赋值运算符重载函数,来执行深拷贝操作,即:不仅复制指针的值,还要对指针所指向的资源进行复制,从而确保每个对象都拥有独立的资源副本,实现正确的资源管理 。
取地址运算符重载:允许我们自定义类对象取地址&操作的行为 。
operator&)用于重载对象的取地址操作,默认情况下,对一个对象使用&运算符会返回该对象在内存中的实际地址。& 运算符在作用于类对象时,执行特定的逻辑,而不是仅仅返回对象的内存地址。取地址运算符重载一般有两种形式:
普通取地址运算符重载 和const取地址运算符重载
普通取地址运算符重载:
函数原型为 类类型* operator&() ,当对类的非const对象使用&运算符时调用,返回对象的地址(通常是this指针)
class MyClass
{
public:
MyClass* operator&()
{
return this;
}
};operator&() 函数返回的是当前对象的地址,即:this 指针。
const 取地址运算符重载:
函数原型为 const 类类型* operator&() const ,当对类的 **const对象 ** 使用&运算符时调用,返回const对象的地址(const修饰的this指针)
class MyClass
{
public:
const MyClass* operator&() const
{
return this;
}
};const 修饰成员函数,表明在该函数内部不会修改对象的成员变量;const 修饰返回值,表示返回的是一个指向 const 对象的指针。同样,在常规场景下,编译器默认生成的该重载版本也能正常工作。
对于现阶段我们还使用还不上取地址运算符重载,并且它的实际使用的场景也比较少,这里我们作为简要的了解即可:
nullptr)或伪装地址,防止外部直接获取对象的真实内存位置,避免因地址暴露可能引发的安全问题。代码示例:取地址运算符重载
#include <iostream>
using namespace std;
// MyClass类定义
class MyClass
{
private:
int _data;
double _value;
public:
/*---------------构造函数---------------*/
MyClass(int data, double value)
{
_data = data;
_value = value;
}
/*---------------普通取地址运算符重载---------------*/
// 重载&运算符(用于非const对象)
MyClass* operator&()
{
cout << "普通取地址运算符被调用" << endl;
return this; // 返回当前对象的地址
}
/*---------------const取地址运算符重载---------------*/
// 重载&运算符(用于const对象)
const MyClass* operator&() const
{
cout << "const取地址运算符被调用" << endl;
return this; // 返回当前const对象的地址
}
/*---------------打印成员函数---------------*/
// const成员函数,保证不修改对象状态
void print() const
{
cout << "_data: " << _data << ", _value: " << _value << endl;
}
};
int main()
{
// 创建普通对象
MyClass obj(42, 3.14);
// 创建const对象
const MyClass constObj(99, 2.71);
/*---------------测试普通取地址运算符---------------*/
// 调用普通版本的operator&
MyClass* ptr = &obj;
cout << "普通对象地址获取结果: ";
ptr->print(); // 输出: _data: 42, _value: 3.14
/*---------------测试const取地址运算符---------------*/
// 调用const版本的operator&
const MyClass* constPtr = &constObj;
cout << "const对象地址获取结果: ";
constPtr->print(); // 输出: _data: 99, _value: 2.71
return 0;
}