当前位置: 首页 > news >正文

类和对象(中):深入理解 C++ 类与对象:6 个默认成员函数核心解析

🔥 脏脏a的技术站 🔥
 
「在代码的世界里,脏脏的技术探索从不设限~」
 


🚀 个人主页:脏脏a-CSDN博客

📌 技术聚焦:六大默认成员函数详解

📊 文章专栏:C++

🔗 上篇回顾:【上篇】类和对象

在 C++ 面向对象编程中,“类与对象” 是核心基石,而默认成员函数则是编译器为类自动生成的 “隐藏助手”—— 即使你没显式写,它们也在默默工作。本文将从实际开发角度拆解类的 6 个默认成员函数,帮你搞懂 “为什么需要它们”“怎么用” 以及 “踩坑点在哪”。

一、 什么是默认成员函数?

如果一个类中什么成员都没写(空类),编译器会自动生成 6 个默认成员函数。它们的核心作用是处理对象的 “初始化、清理、拷贝、赋值、取地址” 等基础操作。。

空类 class Date {}; 看似空,实则包含以下 6 个默认成员函数:

函数类型核心作用显式实现场景
构造函数对象创建时初始化需自定义初始化逻辑(如日期)
析构函数对象销毁时清理资源类内有动态内存(如栈、链表)
拷贝构造函数用已有对象创建新对象(Date d2(d1)涉及资源拷贝(避免浅拷贝)
赋值运算符重载对象间赋值(d2 = d1涉及资源赋值(避免浅拷贝)
普通取地址重载获取普通对象地址几乎不用(默认足够)
const 取地址重载获取 const 对象地址几乎不用(默认足够)

 

其中前 4 个是开发高频用到的,后 2 个编译器默认实现完全满足需求,几乎无需自定义。下面重点拆解前 4 个核心函数。

二、构造函数

每次创建对象时,编译器都会自动调用构造函数 —— 它的核心不是 “创建对象”(开空间由编译器负责),而是 “给对象的成员变量赋初始值”,避免成员变量出现随机值。

2.1 构造函数的 3 个关键特性

  1. 函数名 = 类名,无返回值(连void都不用写);
  2. 对象实例化时自动调用,生命周期内只调用一次
  3. 支持重载(无参、带参、全缺省都可)。

【示例】:日期类的构造函数实现

class Date {
public:// 1. 无参构造(编译器默认生成的就是类似无参版本)Date() {_year = 1900;_month = 1;_day = 1;}// 2. 带参构造(重载版本,支持自定义初始化)Date(int year, int month, int day) {_year = year;_month = month;_day = day;}// 3. 全缺省构造(更灵活,推荐!)// Date(int year = 1900, int month = 1, int day = 1) // {//     _year = year;//     _month = month;//     _day = day;// }private:int _year;int _month;int _day;
};// 使用示例
int main() 
{Date d1;                // 调用无参构造(或全缺省构造)Date d2(2024, 5, 20);   // 调用带参构造// Date d3();            // 错误!这是声明函数,不是创建对象return 0;
}

全缺省的构造函数和无参的构造函数不能同时出现,因为会造成调用歧义

2.2 编译器默认生成的构造函数

很多人会疑惑:“编译器生成的默认构造函数,好像没给内置类型(int/char 等)赋初始值,那它有啥用?”答案是:默认构造函数会调用 “自定义类型成员” 的默认构造函数

比如下面的Date类包含Time类成员,默认构造会自动初始化_t

class Time {
public:// Time的默认构造Time() {cout << "Time()被调用" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour, _minute, _second;
};class Date 
{
private:// 内置类型(默认构造不初始化,值随机)int _year;// 自定义类型(默认构造会调用Time的默认构造)Time _t;
};int main() 
{Date d;  // 输出“Time()被调用”,_t被初始化return 0;
}
补充:C++11 的补丁

为了解决内置类型不初始化的问题,C++11 允许在类内声明内置类型时给默认值:

class Date 
{
private:int _year = 1970;  // 内置类型声明时给默认值int _month = 1;int _day = 1;Time _t;
};

默认值走的是初始化列表(下一篇文章讲解)

2.3 避坑点:默认构造函数只能有一个

无参构造、全缺省构造、编译器默认生成的构造函数,都属于 “默认构造函数”—— 它们的共同点是 “调用时不用传参”。一个类中只能有一个默认构造函数,否则会冲突:

class Date 
{
public:Date() {}  // 无参构造(默认构造)Date(int year = 1900) {}  // 全缺省构造(也是默认构造)
};// 错误!调用时歧义:Date d; 不知道选哪个默认构造

三、析构函数

如果说构造函数负责 “初始化”,析构函数就负责 “销毁前清理”—— 比如释放动态内存等。它在对象生命周期结束时(如局部对象出作用域)由编译器自动调用。

3.1 析构函数的 3 个关键特性

  1. 函数名 = ~ 类名,无参数、无返回值;
  2. 一个类只能有一个析构函数(不能重载);
  3. 若未显式实现,编译器会生成默认析构函数。

3.2 什么时候必须显式写析构函数?

当类内有 “动态申请的资源” 时(如malloc/new分配的内存),必须显式写析构函数,否则会造成资源泄漏。反之,若没有资源申请(如日期类),用编译器默认的即可。

【示例】:栈类的析构函数(必须显式实现)

typedef int DataType;
class Stack {
public:// 构造函数:申请动态内存Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (_array == nullptr) {perror("malloc失败");return;}_capacity = capacity;_size = 0;}// 析构函数:释放动态内存(必须显式写!)~Stack() {if (_array != nullptr)  {free(_array);    // 释放内存_array = nullptr;// 避免野指针_capacity = 0;_size = 0;}}private:DataType* _array;  // 动态内存指针size_t _capacity;size_t _size;
};// 使用示例
void TestStack() 
{Stack s;  // 创建对象,调用构造s.Push(1);s.Push(2);
}  // s出作用域,自动调用析构,释放内存

3.3 编译器默认析构函数的作用

和默认构造类似,默认析构函数会调用 “自定义类型成员” 的析构函数,而内置类型成员无需清理(内存由编译器回收):

class Time {
public:~Time() {cout << "~Time()被调用" << endl;}
private:int _hour, _minute, _second;
};class Date {
private:int _year = 1970;  // 内置类型,默认析构不处理Time _t;           // 自定义类型,默认析构调用~Time()
};int main() {Date d;  // 程序结束时,输出“~Time()被调用”return 0;
}

四、拷贝构造函数 

用一个已存在的对象创建新对象(如Date d2(d1)),编译器会自动调用拷贝构造函数。它的核心是 “复制对象的成员变量”,但要注意 “浅拷贝” 的坑。

4.1 拷贝构造函数的 3 个关键特性

  1. 是构造函数的重载形式,函数名 = 类名;
  2. 参数必须是 “类类型的引用”(用传值会引发无穷递归,下文解释);
  3. 若未显式实现,编译器会生成默认拷贝构造(浅拷贝)。

4.2 为什么参数必须是引用?

如果参数是传值(Date(const Date d)),调用拷贝构造时会先复制实参到形参 —— 而复制形参又需要调用拷贝构造,简而言之,传参会调用拷贝构造,调用拷贝构造又要调用拷贝构造,形成 “无穷递归”:

class Date {
public:// 正确写法:参数是const引用(const避免修改原对象,引用避免递归)Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}// 错误写法:参数传值,引发无穷递归// Date(const Date d) {}
private:int _year, _month, _day;
};int main()
{Date d1;Date d2(d1);return 0;
}

4.3 浅拷贝的坑:什么时候必须显式写拷贝构造?

编译器生成的默认拷贝构造是 “浅拷贝”(按字节逐值复制)—— 对无资源的类(如日期类)没问题,但对有动态内存的类(如栈类)会出大问题:

// 栈类(未显式写拷贝构造,用默认浅拷贝)
class Stack 
{
public:// 构造、析构同上...
private:DataType* _array;size_t _capacity, _size;
};int main() 
{Stack s1;s1.Push(1);Stack s2(s1);  // 默认浅拷贝:s2._array = s1._array(指针指向同一块内存)return 0;
}  // 问题:s1和s2析构时,会重复free同一块内存,导致程序崩溃!

【解决】:显式实现深拷贝

深拷贝的核心是 “给新对象重新申请资源”,避免指针指向同一块内存:

class Stack {
public:// 深拷贝构造Stack(const Stack& s) {// 给新对象重新申请内存_array = (DataType*)malloc(sizeof(DataType) * s._capacity);if (_array == nullptr) {perror("malloc失败");return;}// 复制数据(不是复制指针)memcpy(_array, s._array, sizeof(DataType) * s._size);_capacity = s._capacity;_size = s._size;}// 构造、析构同上...
};

4.4 拷贝构造的 3 个典型调用场景

  1. 用已有对象创建新对象(Date d2(d1));
  2. 函数参数为类类型对象(传值时,会拷贝实参到形参);
  3. 函数返回值为类类型对象(返回时,会拷贝局部对象到临时对象)。

优化建议:传参尽量用引用

函数参数用类类型引用(void Test(Date& d)),避免拷贝构造的调用,提高效率:

// 低效:参数传值,调用拷贝构造
void Test(Date d) {}// 高效:参数传引用,无拷贝
void Test(const Date& d) {}

五、赋值运算符重载 

两个已存在的对象之间赋值时(如d2 = d1),会调用赋值运算符重载函数。它和拷贝构造的区别是:拷贝构造是 “创建新对象时复制”,赋值重载是 “已有对象间复制”

5.1 赋值运算符重载的 4 个关键特性

  1. 返回值类型 operator=(参数列表);
  2. 参数类型为const 类类型&(引用提高效率,const 避免修改原对象);
  3. 返回值类型为类类型&(支持连续赋值,如d3 = d2 = d1);
  4. 必须显式检测 “自己给自己赋值”(如d1 = d1),避免无意义操作。

5.2 日期类赋值运算符的实现

class Date {
public:Date(int year = 1900, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}// 赋值运算符重载Date& operator=(const Date& d){// 1. 检测自己给自己赋值if (this != &d) {// 2. 复制成员变量_year = d._year;_month = d._month;_day = d._day;}// 3. 返回*this,支持连续赋值return *this;}private:int _year, _month, _day;
};// 使用示例
int main() 
{Date d1(2024, 5, 20);Date d2, d3;d3 = d2 = d1;  // 连续赋值,等价于d3.operator=(d2.operator=(d1))return 0;
}

5.3 避坑点:赋值运算符只能是类的成员函数

不能把赋值运算符重载成全局函数 —— 因为编译器会默认生成类内的赋值运算符重载,全局版本会和它冲突:

class Date {
public:Date(int year = 1900) { _year = year; }
private:int _year;
};// 错误!全局赋值运算符重载与默认生成的冲突
Date& operator=(Date& left, const Date& right) {if (&left != &right) {left._year = right._year;}return left;
}

并且成员变量是私有的,类外面不能访问

5.4 浅拷贝的坑:和拷贝构造类似

和拷贝构造一样,编译器默认生成的赋值运算符重载是 “浅拷贝”—— 有动态内存的类必须显式实现深拷贝,否则会重复释放内存: 

// 栈类的深拷贝赋值运算符重载
class Stack {
public:Stack& operator=(const Stack& s) {if (this != &s) {  // 检测自赋值// 1. 释放当前对象的旧资源free(_array);// 2. 给当前对象重新申请资源并复制数据_array = (DataType*)malloc(sizeof(DataType) * s._capacity);if (_array == nullptr) {perror("malloc失败");return *this;}memcpy(_array, s._array, sizeof(DataType) * s._size);_capacity = s._capacity;_size = s._size;}return *this;}// 构造、析构、拷贝构造同上...
};

这块有些同学就会疑惑为啥可以引用返回,原因是虽然this指针出了函数就销毁了,但是this指针指向的对象的声明周期并没有结束,所以返回引用是安全且合法的

5.5 运算符重载 (重点)

在 C++ 中,运算符重载是一项强大的特性,它允许我们为自定义类型赋予内置类型般的操作体验,极大提升了代码的可读性与易用性。 

5.5.1 运算符重载的本质 

运算符重载的本质是函数,其函数名遵循固定格式 operator@@ 为要重载的运算符,如 ==+= 等)。它的参数列表、返回值类型与普通函数一致,只是调用方式被 “伪装” 成了运算符形式。

例如,我们想让两个 Date 对象能通过 == 比较是否相等,就需要重载 operator== 函数。

5.5.2 全局函数 vs 成员函数

运算符重载可以通过全局函数类的成员函数实现,两者的核心区别在于参数列表的隐含性

【全局函数实现】:

若将 operator== 实现为全局函数,需要显式接收两个 Date 对象作为参数:

class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}// 成员变量需公开(或用友元解决封装性问题)int _year, _month, _day;
};// 全局函数重载 operator==
bool operator==(const Date& d1, const Date& d2) {return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}void Test() {Date d1(2024, 10, 1);Date d2(2024, 10, 2);cout << (d1 == d2) << endl; // 调用全局 operator==
}

缺点:若类的成员变量是私有的,全局函数无法直接访问,需通过 “友元” 声明打破封装(或直接将重载函数写为成员函数)。

【成员函数实现】:

若将 operator== 实现为类的成员函数,编译器会自动将左操作数作为隐含的 this 指针,因此参数列表只需显式接收右操作数:

class Date {
public:Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {}// 成员函数重载 operator==:左操作数是 this 指向的对象bool operator==(const Date& d2) const {return _year == d2._year && _month == d2._month && _day == d2._day;}
private:int _year, _month, _day; // 成员变量可私有,保证封装性
};void Test() {Date d1(2024, 10, 1);Date d2(2024, 10, 1);cout << (d1 == d2) << endl; // 调用成员函数 operator==
}

优点:无需破坏封装性,且逻辑更直观(左操作数的属性可直接访问)。

5.5.3 五个不能重载的运算符

C++ 规定以下 5 个运算符无法重载,这是笔试常考的知识点:

  • .(成员访问运算符)
  • .*(成员指针访问运算符)
  • ::(作用域解析运算符)
  • sizeof(字节大小运算符)
  • ?:(三目条件运算符)

六、其他 2 个默认成员函数:取地址重载

取地址重载和 const 取地址重载,编译器默认生成的实现完全满足需求 —— 返回对象的地址(this指针),几乎无需自定义:

class Date {
public:// 编译器默认生成的普通取地址重载Date* operator&(){return this;}// 编译器默认生成的const取地址重载const Date* operator&() const//这块的const修饰的是this所指向的对象,下一章节会具体讲下{return this;}private:int _year, _month, _day;
};

只有特殊场景(如 “隐藏对象真实地址”)才需要自定义,比如返回固定地址:

class Date {
public:Date* operator&() {return (Date*)0x12345678;  // 返回固定地址}
private:int _year, _month, _day;
};

七、总结:默认成员函数核心要点

函数类型显式实现场景核心坑点
构造函数需自定义初始化(如日期)默认构造只能有一个
析构函数类内有动态资源(如栈)忘记释放资源导致内存泄漏
拷贝构造函数类内有动态资源(避免浅拷贝)参数不用引用引发无穷递归
赋值运算符重载类内有动态资源(避免浅拷贝)忘记检测自赋值、返回 * this
取地址重载特殊需求(隐藏真实地址)几乎不用自定义
http://www.dtcms.com/a/569637.html

相关文章:

  • 中山哪里有做微网站的中国经济网
  • win11蓝屏笑脸提示重启怎么回事 蓝屏重启解决方法
  • 佰力博检测与你探讨压电薄膜介电/阻抗-频谱的应用领域有哪些
  • 长沙教育类网站建设好兄弟资源网
  • C++:哈希表的实现
  • 本地化部署后定制解决方案
  • Java中的WebSocket与实时通信!
  • SQL server创建数据表
  • MacOS-Terminal直接command解压缩文件文件夹
  • GIM 模型转 GLB 模型:从格式适配到前端渲染的完整方案
  • 什么是网站平台开发wordpress链接优化
  • 软件测试——自动化测试概念
  • 大模型-详解 Vision Transformer (ViT)
  • 建站公司互橙知乎郑州seo哪家专业
  • 09-ubuntu20.04 执行 apt update时报错,是因为官网已停止维护不再更新的缘故吗?
  • 南通做网站找谁求网站懂的说下开车
  • ps做网站宽度重庆公司团建推荐
  • uniapp中的uni_modules分包
  • 算法笔记 09
  • 【VLAs篇】08:以实时速度运行VLA
  • 广西桂林建设局网站建立网站需要多少钱 索 圈湖南岚鸿
  • 买完服务器怎么做网站网页编辑软件绿色
  • 从奠基到前沿:CIFAR-10 数据集如何驱动计算机视觉研发进化
  • 计算机网络第六章学习
  • 华为A800I A2 arm64架构鲲鹏920cpu的ubuntu22.04 tls配置直通的grub配置
  • 耐热型发光颜料:高温环境下的功能材料新星
  • 简单易做的的网站做网站一定要注册域名吗
  • 正态分布概率:1σ、2σ、3σ、4σ深度解读
  • 红帽Linux-调优系统性能
  • python找到文件夹A中但是不在文件夹B中的文件