介绍:
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) 释放的内存。这是一个错误。

89

被折叠的 条评论
为什么被折叠?



