C++自动定义的成员函数

news2024/9/22 23:33:12

C++自动提供了下面这些成员函数:

  • 默认构造函数,如果没有定义构造函数
  • 默认析构函数,如果没有定义
  • 复制构造函数,如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符,如果没有定义

另有移动构造函数和移动赋值运算符

将一个对象赋给另一个对象,编译器将提供赋值运算符的定义,地址运算符的定义,自动生成复制构造函数,因为它创建对象的一个副本

1.默认构造函数

如果没有提供任何构造函数,C++将创建默认构造函数。例如,假如定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下述默认构造函数:

Klunk::Klunk(){}   //implicit default constructor

也就是说,编译器将提供一个不接受任何参数,也不执行任何操作的构造函数(默认的默认构造函数),这是因为创建对象时总是会调用构造函数:

Klunk lunk;  // invokes default constructor

默认构造函数使Lunk类似于一个常规的自动变量,也就是说,它的值在初始化是未知的。
如果定义了构造函数,C++将不会定义默认构造函数。如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。这种构造函数没有任何参数,但可以使用它来设置特定的值:

Klunk::Klunk() //explicit default constructor
{
	klunk_ct  = 0;
	...
}   

带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。例如,Klunk类可以包含下述内联构造函数:

Klunk(int n = 0) {klunk_ct = n;}

但只能有一个默认构造函数。也就是说,不能这样做:

Klunk() {klunk_ct = n;}     // constructor  #1
Klunk(int n = 0) {klunk_ct = n;}  // ambiguous constructor #2 

这为何有二义性呢?请看下面两个声明:

Klunk kar(10);     // clearly matches Klunk(int n)
Klunk bus;  // could match either constructor  

第二个声明既与构造函数#1(没有参数)匹配,也与构造函数#2(使用默认参数0)匹配。这将导致编译器发出一条错误信息。

2.复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:

Class_name(const Class_name &);

它接受一个指向类对象的常量引用作为参数。例如,String类的复制构造函数的原型如下:

StringBad (const StringBad &);

对于复制构造函数,需要知道两点:何时调用和有何功能。

3.何时调用复制构造函数

新建一个对象并将其初始化为同类现有对象时, 复制构造函数都将被调用。 这在很多情况下都可能发生, 最常见的情况是将新对象显式地初始化为现有的对象。 例如, 假设motto是一个StringBad对象, 则下面4种声明都将调用复制构造函数:

在这里插入图片描述
其中中间的2种声明可能会使用复制构造函数直接创建metoo和also, 也可能使用复制构造函数生成一个临时对象, 然后将临时对象的内容赋给metoo和also, 这取决于具体的实现。 最后一种声明使用motto初始化一个匿名对象, 并将新对象的地址赋给pstring指针。
每当程序生成了对象副本时, 编译器都将使用复制构造函数。 具体地说, 当函数按值传递对象(如程序清单12.3中的callme2()) 或函数返回对象时, 都将使用复制构造函数。 记住, 按值传递意味着创建原始变量的一个副本。 编译器生成临时对象时, 也将使用复制构造函数。 例如, 将3个Vector对象相加时, 编译器可能生成临时的Vector对象来保存中间结果。 何时生成临时对象随编译器而异, 但无论是哪种编译器, 当按值传递和返回对象时, 都将调用复制构造函数。 具体地说, 程序清单12.3中的函数调用将调用下面的复制构造函数:

callme2(headline2);

程序使用复制构造函数初始化sb——callme2()函数的StringBad型形参。
由于按值传递对象将调用复制构造函数, 因此应该按引用传递对象。 这样可以节省调用构造函数的时间以及存储新对象的空间。

4.默认的复制构造函数的功能

默认的复制构造函数逐个复制非静态成员( 成员复制也称为浅复制) , 复制的是成员的值。 在程序清单12.3中, 下述语句:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
程序输出如下:
在这里插入图片描述

StringBad sailor = sports;

与下面的代码等效( 只是由于私有成员是无法访问的, 因此这些代码不能通过编译) :

StringBad sailor;
StringBad.str = sports.str;
StringBad.len = sports.len;

如果成员本身就是类对象, 则将使用这个类的复制构造函数来复制成员对象。 静态函数( 如num_strings) 不受影响, 因为它们属于整个类, 而不是各个对象。 图12.2说明了隐式复制构造函数执行的操作。
在这里插入图片描述
二、回到Stringbad: 复制构造函数的哪里出了问题

现在介绍程序清单12.3的两个异常之处(假设输出为该程序清单后面列出的) 。 首先, 程序的输出表明, 析构函数的调用次数比构造函数的调用次数多2, 原因可能是程序确实使用默认的复制构造函数另外创建了两个对象。 当callme2()被调用时, 复制构造函数被用来初始化callme2()的形参, 还被用来将对象sailor初始化为对象sports。 默认的复制构造函数不说明其行为, 因此它不指出创建过程, 也不增加计数器num_strings的值。 但析构函数更新了计数, 并且在任何对象过期时都将被调用, 而不管对象是如何被创建的。 这是一个问题, 因为这意味着程序无法准确地记录对象计数。 解决办法是提供一个对计数进行更新的显式复制构造函数:

StringBad::StringBad(const String & s)
{
	num_strings++;
	...//import stuff to go here
}

提示:

如果类中包含这样的静态数据成员, 即其值将在新对象被创建时发生变化, 则应该提供一个显式复制构造函数来处理计数问题

第二个异常之处更微妙, 也更危险, 其症状之一是字符串内容出现乱码:
在这里插入图片描述
原因在于隐式复制构造函数是按值进行复制的。 例如, 对于程序清单12.3, 隐式复制构造函数的功能相当于:

sailor.str = sport.str;

这里复制的并不是字符串, 而是一个指向字符串的指针。 也就是说, 将sailor初始化为sports后, 得到的是两个指向同一个字符串的指针。 当operator <<()函数使用指针来显示字符串时, 这并不会出现问题。 但当析构函数被调用时, 这将引发问题。 析构函数StringBad释放str指针指向的内存, 因此释放sailor的效果如下:

delete [] sailor.str;  // delete the string that ditto.str points to

sailor.str指针指向“Spinach Leaves Bowl for Dollars”,因为它被赋值为sport.str,而sport.str指向的正是上述字符串。所以delete语句将释放字符串“Spinach Leaves Bowl for Dollars”占用的内存。

然后, 释放sports的效果如下:

delete [] sport.str;  // effect is undefined

sports.str指向的内存已经被sailor的析构函数释放, 这将导致不确定的、 可能有害的后果。 程序清单12.3中的程序生成受损的字符串, 这通常是内存管理不善的表现。

另一个症状是, 试图释放内存两次可能导致程序异常终止。 例如,Microsoft Visual C++ 2010(调试模式) 显示一个错误消息窗口, 指出“Debug Assertion Failed!”; 而在Linux中, g++ 4.4.1显示消息“double free or corruption”并终止程序运行。 其他系统可能提供不同的消息, 甚至不提供任何消息, 但程序中的错误是相同的。

1. 定义一个显式复制构造函数以解决问题

解决类设计中这种问题的方法是进行深度复制(deep copy) 。 也就是说, 复制构造函数应当复制字符串并将副本的地址赋给str成员, 而不仅仅是复制字符串地址。 这样每个对象都有自己的字符串, 而不是引用另一个对象的字符串。 调用析构函数时都将释放不同的字符串, 而不会试图去释放已经被释放的字符串。 可以这样编写String的复制构造函数:

StringBad::StringBad(const StringBad & st)
{
	num_strings++;   //handle static member update
	len = st.len;    //same length
	str = new char [len + 1]; //allot space
	std::strcpy(str,st.str); //copy string to new location
	cout << num_string << ":\" "<< str
			<< "\"object created\n";//For Your Information
}

在这里插入图片描述
必须定义复制构造函数的原因在于, 一些类成员是使用new初始化的、 指向数据的指针, 而不是数据本身。 图12.3说明了深度复制。
在这里插入图片描述
警告:

如果类中包含了使用new初始化的指针成员, 应当定义一个复制构造函数, 以复制指向的数据, 而不是指针, 这被称为深度复制。 复制的另一种形式(成员复制或浅复制) 只是复制指针值。 浅复制仅浅浅地复制指针信息, 而不会深入“挖掘”以复制指针引用的结构。

三 Stringbad的其他问题: 赋值运算符

并不是程序清单12.3的所有问题都可以归咎于默认的复制构造函数, 还需要看一看默认的赋值运算符。 ANSIC允许结构赋值, 而C++允许类对象赋值, 这是通过自动为类重载赋值运算符实现的。 这种运算符的原型如下:

Class_name & Class_name::operator = (const Class_name &);

接受并返回一个指向类对象的引用。 例如, StringBad类的赋值运算符的原型如下:

StringBad & StringBad::operator = (const StringBad &);

1. 赋值运算符的功能以及何时使用它

  • 将已有的对象赋给另一个对象时, 将使用重载的赋值运算符:
StringBad headline1("Celery Stalks at Midnight");
...
StringBad knot;
knot = headline1;   // assignment operator invoked
初始化对象时,并不一定会使用赋值运算符:
StringBad metoo = knot; // use copy constructor,possibly assignment ,too

这里,metoo是一个新创建的对象,被初始化为knot的值, 因此使用复制构造函数。 然而, 正如前面指出的, 实现时也可能分两步来处理这条语句: 使用复制构造函数创建一个临时对象, 然后通过赋值将临时对象的值复制到新对象中。 这就是说, 初始化总是会调用复制构造函数, 而使用=运算符时也可能调用赋值运算符

与复制构造函数相似, 赋值运算符的隐式实现也对成员进行逐个复制。 如果成员本身就是类对象, 则程序将使用为这个类定义的赋值运算符来复制该成员, 但静态数据成员不受影响

2. 赋值的问题出在哪里

  • 程序清单12.3将headline1赋给knot:
knot = headline1;

为knot调用析构函数时, 将显示下面的消息:

在这里插入图片描述
为Headline1调用析构函数时, 显示如下消息( 有些实现方式在此之前就异常终止了) :

在这里插入图片描述

出现的问题与隐式复制构造函数相同: 数据受损。 这也是成员复制的问题, 即导致headline1.str和knot.str指向相同的地址。 因此, 当对knot调用析构函数时, 将删除字符串“Celery Stalks at Midnight”; 当对headline1调用析构函数时, 将试图删除前面已经删除的字符串。 正如前面指出的, 试图删除已经删除的数据导致的结果是不确定的, 因此可能改变内存中的内容, 导致程序异常终止。 要指出的是, 如果操作结果是不确定的, 则执行的操作将随编译器而异, 包括显示独立声明( Declaration of Independence) 或释放隐藏文件占用的硬盘空间。 当然, 编译器开发人员通常不会花时间添加这样的行为。

3. 解决赋值的问题

对于由于默认赋值运算符不合适而导致的问题, 解决办法是提供赋值运算符( 进行深度复制) 定义。 其实现与复制构造函数相似, 但也有一些差别。

  • 由于目标对象可能引用了以前分配的数据, 所以函数应使用delete[]来释放这些数据。
  • 函数应当避免将对象赋给自身; 否则, 给对象重新赋值前, 释放内
    存操作可能删除对象的内容。
  • 函数返回一个指向调用对象的引用。

通过返回一个对象, 函数可以像常规赋值操作那样, 连续进行赋值, 即如果S0、 S1和S2都是StringBad对象, 则可以编写这样的代码:

S0 = S1 = S2;

使用函数表示法时, 上述代码为:

S0.operator=(s1.operator=(S2));

因此, S1.operator=(S2) 的返回值是函数S0.operator=()的参数。因为返回值是一个指向StringBad对象的引用, 因此参数类型是正确的。
下面的代码说明了如何为StringBad类编写赋值运算符:

StringBad & StringBad::operator=(const StringBad & st)
{
	if (this == &st)      // object assigned to itself
		return *this;     // all done
	delete [] str;        // free old string
	len = st.len;
	str = new char [len + 1]; //get sapce for new string
	std::strcpy(str,st.str);  //copy the string
	return *this;             // return reference to invoking object
}
代码首先检查自我复制, 这是通过查看赋值运算符右边的地址(&s) 是否与接收对象(this) 的地址相同来完成的。 如果相同, 程序将返回*this, 然后结束。 第10章介绍过, 赋值运算符是只能由类成员函数重载的运算符之一。
如果地址不同, 函数将释放str指向的内存, 这是因为稍后将把一个新字符串的地址赋给str。 如果不首先使用delete运算符, 则上述字符串

将保留在内存中。 由于程序中不再包含指向该字符串的指针, 因此这些内存被浪费掉。
接下来的操作与复制构造函数相似, 即为新字符串分配足够的内存空间, 然后将赋值运算符右边的对象中的字符串复制到新的内存单元中。
上述操作完成后, 程序返回*this并结束。
赋值操作并不创建新的对象, 因此不需要调整静态数据成员num_strings的值。
将前面介绍的复制构造函数和赋值运算符添加到StringBad类中后,所有的问题都解决了。 例如, 下面是在完成上述修改后, 程序输出的最后几行:
在这里插入图片描述
现在, 对象计数是正确的, 字符串也没有被损坏。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

业余时间可以做什么副业,什么副业可以赚钱

大家好&#xff0c;我是蝶衣王的小编 现在的年轻人大多是过着朝九晚五的社畜生活&#xff0c;但是朝九晚五就意味着工资可能不是很高&#xff0c;生活压力会比较大&#xff0c;很多人就会想能不能利用业余时间做一些副业呢​&#xff1f;答案当然是可以的 那么&#xff0c;什…

售前工程师工作内幕揭秘:面试实战技巧

售前工程师工作内幕揭秘&#xff1a;面试实战技巧前言一、售前面试问题&#xff0c;基本就下面这些二、售前工程师岗位普遍误区三、售前工程师核心技能四、面试中&#xff0c;主动出击&#xff0c;才是王道五、对行业的了解是做好售前的基础前言 看到网上很多关于售前工程师面…

git分支上的tag

在发布一个版本时&#xff0c;我们通常先在版本库中打一个标签&#xff0c;这样&#xff0c;就唯一确定了打标签时刻的版本。将来无论什么时候&#xff0c;取某个标签的版本&#xff0c;就是把那个打标签的时刻的历史版本取出来。所以&#xff0c;标签也是版本库的一个快照。在…

Spring的创建与使用

⭐️前言⭐️ 在了解了Spring的核心与设计思想以后,下边就是Spring的具体使用&#xff0c;这篇文章主要介绍Spring项目的创建和Bean对象的存放与取出。 &#x1f349;博客主页&#xff1a; &#x1f341;【如风暖阳】&#x1f341; &#x1f349;精品Java专栏【JavaSE】、【备…

Kong(二)通过案例快速了解使用

一 Kong安装目录结构的说明 后续看看里面有啥 /usr/local/bin --> kong命令的路径/etc/kong/ --> kong默认会寻找配置文件/usr/local/kong --> Kong的日志 -->/usr/local/kong/logs/usr/local/lib/lua/5.1/usr/local/lib/…

[附源码]Python计算机毕业设计Django人事系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

【Leetcode】拿捏链表(五)——138. 复制带随机指针的链表

作者&#xff1a;一个喜欢猫咪的的程序员 专栏&#xff1a;《Leetcode》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 138. 复制带随机指针的链表 138. 复制带随机指针的链表 138. 复制带随…

MySQL8.0 OCP最新版1Z0-908认证考试题库整理-005

原题 Choose four.A newly deployed replication master database has a 10/90 read to write ratio.The complete dataset is currently 28G but will never fluctuate beyond -10%.The database storage system consists of two locally attached PCI- E Enterprise grade di…

单纯形法的补充与代码实现

线性规划中&#xff0c;我们介绍了三种求解算法——单纯形法、对偶理论和内点法。传送门&#xff1a;线性规划之单纯形法 线性规划的对偶理论 线性规划之内点法其中单纯形法要建立在标准型上&#xff0c;并且开始迭代要求有一个基本可行解。如果系数矩阵A规模较大&#xff0c;有…

阿里云OSS依赖无法导入的问题

版本背景&#xff1a;springboot:2.4.12&#xff0c;spring-cloud:2020.0.1 在使用阿里云对象存储OSS服务时候&#xff0c;根据官方参考文档&#xff1a;aliyun-spring-boot/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample at master alibaba/aliyun-spring-boot…

第十五章 图的BFS与拓扑序列

图的BFS与拓扑序列一、图的BFS1、思路2、模板&#xff08;1&#xff09;问题&#xff08;2&#xff09;代码模板&#xff08;3&#xff09;代码解析二、拓扑序列引入&#xff1a;1、什么是拓扑序列&#xff1f;2、模板&#xff1a;&#xff08;1&#xff09;问题&#xff1a;&a…

一张图搞懂微服务架构设计

前言 当前&#xff0c;微服务架构在很多公司都已经落地实施了&#xff0c;下面用一张图简要概述下微服务架构设计中常用组件。不能说已经使用微服务好几年了&#xff0c;结果对微服务架构没有一个整体的认知&#xff0c;一个只懂搬砖的程序员不是一个好码农! 流量入口Nginx 在…

Awesome Uplift Modeling【如何学习因果推断、因果机器学习和Uplift建模?All in here】

Awesome-Uplift-Model How to Apply Causal ML to Real Scene Modeling&#xff1f;How to learn Causal ML&#xff1f; Github项目地址&#xff1a;&#x1f449;https://github.com/JackHCC/Awesome-Uplift-Model&#x1f448; &#x1f449;https://github.com/JackHCC/…

汇编原理理论知识复习

书上重点内容 本篇博客整理老师课上强调的重点理论知识&#xff0c;以便复习备考&#xff0c;如有错误欢迎指正。 这门课主要讲CPU芯片与其他芯片&#xff08;内存芯片和I/O接口芯片&#xff09;之间交互。 一条指令的执行过程&#xff1a;取指&#xff08;从主存取到CPU寄…

(五)Vue之data与el的两种写法

文章目录el的两种写法data的两种写法Vue学习目录 上一篇&#xff1a;&#xff08;四&#xff09;Vue之数据绑定 容器&#xff1a; <div id"root"><h1>hello,{{name}}</h1></div>el的两种写法 (1).new Vue时候配置el属性。 new Vue({el:#r…

【C语言航路】第六站:指针初阶

目录 一、指针是什么 二、指针和指针类型 1.指针类型的意义 2.指针-整数 3.指针解引用 三、野指针 1.野指针的成因 &#xff08;1&#xff09;指针未初始化 &#xff08;2&#xff09;指针越界访问 &#xff08;3&#xff09;指针指向的空间释放 2.如何规避野指针 &a…

伸手运动想象训练与伸手抓取想象的关系

本研究旨在确定为期4周的目标导向性伸手&#xff08;抓取任务&#xff09;的运动想象训练&#xff08;MIT&#xff09;是否会以相同的方式影响伸手&#xff08;MIR&#xff09;和抓取&#xff08;MIG&#xff09;运动想象的皮质活动。试验过程中&#xff0c;我们在健康的年轻参…

基于未知环境下四旋飞行器运动规划应用研究(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

QT QDoubleSpinBox 浮点计数器控件(使用详解)

本文详细的介绍了QDoubleSpinBox控件的各种操作&#xff0c;例如&#xff1a;新建界面、获取数值、设置前后缀、设置最大/小值、设置显示精度、关联信号槽、优化信号、关联控件、文件源码、样式表等等操作。 本文是QT控件使用详解的第十五篇 QT QDoubleSpinBox 浮点计数器控件(…

【ArcGIS风暴】ArcGIS栅格影像去除黑边(背景值)方法汇总

文章目录 1. 数据加载时属性中设置去除黑边2. 应用setnull工具去除黑边3. 应用栅格计算器去除黑边4. 应用复制栅格工具去除黑边5. 应用影像分析去除黑边6. 应用镶嵌数据集去除黑边影像产生黑边的原因无外乎在设置无效值时,将无效值设成了0,而影像在导入软件进行渲染时,并没有…