細數 C++ 那些比起 C語言 更爽的特性

最後的紳士發表於2021-05-17

結構體定義

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,甚至 stringchar* 直接相加,得到一個新的 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。

相關文章