当我们在C中编写以下代码行时,
char local_arr[] = "I am here";
字面意思“我在这里”存储在内存的只读部分(比如 RM )。我如何想象它是在RM中连续存储的(是吗?)。然后,数组 local_arr (即本地数组)通过索引从RM中的位置复制此数组索引。
但 local_array 复制之后,文字会发生什么?它是否丢失从而导致内存泄漏?或者是否有类似Java的垃圾收集器清理未引用的对象?
例如,如果我写一段代码如下:
for(int i=0;i<100000;i++)
char local[] = "I am wasting memory";
我不会用完内存吗?每次迭代都会在每次迭代时创建相同文字的新实例吗?或者它们都会引用相同的文字,因为每次文字的值是相同的?
RM 是属于堆内存还是堆中的专用段?
本地数组也存储在堆栈中,对吧?如果我使用动态数组或全局数组该怎么办?那么会发生什么?
答案 0 :(得分:2)
C没有垃圾收集,因此如果您忘记使用正确的解除分配器释放已分配的内存,则会出现内存泄漏。
虽然有时使用像Boehm collector这样的保守垃圾收集器,但这会导致许多额外的麻烦。
现在,C中有四种类型的内存:
malloc
,calloc
,realloc
等内容。不要忘记free
resp。使用另一个适当的解除分配器。您的示例使用local_arr
的自动内存,并使实现免费将其初始化为提供的文字,无论哪种方式最有效。
char local_arr[] = "I am here";
除其他外,这可能意味着:
memcpy
/ strcpy
并将文字放入静态内存。同样有趣的是,C常量文字没有身份,因此可以共享空间 无论如何,使用as-if规则,可以多次优化静态(甚至动态/自动)变量。
答案 1 :(得分:1)
不是答案(Deduplicator已经给出了一个好的答案,我想),但也许这会说明你的问题......
考虑以下C代码:
#include <stdio.h>
int main() {
char foo[] = "012";
/* I just do something with the array to not let the compiler
* optimize it out entirely */
for(char *p=foo; *p; ++p) {
putchar(*p);
}
putchar('\n');
return 0;
}
使用汇编程序输出(在我的机器上使用GCC):
[...]
.LC0:
.string "012"
[...]
main:
[...]
movl .LC0(%rip), %edi
你在只读内存中有一个字符串(该字符串将从程序启动到出口持续存在)。当我将初始化foo
的行更改为
char foo[] = "0123";
GCC认为这样做是值得的:
movl $858927408, (%rsp) # write 858927408 long (4 bytes) to where the stack pointer points to
movb $0, 4(%rsp) # write a 0 byte to the position 4 bytes after where the stack pointer points to
858927408
为0x33323130
(0x30
是'0'
的ASCII代码,0x31
的{{1}}等等;在后一种情况下,字符串不存储在只读存储器中,它存储在指令本身中。在这两种情况下,您最终访问的阵列始终位于堆栈中。在这种情况下,你永远无法访问只读内存中的字符串文字,即使它存在。
HTH
答案 2 :(得分:0)
数组根据其范围类型存储在cml(即连续的内存位置)中。 例如,全局(静态)数组将保存在作为数据段的一部分的符号(bbs)开始的块中,而本地作为计算机存储器中的堆栈创建。 它是一个字符串,因为数组中的每个元素都指向它的下一个元素,形成字符序列,形成字符串。 根据问题的新变化进行编辑 这样做:
char str[] = "Hello World";
你这样做:
char str[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'};
由于最后一个字符为'\0'
/ NULL
/ 0
您不会将信息填充到存储数据类型的最后一个内存块中。在这种情况下,您将终止字符串,但您不会收到泄漏。这就是C如何处理char数组,尤其是字符串。它们是以null结尾的字符串。很多像strlen这样的函数只有在有空终止符的情况下才有效。
此外,如果您使用动态创建的数组,它们将存储在堆中。 我知道堆不多,基本上它提供了一个分配环境并为此目的管理内存。
答案 3 :(得分:-1)
字符串文字存储在静态区域中。将字符串文字复制到局部变量时,将有两个副本:静态区域和堆栈。静态区域中的副本不会被删除。 C中没有GC。 但是如果在函数中使用指针,则可以访问该字符串。
#include <stdio.h>
char *returnStr()
{
char *p="hello world!";
return p;
}
char *returnStr2()
{
char p[]="hello world!";
return p;
}
int main()
{
char *str=NULL;
char *str2=NULL;
str=returnStr();
str2 = returnStr2();
printf("%s\n", str);
printf("%s\n", str2);
getchar();
return 0;
}
所以在第一个函数中,它会打印字符串,因为它使用指针。在第二个函数中,堆栈中的字符串将被删除,因此它将打印垃圾。
答案 4 :(得分:-1)
每次循环命中时,程序都不会创建新的字符串。只有一个字符串已经存在,而文字只是指那个数组。
当编译器看到常规字符串文字时,它会创建 * 一个char
的静态数组(C11§6.4.5/ 6) ,C99§6.4.5/ 5)包含字符串的内容,并将数组(或创建它的代码)添加到其输出。 *
函数中发生的唯一分配是char local_arr[] =...
,它为字符串内容的副本分配足够的空间。因为它是本地的,所以当控制离开定义它的块时它会被有效释放。并且由于大多数编译器实现自动存储(即使对于数组)的方式,它基本上不会泄漏。
* (每个文字可能最终都在它自己的数组中。或者,相同的字符串文字可能引用相同的数组。或者,在某些情况下,甚至可能完全消除该数组但这是所有与实现相关的和/或与优化相关的东西,与大多数定义良好的程序无关。)
答案 5 :(得分:-1)
您编写的循环不会执行任何操作,如果您在现代优秀的编译器完成处理后运行该程序,则您编写的任何一条指令实际上都不会发生。
TLDR:如果您以后无法访问它,那么您要么存在内存泄漏,要么覆盖了数据。由于您没有在代码中使用指针,因此数据存储在您的调用堆栈中 - 函数调用中的每个变量都指定了一个固定的永久内存槽。这意味着不 - 它不会吃光所有的 RAM。不过,这可能不适用于所有编程语言。
大多数人会跳过许多基本步骤,这些步骤将帮助您了解事物为何如此,以及计算机真正是如何工作的。只需要一点点知识就可以让你成为一名出色的程序员。
我觉得你是那些足够好奇的人之一,所以我决定写一个长答案......
我很幸运,我出生在了解它们的工作原理的最佳时间。今天的人们就没那么幸运了,因为计算机的“陈词滥调”被密封在由可触摸玻璃屏幕覆盖的单个防水镁片后面。
<块引用>它们并不平凡,它们确实是一项了不起的工程成就。但经过多次反复试验,工程师和研究人员得出了一些很容易理解的东西。
这种简单为拥有它的人提供了强大的力量,但当他们向他人隐藏它时。一切都很简单,一旦你理解了它。如果它不简单,它就不会成功。这就是为什么大多数仍然存在的东西很容易理解的原因。 :)
当你的源代码被编译时,结果是一个“字节串”/blob/char-array,不管你想怎么称呼它。我将其称为 source[]
。
首先介绍一些背景知识,您可以选择跳到下面的“代码存储器和数据存储器”部分。 :)
在“过去”,CPU 没有 MMU 设备 - 因此没有“只读”RAM。然而,一些计算机区分代码存储器和数据存储器——值得注意的是哈佛架构和冯诺依曼架构。
关于哈佛架构有很多要说的,所以我建议你在https://en.wikipedia.org/wiki/Harvard_architecture阅读它,但在这种情况下 - 重要的是代码属于无法访问的内存范围由您的程序代码。
我猜它不是“通过仔细考虑各种选项而设计的”,而是随着计算机的发明而自然进化的结果;代码记忆实际上就是开关和打孔卡...
它们已经不存在了...
但是修改后的哈佛架构确实如此,而且真的没有必要了解它与我在下面提到的下一个架构之间的区别。
我认为不值得参与关于现代计算机是哈佛还是冯诺依曼的讨论,因为很明显,冯诺依曼计算机正在“模仿”哈佛架构的好处。已经没有明显的区别了。
今天的大多数计算机都是这种类型的架构。软件可以写入代码存储器和数据存储器。任何内存地址都没有什么特别之处。但是在计算机中,某些软件具有其他软件所没有的功能。特别是 KERNEL
(鼓声)。
在更现代的 CPU 设计中,可以虚拟化内存地址。这以前是一个称为 MMU(内存管理单元)的特殊组件。每当 CPU 想要访问内存地址时,MMU 都会将该地址请求转换为不同的虚拟化地址。今天,我怀疑 MMU 是 CPU 内部的 - 但我会谈论这个概念,好像还有一个 MMU。
MMU 是神奇的小芯片,它使您的程序相信它具有连续的可寻址存储器序列 - 因此它使您的程序非常易于理解,这使我可以轻松解释它。当我还是 90 年代的青少年时,程序员就更难了,而我(或感觉)是我所在城市中唯一听说过互联网的人。
通常,这种内存地址转换适用于称为“页面”的 4 KB(左右)内存块。页面大小是一个讨论主题,可能会有所不同。如果您选择更大的页面大小,则这些内存页面的元数据和查找表占用的内存更少。
对于分配的每个页面,内核会告诉 MMU 用“进程所有者 ID”、“交换到磁盘”、“共享”标志、“可执行”标志、“只读'标志和实际的物理内存地址。可能不是完全这些特定标签,但我想说明计算机在管理内存地址方面的能力。
<块引用>如果程序尝试访问交换到磁盘的内存地址,MMU 会在连接到 CPU 的引脚上放一些电。当 CPU 感觉到电震动时,它会立即喷出内部存储在其寄存器中的所有数据,并开始处理内核中某处的指令。这就是 interrupt
的本质。这没什么神奇的。它只是导致 CPU 跳转到其他地方的某些代码,同时确保内核可以再次跳转回来 - 假装什么也没发生。我们称之为多任务处理。
“不幸的是”,我知道很多关于计算机的东西,所以我倾向于打断自己,喷出更多的旁注。也许我就是那个经常脱口而出的人你知道吗....,而大多数人都在翻白眼。不是因为他们知道我要说什么,而是因为大多数人并不关心——他们只是接受事情的现状,然后继续他们关心的事情。根据我的经验,了解事物比了解事物更有价值。
<块引用>旁注:在 iOS 设备上,代码内存会自动标记为只读可执行文件,其他所有内容都是可写的,不可执行的。这使得操作系统本质上更不容易受到多种形式的攻击 - 但它也使您无法带来自己的高级功能,如抖动。这意味着您被迫使用 Apple 提供的技术,而不是使用依赖于 jitting 的第三方功能;快速的 javascript 引擎、快速的脚本语言、正则表达式匹配、基于字节码的编程语言,如 java 和 .NET。
所以,Android 爱好者喜欢攻击 iPhone 爱好者,说他们的手机更可定制。但您现在明白了,这两种选择都需要进行技术论证。
您是否希望能够在开始屏幕上放置一个飞翔的鸟类游戏,还是希望您的移动设备开发人员优先考虑安全性,然后随着时间的推移追赶从 Android 复制最佳创意?
代码内存只是一个范围的内存,数据内存也是一个范围的内存。大多数时候没有办法区分这一点。当你分配内存时,你会得到一个指向明显连续内存地址的指针(由 MMU 映射)。
重要的教训是:在某些操作系统中,代码内存是不可写的,数据内存是不可执行的。在其他系统中,应用程序决定其分配的哪些内存是可执行的,并且所有内存都是可写的。最后,有些系统的整个计算机内存都是可写的。
当操作系统内核收到执行 source[]
的调用时,这些是对您而言最重要的事情:
source[]
位于 RAM 中的某个位置。字符串“我在这里”是您的 source[0]
的一部分。您可能会在 source[]
左右的某个地方找到它。该字符串的最后一个字节将是 source[50]
- 一个空字节。之后,您会发现更多来自您的程序的 CPU 指令。
现在您明白为什么将字符串写入内存而不检查它的长度是否不超过分配的字符串如此危险了吗?如果有人为您提供了一个包含指令的字符串,那么这些指令可能会被执行。这就是为什么我更喜欢 Apple/iOS 更安全的方式而不是抱歉,我更喜欢这个内存是只读的 - 或者使用像 Dalvik 这样的托管代码,但这在 Android 的情况下没有帮助,因为它允许本机二进制文件也是如此。
在您的源示例中:
\0
源代码将作为字节数据存储在 RAM 中的某处。它们不以任何特定的“字符串”形式存储。您可以将它们读取为 for(int i=0;i<100000;i++)
char local[] = "I am wasting memory";
或 char
甚至 uint8_t
值 - 取决于您在指向该内存地址时使用的 float64
。
二进制文件的前几个字节是来自 C 编译器的一些样板代码,用于管理诸如函数堆栈之类的一些事情。
当 CPU 开始从您的程序中读取指令时,前几个字节将 struct
留出的内存范围,我们将其称为堆栈。
栈可以被认为是一个结构体的链表。
您程序中的每个函数都有一个隐藏的 malloc
,代表您在函数内部使用的局部变量。因此,当执行函数调用时,该结构会附加到链表中。在你的情况下:
struct
当您运行程序时,第一个堆栈帧是“全局”范围。第一帧包含在任何函数的外部声明的任何变量。你可以很容易地把它想象成另一个函数——除了它没有名字。
因此,当您的函数被调用时,一个特殊的 /* the "secret function struct" */
struct theSecretStructForYourFunction {
int i; // 8 bytes goes here (for example)
char local[]; // 8 bytes goes here
}
const theSecretMemoryOffsetForYourFunction = 123;
for(int i=0;i<100000;i++)
char local[] = "I am wasting memory";
值会增加 8(因为根据 offset_to_the_of_the_stack
这是所需要的)。请记住,该程序已经为您的堆栈分配了一个块。
您在 C 程序中定义的结构不会编译到程序中。它们只是编译器的查找信息,以便它知道应该如何编译文件。例如,如果您有一个总共为 8 个字节的结构数组,那么它知道每当您想要访问该数组的任意索引时都需要将偏移量乘以 8。这就是为什么当我们想要使用第三方库时拥有一个 theSecretStructForYourFunction
文件会很有帮助的原因。
现在 CPU 开始处理您的循环 - 直接从堆栈中查找 .h
值,以及直接从堆栈中查找 i
值。
对于循环的每一步:
local[]
,则跳过接下来的三个指令。my_local_stack->i < 100000
。my_local_stack->local[]
my_local_stack->i++
这不会消耗更多内存。事实上,一个好的编译器可能会分两步重写你的程序:
jmp (address of step 1)
变成
for(int i=0;i<100000;i++)
char local[] = "I am wasting memory";
变成:
char local[] = "I am wasting memory";
for(int i=0;i<100000;i++);
最终编译为什么都不做的源代码。
char local[] = "I am wasting memory";
int i=100000;
, 0x1, 0x86, 0xA0 ]`