内存:栈和堆(C / Swift)

栈(C 语言描述)


什么是栈(Stack)?这是计算机内存中的一个特殊区域,它存储由每个函数创建的临时变量(包括 main() 函数)。栈是一个“LIFO”(后进先出)的数据结构。它是被 CPU 管理和优化的。每次函数声明一个新变量时,它都被“压入”栈中。然后每次函数退出时,所有由该函数压入栈的变量都被释放(也就是说,它们被删除)。一旦释放栈变量,该区域的内存就可用于其他栈变量。

使用栈来存储变量的优点是内存是自动为你管理的。你无需手动分配内存,或者在你不再需要时释放内存。更重要的是,由于 CPU 如此高效地组织栈内存,读取和写入栈变量的速度非常快。

理解栈的关键是这样一个概念:当一个函数退出时,它的所有变量都从栈中弹出(因此永远丢失)。因此栈变量本质上是本地的。这与我们之前看到的一个概念(称为变量范围)或局部变量或全局变量有关。 C 编程中的一个常见错误是尝试访问某个函数内栈上创建的变量,该函数在该函数之外的某个地方(即该函数退出后)从该程序中创建。

要记住栈的另一个特性是,可以存储在栈上的变量的大小有一个限制(随OS变化)。对于在堆中分配的变量,情况并非如此。

栈的总结:

  • 随着函数压入和弹出局部变量,栈增长和缩小
  • 没有必要自己管理内存,变量内存被自动的分配和释放
  • 栈具有大小限制
  • 栈变量仅在创建它们的函数正在运行时才存在

堆(C 语言描述)


堆是计算机内存中的一个区域,不会自动为你进行内存管理,并且不受 CPU 的严格管理。它是一个更自由的内存区域(并且更大)。要在堆上分配内存,必须使用 malloc()calloc() ,它们是内置的 C 函数。一旦你在堆上分配了内存,当你不再需要它的时候,你就有责任使用 free() 来释放内存。如果你没有做到这一点,你的程序将会有所谓的内存泄漏。也就是说,堆上的内存仍然会被留出(并且不会被其他进程使用)。

与栈不同,堆没有栈那样大小的限制(除了计算机明显的物理限制外)。
由于必须使用指针访问堆上的内存,因此堆内存的读取和写入速度稍慢。
与栈不同,在堆中创建的变量可以通过程序中任何位置的任何函数访问。
堆变量实质上是全局的。

栈与堆的优点和缺点


  • 访问速度非常快
  • 不必显式的释放分配的内存
  • 空间由 CPU 高效管理,内存不会变得碎片化
  • 只有局部变量
  • 栈的大小有限制(取决于操作系统)
  • 变量不能调整内存大小

  • 变量可以全局访问
  • 内存大小没有限制
  • (相对)较慢的访问
  • 没有保证有效利用空间,随着内存块被分配,内存随着时间的推移可能变得碎片化,然后释放
  • 你必须手动管理内存(分配和释放)
  • 可以使用 realloc() 来调整变量的内存大小

例子🌰


下面是一个简短的程序,它在上创建它的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

double multiplyByTwo (double input) {
double twice = input * 2.0;
return twice;
}

int main (int argc, char *argv[])
{
int age = 30;
double salary = 12345.67;
double myList[3] = {1.2, 2.3, 3.4};

printf("double your salary is %.3f\n", multiplyByTwo(salary));

return 0;
}

// double your salary is 24691.340

在第 10, 11 和 12 行,我们声明了变量:一个 int ,一个 double 和一个由三个双精度组成的数组。只要 main() 函数执行,这三个变量就会被压入栈。当 main() 函数退出(并且程序停止)时,这些变量将从栈中弹出。类似地,在函数 multiplyByTwo() 中,一旦 multiplyByTwo() 函数执行, double 变量,即 double 将被压入栈。一旦 multiplyByTwo() 函数退出,两个变量就从栈中弹出,并且永远消失。

另一点说明,有一种方法可以告诉 C 保存栈变量,即使在其创建者函数退出,就是在声明变量时使用 static 关键字。用 static 关键字声明的变量因此变成类似于全局变量的变量,但是仅在创建它的函数内部可见。这是一种奇怪的结构,除非在特定的情况下,否则你可能不需要这种结构。

下面是该程序的另一个版本,它将它的所有变量分配给而不是

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
#include <stdio.h>
#include <stdlib.h>

double *multiplyByTwo (double *input) {
double *twice = malloc(sizeof(double));
*twice = *input * 2.0;
return twice;
}

int main (int argc, char *argv[])
{
int *age = malloc(sizeof(int));
*age = 30;
double *salary = malloc(sizeof(double));
*salary = 12345.67;
double *myList = malloc(3 * sizeof(double));
myList[0] = 1.2;
myList[1] = 2.3;
myList[2] = 3.4;

double *twiceSalary = multiplyByTwo(salary);

printf("double your salary is %.3f\n", *twiceSalary);

free(age);
free(salary);
free(myList);
free(twiceSalary);

return 0;
}

正如你所看到的,使用 malloc() 在堆上分配内存,然后使用 free() 来释放它,没有什么大不了的,但有点麻烦。另外要注意的是,现在到处都有一堆星号(*)。那些是什么?答案是,他们是指针。 malloc()calloc()free() )函数处理的是指针而不是实际值。指针是 C 中的一种特殊数据类型,它将地址存储在内存中,而不是存储实际值。因此,在上面的第 5 行中,两次变量不是 double,而是一个指向 double 的指针。在内存地址中,这是一个 double 类型的数据。

何时使用堆?何时使用栈?


什么时候应该使用堆,什么时候应该使用栈?

  • 如果你需要分配一大块内存(例如一个大的数组,或者一个大的结构体),并且你需要在很长的时间内保存这个变量(比如全局变量),那么你应该把它分配到堆上。
  • 如果你正在处理的是只有在使用它们的函数仍然存在时才需要保持的小变量,那么你应该使用栈,因为它使用起来更容易也更快。
  • 如果你需要动态改变大小的数组和结构体等变量(例如可根据需要增长或缩小的数组),那么你可能需要在堆上分配它们,并使用动态内存分配函数,如malloc()calloc()realloc()free() 来“手动”管理该内存。

数据在内存中的存储(C 语言描述 )


这里有一条黄金法则:

  • 引用类型总是被分配到“堆”上。
  • 值类型总是分配到它声明的地方:
    • 作为引用类型的成员变量分配到“堆”上
    • 作为方法的局部变量时分配到“栈”上

结构体在内存中的存储(Swift 语言描述)


与 C 语言不同,苹果对 Swift 中的结构体有着优化,使其能够存储在栈上,进而加快访问速度与增加操作的安全性。

如果你的结构体只由其他结构体组成,那编译器可以确保不可变性。同样地,当使用结构体时,编译器也可以生成非常快的代码。举个例子,对一个只含有结构体的数组进行操作的效率,通常要比对一个含有对象的数组进行操作的效率高得多。这是因为结构体通常要更直接:值是直接存储在数组的内存中的。而对象的数组中包含的只是对象的引用。最后,在很多情况下,编译器可以将结构体放到栈上,而不用放在堆里。

1
2
3
4
5
6
7
func uniqueIntegerProvider() -> AnyIterator<Int> {
var i = 0
return AnyIterator {
i += 1
return i
}
}

Swift 的结构体一般被存储在栈上,而非堆上。不过这其实是一种优化:默认情况下结构体是存储在堆上的,但是在绝大多数时候,这个优化会生效,并将结构体存储到栈上。当结构体变量被一个函数闭合的时候,优化将不再生效,此时这个结构体将存储在堆上。因为变量 i 被函数闭合了,所以结构体将存在于堆上。这样一来,就算 uniqueIntegerProvider 退出了作用域,i 也将继续存在。与此相似,如果结构体太大,它也会被存储在堆上