strcpy函数_错误更正(拷贝赋值函数的正确使用姿势)

这是一篇对什么是C++的The Rule of Three的错误更正和详细说明。

阅读时间7分钟。难度⭐⭐⭐

d2ae8d894c9d5ed2f1ecbc94410cbc46.png

虽然上一篇文章的阅读量只有凄惨的两位数e8ed25c9ca7b7b4da45247666c85dae0.gif,但是怀着对小伙伴负责的目的,必须保证代码的正确性。这是大厨做技术自媒体的态度。

前文最后一段代码是这样的:

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     name = new char[strlen(that.name)+1];
     strcpy(name, that.name);
     age = that.age;
   }
   '...省略析构函数...'
};

先不谈异常安全,这段拷贝赋值函数的代码本身有什么问题?

有3个问题:

  • 没有释放原对象指针成员指向的内容

  • 没有返回值

  • 没有自赋值检查

下面我们一个一个分析。

 1   没有释放原对象指针

这个问题很严重,因为一定会造成内存泄露

原因是指针所指的内存未被释放,而指针又指向了别处。

例子如下,我们写了一个main函数,长这样:

int main(int argc, char* argv[]) {
    Dog D1("Bobby", 2);
    Dog D2("Teddy", 3);
    Dog D2 = D1;
}

D1和D2分别是是Dog的对象。根据构造函数的定义,D2中的name指针指向了字符数组“Teddy”。而当进行D2 = D1操作时,name = new char[strlen(that.name)+1]这一步会在D2中重新创建一个名字为name且指向“Bobby”的指针。

这么做也许编译器不会报错,但是会有问题。

因为在new一个name指针之前,原本的name指针指向的内存并没有被释放。而新的name指针只对新创建的内存负责,老的内存已经变成无主之地。看来内存泄露是逃不掉了。

这个问题看着复杂,解决的办法倒是简单,只需要在拷贝赋值函数体第一行加上 delete[] name就可以了。

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     delete[] name; //释放原对象指针成员指向的内容
     name = new char[strlen(that.name)+1];
     strcpy(name, that.name);
     age = that.age;
   }
   '...省略析构函数...'
};
2   没有返回值

第二个问题犯的错很低级b530a02792ba4a9a483e332040a47170.png,拷贝赋值函数的行为和普通函数一样需要一个返回值。而返回值的类型通常是类的对象的引用。

参照常用的写法,这里返回*this(this是C++类的隐藏成员,表示对象本身)。

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     delete[] name; //释放原对象指针成员指向的内容
     name = new char[strlen(that.name)+1];
     strcpy(name, that.name);
     age = that.age;
     return *this; //返回对象引用
   }
   '...省略析构函数...'
};

另外大家可能有疑问为什么返回值是一个引用而不是一个值呢?

答案是只有引用才能进行连续赋值。

假设有3个Dog对象:D1、D2、D3,如果返回值不是引用,那么类似D1 = D2 = D3将不能通过编译。

 3   没有自赋值检查

什么叫做自赋值?

就是两个相同对象之间用等号连接,比如:

int main(int argc, char* argv[]) {
    Dog D1("Bobby", 2);
    Dog D1 = D1; //同一个D1相互赋值
}

当然,一般不会有人写出这样的代码来。这里只是举个简单的例子,但是如果在大型项目中不同开发者对同一对象取了不同的别名,那么自赋值的情况是有可能发生的。

对于上面的Dog类而言,如果执行D1 = D1,那么会发生下面的事情:

首先,对象D1中的name指针被析构,name指向的内存被释放;

然后,下一行中的strlen(that.name)又用到了D1的name所指向的内存。

重点来了:这时你会惊讶地发现编译器提示你name已经不存在了!!!

5561f274749912e9a46bc7730ffb8541.gif

因为在编译器看来,你在做对同一对象先释放了内存再使用的非法事情!

就好比你是拆迁大队的,你没有确认拆的是不是自己的房子就不管三七二十一直接拆了,然而你晚上还要回家住......

0080f35ac95f4c86aed8d1df71ba601b.gif

C++真的烧脑,仅仅是不小心把自己赋值给了自己就把自己的一部分给搞丢了,这在其他语言中似乎是天方夜谭。但是C++似乎很情愿把事情搞复杂。

幸好,自赋值问题也很容易修复,只需要在delete指针之前做一个自赋值的判断。

完整代码如下:

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     if(this != &that) { //判断是否自赋值
         delete[] name; //释放原对象的指针指向的内容
         name = new char[strlen(that.name)+1];
         strcpy(name, that.name);
         age = that.age;
     }
     return *this;
   }
   '...省略析构函数...'
};

this != &that这个判断的写法看上去莫名其妙,大厨来给大家分析一下:

this代表D1=D1中等号左边的D1,&that代表等号右边的D1的引用(本质上还是D1)。this和&that二者如果相等就说明是同一个对象,那么拷贝赋值函数就直接返回对象的引用。

至此,三个问题终于都解决了ec71b8812d70d7797fba6e819f193a5c.png

 4   总结时刻

通过以上问题的剖析可以发现,C++一大半奇奇怪怪行为的背后都有一个处理不当的指针。

另外,写一个正确的类真的一点都不简单,需要考虑内存泄露,返回值类型,自赋值等等情况。

打住,再说下去大厨真的转行成C++专业劝退师了。

98bd8ddea5826b57827661d8ef0864e4.png

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

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

相关文章

将Java应用程序打包为一个(或胖)JAR

这篇文章的目标是一个有趣但非常强大的概念:将应用程序打包为单个可运行的JAR文件,也称为一个或胖 JAR文件。 我们习惯了大型WAR归档文件,其中包含所有打包在某些公用文件夹结构下的依赖项。 使用类似于JAR的打包,情况有所不同&a…

学习java的第三天,猜字符的小程序

关于猜字符的小程序 主要实现:随机输出5个字母,用户输入猜测的字母,进行对比得出结果 主要有3个方法:主方法main(); 产生随机字符的方法generate(); 比较用户输入的字符与随机产生的字符的方法check()&…

《Linux命令行与shell脚本编程大全 第3版》创建实用的脚本---10

以下为阅读《Linux命令行与shell脚本编程大全 第3版》的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下:转载于:https://www.cnblogs.com/guochaoxxl/p/7894995.html

python安装包找不到setup_如何安装没有setup.py的Python模块?

在系统上开始使用该代码的最简单的方法是:>将文件放入机器上的目录中,>将该目录的路径添加到您的PYTHONPATH步骤2可以从Python REPL完成如下:import syssys.path.append("/home/username/google_search")您的文件系统的外观示例&#xf…

Spring Batch中面向TaskletStep的处理

许多企业应用程序需要批处理才能每天处理数十亿笔交易。 必须处理这些大事务集,而不会出现性能问题。 Spring Batch是一个轻量级且强大的批处理框架,用于处理这些大数据集。 Spring Batch提供了“面向TaskletStep”和“面向块”的处理风格。 在本文中&a…

布局中常见的居中问题

说到布局除了浮动以及定位外还有一个不得不提的点,那就是居中,居中问题我们在网页布局当中经常遇到,那么以下就是分为两部分来讲,一部分是传统的居中,另一种则是flex居中,每个部分又通过分为水平垂直居中来…

unity json解析IPA后续

以前说到的,有很大的限制,只能解析简单的类,如果复杂的就会有问题,从老外哪里看到一片博客,是将类中的list 等复杂对象序列化, using UnityEngine; using System.Collections; using System.Collections.…

改善代码质量之内连临时变量

待增转载于:https://www.cnblogs.com/muyl/articles/6940896.html

python 矩阵元素相加_Numpy中元素级运算

标量与矩阵的运算:加法:values [1,2,3,4,5]values np.array(values) 5#现在 values 是包含 [6,7,8,9,10] 的一个 ndarray乘法:x np.multiply(some_array, 5)x some_array * 5矩阵与矩阵的运算:加法:对应元素相加,但形状必须相…

排序算法——桶排序

把数据放进若干个桶,然后在桶里用其他排序,近乎分治思想。从数值的低位到高位依次排序,有几位就排序几次。例如二位数就排两次,三位数就排三次,依次按照个十百...的顺序来排序。 第一次排序:50 12 …

原型设计模式:创建另一个小车

创建对象确实是一个耗时的过程,也是一件昂贵的事情。 因此,我们现在正努力节省时间和金钱。 我们该怎么做? 克隆奇迹多莉 有人记得多莉吗? 是的,是绵羊,是第一个被克隆的哺乳动物。 好吧,我不想…

java实现周期任务_java定时任务的实现方式

本文列举常见的java定时任务实现方式,并做一定比较。1. 循环内部sleep实现周期执行创建一个thread,run() while循环里sleep()来实现周期性执行; 简单粗暴,作为一个初学者很容易想到。public class Task1 {public static void main(String[] a…

磁盘镜像工具Guymager

磁盘镜像工具Guymager在数字取证中,经常需要对磁盘制作镜像,以便于后期分析。Kali Linux提供一款轻量级的磁盘镜像工具Guymager。该工具采用图形界面化方式,提供磁盘镜像和磁盘克隆功能。它不仅生成dd的镜像,还能生成EWF和AFF镜像…

python怎么写代码求年华收益率_如何计算年化收益率?

关于投资年化收益率的计算,三思君觉得主要有三种,分别是一次性投资的收益率计算、定期定额的收益率计算、不定期不定额的收益率计算。1.一次性投资的收益率计算对于一次性投资的收益率,相信大家都会计算。比如,小李同学去年买了一…

HTTPS协议在Tomcat中启用的配置

本文将讲解HTTPS协议在Tomcat中启用是如何配置的。 概念简介 Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试 JSP 程序的首选。 HTTP 超文本…

CSS3实现烟花特效 --web前端

烟花特效&#xff0c;比较简单&#xff0c;直接贴代码了……<!DOCTYPE html><html lang"en"><head> <meta charset"UTF-8"> <title>css3实现烟花特效</title> <style> * { margin: 0; padding: 0; } html{ widt…

虚拟机 java 开发_深入浅出 Java 虚拟机 · 通往高级 Java 开发的必经之路

第一章 JVM 内存模型Java 虚拟机(Java Virtual MachineJVM)的内存空间分为五个部分&#xff0c;分别是&#xff1a;程序计数器Java 虚拟机栈本地方法栈堆方法区。下面对这五个区域展开深入的介绍。1.1 程序计数器1.1.1 什么是程序计数器&#xff1f;程序计数器是一块较小的内存…

java.lang.ClassNotFoundException:如何解决

本文适用于当前面临java.lang.ClassNotFoundException挑战的Java初学者。 它将为您提供此常见Java异常的概述&#xff0c;这是一个示例Java程序&#xff0c;可支持您的学习过程和解决策略。 如果您对与更高级的类加载器相关的问题感兴趣&#xff0c;我建议您复习有关java.lang…

iOS10 App跳转到系统设置

实现类似万能钥匙中点击一个Wi-Fi跳转到系统Wi-Fi设置界面的功能。 NSString * urlString "App-Prefs:rootWIFI";if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:urlString]]) {if ([[UIDevice currentDevice].systemVersion doubleValue…

python生成器 图片分类_python批量处理图片图片Python迭代器和生成器介绍

Python迭代器和生成器介绍迭代器迭代器是一个实现了迭代器协议的对象&#xff0c;Python中的迭代器协议就是有next方法的对象会前进到下一结果&#xff0c;而在一系列结果的末尾是&#xff0c;则会引发StopIteration。在for循环中&#xff0c;Python将自动调用工厂函数iter()获…