c++内存机制和安全


1. 动态分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <cstring> // For memset
using namespace std;

class SecureBuffer
{
public:
char *m_data; // 指向动态分配的内存
size_t m_size; // 内存大小
SecureBuffer(size_t m_size)
{
// 分配内存 再赋值
m_data = new char[m_size];
std::memset(m_data, 0xCC, m_size);
}

~SecureBuffer()
{
if (m_data)
{
cout << "memset 0";
std::memset(m_data, 0x00, m_size); // 用 0x00 清空内存
delete[] m_data;
}
}
};

int main()
{
SecureBuffer *sec = new SecureBuffer(12);
cout << "test" << sec->m_data << endl;
delete sec;
return 0;
}

2. 问题描述

2.1 拷贝函数

浅拷贝是两个对象但是共用一个内存 ,深拷贝是两个对象两个内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

#include <iostream>
#include <cstring>
#include <iomanip>
using namespace std;

class SecureBuffer
{
public:
char *m_data;
size_t m_size;

SecureBuffer(size_t size) : m_size(size)
{
m_data = new char[m_size];
memset(m_data, 0xCC, m_size);
cout << "Constructor: " << m_size << endl;
}

// 浅拷贝构造函数
/*
SecureBuffer(const SecureBuffer &other) : m_data(other.m_data), m_size(other.m_size)
{
std::cout << "浅拷贝构造函数被调用" << std::endl;
}
*/
// 深拷贝构造函数

SecureBuffer(const SecureBuffer &other) : m_size(other.m_size)
{
m_data = new char[m_size];
std::memcpy(m_data, other.m_data, m_size);
std::cout << "深拷贝构造函数被调用" << std::endl;
}

~SecureBuffer()
{
cout << "Destructor: " << endl;
if (m_data)
{
memset(m_data, 0x00, m_size);
}
}

void printData(const string &step)
{
cout << step << endl;
cout << "Content: ";
for (size_t i = 0; i < min(m_size, (size_t)4); ++i)
{
cout << hex << setfill('0') << setw(2)
<< static_cast<unsigned>(static_cast<unsigned char>(m_data[i])) << " ";
}
cout << endl;
}
};

int main()
{

SecureBuffer *s1 = new SecureBuffer(12);
SecureBuffer *s2 = new SecureBuffer(*s1);
s1->printData("拷贝之前s1的值");
memset(s2->m_data, 0x11, s2->m_size);
s1->printData("拷贝之后s1的值");
return 0;
}

2.2 悬垂指针
  1. 未定义行为

    • 访问已释放的内存会导致未定义行为,程序可能崩溃或输出不可预测的结果。
  2. 潜在的安全漏洞

    • 如果释放的内存被重新分配给其他对象,攻击者可能利用悬垂指针访问敏感数据。
  3. 调试困难

    • 悬垂指针的问题通常难以复现和调试,尤其是在大型项目中。

使用new 创建的 SecureBuffer 对象之后,显式调用析构函数销毁对象,但不置空指针,并继续通过该指针访问其成员函数,观察并输出悬垂指针带来的后果。

就是销毁对象之后,之前指向对象的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <cstring>
#include <iomanip>
using namespace std;

class SecureBuffer
{
public:
char *m_data;
size_t m_size;

SecureBuffer(size_t size) : m_size(size)
{
m_data = new char[m_size];
memset(m_data, 0xCC, m_size);
cout << "Constructor: " << m_size << endl;
}

// 浅拷贝构造函数
/*
SecureBuffer(const SecureBuffer &other) : m_data(other.m_data), m_size(other.m_size)
{
std::cout << "浅拷贝构造函数被调用" << std::endl;
}
*/
// 深拷贝构造函数
/*
SecureBuffer(const SecureBuffer &other) : m_size(other.m_size)
{
m_data = new char[m_size];
std::memcpy(m_data, other.m_data, m_size);
std::cout << "深拷贝构造函数被调用" << std::endl;
}

*/
~SecureBuffer()
{
cout << "Destructor: " << endl;
if (m_data)
{
memset(m_data, 0x00, m_size);
delete[] m_data;
}
}

void printData(const string &step)
{
cout << step << endl;
cout << "Content: ";
for (size_t i = 0; i < min(m_size, (size_t)4); ++i)
{
cout << hex << setfill('0') << setw(2)
<< static_cast<unsigned>(static_cast<unsigned char>(m_data[i])) << " ";
}
cout << endl;
}
};

int main()
{
for (int i = 0; i < NUM_OBJECTS; ++i)
{
new SecureBuffer(1024); // 每个对象分配 1KB 内存(1024 字节)
}
// 输出内存泄漏提示
cout << "警告:程序未释放 " << NUM_OBJECTS << " 个 SecureBuffer 对象的内存!" << endl;
cout << "这将导致大约 " << (NUM_OBJECTS * 1024) / (1024 * 1024) << " MB 的内存泄漏!" << endl;
return 0;
}
2.3 析构函数的调用
  1. 动态分配的对象不会自动调用析构函数 : 如果你使用 new 动态创建对象(如 new SecureBuffer(1024)),这些对象的生命周期是由你手动管理的,它们不会在作用域结束时自动调用析构函数。

  2. 只有栈上对象会在作用域结束时自动调用析构函数 : 如果你直接创建对象(如 SecureBuffer buffer(1024);),这些对象是分配在栈上的,当它们离开作用域时,析构函数会自动调用

3. 相关解决机制

3.1 智能指针

C++中的智能指针是用于管理动态内存的类模板,通过RAII(资源获取即初始化)机制自动管理资源的生命周期,避免内存泄漏和悬空指针问题。

  1. std::unique_ptr

一个对象进行引用,只属于一个对象。其实智能指针就是一个类,只不过我们拿到类之后。进行对象构造。 不能被拷贝引用。

1
2
3
4
#include <memory>
std::unique_ptr<int> ptr1 = std::make_unique<int>(42); // 创建
std::unique_ptr<int> ptr2 = std::move(ptr1); // 转移所有权
// ptr1现在为空,ptr2管理资源

但是可以通过移动语义去进行放入vector

1
2
3
4
5
6
7
8
9

auto ptr = std::make_unique<int>(42);
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::move(ptr)); // 转移所有权,ptr 变为空

if (!ptr) {
std::cout << "ptr 已经为空" << std::endl;
}

想象你在管理家具:

  • std::unique_ptr

    • 家具只能由一个人拥有(独占所有权)。
    • 如果你想把家具给别人,必须完全转让所有权(std::move)。
    • 你不能同时拥有两份相同的家具。
  • std::shared_ptr

    • 家具可以被多人共享(引用计数)。
    • 每个人都有一份家具的“钥匙”,只有当所有人都归还钥匙时,家具才会被销毁。
  1. std::shared_ptr

其实就是这个指针能被多个对象引用了。

1
2
3
4
auto ptr1 = std::make_shared<int>(100); // 引用计数为1
{
auto ptr2 = ptr1; // 引用计数增至2
} // ptr2析构,引用计数减至1
  1. std::weak_ptr

多个对象引用但是不计数,做一个临时访问。

1
2
3
4
5
auto shared = std::make_shared<int>(200);
std::weak_ptr<int> weak = shared;
if (auto temp = weak.lock()) { // 检查资源是否存在
// 使用temp访问资源
}

3.2 模板函数

1
template <typename T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>

// 定义模板函数
template <typename T>
void print(const T& value) {
std::cout << "Value: " << value << std::endl;
}

int main() {
int i = 42;
double d = 3.14;
std::string s = "Hello, Template!";

// 调用模板函数
print(i); // Value: 42
print(d); // Value: 3.14
print(s); // Value: Hello, Template!

return 0;
}

3.3 引用和指针

为了更好地理解 T &ptr 的作用,我们需要明确引用和指针的区别:

特性 引用 (T&) 指针 (T*)
是否可以为空 不可以为空(必须绑定到有效变量) 可以为空
是否需要解引用 不需要解引用 需要通过*解引用
是否可重新赋值 一旦绑定,不能重新绑定到其他变量 可以随时指向不同的地址
是否占用额外内存 通常不占用额外内存(编译器优化) 占用内存(存储地址)
1
2
3
4
5
6
7
8
9
10
11
12
13
        if (ptr)
{
memset(ptr, 0x00, sizeof(T));
delete[] ptr;
ptr = nullptr; // 避免悬空指针
}
/*

这里面有个很重要的观点,delete 只是清理了内存但是指针指向的内存并没有清理。
于是我们还需要将指针置空

*/

3.4 移动构造函数

我现在发现,其实移动构造函数就是把浅拷贝的构造函数加上把那个赋值的对象的属性放置为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
    SecureBuffer(SecureBuffer &&other) noexcept : m_data(other.m_data), m_size(other.m_size)

    {

        other.m_data = nullptr; // 避免悬空指针

        other.m_size = 0;

        cout << "移动构造函数被调用" << endl;

    }

 
  1. noexcept

不抛出异常处理,因为std:move std:vector 进行处理时候如果抛出异常之后,我们会造成移动拷贝失败,会退回到拷贝操作。影响效率

  1. SecureBuffer &&other

&& 是 C++11 引入的一个特性,表示 右值引用(Rvalue Reference) 。它专门用于绑定到临时对象或即将被销毁的对象(即“右值”)。

  1. 左值必须转换成右值

因为不转换那么c++ 默认是拷贝构造函数调用

1
2
SecureBuffer buf1(100);                  // buf1 是左值
SecureBuffer buf2 = std::move(buf1); // 使用 std::move 触发移动构造函数

从这些分析中我们也可以看到,我们的移动赋值操作其实跟浅拷贝操作是一样的。但是移动拷贝是移动资源。

3.5 移动赋值运算符

如果说深拷贝的话,其实我们是没有为指针提前赋值的,直接把要拷贝的对象放在构造函数参数,所以不用释放,但是赋值运算符是提前有一个对象,所以要先把他释放再深入拷贝

1
2
3
4
5
6
7
8
9
SecureBuffer& operator=(const SecureBuffer& other) {
if (this != &other) { // 防止自赋值
// delete[] buffer; // 忘记释放原有资源
buffer = new char[other.size];
size = other.size;
memcpy(buffer, other.buffer, size);
}
return *this;
}

其实这个移动赋值运算符号,跟深拷贝很像,但是触发还是有区别

1
2
3
SecureBuffer buf1(100);
SecureBuffer buf2(50);
buf2 = std::move(buf1); // 调用移动赋值运算符

必须是已经存在的对象,并且我我们发现必须右边是右值对象,如果不是那么会调用拷贝运算符

3.6 容器

容器 是一种用于存储和管理多个元素的数据结构

1
2
3
4
5
6
7
8
#include <vector>
std::vector<int> vec = {1, 2, 3}; // 动态数组


std::vector<int> vec;
vec.push_back(1); // 动态添加元素

int x = vec[0]; // 访问第一个元素

其实我感觉跟python没啥区别 但就是更快


文章作者: K1T0
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 K1T0 !
  目录