C++进阶:浅拷贝和深拷贝

介绍:

C++在我们不提供拷贝构造(copy constructor)和赋值操作(assignment operator)的情况下会给我们自动提供, 将它们显式实现后如下, 还是之前的分数的例子:

#include <cassert>
#include <iostream>
class Fraction
{
private:
    int m_numerator { 0 };
    int m_denominator { 1 };

public:
    // Default constructor
    Fraction(int numerator = 0, int denominator = 1)
        :m_numerator{numerator}
        ,m_denominator{denominator}
    {
        assert(m_denominator != 0);
    }
    
 friend std::ostream& operator<<(std::ostream& out, const Fraction& f)
    {
        out << f.m_numerator << '/' << f.m_denominator;
        return out;
    }
};

int main()
{
    Fraction init { 1, 2 };
    Fraction fraction { init }; // copy constructor
    std::cout << fraction << std::endl;

    return 0;
}
#include <cassert>
#include <iostream>
class Fraction
{
private:
    int m_numerator { 0 };
    int m_denominator { 1 };

public:
    // Default constructor
    Fraction(int numerator = 0, int denominator = 1)
        :m_numerator{numerator}
        ,m_denominator{denominator}
    {
        assert(m_denominator != 0);
    }

    //  Possible implementation of implicit copy constructor
    Fraction(const Fraction& f)
        :m_numerator{ f.m_numerator }
        ,m_denominator{ f.m_denominator }
    {
        assert(m_denominator != 0);
    }

    // Possible implementation of implicit assigment operator
    Fraction& operator=(const Fraction& f)
    {
        // self-assignment guard
        if(this == &f)
            return *this;

        // do the copy
        m_numerator = f.m_numerator;
        m_denominator = f.m_denominator;

        // return the existing object so we chain this operator
        return *this;
    }

    friend std::ostream& operator<<(std::ostream& out, const Fraction& f)
    {
        out << f.m_numerator << '/' << f.m_denominator;
        return out;
    }
};

int main()
{
    Fraction init { 1, 2 };
    Fraction fraction { init }; // copy constructor
    std::cout << fraction << std::endl;

    return 0;
}

具有同等效果, 均输出1/2
在这里插入图片描述

浅拷贝

这在平时使用中没有什么关系,但是在动态分配涉及到指针时会发生未定义行为:

#include <cstring> // for strlen()
#include <cassert> // for assert()
#include <iostream>

class MyString
{
private:
    char* m_data {};
    int m_length {};   // 1
public:
    MyString(const char* source = " ")
    {
        assert(source);  //make sure source isn't a null string

        m_length = std::strlen(source) + 1;
        m_data = new char[m_length];

        for(int i{}; i < m_length; ++i)  // 2
        {
            m_data[i] = source[i];
        }
    }

    ~MyString()
    {
        delete[] m_data;
    }

    char* getString() { return m_data; }
    int getLength() { return m_length; }  // 3
};

int main()
{
    MyString init{ "Hello, world!" };
    MyString string{ init };

    std::cout << string.getString() << '\n';

    return 0;
}

在这里插入图片描述
考虑到此处下标为int可能不妥, 会丢掉精确度问题, 遂将unsinged long 替换 int. 代码中 1, 2, 3处.
使用:%s/unsigned long/int/g in vim
在这里插入图片描述
详细内容见后面补充:

这里是同一块内存释放了2次, 是C++经典的错误.
我们分析一下问题:

MyString init{ "Hello, world!" }; // Default constructor
MyString string{ init };     // copy constructor

// 当出初始化类init时, 编译器判断出"Hello, world!"是一个字符串字面量, 与MyString(const char* source = " ")
匹配, 但因为已经初始化赋值了"Hello, world!",它不会再设置const char* source = " ", 所以这里m_data 被赋予"Hello, world!".

// 原因就在这里, MyString string{ init }检测到init是一个MyString类型的对象, 又有{}初始化器, 所以编译器调用它默认提供的copy constructor
MyString::MyString(const MyString& source)
	:m_data{source.m_data}
	,m_length{source.m_length}
{ 
}
这时候string.m_data 是一个init.m_data的副本,它们共同指向同一块内存地址, 毛病就出在这里.
当程序运行到main的}, 按照析构函数的调用顺序----构造函数(init->string)的逆序(string->init), 
先调用string的构造函数, 把string.m_data所指的内存地址释放掉了.
而轮到init.m_data释放地址时,发现它不知道指哪儿, 所以报错.
	

本质问题是什么?

  • 分享同一块内存
  • 更进一步, 所指内存地址不独立yi

================================================== 分界线 =======================================

解决思路:

  • 让它们所指的内存地址独立
  • 更进一步, 就是有互不干扰的源和副本

深拷贝

深拷贝会为副本分配内存,然后复制实际值,这样副本就位于与源不同的内存中。这样,副本和源就彼此独立,不会以任何方式相互影响。进行深拷贝需要我们编写自己的复制构造函数和重载赋值运算符。

写一个深度拷贝, 无论是拷贝构造还是拷贝赋值操作, 它们可共用的很多, 所以可以单独写出来, 然后它们各自调用即可;

void MyString::deepCopy(const MyString& source)
{
    delete[] m_data;
    m_length = source.m_length;
    if (source.m_data)
    {
        m_data = new char[m_length];

        for (unsigned long i{}; i < m_length; ++i)
            m_data[i] = source.m_data[i];
    }
    else
        m_data = nullptr;
}

完整代码:

#include <cstring> // for strlen()
#include <cassert> // for assert()
#include <iostream>

class MyString
{
private:
    char* m_data {};
    unsigned long m_length {};
public:
    MyString(const char* source = " ")
    {
        assert(source);  //make sure source isn't a null string

        m_length = std::strlen(source) + 1;
        m_data = new char[m_length];

        for(unsigned long i{}; i < m_length; ++i)
        {
            m_data[i] = source[i];
        }
    }

    void deepCopy(const MyString& source);
    MyString(const MyString& source)
    {
        deepCopy(source);
    }

    MyString& operator=(const MyString& source)
    {
        if (this != &source)
            deepCopy(source);

        return *this;
    }
    char* getString() { return m_data; }
    unsigned long getLength() { return m_length; }

    ~MyString()
    {
        delete[] m_data;
    }

};
void MyString::deepCopy(const MyString& source)
{
    delete[] m_data;
    m_length = source.m_length;
    if (source.m_data)
    {
        m_data = new char[m_length];

        for (unsigned long i{}; i < m_length; ++i)
            m_data[i] = source.m_data[i];
    }
    else
        m_data = nullptr;
}
int main()
{
    MyString init{ "Hello, world!" };
    MyString string{ init };

    std::cout << string.getString() << '\n';

    return 0;
}

运行成功.

在这里插入图片描述

关于上面提到内存泄漏的问题,它报错是free(): double free detected in tcache 2
我们能猜出是动态分配的指针问题,但如果想要更进一步了解, 仅凭有限的报错信息就很难, 其实LLVM流就有完整的工具.

LeakSanitizer-------内存检测工具

同类产品还有Valgrind, 但我选择更加轻量级的, LLVM 本就附带的LeakSanitizer.
对于前面浅拷贝的错误:

||     #0 0x0000004e95a1 in operator delete[](void*) (/home/user/cpp/main+0x4e95a1) (BuildId: 2475ead385557fb5cde774735c363b4f816bfaf1)
||     #1 0x0000004ea1f9 in main (/home/user/cpp/main+0x4ea1f9) (BuildId: 2475ead385557fb5cde774735c363b4f816bfaf1)
||     #2 0x7306aae11574 in __libc_start_call_main (/lib64/libc.so.6+0x3574) (BuildId: 48c4b9b1efb1df15da8e787f489128bf31893317)
||     #3 0x7306aae11627 in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3627) (BuildId: 48c4b9b1efb1df15da8e787f489128bf31893317)
||     #4 0x000000400714 in _start (/home/user/cpp/main+0x400714) (BuildId: 2475ead385557fb5cde774735c363b4f816bfaf1)

介绍

LeakSanitizer 是一款运行时内存泄漏检测器。它可以与 AddressSanitizer结合使用,同时检测内存错误和泄漏,也可以单独使用。LSan 几乎不会增加性能开销,直到流程的最后阶段才会增加额外的泄漏检测阶段。

用法

AddressSanitizer:集成 LeakSanitizer 并在支持的平台上默认启用它。

cat memory-leak.c
include <stdlib.h>
void *p;
int main() {
  p = malloc(7);
  p = 0; // The memory is leaked here.
  return 0;
}
clang -fsanitize=address -g memory-leak.c ; ASAN_OPTIONS=detect_leaks=1 ./a.out
==23646==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 7 byte(s) in 1 object(s) allocated from:
0 0x4af01b in __interceptor_malloc /projects/compiler-rt/lib/asan/asan_malloc_linux.cc:52:3
1 0x4da26a in main memory-leak.c:4:7
2 0x7f076fd9cec4 in __libc_start_main libc-start.c:287
SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).

要在独立模式下使用 LeakSanitizer,请使用 -fsanitize=leak标志链接您的程序。请确保在链接步骤中使用clang(而不是ld),以便将正确的 LeakSanitizer 运行时库链接到最终的可执行文件中。

安全考虑

LeakSanitizer 是一款错误检测工具,其运行时不适用于链接到生产环境的可执行文件。虽然 LeakSanitizer 可能有助于测试,但其运行时在开发时并未考虑安全敏感约束,可能会危及生成的可执行文件的安全性。

支持的平台

  • Android
  • Fuchsia
  • Linux
  • macOS
  • NetBSD

鉴于以上信息,我把单文件里面的构建添加了-fsanitize=address, 完整的~/.vim/tasks.ini
tasks.ini


[file-build]
command:cpp=clang++ -std=c++23 -O2 -Wall -Weffc++ -Wextra -Wconversion -Wsign-conversion  -fsanitize=address "$(VIM_FILEPATH)" -o "$(VIM_PATHNOEXT)" -lm -msse3
command:c=clang -std=c17 -O2 -Wall -Weffc++ -Wextra -Wconversion -Wsign-conversion "$(VIM_FILEPATH)" -o "$(VIM_PATHNOEXT)" -lm
command:make=make -f "$(VIM_FILEPATH)"
output=quickfix
errorformat=
cwd=$(VIM_FILEDIR)
save=2

内存知识补充:

顾名思义,指针指向某块内存。你可以使用多个指针指向同一块内存:
int* x = malloc(sizeof(int));
int* y = x; //x 和 y 指向同一块内存
但是,free它作用于实际内存,而不是指针。这意味着,如果同时使用free这两个指针:
free(x);
free(y);
您“重复释放”了free(y)内存,即试图释放已经被free(x) 释放的内存。这是一个错误。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值