news 2026/10/21 20:34:22

【C++】 初阶篇 类与对象(中) 六大默认成员函数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++】 初阶篇 类与对象(中) 六大默认成员函数

前言

在 C++ 面向对象编程中,“类与对象” 是核心基石,而默认成员函数则是编译器为类自动生成的 “隐藏助手”—— 即使你没显式写,它们也在默默工作。本文将从实际开发角度拆解类的 6 个默认成员函数,帮你搞懂 “为什么需要它们”“怎么用” 以及 “踩坑点在哪”。同时友情提示一下:这一篇的深拷贝可以先不管,我是学完后面的回来写博客,所以随便实现了一下,我在 STL 的底层实现的时候会具体细致地讲。

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

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

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

需要注意的是这 6 个中最重要的是前 4 个。最后两个取地址重载不重要,我们稍微了解一下即可。其次就是 C++11 以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现
?

二、构造函数

2.1 构造函数的定义

每次创建对象时,编译器都会自动调用一次构造函数 —— 它的核心不是 “创建对象”(开空间由编译器负责),而是 “给对象的成员变量赋初始值”,避免成员变量出现随机值。
构造函数的本质是要替代我们以前写的Init函数的功能,构造函数自动调用的特点就完美的替代了Init。

2.2 构造函数特性与创建

  • 函数名与类名相同。
  • 无返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++ 规定如此)
  • 构造函数可以重载(无参、带参、全缺省都可)。
classDate{public:// 1. 无参构造(编译器默认生成的就是类似无参版本)Date(){_year=1900;_month=1;_day=1;}// 2. 带参构造(重载版本,支持自定义初始化)Date(intyear,intmonth,intday){_year=year;_month=month;_day=day;}private:int_year;int_month;int_day;};// 使用示例intmain(){Date d1;// d1实例化时自动调用无参构造Dated2(2024,5,20);// 调用带参构造// Date d3(); // 错误!这是声明函数,不是创建对象return0;}

写到这里我们已经发现构造函数的优势了,但是还可以结合我们的缺省值这个知识点,我们可以只写一个来替代我们刚刚写的那两个构造函数,请看全缺省构造函数:

Date(intyear=2025,intmonth=2,intday=20){_year=year;_month=month;_day=day;}

瞬间减少了我们的代码量。但是这个全缺省的构造函数和无参的构造函数不能同时出现,因为这样编译器就不知道调用哪一个了。

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

如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
我们先来看看编译器生成的

classDate{public:voidprint(){cout<<_year<<"-"<<_month<<"-"<<_day<<endl;}private:int_year;int_month;int_day;};intmain(){Date d1;// d1实例化时自动调用默认生成的无参构造d1.print();return0;}


我们发现好像编译器生成的默认构造函数,好像啥也没干,还是随机值,那它有啥用?别急,我们继续看

classTime{public:// Time的默认构造Time(){cout<<"Time()被调用"<<endl;_hour=0;_minute=0;_second=0;}private:int_hour,_minute,_second;};classDate{public:voidprint(){cout<<_year<<"-"<<_month<<"-"<<_day<<endl;}private:int_year;int_month;int_day;Time _t;};intmain(){Date d1;// d1实例化时自动调用默认生成的无参构造d1.print();return0;}


我们发现系统生成的默认构造函数会调用 “自定义类型成员”(Time)的默认构造函数。也就是它没给内置类型(int/char等)赋初始值,但是会调用 “自定义类型成员” 的默认构造函数。

注意:

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

三、析构函数

3.1 析构函数的定义

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,
C++ 规定对象在销毁时(生命周期结束)会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前的Destroy功能。

  • 为什么析构函数不是完成对对象本身的销毁
    因为对象是存在栈帧,只有函数结束,栈帧销毁并释放

3.2 析构函数特性与创建

  • 函数名 = ~(有点像取反的意思)类名,无参数、无返回值;
  • 一个类只能有一个析构函数(不能重载);
  • 若未显式实现,编译器会生成默认析构函数。

3.3 编译器默认析构函数

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

classTime{public:~Time(){cout<<"~Time()被调用"<<endl;}private:int_hour,_minute,_second;};classDate{private:int_year=1970;// 内置类型,默认析构不处理Time _t;// 自定义类型,默认析构调用~Time()};intmain(){Date d;// 程序结束时,输出"~Time()被调用"return0;}

3.4 什么时候要显式写析构函数?

  • 没有空间申请的类不需要析构函数
  • 比如只有内置类型的,因为它们没有在堆上开辟空间,都是在栈上开空间出来,它们出了函数的作用域会自动销毁,把空间还给操作系统。
    日期类:
classDate{public://…………………private:int_year;int_month;int_day;};

日期类都是内置类型我们就可以不用写

栈类的析构函数

typedefintDataType;classStack{public:// 构造函数:申请动态内存Stack(size_t capacity=4){_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;};// 使用示例voidTestStack(){Stack s;// 创建对象,调用构造s.Push(1);s.Push(2);}// s出作用域,自动调用析构,释放内存

因为像栈这种它有动态开辟内存在堆上的,系统生成的它不会去清理堆上的,就不够用了,我们要显式写析构函数。

3.5 总结

  • 对象生命周期结束,系统会自动调用默认的析构函数。
  • 有资源申请的类需要写析构函数。
  • 有多个对象需要析构的时候后创建的先析构。因为栈帧后进先出。

四、拷贝构造函数

4.1 拷贝构造函数的定义

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

4.2 拷贝构造函数特性与创建

  • 是构造函数的重载形式,函数名 = 类名;
  • 参数必须是 “类类型的引用”(Date& d);
  • 若未显式实现,编译器会生成默认拷贝构造(浅拷贝)。
  • 引用前面最好加上const限制。(保护用来拷贝的那个数据)
Date(constDate&d){_year=d._year;_month=d._month;_day=d._day;}
  • 采用const引用的原因
    可以保护形式参数不被改变。传值传参,拷贝构造函数只是一个中转。我不希望它在函数体内对我的原来的对象造成修改。
  • 为什么参数必须是引用?
    • 如果参数是传值(Date(const Date d)),调用拷贝构造时会先复制实参到形参 —— 而复制形参又需要调用拷贝构造,简而言之,传参会调用拷贝构造,调用拷贝构造又要调用拷贝构造,形成 “无穷递归”:具体可以看下面的图
classDate{public:// 正确写法:参数是const引用(const避免修改原对象,引用避免递归)Date(constDate&d){_year=d._year;_month=d._month;_day=d._day;}Date(intyear=0,intmonth=1,intday=1){_year=year;_month=month;_day=day;}// 错误写法:参数传值,引发无穷递归// Date(const Date d) {}private:int_year,_month,_day;};intmain(){Date d1;Dated2(d1);return0;}

  • 调用拷贝构造函数的另外一种方法
    第二种调用方法实质上等价于第一种。(但是可读性更高)
Date d1;Date d2=d1;

4.3 编译器默认生成拷贝构造函数

与构造函数和析构函数不同,自动生成的拷贝构造函数会管内置类型。

  • 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。
  • 自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝
  • 对自定义类型成员变量会调用它的拷贝构造。

    我们发现自己不写也会完成,因为编译器生成的会按照字节进行拷贝。
  • 情况一:类成员均为内置类型,且不管理资源
    例如Date类。编译器生成的拷贝构造函数会完成值拷贝,因此不需要我们显式实现拷贝构造。
  • 情况二:类成员包含自定义类型,且该类型已正确实现拷贝构造函数

例如Myqueue类,其内部成员主要是自定义类型Stack的对象。编译器自动生成的拷贝构造函数会调用Stack的拷贝构造函数,因此也不需要我们显式实现Myqueue的拷贝构造。

  • 情况三:类管理动态资源

例如Stack类,其成员_a指向了资源。编译器自动生成的拷贝构造函数仅完成值拷贝 / 浅拷贝(仅复制指针值),并不符合我们的需求,因此必须手动实现深拷贝,即对指针所指向的资源也进行完整拷贝。

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

像上面的情况三就要自己写了,系统生成的无法完成。我们先看为什么

  • 解决办法:深拷贝

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

classStack{public:// 深拷贝构造Stack(constStack&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;}// 构造、析构同上...};

五、赋值运算符重载

5.1 运算符重载

比如我们如果要比较日期类对象的大小,按照我们现有的知识要去写一个函数,但是我们如果想直接比较呢?d1>d2?所以有了运算符重载。
在 C++ 中,运算符重载是一项强大的特性,它允许我们为自定义类型赋予内置类型般的操作体验,极大提升了代码的可读性与易用性。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator 操作符(参数列表)

Date&operator=(constDate&d)
  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型 +,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的this
  • .*、::、sizeof、?:、.注意以上 5 个运算符不能重载。

5.2 赋值运算符重载的特性与创建

5.2.1 赋值运算符特性与创建

  • 返回值类型 operator=(参数列表);
  • 参数类型为const 类类型&(引用提高效率,const避免修改原对象);
  • 返回值类型为类类型&(支持连续赋值,如d3 = d2 = d1);
  • 必须显式检测 “自己给自己赋值”(如d1 = d1),避免无意义操作。
classDate{public:Date(intyear=1900,intmonth=1,intday=1){_year=year;_month=month;_day=day;}// 赋值运算符重载Date&operator=(constDate&d){// 1. 检测自己给自己赋值if(this!=&d){// 2. 复制成员变量_year=d._year;_month=d._month;_day=d._day;}// 3. 返回*this,支持连续赋值return*this;//返回d2}private:int_year,_month,_day;};// 使用示例intmain(){Dated1(2024,5,20);Date d2,d3;d3=d2=d1;// 连续赋值,等价于d3.operator=(d2.operator=(d1))return0;}

5.2.2 全局函数 vs 成员函数

运算符重载可以通过全局函数或类的成员函数实现,两者的核心区别在于参数列表的隐含性。
若将operator==实现为全局函数,需要显式接收两个Date对象作为参数:

  • 【全局函数实现】:
classDate{public:Date(intyear=1900,intmonth=1,intday=1):_year(year),_month(month),_day(day){}// 成员变量需公开(或用后面友元解决封装性问题)int_year,_month,_day;};// 全局函数重载 operator==booloperator==(constDate&d1,constDate&d2){returnd1._year==d2._year&&d1._month==d2._month&&d1._day==d2._day;}voidTest(){Dated1(2024,10,1);Dated2(2024,10,2);cout<<(d1==d2)<<endl;// 调用全局 operator==}

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

  • 【成员函数实现】:
    若将operator==实现为类的成员函数,编译器会自动将左操作数作为隐含的this指针,因此参数列表只需显式接收右操作数:
classDate{public:Date(intyear=1900,intmonth=1,intday=1):_year(year),_month(month),_day(day){}// 成员函数重载 operator==:左操作数是 this 指向的对象booloperator==(constDate&d2)const{return_year==d2._year&&_month==d2._month&&_day==d2._day;}private:int_year,_month,_day;// 成员变量可私有,保证封装性};voidTest(){Dated1(2024,10,1);Dated2(2024,10,1);cout<<(d1==d2)<<endl;// 调用成员函数 operator==}

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

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

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

5.3 编译器默认生成的赋值运算符重载

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

// 栈类的深拷贝赋值运算符重载classStack{public:Stack&operator=(constStack&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;}// 构造、析构、拷贝构造同上...};

5.3.1 不需要手动实现的情况

情况一:类成员均为内置类型,且不管理资源
例如Date类。编译器生成的赋值运算符会进行值拷贝。

情况二:类成员包含自定义类型,且该类型已正确实现赋值运算符
例如Myqueue类,其成员为Stack对象。编译器会递归调用Stack的赋值运算符,只要Stack已正确实现深拷贝,Myqueue就无需手动实现。

5.3.2 必须手动实现的情况

类管理动态资源(如堆内存、文件句柄等)
例如Stack类,其成员_a指向动态数组。编译器生成的赋值运算符仅进行浅拷贝(复制指针值),会导致:内存泄漏,重复释放,悬空指针等一系列问题。

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

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

七、总结

函数类型显式实现场景核心坑点
构造函数需自定义初始化(如日期)默认构造只能有一个
析构函数类内有动态资源(如栈)忘记释放资源导致内存泄漏
拷贝构造函数类内有动态资源(避免浅拷贝)参数不用引用引发无穷递归
赋值运算符重载类内有动态资源(避免浅拷贝)忘记检测自赋值、返回*this
取地址重载特殊需求(隐藏真实地址)几乎不用自定义
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 20:33:22

Hetttangk yuese 《荷塘月色》汉语拼音字母标调拼写实测案例

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 20:25:28

Folia 私有音乐站 NAS 部署教程:Docker Compose 6 个容器,10 分钟上线

Folia 私有音乐站 NAS 部署教程&#xff1a;Docker Compose 6 个容器&#xff0c;10 分钟上线 【免费下载链接】folia-major 专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器 项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major …

作者头像 李华
网站建设 2026/10/21 20:18:48

free-stockdb 快速上手:30 分钟搭一套本地 A 股分钟K数据引擎

free-stockdb 快速上手&#xff1a;30 分钟搭一套本地 A 股分钟K数据引擎 【免费下载链接】free-stockdb 面向 A 股日K、分钟K与ETF分钟数据的本地量化引擎&#xff0c;集成增量同步、本地缓存、复权、批量查询、回测与指标计算。 项目地址: https://gitcode.com/GitHub_Tren…

作者头像 李华
网站建设 2026/10/21 20:07:13

HashMap源码和并发异常问题分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 20:05:39

读写数据依赖:芯片设计中的性能隐形杀手与处理策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华