[重学C++]01| C++ 如何进行内存资源管理

科技资讯 投稿 6700 0 评论

[重学C++]01| C++ 如何进行内存资源管理

文章首发

前言

大家好,我是只讲技术干货的会玩code,今天是【重学C++】的第一讲,我们来学习下C++的内存管理。

内存分区

在C++中,将操作系统分配给程序的内存空间按照用途划分了代码段、数据段、栈、堆几个不同的区域,每个区域都有其独特的内存管理机制。

代码区

由于代码区是只读的,所以会被多个进程共享。在多个进程同时执行同一个程序时,操作系统只需要将代码段加载到内存中一次,然后让多个进程共享这个内存区域即可。

数据段

C++中函数调用以及函数内的局部变量的使用,都是通过栈这个内存分区实现的。栈分区由操作系统自动分配和释放,是一种"后进先出"的一种内存分区。每个栈的大小是固定的,一般只有几MB,所以如果栈变量太大,或者函数调用嵌套太深,容易发生栈溢出(stack overflow)。

#include <iostream>

void inner(int a {
    std::cout << a << std::endl;
}
void outer(int n {
	int a = n + 1;
    inner(a;
}

int main( {
    outer(4;
}

上面这段代码运行过程中的栈变化如下图

本地变量是直接存储在栈上的,当函数执行完成后,这些变量占用的内存就会被释放掉了。前面例子中的本地变量是简单类型,在C++中称为POD类型。对于带有构造和析构函数的非POD类型变量,栈上的内存分配同样有效。编译器会在合适的时机,插入对构造函数和析构函数的调用。

答案是会的,C++对于发生异常时对析构函数的调用称为"栈展开"。通过下面这段代码演示栈展开。

#include <iostream>
#include <string>

class Obj {
public:
    std::string name_;
    Obj(const std::string& name:name_(name{std::cout << "Obj( " << name_ << std::endl;};
    ~Obj( {std::cout << "~Obj( " << name_ << std::endl;};
};


void bar( {
    auto o = Obj{"bar"};
    throw "bar exception";
}

int main( {
    try {
        bar(;
    } catch (const char* e {
        std::cout << "catch Exception: " << e << std::endl;
    }
}

执行代码的结果是:

Obj( bar
~Obj( bar
catch Exception: bar exception

可以发现,发生异常时,bar函数中的本地变量o还是能被正常析构。

    程序抛出异常,停止当前执行的调用链,开始寻找与异常匹配的catch子句。
  1. 如果异常发生在try中,则会首先检查与该try块匹配的catch子句。如果异常所在函数体没有try捕获异常。则会直接进入下一步。
  2. 如果第二步未找到匹配的catch,则会在外层的try块中查找,直到找到为止。
  3. 如果到了最外层还没有找到匹配的catch,也就是说异常得不到处理,程序会调用标准库函数terminate终止函数的执行。

在这期间,栈上所有的对象都会被自动析构。

堆内存由程序员手动分配和释放,因此使用堆内存需要注意内存泄漏和内存溢出等问题。当程序员忘记释放已分配的内存时,会导致内存泄漏问题。而当申请的堆内存超过了操作系统所分配给进程的内存限制时,会导致内存溢出问题。

还是上代码

#include <iostream>
#include <string>

class Obj {
public:
    std::string name_;
    Obj(const std::string& name:name_(name{std::cout << "Obj( " << name_ << std::endl;};
    ~Obj( {std::cout << "~Obj( " << name_ << std::endl;};
};

Obj* makeObj( {
	Obj* obj = nullptr;
	try {
		obj = new Obj{"makeObj"};
		...
	} catch(... {
		delete obj;
		throw;
	}
	return obj;
}

Obj* foo( {
	Obj* obj = nullptr;
	try {
		obj = makeObj(;
		...
	} catch(... {
		delete obj;
	}
	return obj;
}
int main( {
    Obj* obj = foo(;
    ...
    delete obj;
}

可以看到,由makeObj函数创建的堆变量obj, 在每个获取该变量的上层调用中,都需要关心对该变量的处理。这无疑极大得增加了开发者的心智负担。

RAII

RAII利用栈对象在作用域结束后会自动调用析构函数的特点,通过创建栈对象来管理资源。在栈对象构造函数中获取资源,在栈对象析构函数中负责释放资源,以此保证资源的获取和释放。

#include <iostream>

class AutoIntPtr {
public:
    AutoIntPtr(int* p = nullptr : ptr(p {}
    ~AutoIntPtr( { delete ptr; }

    int& operator*( const { return *ptr; }
    int* operator->( const { return ptr; }

private:
    int* ptr;
};

void foo( {
	AutoIntPtr p(new int(5;
    std::cout << *p << std::endl; // 5
}

int main( {
    foo(;
}

上面例子中,AutoIntPtr类封装了一个动态分配的int类型的指针,它的构造函数用于获取资源(ptr = p,析构函数用于释放资源(delete ptr。当AutoIntPtr超出作用域时,自动调用析构函数来释放所包含的资源。

std::unique_ptr和std::shared_ptr等智能指针用于内存管理类,使得内存管理变得更加方便和安全。这些内存管理类可以自动进行内存释放,避免了手动释放内存的繁琐工作。值得一提的是,上面的AutoIntPtr就是一个简化版的智能指针了。

总结

本文介绍了C++中的内存管理机制,包括内存分区、栈、堆和RAII技术等内容。通过学习本文,我们可以更好地掌握C++的内存管理技巧,避免内存泄漏和内存溢出等问题。

编程笔记 » [重学C++]01| C++ 如何进行内存资源管理

赞同 (38) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽