C++中的变量存储在哪里?

19

C++中的变量存储在哪里?

是存储在RAM还是处理器缓存中?


这个问题没有意义,因为(大多数)缓存是透明的,实际上只是内存系统的一部分。它也存在缺陷,因为C++(或任何编译语言的)变量存储位置实际上取决于架构和编译器。 - Todd Gamblin
此外,问题标题可以得到显著改善。 - jonner
1
@Tal,正如其他人所说,问题的陈述有点模糊。也许你可以看一下大家的评论,看看能否提出更具体的问题。 - Onorio Catenacci
2
我们能对那些不应该被问的问题做些什么呢?我可以用“Mu”标记它们吗? - user3458
好的,我已经完成了。http://zh.wikipedia.org/wiki/吴 - user3458
9个回答

46

命名变量被存储在:

  • 栈中,如果它们是函数局部变量。
    C++ 称之为 "自动存储"1,并不要求它实际上是汇编调用栈,在一些罕见的实现中它并不是。但在主流实现中是这样的。
  • 如果它们是全局或 static 变量,则存储在每个进程的数据区中。
    C++ 称之为 "静态存储类",通过在 section .data.bss.rodata 或类似的位置放置/保留字节来实现。

如果变量是使用 int *p = new int[10]; 或类似方法初始化的指针,则指针变量 p 将像上面所述一样存储在自动存储或静态存储中。内存中的指向对象为:

  • 堆(C++ 称之为动态存储),使用 newmalloc 等分配。
    在汇编中,这意味着调用一个分配器函数,如果其空闲列表为空,则最终可能通过某种系统调用从操作系统获取新内存。 "堆" 在现代操作系统 / C++ 实现中不是一个单一的连续区域。
C和C++不会自动进行垃圾回收,命名变量本身不能放在动态存储(“堆”)中。动态存储中的对象是匿名的,除了被其他对象指向之外,其中一些对象可能是适当的变量。(结构体或类类型的对象,与像int这样的原始类型不同,可以让您引用该匿名对象中的命名类成员。 在成员函数中,它们甚至看起来相同。)
这就是为什么你不能(安全/有用地)返回指向局部变量的指针或引用的原因。

当然,这全部都是在RAM中的。缓存对用户空间进程透明,但它可能会影响性能。

编译器可以优化代码以将变量存储在寄存器中。这高度依赖于编译器和代码,但好的编译器会积极地这样做。


注1:有趣的事实:auto在C++03及之前版本以及现在的C语言中表示自动存储类别,但现在(C++11)它可以推断类型。


8
实际上,变量并不会存储在堆中。你可能会有一个指向堆中某个位置的变量,但是变量本身会被存储在寄存器、栈上或静态分配的内存中。 - Kristopher Johnson
Kristopher,你说得很对。在C++的定义中,变量是指针,而不是指向数组的指针,所以你是正确的。 - Dan Lenski
请注意,在寄存器中存储变量高度依赖于平台。不同的架构具有不同数量的寄存器,并且并非所有架构中的寄存器都相同。 - David Thornley
3
@Kristopher:你的说法并不完全正确。类对象的成员变量确实被存储/分配在堆上。 - Chethan
@Chethan:你的意思是整个类对象本身都分配在堆上吗?是的,这是可能的,但不是必须的。在C++中,你可以在局部或全局范围内有一个myclass foo = { ... };变量,因此类对象本身(以及所有成员)都在自动(堆栈)或静态存储中,而不是动态(堆)。使用动态分配的存储来存储具有命名子对象的聚合对象与使用指针将该内存作为带编号元素的数组并没有根本区别。你仍然只能通过某些指针或引用访问它。 - Peter Cordes
@KristopherJohnson和Dan:我更新了这个答案,更加精确地说明了动态存储的重要性;实际上,动态分配的对象总是匿名的,而在一些垃圾回收语言中,局部变量可能会逃逸并被提升为堆对象。我尽量不让真正有用的信息被挤占太多,但是Dan你可能需要编辑掉我说的一些内容或者移动它们的位置。 - Peter Cordes

19

对于C++来说,通常情况下,变量存储在编译器决定的位置。除非你指定编译器处理方式,否则不应该做出其他假设。有些变量可以完全存储在寄存器中,有些可能会被优化并替换为某个字面值。在某些平台上,一些编译器实际上可能会将常量存储在ROM中。

关于“处理器缓存”的问题有点混淆。虽然有一些工具可以指导处理器如何处理其缓存,但总体来说,这是处理器的事情,应该对用户透明。您可以将高速缓存视为CPU与RAM之间的窗口。几乎任何内存访问都通过高速缓存。

另一方面,大多数操作系统会将未使用的RAM交换到磁盘上。因此,在某些时刻,您的变量实际上可能会存储在磁盘上,但这种情况较少发生。


1
我知道编译器可以决定做任何它想做的事情。目前是否有编译器执行与通常不同的操作(例如自动=堆栈或寄存器,分配=帮助等)? - user231536
1
@user231536:对于像PIC和8051这样的架构,使用调用堆栈来进行标准C模型是很困难的,显然有一些编译器会将自动存储类变量放入静态存储中。(如果您想要这样做,您必须特别声明函数为可重入。)Supercat在为什么C到Z80编译器会产生糟糕的代码?中对此发表了评论。总的来说,该问答中充满了C语言不易映射到汇编语言的例子。(还有一些汇编语言的代码与现代优化编译器相比较差。) - Peter Cordes

16

变量通常存储在RAM中,它们可以在堆(例如全局变量、方法/函数中的静态变量)或栈(例如在方法/函数内声明的非静态变量)上。堆和栈都是RAM,只是不同的位置。

指针有点特殊。指针本身遵循上述规则,但它们所指向的数据通常存储在堆上(使用malloc创建的内存块或使用new创建的对象)。但是您可以创建指向栈内存的指针:int a = 10; int * b = &a;; b指向a的内存,而a存储在栈中。

CPU缓存中放入什么超出编译器的控制范围,CPU自己决定要缓存什么以及缓存多长时间(取决于因素如“这些数据最近是否被使用过”或“预计这些数据很快会再次使用吗”),当然缓存的大小也会产生很大影响。

编译器只能决定哪些数据进入CPU寄存器。通常情况下,如果一行代码中频繁访问某个数据,则将其保存在寄存器中,因为寄存器访问速度比缓存和RAM更快。在某些系统上,某些操作只能在数据在寄存器中时才能执行,在这种情况下,编译器必须在执行操作之前将数据移动到寄存器中,并且只能在何时将数据移回RAM。

编译器始终会尝试将最常访问的数据保存在寄存器中。当调用一个方法/函数时,通常所有寄存器值都会被写回RAM,除非编译器可以肯定所调用的函数/方法不会访问内存中来自的数据。同时,在方法/函数返回时,必须将所有寄存器数据写回RAM,否则新值将会丢失。在某些CPU体系结构中,返回值本身是通过寄存器传递的,否则通过堆栈传递。


8
在C++中,变量存储在栈或堆上。
栈:
堆:
int x;

堆:

int *p = new int;

话虽如此,两者都是在RAM中构建的结构。

如果您的RAM使用率很高,Windows可以将其交换到磁盘。

当变量进行计算时,内存将被复制到寄存器中。


7

C++不知道你处理器的缓存。

当你运行一个用C++或其他语言编写的程序时,你的CPU会在缓存中保留“热门”的RAM块的副本。这是在硬件级别上完成的。

不要把CPU缓存看作“其他”或“更多”的内存...它只是一种将某些RAM块保持靠近的机制。


4
我认为你混淆了两个概念。一是C++语言如何在内存中存储变量;二是计算机和操作系统如何管理该内存。
在C++中,变量可以分配在堆栈上,这是程序保留的内存,线程启动时大小固定,或者在动态内存中分配,可以使用new进行分配。编译器还可以选择将变量存储在处理器的寄存器中,如果对代码的分析允许的话。这些变量永远不会看到系统内存。
如果一个变量最终出现在内存中,则操作系统和处理器芯片集会接管。基于堆栈的地址和动态地址都是虚拟的。这意味着它们可能在任何给定时间内驻留在系统内存中,也可能不在。内存中的变量可能存储在系统内存中,分页到磁盘上,或者可能驻留在处理器附近的高速缓存中。因此,很难知道数据实际上存储在哪里。如果程序没有闲置一段时间,或者两个程序正在竞争内存资源,值可以保存到页面文件中,并在轮到程序运行时恢复。如果变量局部于某些工作,则在刷新回系统内存之前,它可能在处理器高速缓存中被修改多次。你编写的代码不会知道发生了什么。它所知道的只是有一个地址可以操作,所有其他系统都会处理其余部分。

在大多数现代系统中,堆栈的大小不是固定的,而是在出现页面错误(由于空堆栈)时由操作系统自动扩展。 - Paul de Vrieze
1
在你的回答中,非常清楚地表明了两件不同的事情正在发生:语言的“对象模型”和RAM / SwapFile / Caching系统。好答案! - xtofl
嗨 Paul。感谢您的评论。您是正确的,堆栈是虚拟内存,可以进行分页。我的意思是在线程启动时分配时它的大小是固定的。这是由链接器控制的。 - Todd
Linux上的堆栈大小(对于“main”线程的堆栈)受到ulimit -s设置的限制,该进程从其父进程继承,而不是由链接器设置。此外,整个大小在进程启动时技术上未映射(因此仅在/proc/self/maps中显示出较小的初始大小),更不用说在硬件页表中固定了。但是它是保留的,因此其他映射(如mmap(MAP_ANONYMOUS))不会窃取它。触及堆栈指针下方的内存会触发映射的自动扩展。什么是“自动堆栈扩展”? - Peter Cordes
这种扩展与对分配的堆栈内存进行需求分页是分开的,后者通常对堆分配和BSS发生。 (新堆页面和进程启动时的BSS通常会被复制写入映射到一个全零的物理页面,因此读取不会分配新的物理页面。但虚拟地址空间完全映射的。) - Peter Cordes

1
C++语言通过变量支持两种内存分配方式:
静态分配是在声明静态或全局变量时发生的。每个静态或全局变量定义一个固定大小的空间块。该空间在程序启动时(exec操作的一部分)分配一次,并且永远不会被释放。
自动分配发生在声明自动变量(例如函数参数或局部变量)时。自动变量的空间在包含声明的复合语句进入时分配,并在退出该复合语句时释放。自动存储的大小可以是一个变化的表达式。在其他CPP实现中,它必须是一个常数。
第三种重要的内存分配方式是动态分配,它不由C++变量支持,但可用于库函数。
动态内存分配是一种技术,在程序运行时确定存储某些信息的位置。当您需要的内存量或需要的时间取决于程序运行前未知的因素时,就需要动态分配。
例如,您可能需要一个块来存储从输入文件中读取的行;由于一行可以有任意长度,因此必须动态分配内存并随着读取更多行而动态扩大。

或者,您可能需要为输入数据中的每个记录或每个定义分配一个块;由于您无法预先知道有多少个,因此必须在读取每个记录或定义时为其分配新块。

当您使用动态分配时,内存块的分配是程序明确请求的操作。您在想要分配空间时调用函数或宏,并使用参数指定大小。如果您想释放空间,则通过调用另一个函数或宏来执行此操作。您可以随时以任意频率执行这些操作。

C++变量不支持动态分配;没有存储类“dynamic”,也永远不会有C++变量的值存储在动态分配的空间中。获取动态分配的内存的唯一方法是通过系统调用,引用动态分配的空间的唯一方法是通过指针。由于它不太方便,而且实际的动态分配过程需要更多的计算时间,因此程序员通常仅在静态和自动分配都无法满足需求时才使用动态分配。

例如,如果您想动态分配一些空间来保存一个名为 foobar 的结构体,您不能声明一个类型为 struct foobar 的变量,其内容是动态分配的空间。但是,您可以声明一个指针类型为 struct foobar * 的变量,并将其赋值为该空间的地址。然后,您可以使用运算符‘*’和‘->’来引用该指针变量的空间内容:
 {
   struct foobar *ptr
      = (struct foobar *) malloc (sizeof (struct foobar));
   ptr->name = x;
   ptr->next = current_foobar;
   current_foobar = ptr;
 }

1

变量可以存储在许多不同的位置,有时甚至在多个位置。大多数变量在程序加载时放置在RAM中;有时声明为const的变量会被放置在ROM中。每当访问变量时,如果它不在处理器的缓存中,则会导致缓存未命中,并且处理器将停顿,直到将变量从RAM / ROM复制到缓存中。

如果您有任何半过得去的优化编译器,局部变量通常会存储在处理器的寄存器文件中。变量将在RAM、缓存和寄存器文件之间来回移动,但它们通常始终在RAM / ROM中有一份副本,除非编译器决定这是不必要的。


普通的、非嵌入式架构的编译器不会将变量放置在“ROM”中。 - Dan Lenski
ROM通常指的是在制造过程中只写入的内存 - const变量仍然存储在计算机的RAM中,但在程序执行期间不被写入。 - Chris Johnson
3
Stroustrup经常谈论存储在ROM中的变量。C++标准委员会也是如此(http://www.open-std.org/jtc1/sc22/wg21/docs/PDTR18015.pdf第75页)。实际上,这并不是指物理上的ROM,而是指可执行文件中数据的一个部分(在ELF中是.text节)。 - Max Lybbert

0

根据它们的声明方式,它们将被存储在 "" 或 "" 中。

堆是应用程序可以使用的 动态 数据结构。

当应用程序使用数据时,数据必须在被消耗之前移动到CPU的寄存器中,但这是一种非常不稳定和临时的存储。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接