c語言是一門非常古老的語言,創立於2023年,距今已經有48年的歷史,和很多更現代的語言(python、c#、golang)相比,c語言的編譯過程中存在一些缺陷。這些缺陷不僅會加重開發人員的負擔,也會隱藏一些難以發現的bug。而c++為了保持與c的相容,也繼承其中的很多缺陷。下面是一些常見的c++編譯缺陷。
但是為函式和全域性變數生成的符號資訊存在缺陷,函式的符號不包含返回值資訊,全域性變數的符號不包含型別資訊。
假設我們有乙個 foo.cpp 檔案,其中定義了乙個全域性變數 double g_pi =3.14 ,乙個函式 int foo(int width,int height),如下所示:
// foo.cpp
double g_pi = 3.14;
int foo(int left, int right)
編譯foo.cpp生成的foo.o,其中的符號檔案如下:
$ g++ -c foo.cpp # 僅進行編譯,生成foo.o檔案
$ nm foo.o
0000000000000000 t _z3fooii
0000000000000000 d g_pi
可以看出,函式 foo 的符號是 _z3fooii,其中只有兩個引數的資訊,沒有返回值的資訊。而全域性變數 g_pi 的符號則沒有包含任何型別資訊。
// main.cpp
#include using namespace std;
extern int g_pi; // 變數型別錯誤,正確寫法是 extern double g_pi;
double foo(int left, int right); // 函式返回值錯誤,正確寫法是 int foo(int width, int height)
int main()
編譯的時候,沒有任何報錯,但執行的時候,就出錯了。
$ g++ main.cpp foo.cpp -o main
$ ./main
1374389535
-nan
我們知道,優秀的語言,應該是在編譯過程中能發現盡量多的bug。但c++為了保持相容,在設計上繼承了c的符號系統的缺陷,這就導致這類問題無法在編譯層面解決。
c/c++**的組織方式是,可以將**放到多個原始檔中,各原始檔如果想呼叫對方的函式,只需要 include 相應的標頭檔案即可。標頭檔案中有原始檔中函式和全域性變數的定義。
由此帶來的乙個問題是,乙個函式或者全域性變數的定義,會出現在兩個檔案中,並且必須保持一致。假設原始檔中的函式原型發生變化,還需要修改標頭檔案中的原型,否則肯能導致編譯或者鏈結的錯誤。而且由於c++支援過載,在大多數情況下,是出現令人惱火的鏈結錯誤。
此外,如果程式設計師在修改標頭檔案時因疏忽犯錯,導致標頭檔案和原始檔中函式原型不一致,在某些情況下,編譯器是識別不出來的,可能要等到執行的時候才會出錯,而這個執行錯誤很可能要等程式執行很長時間才發現。上一部分缺陷1中,已經舉例出一些這樣的場景。這裡再舉另外乙個場景。
假設有乙個函式 cylindrical_volume,計算圓柱形的體積,其函式的原型和實現如下
// cylindrical_volume.h
double cylindrical_volume(double radius, double height);
// cylindrical_volume.cpp
double cylindrical_volume(double radius, double height)
假設程式設計師在重構**時,將 cylindrical_volume.cpp 中函式的兩個引數互換了位置,但忘記修改標頭檔案了。由於這個修改不會改變函式的符號資訊,因此這個bug在編譯和鏈結都不會暴露出來,直到程式執行時才會出現。
// cylindrical_volume.h
double cylindrical_volume(double radius, double height);
// cylindrical_volume.cpp
double cylindrical_volume(double height, double radius) // 修改了兩個引數的位置
// main.cpp
#include #include "cylindrical_volume.h"
using namespace std;
int main()
這個**中,main函式以為自己計算的是半徑為1,高度為2的圓柱形體積(等於6.28),但其實計算的是半徑為2,高度為1的圓柱形體積(等於12.56),程式最終得到乙個錯誤的輸出。
這可能是對c/c++初學者最不友好的缺陷了。筆者記得自己在工作後第一次碰到這個問題時,向旁邊的同事狠狠地吐槽:誰tm再說c++是一門高階語言,勞資就跟誰急!
c++對鏈結庫的先後順序是有要求的,假設程式用到了兩個靜態庫 libx.a 和 liby.a,其中 liby.a 會用到 libx.a 中的函式,也就是說 liby.a 依賴 libx.a,那麼在鏈結引數需要這樣寫,也就是說被依賴的庫,應該要寫到依賴庫的後面。
$ g++ -o main main.cpp -l liby.a libx.a
$ g++ -o main main.cpp -l liby.a libx.a liby.a
# or
$ g++ -o main main.cpp -l libx.a liby.a libx.a
那為什麼c++要求一定將被依賴的庫放到後面呢?因為c++從c**繼承了乙個編譯特性——單遍編譯。
所謂單遍編譯,是指編譯的過程中,編譯器只掃瞄一次源**,鏈結器也只掃瞄一次鏈結物件,在任何時候,編譯器和鏈結器都不會回頭看前面的源**或者鏈結物件。
c++由於語法更複雜,目前編譯器已經沒有辦法做到單遍編譯,但鏈結器目前仍然保持了單遍編譯的特性。
鏈結器由於要在一輪的掃瞄中,解析所有物件檔案中所有未決的符號,因此需要以特定的順序來掃瞄這些有相互依賴關係的物件檔案。c和c++選擇的方式是被依賴的檔案放在後面,這樣鏈結器在掃瞄的過程中,只需要記住當前所有未決的符號,在後面的物件檔案中找到相應的符號後,再對其進行解析就可以了。
c++的這個特性,能讓鏈結工具工作效率更高,並且更容易開發。但代價卻是增加了程式設計師的工作負擔。
c++雖然號稱是一門高階語言,是一門現代的語言,但因為要相容c語言的特性,存在很多設計上的缺陷。如果乙個人只學習c/c++,可能對這些缺陷沒有感覺,認為一切都是理所當然的,甚至將這些缺陷當作是語言的特點。但當你接觸更多語言後,對比之下,這種設計上的缺陷就會變得很明顯了。
相容c語言,是c++能廣泛流行的原因之一,但也因為這個原因,導致c++相比其他語言,對開發者不那麼友好,這終將導致其他語言逐步蠶食c++的領域。所謂成也蕭何,敗也蕭何。
c 編譯鏈結過程
llinux下編譯乙個c 程式的典型過程 1.編譯預處理 預編譯程式完成的工作,可以說成是對源程式的 替換 工作。經過這個過程,生成乙個沒有巨集定義 沒有條件編譯指令 沒有特殊符號的輸出檔案。2.編譯 優化階段 通過詞法分析 語法分析,在確認所有的指令都符合語法規則之後,將其翻譯成等價的中間 或彙編...
編譯鏈結過程
在談編譯鏈結過程之前我們需要了解一下虛擬位址空間以及程式在編譯鏈結過程時經過了什麼步驟。虛擬位址空間之前在程序空間的部落格中詳細介紹過了,詳見 上圖就是32位系統中4g虛擬位址空間的分布情況 text 段 指令段,存放的是指令 在程式中,我們把區域性變數定義 區域性變數的 定義是指令而不是資料 還有...
C 中的編譯和鏈結過程
平時我們所說的編譯主要包括預編譯 編譯 彙編三部分,下面分別簡單介紹一下 預編譯 由原始檔 cpp c 生成 i 檔案 主要工作 a 展開所有的巨集定義,消除 define b 刪除所有的注釋 c 處理 include預編譯指令,將包含檔案插入到該預編譯的位置 d 處理所有的預編譯指令,比如 if ...