1. 如何生成 "半全局變量", 就是那種隻能被部分源檔案中的部分函數通路變量?
答:
這在C語言中辦不到. 如果不能或不友善在一個源檔案中放下所有的函數, 那麼有三種的解決方案 :
(1) 為一個庫或相關函數的包中所有函數的包中的所有函數和全局變量增加一個唯一的字首, 并警
告包的使用者不能定義和使用文檔中列出的公有符号意外的任何帶有相同字首的其它符号. (換言之,
文檔中沒有提及的帶有相同字首的全局變量被約定為 "私有")
(2) 使用以下劃線開頭的名稱, 因為這樣的名稱普通代碼不能使用. (下劃線開頭表示"私有", 是一
種限制和建議)
(3) 通過連接配接器操作, 例如
- piyo.c
int love = 1313;
- hoge.c
int like_you(void) {
extern int love;
return love + 1;
}
在連結 hoge.o 的時候, 也需要 piyo.o 确定最終"半全局變量"位址.
2. 如何判斷哪些辨別符可以使用, 那些被保留了 ?
答:
(1) 辨別符的3個屬性: 作用域, 命名空間和連結類型.
[] C 語言有4種作用域(辨別符聲明的有效區域): 函數, 檔案, 塊和原型. (第4種類型僅僅存在于函
數原型聲明的參數清單中)
[] C 語言有4種命名空間: 行标(label, 即 goto 的目的地), 标簽(tag, 結構, 聯合和枚舉名稱), 結構
聯合成員, 以及标準所謂的其它"普通辨別符"(函數, 變量, 類型定義名稱和枚舉常量). 另一個名稱集(
雖然标準并沒有稱其為"命名空間")包括了預處理宏.這些宏在編譯器開始考慮上述4種命名空間之前
就會被擴充.
[] 标準定義了3中"連結類型": 外部連結, 内部連結, 無連結. 對我們來說, 外部連結就是指全部變量,
非靜态變量和函數(在所有的源檔案中有效); 内部連結就是指限于檔案作用域内的靜态函數和變量; 而
"無連結"則是指局部變量及類型定義(typedef)名稱和枚舉常量.
(2) ANSI/ISO C标準辨別符标準建議規則:
規則1: 所有下劃線大頭, 後跟一個大寫字母或另一個下劃線的辨別符永遠保留(所有的作用域, 所
有的命名空間).
規則2: 所有以下劃線打頭的辨別符作為檔案作用域的普通辨別符(函數, 變量, 類型定義和枚舉常
量)保留(為編譯器後續實作保留).
規則3: 被包含的标準頭檔案中的宏名稱的所有用法保留.
規則4: 标準中所有具有外部連結屬性的辨別符(即函數名)永遠保留用作外部連結辨別符.
規則5: 在标準頭檔案中定義的類型定義和标簽名稱, 如果對應的頭檔案被包含, 則在(同一個命名
空間中的)檔案作用域内保留.(事實上, 标準聲稱"所有作用于檔案作用域的辨別符", 但規則4沒有包含
辨別符隻剩下類型定義和标簽名稱了.)
3. char a{[3]} = "abc"; 是否合法 ?
答:
也許遠古時期這樣的表達式是合法的, 但現在(2018-08-14)是不合法的!
> error C2143: 文法錯誤: 缺少“;”(在“{”的前面)
> error C2143: 文法錯誤: 缺少“;”(在“[”的前面)
> error C2109: 下标要求數組或指針類型
> fatal error C1004: 發現意外的檔案尾
但 char a[3] = "abc"; 是合法的. 最後的 '\0' 沒有填充進去.
4. 程式運作正确, 但退出卻 "core dump"(核心轉存)了, 怎麼回事?
struct list {
struct list * next;
char * item;
}
/* Here is the main program */
main(argc, argv) {
puts("Hello, 世界");
}
答:
!!! 不要和寫出上面的格式的人說代碼, 怕你溝通能力有問題 ~
也許以前是崩潰, 但現在并沒有, 可以正常運作. 書中對于崩潰給出理由是, 一般而言, 傳回結構的函數
編譯器在實作時,會加入一個隐含的傳回指針, 這樣産生的 main 函數試圖接受 3 個參數, 而實際上隻有
兩個傳入(這裡,由C的啟動代碼傳入).
他說的很有挖掘價值, 但先進一點編譯器相容性很好. 它也為 main 函數在啟動函數棧中建構了傳回結
構的實體, 是以沒有崩潰.
我們來看這樣一段代碼
1 #include <stdio.h>
2
3 struct list {
4 struct list * next;
5
6 double number;
7 char * item;
8 int piyo;
9 };
10
11 struct list list_get(void) {
12 return (struct list){ NULL, 0, "Hello, 世界", 1 };
13 }
14
15 /* Here is the main program */
16 int main(int argc, char * argv[]) {
17 struct list node = list_get();
18 puts(node.item);
19
20 return 0;
21 }
運作到 17 行調試看反彙編代碼
struct list node = list_get();
002444EE lea eax,[ebp-0FCh]
002444F4 push eax
002444F5 call _list_get (024137Ah)
上面 push eax 表示傳入了隐式位址. 這裡有個有意思的現象, 如果我們傳回的結構體很小例如
struct list {
struct list * next;
char * item;
};
x64 是 16 位元組, 編譯器直接通過兩個寄存器搞定, 來幫我們優化代碼. 也不會在調用局部棧中構
造一個對象, 傳入位址.
在我們傳回結構體時候, 編譯器幫我們"隐含" 傳入結構體指針(寄存器)參數. 目前不推薦這樣的做
法, 因為存在淺拷貝性能浪費. 這也是 C++ 引入移動複制的原因. 但沒有屌用, 因為這本身就應該
編譯器去做.而不是讓程式員和編譯器雙宿雙飛, 可能下一代智能編譯器會優化的更好. 标準應該
推薦采用下面做法. 從這細節也可以看出, C 系列程式員對作業系統有種天然親和力, 這種親和力
也是把雙刃劍, 讓他太過于着魔, 落葉缤紛, 而敗北在楊柳樹下 ~
// 顯示聲明
void list_get(struct list * const out) {
out->next = NULL;
out->item = "Hello, 世界";
}
// 調用
struct list node; list_get(&node);
5. 可否用顯式括号來強制執行我所需要的計算順序并控制相關副作用? 就算括号不行, 操作符優先
級是否能夠計算順序呢?
答:
一般來說, 不行. 操作符優先級和顯示括号對表達式的計算順序隻有部分影響. 在如下的代碼中
f() + g() * h()
盡管我們知道乘法運算在加法之前, 但這并不能說明這3個函數哪個會被調用. 換言之, 操作符優先
級隻是 "部分" 地決定了表達式的求值順序. 這裡的 "部分" 并不包括對操作數的求值.
括号告訴編譯器那個操作數和那個操作數結合, 但并不要求編譯器先對括号内的表達式求值 .
在上面表達式中再加括号
f() + ( g() * h() )
也無助于改變函數調用的順序. 同樣, 對 i++ * i++ 的表達式加括号也毫無幫助, 因為 ++ 比 * 的優
先級高:
(i++) * (i++) /* WRONG */
這個表達式有沒有括号都是未定義的.
如果需要確定子表達式的計算順序, 可能需要使用顯式的臨時變量和獨立語句.
6. 我有些代碼包含這樣的表達式.
a ? b = c : d
有些編譯器可以接受, 有些卻不能. 為什麼 ?
答:
在 C 語言原來的定義中, = 的優先級是低于 ? : 的, 是以早期的編譯器傾向于這樣解釋這個表達式:
(a ? b) : (c : d)
然而, 因為這樣沒什麼意義, 後來編譯器都接受了這種表達式, 并用這樣的方式解釋(就像裡面暗含
了一對括号);
a ? (b = c) : d
這裡, = 号的左操作數隻是 b, 而不是非法的 a ? b. 實際上 ANSI/ISO C 标準中指定的文法就要求這
樣的解釋. (标準中關于這個的文法不是基于優先級的, 且指出了在 ? 和 : 符号之間可以出現任何表
達式).
問題中這樣的表達式可以毫無問題地被 ANSI 編譯器接受. 如果需要在較老的編譯器上編譯, 總
可以增加一對内部括号.
曆史總是那麼有意思, 現在編譯器都已經支援這個跳過優先級的而存在的表達式了 ~ 畢竟那是
标準.
7. 我有一個 char * 型指針碰巧指向一些 int 型變量, 我想跳過它們. 為什麼 ((int *)p)++; 這樣的代碼
不行?
答:
在 C 語言中, 類型轉換操作符并不意味者 "把這些二進制位看作另一種類型, 并作相應的處理". 這是一個
轉換操作符, 根據定義它隻能生成一個右值(rvalue). 而右值即不能指派, 也不能用 ++ 自增. (如果編譯器
接受這樣的表達式, 那要麼是一個錯誤, 要麼是有意做出非标準擴充.) 要達到你的目的可以用
p = (char *)((int *)p + 1);
或者, 因為 p 是 char * 型, 直接用
p += sizeof(int);
要想真正明白無誤, 你得用
int * ip = (int *)p;
p = (char *)(ip + 1);
但是, 可能的話, 你還是應該一開始就選擇适當的指針類型, 而不是一味地試圖桃僵李代.
8. 我看到下面這樣的代碼:
char * p = malloc(strlen(s) + 1);
strcpy(p, s);
難道不應該是 malloc((strlen(s) + 1) * sizeof(char)) 嗎?
答:
永遠不必乘上 sizeof(char), 因為根據定義, sizeof(char) 嚴格為 1. 另一方面, 乘上 sizeof(char) 也沒有
害處, 有時候還可以幫忙為表達式引入 size_t 類型.
而且就算 char 類型定義為 16 位, sizeof (char) 依然是1, 而 <limits.h> 中 CHAR_BIT 會被定義為 16. 屆
時将不能聲明 (或用 malloc 配置設定) 一個 8位的對象.
傳統上, 一個位元組不一定是8位, 它不過是一小段記憶體, 通常适于存儲一個字元. C 标準遵循了這種用
法, 是以 malloc 和 sizeof 所使用的位元組可以是 8 位以上(8位位元組正式稱為八位位元組 octet, 标準不允許
低于 8 位).
為了不用擴充 char 類型就能操作多語言字元集, ANSI/ISO C 定義了 "寬"字元類型 wchar_t 以及對
應的寬字元常量和寬字元串字面量, 同時也提供了操作和轉換寬字元串函數.
可能有些令人驚訝, 在C語言中字元字面量是 int 類型, 是以 sizeof ('a') 是 sizeof (int) 而不是 sizeof (char)
這是和 C++ 中不同地方, C++ 'a' 被當作 char 類型字元字面量.
9. 我很吃驚, ANSI 标準竟然有那麼多未定義的東西. 标準的唯一任務不就是讓這些東西标準化嗎?
答:
某些構造随編譯器和硬體的實作而變化嗎這一直是C語言的一個特點. 這種有意的不嚴格可以讓編譯器
生成效率更高的代碼, 而不必讓所有程式為了不合理的情況承擔額外的負擔. 是以, 标準隻是把現存的實
踐整理成文.
程式設計語言标準可以看作是語言使用者和編譯器實作者之間的協定. 協定的一部分是編譯器實作者同
意提供, 使用者可以使用的功能. 而其它部分則包括使用者同意遵守和編譯器實作者認為會被遵守的規則. 隻
要雙方都恪守自己的保證, 程式就可以正确運作. 如果任何一方違反它的諾言, 則結果肯定失敗.
面對未定義行為的時候(包括範圍内的實作定義行為和不确定行為), 編譯器可能做任何實作, 其中也
包括你所期望的結果. 但是依賴這個實作卻不明智.
Roger Miller 提供了看待這個問題另一個角度:
"有人告訴我打籃球的時候不能抱着球跑. 我拿個籃球, 抱着就跑, 一點問題都沒有. 顯然他并不懂籃球."
10. 有什麼好的方法來檢查浮點數在 "足夠接近" 情況下相等?
答:
浮點數的定義決定它的絕對精度會随着其量級而變化, 是以比較兩個浮點數的最好方法就要利用一個浮
點數的量級相關的精确門檻值. 不要用下面這樣的代碼:
double a, b;
...
if (a == b) /* WRONG */
要用類似這樣的方法(相對因子):
#include <math.h>
#include <float.h>
if (fabs(a - b) <= fabs(a) * DBL_EPSILON)
#define DBL_EPSILON 2.2204460492503131e-016 // smallest such that 1.0+DBL_EPSILON != 1.0
DBL_EPSILON 是 float.h 中一個特定極小的值來控制"接近度".
if (fabs(a - b) < 0.001) /* POOR */
對于上面 0.001 這樣的絕對模糊因子恐怕難以持續有. 随着被比較的數不斷變化, 很有可能兩個較小的, 本
不應該看作不相等的數正好相差小于 0.001, 而兩個本應看作相等的兩個大數相差大于 0.001. (顯然, 模糊
因子修改為0.0005或者0.00001或者其它任何絕對數都無助于解決這個問題.)
Doug Gwyn 推薦用下面的 "相對差" 函數. 它傳回兩個實數的相對內插補點, 如果兩個數完全相同, 則傳回
0.0, 否則, 傳回內插補點和較大數的比值:
#include <math.h>
#include <float.h>
#include <stdlib.h>
inline double reldif(double a, double b) {
double c = fabs(a), d = fabs(b);
d = max(c, d);
return d == 0 ? 0 : fabs(a-b)/d;
}
典型的用法是:
if (reldif(a, b) < DBL_EPSILON) ...
11. 我有個接受 float 型的變參函數, 為什麼 va_arg(arg, float) 卻不行 ?
答:
"預設參數提升" 規則适用于可變參數中可變部分: 參數類型為 float的總是提升到 double, char 和 short
提升到 int (無符号 unsigned). 是以 va_arg(arg, float) 是錯誤用法. 應該使用 va_arg(arg, double). 同理,
要用 va_arg(arg, int) 來取得原來類型是 char, short 或 int 的參數. 基于同樣的理由, 傳給 va_start 的最
後一個"固定"參數類型不會被提升. (printf(char const* const fmt, ...) 類比 fmt 參數一定不會被提升.)
12. 用什麼方法計算整數中為1的位的個數最高效?
答:
許多像這樣的位操作可以使用查找表格來提高效率和速度. 這段代碼是以每次4位的方式計算數值中為1
的位個數的小函數:
int bitcnt(unsigned u) {
static int bitc[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
int n = 0;
while (u) {
n += bitc[u & 0xFF];
u >>= 4;
}
return n;
}
這個查表思路極快, 突破在于更大的表, 空間換時間. 還有一種微軟面試題套路是
int count(unsigned u) {
int n = 0;
while (u) {
++n;
u = (u-1) & u;
}
return n;
}
二者對比一下, 最壞情況 32 個 1 第一個好, 最好情況 0 個 1 兩個一樣. 但對于 0xF000 情況前者是4次循環,
後者 1次. 但拍腦門還是前者好. 預計 32位無符号數出現 1 的期望是 16, 前者最壞執行 8 次, 後者一定要執
行16次.從數學期望上面而言前者占優勢, 畢竟算法 1 後續還可以建構一位元組表更迅速 ~ 權當一樂.
13. 什麼是 "達夫裝置" (Duff's Device)
答:
這是個很棒的迂回循環展開法, 由 Tom Duff 在 Lucasfilm 時設計. 它的 "傳統" 形态是用來複制多個位元組
void copy(int * to, int from[], int count) {
register n = (count + 7) / 8; /* count > 0 assumed */
switch(count % 8) {
case 0:
do {
*to = *from++;
case 7:
*to = *from++;
case 6:
*to = *from++;
case 5:
*to = *from++;
case 4:
*to = *from++;
case 3:
*to = *from++;
case 2:
*to = *from++;
case 1:
*to = *from++;
} while (--n > 0);
}
}
這裡 count 個位元組從 from 指向的數組複制到 to 指向的記憶體位址(這是個記憶體映射的輸出寄存器, 這也是
為什麼它沒有被增加). 它把 switch 語句和複制 8 個位元組的循環交織在一起, 進而解決了剩餘位元組的處理
問題(當 count 不是 8 的倍數時). 信不信由你, 像這樣的 case 标志放在嵌套在 switch 語句内的子產品中是
合法的. 當他向 C 的開發者和世界公布這個技巧時. Diff 注意到 C 的 switch 文法, 特别時"跌落"行為, 一
直是備受争論的, 而 "這段代碼在争論中形成了某種論據, 但我不清楚是贊成還是反對".
後記 - 引述
- 錯誤是難免, 歡迎指正交流提升 ~
13 年剛工作的時候在地鐵上看完 <<C語言問題>>, 随後就扔掉了. 過去好久, 18 年有幸又買了一本
<<C語言問題>> 看完後被其中好多段子說的心癢難耐. 是以就記錄一些很經典的讨論, 供大家一塊開
懷. 了解那些塵封在曆史長河中, 問題由來的真相 ~
-------: ( :--
酷 - https://y.qq.com/n/yqq/song/003K5qlb0r7BDb.html?ADTAG=baiduald&play=1