c++對於c語言的加強
namespace 命名空間
沒啥可以講的,避免了類名字,方法,成員變量的衝突。
實用性增強
變量可以邊定義邊用。
變量檢測增強
int a;
int a=8;會報錯了,不像c 不會報錯。
struct 類型增強
c++的struct 類似於class ,唯一的不同是class定義的函數默認是private 的,但是struct 定義的函數是public 的。而且,c不認爲這是一種新類型,仍然需要struct AAA,進行定義,但是c++則不需要,直接使用AAA 進行定義對象。即認爲是一種新的類型。
C++中所有變量和函數都必須有類型
如下代碼在c語言是OK的,但是在c++就不行咯。
f(i) {
printf("i = %d\n", i); }
g() {
return 5; }
新增bool 類型,返回值爲true 和false
三目運算符增強
返回的是引用,而不是值,所以可以對其進行賦值。
const 增強。
可以分清輸入與輸出的參數。帶const往往是輸入。在c語言中,const 是冒牌貨。
c++的const由編譯器處理,提供類型檢查與作用於檢查。
#define由預處理器處理,單純的文本替換。
c++的const 可能分配空間,也可能不分配空間。
當const 常量是全局的,並且需要在其他文件使用。會分配空間。
當使用&操作符。取const 地址,會分配空間。
const int &a=10; const 修飾引用,也會分配空間。
引用。
Type& name = var;
引用的規則。
1.引用沒有定義,是一種關係聲明,聲明和他原有的某一個變量關係,故不分配 內存,與被引用變量有着同樣的指向地址。
2.聲明的時候,必須得初始化,一經聲明,不能改變。
引用的本質
#include <iostream>
int main() {
int a = 10;
int &b = a; // 注意: 單獨定義的引⽤用時,必須初始化。
b = 11;
printf("a:%d\n", a);
printf("b:%d\n", b);
printf("&a:%p\n", &a);
printf("&b:%p\n", &b);
return 0; }
引用在c++中的內部實現是一個常指針,即一個指向不能改變的指針。
Type& name <===> Type* const name
c++在編譯過程中。使用常指針作爲內部實現。因此,引用所佔用的空間與指針大小相同。
從使用的角度,引用會讓人誤會其只是一個別名,沒有自己的存儲空間。這個是c++爲了實用性,而做出的細節隱藏。
void func(int &a)
{
a = 5; }
void func(int *const a)
{
*a = 5; }
間接賦值的3各必要條件
1定義兩個變量 (一個實參一個形參)
2建立關聯 實參取地址傳給形參
3*p形參去間接的修改實參的值.
引用的實現上。只不過是把,間接賦值成立的三個條件的後面2個合二爲一了。當實參傳給形參引用的時候,只不過是c++編譯器幫我們程序員手工。取了一個實參的地址,傳給了一個常引用。(常量指針)
引用作爲函數的返回值(引用當左值)
當函數返回值爲引用時候。若返回棧變量,不能成爲其他引用的初始值。(不能作爲左值引用。)
當函數返回值爲引用時,如果返回靜態變量或全局變量,可以成爲其他引用的初始值。可以作爲右值,也可以成爲左值。
什麼是引用當左值?
int& fun(){
int a=10;
return a;
}
fun()=10;
//如下的只是值拷貝。
int x=fun();
指針引用
const 引用
const對象的引用必須是const的,將普通引用綁定到const 對象是不合法的。如下。
const int a=1;
int &b=a;
//常引用在初始化其實變成了2個步驟
int temp = val;
const int &ref = temp;
這也是爲什麼
int &a=1;//是錯誤的
const int &a=1;//卻是正確的的原因。
結論:
1.const int &a;相當於const int * const a;
2.普通引用相當於int * const e;
3.當使用const對應用初始化的時候,c++會爲常量值分配空間,並將引用名作爲這段空間的別名。
4.使用字面量對const引用初始化後,將生成一個只讀的變量。
inline 函數
可以想是宏函數的拓展,內聯的特點。
1.沒有函數調用的開銷。(壓棧,跳轉,返回)
2.內聯函數由編譯器處理,直接將編譯後的代碼插入調用的地方。宏片段由預處理器處理,進行簡單的文本替換,沒有任何的編譯過程。
同時存在着一定的限制。
- 不能存在任何形式的循環語句
- 不能存在過多的條件判斷語句
- 函數體不能過於龐大
- 不能對函數進行取址操作
- 函數內聯聲明必須在調用語句之前
當函數體執行的開銷遠遠大與壓棧,跳轉,返回的開銷,內聯將失去意義
函數重載
C++利用 name mangling(傾軋)技術,來改名函數名,區分參數不同的同 名函數。
實現原理:用 v c i f l d 表示 void char int float long double 及其引 用。
每一個方法都有一個特殊的Symbol 符號。
拷貝構造函數調用的場景
場景1
Test t1(10);
Test t2=t1;
場景2
Test t1(10);
Test t2(t1);//使用對象t1初始化對象t2
1.函數返回值是一個元素(複雜類型),返回的是一個新的匿名對象(所以會調用匿名對象的copy構造函數)
2.有關匿名對象的去和留
如果匿名對象去初始化另外一個同類型的對象,匿名對象被扶正
如果用匿名對象去賦值給另外一個同類型的對象,匿名對象被析構
new delete
注意,new 與delete 返回的都是指針類型。是一塊內存地址。不能夠用普通對象直接接收,與malloc delete 有着同樣的功能,唯一的不同是,malloc free在初始化類對象的時候,是不會自動的幫我們調用構造函數,但是new 與delete 是會自動的幫我們調用。
靜態成員的定義
static 數據類型 成員變量; //在類的內部
//初始化
數據類型 類名::靜態數據成員 = 初值; //在類的外部
//調⽤用 類名::靜態數據成員 類對象.靜態數據成員
編譯器對屬性和方法的處理機制。
普通成員變量,存儲於對象中,與struct變量有着相同的內存佈局和字節對齊方式。
靜態成員變量,存儲於全局數據區
成員函數,存儲於代碼區
很多對象共用一塊代碼,代碼如何區分具體對象是那?
如下的圖應該算是很經典的一張圖了。
c++的普通成員函數都隱含一個包含當前對象的this指針
靜態成員函數不包含指向具體對象的指針。普通成員函數含有一個指向具體對象的指針。
友元
友元聲明以關鍵字friend開始,只能在類定義中出現,因爲不是授權類的成員,所以他不受類聲明的public private 和protected的影響
利弊:可以直接訪問類中的私有成員,破壞了類的封裝性和隱蔽性。但是提高了編碼的靈活性。
注意事項
友元關係無法繼承
友元關係無法交換
友元關係不具有傳遞性
運算符重載
執行重載的時候,記得對參數加const
雙目運算符重載
//使⽤用: L#R operator#(L,R);
//全局函數 L.operator#(R); //成員函數
Complex& operator+=(const Complex &c) {
this->_x += c._x; this->_y += c._y;
return * this;
}
重載規則請務必記住
//重載+號 (全局)
friend const Complex operator+(const Complex &c1,const Complex &c2);
//重載+號(成員)
const Complex operator+(const Complex &another);
//a+=b;對於a應該返回的還是原來的a 所以不能爲const
Complex& operator+=(const Complex &c)
//a-=b;同樣的是返回可以修改的內容。
friend Complex& operator-=(Complex &c1, const Complex & c2);
//前置++ 注意返回的是引用
friend Complex & operator++(Complex& c);
friend const Complex operator++(Complex &c,int);
friend ostream & operator<<(ostream &os, const Complex & c);
friend istream & operator>>(istream &is, Complex &c);
//賦值運算符重載 返回的是引用,不能用const 修飾,其目的是連等式
A& operator=(const A& another)
//數組符號的重載
類型 類 :: operator[] ( 類型 ) ;
結論:
1,一個操作符的左右操作數不一定是相同類型的對象,這就涉及到將該操作符函 數定義爲誰的友元,誰的成員問題。
2,一個操作符函數,被聲明爲哪個類的成員,取決於該函數的調用對象(通常是左 操作數)。
3,一個操作符函數,被聲明爲哪個類的友員,取決於該函數的參數對象(通常是右 操作數)。
重載&& 和||會丟失短路特性。
無參構造函數調用不能帶括號!
類的繼承方法
class 派⽣生類名:[繼承⽅方式] 基類名 { 派⽣生類成員聲明;
};
private成員在子類中依然存在,但是卻無法訪問到。不論何種方式繼承 基類,派生類都不能直接使用基類的私有成員 。
三種繼承關係
保護繼承中,基類的公有成員和私有成員都以保護成員的身份出現在派生
類 中,而基類的私有成員不可訪問。
當類的繼承方式爲私有繼承時,基類中的公有成員和保護成員都以私有成
員身 份出現在派生類中,而基類的私有成員在派生類中不可訪問。
當類的繼承方式爲公有繼承時,基類的公有和保護成員的訪問屬性在派生
類中 不變,而基類的私有成員不可訪問。
不論是什麼類型的繼承,在基類中不能使用的,只有私有成員,其餘就算因爲私有繼承而退化爲私有的,子類也是依然可以進行使用 的。
1.子類對象在創建時會先調用父類構造函數
2.父類構造函數執行結束後,執行子類構造函數
3.當父類構造函數有參數時,需要子類初始化列表中顯式調用
4.析構函數的調用順序與構造函數剛好相反。
先構造父類,再構造成員變量,最後再構造自己
先析構自己,再析構成員變量,最後析構父類
繼承總結
1、當子類成員變量與父類成員變量同名時
2、子類依然從父類繼承同名成員
3、在子類中通過作用域分辨符::進行同名成員區分(在派生類中使用基
類的同名成員,顯式地使用類名限定符)
4、同名成員存儲在內存中的不同位置
cout<<c.age<<endl;
cout<<c.Parent::age<<endl;
cout<<c.Child::age<<endl;
派生類中的static關鍵字
基類定義的靜態成員,將被所有派生類共享
根據靜態成員自身的訪問特性和派生類的繼承方式,在類層次體系中具 有不同的訪問性質 (遵守派生類的訪問控制)
派生類中訪問靜態成員,用以下形式顯式說明:
類名::成員、對象名 . 成員
靜態成員變量,無論使用父類名,還是子類名都可以直接調用得到。或者是實例,也可以調用。
cout<<c.count<<endl;
cout<<Parent::count;
Parent::count++;
cout<<Child::count;
Child::count++;
cout<<Object::count;
Object::count++;
cout<<Object::count;
多態
一般情況下,如果基類與子類有着同名的方法,即子類重寫了父類的方法。如果使用父類指針去承接子類對象,那麼調用被重寫的方法的時候,是執行父類的方法,而不是子類的方法。
如何實現,根據對象實際類型去打印?virtual 可以解決。通過使用virtual 修飾函數。可以視作是java中的abstract 抽象方法,不過這個virutal方法在是可以有方法體的。
virtual int fun()
多態成立的3個條件。
1.要有繼承
2.要有虛函數重寫
3.要有父類指針(父類引用)指向子類對象。
靜態聯編 動態聯編
靜態聯編,由於程序沒有運行,所以不可能知道父類指針指向的具體對象是父類對象還是子類對象,從程序安全的角度,編譯器假設父類指針只指向父類對象,因此編譯結果爲調用父類的成員函數。
多態是發生在動態聯編,是在程序執行的時候,判斷父類指針應該調用的方法。
虛析構函數
虛析構函數用於指引delete操作符正確析構動態對象。
在手動delete obj時候,如果是父類指針,沒有寫virutal 析構,那麼只會執行父類的析構函數代碼,而不會執行子類的析構函數。
重載 重寫 重定義
函數重載
必須在同一個類中進行,子類無法重載父類的函數,父類同名函數將被名稱覆蓋,重載是在編譯期間根據參數類型和個數決定函數調用
//以下都是重載
void a();
void a(int)
void a(char)
void a(int,char)
函數重寫
必鬚髮生在父類與子類之間 ,並且父類與子類必須有完全相同的原型
使用virtual聲明之後能夠產生多態(如果不使用virutal ,那就叫重定義)
virtual void fun();
-->
void fun();//多態如果沒有virutal 那就是重定義
虛函數表和Vptr指針
1.當類聲明虛函數時,編譯器會生成一個虛函數表
2.虛函數表是一個存儲類成員函數指針 的數據結構
3.虛函數表是 編譯器自動生成和維護的。
4.virtual 成員函數會被編譯進入虛函數表中。
5.存在虛函數的時候,每個對象中都有一個指向虛函數表的指(vptr指針。)
編譯器確定func 是不是虛函數
1)不是,編譯器可直接確定被調用的成員函數(靜態聯編,根據父類類型決定)
2) 是,編譯器根據對象的vptr指針,所指向虛函數表查詢到func 函數並且調用。注意,查找與調用是在運行的時候執行
由於虛函數是在運行的時候動態進行調用。效率低是一個問題。構造函數無法實現多態,父類構造的時候,vptr 指向父類的虛函數表,子類構造時候,指向子類。
base(){
this->print()///調用的是父類的print方法
}
child(){
this->print()//調用的是子類的print 方法
}
純虛函數
virtual 類型 函數名(參數表) = 0;
類似於java 的抽象方法。主要特點是無方法體。子類繼承必須實現,否則子類也是抽象類’
1.含有純虛函數的類,稱爲抽象基類,不可實列化。即不能創建對象,存在 的意義 就是被繼承,提供族類的公共接口。
2,純虛函數只有聲明,沒有實現,被“初始化”爲 0。
3,如果一個類中聲明瞭純虛函數,而在派生類中沒有對該函數定義,則該虛函數在 派生類中仍然爲純虛函數,派生類仍然爲純虛基類。
class Triangle:public Shape{
protected:
double w;
double h;
public:
Triangle(double w,double h):w(w),h(h){}
virtual double getArea() {
return w*h/2;
};
};
class Rectangle:public Triangle{
public:
Rectangle(double w,double h):Triangle(w,h){
}
double getArea(){
//可以調用到父類方法,只不過需要加上作用範圍。
return this->Triangle::getArea()*2;
}
};
C 語言面向接口編程
c 語言通過函數指針實現面向接口編程
關於模板類使用友元重載<<的部分說明
如果單獨把代碼寫在h文件裏面,此時需要注意的細節會比較少,但是如果需要分文件,即使h cpp
如下只是重載了<<運算符。
friend ostream& operator<< <T>(ostream &o,Animal<T> t);
//此時,在h文件頭我們需要做如下聲明
template<class T>
class Animal;
template<class T>
ostream& operator<<(ostream &o,Animal<T> t);
//同時,我們需要在cpp文件裏面完善代碼。
template<class T>
ostream& operator<<(ostream &o,Animal<T> t){
o<<t.t;
return o;
}
//最後,在main裏面,別忘記了,要引入我們的cpp文件。
所以這也是爲什麼我們對於這些模板類的編寫,一般就用一個文件hpp,我們不把代碼分離開,因爲如果分開用戶在使用的時候,還需要手工引入hpp,多麻煩啊。所以乾脆寫在一個文件裏面算了。
一般性結論
friend ostream & operator<< <T> (ostream &os, Complex<T> &c);
//在模板類中 如果有友元重載操作符<<或者>>需要 在 operator<< 和 參數列表之間 //加⼊入
//濫⽤用友元函數,本來可以當成員函數,卻要⽤用友元函數
//如果說是⾮非<< >> 在模板類中當友元函數
//在這個模板類 之前聲明這個函數
friend Complex<T> mySub <T>(Complex<T> &one, Complex<T> &another);
//最終的結論, 模板類 不要輕易寫友元函數, 要寫的 就寫<< 和>> 。
傳說中的hpp
類型轉換
C++風格的類型轉換提供了 4 種類型轉換操作符來應對不同場合的應用。
static_cast 靜態類型轉換。如 int 轉換成 char
reinterpreter_cast 重新解釋類型
dynamic_cast 命名上理解是動態類型轉換。如子類和父類之間的多態類型轉換。 const_cast, 字面上理解就是去 const 屬性。
由於二次編譯,模板類在.h在第一次編譯之後,並沒有最終確定類的具 體實現,只是編譯器的詞法校驗和分析。在第二次確定類的具體實現後,是 在.hpp文件生成的最後的具體類,所以main函數需要引入.hpp文件。
綜上:引入hpp文件一說也是曲線救國之計,所以實現模板方法建議在同 一個文件.h中完成
異常
捕捉萬能異常?
//設置未知異常處理回調函數
set_terminate(my_tm_h);
//typedef void (*terminate_handler)();
//編寫回調函數
void my_tm_h(){
cout<<"error occure with clear reason!"<<endl;
}
統一的異常處理機制,將讓我們捕捉異常的時候,更加方便。能夠進行統一的控制操作。
關於標準輸入輸出流
標準輸入流對象cin
cin.get() //一次只能讀取一個字符
cin.get(一個參數) //讀一個字符
cin.get(三個參數) //可以讀字符串
cin.getline() //獲取緩衝區的一行
cin.ignore() //跳過緩衝區幾個字符
cin.peek() //查看緩衝區有沒有數據,阻塞
cin.putback() //塞進去緩衝區
ctrl-z 將產生一個EOF,mac 測試無效。
標準輸出流對象cout
控制符詳解
控制符,可以直接cout<<控制符,也就是追加
控制符 | 作用 |
---|---|
endl | 不做解釋 |
dec | 設置數字基數爲10進制 |
hex | 16進制 |
oct | 8進制 |
setfill(’*’) | 填充字符,一般與setw 配合使用,一次性用,endl作廢 |
setprecision(n) | 設置浮點數字的輸出精度,多次有效 |
setw(12) | 設置字段寬度,本次有效。僅僅數字不足才補足,一旦設置setprecision將優先填充到precision 的位數 |
setiosflags(ios::a|ios::b…) | 可以設置標誌位 |
resetiosflags(ios::a|ios::b…) | 重置io標誌 |
標誌位作用講解
標誌位 | 作用 |
---|---|
ios::fixed | 有效數字位數默認爲爲6位,可以通過setprecision來定製。 |
ios::scientific | 設置浮點數以科學計數法表示 |
ios::left | 左對齊,僅僅setw ,並且字符串長度超過有效 。 |
ios:right | 右對齊 |
ios::skipws | skipws是作用於流式輸入的,而非輸出。 cin默認是已經把skipws開啓了。 也就是說 a b c 讀入三個字符,不會吧空格讀進去。 |
ios::uppercase | 只有16進制輸出才以大寫輸出 |
ios::lowercase | 小寫輸出 |
ios::showpos | 輸出正數給出“+”號 |
ios::internal | 數值的符號位在域寬左對齊,數值右對齊,中間填充字符 |
ios:unitbuf | 每次輸出後刷新所有的流 |
ios::stdio | 每次輸出後清除stdout,stderr |
流成員函數
如下方法屬於cout成員,使用cout.method進行調用即可。
函數 | 功能 |
---|---|
precision(n) | 設置精度 |
width(n) | 設置字符寬度 |
fill© | 設置填充字符 |
setf() | 設置ios標誌 |
unsetf() | 取消設置ios標誌 |
文件流
引入頭文件
#include <fstream>
文件的輸入輸出方式控制
STL
常見的容器有如下的幾種
vector list deque set map stack queue
string
如何寫一個標準的替換字符串函數
string str="jflkasfijfamsfkajsaf";
string find="j";
string rep="*";
int pos=0;
while((pos=str.find(find,pos))!=-1){
str.replace(pos,find.length(),rep);
pos+=rep.length();
}
vector
deque
與vector 不同在於,deque 可以push_front,即雙端操作。pop_front()可以取除第一個元素
stack
支持push 與pop ,通過top 獲取元素
queue
隊列,pop時候,只會把隊頭元素拋出。支持front 與end
list
不可隨機存儲讀寫,但是支持插入?支持push pop back front
總結一句話,能夠用下標獲取,其底層機構一定是數組,如果無法,但是可以通過不斷的++ ,那他一定是一條鏈路。
各個容器的使用正確時機。
簡單的總結
deque的使用場景:比如排隊購票系統,對排隊者的存儲可以採用deque,支持頭 端的快速移除,尾端的快速添加。如果採用 vector,則頭端移除時,會移動大量的 數據,速度慢。
- vector 與 deque 的比較:
- 一:vector.at()比 deque.at()效率高,比如 vector.at(0)是固定的,deque 的開始位置卻
是不固定的。 - 二:如果有大量釋放操作的話,vector 花的時間更少,這跟二者的內部實現有關。
- 三:deque 支持頭部的快速插入與快速移除,這是 deque 的優點。
- list 的使用場景:比如公交車乘客的存儲,隨時可能有乘客下車,支持頻繁的不確實
位置元素的移除插入。 - set 的使用場景:比如對手機遊戲的個人得分記錄的存儲,存儲要求從高分到低分的
順序排列。 - map 的使用場景:比如按 ID 號存儲十萬個用戶,想要快速要通過 ID 查找對應的用
戶。二叉樹的查找效率,這時就體現出來了。如果是 vector 容器,最壞的情況下可 能要遍歷完整個容器才能找到該用戶。