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


    自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
    ", str, this);
        }
            A(const A &a)
            {
            debug_printf("A.Copy %s 0x%08X => %s 0x%08X
    ", a.str, &a, str, this);
            }
        ~A()
        {
            debug_printf("~A %s 0x%08X
    ", str, this);
        }
    };
    
    class B : public A
    {
    public:
        B(char* s) : A(s)
        {
            debug_printf("B %s 0x%08X
    ", str, this);
        }
            B(const B &b) : A(b.str)
            {
            debug_printf("B.Copy %s 0x%08X => %s 0x%08X
    ", b.str, &b, str, this);
            }
        ~B()
        {
            debug_printf("~B %s 0x%08X
    ", str, this);
        }
            B& operator=(const B &b)
            {
            debug_printf("B.Assign %s 0x%08X => %s 0x%08X
    ", 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 
    ");
        b = fun(a);
            debug_printf("end 
    ");
    }

    执行结果如下:

    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

  • 相关阅读:
    solr 最佳实践
    DNS 域名解析过程
    mac 下 virtualbox 配置全网通
    搜索引擎使用技巧
    三叉搜索树
    双数组trie树的基本构造及简单优化
    基于回归-马尔科夫模型的客运量预测
    solr 常用命令
    PHP yield 分析,以及协程的实现,超详细版(上)
    C语言,简单计算器【上】
  • 原文地址:https://www.cnblogs.com/nnhy/p/cpp_ctor.html
Copyright © 2020-2023  润新知