自2003年开始,断断续续用了12年C++,直到这两年做物联网嵌入式开发,感觉对C++的掌握仅有10%左右。
习惯了C#开发,C++倒显得难以下手!今天就一个函数返回问题跟辉月兄弟讨论一番,大有所获,足以解决我们目前80%的问题,感觉对C++的掌握上升到了20%。

背景,现有字节数组ByteArray和字符串String,(不要激动,单片机嵌入式C++很难用起来标准类库)
我们需要实现函数String& ByteArray::ToHex()
其实这是我们在C#上非常常用的函数,把一个字节数组转为字符串,然后别的地方使用或者显示出来。C#原型String ToHex(this Byte[] buf)
这里有一个老大难题:
1,如果ToHex内部栈分配字符串空间,把字节数组填充进去,那么离开ToHex的时候栈回收,对象数据无效
2,如果ToHex内部堆分配空间,字节数组填充,离开ToHex的时候得到指针。但是这样违背了C/C++谁申请谁释放的原则,其它小伙伴使用ToHex的时候可能忘了释放
3,最后只能折中,做成String& ByteArray::ToHex(String& str); 别提多憋屈!最受不了的是,外部分配str的时候,还得考虑数组有多长!这些本来最好由ToHex内部解决的问题。
总之,这个问题就这样折腾了我12年!

知道今天,跟辉月兄弟聊起这个问题,他也有十多年C++历史,用得比我要多一些。他有一段常用代码大概如下:

CString Test()
{
CString a = "aaaa";
CString b = "bbbb";
CString c = a + b; return c;
}

按他说法,就这样子写了十多年!
我说c不是栈分配吗?离开的时候会被析构吧,外部怎么可能拿到?他说是哦,从来没有考虑过这个问题。
我们敏锐的察觉到,C++一定可以实现类似的做法,因为字符串相加就是最常见的例子。

经过一番探讨,我们发现关键点出在拷贝构造函数上面

测试环境:编译器Keil MDK 5.14,处理器STM32F407VG

1、进出两次拷贝
做了一个测试代码,两次调用拷贝构造函数

class A
{
public:
char* str; A(char* s)
{
str = s;
debug_printf("A %s 0x%08X\r\n", str, this);
}
A(const A &a)
{
debug_printf("A.Copy %s 0x%08X => %s 0x%08X\r\n", a.str, &a, str, this);
}
~A()
{
debug_printf("~A %s 0x%08X\r\n", str, this);
}
}; class B : public A
{
public:
B(char* s) : A(s)
{
debug_printf("B %s 0x%08X\r\n", str, this);
}
B(const B &b) : A(b.str)
{
debug_printf("B.Copy %s 0x%08X => %s 0x%08X\r\n", b.str, &b, str, this);
}
~B()
{
debug_printf("~B %s 0x%08X\r\n", str, this);
}
B& operator=(const B &b)
{
debug_printf("B.Assign %s 0x%08X => %s 0x%08X\r\n", b.str, &b, str, this);
return *this;
}
}; B fun(B c)
{
c.str = "c";
return c;
} void CtorTest()
{
B a("a"), b("b");
debug_printf("start \r\n");
b = fun(a);
debug_printf("end \r\n");
}

执行结果如下:

A a 0x2001FB78
B a 0x2001FB78
A b 0x2001FB74
B b 0x2001FB74
start
A a 0x2001FB7C
B.Copy a 0x2001FB78 => a 0x2001FB7C
A c 0x2001FB80
B.Copy c 0x2001FB7C => c 0x2001FB80
B.Assign c 0x2001FB80 => b 0x2001FB74
~B c 0x2001FB80
~A c 0x2001FB80
~B c 0x2001FB7C
~A c 0x2001FB7C
end
~B b 0x2001FB74
~A b 0x2001FB74
~B a 0x2001FB78
~A a 0x2001FB78
  • 进入func的时候,参数进行了一次拷贝,c构造,也就是7C,然后a拷贝给c
  • 离开func的时候,产生了临时对象80,并把7C拷贝给80
  • func返回值赋值给b,也就是临时对象80赋值给74
  • 然后才是80和7C的析构。
  • 那么关键点就在于这个临时对象,它的作用域横跨函数内部和调用者,自然不怕析构回收。
  • 不过奇怪的是,内部参数7C为何在外面析构??

2、进去拷贝出来引用
修改func函数,返回引用,少一次拷贝构造

B& fun(B c)
{
c.str = "c";
return c;
}

执行结果如下:

A a 0x2001FB70
B a 0x2001FB70
A b 0x2001FB6C
B b 0x2001FB6C
start
A a 0x2001FB74
B.Copy a 0x2001FB70 => a 0x2001FB74
B.Assign c 0x2001FB74 => b 0x2001FB6C
~B c 0x2001FB74
~A c 0x2001FB74
end
~B b 0x2001FB6C
~A b 0x2001FB6C ~A a 0x2001FB70
  • 进去的时候参数来了一次拷贝构造74
  • 出来的时候74直接赋值给6C,也就是b。看样子,按引用返回直接省去了临时对象。
  • 但是上面这个代码编译会有一个警告,也就是返回本地变量的引用。
  • 赋值以后,内部对象74才被析构
  • 虽然有警告,但是对象还没有被析构,外面可以使用。按理说每个线程都有自己的栈,不至于那么快被别的线程篡改数据。但是很难说硬件中断函数会不会用到那一块内存。
  • 这里有个非常奇怪的现象,没有见到70的B析构,不知道是不是串口输出信息太快,丢失了这一部分数据,尝试了几次都是如此。

3、引用进去引用出来
修改参数传入引用,再少一次拷贝构造

B& fun(B& c)
{
c.str = "c";
return c;
}

执行结果如下:

A a 0x2001FB88
B a 0x2001FB88
A b 0x2001FB84
B b 0x2001FB84
start
B.Assign c 0x2001FB88 => b 0x2001FB84
end
~B b 0x2001FB84
~A b 0x2001FB84
~B c 0x2001FB88
~A c 0x2001FB88
  • 更加彻底,没有任何拷贝构造函数被执行
  • 并且没有“返回本地变量引用”的警告

End

深度解析C++拷贝构造函数的更多相关文章

  1. C++ 默认拷贝构造函数 深度拷贝和浅拷贝

    C++类默认拷贝构造函数的弊端 C++类的中有两个特殊的构造函数,(1)无参构造函数,(2)拷贝构造函数.它们的特殊之处在于: (1) 当类中没有定义任何构造函数时,编译器会默认提供一个无参构造函数且 ...

  2. 【转】C++的拷贝构造函数深度解读,值得一看

    建议看原帖  地址:http://blog.csdn.net/lwbeyond/article/details/6202256 一. 什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很 ...

  3. Kafka深度解析

    本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅 ...

  4. java内存分配和String类型的深度解析

    [尊重原创文章出自:http://my.oschina.net/xiaohui249/blog/170013] 摘要 从整体上介绍java内存的概念.构成以及分配机制,在此基础上深度解析java中的S ...

  5. c++ 构造函数,拷贝构造函数,析构函数与赋值操作符

    题目: 为下面的Rectangle类实现构造函数,拷贝构造函数,赋值操作符,析构函数. class Shape { int no; }; class Point { int x; int y; }; ...

  6. 【C++对象模型】构造函数语意学之二 拷贝构造函数

    关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在[需要的时候]才去合成默认的拷贝构造函数. 在什么时候才是[需要的时候]呢? 也就是类不展现[bitwise copy semantic ...

  7. 编写类String的构造函数、拷贝构造函数、析构函数和赋值函数

    一.题目: class String { public: String(const char *str = NULL); // 普通构造函数 String(const String &othe ...

  8. 拷贝构造函数,深拷贝,大约delete和default相关业务,explicit,给定初始类,构造函数和析构函数,成员函数和内联函数,关于记忆储存,默认参数,静态功能和正常功能,const功能,朋友

     1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.另外一种初始化的方式是直接在构造方法里面实现初始化. 案比例如以 ...

  9. String深度解析

    文章出处:http://my.oschina.net/xiaohui249/blog/170013 一.引题 String类型是比较特殊的一种类型,同时也是面试经常被问到的一个知识点,本文结合java ...

随机推荐

  1. 基于Entity Framework的自定义分页,增删改的通用实现

    简介 之前写个一个基于Dapper的分页实现,现在再来写一个基于Entity Framework的分页实现,以及增删改的通用实现. 代码 还是先上代码:https://github.com/jinwe ...

  2. 修改User-Agent来伪装浏览器访问手机站点

    有时候为了测试需要,可能需要使用测试手机wap这样的站点,如果用真正的手机去测试也可以实现,但是比较麻烦,我们可以通过设置chrome的user agent来伪装浏览器,达到我们的测试目的. 代码如下 ...

  3. java 常见数据结构

    1)tree 2) queue 3) list 4) stack 5) heap 6) map

  4. Java工程师书单(初级、中级、高级)

    简介 怎样学习才能从一名Java初级程序员成长为一名合格的架构师,或者说一名合格的架构师应该有怎样的技术知识体系,这是不仅一个刚刚踏入职场的初级程序员也是工作一两年之后开始迷茫的程序员经常会问到的问题 ...

  5. JAVA环境变量关于

    1.为什么要设置classPath? 用于通知JVM Java基础类库的位置.classPath告诉类装载器去哪里寻找第三方类库 自JDK1.5之后便不需要再配置这个变量了 2.为什么安装两个JRE( ...

  6. 快速排序算法的C语言实现

    #include<stdio.h> int partition(int a[],int low,int high) { int key=a[low]; while(low<high) ...

  7. 奇怪的道路[JXOI2012]

    题目描述 小宇从历史书上了解到一个古老的文明.这个文明在各个方面高度发达,交通方面也不例外.考古学家已经知道,这个文明在全盛时期有n座城市,编号为1..n.m条道路连接在这些城市之间,每条道路将两个城 ...

  8. 日常API之QQ登录

    这次的QQ登录我研究了好久惹,今天终于可以和大家分享啦! 大家都知道,QQ登录有很多方法,例如使用账号密码登录,手机版企鹅扫码登录等等(这些方法只能验证QQ是否成功登录,并没有聊天等功能) 首先就来使 ...

  9. 关于发布中报“未能加载文件或程序集“Newtonsoft.Json”或它的某一个依赖项”的问题解决方法

    遇到这个问题了,我也是醉了,开发就一个还在忙别的事情,我想想自己解决 你们遇到过吗?我在网上找到好多解决的方法,比如改webconfig文件,或者改package.config文件,都没用.但是我看到 ...

  10. 常用 Http 的请求方法

    第一次用markdown写博客,体验下 因为常用Http的几种请求方式,总结一下. 1.封装两个Http的最常用方法,叫做HttpHelper类. HttpPost: public static st ...