深度解析C++拷贝构造函数

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 自2003年开始,断断续续用了12年C++,直到这两年做物联网嵌入式开发,感觉对C++的掌握仅有10%左右。习惯了C#开发,C++倒显得难以下手!今天就一个函数返回问题跟辉月兄弟讨论一番,大有所获,足以解决我们目前80%的问题,感觉对C++的掌握上升到了20%。

自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

我不相信神话,我只相信汗水!我不相信命运,我只相信双手!
目录
打赏
0
0
0
0
15
分享
相关文章
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
22天前
|
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
42 4
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
125 1
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
164 1
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
46 12
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
55 16
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。

热门文章

最新文章

推荐镜像

更多
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等