在前一篇,我們提供了一個方向性的指南,但是學什么,怎么學卻沒有詳細展開,本篇將在前文的基礎上,著重介紹下怎樣學習C++的型別系統,
寫在前面
在進入型別系統之前,我們應該先達成一項共識——盡可能使用C++的現代語法,眾所周知,出于兼容性的考慮,C++中很多語法都是合法的,但是隨著新版本的推出,有些語法可能是不推薦或者是需要避免使用的,所以本篇也盡可能采用推薦的語法形式(基于C++11或以上版本),這也是現代C++標題的含義,
采用現代語法有兩點好處,其一,現代語法可以編譯出更快更健壯的代碼,編譯器也是隨著語言的發展而發展的,現代語法可以在一定程度上幫助編譯器做更好的優化,其二,現代語法通常更簡潔,更直觀,也更統一,有助于增強可讀性和可維護性,
明確了這點后,讓我們一起踏入現代C++的大門吧,
型別系統
程式是一種計算工具,根據輸入,和預定義的計算方法,產生計算結果,當程式運行起來后,這三者都需要在記憶體中表示成合適的值才能讓程式正常作業,負責解釋的這套工具就是型別系統,數字,字串,鍵盤滑鼠事件等都是資料,而且在記憶體中實際存在的形式也是一樣的,但是按我們人類的眼光來看的話,對它們的處理是不一樣的,數字能進行加減乘除等算術運算,但是對字串進行算術運算就沒有意義,而鍵盤滑鼠的值通常只是讀取,不進行計算的,正是由于這些差異,編程語言的第一個任務就是需要定義一套型別系統,告訴計算機怎樣處理記憶體中的資料,
為了讓編程語言盡可能簡單,編程語言一般把型別系統分為兩步實作,一部分是編譯器,另一部分是型別,編譯器那部分負責將開發者的代碼解釋成合適的形式,以便可以高效,準確在記憶體中表示,型別部分則定義一些編譯器能處理的型別,以便開發者可以找到合適的資料來完成輸入輸出的表示和計算方法的描述,這兩者相輔相成,相互成就,
型別作為型別系統的重要表現形式,在編程語言中的重要性也就不言而喻了,如果把寫程式看成是搭積木的話,那么程式的積木就是型別系統,型別系統是開發者能操作的最小單位,它限制了開發者的操作規則,但是提供了無限的可能,C++有著比積木更靈活的型別系統,
型別
型別是編程語言的最小單位,任何一句代碼都是一種記憶體使用形式,
而談到C++的型別也就不得不談到它的三種型別表現形式——普通型別,指標,參考,它們是三種不同的記憶體使用和解釋形式,也是C++的最基礎的形式,和大部分編程語言不同,C++對內置型別沒有做特權處理,只要開發者愿意,所有的型別都可以有一致的語法形式(通過運算子多載),所以下面關于型別的舉例適合所有的型別,
普通型別就是沒有修飾的型別,如int
,long
,double
等,它們是按值傳遞的,也就是賦值和函式傳參是拷貝一份值,對拷貝后的值進行操作,不會再影響到老值,
int a=1; //老值,存在地址1
int b=a; //新值,存在地址2
b=2; //改變新值,改變地址2
//此時a還是1,b變成了2
那假如我們需要修改老值呢,有兩種途徑,一種是指標,另一種則是參考,
指標是C/C++里面的魔法,一切皆可指標,指標包含兩個方面,一方面它是指一塊記憶體,另一方面它可以指允許對這塊記憶體進行的操作,指標的值是一塊記憶體地址,操作指標,操作的是它指向的那塊地址,
int a=1; //老值,存在地址1
int* b=&a; //&代表取地址,從右往左讀,取a的地址——地址1,存在地址2
*b=2; //*是解參考,意思是把存在地址2(b)的值取出來,并把那個地址(地址1)的值改成2
//此時a,*b變成了2
參考則是指標的改進版,參考能避免無效參考,不過參考不能重設,比指標缺少一定的靈活性,
int a=1; //老值,存在地址1
int& b=a; //&出現在變數宣告的位置,代表該變數是參考變數,參考變數必須在宣告時初始化
b=2; //可以像普通變數一樣操作參考變數,同時,對它的操作也會反應到原始物件上
//此時a,b變成了2
變數定義
型別僅僅是一種語法定義,而要真正使用這種定義,我們需要用型別來定義變數,即變數定義,
C++變數定義是以下形式:
type name[{initial_value}]
這里的關鍵在于type
,type
是型別和限定符的組合,看下面的例子:
int a; //普通整型
int* b; //型別是int和*的組合,組成了整型指標
const int* c; //從右往左讀,*是指標,const int是常量整型,組成了指向常量整型的指標型別
int *const d; //也是從右往左讀,const是常量,后面是指標,說明這個指標是常量指標,指向最左邊的int,組成常量指標指向整型
int& e=a; //型別是int和&的組合,組成了整型參考
constexpr int f=a+e; //constexpr代表這個變數需要在編譯期求值,并且不再可變,
以上,基本就是變數定義的所有形式了,型別確定了變數的基本屬性,而限定符限定了變數的使用范圍,
定義變數也是按照這個步驟進行,首先確定我們需要什么型別的變數,其次再進一步確定是否需要對這個變數添加限定,很多時候是需要的,可以按以下步驟來確定添加什么樣的限定符:
- 是個大物件,可以考慮把變數宣告成參考型別,通常參考型別是比指標型別更優的選擇,
- 大物件可能需要被重置,可以考慮宣告為指標,
- 只想要個常量,添加
constexpr
, - 只想讀這個變數,添加
const
,
變數初始化
變數定義往往伴隨著初始化,這對于區域變數來說很重要,因為區域變數的初值是不確定的,在沒有對變數進行有效初始化前就使用變數,會導致不可控的問題,所以嚴格來說,前面的變數定義是不完全正確的,
C++11推出了全新的,統一的初始化方式,即在變數名后面跟著大括號,大括號里包著初始化的值,這種方式可以用在任何變數上,稱之為統一初始化,如:
int a{9527}; //普通型別
string b={"abc"}; //另一種寫法,等價但是不推薦
Student c{"張三","20220226",18}; //大括號中是建構式引數
當然,除了用型別名來定義變數外,還可以將定義和初始化合二為一,變成下面這種最簡潔的形式:
auto a={1}; //推導為整型
auto b=string{"abc"};
auto c=Student{"張三","20220226",18}
這里auto
是讓編譯器自己確定型別的意思,上面這種寫法是完全利用了C++的型別推導,這也是好多現代語言推薦的形式,不過需要注意的是,使用型別推導后,=
就不能省略了,
有了初始化的變數后,我們就可以用它們完成各種計算任務了,C++為開發者實作了很多內置的計算支持,如數字的加減乘除運算,陣列的索引,指標的操作等,還提供了分支if
,switch
,回圈while
,for
等陳述句,為我們提供了更靈活的操作,
函式
變數是編程語言中的最小單位,隨著業務的復雜度增加,有些時候中間計算會分散業務的邏輯,增加復雜度,為了更好地組織代碼,型別系統增加了 函式來解決這個問題,
函式也是型別,是一種復合型別,它的型別由引數串列,回傳值組合而成,也就是說兩個函式,假如引數串列和回傳值一樣,那么它們從編譯器的角度來看是等價的,當然光有它們還不夠,不然怎么能出現兩個引數串列和回傳值一樣的函式呢,一個完整的函式還需要有個函式體和函式名,所以函式一般是下面這種形式:
//常規函式形式
[constexpr] 回傳值 函式名(引數串列)[noexcept]{
函式體
}
//回傳值后置形式
auto 函式名(引數串列)->回傳值
當一個函式沒有函式體的時候,我們通常稱之為函式宣告,加上函式體就是一個函式定義,
void f(int); //函式宣告
void fun(int value){ //函式定義,因為有大括號代表的函式體
}
以上就是函式的基本框架,接下來我們分別來看一看組成它的各部分,
先說最簡單的函式名,它其實是函式這種型別的一個變數,這個變數的值表示從記憶體地址的某個位置開始的一段代碼塊,前面也說過之所以能出現兩個引數串列和回傳值都相同的函式,但是編譯器能識別,其主要功勞就在函式名上,所以函式名也和變數名一樣,是一種識別符號,那假如反過來,函式名相同,但是引數串列或者回傳值不同呢,這種情況有個專有名詞——函式多載,基于函式是復合型別的認識,它們中只要其中一種不同就算多載,另外,在C++11,還有一種沒有名字的函式,稱為lambda運算式,lambda運算式是一種類似于直接量的函式值,就像13,'c'這種,是一種不提前定義函式,直接在呼叫處定義并使用的函式形式,
引數串列是前面型別定義的升級款,所有前面說的關于變數定義的都適用于它,三種形式的變數定義,多個變數,變數初始化等,不過,它們都有了新名詞,引數串列的變數稱為形式引數,初始化稱為默認引數,同樣形參在實際使用的時候需要初始化,不過初始化來自呼叫方,形式引數沒有默認值就需要在呼叫的時候提供引數,有默認值的可以省略,
int plus(int a,int b=1){ //b是一個默認引數
return a+b;
}
int main(void){
int c=plus(1); //沒有提供b的值,所以b初始化為1,結果是2
int d=plus(2,2); //a,b都初始化為2,結果是4
//int f=plus(1,2,3); //plus只有兩個形參,也就是兩個變數,沒法保存三個值,所以編譯錯誤
return 0;
}
和引數串列一樣,回傳值也是一個變數,這個變數會通過return
陳述句回傳給呼叫者,所以從記憶體操作來看,它是一個賦值操作,
std::string msg(){
std::string input;
std::cin>>input;
return input;
}
int main(void){
auto a=msg();
std::string b=msg();//msg回傳的input復制到了b中
return 0;
}
遺憾的是C++只支持單回傳值,也就是一個函式呼叫最多只能回傳一個值,假如有多個值就只能以形參形式回傳了,這種方式對于函式呼叫就不是很友好,所以C++提出了新的解決思路,
類
隨著業務的復雜度再次增加,函式形參個數可能會增加,或者可能需要回傳多個值,然后在多個不同的函式間傳遞,這樣會導致資料容易錯亂,并且增加使用者的學習成本,
為了解決這些問題,工程師們提出了面向物件——多個資料打包的技術,表現在語言層面上,就是用類把一組操作和完成這組操作需要的資料打包在一起,資料作為類的屬性,操作作為類的方法,使用者通過方法操作內部資料,資料不再需要使用者自己傳遞,管理,這對于開發者無疑是大大簡化了操作,我們稱之為面向物件編程,而在函式間傳遞資料的方式稱為面向程序編程,這兩種方式底層邏輯其實是一致的,該傳遞的引數和函式呼叫一樣都不少,但是面向物件的區別是這些繁瑣、容易出錯的作業交給編譯器來做,開發者只需要按照面向物件的規則做好設計作業就好了,剩下的交給編譯器,至此,我們的型別系統又向上提升了一級,類不僅是多個型別的聚合體,還是多個函式的聚合體,是比函式更高級的抽象,
可以看下面面向程序編程和面向物件編程的代碼對比
struct Computer{
bool booted;
friend std::ostream& operator<<(std::ostream& os,const Computer & c){
os<<"Computing";
return os;
}
};
void boot(Computer& c){
c.booted=true;
std::cout<<"Booting...";
}
void compute(const Computer& c){
if(c.booted){
std::cout<<"Compute with "<<c;
}
}
void shutdown(Computer& c){
c.booted=false;
std::cout<<"Shutdown...";
}
int main(void){
auto c=Computer();
boot(c);
compute(c);
shutdown(c);
return 0;
}
面向程序最主要的表現就是,開發者需要在函式間傳遞資料,并維護資料狀態,上面例子中的資料是c
,
struct Computer{
bool booted;
friend std::ostream& operator<<(std::ostream& os,const Computer & c){
os<<"Computing";
return os;
}
void boot(){
booted=true;
std::cout<<"Booting...";
}
void compute(){
if(booted){
std::cout<<"Compute with "<<this;
}
}
void shutdown(){
booted=false;
std::cout<<"Shutdown...";
}
};
int main(void){
auto c=Computer();
c.boot();
c.compute();
c.shutdown();
return 0;
}
可以看出面向物件的代碼最主要的變化是,方法的引數變少了,但是可以在方法里面直接訪問到類定義的資料,另一個變化發生在呼叫端,呼叫端是用資料呼叫方法,而不是往方法里面傳遞資料,這也是面向物件的本質——以資料為中心,
當然,類的封裝功能只是類功能的一小部分,后面我們會涉及到更多的類知識,作為初學者,我們了解到這一步就能讀懂大部分代碼了,
總結
型別系統是一門語言的基本構成部分,它支撐著整個系統的高級功能,很多高級特性都是在型別系統的基礎上演化而來的,所以學習語言的型別系統有個從低到高,又從高到低的程序,從最基礎的型別開始,學習如何從低級型別構筑出高級型別,然后站在高級型別的高度上,審視高級型別是怎樣由低級型別構筑的,這一上一下,一高一低基本上就能把語言的大部分特性了解清楚了,
低級型別更偏向于讓編譯器更好地作業,高級型別偏向于讓開發者更好地作業,C++從普通型別,函式,類提供了各個層級的支持,讓開發者有更多自由的選擇,當然也就增加了開發者的學習難度,但是開發者并不是都需要所有選擇的,所以我覺得正確的學習應該是以專案規模為指導的,一些專案,完全用不到面向物件,就可以把精力放在打造好用的函式集上,而有的專案,面向物件是很好的選擇,就需要在類上花費時間,回到開頭的積木例子,選用什么積木完全看我們想搭什么模型,要是沒有合適的積木,我們可以自己創造,這就是C++的迷人之處,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555073.html
標籤:其他
上一篇:【解決一個小問題】golang 的 `-race`選項導致 unsafe代碼 panic
下一篇:返回列表