前言

内容主要是深浅复制、复制构造函数以及赋值运算符的问题。

先从一段简单的代码开始:

#include <iostream>
#include <string.h>
using namespace std;class Student
{private:char *name;int number;public:Student(char *na, int n);~Student();setnumber(int n);setname(char *na);void Print();
};Student::Student(char *na, int n)
{name = new char[strlen(na)+1];strcpy(name,  na);number = n;cout << "Student:" << (int)name << "   Number:" << number <<endl;
}Student::~Student()
{cout << "~Student:" << (int)name << "   Number:" << number <<endl;delete name;
}Student::setnumber(int n)
{number = n;
}Student::setname(char *na)
{strcpy(name,  na);
}void Student::Print()
{cout << "student:" << name << "  " << "Number:" << number <<endl;
}int main()
{Student s1((char*)"Tom", 123);Student s2(s1);//复制对象cout <<endl;s2.setname((char*)"Bob");s2.setnumber(456);s1.Print();s2.Print();cout <<endl;return 0;
}

细细分析一下上述代码中的 "TOM" 为什么会变成 "Bob"  ,可以发现当用s1去初始化s2时,s1的name的地址也复制给了s2。那么自然而然,当修改s2的name时,s1的name也跟着变化。这对Student对象的使用造成了麻烦,而且还会带来别的问题。

这里先给出解决办法:

Student::Student(const Student &st)
{int len = strlen(st.name);name = new char[len + 1];strcpy(name, st.name);
}

至于为什么这么做,我们需要接着看下面的内容。

上面的程序只是在抛砖引玉,接下来的内容是《C++ Primer Plus》中给出的程序代码。通过下面的代码,能让我们发现很多潜在的问题。

#include <iostream>
#include <cstring>
#include <string.h>
using namespace std;class StringBad
{
private:char *str;int len;static int num_strings;
public:StringBad(const char *s);StringBad();~StringBad();friend std::ostream & operator << (std::ostream & os, const StringBad & st);
};int StringBad::num_strings = 0;StringBad::StringBad(const char *s)
{len = strlen(s);str = new char[len + 1];strcpy(str, s);num_strings++;cout << num_strings << ": \"" << str << "\" object created\n";
}StringBad::StringBad()
{len = 4;str = new char[4];strcpy(str, "C++");num_strings++;cout << num_strings << ": \"" << str << "\" default object created\n";
}StringBad::~StringBad()
{cout << "\"" << str << "\" object deleted, ";--num_strings;cout << num_strings << " left\n";delete []str;
}ostream & operator << (ostream & os, const StringBad &st)
{os << st.str;return os;
}void callme1(StringBad &);
void callme2(StringBad);int main()
{cout << "String an inner block.\n";StringBad headline1("asd");StringBad headline2("qwe");StringBad sports("zxc123");cout << "headline1: " << headline1 <<endl;cout << "headline2: " << headline2 <<endl;cout << "sports: " << sports <<endl;callme1(headline1);cout << "headline1: " << headline1 <<endl;callme2(headline2);cout << "headline2: " << headline2 <<endl;cout << "Initialize one object to another:\n";StringBad sailor = sports;cout << "sailor: " << sailor <<endl;cout << "Assign one object to another:\n";StringBad knot;knot = headline1;cout << "knot: " << knot <<endl;cout << "Exiting the block.\n";return 0;
}void callme1(StringBad &rsb)
{cout << "String passed by reference:\n";cout << "    \"" << rsb << "\"\n";
}void callme2(StringBad sb)
{cout << "String passed by value:\n";cout << "    \"" << sb << "\"\n";
}

这里还要说明的一点是:我用CodeBlocks运行上述代码,没有达到书本分析的预期结果。具体情况是临时对象调用了析构函数释放相应内存后,还可以获取内存中的值。查阅资料后发现原因是:RVO(return value optimization),被C++进行值返回的优化。有的软件可以关闭RVO,因为我不太清楚CodeBlocks如何关闭它。所以我将代码放到了Linux系统下运行。将RVO优化关闭,可以对g++增加选项-fno-elide-constructors,重新编绎之后,执行结果如下:

关闭RVO后,一下子就发现问题了。

这里有一个问题是因为当时忘写knot = headline1;这一条赋值语句
导致"Exiting the block.下面出现的是
"C++" object deleted, 2 left而不是"asd" object deleted, 2 left

程序第一个问题是输出中出现的各种非标准字符随系统而异,另一个问题是对象计数为负。程序开始时还是正常的,但逐渐变得异常,最终导致了灾难性结果。可以看出到headline1传递给callme1()函数,并在调用后重新显示headline1。

这一块代码运行都是正常的:

String passed by reference:"asd"
headline1: asd

但随后程序将headline2传递给了callme2,出现了一个严重的问题:

String passed by value:"qwe"
"qwe" object deleted, 2 left
headline2:

首先,将headline2作为函数参数来传递给函数,导致析构函数被调用。其次,虽然按值传递可以防止原始参数被修改,但实际上函数已使原字符串无法识别,导致显示一些非标准字符(显示的内容取决于内存中所包含的内容)。

在为每一个创建的对象自动调用析构函数时,情况更糟糕:

上面的计数异常是一条线索,因为每个对象被构造和析构一次,因此调用构造函数的次数应当与析构函数的调用次数相同。对象计数递减的次数比递增的次数多2,这表明使用了不将num_string递增的构造函数创建了两个对象。此时使用的不是默认的构造函数,也不是参数为const char *的构造函数,而是复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。也就是说它用于初始化过程中,而不是常规的赋值过程中。

类的复制构造函数原型通常如下:

Class_name(const Class_name &);

它接受一个指向类对象的常量引用作为参数。例如:

class StringBad
{
private:char *str;int len;static int num_strings;
public:StringBad(const char *s);StringBad();StringBad(const StringBad &);      //复制构造函数~StringBad();friend std::ostream & operator << (std::ostream & os, const StringBad & st);
};

新建一个对象并将其初始化为同类现有的对象时,复制构造函数都将被调用。假设motto是一个StringBad对象,则下面4种声明将调用复制构造函数:

StringBad ditto(motto);
StringBad metoo = mott;
StringBad also = StringBad (motto);
StringBad * pStringBad = new StringBad(motto);

其中中间的2种声明可能会使用复制构造函数直接创造metoo和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋值给metoo和also,这取决于具体实现。每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。

调用复制构造函数总结:

(1)用对象去初始化另一个对象。

(2)函数的参数是类对象(值传递)。

(3)返回值是类对象。

上述代码存在的问题

(1)默认的复制构造函数不说明其行为(逐个赋值非静态成员,只是复制成员的值,成员复制也称为浅复制),因为它不指出创建过程,也不增加计数器num_strings的值。但析构函数更新了计数,并且在任何对象过期时都将被调用,而不管对象是如何被创建的。这就导致了程序无法准确地记录对象的个数。

(2)就像开头的小程序一样,程序复制的不是字符串,而是一个指向字符串的指针。也就是说,将sailor初始化为sports后,会有两个指向同一个字符串的指针。当析构函数被调用时,str指针所指向的内存将被释放。此时,另一个对象再用str指针去访问这块区域,必然导致不确定的、可能有害的后果。

(3)最后一个问题是,试图释放内存两次可能导致程序异常终止(不同系统提供的信息不同)。

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

解决类设计中的这种问题的方法是进行深度复制(deep copy)。也就是说,复制构造函数应当复制字符串并将副本的地址赋值给str成员,而不是仅仅复制字符串地址。

StringBad::StringBad(const StringBad & st)
{num_strings++;len = st.len;str = new char[len + 1];strcpy(str, st.str);cout << num_strings << ":  \"" << str << "\" object created\n";
}

这时程序可以打印出headline2的值、字符串不乱码以及计数恢复正常。但是最后一条的字符串不应该为空,这又是一个问题。

StringBad的其他问题:赋值运算符

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

StringBad headline1(“asd”);
...
StringBad knot;
knot = headline1;
初始化对象时,不一定会使用赋值运算符
StringBad metoo = knot;   //使用复制构造函数

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

(2)赋值的问题

上述程序中将headline1赋值给了knot:

knot = headline1;

为knot调用析构函数时,knot.str所指向的区域将被释放。那么就会出现和隐式复制构造函数一样的结果:数据受损。以及试图删除已经删除的数据导致的结果是不确定的,因此可能会改变内存中的内容,导致程序异常终止。

(3)解决赋值问题

对于由与默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。

StringBad & StringBad::operator = (const StringBad & st)
{if (this == &st)return *this;delete []str;len = st.len;str = new char[len + 1];strcpy(str, st.str);return *this;
}

代码首先检查自我复制,这是通过查看赋值运算符右边的地址是否与接受对象的地址相同来完成的。如果相同,程序返回*this,然后结束。如果地址不同,函数将释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str。赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。通过修改程序,上述代码存在的3个问题都得到了解决。最终的运行结果如下:

总结:深复制开辟新的空间,而浅复制没有。

C++ 深复制与浅复制 RVO问题相关推荐

  1. php对象当参数传递 php深复制和浅复制

    把对象当参数传递给方法,在方法里改过对象后,影响到外面的对象  因为对象是引用传递过去的 class Book {public $name;public function __construct($n ...

  2. python列表浅复制_Python列表的深复制和浅复制示例详解

    一.深复制与浅复制 列表是Python中自带的一种数据结构,在使用列表时,拷贝操作不可避免,下面简单讨论一下列表的深复制(拷贝)与浅复制 首先看代码: l1 = [5, 4, 3, 2, 1] # 用 ...

  3. C#中的深复制和浅复制(在C#中克隆对象)

    C#中的深复制和浅复制(在C#中克隆对象) 转载于:https://www.cnblogs.com/dalovess/p/7002947.html

  4. python list 深复制_Python列表的深复制和浅复制示例详解

    免费资源网,https://freexyz.cn/ 一.深复制与浅复制 列表是Python中自带的一种数据结构,在使用列表时,拷贝操作不可避免,下面简单讨论一下列表的深复制(拷贝)与浅复制 首先看代码 ...

  5. C++学习基础七——深复制与浅复制

    C++学习基础七--深复制与浅复制 一.深复制与浅复制基本知识 深复制和浅复制,又称为深拷贝和浅拷贝. 深复制和浅复制的区别如下图1所示: 图1 图1表示的是,定义一个类CDemo,包含int a和c ...

  6. 深复制VS浅复制(MemberwiseClone方法介绍)

    MemberwiseClone方法,属于命名空间System,存在于程序集 mscorlib.dll中.返回值是System.Object.其含义是:创建一个当前object对象的浅表副本. MSDN ...

  7. Objective-C 深复制和浅复制与NSCopying协议

    1.简单复制只能实现浅拷贝:指针赋值,使两个指针指向相同的一块内存空间,操作不安全. 2. Foundation类已经遵守了<NSCopying>和 <NSMutableCopyin ...

  8. iOS 集合的深复制与浅复制

    2019独角兽企业重金招聘Python工程师标准>>> 概念 对象拷贝有两种方式:浅复制和深复制.顾名思义,浅复制,并不拷贝对象本身,仅仅是拷贝指向对象的指针:深复制是直接拷贝整个对 ...

  9. IOS学习笔记十八(copy、mutableCopy、NSCopying、NSMutableCopy、深复制、浅复制)

    1. copy.mutableCopy方法 copy方法返回对象的不可修改的副本 mutableCopy方法返回的对象可修改的副本 1).测试demo int main(int argc, char ...

  10. [C++]深复制与浅复制

    深复制与浅复制 C++中,默认的复制构造函数只能实现浅复制. 浅复制指的是在对象复制前,只对对象中的数据成员进行简单的复制 大多数情况下"浅复制"已经能很好的工作了,但是当类的数据 ...

最新文章

  1. 汇编语言--可屏蔽中断
  2. 用iframe实现局部刷新的各种跳转方法(网上总结)
  3. [optee]-opteeTA启动的过程(open_ta的过程)
  4. Metal之探究理解视频渲染RGB与YUV颜色编码
  5. Shiro安全框架-简介
  6. JFreeChart(四)之线型图
  7. [css] 你用过css的tab-size属性吗?浏览器默认显示tab为几个空格?
  8. C# 获得当前目录和执行目录的一些方法
  9. iOS pickerView(所有类型一网打尽)
  10. Git学习笔记总结和注意事项
  11. Python 柱状图 横坐标 名字_Python爬虫实例(二)——爬取新馆疫情每日新增人数
  12. 斯皮尔曼相关系数范围_Spearman Rank(斯皮尔曼等级)相关系数
  13. 阿里矢量图标(字体图标)
  14. Linux 2.6内核配置说明
  15. java电话号码生成器
  16. 安卓电子书格式_在安卓上比较适合看英文电子书的软件
  17. 《Wireshark数据包分析实战》读书笔记
  18. Golang + Gin + cytocsape.js + neo4j
  19. 简单的C语言宏定义结合全局变量的方法实现单片机串口实现透传模式
  20. storm trident mysql,storm_Trident

热门文章

  1. IDA Plugin 编写基础
  2. VC++ 创建Windows服务
  3. ADO学习(三)Command 对象
  4. Linux 内存管理 | 地址映射:分段、分页、段页
  5. kubernetes(二)k8s组件
  6. 你离顶尖网络工程师有多远?
  7. Kafka:那我走?这个新一代消息中间件,彻底火了!
  8. 集群管理工具KafkaAdminClient——改造
  9. RabbitMQ管理(4)——应用管理
  10. 音视频技术开发周刊 | 148