代码之家  ›  专栏  ›  技术社区  ›  Adam Gent

C:为什么未分配的指针指向不可预知的内存,而不是指向空?

  •  46
  • Adam Gent  · 技术社区  · 14 年前

    很久以前,我在学校里用C语言编程。我记得一些我非常讨厌的关于C的东西:未分配的指针不指向空值。

    我问了很多人,包括老师,他们为什么要 未分配指针的默认行为不指向空值 因为它似乎更危险,因为它是不可预测的。

    答案应该是表演,但我从来没有买过。我认为,如果C默认为空,编程历史上的许多错误都可以避免。

    这里有一些C代码指出(双关语的意思)我在说什么:

    #include <stdio.h>
    
    void main() {
    
      int * randomA;
      int * randomB;
      int * nullA = NULL;
      int * nullB = NULL;
    
    
      printf("randomA: %p, randomB: %p, nullA: %p, nullB: %p\n\n", 
         randomA, randomB, nullA, nullB);
    }
    

    它编译时带有警告(很高兴看到C编译器比我在学校时好得多)和输出:

    兰多玛:0xB779EFF4,兰多姆:0x804844B,空值:(零),空值:(零)

    11 回复  |  直到 14 年前
        1
  •  41
  •   Georg Fritzsche    14 年前

    实际上,它取决于指针的存储。具有静态存储的指针使用空指针初始化。具有自动存储持续时间的指针未初始化。见ISO C 99 6.7.8.10:

    如果没有显式初始化具有自动存储持续时间的对象,则其值为 不确定的如果没有显式初始化具有静态存储持续时间的对象, 然后:

    • 如果有指针类型,则初始化为空指针;
    • 如果它有算术类型,则初始化为(正或无符号) 零;
    • 如果是聚合,则初始化每个成员(递归) 根据本规则;
    • 如果是联合,则初始化第一个命名成员(递归) 根据这些规则。

    是的,由于性能原因,具有自动存储持续时间的对象不会初始化。想象一下,每次调用一个日志函数时都会初始化一个4K数组(这是我在一个项目中看到的,谢天谢地,C让我避免初始化,从而提高了性能)。

        2
  •  26
  •   detly    14 年前

    因为在C语言中,声明和初始化是 故意采取不同的步骤 . 它们故意不同,因为C就是这样设计的。

    当你在函数中这样说时:

    void demo(void)
    {
        int *param;
        ...
    }
    

    您在说,“亲爱的C编译器,当您为这个函数创建堆栈框架时,请记住保留 sizeof(int*) 用于存储指针的字节数。“编译器不会询问那里发生了什么——它假定您很快就会告诉它。如果你不这样做,也许有一种更好的语言适合你;)

    也许生成一些安全的堆栈清除代码并不难。但在每次函数调用时都必须调用它,我怀疑很多C开发人员在自己要填充它的时候是否会感激它。顺便说一句,如果允许您灵活处理堆栈,那么您可以为性能做很多工作。例如,编译器可以在以下情况下进行优化…

    如果你 function1 叫另一个 function2 并存储其返回值,或者可能有一些参数传入 功能2 that aren't changed inside 功能2 …我们不需要创造额外的空间,是吗?只需对这两个部分使用堆栈的相同部分!请注意,这与每次使用前初始化堆栈的概念直接冲突。

    但在更广泛的意义上,(在我看来,更重要的是),它符合C的哲学,即不做太多超过绝对必要的事情。无论您是在使用PDP11、pic32mx(我使用它的目的)还是cray xt3,这都适用。确切地说 为什么? 人们可能会选择使用C语言而不是其他语言。

    • 如果我想写一个没有 malloc free 我不需要!没有记忆管理强迫我!
    • 如果我想位压缩和键入双关一个数据联合,我可以!(当然,只要我阅读了关于标准遵守的实施说明。)
    • 如果我确切地知道我对堆栈框架做了什么,编译器就不必为我做任何其他的事情!

    简而言之,当您请求C编译器跳转时,它不会询问有多高。产生的代码可能不会再出现了。

    因为大多数选择用C语言发展的人都喜欢这样,所以它有足够的惯性,不会改变。您的方法可能不是一个固有的坏主意,只是许多其他C开发人员并没有真正要求它。

        3
  •  14
  •   David Sykes    14 年前

    这是为了表演。

    C was first developed 大约在pdp 11的时候,60k是一个常见的最大内存量,许多人将拥有更少的内存。不必要的任务会特别昂贵是这种环境

    现在有很多使用C的嵌入式设备,60K的内存看起来是无限的,而图12F675有1K的内存。

        4
  •  8
  •   slaphappy    14 年前

    这是因为当您声明一个指针时,C编译器将只保留放置它所需的空间。因此,当您运行程序时,这个非常大的空间中可能已经有了一个值,这可能是先前在内存的这一部分分配的数据的结果。

    C编译器可以为这个指针分配一个值,但在大多数情况下,这是浪费时间的,因为在代码的某些部分中,您需要自己分配一个自定义值。

    这就是为什么好的编译器在不初始化变量时发出警告的原因;所以我不认为有这么多的bug是因为这种行为。你只需阅读警告。

        5
  •  7
  •   caf    14 年前

    指针在这方面并不特殊;如果未初始化地使用其他类型的变量,则它们具有完全相同的问题:

    int a;
    double b;
    
    printf("%d, %f\n", a, b);
    

    原因很简单:要求运行时将未初始化的值设置为已知值会增加每个函数调用的开销。对于单个值,开销可能不多,但如果您有大量指针,请考虑:

    int *a[20000];
    
        6
  •  4
  •   Tim Schaeffer    14 年前

    When you declare a (pointer) variable at the beginning of the function, the 编译器将做两件事之一:留出一个寄存器用作 这个变量,或者在堆栈上为它分配空间。对于大多数 处理器,为堆栈中的所有局部变量分配内存 是用一条指令完成的;它计算出所有 本地变量将需要,并下拉(或向上推,对某些变量) processors) the stack pointer by that much. 已经在里面的东西 当时的记忆不会改变,除非你明确地改变 它。

    指针没有“设置”为“随机”值。在分配之前, 堆栈指针(SP)下面的堆栈内存包含任何内容 从早期使用开始:

             .
             .
     SP ---> 45
             ff
             04
             f9
             44
             23
             01
             40
             . 
             .
             .
    

    在它为一个本地指针分配内存之后,只有 更改的是堆栈指针:

             .
             .
             45
             ff |
             04 | allocated memory for pointer.
             f9 |
     SP ---> 44 |
             23
             01
             40
             . 
             .
             .
    

    这允许编译器在一条将堆栈指针向下移动的指令中分配所有本地变量。 (并通过移动堆栈指针,在一条指令中释放它们 备份),但如果需要,强制您自己初始化它们 那样做。

    在C99中,您可以混合使用代码和声明,这样就可以推迟 在代码中声明,直到您能够初始化它。这个 将允许您避免将其设置为空。

        7
  •  3
  •   David Thornley    14 年前

    首先,强制初始化不能修复错误。它掩盖了他们。使用没有有效值的变量(以及因应用程序而异的变量)是一个错误。

    第二,您可以经常进行自己的初始化。而不是 int *p; int *p = NULL; int *p = 0; . 使用 calloc() (将内存初始化为零)而不是 malloc() (但事实并非如此)。(不,所有的零位不一定意味着零的空指针或浮点值。是的,它适用于大多数现代实现。)

    第三,C(和C++)哲学是给你快速做事的方法。假设您可以选择在语言中实现一种安全的方法来做某件事情,一种快速的方法来做某件事情。你不能通过在它周围添加更多的代码来更快地建立一个安全的方法,但是这样做可以使一个快速的方法更安全。此外,您有时可以通过确保操作在没有额外检查的情况下是安全的,从而使操作快速、安全——当然,假设您可以从快速选项开始。

    C最初是设计用来编写操作系统和相关代码的,操作系统的某些部分必须尽可能快。这在C语言中是可能的,但在更安全的语言中就不那么可能了。此外,C是在最大的计算机不如我口袋里的电话强大的时候开发出来的(我很快就升级了,因为它感觉又老又慢)。在经常使用的代码中保存几个机器周期可能会有可见的结果。

        8
  •  1
  •   S.C. Madsen    14 年前

    所以,为了总结Ninjalj所解释的,如果你稍微改变你的示例程序,你需要指出 infact初始化为空:

    #include <stdio.h>
    
    // Change the "storage" of the pointer-variables from "stack" to "bss"  
    int * randomA;
    int * randomB;
    
    void main() 
    {
      int * nullA = NULL;
      int * nullB = NULL;
    
      printf("randomA: %p, randomB: %p, nullA: %p, nullB: %p\n\n", 
         randomA, randomB, nullA, nullB);
    }
    

    在我的机器上打印

    randoma:00000000,randomb:00000000,nulla:00000000,nullab:00000000

        9
  •  0
  •   ShinTakezou    14 年前

    我认为这是由以下原因引起的:没有理由解释为什么记忆应该包含(通电时)特定的值(0、空或其他)。因此,如果以前没有专门写过,一个内存位置可以包含任何值,从你的角度来看,这个值是随机的(但是这个位置以前可能被其他软件使用过,因此包含一个对该应用程序有意义的值,例如计数器,但从“你的”角度来看,它只是一个随机数)。 要将其初始化为特定的值,至少还需要一条指令;但在某些情况下,您不需要进行此初始化 先验的 ,例如 v = malloc(x) 将分配给v的有效地址或空值,不管v的初始内容是什么,因此,初始化它可能被认为是浪费时间,并且语言(如c)可以选择不这样做。 先验的 . 当然,现在这主要是无关紧要的,有些语言中未初始化的变量有默认值(指针为空,如果支持;数字为0/0.0…等等;懒惰的初始化当然会使初始化一个包含100万个元素的数组变得不那么昂贵,因为只有在赋值之前才访问它们,它们才被初始化为真正的。

        10
  •  0
  •   R.. GitHub STOP HELPING ICE    14 年前

    当机器通电时,这种与随机存储器内容有关的想法是假的,除了嵌入式系统。任何具有虚拟内存和多进程/多用户操作系统的机器在将内存提供给进程之前都将初始化内存(通常为0)。不这样做将是一个重大的安全漏洞。自动存储变量中的“随机”值来自同一进程以前对堆栈的使用。类似地,malloc/new/etc返回的内存中的“随机”值来自同一进程中以前的分配(随后释放)。

        11
  •  -1
  •   Gabriel Russell    14 年前

    如果它指向空值,则必须将空值赋给它(即使它是自动透明地完成的)。

    因此,要回答您的问题,指针不能同时被取消分配和空的原因是指针不能同时被分配和分配。