【C++】类的默认成员函数(上)

在这里插入图片描述
🔥博客主页 小羊失眠啦.
🎥系列专栏《C语言》 《数据结构》 《C++》 《Linux》 《Cpolar》
❤️感谢大家点赞👍收藏⭐评论✍️


在这里插入图片描述

文章目录

  • 一、默认成员函数
  • 二、构造函数
    • 构造函数的概念及特性
  • 三、析构函数
    • 析构函数的特性
  • 四、拷贝构造函数
    • 拷贝构造函数的特性

一、默认成员函数

上一章中我们谈到,如果一个类中什么成员也没有,那么这个类就叫作空类。其实这么说是不太严谨的,因为一个类不可能什么都没有

当我们定义好一个类,不做任何处理时,编译器会自动生成以下6个默认成员函数

  • 默认成员函数:如果用户没有手动实现,则编译器会自动生成的成员函数。

在这里插入图片描述

  • 构造函数:主要完成初始化工作;
  • 析构函数:主要完成清理工作;
  • 拷贝构造:使用一个同类的对象初始化创建一个对象;
  • 赋值重载:把一个对象赋值给另一个对象;
  • 取地址重载普通对象取地址操作;
  • 取地址重载(const):const对象取地址操作;

本章我们将学习四个默认成员函数——构造函数析构函数——拷贝构造赋值重载


二、构造函数

在C语言阶段,我们实现的数据结构时,有一件事很苦恼,就是每当创建一个stack对象(之前叫作定义一个stack类型的变量)后,首先得调用它的专属初始化函数StackInit来初始化对象。

typedef int dataOfStackType;typedef struct stack
{dataOfStackType* a;int top;int capacity;
}stack;void StackInit(stack* ps);
//...int main(){stack s;StackInit(&s);//...return 0;}

这不免让人觉得有点麻烦。在C++中,构造函数为我们很好的解决了这一问题。

构造函数的概念及特性

构造函数是一个特殊的成员函数。构造函数虽然叫作构造,但是其主要作用并不是开辟空间创建对象,而是初始化对象

构造函数之所以特殊,是因为相比于其它成员函数,它具有如下特性

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时,编译器自动调用对应的构造函数
  4. 构造函数可以重载

举例

class Date
{
public://无参的构造函数Date(){};//带参的构造函数Date(int year,int month,int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};void TestDate()
{Date d1;//调用无参构造函数(自动调用)Date d2(2023, 3, 29);//调用带参构造函数(自动调用)
}

特别注意

  • 创建对象时编译器会自动调用构造函数,若是调用无参构造函数,则无需在对象后面使用()。否则会产生歧义:编译器无法确定你是在声明函数还是在创建对象

错误示例

//错位示例
Date d3();
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date
{
public://若用户没有显示定义,则编译器自动生成。/*Date(int year,int month,int day){_year = year;_month = month;_day = day;}*/private:int _year;int _month;int _day;
};
  1. 默认生成构造函数,对内置类型成员不作处理;对自定义类型成员,会调用它的默认构造函数
  • C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char、double…,自定义类型就是我们使用class、struct、union等自己定义的类型。

举例

默认构造函数对内置类型

class Date
{
public://此处不对构造函数做显示定义,测试默认构造函数/*Date(){}*/void print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
void TestDate1()
{Date d1;d1.print();
}

在这里插入图片描述

  • 如图所示,默认构造函数的确未对内置类型做处理。

默认构造函数对自定义类型

class stack
{
public://此处对stack构造函数做显示定义stack(){cout <<"stack()" << endl;_a = nullptr;_top = _capacity = 0;}
private:int* _a;int _top;int _capacity;
};class queue
{
public://此处不对queue构造函数做显示定义,测试默认构造函数/*queue(){}*/
private://自定义类型成员stack _s;
};void TestQueue()
{queue q;
}

在这里插入图片描述

  • 如图所示,在创建queue对象时,默认构造函数对自定义成员_s做了处理,调用了它的默认构造函数stack()

这一波蜜汁操作让很多C++使用者感到困惑与不满,为什么要针对内置类型和自定义类型做不同的处理呢?终于,在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:

  1. 内置类型成员变量在类中声明时可以给默认值

举例

class Date
{
public:
//...void print(){cout << _year << "-" << _month << "-" << _day << endl;}
private://使用默认值int _year = 0;int _month = 0;int _day = 0;
};
void TestDate2()
{Date d2;d2.print();
}

在这里插入图片描述

  • 默认值:若不对成员变量做处理,则使用默认值。
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

举例

class Date
{
public://无参的默认构造函数//Date()//{//}//全缺省的默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}void print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year = 0;int _month = 0;int _day = 0;
};

默认构造函数:

  1. 无参的构造函数
  2. 全缺省的构造函数
  3. C++编译器生成的无参的构造函数

即三种必须要有一种,如果没有默认的构造函数(写的构造函数不是无参的,也不是全缺省的)就会报错


三、析构函数

析构函数构造函数的特性相似,但功能有恰好相反。构造函数是用来初始化对象的,析构函数是用来销毁对象的。

  • 需要注意的是,析构函数并不是对对象本身进行销毁(因为局部对象出了作用域会自行销毁,由编译器来完成),而是在对象销毁时会自动调用析构函数,对对象内部的资源做清理(例如stack _s中的int* a)。

同样,有了析构函数,我们再也不用担心创建对象(或定义变量)后由于忘记释放内存而造成内存泄漏了。

举例

class Stack
{
public:Stack(){//...}void Push(int x){//...}bool Empty(){// ...}int Top(){//...}void Destory(){//...}
private:// 成员变量int* _a;int _top;int _capacity;
};void TestStack()
{Stack s;st.Push(1);st.Push(2);//过去需要手动释放st.Destroy();
}

析构函数的特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数
  3. 无返回值
  4. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  5. 析构函数不能重载

举例

class Date
{
public:Date(){cout << "Date()" << endl;}~Date(){cout << "~Date()" << endl;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate3()
{Date d3;//d3生命周期结束时自动调用构造函数
}

在这里插入图片描述

  1. 编译器生成的默认析构函数,对自定类型成员调用它的析构函数

举例

class stack
{
public://此处对stack构造函数做显示定义stack(){cout <<"stack()" << endl;_a = nullptr;_top = _capacity = 0;}~stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:int* _a;int _top;int _capacity;
};
class queue
{
public://此处不对queue构造函数做显示定义,测试默认构造函数/*queue(){}*/
private://自定义类型成员stack _s;
};void TestQueue1()
{queue q;
}

在这里插入图片描述

  • 这里可能有小伙伴会好奇:为什么析构函数不像构造函数那样区分内置类型与自定义类型呢
    答案是:因为内置类型压根不需要我们担心清理工作,在其生命周期结束时会自动销毁。而自定义类型需要担心,因为自定义类型里可能含有申请资源(例如:malloc申请内存须手动释放)。
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如stack类。

四、拷贝构造函数

同样,拷贝构造函数也属于6个默认成员函数,而且拷贝构造函数构造函数的一种重载形式

  • 拷贝构造函数的功能就如同它的名字——拷贝。我们可以用一个已存在的对象来创建一个与已存在对象一模一样的新的对象

举例

class Date
{
public://构造函数Date(){cout << "Date()" << endl;}//拷贝构造函数Date(const Date& d){cout << "Date()" << endl;_year = d._year;_month = d._month;_day = d._day;}//析构函数~Date(){cout << "~Date()" << endl;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate()
{Date d1;//调用拷贝构造创建对象Date d2(d1);
}

在这里插入图片描述

拷贝构造函数的特性

拷贝构造函数作为特殊的成员函数同样也有异于常人的特性:

  1. 拷贝构造函数是构造函数的重载
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用。若使用传值的方式,则编译器会报错,因为理论上这会引发无穷递归

错误示例

class Date
{
public://错误示例//如果这样写,编译器就会直接报错,但我们现在假设如果编译器不会检查,//这样的程序执行起来会发生什么Date(const Date d){_year = d._year;_month = d._month;_day = d._day;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate()
{Date d1;//调用拷贝构造创建对象Date d2(d1);
}
  • 当拷贝构造函数的参数采用传值的方式时,创建对象d2,会调用它的拷贝构造函数d1会作为实参传递给形参d。不巧的是,实参传递给形参本身又是一个拷贝,会再次调用形参的拷贝构造函数…如此便会引发无穷的递归。

在这里插入图片描述

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或者值拷贝

举例

class Date
{
public://构造函数Date(int year = 0, int month = 0, int day = 0){//cout << "Date()" << endl;_year = year;_month = month;_day = day;}//未显式定义拷贝构造函数/*Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}*/void print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year = 0;int _month = 0;int _day = 0;
};void TestDate()
{Date d1(2023, 3, 31);//调用拷贝构造创建对象Date d2(d1);d2.print();
}

在这里插入图片描述

  • 有的小伙伴可能会有疑问:编译器默认生成的拷贝构造函数貌似可以很好的完成任务,那么还需要我们手动来实现吗?
    答案是:当然需要。Date类只是一个较为简单的类且类成员都是内置类型,可以不需要。但是当类中含有自定义类型时,编译器可就办不了事儿了。
  1. 类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

错误示例

class stack
{
public:stack(int defaultCapacity=10){_a = (int*)malloc(sizeof(int)*defaultCapacity);if (_a == nullptr){perror("malloc fail");exit(-1);}_top =  0;_capacity = defaultCapacity;}~stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}void push(int n){_a[_top++] = n;}void print(){for (int i = 0; i < _top; i++){cout << _a[i] << " ";}cout << endl;}
private:int* _a;int _top;int _capacity;
};void TestStack()
{stack s1;s1.push(1);s1.push(2);s1.push(3);s1.push(4);s1.print();stack s2(s1);s2.print();s2.push(5);s2.push(6);s2.push(7);s2.push(8);s2.print();
}

在这里插入图片描述

如图所示,这段程序的运行结果是程序崩溃了,且通过观察发现,是在第二次析构时出现了错误。其实出现错误的原因是在第二次析构时对野指针进行free了。

一个小tip

  • 多个对象进行析构的顺序如同一样,先创建的对象后析构,后创建的对象先析构

为什么会出现对野指针进行free呢?

  • 原因是,对象s1与对象s2中的成员_a,指向的是同一块空间。在s2析构完成后,这块空间已经被释放,此时的s1._a就是野指针。这就是浅拷贝导致的后果。

理解浅拷贝

编译器默认生成的拷贝构造函数是按字节序拷贝的,在创建s2对象时,仅仅是把s1._a的值赋值给s2._a并没有重新开辟一块与s1._a所指向的空间大小相同内容相同的空间。我们把前者的拷贝方式称为浅拷贝后者称为深拷贝

在这里插入图片描述

当开启监视窗口来观察这一过程,我们可以看到s2在进行push时,s1的内容也在跟着改变,且s1._a=s2._a

在这里插入图片描述

正确的做法

class stack
{
public:stack(int defaultCapacity=10){_a = (int*)malloc(sizeof(int)*defaultCapacity);if (_a == nullptr){perror("malloc fail");exit(-1);}_top =  0;_capacity = defaultCapacity;}//用户自己定义拷贝构造函数stack(const stack& s){_a= (int*)malloc(sizeof(int) * s._capacity);if (_a == nullptr){perror("malloc fail");exit(-1);}memcpy(_a, s._a, sizeof(int) * s._capacity);_top = s._top;_capacity = s._capacity;}~stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}void push(int n){_a[_top++] = n;}void print(){for (int i = 0; i < _top; i++){cout << _a[i] << " ";}cout << endl;}
private:int* _a;int _top;int _capacity;
};
  1. 拷贝构造函数典型调用场景
  • 使用已存在对象创建新对象;
  • 函数参数类型为类类型对象;
  • 函数返回值类型为类类型对象。

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/270123.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

论文笔记:Compact Multi-Party Confidential Transactions

https://link.springer.com/chapter/10.1007/978-3-030-65411-5_21 A compact, private, Multi-Party Confidential Transactions (MCT) 紧凑型多方机密交易&#xff08;Compact MCT&#xff09;&#xff1a;MCT的长度与常规的单一所有者交易一样短&#xff1b;换句话说&…

【大数据架构(3)】Lambda vs. Kappa Architecture-选择你需要的架构

文章目录 一. Data Processing Architectures1. Lambda Architecture1.1. 架构说明a. Data Ingestion Layerb. Batch Layer (Batch processing)c. Speed Layer (Real-Time Data Processing)d. Serving Layer 1.2. Lambda Architecture的优缺点1.3. 使用案例 2. Kappa Architect…

MongoDB Helloworld For Window

1. 下载MongoDB Download MongoDB Community Server | MongoDB 2. 安装MongoDB 3. 创建DB. 4. 用java code 连接mongo. 做增删改查操作。 pom.xml <dependency><groupId>org.mongodb</groupId><artifactId>mongodb-driver-sync</artifactId>&…

docker 基础(二)

常见命令 Docker最常见的命令就是操作镜像、容器的命令&#xff0c;详见官方文档&#xff1a;https://docs.docker.com/ 数据卷 命令说明文档地址docker volume create创建数据卷docker volume createdocker volume ls查看所有数据卷docker volume lsdocker volume rm删除数…

0.8秒一张图40hx矿卡stable diffusion webui 高质极速出图组合(24.3.3)

新消息是。经过三个月的等待&#xff0c;SD Webui (automatic1111)终于推出了新版本1.8.0&#xff0c;本次版本最大的更新&#xff0c;可能就是pytorch更新到2.1.2, 不过还是晚了pytorch 2.2.2版。 不过这版的一些更新&#xff0c;在forget分支上早就实现了&#xff0c;所以。…

C及C++每日练习(2)

1.选择&#xff1a; 1.使用printf函数打印一个double类型的数据&#xff0c;要求&#xff1a;输出为10进制&#xff0c;输出左对齐30个字符&#xff0c;4位精度。以下哪个选项是正确的&#xff1f; A.%-30.4e B.%4.30e C.%-30.4f D.%-4.30 在上一篇文章中&#xff0c;提到了…

【C++实战项目】Date日期类 --- 运算符重载的深入探索

&#x1f4f7; 江池俊&#xff1a;个人主页 &#x1f525; 个人专栏&#xff1a;✅C那些事儿 ✅Linux技术宝典 &#x1f305; 此去关山万里&#xff0c;定不负云起之望 文章目录 引言一、为什么需要运算符重载&#xff1f;二、日期类的实现1. 基本框架2. 预备工作3. Date 类…

Linux Watchdog 机制是什么

当涉及到Linux操作系统的稳定性和可靠性时&#xff0c;Linux Watchdog机制是一个至关重要的议题。该机制旨在监控系统状态&#xff0c;确保在出现问题时采取适当的措施以维持系统的正常运行。本文将深入探讨Linux Watchdog机制的工作原理、应用范围以及如何配置和使用该机制来提…

【leetcode C++】电话号码的字母组合

17. 电话号码的字母组合 题目 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 题目链接 . - 力扣&#xff08;LeetCode&…

MyBatis 学习(六)之动态 SQL

目录 1 动态 SQL 介绍 2 if 标签 3 where 标签 4 set 标签 5 trim 标签 6 choose、when、otherwise 标签 7 foreach 标签 8 bind 标签 1 动态 SQL 介绍 动态 SQL 是 MyBatis 强大特性之一&#xff0c;极大的简化我们拼装 SQL 的操作。MyBatis 的动态 SQL 是基于 OGNL 的…

腾讯云8核32G22M服务器优惠价格115元1个月、345元3个月

腾讯云8核32G22M服务器优惠价格115元1个月、345元3个月 一张表看懂腾讯云服务器租用优惠价格表&#xff0c;一目了然&#xff0c;腾讯云服务器分为轻量应用服务器和云服务器CVM&#xff0c;CPU内存配置从2核2G、2核4G、4核8G、8核16G、4核16G、8核32G、16核32G、16核64等配置可…

基于springboot+vue的科研工作量管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

基于 HBase Phoenix 构建实时数仓(1)—— Hadoop HA 安装部署

目录 一、主机规划 二、环境准备 1. 启动 NTP 时钟同步 2. 修改 hosts 文件 3. 配置所有主机间 ssh 免密 4. 修改用户可打开文件数与进程数&#xff08;可选&#xff09; 三、安装 JDK 四、安装部署 Zookeeper 集群 1. 解压、配置环境变量 2. 创建配置文件 3. 创建新…

Vue+腾讯地图-实现关键词输入提示功能

不废话&#xff0c;上代码~~~ 效果图&#xff1a; 1、先去腾讯地图后台创建自己的应用获取到应用的 Key 腾讯地图后台地址&#xff1a;腾讯位置服务 - 立足生态&#xff0c;连接未来 创建应用的 Key 如下&#xff1a; 2、在项目中添加腾讯地图API的js插件&#xff0c;如…

学习JAVA的第十四天(基础)

目录 Collection集合 迭代器遍历 增强for遍历 Lambda表达式遍历 List集合 遍历 数据结构 栈 队列 数组 链表 前言&#xff1a; 学习JAVA的第十三天 Collection集合 Collection的遍历方式&#xff1a; 迭代器&#xff08;不依赖索引&#xff09;遍…

Java线程池及Thread相关问题

Java线程池及Thread相关问题 一、Java线程池有哪些核心参数&#xff0c;分别有什么的作用&#xff1f;二、线程池有哪些拒绝策略&#xff1f;三、线程池的执行流程?四、线程池核心线程数怎么设置呢&#xff1f;方式一方式二基本原则 五、ThreadLocal底层是怎么实现的&#xff…

SoraAI优先体验资格注册教程

SoraA1视频工具优先体验资格申请 申请网址&#xff1a;https://openai.com/form/red-teaming-network 申请步骤&#xff1a; 填写基础信息 请使用英文根据内容填写以下内容&#xff0c;名、姓、电子邮件、居住国家、组织隶属关系(如果有)、教育水平 、学位&#xff08;哪个领…

利用Python副业赚钱,看完这篇你就懂了!

Python都可以做哪些副业&#xff1f; 1、兼职处理数据Excel整理数据功能虽然很强大&#xff0c;但在Python面前&#xff0c;曾经统治职场的它也的败下阵来。因为Python在搜集数据整理分析数据的过程中更加便捷&#xff0c;通过几行代码还可以实现自动化操作。 如果你学会Pyth…

指针运算笔试题解析

题目1&#xff1a; int main() { int a[5] { 1, 2, 3, 4, 5 }; int* ptr (int*)(&a 1); printf("%d %d", *(a 1), *(ptr - 1)); return 0; } ptr中存放了整个数组的地址&#xff0c;ptr是int*类型&#xff0c;&a1跳到5的地址后又被强制类…

EasyX的学习2

消息处理——漂亮的按钮(鼠标) 用到的函数 1.消息结构体变量类型&#xff1a;使用ExMessage ExMessage msg{ 0 }; 定义一个变量名为msg的ExMessage结构体变量并初始化为0 2.获取消息函数&#xff1a;peekmessage函数 //获取消息 peekmessage(&msg, EX_MOUSE); 两个参…