你可能聽說過C ++是C的超集。但如果你有兩種編程語言的經驗,你就會知道這根本不是真的。
當然,C ++有許多功能,C沒有;但也有一些功能只有C有,而C++沒有。 並且,也許最重要的是,有些代碼可以在兩種語言中編譯,但卻執行不同的操作。
你可以找到很多關於C ++、C之間異同的信息,但很多看起來很分散。在這裏,我創建了一個簡明的對比指南,並從C、C++語言規範標準中摘錄一些內容來支持這些異同。
注意事項:
本文主要針對C、C++語言, 所以你需要熟悉C或C ++中的其中之一,兩個都熟悉則更好。
當我提到C ++時,我指的是C ++ 11以上的版本,儘管本文大部分都適用於C++早期的標準。 我也將引用C ++ 17標準 (目前C++的最新標準)。
當我提到C時,我指的是C99標準,同時我也將參考C11標準(目前C的最新標準)。
值得注意的是,許多編譯器不完全兼容編程語言標準。這正是難以確定什麼是標準,什麼是不合規以及什麼是實現定義的部分原因。如果你想要查看其他編譯器的示例,我建議使用Compiler Explorer親自動手實踐一番,對比很有趣。
同樣的代碼,用兩種語言編譯,但結果不同
我認爲這是最重要的差異類別方法策略。
const
關鍵字const在C ++中與在C中具有不同的語義,實際上,它比我在第一次撰寫此博客文章時的想法更爲微妙。
差異歸結爲編程語言是否允許常量表達的編寫,常量表達式可以在編譯器編譯通過。例如,這裏通過常量來界定靜態數組的大小,下面的示例將用C ++編譯,但它是否在C中編譯將是實現定義的:
1 const size_t buffer_size = 5;
2 int buffer[buffer_size];
3
4 // int main() {
5 // ...
6 // }
但是常量表達式在C中的表現如何呢?
在這裏,我們引用C11標準的幾個部分以闡述爲什麼如此實現,C11 6.6 第6段定義了一個整數常量表達式:
整數常量表達式應具有整數類型,並且只能具有整數常量的操作數、枚舉常量、字符常量,結果爲整數常量的sizeof表達式,以及作爲強制轉換的直接操作數的浮點常量。 整數常量表達式中的轉換運算符只能將算術類型轉換爲整數類型,除非作爲sizeof運算符的操作數的一部分。
但什麼是“整數常數”? 從6.4.4開始,這些是字面值,而不是變量,例如 1。
這歸結爲只有像 1 或 5 + 7這樣的表達式可以是C中的常量表達式。變量不能是常量表達式。 正如我所料,此示例在gcc編譯編譯不通過,但它確實可以在Clang編譯通過:爲什麼?
答案見 C11 6.6第10段:
一種實現可以接受其他形式的常量表達式。
所以在C中,如果要編寫可移植版本代碼,上面的代碼必須使用宏預處理器:
1 #define BUFFER_SIZE (5)
2 int buffer[BUFFER_SIZE];
關鍵字const是由Bjarne Stroustrop爲C++創建的:減少對宏的需求。 所以,C ++對於什麼是常量表達式更加寬容,使得const變量更強大。
我驚訝地發現const起源於C ++,然後由C所採納。我假設const來自C,而C ++採用相同的概念並擴展它以減少對宏的需求。我理解C語言對宏的廣泛使用,但在標準化C時故意減少const的使用似乎並不明智。
修改const變量
以下代碼在C中使用導致約束違規:
1 const int foo = 1;
2 int* bar = &foo;
3 *bar = 2;
C11 6.5.16.1第1段列出了一些約束說明,其中一個約束必須爲真,類型轉換纔有效。我們的例子的相關約束如下:
左操作數具有原子性,限定或非限定指針類型,並且(考慮左值操作數在左值轉換後將具有的類型)兩個操作數都是指向兼容類型的限定或非限定版本的指針,左側指向的類型具有全部右邊指出的類型的限定符。
爲了符合要求,如果存在約束違規,編譯器必須進行診斷,這可能是警告或錯誤。 我發現它通常是一個警告,這意味着它通常可以在C中編譯,但運行後會給出未定義的結果:
上述代碼,在C ++中不會編譯。 我認爲這是因爲const T是與T不同的類型,並且不允許隱式轉換。 而在C中,const只是一個限定符。
C ++ 17 6.7.3:
類型的cv限定或cv非限定版本是不同類型。
無參的函數聲明
1 int func();
在C ++中,這聲明瞭一個不帶參數的函數。但同樣的語法,在C中則聲明瞭一個可以接受任意類型參數、任意數量參數的函數。
根據C11標準6.7.6.3第10和14段:
void類型的未命名參數作爲列表中唯一項的特殊情況指定該函數沒有參數。
函數聲明符中的空列表是該函數定義的一部分,指定該函數沒有參數。函數聲明符中的空列表不是該函數定義的一部分,它指定不提供有關參數數量或類型的信息。
所以在C中,以下代碼將是合法的:
1 // func.h
2 int func();
1 // func.c
2 int func(int foo, int bar) {
3 return foo + bar;
4 }
1 // main.c
2 #include "func.h"
3
4 int main() {
5 return func(5, 6);
6 }
不過,同樣代碼將導致C ++中的編譯器報錯:
main.c:5:12: error: no matching function for call to ‘func’
return func(5, 6);^~~~
./func.h:2:5: note: candidate function not viable:
requires 0 arguments, but 2 were provided
名稱解析
有一些常見的實現細節,使我們可以進一步闡明這一點。 假如我在Linux機器上使用Clang編譯器,則以下代碼可以在C下編譯和鏈接:
1 // func.h
2 int func(int foo, int bar);
1 #include <stdio.h>
2
3 // func.c
4 int func(float foo, float bar) {
5 return printf("%f, %f\n", foo, bar);
6 }
1 // main.c
2 #include "func.h"
3
4 int main() {
5 return func(5, 6);
6 }
但是上述代碼卻不能在C ++中編譯通過。
因爲,C ++編譯器通常使用名稱來進行函數重載。它們“破壞”函數的名稱以便對它們的參數進行編碼,例如:通過將參數類型附加到函數中。通常,C編譯器只將函數名稱存儲爲符號。我們可以通過反編譯C和C ++,來比較func.o的符號表看看這些區別。
C編譯的func.o解析如下:
╰─λ objdump -t func.o
func.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df ABS 0000000000000000 foo.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata.str1.1 0000000000000000 .rodata.str1.1
0000000000000000 g F .text 000000000000002e func
0000000000000000 UND 0000000000000000 printf
C++編譯的func.o解析如下:
╰─λ objdump -t func.o
func.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df ABS 0000000000000000 foo.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata.str1.1 0000000000000000 .rodata.str1.1
0000000000000000 g F .text 000000000000003b _Z4funcff
0000000000000000 UND 0000000000000000 printf
auto
auto在C ++中用於類型自動推斷,但同時auto也是一個C關鍵字,只是我從未真正看到工程實踐的應用。
以下C具有約束違規,即未指定type。這可能是錯誤,但我從來沒有找到一個編譯器給它任何東西,只是一個關於隱式轉換的警告:
1 int main() {
2 auto x = "actually an int";
3 return x;
4 }
在C99之前,如果沒有類型說明符是合法的,並且類型將被假定爲int。當我使用Clang和gcc編譯它時會發生這種情況,因此我們得到一個警告,因爲隱式將char數組轉換爲int。
在C ++中,直接顯示編譯不通過,因爲x的類型被推斷爲,
error: cannot initialize return object of type ‘int’ with an lvalue of type ‘const char *’
return x;
一些C有,但C ++沒有的功能
儘管C是一種非常短小精悍的編程語言,並且C ++很龐大,但C語言中有一些C ++沒有的有用功能。
可變長度數組
VLA允許定義具有可變長度的自動存儲的數組。例如:
1 void f(int n) {
2 int arr[n];
3 // ......
4 }
實際上,VLA在C11標準中是可選的,這使得它們無法移植。
但這些卻不是C ++的一部分,部分可能是因爲C ++標準庫在很大程度上依賴於動態內存分配來創建使用std::vector類似的容器。
受限的指針
C定義了第三種類型限定符(除了const和volatile):restrict。這僅用於指針。使指針受限制告訴編譯器“我將只通過此指針訪問底層對象以獲取此指針的範圍”,因此它不能混淆。如果你打破這個約束,你將得到未定義的行爲。
這有助於優化。一個典型的例子是memmove,你可以告訴編譯器src和dst不重疊。
引用 C11 6.7.3 第8段:
通過限制限定指針訪問的對象與該指針具有特殊關聯。這種關聯在下面的6.7.3.1中定義,要求對該對象的所有訪問都直接或間接地使用該特定指針的值.135)。
限制限定符(如寄存器存儲類)的預期用途是促進優化,並從構成符合程序的所有預處理翻譯單元中刪除限定符不會改變其含義(即可觀察行爲)。
受限的指針不是C ++標準的一部分,但實際上被許多編譯器擴展支持。
我對受限的指針感到疑惑,因爲它看起來好像玩火。有趣的是,在使用它時遇到編譯器優化錯誤似乎很常見,因爲我從未在真正使用過的代碼中應用過它。
特定初始化程序
C99引入了一種非常有用的初始化結構的方法,但我不明白它爲什麼沒有被C ++採用。
1 typedef struct {
2 float red;
3 float green;
4 float blue;
5 } Colour;
6
7 int main() {
8 Colour c = { .red = 0.1, .green = 0.5, .blue = 0.9 };
9 return 0;
10 }
在C ++中,你必須像這樣初始化:Colour c = {0.1,0.5,0.9}; 這對於Color的定義更改來說更難閱讀並且不健壯。我聽說指定的初始化程序未來將會在C ++ 20中實現,不過已經等了21年了。