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

不知道爲何,彷佛不少人理解跑偏了,在這裏我要說明一下。ios

首先,我並無對C++語言有偏見,我只是單純的在學習時,在理解時,對C++語言進行一些吐槽,我相信,不少學習C++的人,也會有相似的吐槽。c++

其次,我吐槽了我之前的一些C++同事,這與其餘C++開發無關,若是你感同身受,那說明你要檢討一下了。git

前言程序員

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

做爲一個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++開發對設計模式理解水平很低也是能夠理解的了。事實上,C++開發是很難出高級程序員,大部分都被困在中級程序員這個水平線上了。

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

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

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

代碼已經傳到Github上了,歡迎你們下載。

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

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

注:此文章爲原創,歡迎轉載,請在文章頁面明顯位置給出此文連接!
若您以爲這篇文章還不錯,請點擊下方的推薦】,很是感謝!

相關文章
相關標籤/搜索