【问题标题】:C: Why do unassigned pointers point to unpredictable memory and NOT point to NULL?C:为什么未分配的指针指向不可预知的内存而不是指向NULL?
【发布时间】:2010-06-23 13:12:47
【问题描述】:

很久以前,我曾经在学校用 C 编程。我记得我非常讨厌 C 的一点:未分配的指针不指向 NULL。

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

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

这里有一些 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 编译器比我在学校时好得多)并输出:

randomA: 0xb779eff4, randomB: 0x804844b, nullA: (nil), nullB: (nil)

【问题讨论】:

  • 多么有趣的问题:)
  • C 说:相信程序员。程序员学会跟踪他们的变量。
  • 我认为您将 C 与高级语言混淆了。不是。
  • @Adam Gent:这种区别仅在您的代码访问未初始化变量的值时才重要,它不应该这样做。大多数现代 C 编译器都会抱怨这一点,这是有充分理由的。
  • 指针的正确 printf 格式说明符是 %p,而不是 %d

标签: c pointers


【解决方案1】:

其实还是要看指针的存储。具有静态存储的指针使用空指针初始化。具有自动存储持续时间的指针未初始化。参见 ISO C 99 6.7.8.10:

如果具有自动存储持续时间的对象未显式初始化,则其值为 不定。如果具有静态存储持续时间的对象未显式初始化, 那么:

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

是的,出于性能原因,具有自动存储持续时间的对象不会被初始化。想象一下在每次调用日志函数时初始化一个 4K 数组(我在我从事的一个项目中看到了这一点,幸好 C 让我避免了初始化,从而带来了不错的性能提升)。

【讨论】:

  • 当然,初始化 4K 数组不会那么慢,因为像 Java 这样的语言一直都在这样做(初始化所有引用)。你一定有一个非常高性能的项目
  • 是的,性能对那个项目很重要。除此之外,大约 99.99% 的时间,日志记录函数只是根据共享内存中的某些标志检查其参数,并看到日志记录被禁用并返回。想象一下当我发现初始化位于缓存研磨配置文件的前 5 个位置之一时我的表情。
【解决方案2】:

因为在 C 中,声明和初始化是故意不同的步骤。它们是故意不同的,因为这就是 C 的设计方式。

当你在函数中这样说时:

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

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

也许生成一些安全的堆栈清除代码并不难。但是它必须在每次函数调用时被调用,而且我怀疑许多 C 开发人员在他们无论如何都打算自己填充它时会欣赏这个命中。顺便说一句,如果允许您灵活地使用堆栈,您可以为性能做很多事情。例如,编译器可以在哪里进行优化...

如果您的function1 调用另一个function2 并存储它的返回值,或者可能有一些参数传入function2 而在function2 内部没有更改...我们不必创建额外的空间,是吗?只需为两者使用堆栈的相同部分!请注意,这与在每次使用前初始化堆栈的概念直接冲突。

但在更广泛的意义上,(在我看来,更重要的是)它与 C 的哲学相一致,即不做超过绝对必要的事情。无论您是在使用 PDP11、PIC32MX(我使用它的目的)还是 Cray XT3,这都适用。这正是为什么人们可能会选择使用 C 而不是其他语言的原因。

  • 如果我想编写一个没有mallocfree 痕迹的程序,我不必这样做!没有强制我进行内存管理!
  • 如果我想对数据联合进行位打包和打字,我可以! (当然,只要我阅读了关于标准遵守的实施说明。)
  • 如果我确切地知道我在用堆栈帧做什么,编译器就不必为我做任何其他事情!

简而言之,当你要求 C 编译器跳转时,它不会问多高。生成的代码可能甚至不会再次返回。

由于大多数选择用 C 开发的人都喜欢这种方式,它有足够的惯性不改变。您的方式可能不是一个天生的坏主意,只是许多其他 C 开发人员并没有真正要求它。

【讨论】:

  • @Adam Gent:批评一种语言不是你喜欢的那种语言是徒劳的,不是吗? C 被大量用于嵌入式编程的原因是它很高效。如果您销售的系统数以千万计,那么让工程师确保代码安全要比为每个人多花一美元购买更快的 CPU 或更大的程序存储要便宜得多。
  • @Adam Gent - 我希望在我的高精度科学仪器中严格控制时间和内存 :) 而且它是安全的,因为我始终知道我的代码在做什么——我告诉它的去做。您对安全的定义可能会有所不同。 (此外,给我找一个适用于 PIC32MX 系列或 dsPIC24 的 Haskell 编译器。)
  • @detly touche.. touche... 您的观点已被采纳,我无意冒犯。但是,如果这是真的,为什么不在汇编中编程呢?现在是 2010 年,芯片变得相当便宜?我们什么时候可以停止使用 C 进行编程,或者您认为 C 更优越?这些天我们不是在转向更并发的架构吗?看来 C 在这方面有点弱。
  • @NoMoreZealots 和 @detly 我同意 C 确实很好地映射到 stuff 的真正工作原理,我很高兴我学会了这门语言,因为它帮助我理解了 CPU 架构我在学校。
  • @Adam Gent:有趣的是,除了 IO 问题,很容易抽象,CPU 密集型应用程序需要并发编程。将 C 应用于据称在高级语言中更容易的领域,大多数时候 消除了对并发编程的需求,因为与其他语言的实现相比具有极大的性能提升。如果这还不够,那么无论如何您都希望在并发实现中使用 C。切换到 C:50 倍性能。切换到并发:次线性核心数性能提升。
【解决方案3】:

这是为了性能。

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

现在有许多使用 C 的嵌入式设备,60k 的内存似乎是无限的,PIC 12F675 有 1k 的内存。

【讨论】:

  • 我就是不明白。它从某个地方获得价值吗?某处被分配。为什么 C 的运行时指向 NULL 比将它们分配给某个随机值的成本更高。
  • @Adam:该值以前存在。这只是对特定内存位置的重用。
  • 不需要启动嵌入式设备,只需记住 C 是为同时允许 64K 代码和 64K 数据的计算机设计的。其他时间,其他限制,其他决定。
  • 运行时不分配任何东西,只是重用那里发生的事情。
  • @Adam,顺便说一句,我常用的编译器能够警告使用未初始化的变量。提高您的警告级别并修复发现的问题。另一件事,警告和编译时间选项超出了标准的范围,如果您想要它们但没有它们,请游说您的编译器供应商。
【解决方案4】:

这是因为当你声明一个指针时,你的 C 编译器只会保留必要的空间来放置它。所以当你运行你的程序时,这个空间可能已经有一个值,这可能是之前在这部分内存上分配的数据的结果。

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

这就是为什么好的编译器在你没有初始化你的变量时会给出警告的原因;所以我不认为因为这种行为而有这么多的错误。您只需阅读警告即可。

【讨论】:

  • 我相信他在历史上的意思是“许多错误”;显然较旧的 C 编译器不如同时代的编译器友好。
  • 恕我直言,任何访问未初始化变量值的尝试都是错误。它是零还是随机都没有关系,如果你没有明确地将它设置为某个东西,你就不应该尝试阅读它。
  • @joefis 但如果它为 NULL 而不是一些随机值,则更容易找到和理解错误。如果您正在进行并发编程,这将特别有用。
  • @Adam Gent:嗯,这取决于你的观点。就我个人而言,我认为垃圾是我忘记设置变量的更明确的指标,而不是我可能故意设置的 NULL...
  • @AdamGent 如果需要,程序员有责任在使用前初始化值,编译器已经对此发出警告。此外,将其设置为 NULL 可能无法帮助您更轻松地找到错误。 MSVC 和许多其他编译器在调试模式下已经用0xCC, 0xCD 或其他一些值填充了未初始化的内存,并且它比 0 更容易识别。你会看到一些关于 0xCCCCCCCC 的参考,一些奇怪的重复字符或许多类似的情况
【解决方案5】:

指针在这方面并不特殊;如果您在未初始化的情况下使用其他类型的变量,则会出现完全相同的问题:

int a;
double b;

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

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

int *a[20000];

【讨论】:

  • 是的,对于 C 的过程性质,您经常在分配变量之前定义变量。所以我可以看到这如何成为一个性能问题。
【解决方案6】:

当你在函数的开头声明一个(指针)变量时, 编译器将做两件事之一:留出一个寄存器用作 该变量,或在堆栈上为其分配空间。对于大多数 处理器,为堆栈中的所有局部变量分配内存 用一条指令完成;它计算出有多少内存 本地变量将需要,并且在某些情况下会拉下(或上推) 处理器)的堆栈指针那么多。无论已经在 除非您明确更改,否则当时的记忆不会改变 它。

指针未“设置”为“随机”值。分配前, 堆栈指针(SP)下方的堆栈内存包含那里的任何内容 从早期使用:

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

在它为本地指针分配内存之后,唯一拥有 改变的是堆栈指针:

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

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

在 C99 中,您可以混合使用代码和声明,因此您可以推迟您的 在代码中声明,直到您能够对其进行初始化。这 将允许您避免将其设置为 NULL。

【讨论】:

    【解决方案7】:

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

    其次,您通常可以自己进行初始化。而不是int *p;,写int *p = NULL;int *p = 0;。使用calloc()(将内存初始化为零)而不是malloc()(不会)。 (不,所有位为零并不一定意味着 NULL 指针或浮点值为零。是的,在大多数现代实现中都是如此。)

    第三,C(和 C++)哲学是为你提供快速做某事的方法。假设您可以选择在语言中实现一种安全的做某事的方法和一种快速的做某事的方法。您无法通过在其周围添加更多代码来使安全方式更快,但是您可以通过这样做使快速方式更安全。此外,您有时可以通过确保操作在没有额外检查的情况下安全来使操作快速且安全 - 当然,假设您一开始就有快速选项。

    C 最初是为编写操作系统和相关代码而设计的,操作系统的某些部分必须尽可能快。这在 C 中是可能的,但在更安全的语言中则不太可能。此外,C 是在最大的计算机不如我口袋里的电话强大的时候开发的(我很快就会升级它,因为它感觉又旧又慢)。在常用代码中节省一些机器周期可能会产生明显的结果。

    【讨论】:

      【解决方案8】:

      所以,总结一下 ninjalj 的解释,如果你稍微改变你的示例程序,你的指针 实际上初始化为 NULL:

      #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);
      }
      

      在我的机器上打印

      随机A:00000000,随机B:00000000,空A:00000000,空B:00000000

      【讨论】:

      • 有趣的是,我的 C 运行时似乎打印出 nil() 而不是 00000000。您使用的是什么 OS/C 编译器。
      • MSYS / MinGW,GCC 版本 3.4.2(mingw-special)
      【解决方案9】:

      我认为它来自以下原因:内存没有理由应该包含(当通电时)特定值(0、NULL 或其他)。因此,如果以前没有专门编写,内存位置可以包含任何值,从您的角度来看,无论如何都是随机的(但该位置可能之前已被其他软件使用过,因此包含一个有意义的值该应用程序,例如计数器,但从“您的”角度来看,只是一个随机数)。 要将其初始化为特定值,至少还需要一条指令;但是在某些情况下您不需要此初始化先验,例如v = malloc(x) 将分配给 va 有效地址或 NULL,无论 v 的初始内容如何。因此,初始化它可能被认为是浪费时间,并且语言(如 C)可以先验地选择不这样做。 当然,现在这主要是微不足道的,并且有些语言中未初始化的变量具有默认值(指针为 null,当支持时;0/0.0 为数字……等等;惰性初始化当然使初始化成本不那么高一个由 100 万个元素组成的数组,因为它们只有在赋值之前才被真正地初始化)。

      【讨论】:

        【解决方案10】:

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

        【讨论】:

        • 他认为这与随机内存内容无关,他只是在示例中使用变量名 randomX 来指出未初始化的指针似乎被初始化为“随机”地址
        【解决方案11】:

        为了让它指向 NULL,它必须有 NULL 分配给它(即使它是自动和透明地完成的)。

        所以,回答你的问题,一个指针不能既未赋值又为 NULL 的原因是指针不能同时未赋值和赋值。

        【讨论】:

        • 在 Java 和 C# 等其他语言中,未分配的值仍然可以得到一些可预测的值。你的论点完全基于你的语义而不是我的。
        猜你喜欢
        • 1970-01-01
        • 2021-08-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-22
        • 2023-04-01
        • 2021-12-26
        相关资源
        最近更新 更多