結構體定義
C:
typedef struct Vertex {
int x, y, z;
} Vertex;
Vertex v1 = { 0 };
// or
struct Vertex {
int x, y, z;
};
struct Vertex v1 = { 0 };
C++:
struct Vertex {
int x, y, z;
};
Vertex v1 = {};
如果你一開始學的C++,再去寫C的時候,你就會一臉懵逼怎麼我的結構體編譯不了。。。
為特定型別分配堆記憶體
C:
Vertex* ptr = malloc(sizeof(Vertex) * 10);
free(ptr);
C++:
Vertex* ptr = new Vertex[10];
delete[] ptr;
malloc 的引數是位元組,所以得配合 sizeof 用。C++ 的 new 引數是個數,自動根據型別分配對應位元組,看起來可讀性更強。malloc始終返回的是 void*
, C 裡面 void*
可以任意轉換到其他型別的指標。C++ 的 new 返回的是指定型別的指標,型別系統進更加嚴格。
計算固定大小陣列的元素個數
C:
Vertex arr[1024];
int arrSize = sizeof(arr) / sizeof(Vertex);
C++:
Vertex arr[1024];
int arrSize = std::size(arr);
你當然可以寫死 int arrSize = 1024; 但這樣就不優雅了,不爽了。
RAII
C 語言經常出現 alloc、free 這樣用來建立銷燬資源的成對函式,新手很容易忘記呼叫 free 導致記憶體洩漏:
Ball* ball = ball_alloc();
// ...
while (ball->isLive) {
// ...
if (ball->size > 5) {
return; // 哦豁,完蛋
}
}
ball_free(ball);
return;
特別是各種條件判斷裡面帶 return 的,可能有人覺得在條件裡面寫 return 那是你程式碼風格有問題,這個就見仁見智了。
C++ 只要你寫好解構函式,那以上問題你就不需要操心:
class Ball {
public:
Ball ();
~Ball ();
}
void foo () {
Ball ball();
// ...
while (ball.isLive) {
// ...
if (ball.size > 5) {
return;
}
}
return;
} // 退出 foo 函式之前必定會執行 ~Ball
準確來說,C++ 變數結束生命週期的時候,就會執行它對應的解構函式,再具體一點,就是當你離開一個大括號的範圍時,在這個大括號裡面建立的變數,都會析構,比如 for while 迴圈裡面建立的變數,或者是 if 語句塊裡面建立的變數都是這樣的,或者乾脆你自己在中間寫一個大括號:
int main () {
{
Ball ball;
printf("");
} // 這裡 ball 會析構
return 0;
}
可惜 C++ 不能從語句塊返回一個值,rust 就有這個不錯的特性。
引用
引用用的好,指標不需要,當你用引用可以解決問題的時候,就別用指標。引用不存在野指標這類情況,他的作用範圍更加嚴格。對引用操作,就是對本體操作,也不需要和指標一樣用 ->
,直接 .
就好。指標型別的變數需要記憶體空間來儲存一個記憶體地址,而引用只是一個別名,不需要空間儲存記憶體地址。對於 a.b.c.d
這樣一長串的表示式,用引用會更舒服(auto& d = a.b.c.d
)。
rust語言裡面變數所有權概念,就是對C++引用擴充而已。
動態陣列 vector
前面說了 C++ 的 new 是個好東西,但是 vector 更好。vector 本身有解構函式,生命週期結束自動呼叫裡面每一個物件的解構函式,所以不用像 new 一樣需要 delete。通常 C語言函式 傳入一個陣列,一般需要同時傳入陣列指標和陣列大小,但是 C++ 你可以直接把 vector 當引數傳入,本身就可以呼叫 size() 獲取大小。
C:
void foo (Vertex* arr, int size) {
// ...
}
C++:
void foo (vector<Vertex>& arr) {
// ...
}
C++ 可以自由選擇傳引用還是傳值,C語言只能傳指標。即便你在引數寫上 Vertex arr[10],你以為他就能傳值了?錯了,當你想用 sizeof (arr) 得到陣列大小時,它返回的是指標的大小,所以這就說明傳進來的還是指標。
同樣的道理,當你想返回陣列,在函式返回型別寫上 Vertex[10] 的時候,也是不行的,沒有這樣的寫法,即便是固定大小的陣列都不行。所以很多 C API 需要返回陣列的時候怎麼辦?答案就是,你先自己分配好記憶體,再把指標傳進去,他寫入內容。那如果你也不知道陣列長度多少怎麼辦,那一般會有一個API負責可以返回大小。
C++ 就爽快多了,你直接返回你在函式裡面建立的 vector 就行,編譯器會很貼心把這個變數的生命週期轉移給呼叫者,不會發生任何額外複製。
C:
{
int size = GetSize();
Ball* balls = malloc(sizeof(Ball) * size);
GetBalls(balls, size);
free(balls);
}
C++:
{
vector<Ball> balls = GetBalls();
// 爽爽爽
}
對了,vector<bool>
請謹慎使用?
auto 關鍵字
這個僅限於寫的人爽,看的人應該會很痛苦。因為C++有了泛型(呃,或者我應該叫它模板類?),導致型別名字會變得很長,特別是模板類裡面還有模板類的套娃情況,此時用 auto 就會十分爽了。更加驚喜的是,連函式返回型別都可以 auto。
auto GetBalls () {
vector<Ball> balls;
// ...
return balls;
}
int main () {
auto balls = GetBalls();
}
不知道有沒有開源專案全程 auto 的,我想觀摩觀摩。。。
std::string
C語言 表達字串就是很簡單的用 char*
表示, 最後一個 char 為 0,代表字串結束,這很便利,所以 printf 等函式不需要你告訴他字串的長度,他自己遇到 0 就停下來了。函式 strlen 也因此可以計算字串長度。如果你是其他語言過來的,期待可以字串可以用 + 號連線,那你要失望了,C語言沒有這種操作,通常做法是用 sprintf,不僅寫起來麻煩,還需要你自己先準備好一個“足夠”長的緩衝區,每次一些函式告訴我需要一個緩衝區但不告訴我多長的時候,我就會生理不適。後期增加了一個新函式 sprintf_s ,需要明確告訴函式你的緩衝區有多長,這樣可以避免寫出界,但依然沒有改變用起來很麻煩的情況。
C++ 有了一個新選擇:std::string,他和 vector 非常相似,也支援很多類似的操作。最驚喜的是,它過載了 + 運算子,可以直接把 string 和 string,甚至 string
和 char*
直接相加,得到一個新的 string:
string str = string("one") + "two" + "three";
printf(str.c_str());
c_str() 返回一個 const char*
來相容 C API 的操作,但是千萬注意這個指標的生命週期,當你拿著它到處傳遞的時候,務必注意 string str 什麼時候會析構。
有的時候 sprintf 其實比+更有用,但 string 和 sprintf 一起用的時候,又回到了從前。。。也許 C++ 應該有個配套的字串格式化函式吧。。。但不好意思,很長時間都沒有這種東西,直到 C++20,才有了 std:format
,起碼過去了20年,20年!知道這20年大家怎麼過的嗎!?
函式過載與預設引數
C++:
void foo (int a = 0, int b = 0);
void foo (int a, int b) {
// ...
}
int main () {
foo(); // ok
foo(1); // ok
foo(1, 2); // ok
}
不多解釋,反正 C語言 就是不行。
名稱空間
C語言 你寫的每一個函式其實都是全域性的,都得給他取一個名字,當你把其他庫連結進來的時候,這些名字可能會和其他庫裡面名稱產生衝突,唯一的解決辦法就是改名字。
C++ 的名稱空間完美解決了此類問題,你可以起一個長一點的 namespace,然後使用短的函式名稱,別人可以決定使用完整的名稱,又或者宣告省略整個空間名(其實是把指定空間合併到當前的名稱空間),又或者給空間名取一個別名。
namespace giegie {
void xinteng() {
}
}
int main()
{
giegie::xinteng();
{
using namespace giegie;
xinteng();
}
{
namespace gg = giegie;
gg::xinteng();
}
return 0;
}
並且可以自由決定這種行為的作用域。
lambda 表示式
很多場景需要你傳遞一個函式指標,用於回撥,C語言你就得在全域性宣告一個函式了,而 C++ 你可以直接在函式,甚至語句塊內部使用 lambda 表示式,嚴格限制範圍,增強程式碼可讀性。lambda 在不使用捕獲的情況下可以輕鬆自動轉換為純函式指標。lambda 的捕獲不得不說實在是非常驚豔,可以像 Javascript 語言那樣直接訪問到 lambda 外部的變數:
int main() {
int a = 1;
int b = 2;
// 這裡要是沒有 auto 我都不會寫了?
auto foo = [&a, &b](int c) {
return a + b + c;
};
int sum = foo(3); // sum is 6
}
你可以自由決定是把 a、b 複製傳遞,還是直接傳引用。複製你就無需擔心捕獲變數的生命週期問題,適用於非同步呼叫的情況。引用捕獲你可以對外部變數直接修改。
結尾
以上說的這些爽快的特性,必須要你經歷過C語言一段時間的洗禮後,才能深有體會。C++ 當然還有很多沒說到到的新特性,我也只是挑一點來說而已,比如最重要的 class 我反而隻字未提,很多人覺得必須要把 C++ 所有特性全部掌握,才算是會 C++,才有資格用,我認為大可不必,並不是語言提供了什麼特性你都非得要用上,程式導向可以乾淨利落解決問題就沒有必要非得物件導向。況且有些“特性”真的一言難盡,比如我就寧願用 printf 而不是 cout。