【C++】类的六个默认成员函数

2024-06-21 19:36
文章标签 c++ 默认 六个 函数 成员

本文主要是介绍【C++】类的六个默认成员函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 类的六个默认成员函数
  • 一、构造函数
  • 二、析构函数
  • 三、拷贝构造函数
  • 四、赋值运算符重载
  • 五、const成员
  • 六、取地址及const取地址操作符重载

类的六个默认成员函数

如果一个类中什么成员都没有,称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数

在这里插入图片描述
前面四个默认成员函数很重要,后面两个很少自己实现。

默认成员函数:用户没有显式实现,编译器会自己生成的成员函数称为默认成员函数。

一、构造函数

构造函数的概念

构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有
一个合适的初始值,并且在对象整个生命周期内只调用一次。

构造函数的主要任务并不是开空间创建对象,而是初始化对象

构造函数的特性

1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载,在创建对象时根据传递实参来判断调用哪一个构造函数。


在这里插入图片描述
在这里插入图片描述

注意:通过无参构造函数创建对象时,对象后面不能跟括号,否则就成了函数声明。

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


在这里插入图片描述

这里显示定义了一个带参的构造函数,编译器将不会生成无参的默认构造函数,由于不存在无参构造函数,所以这样调用是错误的。

一般情况下,我们可以写成全缺省的构造函数,这样既可以无参调用,也可以带参调用构造函数。

6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。因为都可以无参调用,不能同时存在,否则调用会存在歧义。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

class Date
{
public://全缺省构造函数 可以认为是默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{Date d1;//可以无参调用Date d2(2024, 6, 1);//也可以带参调用return 0;
}

如果我们不写构造函数,使用编译器默认生成的构造函数,会发现打印结果是随机数。

在这里插入图片描述

不实现构造函数的情况下,编译器会生成默认的构造函数。d1对象调用了编译器生成的默认构造函数,但是它的_year、_month、_day,依旧是随机值。也就说在这里编译器生成的默认构造函数似乎并没有什么用?

这是因为,C++把类型分成内置类型(基本类型)和自定义类型。
内置类型就是语言提供的数据类型,如int、char、指针等基本类型。
自定义类型就是struct、class、union等自己定义的类型。

7.我们不写,编译器默认生成的构造函数,对内置类型不做处理,对于自定义类型会去调用它的默认构造函数。

在这里插入图片描述可以发现,创建一个A类的对象,会自动调用B的默认构造函数(自己定义的无参构造函数也认为是默认构造函数)。

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

在这里插入图片描述

那我们什么时候自己写构造函数,什么时候不写,使用编译器默认生成的呢?

1.一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的。如果内置类型成员在定义时已初始化且初始化符合我们的要求,这种情况可以不写。
2.全部是自定义类型成员,可以考虑让编译器自己生成。

怎么判断是不是默认构造函数呢?

可以不传参调用的都可以看做是默认构造函数。 无参构造函数、全缺省构造函数、我们不写编译器自己生成的都叫做默认构造函数,都无需传参。没有缺省值需要传参调用的不是默认构造。

二、析构函数

析构函数的概念

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数的特性

1.析构函数名是在类名前加上字符 ~
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4.对象生命周期结束时,C++编译系统会自动调用析构函数。

在这里插入图片描述

比如我们写一个顺序栈。

typedef int STDateType;
class Stack
{
public://给缺省值的默认构造函数 完成初始化工作Stack(int capacity = 4){_a = (STDateType*)malloc(sizeof(STDateType) * capacity);if (nullptr == _a){perror("malloc");return;}_capacity = capacity;_top = 0;}void Push(STDateType x){// CheckCapacity();_a[_top++] = x;}//...//析构函数完成销毁工作~Stack(){if (nullptr != _a)//空指针不能free{free(_a);_a = nullptr;_capacity = _top = 0;}}
private:STDateType* _a;int _top;int _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(10);return 0;
}

是不是比C语言的好写并且好用,不需要我们手动去调用初始化函数和销毁函数,构造函数和析构函数会自动调用。

5.系统自动生成的默认析构函数对内置类型不做处理,自定义类型会去调用它的析构函数。

注意

1.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如日期Date类;有资源申请时,析构函数一定要写,否则会造成资源泄漏,比如Stack类。
2.析构函数不能重载!

三、拷贝构造函数

拷贝构造函数的概念

只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

拷贝构造函数与构造函数的名字相同,二者构成重载关系;拷贝构造是用已存在的类对象来初始化新对象

拷贝构造函数的特性

1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式引发无穷递归调用。

//拷贝构造函数
Date(const Date& d)//引用类型
{_year = d._year;_month = d._month;_day = d._day;
}
int main()
{Date d1(2024, 6, 10);Date d2(d1);//调用拷贝构造,用d1初始化d2return 0;
}

3.若未显式定义,编译器会生成默认的拷贝构造函数。默认拷贝构造对于内置类型按照字节拷贝;对于自定义类型则调用它的拷贝构造函数。(这点与构造、析构函数一样)
4.编译器生成的默认拷贝构造函数只能完成浅拷贝也就是按照字节拷贝,涉及到资源申请时需要我们自己写深拷贝。

浅拷贝:通过默认的拷贝构造函数构造的对象,按字节完成拷贝,这种拷贝被称为浅拷贝或者值拷贝

对于日期类这种内置类型的成员,我们不需要显示定义拷贝构造,使用编译器默认生成的就可以。
在这里插入图片描述

但是涉及到动态开辟空间时,如果还使用浅拷贝,就会出现问题。

在这里插入图片描述

浅拷贝是按字节进行拷贝,所以s1和s2的指针内容是一样的,即指向同一块地址。所以其中一个修改,另一个也会修改。生命周期结束时,s2先析构,s1再析构,一块空间释放两次,就会出错。

因此,对于需要动态申请资源的类,我们就需要自己写深拷贝。

class Stack
{
public://构造函数Stack(int capacity = 4){_a = (int*)malloc(sizeof(int) * capacity);if (nullptr == _a){perror("malloc");return;}_top = 0;_capacity = capacity;}//深拷贝构造Stack(const Stack& st){_a = (int*)malloc(sizeof(int) * st._capacity);//重新开辟空间if (nullptr == _a){perror("malloc");return;}memcpy(_a, st._a, sizeof(int) * st._top);//内容拷贝过来_capacity = st._capacity;_top = st._top;}//析构函数~Stack(){if (nullptr != _a){free(_a);_a = nullptr;_capacity = _top = 0;}}
private:int* _a;int _top;int _capacity;
};int main()
{Stack s1;Stack s2(s1);//调用拷贝构造return 0;
}

拷贝构造函数的调用场景
1.使用已存在的对象创建新对象

Date d1(2024, 6, 10);//构造函数
Date d2(d1);//拷贝构造函数

2.函数参数类型为类的类型对象(传值调用,实参拷贝给形参)

在这里插入图片描述

所以我们自定义拷贝构造函数只能传引用,不可以传值,否则会引发无穷递归调用。因为如果拷贝构造的参数类型是类类型的对象(传值),则又会调用拷贝构造即调用自身,于是就会无限递归循环调用,程序会出错。

3.函数返回值类型为类类型对象(传值返回)

在这里插入图片描述

注意:不能返回局部对象的引用!
之前通过C++入门篇,我们知道,引用做返回值时是不可以返回局部对象的

在这里插入图片描述
可以看到结果是随机数,其实返回的结果可能是随机数也可能是正确结果。
因为是局部变量,函数结束,栈帧销毁,如果没有清理栈帧,那么返回结果侥幸是正确的;如果清理了栈帧,那么结果就是随机值。

函数参数是类类型的引用或者返回类型是类类型的引用时,是不会调用拷贝构造的,因此效率也比较高。为了提高程序效率,一般对象传参时,尽量使用引用类型;返回时根据实际场景,能用引用尽量使用引用。

四、赋值运算符重载

首先,什么是运算符重载?
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。

函数名:关键字operator后面加需要重载的运算符符号
运算符重载定义:返回值类型 operator操作符(参数列表)

例如,对于日期类,我们如何进行日期的大小比较呢?
先写一个判断日期是否相等的运算符重载函数。

bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}

判断是否相等的操作符==是双目运算符,所以参数列表有两个参数。因为传值传参会调用拷贝构造,降低效率,所以用传引用传参;因为不改变实参,所以最好加上const,提高代码健壮性。

但是我们如果重载成全局的,就需要类成员变量是公有属性,因为类外是无法访问私有成员的。但是这样无法保证封装性。

在这里插入图片描述
重载为全局函数,则调用方法如下
在这里插入图片描述

除了上述方法(不建议,封装性很差),我们嗨可以用友元来解决(下篇内容),或者重载为类的成员函数

在这里插入图片描述
可不能直接这些写,别忘了,每个成员函数都有一个隐藏的this指针,为第一个参数,也就是说,上述函数写法其实有三个参数。所以显示参数只需要写一个。正确写法
在这里插入图片描述
重载运算符的调用方法
在这里插入图片描述

以上就是运算符重载的使用方法和注意事项,我们再来写一下日期类的比较<、<=等运算符来练习一下。

bool operator<(const Date& d)
{if (_year == d._year){if (_month == d._month)return _day < d._day;elsereturn _month < d._month;}else{return _year < d._year;}
}

写<=的时候就可以调用前面写的<和==运算符,同理后面的>、>=、!=等都可以这样实现,自己可以练习实现一下。

bool operator<=(const Date& d)
{return *this < d || *this == d;
}

运算符重载的注意事项:

1.不能创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数(内置类型不需要操作符重载)
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5.>* :: sizeof ?: .这5个运算符不能重载。

赋值运算符重载

赋值运算符重载用于自定义类型,在两个及以上已存在的对象之间进行赋值。

Date d1(2024, 6, 1);
Date d2(2000, 1, 1);
d1 = d2;
Date d3, d4;
d4 = d3 = d1;//连续赋值

重载后应实现原本赋值运算符应实现的功能,赋值以及连续赋值等。所以赋值运算符重载是带有返回值的(支持连续赋值),返回引用和传引用可以提高返回效率,因为不用调用拷贝构造(拷贝构造调用的场景)。赋值前可以进行判断优化,相同就不用继续赋值。下面是日期类的赋值运算符重载。

Date& operator=(const Date& d)//传引用提高传参效率
{if (&d != this)//判断优化{_year = d._year;_month = d._month;_day = d._day;return *this;}
}

前面提到过,引用返回不可以返回临时对象,而*this就是对象本身,作用域在定义的域(此处定义在main()函数中)。

赋值运算符和拷贝构造的区别
赋值运算符:两个已经存在的对象之间进行赋值;

Date d1(2024, 6, 1);
Date d2(2024, 6, 1);
d2 = d1;
//或者
Date d1(2024, 6, 1);
Date d2, d3;
d2 = d3 = d1;//d2,d3是已存在的对象

拷贝构造:用一个已经存在的对象初始化新对象;

Date d1(2024, 6, 1);
Date d2(d1);
//或者
Date d1(2024, 6, 1);
Date d2 = d1;//d2是新创建对象

搞明白底层逻辑,就很容易区分了。
在这里插入图片描述

赋值运算符重载也是类的默认成员函数,我们不写,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。(浅拷贝)

和前面的拷贝构造一样,对于内置类型例如日期类这种,我们不需要显示定义赋值运算符重载,使用编译器默认生成的就可以了。但是涉及到资源申请的比如栈这种,浅拷贝就不可以了,需要我们自己显示定义赋值运算符重载完成深拷贝。(参考拷贝构造)

赋值运算符只能重载成类的成员函数不能重载成全局函数。

因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中默认生成的起冲突,所以赋值运算符重载只能是类的成员函数。

五、const成员

在介绍最后两个默认成员函数之前,我们先了解下什么是const成员。
const修饰的类成员函数称为const成员函数,const实际修饰的是隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

如果我们定义一个const的类对象d1,那么这个const对象还可以使用我们之前定义的函数吗?

在这里插入图片描述不可以,之前在C++入门篇中提过,权限不可以放大,只能平移或者缩小。上述Print()函数中有一个隐藏的Date*类型的this指针,而d1是const对象,所以传的是const Date*类型的this指针,所以权限会放大,因此报错。

cosnt成员函数只需要在原本成员函数后面加上cons即可,这样const对象就可以正常调用。

在这里插入图片描述

const对象不可以调用非const成员函数(权限放大
const成员函数不可以调用其他非成员函数(权限放大
非const对象可以调用const成员函数(权限缩小
非const成员函数可以调用其他非const成员函数(权限缩小

总结: 不修改成员变量的成员函数,最好都加上const修饰成const成员函数,这样const对象和非const对象都可以调用。

六、取地址及const取地址操作符重载

这两个默认成员函数基本上很少需要我们自己去实现,一般不需要重载,使用编译器默认生成的即可,除非想让别人通过取地址操作符获取到指定的内容

class Date
{
public:Date* operator&()//取地址操作符重载{return this;}const Date* operator&()const//const取地址操作符重载{return this;}
private:int _year;int _month;int _day;
};

这篇关于【C++】类的六个默认成员函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1082133

相关文章

关于C++中的虚拟继承的一些总结(虚拟继承,覆盖,派生,隐藏)

1.为什么要引入虚拟继承 虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下: class A class B1:public virtual A; class B2:pu

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

C++的模板(八):子系统

平常所见的大部分模板代码,模板所传的参数类型,到了模板里面,或实例化为对象,或嵌入模板内部结构中,或在模板内又派生了子类。不管怎样,最终他们在模板内,直接或间接,都实例化成对象了。 但这不是唯一的用法。试想一下。如果在模板内限制调用参数类型的构造函数会发生什么?参数类的对象在模板内无法构造。他们只能从模板的成员函数传入。模板不保存这些对象或者只保存他们的指针。因为构造函数被分离,这些指针在模板外

C++工程编译链接错误汇总VisualStudio

目录 一些小的知识点 make工具 可以使用windows下的事件查看器崩溃的地方 dumpbin工具查看dll是32位还是64位的 _MSC_VER .cc 和.cpp 【VC++目录中的包含目录】 vs 【C/C++常规中的附加包含目录】——头文件所在目录如何怎么添加,添加了以后搜索头文件就会到这些个路径下搜索了 include<> 和 include"" WinMain 和

C/C++的编译和链接过程

目录 从源文件生成可执行文件(书中第2章) 1.Preprocessing预处理——预处理器cpp 2.Compilation编译——编译器cll ps:vs中优化选项设置 3.Assembly汇编——汇编器as ps:vs中汇编输出文件设置 4.Linking链接——链接器ld 符号 模块,库 链接过程——链接器 链接过程 1.简单链接的例子 2.链接过程 3.地址和

C++必修:模版的入门到实践

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:C++学习 贝蒂的主页:Betty’s blog 1. 泛型编程 首先让我们来思考一个问题,如何实现一个交换函数? void swap(int& x, int& y){int tmp = x;x = y;y = tmp;} 相信大家很快就能写出上面这段代码,但是如果要求这个交换函数支持字符型

【操作系统】信号Signal超详解|捕捉函数

🔥博客主页: 我要成为C++领域大神🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞👍收藏⭐评论✍️ 本博客致力于知识分享,与更多的人进行学习交流 ​ 如何触发信号 信号是Linux下的经典技术,一般操作系统利用信号杀死违规进程,典型进程干预手段,信号除了杀死进程外也可以挂起进程 kill -l 查看系统支持的信号

C++入门01

1、.h和.cpp 源文件 (.cpp)源文件是C++程序的实际实现代码文件,其中包含了具体的函数和类的定义、实现以及其他相关的代码。主要特点如下:实现代码: 源文件中包含了函数、类的具体实现代码,用于实现程序的功能。编译单元: 源文件通常是一个编译单元,即单独编译的基本单位。每个源文件都会经过编译器的处理,生成对应的目标文件。包含头文件: 源文件可以通过#include指令引入头文件,以使

java中查看函数运行时间和cpu运行时间

android开发调查性能问题中有一个现象,函数的运行时间远低于cpu执行时间,因为函数运行期间线程可能包含等待操作。native层可以查看实际的cpu执行时间和函数执行时间。在java中如何实现? 借助AI得到了答案 import java.lang.management.ManagementFactory;import java.lang.management.Threa

SQL Server中,isnull()函数以及null的用法

SQL Serve中的isnull()函数:          isnull(value1,value2)         1、value1与value2的数据类型必须一致。         2、如果value1的值不为null,结果返回value1。         3、如果value1为null,结果返回vaule2的值。vaule2是你设定的值。        如