終於進入了C++的學習。正所謂學好C++,走遍全天下。C++可以說爲我打開了新世界。不過數據結構的知識還是得不斷地複習啊
C++關鍵字
關鍵字在C語言中就已經見過了許多比如最常見的if-else,for,int,double等等。只不過在C++中還會遇到更多的,以後會慢慢學習
命名空間
C++引入命名空間這個概念是因爲在我們編程中可能寫到大量的函數,比如我在某個頭文件中寫了abc()這樣一個函數,但是在我的另一個庫文件中本身就存在abc()這樣一個函數。那麼我一旦調用就會發生衝突
所以使用命名空間的目的是對標識符的名稱進行本地化,以避免命名衝突或名字污染,namespace關鍵字的出現就是針對這個問題的
命名空間的定義
//普通的命名空間
namespace N1{//N1爲命名空間的名稱
int a;
//命名空間的內容,既可以定義變量,也可以定義函數
int add(int b, int c){
return b + c;
}
}
//命名空間也可以嵌套
namespace N1{
int a;
int add(int b, int c){
return b + c;
}
namespace N2{
int add(int b, int c){
return b + c;
}
}
}
//同一個工程中允許存在多個相同名稱的命名空間
//編譯器最後會合成同一個命名空間
namespace N1{
int Mul(int b, int c){
return b * c;
}
}
注意:一定命名空間定義了一個新的作用域,命名空間中的所有內容都侷限於該命名空間中
命名空間的使用
1、加命名空間名稱及作用域限定符
int main(){
printf("%d/n",N1::a);
return 0;
}
2、使用using將命名空間中成員引入
using N1::a;
int main(){
printf("%d\n",a);
}
3、使用using namespace命名空間引入
using namespace N1;
int main(){
printf("%d\n",a);
add(10,20);
}
C++輸入和輸出
學習一門語言最先開始要進行的打招呼儀式
#include <iostream>
using namespace std;
int main(){
cout<< "hello,world" <<endl;
return 0;
}
使用cout標準輸出
和cin標準輸入時
,必須包含<iostream>
頭文件以及std標準命名空間。
關於.h
:早期標準庫將所有功能在全局域中實現,聲明在.h
的後綴的頭文件中,使用時只需要包含對應的頭文件即可,後來將其實現在std命名空間下,爲了和C頭文件區分,也爲了正確使用命名空間,規定C++頭文件不帶.h
;舊編譯器(VC++6.0)還支持,後續不再支持。現在推薦使用<iostream> + std
的方式
使用c++輸入輸出更方便,不需增加數據格式控制
#include <iostream>
using std::cout;
using std::endl;
int main(){
int a;
double b;
char c;
cin>>a;
cin>>b>>c;
cout<< a <<endl;
cout<< b <<" "<<endl;
return 0;
}
缺省參數
缺省參數可以理解爲備用值。
缺省參數概念
缺省參數是聲明或定義函數時爲函數的參數指定一個默認值。在調用該函數時,如果沒有指定實參則採用該默認值,否則使用指定的實參。
#include <iostream>
using std::cout;
using std::endl;
void Func(int a = 9){
cout<<a<<endl;
}
int main(){
Func();
Func(100);
return 0;
}
缺省參數的分類
全缺省參數
void testfunc(int a = 10,int b = 20,int c = 30){
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
半缺省參數
void testfunc2(int a,int b = 20,int c = 30){
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
半缺省參數的函數在調用時,必須加上參數a的賦值,否則會報錯。
半缺省參數必須從右往左依次來給,不能間隔着給
缺省參數不能在函數聲明和定義中同時出現,因爲當聲明與定義位置同時出現,恰巧兩個位置提供的值不同,那編譯器就無法確認到底該用哪個缺省值
缺省值必須是常量或者全局變量
函數重載
函數重載的概念
函數重載是函數的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參列表(參數個數 或 類型 或 順序)必須不同,常用來處理實現功能類似數據類型不同的問題
#include <iostream>
using std::cout;
using std::endl;
int add(int a,int b){
return a + b;
}
double add(double a, double b){
return a + b;
}
long add(long a,long b){
return a +b;
}
int main(){
cout<<add(10,20)<<endl;
cout<<add(10.0,20.0)<<endl;
cout<<add(10L,20L)<<endl;
return 0;
}
雖然函數名都是一樣的,但是參數傳參的類型不同。當在調用的時候,編譯器檢查到參數的不同,對應到相應的函數中,這樣就可以實現不同類型的計算了。
//下面兩個函數,雖然參數不同,但是函數類型已經確定了返回值,這樣的話函數不管參數給什麼都沒法實現不同類型的計算,就不存在重載的意義了
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
名字修飾
在C++中,一個程序要運行起來,需要經歷以下幾個階段:預處理,編譯,彙編,鏈接
而名字修飾是一種在編譯過程中,將函數、變量的名稱重新改編的機制,簡單來說就是編譯器爲了區分各個函數,將函數通過某種算法,重新修飾爲一個全局唯一的名稱
C語言的名字修飾比較簡單,就直接在函數名前面加了一個下劃線。比如add
修飾爲_add
但C++不同,由於C++要支持重載,命名空間等,使得其修飾規則比較複雜,不同編譯器在底層的實現方式可能都有差異
名字的修飾過程不必深入瞭解,只需知道被重新修飾後的名字中包含了:函數的名字以及參數類型,這就是爲什麼函數重載中幾個同名函數要求其參數列表不同。
Linux下我們通過指令反彙編來查看objdump -d a.out >log.txt
我們可以看到三個很相似的名字,_Z3addii
,_Z3adddd
,_Z3addll
但是仔細觀察它們的最後兩個字母都不相同,這代表了參數類型不同。分別是int,double,long
但是它們的規制是一樣的"返回類型+函數名+參數列表"
這也可以解釋了爲什麼C語言中不支持函數重載,因爲在函數編譯過程中,C語言並不能將同名不同參數的函數名字修飾,所以它沒法支持函數重載。
extern“C”
有時候在C++工程中可能需要將某些函數按照C的風格來編譯,在函數前加extern “C”,意思是告訴編譯器,將該函數按照C語言規則來編譯。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
//可以編譯通過,但是鏈接時出現錯誤
引用
引用的概念
引用不是新定義一個變量,而是給已存在的變量取了一個別名,編譯器不會爲引用變量開闢內存空間,它和引用的變量共用同一塊內存空間
類型& 引用變量名(對象名) = 引用實體
void TestRef()
{
int a = 10;
int& ra = a;//<====定義引用類型
printf("%p\n", &a);
printf("%p\n", &ra);
}
引用類型必須和引用實體是同種類型的
引用特性
1、引用在定義時必須初始化
2、一個變量可以有多個引用
3、引用一旦引用一個實體,再不能引用其他實體
常引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 該語句編譯時會出錯,a爲常量
const int& ra = a;
// int& b = 10; // 該語句編譯時會出錯,b爲常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 該語句編譯時會出錯,類型不同
const int& rd = d;//這樣引用類型可以,但是返回值是int類型
}
int const&表示此引用所代表的int類型變量無法使用此引用修改.
const int&表示此引用所代表的是const int類型變量,同樣也無法用此引用修改.
使用場景
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
當執行這段代碼時,輸出應該是3,但是輸出的結果是7
這是因爲再函數返回時,離開函數作用域後,其棧上空間已經還給了系統,因此不能用棧上的空間作爲引用類型返回。如果以引用類型返回,返回值的生命週期必須不受函數的限制(即比函數生命週期長)
再來看這段代碼,ret第一次引用之後爲3,但是函數離開了作用域後,棧上的空間被歸還了。但此時又調用了一次這個函數,棧上的空間可能還爲上一次使用的區域,此時ret就爲7了。
引用和指針的區別
在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間
但是在底層實現上實際是有空間的,因爲引用按照指針方式實現的
引用和指針的不同點
1. 引用在定義時必須初始化,指針沒有要求
2. 引用在初始化時引用一個實體後,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實
體
3. 沒有NULL引用,但有NULL指針
4. 在sizeof中含義不同:引用結果爲引用類型的大小,但指針始終是地址空間所佔字節個數(32位平臺下佔4
個字節)
5. 引用自加即引用的實體增加1,指針自加即指針向後偏移一個類型的大小
6. 有多級指針,但是沒有多級引用
7. 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理
8. 引用比指針使用起來相對更安全
內聯函數
以inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開,沒有函數壓棧的開銷,內聯函數提升程序運行的效率。
內聯是一種空間換時間的做法,省去調用函數額開銷。所以代碼很長或者有循環/遞歸的函數不適宜使用作爲內聯函數。
如果在上述函數前增加inline關鍵字將其改成內聯函數,在編譯期間編譯器會用函數體替換函數的調用。
編譯器在不同的模式下查看有區別:
在release模式下,查看編譯器生成的彙編代碼中是否存在call Add
在debug模式下,需要對編譯器進行設置,否則不會展開
auto關鍵字
C++11中,標準委員會賦予了auto全新的含義:auto不再是一個存儲類型指示符,而是作爲一個新的類型指示符來指示編譯器,auto聲明的變量必須由編譯器在編譯時期推導而得
//simple demo
#include<iostream>
using std::cout;
using std::endl;
int main(){
int a = 10;
auto b = a;
cout<< "b is "<< b <<endl;
cout<< a << endl;
}
注意!!
注意!!
注意!!
使用auto定義變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導auto的實際類型。因此auto並非是一種“類型”聲明,而是一個類型聲明時的“佔位符”,編譯器在編譯期間會將auto替換爲變量實際的類型
auto使用規則
1、auto與指針和引用結合起來使用
用auto聲明指針類型時,用auto和auto*沒有任何區別,但用auto聲明引用類型時,則必須加&
int main(){
int a = 10;
auto b = a;
auto* c = &a;
auto& d = a;
cout << "b is " << b << endl;
cout << a << endl;
cout << *c << endl;
cout << d << endl;
return 0;
}
2、在同一行定義多個變量
當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因爲編譯器實際只對第一個類型進行推導,然後用推導出來的類型其他變量。
int main(){
auto a = 1,b = 2;
auto c = 3,d = 4.0;//編譯會失敗,因爲c和d的初始化表達式類型不同
}
auto不能推導的場景
1、auto不能作爲函數的參數
void TestAuto(auto a){
pass;
}
上面代碼是不能編譯通過的,auto不能作爲形參類型,因爲編譯器無法對a的實際類型進行推導
2、auto不能直接用來聲明數組
void TestAuto(){
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
3、auto不能定義類的非靜態成員變量
4、實例化模板時不能使用auto作爲模板參數
基於範圍的for循環(C++11)
範圍for的語法
在C++98如果要遍歷一個數組,可以按照以下方式進行:
void TestFor(){
int array[] = {1,2,3,4,5};
for(int i = 0;i < sizeof(array)/sizeof(array[0]);++p){
array[i] *= 2;
}
for(int* p = array;p < array + sizeof(array)/sizeof(array[0]);++p)
cout<< *p <<endl;
}
C++是一個追求極致性能的語言,在有範圍的數組中,由程序員來說明循環的範圍是多餘的,有時還會容易犯錯誤。for循環後的括號由冒號":"分爲兩部分,第一部分是範圍內用於迭代的變量,第二部分則表示被迭代的範圍
void TestFor(){
int array[] = {1,2,3,4,5};
for(auto& e : array)
e *= 2;
for(auto e : array)
cout<< e << " ";
}
範圍for的使用條件
1、for循環迭代的範圍必須是確定的
對於數組而言,就是數組中第一個元素和最後一個元素的範圍;對於類而言,應該提供begin和end的方法,begin和end就是for循環迭代的範圍
比如如下代碼,數組作爲形參傳入時,這時候的循環條件是不確定的。
void TestFor(int array[]){
for(auto& e : array)
cout<< e <<endl;
}
2、迭代的對象要實現++和==的操作
指針空值nullptr
在C語言中,我們一般定義一個空指針是int *p = NULL
或者int *p = 0
兩者的意思基本上是相似的。不過NULL實際上是一個宏
#define NULL 0
#define NULL ((void*) 0)
在頭文件的定義中,一般是這樣的。可以看到,NULL可能被定義爲字面常量0,或者被定義爲無類型指針(void*)0)
比如在函數中想調用int*的函數,但是NULL被定義成0,因此與程序的初衷相悖
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;
}
nullptr和nullptr_t
爲了考慮兼容性,C++11並沒有消除常量0的二義性,C++11給出了全新的nullptr表示空值指針。C++11爲什麼
不在NULL的基礎上進行擴展,這是因爲NULL以前就是一個宏,而且不同的編譯器廠商對於NULL的實現可能
不太相同,而且直接擴展NULL,可能會影響以前舊的程序。因此:爲了避免混淆,C++11提供了nullptr,
即:nullptr代表一個指針空值常量。nullptr是有類型的,其類型爲nullptr_t,僅僅可以被隱式轉化爲指針類
型,nullptr_t被定義在頭文件中:
typedef decltype(nullptr) nullptr_t;
注意!!!
注意!!!
注意!!!
1.在使用nullptr表示指針空值時,不需要包含頭文件,因爲nullptr是C++11作爲新關鍵字引入的。
2.在C++11中,sizeof(nullptr) 與 sizeof((void)0)所佔的字節數相同。*
3.爲了提高代碼的健壯性,在後續表示指針空值時建議最好使用nullptr