文章目錄
- 前言
- 1.C++關鍵字
- 2.命名空間
- 3.C++的輸入輸出
- 4.缺省參數
- 4.1 全缺省
- 4.2 半缺省
- 5.函數重載
- 6. 引用
- 6.1 引用的特性
- 6.2 引用的使用場景
- 6.3 引用和指針
- 7.內聯函數
- 7.1 特性
- 8.auto關鍵字
- 8.1 注意事項
- 9. 基于范圍的for循環
- 9.1 使用條件
- 10.指針控制nullptr
- 10.1 注意事項
- 11.總結
前言
??C語言是結構化和模塊化的語言,適合處理較小規模的程序。對于復雜的問題,規模較大的程序,需要高度的抽象和建模時,C語言則不合適。為了解決軟件危機, 20世紀80年代, 計算機界提出了OOP(object oriented programming:面向對象)思想,支持面向對象的程序設計語言應運而生。
??1982年,Bjarne Stroustrup博士在C語言的基礎上引入并擴充了面向對象的概念,發明了一種新的程序語言。為了表達該語言與C語言的淵源關系,命名為C++。因此:C++是基于C語言而產生的,它既可以進行C語言的過程化程序設計,又可以進行以抽象數據類型為特點的基于對象的程序設計,還可以進行面向對象的程序設計。
階段 | 內容 |
---|---|
C with classes | 類及派生類、公有和私有成員、類的構造和析構、友元、內聯函數、賦值運算符重載等 |
C++1.0 | 添加虛函數概念,函數和運算符重載,引用、常量等 |
C++2.0 | 更加完善支持面向對象,新增保護成員、多重繼承、對象的初始化、抽象類、靜態成員以及const成員函數 |
C++3.0 | 進一步完善,引入模板,解決多重繼承產生的二義性問題和相應構造和析構的處理 |
C++98 | C++標準第一個版本,絕大多數編譯器都支持,得到了國際標準化組織(ISO)和美國標準化協會認可,以模板方式重寫C++標準庫,引入了STL(標準模板庫) |
C++03 | C++標準第二個版本,語言特性無大改變,主要:修訂錯誤、減少多異性 |
C++05 | C++標準委員會發布了一份計數報告(Technical Report,TR1),正式更名C++0x,即:計劃在本世紀第一個10年的某個時間發布 |
C++11 | 增加了許多特性,使得C++更像一種新語言,比如:正則表達式、基于范圍for循環、auto關鍵字、新容器、列表初始化、標準線程庫等 |
C++14 | 對C++11的擴展,主要是修復C++11中漏洞以及改進,比如:泛型的lambda表達式,auto的返回值類型推導,二進制字面常量等 |
C++17 | 在C++11上做了一些小幅改進,增加了19個新特性,比如:static_assert()的文本信息可選,Fold表達式用于可變的模板,if和switch語句中的初始化器等 |
C++20 | 自C++11以來最大的發行版,引入了許多新的特性,比如:模塊(Modules)、協程(Coroutines)、范圍(Ranges)、概念(Constraints)等重大特性,還有對已有特性的更新:比如Lambda支持模板、范圍for支持初始化等 |
C++23 | 制定ing |
1.C++關鍵字
??這是C++的所有關鍵字,每一個都有特殊含義,此處一個一個講麻煩且不易理解,因此等以后遇到了再具體講解。
2.命名空間
??在C/C++中,變量、函數和后面要學到的類都是大量存在的,這些變量、函數和類的名稱將都存在于全局作用域中,可能會導致很多沖突。使用命名空間的目的是對標識符的名稱進行本地化,以避免命名沖突或名字污染,namespace關鍵字的出現就是針對這種問題的。
??舉個例子,比如在<stdlib.h>中有一個rand()函數,是用來產生隨機數的一個函數。
??此時我們可以看到在還沒有包<stdlib.h>頭文件時是可以正常運行的,那么我們再看看包頭文件之后會發生什么。
??我們發現竟然編譯失敗了,這是因為在編譯時會將頭文件內的內容展開,就會出現rand()函數與我們定義的全局變量 rand 一樣的名字,因此就會產生重定義,因此 namespace 命名空間就隨之產生了,它的作用就是為了避免這種情況的發生。
??命名空間就是用 namespace 關鍵字加上任意的名字和花括號即可。但是大家可以觀察到在輸出的那一行我寫的是WY : : rand,這個的意思就是輸出 WY 命名空間中的 rand,而不是使用頭文件中的 rand。
??命名空間就像一堵無形的墻,將同名的變量分隔到了不同的空間,你需要哪個空間的變量,就去哪個空間去尋找。但是一個命名空間里又有同名的變量存在呢?C++開創者也早已想到了這個問題,那就是在命名空間中還可以再開辟命名空間,俗稱套娃。
??命名空間可以定義變量、函數以及類型。但是需要注意的是在定義結構體類型時,在訪問時是要在 struct 后面寫的。同時同一個工程中是允許出現同名的命名空間的,在編譯時編譯器會自動將其合并。
int rand = 0;//全局變量
namespace WY
{int rand = 1;int Add(int a, int b){return a + b;}struct Node{int data;int* next;};
}
int main()
{//域作用限定符printf("%d", rand);printf("%d", WY::rand);printf("%d", WY::Add(1,2));struct WY::Node node;return 0;
}
??前面都還是用C語音來模擬一些環境,那么C++本身是什么樣的呢?C++有自己專屬的命名空間和頭文件。
//C++標準庫的命名空間,將標準庫的定義與實現都放入了這個命名空間中
#include<iostream>
using namespace std;
int main()
{cout << "Wang You" << endl;
}
??但是這樣的寫法就是將命名空間中的所有內容全部展開了,是非常容易造成重定義問題的,比如在你和其他人共同寫一個項目時,用到了同一個變量名字,結果在最后一起運行時都將自己的命名空間全部展開,那么豈不是還會出現重定義的情況嗎?因此這種寫法在與他人共同合作時不可取,但是在自己日常練習中還是可以的,畢竟變量名都是由自己控制的,可以避免這種情況。
??同時展開也全部展開和部分展開,比如 Add 函數需要經常使用,就可以進行部分展開,這樣就不用在每次使用前都加域作用限定符( : : )了。
//全部展開
using namespace WY;//部分展開
using WY::Add;
3.C++的輸入輸出
??在上面其實也寫過一次,請看:
#include<iostream>
using namespace std;
int main()
{int a;cin >> a;cout << "Wang You" << endl;return 0;
}
cout是輸出時使用的,與C語言中的 printf 作用一致,還有一個是 cin,與 scanf 作用一致。
與C語言相比需要注意的是:
- 在使用cout標準輸出對象(控制臺)和cin標準輸入對象(鍵盤)時,必須包含< iostream >頭文件以及按命名空間使用方法使用std。
- cout和cin是全局的流對象,endl是特殊的C++符號,表示換行輸出,他們都包含在包含< iostream >頭文件中。
- << 是流插入運算符,>> 是流提取運算符。
- 使用C++輸入輸出更方便,不需要像printf/scanf輸入輸出時那樣,需要手動控制格式。C++的輸入輸出可以自動識別變量類型。
- 實際上cout和cin分別是ostream和istream類型的對象,>>和<<也涉及運算符重載等知識,這些知識后續將會進行講解,所以這里只是簡單學習他們的使用。
using namespace std展開,標準庫就全部暴露出來了,如果我們定義跟庫重名的類型/對象/函數,就會存在沖突問題。該問題在日常練習中很少出現,但是項目開發中代碼較多、規模大,就很容易出現。所以建議在項目開發中使用,像std::cout這樣使用時指定命名空間 + using std::cout展開常用的庫對象/類型等方式。
4.缺省參數
??缺省參數是聲明或定義函數時為函數的參數指定一個缺省值。在調用該函數時,如果沒有指定實參則采用該形參的缺省值,否則使用指定的實參。
??一般在調用一個函數時,我們都會對它進行傳參,缺省參數的意義在于,當你沒有給函數傳參時,它會自動默認傳入你設置的缺省參數(int a = 0)。
4.1 全缺省
void fun(int a = 10, int b = 20, int c = 30)
{cout << a << " ";cout << b << " ";cout << c << " ";cout << endl;
}
??由此我們也可以看出缺省參數必須從右往左給,因此我們給的參數是從左往右的,左邊的數據可以我們顯示傳參,右邊的可以使用缺省參數。
4.2 半缺省
??那我們就再來看看半缺省,顧名思義就是只有一部分有缺省值。
void fun(int a,int b,int c = 30)
{cout << a << endl;cout << b << endl;cout << c << endl;
}
注意:
- 半缺省參數必須從右往左依次來給出,不能間隔著給
- 缺省參數不能在函數聲明和定義中同時出現
- 缺省值必須是常量或者全局變量
- C語言不支持(編譯器不支持)
??不同聲明和定義中同時出現是因為如果定義中給的默認參數與聲明中給的默認參數不同的話,編譯器是無法分別的,因此統一都是在定義是再給默認參數(缺省參數)。
5.函數重載
??函數重載是函數的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參列表(參數個數或類型或類型順序)不同,常用來處理實現功能類似數據類型不同的問題。
??系統會根據它們的參數類型自動匹配最相符的函數進行調用,即使函數名相同也是可以的。
但是必須要滿足形參列表(參數個數或類型或類型順序)不同,這三個條件中的一個才可以。
那么編譯器是如何區分的呢?
??其實在編輯器內部調用這個函數時,是通過它的函數名加地址去尋找它的,每一個函數在鏈接時在內部的函數名,編譯器都會在你書寫的函數名(fun)的基礎上再根據它的參數來進行添加一些符號來修飾它。比如在VS2019中,在鏈接過程中這個兩個函數的函數名就是如圖這樣的,在后面一個是 HN,一個是NH。因此編譯器才會準確的區分它們。
??而在C語言中,對于內部的函數名修飾是一樣,所以編譯器是無法區別的。 通過這里就理解了C語言為什么沒辦法支持重載了,因為同名函數沒辦法區分。而C++是通過函數修飾規則來區分,只要參數不同,修飾出來的名字就不一樣,就支持了重載。
如果兩個函數函數名和參數是一樣的,返回值不同是不構成重載的,因為調用時編譯器沒辦法區分。
6. 引用
??引用不是新定義一個變量,而是給已存在的變量取了一個別名(也就是外號),編譯器不會為引用變量開辟內存空間,它和它引用的變量共用同一塊內存空間。
??就比如一塊空間名字是 a,你又給它起了個外號叫 b,雖然名字不一樣,但是是同一塊空間。
??看似兩個名字,實則是同一塊空間,一榮俱榮一損俱損,你是我,我是你的關系。需要注意的是引用類型必須和引用實體是同種類型的。
6.1 引用的特性
- 引用在定義時必須初始化
- 一個變量可以有多個引用
- 引用一旦引用一個實體,再不能引用其他實體
int main()
{int a = 10;int& b;//正確寫法:int& b = a;return 0;
}
??這樣寫是不行的,因為你是在給已存在變量取別名,不初始化是無法作為別人的外號存在的。
int main()
{int a = 10;int& ra = a;int& raa = a;return 0;
}
??這樣寫是可以的,一個變量可以有很多個外號。但是一個外號只能給一個人用,誰都能用豈不是就亂套了,所以引用一旦引用一個實體,再不能引用其他實體。
??對于 const 修飾的變量意味著只能讀不能寫,因此在為 const 變量取別名時也要加 const,不能出現本來無法修改,你加個外號就能修改的情況吧。
void test()
{const int a = 10;int& ra = a; // 該語句編譯時會出錯,a為常量//正確寫法:const int& ra = a;const int& ra = a;int& b = 10; // 該語句編譯時會出錯,b為常量//正確寫法:const int& b = 10;const int& b = 10;double d = 12.34;int& rd = d; // 該語句編譯時會出錯,類型不同//正確寫法:const int& rd = d;
}
??而對于最后一個類型不一樣的,為什么加了 const 就可以了呢?這是因為在進行隱式類型轉化時,會先生成一個臨時的 int 常量,先將 d 的值由 double 類型轉為 int 類型賦給臨時變量,然后再為臨時變量取的別名。因為是常量無法修改,所以在取別名時也需要加 const。
6.2 引用的使用場景
??作為函數的參數,這樣不需要指針就可以交換兩個變量的值。
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}
??作返回值。引用返回效率會更快一點,因為直接返回的是變量的別名。在正常的函數中,因此函數在調用完就被銷毀了,因此它是將返回值先給了一個臨時變量,由臨時變量返回給主函數。因此需要注意的是,引用返回時返回的必須是靜態變量或者是堆上的變量。
int& Count()
{static int n = 0;n++;// ...return n;
}
以值作為參數或者返回值類型,在傳參和返回期間,函數不會直接傳遞實參或者將變量本身直接返回,而是傳遞實參或者返回變量的一份臨時的拷貝,因此用值作為參數或者返回值類型,效率是非常低下的,尤其是當參數或者返回值類型非常大時,效率就更低。
6.3 引用和指針
??指針和引用使用起來不一樣,但在底層實現是其實是一樣的,我們來看這樣一段代碼。
#include<iostream>
using namespace std;
int main()
{int a = 10;int* pa = &a;int& ra = a;++(*pa);++ra;return 0;
}
??雖然對于匯編不太理解,但是不妨礙我們可以看出對于指針運算和引用運算,編譯器在底層用匯編實現是一致的,因此我們就可以知道引用實際上也是通過指針來實現的。
- 引用概念上定義一個變量的別名,指針存儲一個變量地址。
- 引用在定義時必須初始化,指針沒有要求
- 引用在初始化時引用一個實體后,就不能再引用其他實體,而指針可以在任何時候指向任何
一個同類型實體 - 沒有NULL引用,但有NULL指針
- 在sizeof中含義不同:引用結果為引用類型的大小,但指針始終是地址空間所占字節個數(32
位平臺下占4個字節) - 引用自加即引用的實體增加1,指針自加即指針向后偏移一個類型的大小
- 有多級指針,但是沒有多級引用
- 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理
- 引用比指針使用起來相對更安全
7.內聯函數
??以inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開,沒有函數調用建立棧幀的開銷,內聯函數提升了程序運行的效率。
inline void Fun(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}int main()
{int a = 10, b = 20;Fun(a, b);return 0;
}
int main()
{int a = 10, b = 20;int tmp = a;a = b;b = tmp;return 0;
}
??內聯函數的作用就是將上面的代碼轉化為下面的代碼,也就是不去調用函數了,而是將函數代碼在原地展開。那大家有沒有發現這和C語言中的什么比較相似呢?沒錯,就是宏。
#define Add(a,b) ((a) + (b))
??這是用宏實現了一個加法函數,我們知道宏就是直接在相應的位置替換,但是我們知道操作符是有優先級的,直接替換過去是有可能發生操作符優先級不正確導致出錯的。
宏的缺點:
- 不方便調試宏。(因為預編譯階段進行了替換)
- 導致代碼可讀性差,可維護性差,容易誤用。
- 沒有類型安全的檢查 。
宏的優點
- 沒有類型的嚴格控制。
- 針對頻繁調用小函數,不需要建立棧幀,提高性能。
??因此為了避免這種錯誤,C++新增了內聯函數inline來解決這個問題。不需要大家刻意的寫很多括號來保證操作符的正確使用順序。
7.1 特性
- inline是一種以空間換時間的做法,如果編譯器將函數當成內聯函數處理,在編譯階段,會用函數體替換函數調用,缺陷:可能會使目標文件變大,優勢:少了調用開銷,提高程序運行效率。
- inline對于編譯器而言只是一個建議,不同編譯器關于inline實現機制可能不同,一般建議:將函數規模較小(即函數不是很長,具體沒有準確的說法,取決于編譯器內部實現)、不是遞歸、且頻繁調用的函數采用inline修飾,否則編譯器會忽略inline特性。《C++prime》第五版關于inline的建議:內聯只是向編譯器發出一個請求,編譯器可以忽略這個請求。
- inline不建議聲明和定義分離,分離會導致鏈接錯誤。因為inline被展開,就沒有函數地址了,鏈接就會找不到。
8.auto關鍵字
??在早期C/C++中auto的含義是:使用auto修飾的變量,是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它,大家可思考下為什么?
??C++11中,標準委員會賦予了auto全新的含義即:auto不再是一個存儲類型指示符,而是作為一個新的類型指示符來指示編譯器,auto聲明的變量必須由編譯器在編譯時期推導而得。
??也就是 auto 會自動推到出變量的類型,在對于一些變量類型比較長是會方便的,后續就會遇到。
typeid().name() 的作用就是識別一個變量的類型。
?
使用auto定義變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導auto的實際類型。因此auto并非是一種“類型”的聲明,而是一個類型聲明時的“占位符”,編譯器在編譯期會將auto替換為變量實際的類型。
8.1 注意事項
??用auto聲明指針類型時,用auto和auto*沒有任何區別,但用auto聲明引用類型時則必須加&。
??當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因為編譯器實際只對第一個類型進行推導,然后用推導出來的類型定義其他變量。
void test_auto()
{auto a = 1, b = 2; auto c = 3, d = 4.0; // 該行代碼會編譯失敗,因為c和d的初始化表達式類型不同
}
- auto不能作為函數的參數。
- auto不能直接用來聲明數組。
- 為了避免與C++98中的auto發生混淆,C++11只保留了auto作為類型指示符的用法。
- auto在實際中最常見的優勢用法就是跟以后會講到的C++11提供的新式for循環,還有lambda表達式等進行配合使用。
9. 基于范圍的for循環
??我們通常遍歷一個數組是這樣的:
int main()
{int a[5] = { 0,1,2,3,4 };for(int i = 0; i < 5; i++){cout << a[i] << " ";}return 0;
}
??對于一個有范圍的集合而言,由程序員來說明循環的范圍是多余的,有時候還會容易犯錯誤。因此C++11中引入了基于范圍的for循環。for循環后的括號由號“ :”分為兩部分:第一部分是范圍內用于迭代的變量,第二部分則表示被迭代的范圍。
??實際過程是將 arr 中的一個數拿出來放到 e 中,進行輸出,再繼續拿第二個數……
9.1 使用條件
- for循環迭代的范圍必須是確定的
??對于數組而言,就是數組中第一個元素和最后一個元素的范圍;對于類而言,應該提供begin和end的方法,begin和end就是for循環迭代的范圍。
void test_for(int arr[])
{for(auto& e : arr)cout<< e <<endl;
}
??這樣寫就是有問題的,因為 for 的范圍不確定。
2. 迭代的對象要實現++和==的操作,這里針對的是類。
10.指針控制nullptr
??在良好的C/C++編程習慣中,聲明一個變量時最好給該變量一個合適的初始值,否則可能會出現不可預料的錯誤,比如未初始化的指針。如果一個指針沒有合法的指向,我們基本都是按照如下方式對其進行初始化:
void test_ptr()
{int* p1 = NULL;int* p2 = 0;//……
}
??NULL實際是一個宏,在傳統的C頭文件(stddef.h)中,可以看到如下代碼:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
??可以看到,NULL可能被定義為字面常量0,或者被定義為無類型指針(void*)的常量。不論采取何種定義,在使用空值的指針時都不可避免的會遇到一些麻煩,比如:
void f(int)
{cout<<"f(int)"<<endl;
}
void f(int*)
{cout<<"f(int*)"<<endl;
}
int main()
{f(0);f(NULL);f((int*)NULL);return 0;
}
??程序本意是想通過f(NULL)調用指針版本的f(int*)函數,但是由于NULL被定義成0,因此與程序的初衷相悖。
??在C++98中,字面常量0既可以是一個整形數字,也可以是無類型的指針(void*)常量,但是編譯器默認情況下將其看成是一個整形常量,如果要將其按照指針方式來使用,必須對其進行強轉(void *)0。
??因此在C++中,區分了這種情況,讓 nullptr 專門代指了指針。
10.1 注意事項
- 在使用nullptr表示指針空值時,不需要包含頭文件,因為nullptr是C++11作為新關鍵字引入的。
- 在C++11中,sizeof(nullptr) 與 sizeof((void*)0)所占的字節數相同。
- 為了提高代碼的健壯性,在后續表示指針空值時建議最好使用nullptr。
11.總結
??在初學C++時,需要記憶的地方有很多,難度不大但細節較多,建議大家整理一些筆記來幫助日后的復習。
??此篇內容字數尚可,初學C++還是略感疲憊(嘆氣),不過與之前相比好多了(愉悅)。如果大家發現有什么錯誤的地方,可以私信或者評論區指出喔(虛心請教,渴望大佬幫助)。我會繼續深入學習C++,希望能與大家共同進步,那么本期就到此結束,讓我們下期再見!!覺得不錯可以點個贊以示鼓勵喔!!