浅谈C和C++中的资源释放
一、简介
本文主要探究在C/C++
中关于资源释放的有效途径。
作为一名程序员,本人的日常工作主要涉及Linux
平台下的嵌入式软件开发。在实际的开发过程中,经常困扰我的一件事就是如何实现资源(应该对资源有一个界定)的有效回收,避免内存泄漏,从而保证程序的稳定性。1
2
3
4
5
6
7
8
9
10
11
12
13
14int Function()
{
char *pStr = new char[10];
... ///包含一些具体的业务处理
if() ///异常判断
{
return -1; ///直接返回
}
...
delete pStr; ///释放资源
return 0; ///函数正常返回
}
在上图的代码片段中,首先向系统申请一段内存(位于堆上)。对于一些稍微严谨的程序员而言,在申请完资源后,很有可能会下意识的在函数尾补上delete pStr;
,以提醒自己不要忘记资源释放,这个函数在处理逻辑不够复杂的情况,凭借自己的小心机敏,也许不会出现漏洞。但是,随着业务
复杂性增高,抑或是换由他人来维护此代码,所谓百密一疏,难免会出现内存泄漏。
如何解决上述可能出现的内存泄漏问题呢?以下是个人的一些经验总结。本着互相交流,共同进步的目的,以下仅为一加之言,不当之处,希望各位批评指正。
二、实现资源释放的有效途径
由于C/C++
在本身语言特性上的差异,导致在具体的防止内存泄漏的处理上,略有不同,因此,该章节会针对两者分别介绍。
1. C实现资源释放的有效途径
(1) 巧用
do{}while{0}
不知道你是否看到过类似于下面的宏实现:
1 | ///通过宏定义交换两个整数 |
起初看到这段代码时,让我纠结的点不是说一个简单的交换两个整数的功能为何要用宏来实现(希望也不要遇到有人杠说,为什么还非要搞一个临时变量
nTemp
),而在于几行代码,为何还非要用一层do{}while{0};
来进行包裹,这不是多此一举么?不知道是否有人跟我有过同样的想法,但在我刚发现这点时,我还“自作聪明”的拉人一起,事实证明,自己简直是个智障。之所要用
do{}while{0};
包裹的原因在于:宏是在预编译阶段直接插进源代码的,因此,倘若没有这层包裹,那么临时变量int nTemp
就是我们“悄悄”定义在函数中的,此时,如果在一个函数中多次使用SWAP_INT
,势必会编译报错;但是,因为有了这层包裹,我们实际上就是定义了一个代码块,因此,int nTemp
的作用域就仅限于do{}while{0};
中,也就不会出现上述问题。此外,倘若我们在处理过程中需要终止宏定义的中的执行流的话,一条简单的
break;
即可实现。基于以上内容,我们可以对文章开头的代码修改如下:
1 | int Function() |
如此一来,我们既可以实现对资源的统一回收,又能够对返回值进行统一处理,岂不美哉?
(2) goto
语句实现资源回收
见名知义,其实就是通过goto强制跳转来实现资源的统一回收,其效果与do{}while{0};
一致:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int Function()
{
char *pStr = new char[10];
int nRst = -1; ///用以保存返回值
do
{
... ///包含一些具体的业务处理
if() ///异常判断
{
nRst = -2;
goto Rst;
}
...
}
while(0);
Rst:
delete pStr; ///释放资源
return nRst; ///函数正常返回
}
啧啧啧,虽然上述代码也实现了相同的目的,但是,倘若想要大范围使用该方式,未免显得“刺眼”,因此,个人并不推荐该方式。
2. C++实现资源释放的有效途径
(1) RAII——资源获取即初始化
RAII(Resource Acquisition Is Initialization),直译为“资源获取即初始化”,是C++语言的一种管理资源、避免泄漏的机制。
“资源获取”指的是那些在初始阶段需要获取,并在最后需要负责回收的资源,例如:
- 一个打开的文件句柄(后续需要关闭)
- 分配的一段内存(后续需要释放)
- 获取的一把锁(后续需要释放)
“初始化”意味着资源的获取通常发生在构造函数内部。当然,这也不是必须的,你也可以在后续对其进行初始化,只是,当我们在构造函数中对其进行初始化的话,会使问题变得简单。
可是,具有讽刺意味的是,缩略词RAII
中,并没有解释设计中最重要的部分——资源的释放与回收需要借助析构函数来完成。
为了说明这一点,我们借助以下例子来进行说明: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
int g_i = 0;
std::mutex g_i_mutex; // protects g_i
void safe_increment()
{
///持有该所,并尝试加锁
std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
///在函数返回前,对于分配在栈上的对象 lock ,会自动调用其析构函数,并解锁 g_i_mutex
}
int main()
{
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
在上述例子中,我们使用到了C++11
中的std::lock_guard
特性(使用方式详见:std::lock_guard)。std::lock_guard
的构造函数要求我们传入一把class Mutex
类型的锁,并尝试自动对其进行加锁,如此一来,就达到了资源保护的目的;此外,借助于C++
在函数返回前(或代码块结束时),会自动调用临时对象析构函数的机制,std::lock_guard
的析构函数,就会自动释放获取的锁。借用这种方式,我们将资源的获取与释放都交由系统来完成,换句话说,其实就是将我们手动申请的堆上的资源巧妙的委托给栈上的临时对象,并由其代我们进行管理。
(2) 智能指针
智能指针作为C++11中引入的新特性,其核心还是利用了面向对象的特点,在这里,我们直接引用官方给出的一个例子(关于智能指针的相关用法详见:C++智能指针简单剖析):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
struct Base
{
Base() { std::cout << " Base::Base()\n"; }
// 注意:此处非虚析构函数 OK
~Base() { std::cout << " Base::~Base()\n"; }
};
struct Derived: public Base
{
Derived() { std::cout << " Derived::Derived()\n"; }
~Derived() { std::cout << " Derived::~Derived()\n"; }
};
void thr(std::shared_ptr<Base> p)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::shared_ptr<Base> lp = p; // 线程安全,虽然自增共享的 use_count
{
static std::mutex io_mutex;
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << "local pointer in a thread:\n"
<< " lp.get() = " << lp.get()
<< ", lp.use_count() = " << lp.use_count() << '\n';
}
}
int main()
{
std::shared_ptr<Base> p = std::make_shared<Derived>();
std::cout << "Created a shared Derived (as a pointer to Base)\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
std::thread t1(thr, p), t2(thr, p), t3(thr, p);
p.reset(); // 从 main 释放所有权
std::cout << "Shared ownership between 3 threads and released\n"
<< "ownership from main:\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
t1.join(); t2.join(); t3.join();
std::cout << "All threads completed, the last one deleted Derived\n";
}
在我个人看来,智能指针对于资源的释放更具有普遍性,我们将句柄、内存空间、临时变量等手动申请的资源的控制权统一交由智能指针处理,借助临时变量在函数返回时自动析构的特性,可以实现资源的妥善管理。
但是由于部分特性是在C++17
等新标准加入的,而在实际工程实践中,服务器上的编译器版本较低,暂不支持某些特性,这就导致代码在可移植性上收到了影响,因此,这也是一个不得不考虑的问题。
三、总结与反思
1. 总结
在实际的工程开发过程中,C/C++
似乎总是形影不离,我们在编程过程中也不会对两者进行严格的区分。但是,当我们在讨论上述问题时,我还是有意的把它给区分开来,为什么要这样做呢?
编程语言作为我们工作中的一种工具,所谓万变不离其宗,但是由于语言本身的特性,所以在其具体的应用领域可能稍有不同。就拿C而言,通过指针,我们可以方便而灵活的实现数据操作,但随之而来的是,由于程序员的疏忽,可能带来意想不到的问题。因此,在基于对稳定性以及效率的考虑而言,就需要我们根据具体情况来进行选择。
坦白来讲,私以为:在以往的工程实践中,相较于有限的效率提升,程序的稳定行才是更值得考虑的问题。
而且C++
作为一门比C
更“高级”的语言,虽然屏蔽了一些底层实现(这一部分其实还有待探讨,因为更多时候,它只是代替我们做了一些琐碎的工作),但无疑也更为周全,减少了程序员犯错的机会。
2. 关于关于程序员扮演角色的反思
在刚开始学编程的时候,不知是在哪里看到过一句:
…
开发的前提是,我们假定程序员是不会犯错的
…
——佚名
对于每个程序员而言,想必都不愿听到一句话是:“你的程序似乎有Bug啊。。。”
天不遂人愿,尽管我也曾有过美好的希冀,但是在实际的工作过程中,难免会犯各种错误。
所谓“吃一堑长一智”,发现问题时,我们可以及时的对问题的进行总结分析,避免再犯;有时也会尝试自己造个轮子,或者是通过公共库来实现那些重复,琐碎的操作。我想,这样也无可厚非。但是,随着经验的增加,我们对代码进行层层封装,仅需调用一个接口,具体的实现都交由底层来完成,这样一来,对于企业而言,确实降低了出现问题的风险。但是对于程序员个人而言,这又会产生怎样的后果呢?
我们每天的工作可能仅是调用高度抽象的接口,工作是轻松了许多,但是倘若有天接口变了,或是,或是有一天你跳槽了呢?Hr问你都擅长哪些领域,你说我在上加公司,接口调的贼6,这,,,也许只能说是人家接口写的好而已吧?换言之,倘若有天你进入到一家类似的公司,工作内容就是按照接口文档实现相应功能,这你又要作何感想呢?——纵然工作轻松了很多,可是,我们对于知识的理解又有多少呢?
作为程序员,有时我们会自嘲说每天的工作就是搬砖的。可是在领导眼里,作为普通的程序员,我们是扮演着怎样的角色呢?如果我们对于工作的态度,总是不求甚解,也许有一天,当日的自嘲,竟成了事实。也学这也是需要我们警惕的吧?
四、参考与链接
- C语言块级变量,在代码块内部定义的变量:http://c.biancheng.net/cpp/html/3466.html
- C语言中使用goto语句:https://blog.csdn.net/baidu_36649389/article/details/54582619
- C++智能指针简单剖析:https://www.cnblogs.com/lanxuezaipiao/p/4132096.html
- C++11新特性总结:https://www.cnblogs.com/wangqiguo/p/5635441.html
- std::lock_guard:https://en.cppreference.com/w/cpp/thread/lock_guard
五、文档信息
作者: Litost_Cheng
发表日期:2019年06月17日
更多内容: