不知道爲何,彷佛不少人理解跑偏了,在這裏我要說明一下。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
----------------------------------------------------------------------------------------------------
注:此文章爲原創,歡迎轉載,請在文章頁面明顯位置給出此文連接!
若您以爲這篇文章還不錯,請點擊下方的【推薦】,很是感謝!