C++的初步學習有如下幾個方面ios
咱們知道,在c中有32個關鍵字,而c++中有63個關鍵字
分別爲
c++
爲何會有命名空間,他的做用是什麼?
在一個大的工程裏,要定義不少變量和函數,若將這些變量和函數都定義在全局做用域中,一不當心就可能出現重複定義的狀況。於是引入命名空間的概念,其目的是對標識符名稱進行本地化,以免命名衝突或名字衝突。
命名空間是什麼?
一個命名空間就定義了一個新的做用域,命名空間中的全部內容都侷限於該命名空間裏。命名空間裏可有變量、函數、結構體、另外一個命名空間等等普通在全局定義的命名空間裏均可以有。在不一樣的命名空間裏可使用一個變量名。之後在使用某個命名空間裏的某個變量,引入就能夠了。這樣定義變量時,就不用考慮以前這個名字有沒有用過,只用看在這個命名空間裏存不存在該變量。
命名空間的定義
定義命名空間,須要使用到namespace關鍵字,後面跟命名空間的名字,而後接一對{}便可,{}中即爲命名空間的成員。命名空間的定義有如下三種形式:程序員
//1.普通定義 namespace N1 // N1爲命名空間的名稱 { int a; int Add(int left, int right) { return left + right; } } //2.嵌套定義 namespace N2 { int a; int b; int Add(int left, int right) { return left + right; } namespace N3 { int c; int d; int Sub(int left, int right) { return left - right; } } } //3.重複的定義 namespace N1{int a}; namespace N1{int b}; //在編譯時,編譯器會自動將其合併爲一個命名空間,在定義的時候也可將其看作同一個命名空間,於是同名命名空間不要使用相同變量
命名空間的使用
在命名空間裏定義的內容是不能夠直接使用的。
引用一個操做符 ‘::’ 做用域限定符用於在做用域外引用做用域裏的內容
引用一個關鍵字:在一個做用域中使用 using 將另外一個命名空間裏的想要的內容拿出來,方便下面使用數據庫
使用方式有如下三種:數組
//1.加命名空間名稱及做用域限定符 namespace N { int a; int b; } int main{ printf("%d\n", N::a); 打印N中的a return 0; } //2.使用using將命名空間中成員引入 using N::b; int main() { printf("%d\n", N::a); //並沒引入a printf("%d\n", b); //在此的b就能夠直接使用了 return 0; } // 3.使用using namespace 命名空間名稱引入 using namespce N; //將N 中全部的內容都引入 int main() { printf("%d\n", a); printf("%d\n", b); return 0; }
輸出函數:cout標準輸出(控制檯)相似於printf
輸入函數:cin標準輸入(鍵盤)相似於scanf
兩個函數屬於標準庫 iostream 再引入命名空間std
用法:他們的用法比printf和scanf要靈活,輸出不用再加%d..來講明輸出/輸入什麼類型的值,可鏈接各類類型的值
例如以下代碼安全
#include <iostream> using namespace std; int main() { int a; double b; char c; cin>>a; cin>>b>>c; cout<<a<<endl; cout<<b<<" "<<c<<endl; return 0; }
概念:缺省參數是聲明或定義函數時爲函數的參數指定一個默認值。在調用該函數時,若是沒有指定實參則採用該默認值,不然使用指定的實參。例如:ide
void TestFunc(int a = 0) { cout<<a<<endl; } int main() { TestFunc(); // 沒有傳參時,使用參數的默認值 0 TestFunc(10); // 傳參時,使用指定的實參 }
在一個函數的形參列表中,咱們能夠給一部分形參默認值,也能夠全給。所以分爲半缺省參數和全缺省參數,用法及要求以下函數
全缺省參數:每一個形參都賦了缺省值 性能
void TestFunc(int a = 10, int b = 20, int c = 30) { cout<<"a = "<<a<<endl; cout<<"b = "<<b<<endl; cout<<"c = "<<c<<endl; } int main() { TestFunc(); //10 20 30 TestFunc(1); //1 20 30 TestFunc(1,2); // 1 2 30 //爲何把1給a呢?咱們從半缺省參數用法裏找答案 }
半缺省參數:不是全部的形參都賦了缺省值,但賦半缺省參數有必定規則: 半缺省參數必須從右往左依次來給出,不能間隔着給,就是前面的能夠省略,但一旦給值,後面的都必須都給值 。所以學習
void TestFunc(int a, int b = 10, int c = 20)√ void TestFunc(int a=10, int b , int c = 20) × void TestFunc(int a=10, int b=20 , int c ) ×
經過半缺省參數的規則,咱們可回答爲何全缺省參數給值是從前日後給的:半缺省參數前面的能夠省略,因此在不知道函數是否是半缺省參數的狀況下,實參要賦從第一個形參開始賦值
定義:在同一做用域中聲明幾個功能相似的同名函數,這些同名函數的形參列表(參數個數 或 類型 或 順序)必須不一樣,經常使用來處理實現功能相似數據類型不一樣的問題。
如如下代碼:
int Add(int left, int right) { return left+right; } double Add(double left, double right) { return left+right; } long Add(long left, long right) { return left+right; } int main() { Add(10, 20); Add(10.0, 20.0); Add(10L, 20L); //經過實參類型來找函數 return 0; }
注:函數不可僅靠返回值類型來實現重載
short Add(short left, short right) { return left+right; } int Add(short left, short right) { return left+right; } //這兩個函數沒法實現重載
void TestFunc(int a = 10); void TestFunc( ); //這兩個函數就沒法造成重載,在另外一個函數中調用TestFunc( ),編譯器不知道要調用哪個;
缺省函數與普通函數沒法造成重載,例如:
void TestFunc(int a = 10); void TestFunc(int a ); //這兩個函數就沒法造成重載,在另外一個函數中調用TestFunc(num ),編譯器不知道要調用哪個;
於是:想要造成函數重載,要確保兩個函數在調用的時候不會起衝突,不會出如今傳某個值的時候,兩個函數均可以調的狀況。
咱們知道:c語言中不能夠實現函數重載,爲何c++中能夠呢?由於在程序編譯時,編譯器會對每一個函數名進行命名修飾,下面咱們來引入命名修飾的概念
在c++程序編譯時,編譯器爲區分各個函數,會將函數、變量名從新改變,使每一個函數名成爲全局惟一的名稱,將參數類型包含在最終的名字中,於是經過形參列表的不一樣能夠將同名函數進行區分,就可保證名字在底層的全局惟一性。
那麼c++中具體將名字修改爲什麼樣子了呢?
有以下代碼:
int Add(int left, int right); double Add(double left, double right); int main() { Add(1, 2); Add(1.0, 2.0); return 0; } //在vs下,對上述代碼進行編譯連接,最後編譯器報錯: //error LNK2019: 沒法解析的外部符號 "double cdecl Add(double,double)" (?Add@@YANNN@Z) // error LNK2019: 沒法解析的外部符號 "int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
經過上述錯誤能夠看出,編譯器實際在底層使用的不是Add名字,而是被從新修飾過的一個比較複雜的名字,被從新修飾後的名字中包含了:函數的名字以及參數類型。
visual stdio 下c++的修飾規則:
經過以上簽名及修飾後的名字可推得命名方式:
修飾後名字由「?」開頭,接着是函數名由「@"符號結尾的函數名:後面跟着由「@"結尾的類名「C」和名稱空間「N」,再一個「@」表示函數的名稱空間結束:第一個「A」表示函數調用類型爲「_ cdecl」 ,接着是函數的參數類型及返回值,由「@」結束,最後由「Z」結尾。其中A後面第一個是返回值類型,而後接下來到@以前都是形參的類型,H表示int,M表示float
那爲何c語言中,同名函數爲何不能構成重載呢?
由於c語言中的名字修飾只是在函數名前加了個下劃線,形參列表並未參與名字修飾,於是不可以經過形參列表來區分各個同名函數。
在某個函數前加extern 「C」,可將c++工程中某些函數按c的風格來編譯
概念:給變量取了個別名,和變量共用一塊內存空間,能夠經過引用來改變變量。
定義:類型& 引用變量名=引用實體
注意:引用類型必須和引用實體的類型必須相同。
如:
int a = 10; int& ra = a;//定義引用類型 printf("%p\n", &a); printf("%p\n", &ra); //結果相同
引用特性:
1>引用在定義時必須初始化,不能存在空着的引用
int& ra ;//會發生錯誤 //起了外號,這個外號又不是任何人的,這個外號存在有什麼意義?
2>一個變量可有多個引用(一我的能夠起不少個別名)
3>引用一旦引用一個實體,再不能引用其餘實體
int a=0; int b=1; int& ra=a; ra=b; //ra不是改變了引用,只是將b的值賦給ra printf("%d",a); //->1
常引用
const int a = 10; int& ra = a; // 該語句編譯時會出錯,a爲常量 //const修飾的變量,引用前也要加const,若不加,那麼就能夠經過引用修改變量的值了。 const int& ra = a;//正確寫法 int& b = 10; // 該語句編譯時會出錯,10爲常量 //引用不能作常數的引用,要引用前面加const,常熟也是不可以被修改的 const int& b = 10; double d = 12.34; int& rd = d; // 該語句編譯時會出錯,類型不一樣 const int& rd = d;//這個是正確的的,但rd並非d的別名 //而是先經過a來造成一個臨時變量存放a的整數部分,而後ra引用這個臨時變量。可是該臨時變量不知道名字,也不知道地址,於是也修改不了,該臨時變量具備必定的常性,於是要在ra前加const
引用使用場景
1>作參數:函數形參設爲引用類型
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; }
說明:若是想要經過形參改變實參,可將形參設爲普通類型 若是不想要經過形參改變實參,可將形參設爲const類型。
效率:傳值的效率低於傳址、傳引用效率。傳地址和傳引用時間相同。由於傳引用和傳指針的過程在內存中的變化實際上是同樣的,傳引用的過程在編譯時,會轉成傳指針的形式,在編譯過程當中,引用是按照指針方式來實現的
#include <time.h> struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值做爲函數參數 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用做爲函數參數 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分別計算兩個函數運行結束後的時間 cout << "TestFunc1(int*)-time:" << end1 - begin1 << endl; cout << "TestFunc2(int&)-time:" << end2 - begin2 << endl; } // 運行屢次,檢測值和引用在傳參方面的效率區別 //結果都很小,並且相差無幾 //反彙編後,可看到傳引用的過程和傳指針的過程如出一轍。 int main() { for (int i = 0; i < 10; ++i) { TestRefAndValue(); } return 0; }
2>作返回值:將返回值類型設爲引用類型
int& TestRefReturn(int& a) { a += 10; return a; }
注意:若是函數返回時,離開函數做用域後,其棧上空間已經還給系統,所以不能用棧上的空間做爲引用類型返回。所以,引用做爲返回值,返回變量不該受函數控制,即函數結束,變量的生命週期存在。好比:全局變量,static修飾的局部變量,用戶未釋放的堆,引用類型參數
發生該錯誤有如下代碼:
int& Add(int a, int b) { int c = a + b; return c; } //在函數調用完後,棧上的c佔用的那一塊空間就被釋放了(能夠覆蓋),所以就沒什麼意義了 int main() { int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; //->7,Add(3, 4)將c的那一塊空間又覆蓋掉了 return 0; }
經過比較,發現傳值和指針在做爲傳參以及返回值類型上效率相差很大,於是可讓引用做爲返回值的地方就用引用,除非是要返回一個函數中定義的變量(該變量的空間會隨函數調用完而變得無效)要返回值外,其餘狀況均可用引用返回。
#include <time.h> struct A { int a[10000]; }; A a; A TestFunc1() { return a; } A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值做爲函數的返回值類型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用做爲函數的返回值類型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 計算兩個函數運算完成以後的時間 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } // 測試運行10次,值和引用做爲返回值效率方面的區別 int main() { for (int i = 0; i < 10; ++i) TestReturnByRefOrValue(); return 0; }
引用與指針
在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間,但在底層實現上實際是有空間的,由於引用是按照指針方式來實現的。
int main() { int x = 10; int& rx = x; rx = 20; int* px = &x; *px = 20; return 0; }
對於該代碼咱們來看反彙編代碼:
可發現,在內存中二者在底層的使用方式是同樣的,引用也是按照指針方式來實現的
那二者又有什麼不一樣呢?
1> 引用在定義時必須初始化,指針沒有要求。於是指針須要判空,而引用不用,由於引用定義時就初始化了
2> 引用在初始化時引用一個實體後,就不能再引用其餘實體,而指針能夠在任什麼時候候指向任何一個同類型實體
3> 沒有NULL引用,但有NULL指針
4>在sizeof中含義不一樣:引用結果爲引用類型的大小,但指針始終是地址空間所佔字節個數(32位平臺下佔4個字節)
5>引用自加即引用的實體增長1,在連續的空間中指針自加即指針向後偏移一個類型的大小
6>有多級指針,可是沒有多級引用
7> 訪問實體方式不一樣,指針須要顯式解引用,引用編譯器本身處理
8> 引用比指針使用起來相對更安全。
概念:以inline修飾的函數叫作內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開,沒有函數壓棧的開銷,內聯函數提高程序運行的效率。
普通函數會進行壓棧造成棧幀等操做
而內聯函數在編譯時會直接將調用函數換爲函數內部的操做
查看方式:1. 在release模式下,查看編譯器生成的彙編代碼中是否存在call Add2. 在debug模式下,須要對編譯器進行設置,不然不會展開(由於debug模式下,編譯器默認不會對代碼進行優化,給出vs2013的設置方式):功能->屬性->配置->c/c++->將常規中的調試信息格式改成程序數據庫,再將優化中的內聯函數擴展改成只適用於_inline
特性
1> inline是一種以空間換時間的作法。因此代碼很長或者有循環/遞歸的函數不適宜使用做爲內聯函數。
2>inline對於編譯器而言只是一個建議,編譯器會自動優化,若是定義爲inline的函數體內有循環/遞歸等等,編譯器優化時會忽略掉內聯。
3>inline不建議聲明和定義分離,分離會致使連接錯誤。由於inline被展開,就沒有函數地址了,連接就會找不到。於是內聯函數具備文件做用域,只在本文件有用,其餘文件不可用。
// F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 連接錯誤:main.obj : error LNK2019: 沒法解析的外部符號 "void __cdecl f(int)" (? f@@YAXH@Z),該符號在函數 _main 中被引用
內聯函數與const、宏
在c++中,const修飾的變量有常量的特性也有宏的特性,在編譯時會發生替換和檢測,即便經過指針修改也沒法改變變量值。有以下代碼
const int a=1; int *pa=(int *)a; *pa=2; printf("%d,%d",*pa,a); //結果爲2,1 a仍然沒有修改
而在c中是能夠的,由於c中是不會檢測的,經過指針也是修改const變量的
宏是在預處理時替換的,不參與編譯,也不可調試。
宏的優勢:加強代碼的複用性。提升性能。
缺點:
1>不方便調試宏。(由於預處理階段進行了替換)
2>致使代碼可讀性差,可維護性差,容易誤用。
3>沒有類型安全的檢查 。
所以在c++中,可經過const來代替宏對常量的定義,用內聯函數來代替宏對函數的定義
內聯函數的優缺點:
https://mp.csdn.net/mdeditor/101083065#
概念:在C++中,auto做爲一個新的類型指示符來定義變量,auto聲明的變量是由編譯器在編譯時期推導而得,變量被賦值什麼類型,由初始化的值而定。
特性:
1>使用auto定義變量時必須對其進行初始化,在編譯階段編譯器須要根據初始化表達式來推導auto的實際類型。
2>auto並不是是一種「類型」的聲明,而是一個類型聲明時的「佔位符」,編譯器在編譯期會將auto替換爲變量實際的類型
int TestAuto() { return 10; } int main() { int a = 10; auto b = a; auto c = 'a'; auto d = TestAuto(); cout << typeid(b).name() << endl; //int cout << typeid(c).name() << endl; //char cout << typeid(d).name() << endl; //int //auto e; 沒法經過編譯,使用auto定義變量時必須對其進行初始化 return 0; }
使用方法
1>auto與指針和引用結合:用auto聲明指針類型時,用auto和auto* 沒有任何區別,但用auto聲明引用類型時則必須加&.
int x = 1; auto px = &x; auto *ppx = &x; auto& rx = x; auto rrx = x; cout << typeid(px).name() << endl; cout << typeid(ppx).name() << endl; cout << typeid(rx).name() << endl; cout << typeid(rrx).name() << endl; rx = 3; cout << x << endl; //x發生了變化說明是引用 rrx = 2; cout << x << endl; //x未發生變化,說明不是引用
2>auto在同一行定義多個變量,當在同一行聲明多個變量時,這些變量必須是相同的類型,不然編譯器將會報錯,由於編譯器實際只對第一個類型進行推導,而後用推導出來的類型定義其餘變量。
auto f = 1, g = 2; //auto h = 1, i = 2.3; //編譯會報錯,h和i類型不一樣
3>auto不能直接用來聲明數組
int h[] = { 1, 2, 3 }; //auto t[] = { 4,5,6 };//編譯時會發生錯誤
爲何要引入這個概念?
對一個有範圍的集合由程序員來講明循環的範圍是多餘的,有時候還會容易犯錯誤。所以C++11中引入了基於範圍的for循環。
用法:for循環後的括號由冒號「 :」分爲兩部分:第一部分是範圍內用於迭代的變量,第二部分則表示被迭代的範圍。
int arr[] = { 1, 2, 3, 4, 5 }; for (auto& e : arr) //=>for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) e *= 2; for (auto e : arr) //要對元素值進行改變,變量前要加&,不改變,直接普通變量 cout << e << " ";
對於數組而言,就是數組中第一個元素和最後一個元素的範圍;對於類而言,應該提供begin和end的方法,begin和end就是for循環迭代的範圍。
概念:nullptr指針空值常量,表示指針空值使用nullptr。
爲何要有nullptr,NULL爲何沒法用於表示空指針了?
在指針定義時,要初始化(不然會出現野指針),在c中用NULL來給一個沒有指向的指針,但其實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); //變成0了,進了第一個函數,但咱們NULL想表示指針本是想進入第二個函數 f((int*)NULL); return 0; }
於是用nullptr來代替C中NULL在指針中的用法。
而且nullptr也是有類型的,其類型爲nullptr_t,僅僅能夠被隱式轉化爲指針類型,nullptr_t被定義在頭文件中:typedef decltype(nullptr) nullptr_t;
注意: