程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> 評:C語言18個經典問題答錄

評:C語言18個經典問題答錄

編輯:關於C
1.這樣的初始化有什麼問題?char *p = malloc(10); 編譯器提示“非法初始式” 雲雲。

答:這個聲明是靜態或非局部變量嗎?函數調用只能出現在自動變量(即局部非靜態變量) 的初始式中。因為靜態變量的地址必須在編譯的過程中就確定下來而malloc()申請的內存地址是在運行時確定的。

評:gcc編譯不會報錯。但是這樣的編程習慣確實不好,即使知道需要內存大小,一般也是使用宏定義來標識需要分配的固定內存大小。這樣既便於理解,也為今後的代碼修改和移植提供了基礎。

2. *p++ 自增p 還是p 所指向的變量?

答:後綴++ 和-- 操作符本質上比前綴一目操作的優先級高, 因此*p++ 和*(p++) 等價, 它自增p 並返回p 自增之前所指向的值。要自增p 指向的值, 使用(*p)++, 如果副作用的順序無關緊要也可以使用++*p。

評:程序是按照運算符固定的邏輯關系執行。為了便於理解和閱讀,編碼時盡量采用(),避免理解出現的二義問題,這是一個編程習慣問題。

3. 我有一個char * 型指針正巧指向一些int 型變量, 我想跳過它們。為什麼如下的代碼((int *)p)++; 不行?

答:在C 語言中, 類型轉換意味著“把這些二進制位看作另一種類型, 並作相應的對待”; 這是一個轉換操作符, 根據定義它只能生成一個右值(rvalue)。而右值既不能賦值, 也不能用++ 自增。(如果編譯器支持這樣的擴展, 那要麼是一個錯誤, 要麼是有意作出的非標准擴展。) 要達到你的目的可以用:p = (char *)((int *)p + 1);或者,因為p 是char * 型, 直接用p += sizeof(int);

評:gcc編譯器會根據指針所指向類型的大小,對++操作移動相應的類型大小字節數。

4.空指針和未初始化的指針是一回事嗎?

答:空指針在概念上不同於未初始化的指針:空指針可以確保不指向任何對象或函數;而未初始化指針則可能指向任何地方。

評:任何指針必須初始化,如果不確定指針用途的可以初始化為NULL值。通常編程軍規中約定,當然在優化代碼中會有明顯的說明和解釋。

5.我可以用0來表示空指針嗎?

答:根據語言定義, 在指針上下文中的常數0 會在編譯時轉換為空指針。也就是說, 在初始化、賦值或比較的時候,

如果一邊是指針類型的值或表達式, 編譯器可以確定另一邊的常數0 為空指針並生成正確的空指針值。因此下邊的代碼段完全合法:

char *p = 0;

if(p != 0)

然而, 傳入函數的參數不一定被當作指針環境, 因而編譯器可能不能識別未加修飾的0 “表示” 指針。

在函數調用的上下文中生成空指針需要明確的類型轉換,強制把0 看作指針。

例如, Unix 系統調用execl 接受變長的以空指針結束的字符指針參數。它應該如下正確調用:

execl("/bin/sh", "sh", "-c", "date", (char *)0);

如果省略最後一個參數的(char *) 轉換, 則編譯器無從知道這是一個空指針,從而當作一個0 傳入。(注意很多Unix 手冊在這個例子上都弄錯了。)

摘要:

==========================|=============================

|| 可以使用未加修飾的0 | 需要顯示的類型轉換 ||

||------------------------|---------------------------||

|| *初始化 | *函數調用, 作用域內無原型 ||

|| *賦值 | *變參函數調用中的可變參數 ||

|| *比較 | ||

|| *固定參數的函數調用 | ||

|| 且在作用域內有原型 | ||

==========================|=============================

有兩條簡單規則你必須遵循:

1) 當你在源碼中需要空指針常數時, 用“0” 或“NULL”。

2) 如果在函數調用中“0” 或“NULL” 用作參數, 把它轉換成被調函數需要的指針類型

評:建議使用NULL值,通用且容易判斷該變量是指針。參見/usr/include/linux/stddef.h, Linux下C語言NULL定義為#define NULL ((void *)0) 。

6. 既然數組引用會蛻化為指針, 如果arr 是數組, 那麼arr 和&arr 又有什麼區別呢?

答:區別在於類型:

在標准C 中, &arr 生成一個“T 型數組” 的指針, 指向整個數組。

在所有的C 編譯器中, 對數組的簡單引用(不包括& 操作符)生成一個T 的指針類型的指針, 指向數組的第一成員。

評:int array[100];

關於對數組名取地址的問題,由於數組名是右值,本來&array 是不合法的,早期不少編譯器就是指定&array 是非法的,但後來C89/C99認為數組符合對象的語義,對一個對象取地址是合理的,因此,從維護對象的完整性出發,也允許&array 。只不過,&array 的意義並非對一個數組名取地址,而是對一個數組對象取地址,也正因為如此,array 才跟&array 所代表的地址值一樣,同時sizeof(array )應該跟sizeof(&array )一樣,因為sizeof(&array )代表取一個數組對象的長度。

但要注意到 array 和 &array 的類型是不同的。array為一個指針,而&array是指向數組int [100]的指針。array 相當於 &array[0],而 &array 是一個指向 int[100] 的指針,類型是 int(*)[100]。類型為:類型 (*)[數組大小],所以&a+1大小為:首地址+sizeof(a)。

7. 我如何聲明一個數組指針?

答:通常, 你不需要。當人們隨便提到數組指針的時候, 他們通常想的是指向它的第一個元素的指針。

考慮使用指向數組某個元素的指針, 而不是數組的指針。類型T 的數組蛻變成類型T 的指針, 這很方便;

在結果的指針上使用下標或增量就可以訪問數組中單獨的成員。而真正的數組指針, 在使用下標或增量時, 會跳過整個數組,

通常只在操作數組的數組時有用。如果還有一點用的話。如果你真的需要聲明指向整個數組的指針,

使用類似“int (*ap)[N];”這樣的聲明。其中N 是數組的大小。如果數組的大小未知, 原則上可以省略N, 但是這樣生成的類型,

“指向大小未知的數組的指針”, 毫無用處。

評:數組本是一堆數據的集合,數組常使用於算法或者固定參數。如果有這樣的數據集合,使用時,不如直接引用數據集合的名字來的更為簡單方便,何必用指針。而一般應用真正關心的是數組裡面的元素。因此指向數組元素的指針更重要。

8.當我向一個接受指針的指針的函數傳入二維數組的時候, 編譯器報錯了,這是怎麼回事?

答:數組蛻化為指針的規則不能遞歸應用。數組的數組(即C 語言中的二維數組) 蛻化為數組的指針, 而不是指針的指針。

數組指針常常令人困惑, 需要小心對待; 如果你向函數傳遞二位數組:

int array[NROWS][NCOLUMNS];

f(array);

那麼函數的聲明必須匹配:

void f(int a[][NCOLUMNS])

{ ... }

或者

void f(int (*ap)[NCOLUMNS]) /* ap 是個數組指針*/

{ ... }

在第一個聲明中, 編譯器進行了通常的從“數組的數組” 到“數組的指針” 的隱式轉換; 第二種形式中的指針定義顯而易見。

因為被調函數並不為數組分配地址,所以它並不需要知道總的大小, 所以行數NROWS 可以省略。但數組的寬度依然重要,

所以列維度NCOLUMNS (對於三維或多維數組, 相關的維度) 必須保留。

如果一個函數已經定義為接受指針的指針, 那麼幾乎可以肯定直接向它傳入二維數組毫無意義。

評:函數入參主要就是傳值和傳址:傳值主要是變量傳遞,傳址主要是地址傳遞。按照對象的角度解釋,一個變量和地址都是一個對象,而數組也是一個對象,而這個對象並不能作為函數的參數傳入。

9. 我的strcat() 不行.我試了char *s1 = "Hello, "; char *s2 = "world!"; char *s3 = strcat(s1, s2); 但是我得到了奇怪的結果。

答:這裡主要的問題是沒有正確地為連接結果分配空間。C 沒有提供自動管理的字符串類型。

C 編譯器只為源碼中明確提到的對象分配空間(對於字符串, 這包括字符數組和串常量)。

程序員必須為字符串連接這樣的運行期操作的結果分配足夠的空間,

常可以通過聲明數組或調用malloc() 完成。strcat() 不進行任何分配; 第二個串原樣不動地附加在第一個之後。

因此, 一種解決辦法是把第一個串聲明為數組:

char s1[20] = "Hello, ";

由於strcat() 返回第一個參數的值, 本例中為s1, s3 實際上是多余的; 在strcat() 調用之後, s1 包含結果。

提問中的strcat() 調用實際上有兩個問題: s1 指向的字符串常數, 除了空間不足以放入連接的字符串之外,

甚至都不一定可寫。

評:這裡主要問題是對C語言基本編程裡面字符串常量和strcat的理解不清晰。首先,字符串常量放在rodata段裡,而字符串數組放在rwdata段裡;其次,strcat是將src指向的字符串追加到dest的NULL結尾,當dest越界將會發生不可知異常。

char *strcat(char *dest, const char *src);

The strcat() function appends the src string to the dest string, overwriting the terminating null byte ('\0') at the end of dest, and then adds a terminating null byte. The strings may not overlap, and the dest string must have enough space for the result. If dest is not large enough, program behavior is unpredictable; buffer overruns are a favorite avenue for attacking secure programs.

10. 那麼返回字符串或其它集合的正確方法是什麼呢?

答:返回指針必須是靜態分配的緩沖區, 或者調用者傳入的緩沖區,

或者用malloc() 獲得的內存, 但不能是局部(自動) 數組。

評:局部數組的作用范圍是函數內部,當函數返回,該內存區域將被釋放,此時返回的數組地址指向的是無效數據,因此返回的必須是調用者所在或者以上作用范圍的內存空間。

11. 我有個程序分配了大量的內存, 然後又釋放了。但是從操作系統看,內存的占用率卻並沒有回去。

答:多數malloc/free 的實現並不把釋放的內存返回操作系統, 而是留著供同一程序的後續malloc() 使用。

評:glibc庫采用的ptmalloc機制,該機制通過mmap和mumap獲取或者釋放系統內存,而在實際使用過程中,malloc和free並不直接與系統交互,該API主要是維護進程中的內存資源,因此,雖然free了內存,從ptmalloc機制看,free掉的資源並沒有立即釋放退還給操作系統。

12. calloc() 和malloc() 有什麼區別?利用calloc 的零填充功能安全嗎?free() 可以釋放calloc() 分配的內存嗎, 還是需要一個cfree()?

答:calloc(m, n) 本質上等價於:

p = malloc(m * n);

memset(p, 0, m * n);

填充的零是全零, 因此不能確保生成有用的空指針值或浮點零值free()

可以安全地用來釋放calloc() 分配的內存。

評:malloc和calloc主要還是考慮其性能在memset上的開銷。

13. 我認為我的編譯器有問題: 我注意到sizeof('a') 是2 而不是1 (即,不是sizeof(char))。

答:可能有些令人吃驚, C語言中的字符常數是int 型, 因此sizeof('a') 是sizeof(int),這是另一個與C++ 不同的地方。

評:這個僅僅是編譯器的定義問題,確實挺有意思的。64位系統sizeof(int)是4,32位系統sizeof(int)是2。

$ cat main.c

#include

int main()

{

char a;

printf("sizeof(a) = %d\n", sizeof(a));

printf("sizeof('a') = %d\n", sizeof('a'));

printf("sizeof(char) = %d\n", sizeof(char));

printf("sizeof(int) = %d\n", sizeof(int));

return 0;

}

$ gcc main.c

daniel@ubuntu:~/test$ ./a.out

sizeof(a) = 1

sizeof('a') = 4

sizeof(char) = 1

sizeof(int) = 4

$ g++ main.c

$ ./a.out

sizeof(a) = 1

sizeof('a') = 1

sizeof(char) = 1

sizeof(int) = 4

14. 為什麼聲明extern int f(struct x *p); 報出了一個奇怪的警告信息“結構x 在參數列表中聲明”?

答:與C 語言通常的作用范圍規則大相徑庭的是, 在原型中第一次聲明(甚至提到)的結構不能和同一源文件中的其它結構兼容,

它在原型的結束出就超出了作用范圍。要解決這個問題, 在同一源文件的原型之前放上這樣的聲明:

struct x;

它在文件范圍內提供了一個不完整的結構x 的聲明, 這樣, 後續的用到結構x的聲明至少能夠確定它們引用的是同一個結構x。

評:在代碼中盡量減少extern定義,該定義是比較破壞模塊化,增加耦合度的東西,就像編程中也不提倡使用goto語句。

15. 我不明白為什麼我不能象這樣在初始化和數組維度中使用常量:const int n = 5; int a[n];

答:const 限定詞真正的含義是 “只讀的”; 用它限定的對象是運行時 (同常) 不能被賦值的對象。

因此用 const 限定的對象的值並不完全是一個真正的常量。

在這點上 C 和 C++ 不一樣。如果你需要真正的運行時常量, 使用預定義宏 #define(或enum)。

評:const與define。兩者都可以用來定義常量,但是const定義時,定義了常量的類型,所以更精確一些。#define只是簡單的文本替換,除了可以定義常量外,還可以用來定義一些簡單的函數。

16. 我能否把 main() 定義為 void, 以避免擾人的 “main無返回值”警告?

答:不能。main() 必須聲明為返回 int, 且沒有參數或者接受適當類型的兩個參數。

如果你調用了 exit() 但還是有警告信息, 你可能需要插入一條冗余的 return語句

(或者使用某種 “未到達”指令, 如果有的話)。很多書不負責任地在例子中使用 void main(),

並宣稱這樣是正確的。但他們錯了。

評:gcc中void main()可以定義並使用,但從正常的角度來說main需要int返回,確保命令行執行命令有正確的$?返回值。

17. #pragma 是什麼, 有什麼用?

答:#pragam 指令提供了一種單一的明確定義的 “救生艙”, 可以用作各種 (不可移植的) 實現相關的控制和擴展:

源碼表控制、結構壓縮、警告去除 (就像 lint 的老 /* NOTREACHED */注釋), 等等。

評:在所有的預處理指令中,#Pragma 指令可能是最復雜的了,它的作用是設定編譯器的狀態或者是指示編譯器完成一些特定的動作。#pragma指令對每個編譯器給出了一個方法,在保持與C和C++語言完全兼容的情況下,給出主機或操作系統專有的特征。依據定義,編譯指示是機器或操作系統專有的,且對於每個編譯器都是不同的。

18. “#pragma once” 是什麼意思?我在一些頭文件中看到了它。

答:這是某些預處理器實現的擴展用於使頭文件自我識別; 它跟#ifndef技巧等價, 不過移植性差些。

評:其格式一般為: #pragma Para。其中Para 為參數,下面來看一些常用的參數message,code_seg,data_seg,once,hdrstop,resource,warning,comment,disable,region,pack

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved