一個C#開發者重溫C++的心路歷程

kiba518發表於2019-05-20

前言

這是一篇C#開發重新學習C++的體驗文章。

作為一個C#開發為什麼要重新學習C++呢?因為在C#在很多業務場景需要呼叫一些C++編寫的COM元件,如果不瞭解C++,那麼,很容易註定是要被C++同事忽悠的。

我在和很多C++開發者溝通的時候,發現他們都有一個非常奇怪的特點,都很愛裝X,都覺得自己技術很好,還很愛瞧不起人;但如果多交流,會發現更奇怪的問題,他們幾乎都不懂程式碼設計,程式碼寫的也都很爛。

所以,這次重溫C++也是想了解下這種奇異現象的原因。

C++重溫

首先開啟VisualStudio,建立一個C++的Windows控制檯應用程式,如下圖:

圖中有四個檔案,系統預設為我開啟了標頭檔案和原始檔的資料夾。

系統這麼做是有意義的,因為剛學習時,外部依賴項,可以暫時不用看,而資原始檔夾是空的,所以我們只專注這兩個資料夾就可以了。

作為一個C#開發,我對C++就是一知半解,上學學過的知識也都忘記的差不多了,不過,我知道程式入口是main函式,所以我在專案裡先找擁有main函式的檔案。

結果發現ConsoleTest.cpp 檔案裡有main函式,那麼,我就在這個檔案裡開始學習C++了,而且它的命名和我專案名也一樣,所以很確定,它就是系統為我建立的專案入口檔案。

然後我開啟ConsoleTest.cpp 檔案,定義一個字串hello world,準備在控制檯輸出一下,結果發現編譯器報錯。。。只好調查一下了。

調查後得知,原來,c++裡沒有string型別,想使用string型別,只能先引用string的標頭檔案,在引用名稱空間std,如下:

#include "pch.h" 
#include <string>
using namespace std;
int main()
{
	string str = "Hello World!\n"; 
}

標頭檔案

標頭檔案到底是什麼呢?

標頭檔案,簡單來說就是一部分寫在main函式上面的程式碼。

比如上面的程式碼,我們將其中的引用標頭檔案和使用名稱空間的程式碼提取出來,寫進pch.h標頭檔案;然後,我們得到程式碼如下圖:

pch.h標頭檔案:

ConsoleTest.cpp檔案:

也就是說,標頭檔案是用來提取.cpp檔案的程式碼的。

呃。。。好像標頭檔案很雞肋啊,一個檔案的程式碼為什麼要提取一部分公共的?寫一起不就好了!為什麼要搞個檔案來單獨做,多傻的行為啊!

好吧,一開始我也的確是這麼想的。

後來我發現,標頭檔案,原來並不是單純的提取程式碼,還是跨檔案呼叫的基礎。

也就是說,ConsoleTest.cpp檔案,想呼叫其他Cpp檔案的變數,必須通過標頭檔案來呼叫。

比如,我新建一個test.cpp和一個test.h檔案。

然後我在test.cpp中,定義變數test=100;如下:

#include "pch.h"
#include "test.h"
int test = 100;

接著我在test.h檔案中再宣告下test變數,並標記該變數為外部變數,如下。

extern int test;

現在,我在回到ConsoleTest.cpp檔案,引用test.h檔案;然後我就可以在ConsoleTest.cpp檔案中使用test.cpp中定義的test變數了,如下:

#include "pch.h" 
#include "test.h" 
int main()
{
	string str = "Hello World!\n"; 
	cout << test << endl;
}

如上述程式碼所示,我們成功的輸出了test變數,其值為100。

到此,我們應該瞭解到了,標頭檔案的主要作用應該是把被拆散的程式碼,扭到一起的紐帶。

----------------------------------------------------------------------------------------------------

PS:我在上面引用字串標頭檔案時,使用的引用方法是【#include <string>】;我發現,引用該標頭檔案時,並沒有加字尾.h;我把字尾.h加上後【#include <string.h>】,發現編譯依然可以通過。

簡單的調查後得知,【#include <string>】是C++的語法,【#include <string.h>】是語法。因為C++要包含所有C的語法,所以,該寫法也支援。

Cin與Cout

Cin與Cout是控制檯的輸入和輸出函式,我在測試時發現,使用Cin與Cout需要引用iostream標頭檔案【#include <iostream>】,同時也要使用名稱空間std。

#include <iostream>
using namespace std;

在上面,我們提到過,使用字串型別string時,需要引用標頭檔案string.h和使用名稱空間std,那麼現在使用Cout也要使用名稱空間std。這是為什麼呢?

只能推斷,兩個標頭檔案string.h和iostream.h在定義時,都定義在名稱空間std下了。而且,通過我後期使用,發現還有好多類和型別也定義在std下了。

對此,我只能說,好麻煩。。。首先,缺失基礎型別這種事,就很奇怪,其次不是一個標頭檔案的東西,定義到一個名稱空間下,也容易讓人混亂。

不過,對於C++,這麼做好像已經是最優解了。

----------------------------------------------------------------------------------------------------

PS:Cin與Cout是控制檯的輸入和輸出函式,開始時,我也不太明白,為什麼使用這樣兩個不是單詞的東西來作為輸入輸出,後來,在調查資料時,才明白,原來這個倆名字要拆開來讀。

讀法應該是這樣的C&in和C&out,這樣我們就清晰明白的理解了該函式了。

define,typedef,指標,引用型別,const

define

首先說define,define在C++裡好像叫做巨集。就定義一個全域性的字串,然後再任何地方都可以替換,如下:

#include "pch.h" 
#include "test.h" 
#define ERROR 518
int defineTest()
{
	return ERROR;
}
int main()
{ 
	cout << defineTest() << endl;
} 

也就是說,define定義的巨集,在C++裡就是個【行走的字串】,在編譯時,該字串會被替換回最初定義的值。這。。。這簡直就是編譯器允許的bug。。。

不過,它當然也有好處,就是字串更容易記憶和理解。但是說實話,定義一個列舉一樣好記憶,而且適用場景更加豐富,所以,個人感覺這個功能是有點雞肋,不過C++好多程式碼都使用了巨集,所以還是需要了解起來。

typedef

typedef是一個別名定義器,用來給複雜的宣告,定義成簡潔的宣告。

struct kiba_Org {
	int id;
};
typedef struct kiba_new {
	int id;
} kiba;
int main()
{
	struct kiba_Org korg;
	korg.id = 518;
	kiba knew;
	knew.id = 520;
	cout << korg.id << endl;
	cout << knew.id << endl;
} 

如上述程式碼所示,我定義了一個結構體kiba_Org,如果我要用kiba_Org宣告一個變數,我需要這樣寫【struct kiba_Org korg】,必須多寫一個struct。

但我如果用typedef給【struct kiba_Org korg】定義一個別名kiba,那麼我就可以直接拿kiba宣告變數了。

呃。。。對此,我只能說,為什麼會這麼麻煩!!!

以為這就很麻煩了嗎?NO!!!還有更麻煩的。

比如,我想在我定義的結構體裡使用自身的型別,要怎麼定義呢?

因為在C++裡,變數定義必須按照先宣告後使用的【絕對順序】,那麼,在定義時就使用自身型別,編譯器會提示錯誤。

如果想要讓編譯器通過,就必須在使用前,先給自身型別定義個別名,這樣就可以在定義時使用自身型別了。

呃。。。好像有點繞,我們直接看程式碼。

typedef struct kibaSelf *kibaSelfCopy;
struct kibaSelf
{
	int id;
	kibaSelfCopy myself;
};
int main()
{
	kibaSelf ks;
	ks.id = 518;
	kibaSelf myself;
	myself.id = 520;
	ks.myself = &myself;
	cout << ks.id << endl;
	cout << ks.myself->id << endl; 
} 

如上述程式碼所示,我們在定義結構體之前,先給它定義了個別名。

那麼,變數定義不是必須按照先宣告後使用的【絕對順序】嗎?為什麼這裡,又在定義前,可以定義別名了呢?這不是矛盾了嗎?

不知道,反正,C++就是這樣。。。就這麼屌。。。

指標

指標在C++中,就是在變數前加個*號,下面我們定義個指標來看看。

int i = 518;
int *ipointer = &i;
int* ipointer2 = &i;
cout << "*ipointer" << *ipointer << "===ipointer" << ipointer << endl;

如上述程式碼所示,我們定義了倆指標,int *ipointer 和int* ipointer2。可以看到,我這倆指標的*一個靠近變數一個靠近宣告符int,但兩種寫法都正確,編譯器可以編譯通過。

呃。。。就是這麼屌,學起來就是這麼優雅。。。

接著,我們用取地址符號&,取出i變數的地址給指標,然後指標變數*ipointer中ipointer儲存的是i的地址,而*ipointer儲存的是518,如下圖:

那麼,我們明明是把i的地址給了變數*ipointer,為什麼*ipointer儲存的是518呢?

因為。。。就是這麼屌。。。

哈哈,不開玩笑了,我們先看這樣一段程式碼,就可以理解了。

int i = 518;
int *ipointer;
int* ipointer2;
ipointer = &i;
ipointer2 = &i;
cout << "*ipointer" << *ipointer << "===ipointer" << ipointer << endl;

如上述程式碼所示,我把宣告和賦值給分開了,這樣就形象和清晰了。

我們把i的地址給了指標(*ipointer)中的ipointer,所以ipointer存的就是i的地址,而*ipointer則是根據ipointer所儲存的地址找到對應的值。

那麼,int *ipointer = &i;這樣賦值是什麼鬼?這應該報錯啊,應該不允許把i的地址給*ipointer啊。

呃。。。還是那句話,就是這麼屌。。。

->

->這個符號大概是指標專用的。下面我們來看這樣一段程式碼來了解->。

kiba kinstance;
kiba *kpointer;
kpointer = &kinstance;
(*kpointer).id = 518;
kpointer->id = 518;
//*kpointer->id = 518;

首先我們定義一個kiba結構體的例項,定義定義一個kiba結構體的指標,並把kinstance的地址給該指標。

此時,如果我想為結構體kiba中的欄位id賦值,就需要這樣寫【(*kpointer).id = 518】。

我必須把*kpointer擴起來,才能點出它對應的欄位id,如果不擴起來編譯器會報錯。

這樣很麻煩,沒錯,按說,微軟應該在編譯器中解決這個問題,讓他*kpointer不用被擴起來就可以使用。

但很顯然,微軟沒這樣解決,編譯器給的答案是,我們省略寫*號,然後直接用儲存地址的kpointer來呼叫欄位,但呼叫欄位時,就不能再用點(.)了,而是改用->。

呃。。。解決的就是這麼優雅。。。沒毛病。。。

引用型別

我們先定義接受引用型別的函式,如下。

int usage(int &i) {
	i = 518;
	return i;
}
int main()
{
	int u = 100;
	usage(u);
	cout << "u" << u << endl; 
} 

如上述程式碼所示,u經過函式usage後,他的值被改變了。

如果我們刪除usage函式中變數i前面的&,那麼u的值就不會改變。

好了,那麼&符號不是我們剛才講的取地址嗎?怎麼到這裡又變成了引用符了呢?

還是那句話。。。就是這麼屌。。。

呃。。。還有更屌的。。。我們來引用個指標。

void usagePointer(kiba *&k, kiba &kiunew) {
	k = &kiunew;
	k->id = 518;
}
int main()
{
	kiba kiunew;
	kiba kiu;
	kiba *kiupointer;
	kiupointer = &kiu; 
	kiupointer->id = 100;
	kiunew.id = 101;
	cout << "kiupointer->id" << kiupointer->id << "===kiupointer" << kiupointer << endl;
	usagePointer(kiupointer, kiunew);
	cout << "kiupointer->id" << kiupointer->id << "===kiupointer" << kiupointer << endl;
}

如上述程式碼所示,我定義了兩個結構體變數kiunew,kiu,和一個指標*kiupointer,然後我把kiu的地址賦值給指標。

接著我把指標和kiunew一起傳送給函式usagePointer,在函式裡,我把指標的地址改成了kiunew的地址。

執行結果如下圖。

可以看到,指標地址已經改變了。

如果我刪除掉函式usagePointer中的【引用符&】(某些情況下也叫取地址符)。我們將得到如下結果。

我們從圖中發現,不僅地址沒改變,賦值也失敗了。

也就是說,如果我們不使用【引用符&】來傳遞指標,那麼指標就是隻讀的,無法修改。

另外,大家應該也注意到了,指標的引用傳遞時,【引用符&】是在*和變數之間的,如果*&k。而普通變數的引用型別傳遞時,【引用符&】是在變數前的,如&i。

呃。。。指標,就是這麼屌。。。

const

const是定義常量的,這裡就不多說了。下面說一下,在函式中使用const符號。。。沒錯,你沒看錯,就是在函式中使用const符號。

int constusage(const int i) { 
	return i;
}

如程式碼所示,我們在入參int i前面加上了const修飾,然後,我們得到這樣的效果。

i在函式constusage,無法被修改,一但賦值就報錯。

呃。。。基於C#,估計肯定不好理解這個const存在的意義了,因為如果不想改,就別改啊,標只讀這麼費勁幹什麼。。。

不過我們換位思考一下,C++中這麼多記憶體控制,確實很亂,有些時候加上const修飾,標記只讀,還是很有必要的。

PCH

在專案建立的時候,系統為我們建立了一個pch.h標頭檔案,並且,每個.cpp檔案都引用了這個標頭檔案【#include "pch.h"】。

開啟.pch發現,裡面是空程式碼,在等待我們填寫。

既然.pch沒有被使用,那麼將【#include "pch.h"】刪掉來簡化程式碼,刪除後,發現編譯器報錯了。

調查後發現,原來專案在建立的時候,為我們設定了一個屬性,如下圖。

如圖,系統我們建立的pch.h標頭檔案,被設定成了預編輯標頭檔案。

下面,我修改【預編譯頭】屬性,修改為不使用預編譯頭,然後我們再刪除【#include "pch.h"】引用,編譯器就不會報錯了。

那麼,為什麼建立檔案時,會給我們設定一個預編譯頭呢?微軟這麼做肯定是有目的。

我們通過名字,字面推測一下。

pch.h是預編譯頭,那麼它的對應英文,大概就是Precompile Header。即然叫做預編譯,那應該在正式編譯前,執行的編譯。

也就是,編譯時,檔案被分批編譯了,pch.h預編譯頭會被提前編譯,我們可以推斷,預編譯頭是用於提高編譯速度的。

C++是一個同時程式導向和麵向物件的程式語言,所以,C++裡也有類和物件的存在。

類的基礎定義就不多說了,都一樣。

不過在C++中,因為,引用困難的原因(上面已經描述了,只能引用其他.cpp檔案對應的標頭檔案,並且,.cpp實現的變數,還得在標頭檔案裡外部宣告一下),所以類的定義寫法也發生了改變。

C++中建立類,需要在標頭檔案中宣告函式,然後在.cpp檔案中,做函式實現。

但是這樣做,明顯是跨檔案宣告類了,但C++中又沒有類似partial關鍵字讓倆個檔案合併編譯,那麼怎麼辦呢?

微軟給出的解決方案是,在.Cpp檔案中提供一個類外部編寫函式的方法。

下面,我們簡單的建立一個類,在標頭檔案中宣告一些函式和一些外部變數,然後在.cpp檔案中實現這些函式和變數。

右鍵標頭檔案資料夾—>新增——>類,在類名處輸入classtest,如下圖。

然後我們會發現,系統為我們建立了倆檔案,一個.h標頭檔案和一個.cpp檔案,如下圖。

然後編寫程式碼如下:

classtest.h標頭檔案:

class classtest
{
public:
	int id;
	string name;
	classtest();
	~classtest();
	int excute(int id);
private:
	int number;
	int dosomething();
};

calsstest.cpp檔案:

#include "pch.h"
#include "classtest.h" 
classtest::classtest()
{
} 
classtest::~classtest()
{
}
int classtest::excute(int id)
{
	this->id = id;
	return this->id;
}
int classtest::dosomething()
{
	this->number = 520;
	return this->number;
}

呼叫測試程式碼如下:

#include "pch.h" 
#include "classtest.h" 
int main()
{
	classtest ct;
	ct.excute(518);
	classtest *ctPointer = new classtest;
	ctPointer->excute(520);
	cout << "ct.id" << ct.id << "===ctPointer" << ctPointer->id << endl;
}

結語

通過重溫,我得出如下結論。

一,C++並不是一門優雅的開發語言,他自身存在非常多的設定矛盾和混淆內容,因此,C++的學習和應用的難度遠大於C# ;其難學的原因是C++本身缺陷導致,而不是C++多麼難學。

二,指標是C++開發學習設計模式的攔路虎,用C++學習那傳說中的26種設計模式,還勉強可以;但,如果想學習MVVM,AOP等等這些的設計模式的話,C++的指標會讓C++開發付出更多的程式碼量,因此多數C++開發對設計模式理解水平很低也是可以理解的了。

三,通過學習和反思,發現,我曾經接觸的那些愛裝X的C++開發,確實是坐井觀天、夜郎自大,他們的編寫程式碼的思維邏輯,確確實實是被C++的缺陷給限制住了。

----------------------------------------------------------------------------------------------------

到此,我重溫C++的心路歷程就結束了。

程式碼已經傳到Github上了,歡迎大家下載。

Github地址:https://github.com/kiba518/C-ConsoleTest

----------------------------------------------------------------------------------------------------

注:此文章為原創,歡迎轉載,請在文章頁面明顯位置給出此文連結!
若您覺得這篇文章還不錯,請點選下方的推薦】,非常感謝!

 

相關文章