前言
本期的主题是栈作用域中对象的生存期,通俗来讲,就是讨论对象是如何在栈上生存的。
这章内容整体分为两部分。
- 第一部分是,你必须理解栈上的东西是如何存在的,这样你才能真正写出能正常工作的代码。
- 第二部分是,一旦你知道了它是如何运作的,那要如何利用好它,做你想做的事情,能想出一些聪明的方法来做一些事情。
首先我们先要了解栈的概念。
栈是一种数据结构,你可以在上面堆叠一些东西。
假设你的桌子上有一堆书,为了访问中间的一个,你得先把上面的前几个拿掉,然后找到中间那本书,(当然现实世界中,你可以直接把书抽出来,但这不是栈在编程中的工作方式)。
所以每次我们在 C++ 中进入一个作用域时,我们是在 push 栈帧,但它不一定非得是将数据推进(push)一个栈帧,你可以把它想成是把一本书放在书堆上,在此作用域下(这本书内)声明的变量,就像是在书中写东西,一旦作用域结束,将这本书从书堆中拿出来,然后扔掉,那么在书中申明的每一个基于栈的变量,以及你在书中栈里创造的所有对象就都消失了。
这样有好处也有坏处,但是如果你知道你自己在做什么,百分百是有好处的。接下来一起来看一些例子,了解一下这一切是如何结合在一起的以及这一切是如何运作的。
01 什么是作用域。
作用域有很多使用场景。比如函数作用域,类作用域,比如 if 语句作用域,或者是 for 循环、while 循环作用域,甚至是空作用域。
我们来完善一个类的例子来说明。
02 在栈中创建对象
#include <iostream>
class Demo
{
public:
Demo()
{
std::cout << "构造函数" << std::endl;
}
~Demo()
{
std::cout << "析构函数" << std::endl;
}
};
int main()
{
Demo demo;
return 0;
}
这是一个简单的 Demo 类,包括构造函数和析构函数。在下面的空作用域中声明了一个 Demo ,这样写,就不是将它创建在堆上,而是创建在栈上,这将调用默认构造函数,
在main函数设置断点调试一下。
运行一步后,可以看到 "构造函数" 被打印到控制台,到了作用域的最后 },再运行一步,可以看到 "析构函数" ,意味着正在摧毁 Demo类,这部分内存已经被释放了。
03 在堆中创建对象
怎么在堆中创建对象呢?
很简单,加*和加new关键字
而如果改为堆分配,则是这样的效果。
断点调试一下
可以看到 "构造函数!" 后程序直接跳过了作用域的 } 这一行,并没有输出 "析构函数" ,Demp永远不会被销毁(当然,当应用程序终止时,操作系统会清除这些内存)。现在你应该已经明白了基于栈的变量和基于堆的变量在对象生存期上的区别,基于栈的变量,一出作用域就被释放了,这就是本章的重点。
04 堆中创建变量的案例
现在有了这个基础后,我们来看看一些你可能会经常做的事情。
我们可能在函数中创建数组,然后返回一个 int 类型指针,我们可能会在函数中写下面这些代码。
这看起来非常合理,但是,这完全是不对的。
这样的数组并没有在堆上分配,因为没有使用 new,我们只是在栈上声明它,当返回一个指向它的指针时,它返回一个指向栈内存的指针,这个栈内存,一旦离开作用域,内存就会被清除,在使用的时候就会失败。
如果你想实现你想要的功能,有两个选择。
- 第一,你可以在堆上分配数组,从而确保它的生存期会一直存在。
- 第二,你可以将这里创建的数据赋值给一个在栈作用域之外存在的变量,采取传参或者其他的方式来操作数组。在这个意义上,我们只是传递一个指针,所以不会做分配这个操作。
这个错误很常见,那有没有一种方法,可以将它这个特性用于好的方面呢?
答案是肯定的,它在很多方面都非常有用,比如可以帮助我们自动化代码。
其中一件事就是作用在类的作用域,例如智能指针 smart_ptr,或是 unique_ptr,作用域指针,或者像作用域锁(scoped_lock),有很多例子,本期先不涉及。但最简单的例子可能是作用域指针。
作用域指针基本上是一个类,一个指针的包装器,在构造时用堆分配指针,然后在析构时删除指针。所以我们可以自动化这个 new 和 delete。让我们看看如何编写这样的类。
上面是一个 DemoPtr 的类,有一个 Entity 指针成员,有一个参数为 Demo指针类型的构造函数,并完成初始化,在析构函数中,调用 delete 释放成员指向的内存。
这是一个基本的作用域指针,让我们看看如何使用它。
可以看到,这里还有一个隐式转换的过程。
设置断点调试一下。
可以看到,即使我们用 new 来做堆分配,还是可以做到内存释放。
这里不好看出来,你们还是拿代码测试一下
这是一个很好的例子,其实这是 smart_ptr、unique_ptr 做的最基本的事情。
这种自动构造,自动析构这种基于栈的变量,离开作用域后会自动销毁,是非常有用的。
#include <iostream>
class Demo
{
public:
Demo()
{
std::cout << "构造函数" << std::endl;
}
~Demo()
{
std::cout << "析构函数" << std::endl;
}
};
class DemoPrt
{
private:
Demo* m_demo;
public:
DemoPrt(Demo* demo)
: m_demo(demo)
{
}
~DemoPrt()
{
delete m_demo;
}
};
int * demoarr()
{
return NULL;
}
int main()
{
DemoPrt d = new Demo();
return 0;
}