簡體   English   中英

C:為什么未分配的指針指向不可預測的內存而NOT指向NULL?

[英]C: Why do unassigned pointers point to unpredictable memory and NOT point to NULL?

很久以前我曾經在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)

實際上,它取決於指針的存儲。 帶有靜態存儲的指針使用空指針初始化。 具有自動存儲持續時間的指針未初始化。 見ISO C 99 6.7.8.10:

如果未顯式初始化具有自動存儲持續時間的對象,則其值不確定。 如果未顯式初始化具有靜態存儲持續時間的對象,則:

  • 如果它有指針類型,則將其初始化為空指針;
  • 如果它有算術類型,則初始化為(正或無符號)零;
  • 如果它是一個聚合,則根據這些規則初始化(遞歸)每個成員;
  • 如果它是一個聯合,則根據這些規則初始化(遞歸)第一個命名成員。

是的,出於性能原因,沒有初始化具有自動存儲持續時間的對象。 想象一下,在每次調用日志函數時初始化一個4K數組(我在我工作的項目上看到的東西,謝天謝地,C讓我避免初始化,從而帶來不錯的性能提升)。

因為在C中,聲明和初始化是故意不同的步驟 它們是故意不同的,因為這就是C的設計方式。

當你在函數中說這個時:

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

你說,“親愛的C編譯器,當你為這個函數創建堆棧幀時,請記住保留sizeof(int*)字節來存儲指針。” 編譯器不會詢問那里會發生什么 - 它假設你很快會告訴它。 如果你不這樣做,也許對你有更好的語言;)

也許生成一些安全的堆棧清除代碼並不困難。 但它必須在每個函數調用上被調用,我懷疑許多C開發人員會在他們自己完全填充它時才會喜歡它。 順便提一下,如果你允許靈活使用堆棧,那么你可以為性能做很多事情。 例如,編譯器可以進行優化......

如果您的function1調用另一個function2和存儲其返回值,或者可能有傳遞一些參數function2未改變內部function2 ......我們沒有創造額外的空間,我們呢? 只需使用堆棧的相同部分! 請注意,這與每次使用前初始化堆棧的概念直接沖突。

但是從更廣泛的意義上說(而且在我看來,更重要的是)它與C的理念是一致的,即不是做得非常絕對必要。 無論您是在使用PDP11,PIC32MX(我用它)還是Cray XT3,這都適用。 這正是為什么人們會選擇用C來代替其他語言的。

  • 如果我想編寫一個沒有mallocfree ,我沒有必要! 沒有內存管理強加給我!
  • 如果我想對數據聯合進行bit-pack和type-pun,我可以! (當然,只要我閱讀我的實施關於標准依從性的說明。)
  • 如果我確切地知道我正在使用我的堆棧幀做什么,編譯器不必為我做任何其他事情!

總之,當你要求C編譯器跳轉時,它不會問多高。 結果代碼可能甚至不會再次降低。

由於大多數選擇用C開發的人都喜歡這種方式,因此它具有足夠的慣性而不會改變。 你的方式可能不是一個天生就是壞主意,它並沒有被許多其他C開發人員真正要求。

這是為了表現。

C最初是在PDP 11的時代開發的,其中60k是常見的最大內存量,許多內存將少得多。 在這種環境下,不必要的任務特別昂貴

目前有許多嵌入式設備使用C,其中60k的內存看起來無限,PIC 12F675有1k的內存。

這是因為當您聲明一個指針時,您的C編譯器將保留必要的空間來放置它。 所以當你運行你的程序時,這個空間可能已經有了一個值,可能是因為在這部分內存中分配了先前的數據。

C編譯器可以為該指針賦值,但在大多數情況下這會浪費時間,因為除了在代碼的某些部分自己分配自定義值之外。

這就是為什么好的編譯器在你不初始化你的變量時會發出警告; 所以我不認為因為這種行為會有這么多錯誤。 你只需要閱讀警告。

指針在這方面並不特別; 如果您使用未初始化的其他類型的變量具有完全相同的問題:

int a;
double b;

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

原因很簡單:要求運行時將未初始化的值設置為已知值會增加每個函數調用的開銷。 單個值的開銷可能不大,但請考慮是否有大量指針:

int *a[20000];

當您在函數開頭聲明一個(指針)變量時,編譯器將執行以下兩項操作之一:將寄存器用作該變量,或者為堆棧分配空間。 對於大多數處理器,為堆棧中的所有局部變量分配內存是通過一條指令完成的; 它計算出所有本地變量需要多少內存,並將堆棧指針拉下(或向上推,在某些處理器上)。 除非你明確地改變它,否則當時記憶中的任何東西都不會改變。

指針未“設置”為“隨機”值。 在分配之前,堆棧指針(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。

首先,強制初始化不會修復錯誤。 它掩蓋了他們。 使用沒有有效值的變量(以及因應用程序而異的變量)是一個錯誤。

其次,您可以經常進行自己的初始化。 而不是int *p; ,寫int *p = NULL; int *p = 0; 使用calloc() (將內存初始化為零)而不是malloc() (沒有)。 (不,所有位零都不一定意味着NULL指針或浮點值為零。是的,它在大多數現代實現中都有。)

第三,C(和C ++)哲學是為你提供快速做事的手段。 假設您可以選擇在語言中實施一種安全的方式來做某事並快速做某事。 通過在其周圍添加更多代碼,您無法更快地建立安全的方式,但通過這樣做,您可以更快地安全。 此外,您有時可以通過確保操作安全而無需額外檢查來快速安全地進行操作 - 當然,假設您有快速選擇。

C最初設計用於編寫操作系統和相關代碼,並且操作系統的某些部分必須盡可能快。 這在C中是可能的,但在更安全的語言中則更少。 此外,當最大的計算機不如我口袋里的電話那么強大時,我就開發了C(我很快就升級了,因為它感覺很舊而且很慢)。 在常用代碼中保存幾個機器周期可能會產生明顯的結果。

所以,總結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);
}

在我的機器上打印

randomA:00000000,randomB:00000000,nullA:00000000,nullB:00000000

我認為它來自以下內容:沒有理由為什么內存應該包含(啟動時)特定值(0,NULL或其他)。 因此,如果以前沒有特別編寫過,內存位置可以包含任何值,從您的角度來看,無論如何都是隨機的(但是這個位置之前可能已被其他軟件使用過,因此包含一個有意義的值該應用程序,例如計數器,但從“你的”觀點來看,只是一個隨機數)。 要將其初始化為特定值,您至少需要一條指令; 但是有些情況下你不需要先驗地進行初始化,例如v = malloc(x)將分配給va有效地址或NULL,無論v的初始內容如何。因此,初始化它可能被認為是浪費時間和語言(如C)可以選擇不先驗地做。 當然,現在這主要是微不足道的,並且有些語言中未初始化的變量具有默認值(指針為null,支持時為0;對於數字為0 ...等等;延遲初始化當然使初始化不那么昂貴一個說100萬個元素的數組,因為只有在賦值之前訪問它們才會被初始化為真實元素。

當機器加電時,這與隨機存儲器內容有關的想法是虛假的,除了在嵌入式系統上。 任何具有虛擬內存和多進程/多用戶操作系統的計算機都會在將內存提供給進程之前初始化內存(通常為0)。 如果不這樣做將是一個重大的安全漏洞。 自動存儲變量中的“隨機”值來自先前使用相同進程的堆棧。 類似地,malloc / new / etc返回的內存中的“隨機”值。 來自之前的分配(隨后被釋放)在同一過程中。

為了指向NULL,它必須為其分配NULL(即使它是自動且透明地完成的)。

因此,要回答您的問題,指針不能同時取消分配和NULL的原因是因為指針既不能同時分配和分配。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM