一個C#開發者重溫C++的心路歷程
前言
這是一篇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
----------------------------------------------------------------------------------------------------
注:此文章為原創,歡迎轉載,請在文章頁面明顯位置給出此文連結!
若您覺得這篇文章還不錯,請點選下方的【推薦】,非常感謝!