前言#
這篇文章是對C++的知識點做了一些簡單的總結,基本包含了所有的C++基礎知識點。以下提到的知識點並非深入講解,只是大概講解了各個知識點的基本使用。如需要深入了解,可以針對某個知識點去深入學習。
一、C++常用后綴#
cpp, .h, cc, cxx, hpp
二、頭文件#
1、C++輸入輸出#
- 頭文件#include
- 標准輸入(standard input)與預定義的 istream 對象 cin 對應
- 標准輸出(standard output) 與預定義的 ostream 對象 cout 對應
- 標准出錯(standard error)與預定義的的 ostream 對象 cerr 對應
例子:用c++寫一個簡單計算器
intmain(void) | |
{ | |
int a=0, b=0; | |
char c=0; | |
std::cout<<"please input type: a+b" <<std::endl; | |
std::cin>>a>>c>>b; | |
switch(c) | |
{ | |
case '+': | |
std::cout<<a<<c<<b<<"="<<a+b<<std::endl; break; | |
case '-': | |
std::cout<<a<<c<<b<<"="<<a-b<<std::endl; break; | |
case '*': | |
std::cout<<a<<c<<b<<"="<<a*b<<std::endl; break; | |
case '/': | |
std::cout<<a<<c<<b<<"="<<a/b<<std::endl; break; | |
} | |
return 0; | |
} |
2、在C++中使用C的庫函數#
extern "C" | |
{ | |
} |
三、 指針與動態內存分配#
靜態內存分配(全局變量, 局部變量), 動態內存分配(在 c 中用 malloc 分配的堆空間 free 來釋放)c++中用 new 分配堆空間 delete 釋放。
1、C#
char *name =(char*) malloc(100); | |
free(name); |
2、C++#
- 整形數: int *p = new int(10) ; 分配空間並且初始化為 10 釋放 delete p
- 整形數組:int *arr = new int[10] ; 分配十個連續整數空間 釋放 delete []arr
- 字符型:char *p = new char('a'); 釋放 delete p;
- 字符串:char *arr = new char[100];分配 100 個字符空間 釋放 delete []arr;
四、命名空間#
為了確保程序中的全局實體的名字不會與某些庫中聲明的全局實體名沖突,引入了命名空間。
1、作用#
- 避免名稱沖突;
- 模塊化應用程序;
- 匿名的命名空間可避免產生全局靜態變量,創建的 “匿名” 命名空間只能在創建它的文件中訪問。
2、定義#
除main函數外所有函數, 變量, 類型。
namespace 空間名{ | |
函數,變量, 類型 | |
} | |
例子: | |
namespace class01 | |
{ | |
std::string name="jack"; | |
int age=19; | |
int number = 123; | |
} |
3、使用空間成員#
1、 直接通過空間名::成員名 --標准使用--提倡使用的方法 | |
class01::name -- ::所屬符號 | |
2、using指示符指引 | |
using namespace calss01; // 把class01空間了里面的內容暴露在當前位置,當文件有變量與命名空間的成員一樣時,則后期使用該成員或變量時,程序運行時會報錯;但能編譯通過 | |
3、 using聲明 | |
using class01::number; | |
// 當文件也有number變量時,則編譯的時候就報錯,相當於定義了兩次該變量。 |
4、命名空間嵌套#
namespace AAA{ | |
namespace BBB | |
{ | |
int number=0; | |
} | |
} |
使用:
- AAA::BBB::number;
- using namespace AAA::BBB; number;
- using AAA::BBB::number;
5、匿名空間#
相當於全局變量直接使用(只能在本文中使用)
static。
定義:
namespace { | |
int data; | |
} |
匿名空間與static的異同:
static 無法修飾自定義類型;static 產生當前 符號只在當前源文件有效,是因為它修改了符號的Bind屬性,使之變為局部的;而匿名空間雖然可以產生相同效果,但是符號還是具有外部鏈接屬性。匿名命名空間內的變量與函數,只在當前源文件內有效;不同源文件的匿名命名空間,可以存在同名符合。static需要在每個變量加上
六、引用#
引用:就是某一變量(目標)的一個別名,對引用的操作與對變量直接操作完全一樣。
引用的聲明方法:類型標識符 &引用名=目標變量名;(別名)
int a = 10;
int &ra = a; (ra 就是 a 的引用 ,也稱 a 的別名)
1、引用特點:#
- &在此不是求地址運算符,而是起標識作用。
- 類型標識符是指目標變量的類型。
- 聲明引用時,必須同時對其進行初始化。
- 引用聲明完畢后,相當於目標變量有兩個名稱即該目標原名稱和引用名,且不能再把該引用名作為其他變量名的別名。
- 聲明一個引用,不是新定義了一個變量,它只表示該引用名是目標變量名的一個別名,它本身不是一種數據類型,因此引用本身不占存儲單元,系統也不給引用分配存儲單元,但引用本身是有大小的,一個指針的大小,在64位系統:sizeof(&ra) = 8,32位為4字節,sizeof(ra)=sizeof(a)被引用對象的大小。故:對引用求地址,就是對目標變量求地址。 &ra 與&a 相等。
- 不能建立數組的引用。因為數組是一個由若干個元素所組成的集合,所以無法建立一個
2、引用的應用:#
1.引用作為參數
引用的一個重要作用就是作為函數的參數。以前的 C 語言中函數參數傳遞是值傳遞,如果有大塊數據作為參數傳遞的時候,采用的方案往往是指針,因為這樣可以避免將整塊數據全部壓棧,可以提高程序的效率。但是現在(C++中)又增加了一種同樣有效率的選擇
2.常引用
常引用聲明方式:const 類型標識符 &引用名 = 目標變量名;
用這種方式聲明的引用,不能通過引用對目標變量的值進行修改,從而使引用的目標成為 const,達到了引用的安全性。
3.引用作為函數返回值
要以引用返回函數值,則函數定義時要按以下格式:
類型標識符 &函數名 (形參列表及類型說明){ 函數體 }
特點:
- 以引用返回函數值,定義函數時需要在函數名前加&
- 用引用返回一個函數值的最大好處是,在內存中不產生被返回值的副本。
- 不能返回局部變量的引用
七、函數重載#
函數重載只能在同一個類中
int open();
int open(const char* filename);
int open(const char *filename , int flag);
c++中編譯程序的是檢測函數,通過函數名和參數列表
如果在一個文件中出現同名的函數但參數列表不同,那么這些函數屬於重載
函數重載的依據:
-
函數名相同,
-
參數列表不同,(個數, 類型) add(int, doube) add(double, int)
-
如果參數是指針, 或引用,那么const修飾也可以作為重載依據
-
函數的返回不能作為函數重載的依據
-
參數是否為默認參數不能作為函數重載的依據
八、函數缺省參數(默認參數)#
int open(const char *filename, int flag=10)
int open(const char *filename="c++", int flag=10)
int open(const char *filename="c++", int flag) 錯誤
注意: 若給某一參數設置了默認值,那么在參數表中其后(也就是右邊)所有的參數都必須也設置默認值
九、類與對象#
1、類#
(1)定義:
class 類名{ | |
類的特征(屬性) 成員變量 | |
類的行為(功能) 成員方法, 函數 | |
}; | |
注意:當類里面的成員參數函數有默認值時,若需要在外部定義該函數時,不能寫默認值,默認值只能在類里面聲明的時候寫默認值。 | |
例子: | |
classTdate | |
{ | |
public: | |
voidSet(int m, int d, int y ) | |
{ | |
month = m ; | |
day = d ; | |
year = y ; | |
} | |
private: | |
int month; | |
int day; | |
int year; | |
}; |
struct和class在C++都能定義類,其區別:
- struct作為數據結構的實現體,它默認的數據訪問控制是public的,而class作為對象的實現體,它默認的成員變量訪問控制是private的。
- 默認的繼承訪問權限。struct是public的,class是private的。
(2)類成員權限控制
- public 公有 公有段的成員是提供給外部的接口,在外部可以訪問
- protected 保護 保護段成員在該類和它的派生類中可見,在類外不能訪問(也就是在外部創建對象時不能訪問)
- private 私有 私有段成員僅在類中可見(友元函數或者友元類可以訪問),在類外不能訪問(也就是在外部創建對象時不能訪問)
(3)類內/外訪問
- 類內訪問:在類的成員函數中訪問成員(沒有任何限制)
- 類外訪問: 在類的外部通過類的對象訪問類的成員
2、對象#
定義與成員訪問:
classTdate | |
{ | |
public: | |
int num; | |
voidset(int m, int d, int y ) | |
{ | |
month = m ; | |
day = d ; | |
year = y ; | |
} | |
private: | |
int month; | |
int day; | |
int year; | |
}; | |
//定義對象 | |
Tdate A; | |
Tdate *B = new Tdate( ); | |
//對象成員訪問 | |
A.set(1,1,1); | |
A.num = 2; | |
B->set(1,1,1); | |
B->num = 2; |
十、構造和析構函數#
1、構造函數#
構造函數是成員函數,函數名與類名相同,函數沒有返回值, 函數不需要用戶調用,在創建對象的時候自動調用。
(1)如果創建一個類你沒有寫任何構造函數,則系統會自動生成默認的無參構造函數,函數為空,什么都不做。
(2)只要你寫了一個下面的某一種構造函數,系統就不會再自動生成這樣一個默認的構造函數,如果希望有一個這樣的無參構造函數,則需要自己顯示地寫出來
(3)參數列表初始化:只有構造函數才有參數列表初始化。若要在類內聲明,類外定義構造函數,且使用參數列表初始化參數時,則在類內聲明的時候不允許參數列表初始化,只能類外定義的時候進行參數列表初始化
(4)函數默認參數:無論是成員函數還是構造函數,若需要類內聲明和類外定義的時候,默認參數值在聲明或者定義的時候都可賦值,但聲明和定義的參數不能有默認值
<1>構造函數定義及重載
classComplex | |
{ | |
public: | |
Complex()//構造函數定義 | |
{ | |
m_real = 0.0; | |
m_imag = 0.0; | |
} | |
Complex(int a,int b)//重載構造函數定義 | |
{ | |
m_real = a; | |
m_imag = b; | |
} | |
private : | |
double m_real; | |
double m_imag; | |
}; | |
//創建對象 | |
Complex A;//這個時候不能有() | |
Complex A(1,1); | |
Complex *A = new Complex( );//可以有()也可以沒有 | |
Complex *A = new Complex(1,1); | |
<2>構造函數參數列表初始化
Student(string n="name", int num=1000) | |
:name(n),number(num){ | |
//name = n; | |
//number = num; | |
} | |
注意: | |
* name、number:是本類里面的成員; | |
* n、num:是對成員賦的值或者變量; | |
* 不能在類里面聲明的時候用參數列初始化,聲明的時候可以加默認值; | |
對對象成員進行列表初始化: | |
classA | |
{ | |
public: | |
A(int a,int b){} | |
} | |
classB:public A | |
{ | |
A a; | |
public: | |
B(int c,int d ):a(c,d){} | |
} |
使用原因及用處:
- 構造函數是成員函數,必須創建對象后才能調用
- 參數列表初始化是在申請空間的同時就初始化
- 如果成員是const修飾的成員、引用成員、繼承時候調用父類構造函數,這幾種情況就必須用參數列表初始化。
<3>拷貝構造函數
(1)如果沒有自定義拷貝構造函數,系統會默認生成一個拷貝構造函數(淺拷貝構造函數,不會拷貝堆空間)
Student Jack; //構造函數 | |
Student Rose = Jack; //拷貝構造函數 | |
Student Tom(Jack); //拷貝構造函數 | |
后面兩種拷貝構造函數不會再次調用構造函數 |
(2)深拷貝構造函數
classStudent | |
{ | |
public: | |
Student(int age, const char *n){ | |
this->age = age; | |
this->name = new char[32]; | |
strcpy(this->name, n); | |
cout<<"Student()"<<endl; | |
}//this指針就是函數調用者 | |
~Student(){ | |
delete []this->name; | |
cout<<"~Student()"<<endl; | |
} | |
//深拷貝構造函數 | |
Student(Student& s) | |
{ | |
cout<<"Student(Student&)"<<endl; | |
this->age = s.age; | |
this->name = new char[32]; | |
strcpy(this->name, s.name); | |
//this->name = s.name; | |
} | |
private: | |
int age; | |
char *name; | |
}; |
2、析構函數#
- 函數名有類一樣在函數名前面添加~符號
- 析構函數沒有返回值, 也沒有參數
- 析構函數在對象銷毀的時候自動調用(如果new一個對象,構造函數會自動執行,只有在delete的時候才調用析構函數)
例子:
classComplex | |
{ | |
public: | |
Complex( )//構造函數定義 | |
{ | |
cout << "complex" << endl; | |
} | |
~ Complex( )//析構函數定義 | |
{ | |
cout << "~complex" << endl; | |
} | |
}; | |
intmain( ) | |
{ | |
Complex a; | |
Complex *p = new Complex(); | |
delete p; | |
return 0; | |
} | |
結果: | |
complex | |
complex | |
~complex | |
~complex |
十一、類的內存空間#
類本身是一個數據類型,在沒有定義對象前是不占用內存空間的,定義對象的時候才會分配空間。
- 計算一個類的對象占用多少空間用sizeof(類名或對象)
- 類的對象大小是其數據成員(非靜態數據段),和虛函數表指針(一個類里最多只能有兩個指針,一個是虛函數的指針,一個是虛繼承的指針)大小和。普通方法(普通函數)不占用內存,但用virtual修飾的虛函數占用一個指針大小的內存。注:一個指針的大小、內存的對齊方式和編譯器有關;64位的話,大小為8;32位的話,大小為4。
- 如果一個類中沒有數據成員,也沒有虛表那么這個類的大小規定為 1 個字節。
十二、類繼承#
繼承:
- 新的類從已知的類中得到已有的特征的過程
- 新類叫派生類/子類
- 已知的類叫基類/父類
- 如果直接將派生類的對象賦值給基類對象,派生類自身的成員就會被丟棄,只保留基類繼承來的成員。
- 將基類指針指向派生類對象是安全的,因為派生類對象“是”它的基類的對象。但是要注意的是,這個指針只能用來調用基類的成員函數。
作用:
繼承可以減少重復的代碼。比如父類已經提供的方法,子類可以直接使用,不必再去實現。
類的繼承格式:
class 子類名 :繼承方式 父類 | |
{ | |
子類成員 | |
}; |
例如:
classBase | |
{ | |
public: | |
Base() {} | |
int b; | |
}; | |
classChild: public Base | |
{ | |
}; |
1、繼承方式#
繼承方式: 公有繼承, 保護繼承, 私有繼承
- 公有繼承(public):繼承時保持基類中各成員屬性不變,並且基類中的private成員被隱藏。派生類的成員只能訪問基類中的public/protected成員,而不能訪問private成員;派生類的對象只能訪問基類中的public成員。
- 保護繼承(protected):繼承時基類中各成員屬性均變為protected,並且基類中的private成員被隱藏。派生類的成員只能訪問基類中的public/protected成員,而不能訪問private成員;派生類的對象不能訪問基類中的任何的成員。
- 私有繼承(private):繼承時基類中各成員屬性均變為private,並且基類中private成員被隱藏。派生類的成員也只能訪問基類中的public/protected成員,而不能訪問private成員;派生類的對象不能訪問基類中的任何的成員。
注意1: 私有繼承在下一次被繼承時,所有從父類繼承而來的都會別隱藏,而保護繼承在下次被繼承時根據繼承的屬性其中的數據可能被從新利用,所以私有繼承的保護性更加強。
注意2: 無論那種繼承子類的大小為子類+父類(所有成員都要加起來,包括私有成員)
2、繼承#
(1)繼承構造方法和析構方法的定義和調用
因為有父類才有子類,所以調用順序如下:
構造函數的調用順序父類構造函數—對象成員構造函數—子類構造函數。
析構函數則相反。
注意:
- 當派生類的構造函數為B(){cout << "Afather\n";}時,創建一個派生類默認會調用沒有參數的父類構造函數A()。
- 如果父類構造函數帶無默認值參數,派生類構造函數怎么寫?
如下:
例子一: | |
父類構造函數 | |
public: | |
Person(string name, string sex, int age):name(name),sex(sex),age(age) { | |
cout<<"Person()"<<endl; | |
} | |
子類構造函數 | |
public: | |
Student( ):Person("jack","man",19){ | |
cout<<"Student()"<<endl; | |
} //==>Person(); | |
例子二: | |
classAnimal | |
{ | |
public: | |
Animal(int w, const char *color, int age){ | |
this->weight = w; | |
this->age = age; | |
strcpy(this->color, color); | |
} | |
protected: | |
int weight; | |
int age; | |
char color[32]; | |
}; | |
classCat:public Animal | |
{ | |
public: | |
Cat(int w, const char *color, int age, const char *type):Animal(w, color, age) | |
{ | |
strcpy(this->type, type); | |
} | |
voidshow() | |
{ | |
cout << "weight=" << weight << "\nage=" << age << "\ncolor=" << color <<endl; | |
} | |
protected: | |
char type[32]; | |
}; |
(2)繼承后成員函數調用
- 父子類成員函數名相同時,不是重載,這時父類的此函數會別隱藏
- 子類調用成員函數時候會檢測子類是否存在,如果存在就調用自己的, 如果不存在就調用父類的(前提是父要有這個函數)
- 如果子類和父同時存在這個函數,一定要調用父類函數,可以用(父類名::函數名( ))調用。
例如:
using namespace std; | |
classA{ | |
public: | |
A(){cout << "Afather\n";} | |
~A(){cout << "~Afather\n";} | |
voidfun( ){ | |
cout << "father fun\n"; | |
} | |
}; | |
classB:public A{ | |
public: | |
B(){cout << "Bchildren\n";} | |
~B(){cout << "~~Bchildren\n";} | |
voidfun(){ | |
cout << "children fun\n"; | |
} | |
}; | |
intmain( ) | |
{ | |
B x; | |
x.A::fun( );//調用父類的fun | |
return 0; | |
} | |
輸出結果: | |
Afather | |
Bchildren | |
father fun | |
~~Bchildren | |
~Afather |
3、多繼承#
(1)語法:
class <派生類名>:<繼承方式1> <基類名1>,<繼承方式2><基類名2>,… | |
{ | |
<派生類類體> | |
}; |
(2)例子1:
classA{ | |
public: | |
A(){cout<<"A()"<<endl;} | |
~A(){cout<<"~A()"<<endl;} | |
protected: | |
int dataA; | |
}; | |
classB{ | |
public: | |
B(){cout<<"B()"<<endl;} | |
~B(){cout<<"~B()"<<endl;} | |
protected: | |
int dataB; | |
}; | |
classC:public A, public B | |
{ | |
public: | |
C(){cout<<"C()"<<endl;} | |
~C(){cout<<"~C()"<<endl;} | |
protected: | |
int dataC; | |
}; |
注意:創建子類對象構造順序 A->B->C
如果改為:class C:public B, public A,創建子類對象構造順序 B->A->C
(3)例子2: 如果父類構造函數帶參數
繼承關系classC:public A, public B | |
父類帶參數 | |
A(int a):dataA(a){cout<<"A()"<<endl;} | |
B(int b):dataB(b){cout<<"B()"<<endl;} | |
C(int a, int b, int c):A(a), B(b),dataC(c){cout<<"C()"<<endl;} |
(4)多個父類有同名的成員
多個父類有同名的成員, 在子類中訪問會出現歧義
- 顯示調用對應父類中的成員
c.A::info();
c.B::info(); - 在子類中添加父類的同名成員
這個時候系統會將父類的同名成員隱藏,調用子類的成員
4、虛擬繼承#
語法:
classD :virtual public B{ //虛擬繼承 | |
... | |
}; |
多繼承中多級繼承時候多個父類同時繼承同一個基類出現二義性問題--用虛擬繼承解決。
例如:
classA{ | |
public: | |
voidfun(){} | |
}; | |
classB:virtual public A{ }; | |
classC:virtual public A{ }; | |
classD:public B,public C{ }; | |
intmain(void) | |
{ | |
D x; | |
x.fun();//如果不是虛繼承,會出現二異性,因為在D類繼承了兩次A類 | |
return 0; | |
} |
十三、虛函數、虛表#
定義: 在類的成員函數聲明前面添加virtual
virtualvoidshow(){cout<<data<<endl;}
- 如果一個類中包含虛函數, 那么這個類的對象中會包含一個虛表指針vptr
- 虛表指針保存在對象空間的最前面
- 虛表中存儲的是類中的虛函數地址
- 對象調用類中虛函數,會查詢虛表指針再執行函數
- 一個類里最多只有兩個虛表指針(一個是虛函數的指針,一個是虛繼承的指針)
- 用virtual修飾的虛函數占用一個指針大小的內存。64位的話,大小為8;32位的話,大小為4。
- 同一個類的不同實例共用同一份虛函數表, 它們都通過一個所謂的虛函數表指針__vfptr(定義為void**類型)指向該虛函數表.
例子1:觀察輸出的最后結果是什么(一定要看)
using namespace std; | |
classBase | |
{ | |
public: | |
Base(){} | |
virtual ~Base(){} | |
public: | |
virtualvoidshow(int a=123){ | |
cout<<"Base::show()"<<a<<endl; | |
} | |
}; | |
classChild:public Base | |
{ | |
public: | |
Child(){} | |
~Child(){} | |
virtualvoidshow(int a=321){ | |
cout<<"Child::show()"<<a<<endl; | |
} | |
virtualvoidinfo() | |
{ | |
cout<<"Child::info()"<<endl; | |
} | |
}; | |
intmain() | |
{ | |
Child c; | |
Base *p = &c; | |
p->show(); | |
return 0; | |
} | |
結果: | |
Child::show()123 | |
注意: | |
(1)當show函數不是虛繼承時,輸出結果為Base::show()123,因為父類的指針只能調用自己的成員,如果有虛繼承,則虛表里面父類的show函數的地址會被子類的show函數地址覆蓋,被覆蓋的前提是:兩個函數的名稱和參數類型、個數和返回值類型一樣。 |
例子2:通過指針調用虛表中的虛函數(在ubuntu下運行,虛表地址通過qt調試查看)
using namespace std; | |
classBase | |
{ | |
public: | |
Base(){} | |
virtual ~Base(){} | |
protected: | |
virtualvoidshow(int a= 0){ | |
cout<<"Base::show()"<<endl; | |
} | |
}; | |
classChild:public Base | |
{ | |
public: | |
Child(){} | |
~Child(){} | |
virtualvoidshow(){ | |
cout<<"Child::show()"<<endl; | |
} | |
virtualvoidinfo() | |
{ | |
cout<<"Child::info()"<<endl; | |
} | |
}; | |
intmain() | |
{ | |
Child c; | |
typedefvoid(*Fun)(); | |
c.show(); | |
Fun f = (Fun)(((long*)(*((long*)(&c))))[2]); | |
f(); | |
return 0; | |
} | |
結果: | |
Child::show() | |
Base::show() |
十四、純虛函數(抽象函數)、抽象類#
(1)純虛函數--虛函數不需要實現直接賦值為0,純虛函數有時稱為抽象函數。
定義:
virtualvoidrun()=0;
(2)抽象類
- 如果一個類中包含純虛函數,那么這個就是抽象類,抽象類是不能創建對象。
- 抽象類可以派生出子類, 如果在子類中沒有把父類中的純虛函數全部實現,那么子類照樣是抽象類。
例子:線程獲取時間
using namespace std; | |
classThread{ | |
public: | |
Thread(){} | |
~Thread(){} | |
voidstart(); | |
virtualvoidrun()=0; | |
protected: | |
pthread_t id; | |
}; | |
void *handle(void *arg) | |
{ | |
Thread* th = (Thread*)arg; | |
th->run(); | |
} | |
voidThread::start() | |
{ | |
int ret = pthread_create(&id, NULL, handle, (void*)this); | |
if(ret < 0) | |
{ | |
cout<<"create fail"<<endl; | |
} | |
} | |
//派生一個線程子類--獲取系統時間 | |
classTimeThread: public Thread{ | |
public: | |
virtualvoidrun() | |
{ | |
while(1) | |
{ | |
cout<<"TimeThread::run()"<<endl; | |
Sleep(1000); | |
time_t t; | |
time(&t); | |
cout<<pthread_self()<<"-----------"<<ctime(&t)<<endl; | |
} | |
} | |
}; | |
intmain() | |
{ | |
TimeThread tth; | |
tth.start(); | |
TimeThread tt; | |
tt.start(); | |
while(1){} | |
return 0; | |
} |
十五、多態、虛析構#
(1)多態#
<1>概念
C++中,多態性是指具有不同功能的函數可以用同一個函數名,這樣就可以用一個函數名調用不同內容的函數。
在面向對象方法中一般是這樣表述多態性的:向不同的對象發送同一消息(調用函數),不同的對象在接收時會產生不同的行為(即方法,不同的實現,即執行不同的函數)。可以說多態性是“一個接口,多種方法”。
多態性分為兩類:
(1)靜態多態性:在程序編譯時系統就能決定調用的是哪個函數,因此又稱為編譯時的多態性,通過函數的重載實現(運算符重載實際上也是函數重載);
(2)動態多態性:在程序運行過程中才動態地確定操作所針對的對象,又稱為運行時多態性,通過虛函數實現。
區別:函數重載是同一層次上的同名函數(首部不同,即參數個數或類型不同),虛函數是不同層次上的同名函數(首部相同)。
<2>動態多態性和虛函數
父類引用(指針變量)指向子類對象時,調用的方法仍然是父類中的方法。如果將父類中的該方法定義為virtual,則調用的方法就是子類中的方法了。
說明:本來,父類指針是用來指向父類對象的,如果指向子類對象,則進行類型轉換,將子類對象的指針轉為父類的指針,所以父類指針指向的是子類對象中的父類部分,也就無法通過父類指針去調用子類對象中的成員函數。但是,虛函數可以突破這一限制!如果不使用虛函數,企圖通過父類指針調用子類的非虛函數是絕對不行的!
注意:父類中非虛函數被子類重寫后,父類指針調用的是父類的成員函數,子類指針調用的是子類中的成員函數,這並不是多態!因為沒有用到虛函數!
using namespace std; | |
classPerson{ | |
public: | |
Person(){} | |
~Person(){} | |
virtualvoidwork(){cout<<"Person::work()"<<endl;} | |
protected: | |
int data; | |
}; | |
classChildPerson: public Person{ | |
public: | |
ChildPerson(){} | |
~ChildPerson(){} | |
virtualvoidshow(){cout<<"ChlidPerson::show()"<<endl;} | |
virtualvoidwork(){cout<<"ChildPerson::work()"<<endl;} | |
virtualvoidinfo(){} | |
protected: | |
int number; | |
}; | |
classA: public Person{ | |
public: | |
A(){} | |
~A(){} | |
virtualvoidshow(){cout<<"A::show()"<<endl;} | |
virtualvoidwork(){cout<<"A::work()"<<endl;} | |
virtualvoidinfo(){} | |
protected: | |
int num; | |
}; | |
intmain() | |
{ | |
ChildPerson cp; | |
A a; | |
Person* p = &a; | |
Person* pson = &cp; | |
pson->work(); //ChildPerson::work(); | |
p->work();//A:work(); | |
return 0; | |
} |
(2)虛析構#
多態的時候,用父類指針指向子類對象, 在delete 父類指針的時候默認只會調用父類析構函數,子類析構函數沒有執行(可能會導致子類的內存泄漏)--通過設置父類析構函數為虛函數類解決,執行子類析構函數后,自動執行父類析構函數。
例如:
using namespace std; | |
classBase | |
{ | |
public: | |
Base(){cout<<"create Base"<<endl;} | |
virtual ~Base(){cout<<"delete Base"<<endl;} | |
}; | |
classDer : public Base | |
{ | |
public: | |
Der(){cout<<"create Der"<<endl;} | |
~Der(){cout<<"Delete Der"<<endl;} | |
}; | |
intmain(int argc, charconst* argv[]) | |
{ | |
Base *b = new Der; | |
delete b; | |
return 0; | |
} |
十六、友元#
友元:是c++里面一個特性,為了解決在函數中可以訪問類的私有,或保護成員。
友元函數是可以直接訪問類的私有成員的非成員函數。它是定義在類外的普通函數,它不屬於任何類。
- 友元優點: 可以在函數中直接訪問成員數據, 可以適當提高程序效率
- 友元缺點:在函數類的權限失效, 破壞了類的封裝性
friend關鍵聲明友元函數,或類。
第一種定義情況:類外定義:例如
classData | |
{ | |
public: | |
Data() {} | |
voidsetA(int a) | |
{ | |
this->a = a; | |
} | |
protected: | |
int a; | |
private: | |
int b; | |
//在Data類中聲明函數fun為友元函數 | |
friendvoidfun(); | |
}; | |
voidfun() | |
{ | |
Data data; | |
//data.a = 120; | |
data.setA(120); | |
data.b = 220; | |
} | |
友元聲明只放類內部聲明, 可以放在類內任意位置 |
第二種定義情況:類內定義例如:
classData | |
{ | |
public: | |
Data() {} | |
voidsetA(int a) | |
{ | |
this->a = a; | |
} | |
protected: | |
int a; | |
private: | |
int b; | |
//在Data類中聲明函數fun為友元函數 | |
friendvoidfun(); | |
//聲明show為友元函數,show不是成員函數 | |
friendvoidshow() | |
{ | |
Data data; | |
data.a = 130; | |
cout<<data.a<<" "<<data.b<<endl; | |
} | |
}; | |
voidshow(); //在外面聲明函數 |
十七、友元類#
在一個類中的成員函數可以訪問另外一個類中的所有成員比如在 A 類中的成員函數可以訪問 B 類中的所有成員。有兩種方法如下:
(1)在 B 類中設置 A 類為友元類。
(2)A::fun 要訪問B類中的所有成員, 把A::fun函數聲明為B類的友元函數。
(1)例如:在 B 類中設置 A 類為友元類,A類成員函數可以訪問B類的protected、private成員,B類不能訪問A類,如果要雙向訪問則要在兩個類中聲明對方為友元類。友元關系不能被繼承。
classB | |
{ | |
public: | |
B(){} | |
friend classA;//在 B 類中聲明 A 類為友元類 | |
private: | |
int bdata; | |
}; | |
classA | |
{ | |
public: | |
A(){} | |
voidshowB(B &b) | |
{ | |
b.bdata = 100;//在 A 類中成員函數使用 B 類的私有數據 | |
} | |
private: | |
int adata; | |
}; |
(2)A::fun 要訪問B類中的所有成員, 把A::fun函數聲明為B類的友元函數
using namespace std; | |
//前向聲明----只能用於函數形參, 定義指針, 引用,不能使用類具體成員 | |
classB; | |
classA{ | |
public: | |
voidfun(B& b); | |
}; | |
classB{ | |
public: | |
B(){} | |
protected: | |
int mb; | |
private: | |
int nb; | |
friendvoidA::fun(B& b);//在B類中聲明fun為友元函數 | |
}; | |
voidA::fun(B& b){ | |
cout<<b.mb<<b.nb<<endl; | |
} | |
intmain() | |
{ | |
cout << "Hello World!" << endl; | |
return 0; | |
} |
十八、運算符重載#
運算符重載 關鍵子函數operator
- 根據實際應用需求來重載運算符, 重載的時候必須保持不能改變運算符本來的特性
- 只能重載c++已有的運算符,不能自己新創建運算符
1、那些運算能重載
- 雙面運算符 (+,-,*,/, %)
- 關系運算符 (==, !=, <, >, <=, >=)
- 邏輯運算符 (||, &&, !)
- 單目運算符 (*, &, ++, --)
- 位運算符 (|, &, ~, ^, <<, >>)
- 賦值運算符 (=, +=, -=, .....)
- 空間申請運算符 (new , delete)
- 其他運算符 ((), ->, [])
2、那些運算符不能重載
- .(成員訪問運算符)
- .*(成員指針訪問運算符)
- ::(域運算符)
- sizeof(數據類型長度運算符)
- ?:(條件運算符, 三目運算符)
3、格式:
返回類型說明符 operator 運算符符號(<參數表>) | |
{ | |
函數體 | |
} |
4、重載方式:
1.重載方式---成員函數重載
Complex C = A+B; --》A.operator+(B); | |
規定:左值是函數調用者, 右值函數的參數 |
2.重載方式--友元重載(普通函數重載)(可以在類里面定義,也可以在類外定義類內聲明)
Complex C = A-B; -->operator-(A, B); | |
規定:左值為第一個參數, 右值為第二個參數 |
1、雙目運算符重載#
(1)+ -重載
using namespace std; | |
classComplex | |
{ | |
public: | |
Complex(int r, int i):real(r), image(i) {} | |
//成員重載實現重載加法+ | |
Complex operator+ (Complex &B) | |
{ | |
cout<<"operator"<<endl; | |
Complex C(0,0); | |
C.real = this->real + B.real; | |
C.image = this->image + B.image; | |
return C; | |
} | |
//成員重載實現 對象加一個整型數 | |
int operator+(const int &a) | |
{ | |
this->real += a; | |
this->image += a; | |
return this->real; | |
} | |
private: | |
int real; | |
int image; | |
friend Complex operator- (Complex &A, Complex &B); | |
}; | |
//友元重載實現重載減法 | |
Complex operator- (Complex &A, Complex &B) | |
{ | |
Complex C(0,0); | |
C.real = A.real - B.real; | |
C.image = A.image - B.image; | |
return C; | |
} | |
intmain() | |
{ | |
Complex A(2,2); | |
Complex B(1,1); | |
Complex C = A+B; //==>A.operator+(B) | |
int c = A+100; //==>A.operator+(100) | |
Complex D = A-B; //operator-(A, B) | |
return 0; | |
} |
(2)輸出、輸入, 運算符重載
using namespace std; | |
classPoint | |
{ | |
public: | |
Point (int x=0, int y=0):x(x),y(y){} | |
voidshow() | |
{ | |
cout<<"("<<x<<","<<y<<")"<<endl; | |
} | |
private: | |
int x, y; | |
//聲明友元函數 | |
friend ostream& operator<<(ostream &out, Point& p); | |
friend istream& operator>>(istream &in, Point& p); | |
}; | |
//重載輸出 | |
ostream& operator<<(ostream &out, Point& p) | |
{ | |
out<<"("<<p.x<<","<<p.y<<")"<<endl; | |
return out; | |
} | |
//輸入重載 | |
istream& operator>>(istream &in, Point& p) | |
{ | |
in>>p.x>>p.y; | |
return in; | |
} | |
intmain() | |
{ | |
Point p(10,20); | |
p.show(); | |
cout<<p<<endl; //==> ostream& operator<<(cout, p) | |
Point A(0,0); | |
cin>>A; | |
cout<<A; | |
return 0; | |
} |
3、單目運算符重載#
(1)++A、A++重載
成員函數重載
using namespace std; | |
classData | |
{ | |
public: | |
Data(int d=0):data(d) {} | |
//重載A++ | |
Data operator++(int) | |
{ | |
Data old(*this);//保存原先的數捍 | |
this->data += 1;//對原數進行自劍 | |
return old;//返回未加之前的數捍 | |
} | |
//重載++A | |
Data& operator++() | |
{ | |
this->data += 1;//對原數進行自劍 | |
return *this; | |
} | |
private: | |
int data; | |
friend ostream &operator<<(ostream& out, Data &d); | |
}; | |
ostream &operator<<(ostream& out, Data &d) | |
{ | |
out<<d.data<<endl; | |
return out; | |
} | |
intmain() | |
{ | |
Data A; | |
Data d = A++; //==>A.operator++(int) | |
cout<<d<<A<<endl; | |
Data &c = ++A; | |
cout<<c<<A<<endl; | |
return 0; | |
} |
友元函數重載
using namespace std; | |
classA | |
{ | |
int data; | |
public: | |
A(int d = 0):data(d) {} | |
voidshow() | |
{ | |
cout << this->data << endl; | |
} | |
//友元函數重載++A | |
friend A& operator++ (A &a); | |
//友元函數重載A++ | |
friend A operator++ (A &b,int); | |
//友元函數重載<< | |
friend ostream& operator<< (ostream &out,A &a); | |
}; | |
//友元函數重載++A | |
A& operator++ (A &a) | |
{ | |
a.data += 1; | |
return a; | |
} | |
//友元函數重載A++ | |
A operator++ (A &b,int) | |
{ | |
A old(b); | |
b.data += 1; | |
return old; | |
} | |
//友元函數重載<< | |
ostream& operator<< (ostream &out,A &a) | |
{ | |
out << a.data; | |
return out; | |
} | |
intmain(int argc,char **argv) | |
{ | |
A a(5); | |
A b = ++a; | |
cout << a << " " << b << endl; | |
A c(5); | |
A d = c++; | |
cout << c << " " << d << endl; | |
return 0; | |
} |
(2)重載中括號[ ]
using namespace std; | |
classArray | |
{ | |
public: | |
Array(int n):length(n) { | |
this->ptr = new int[this->length]; | |
} | |
~Array(){ | |
delete []this->ptr; | |
} | |
//拷貝構造函數---深拷貝(類的成員有指針指向堆空間) | |
Array(Array& array) | |
{ | |
this->length = array.length; | |
this->ptr = new int[this->length]; | |
memcpy(this->ptr, array.ptr , this->length); | |
} | |
//重載[] | |
int& operator[](int i) | |
{ | |
cout<<i<<endl; | |
return this->ptr[i];//返回第i個對象 | |
} | |
private: | |
int length; | |
int *ptr; | |
}; | |
intmain() | |
{ | |
Array mArr(10); | |
mArr[0] = 100; // | |
return 0; | |
} |
4、用運算符重載實現數據類型轉換#
(1)轉換構造函數
轉換構造函數的作用:是將一個其他類型的數據轉換成一個類的對象。 當一個構造函數只有一個參數,而且該參數又不是本類的const引用時,這種構造函數稱為轉換構造函數。 轉換構造函數是對構造函數的重載。
例如:
using namespace std; | |
classComplex | |
{ | |
public: | |
Complex():real(0),imag(0){cout << "test1\n";} | |
Complex(double r, double i):real(r),imag(i){cout << "test2\n";} | |
// 定義轉換構造函數 | |
Complex(double r):real(r),imag(0){cout << "test3\n";} | |
/* | |
// 拷貝構造函數 | |
Complex(Complex &a){ cout << "test4\n"; }//當此函數存在時,Complex c = 1;Complex c2 = c1 + 3.1;編譯時都會報錯 | |
*/ | |
voidPrint(){ | |
cout<<"real = " << real <<" image = "<<imag<<endl; | |
} | |
Complex operator+(Complex c){ | |
Complex ret(this->real + c.real, this->imag + c.imag); | |
return ret; | |
} | |
private: | |
double real; | |
double imag; | |
}; | |
intmain() | |
{ | |
Complex c; | |
c = 4; // 調用轉換構造函數將1.2轉換為Complex類型,此時會調用轉換構造函數 | |
c.Print(); | |
Complex c1(2.9, 4.2); | |
Complex c2 = c1 + 3.1; // 調用轉換構造函數將3.1轉換為Complex類型 | |
c2.Print(); | |
return 0; | |
} | |
輸出結果: | |
test1 | |
test3 | |
real = 4 image = 0 | |
test2 | |
test3 | |
test2 | |
real = 6 image = 4.2 |
注意:
- 1、用轉換構造函數可以將一個指定類型的數據轉換為類的對象。但是不能反過來將一個類的對象轉換為一個其他類型的數據(例如將一個Complex類對象轉換成double類型數據)。
- 2、如果不想讓轉換構造函數生效,也就是拒絕其它類型通過轉換構造函數轉換為本類型,可以在轉換構造函數前面加上explicit。
(2)用運算符重載實現數據類型轉換
用轉換構造函數可以將一個指定類型的數據轉換為類的對象。但是不能反過來將一個類的對象轉換為一個其他類型的數據(例如將一個Complex類對象轉換成double類型數據)。而類型轉換函數就是專門用來解決這個問題的!
類型轉換函數的作用是將一個類的對象轉換成另一類型的數據。
using namespace std; | |
classPerson | |
{ | |
public: | |
Person(int age=0):age(age) {} | |
operatorint()//通過運算符重載來實現數據類型轉換 | |
{ | |
cout<<"int"<<endl; | |
return age; | |
} | |
operatorlong()//通過運算符重載來實現數據類型轉換 | |
{ | |
cout<<"long"<<endl; | |
return num; | |
} | |
intgetAge(){return age;} | |
private: | |
int age; | |
long num; | |
}; | |
intmain() | |
{ | |
Person Jack(19); | |
int age = Jack; | |
int a = Jack.getAge(); | |
long b = Jack; | |
return 0; | |
} |
注意:
- 1、在函數名前面不能指定函數類型,函數沒有參數。
- 2、其返回值的類型是由函數名中指定的類型名來確定的。
- 3、類型轉換函數只能作為成員函數,因為轉換的主體是本類的對象,不能作為友元函數或普通函數。
- 4、從函數形式可以看到,它與運算符重載函數相似,都是用關鍵字operator開頭,只是被重載的是類型名。double類型經過重載后,除了原有的含義外,還獲得新的含義(將一個Complex類對象轉換為double類型數據,並指定了轉換方法)。這樣,編譯系統不僅能識別原有的double型數據,而且還會把Complex類對象作為double型數據處理。
十九、模板函數#
1、概念: 如果一個函數實現的功能類似,但是函數參數個數相同類型不同,這樣就可以把實在該功能的函數設計為模板函數。
2、格式:
template <typename T> //T為類型名 | |
數據類型 函數名(參數列表){ | |
函數體 | |
} |
3、注意:
- (1)在編譯時,根據變量生成實例。
- (2)template T只對其下面的函數模板有效。如果要定義第二個模板函數時,則要再寫template 。
- (3)typename也可以用class。
- (4)T名字可以隨便取。
- (5)當參數不一樣時,可以這樣定義參數列表template <class T,class Tp>
- (6)參數列表可以帶默認類型,template <class T,class Tp = int>。
- (7)模板函數只有在使用(不是運行)的時候才會檢測錯誤。
例子1:
//設計一個模板函數實現兩個對象交換 | |
template <typename T> | |
voidmswap(T &a, T &b) | |
{ | |
T c = a; | |
a = b; | |
b = c; | |
} | |
intmain() | |
{ | |
int a = 10; | |
int b = 20; | |
cout<<a<<" "<<b<<endl; | |
mswap(a, b); | |
cout<<a<<" "<<b<<endl; | |
return 0; |
例子2:
//錯誤,因為不能夠確定返回值的類型 | |
template <classT, classTp> | |
Tp fun(T &a) | |
{ | |
return a; | |
} | |
//修改,但返回值定死了 | |
template <classT, classTp = int> | |
Tp fun(T &a) | |
{ | |
return a; | |
} | |
//調用函數時指定類型 | |
template <classT, classTp = int> | |
Tp fun(T &a) | |
{ | |
return a; | |
} | |
intmain(void) | |
{ | |
int a = 2; | |
double ret = fun<int, double>(a) | |
} |
4、模板函數與函數普通同時存在該如何調用
template <typename T> | |
voidmswap(T &a, T &b) | |
{ | |
cout<<"template"<<endl; | |
T c = a; | |
a = b; | |
b = c; | |
} | |
//普通函數 | |
voidmswap(int &a, int &b) | |
{ | |
cout<<"std"<<endl; | |
int c = a; | |
a = b; | |
b = c; | |
} | |
調用(1) | |
int a = 10; | |
int b = 20; | |
cout<<a<<" "<<b<<endl; | |
mswap(a, b);//---普通函數 | |
cout<<a<<" "<<b<<endl; | |
調用(2) | |
double a = 10; | |
double b = 20; | |
cout<<a<<" "<<b<<endl; | |
mswap(a, b);//---模板函數 | |
cout<<a<<" "<<b<<endl; | |
如果模板函數和普通函數同時存在, 調用的時候會根據參數選擇最優函數 |
二十、模板類#
1、模板類的定義#
//設計一個模板類 -模板類的類名 A<T> | |
//template< class T , class Ty> //A<T, Ty> | |
template< classT > //A<T> | |
classA | |
{ | |
public: | |
A() {} | |
protected: | |
T dataA; | |
}; | |
intmain() | |
{ | |
A<int> a;//定義模板類對象 | |
return 0; | |
} |
注意:
- (1)如果是浮點型或者其他普通類型, 是指針或者是引用 template <double &N,class T=int>
class array{...}
定義對象: array<N,int> a ;這里的 N 必須是全局變量。 - (2)參數列表可以帶默認類型,template <class T,class Tp = int> , 如果是默認類型, 與函數的默認參數類似, 必須是如果從那個一個開始默認, 那么后面的所有模板類型多必須有默認類型。
- (3)如果使用數值為整型( char, short, int, long) 時候。template <int N,class T=int> class array{...},這里的N只能是常量不能是變量,例如 array<10,int> a或者const int a = 5; array<a,int>。
2、模板類友元重載輸出#
例如: 用模板類設計一個順序表(數組)
using namespace std; | |
template< classT > | |
classMVector{ | |
public: | |
MVector(){ | |
this->size = 1024; | |
this->count = 0; | |
this->ptr = new T[this->size]; | |
} | |
~MVector(){ | |
delete []this->ptr; | |
} | |
//拷貝構造函數 | |
MVector(MVector& mv){ | |
this->size = mv.size; | |
this->ptr = new T[this->size]; | |
memcpy(this->ptr, mv.ptr, this->size*sizeof(T)); | |
} | |
//添加數據 | |
voidappend(const T &data){ | |
this->ptr[this->count] = data; | |
this->count++; | |
} | |
//重載<<追加數據 | |
void operator<<(int data) | |
{ | |
this->ptr[this->count] = data; | |
this->count++; | |
} | |
//聲明友元重載輸出<< | |
friend ostream& operator<<(ostream& out, MVector &mv) | |
{ | |
for(int i=0; i<mv.count; i++) | |
{ | |
out<<mv.ptr[i]<<" "; | |
} | |
out<<endl; | |
return out; | |
} | |
//template<class Ty> | |
//friend ostream& operator<<(ostream& out, MVector<Ty> &mv); | |
protected: | |
int count; | |
int size; | |
T* ptr; | |
}; | |
//重載輸出<<運算符 | |
template< classTy > | |
ostream& operator<<(ostream& out, MVector<Ty> &mv) | |
{ | |
for(int i=0; i<mv.count; i++) | |
{ | |
out<<mv.ptr[i]<<" "; | |
} | |
out<<endl; | |
return out; | |
} | |
//模板函數在使用(不是運行)該函數的時候才會檢查語法 | |
intmain() | |
{ | |
MVector<int> mvs; | |
mvs.append(100); | |
mvs<<200; | |
cout<<mvs; | |
return 0; | |
} |
3、模板類繼承#
如果在派生子類的時候父類類沒有確定class B: public A,那么子類也是模板類。
例如:
using namespace std; | |
//設計一個模板類A<T> | |
template< classT > | |
classA | |
{ | |
public: | |
A(T a) {} | |
protected: | |
T data; | |
}; | |
//設計一個子類B 繼承模板類A<T> --B類也是模板類 | |
template< classT > | |
classB: public A<T> | |
{ | |
public: | |
B(T a):A<T>(a){} | |
protected: | |
int datab; | |
}; | |
//設計一個子類C 繼承模板類A<int> --C類就是一個具體類 | |
classC: public A<int> | |
{ | |
public: | |
C():A<int>(10){} | |
}; | |
intmain() | |
{ | |
A<char> a(10); //模板類創建對象 | |
B<string> b("hello"); //模板類子類創建對象 | |
C c; | |
return 0; | |
} |
4、模板類中的靜態成員#
編譯時根據模板生成的不同類的靜態成員是不同內存空間的;在同一個類中創建的對象的靜態成員是共用一個內存空間的。
如下:
using namespace std; | |
template<classT> | |
classData | |
{ | |
public: | |
Data() {} | |
voidshow(T msg) | |
{ | |
data = msg; | |
cout<<data<<endl; | |
} | |
public: | |
static T data; | |
}; | |
//類外初始化靜態成員 | |
template<classT> | |
T Data<T>::data ; | |
intmain() | |
{ | |
//創建一個對象 | |
Data<int> mydata; //編譯的時候會生成一個 T為int的類 | |
mydata.show(100); | |
Data<string>::data = "hello"; //編譯的時候會生成一個T 為string的類 | |
cout<<Data<string>::data<<endl; | |
Data<string> mystr; | |
cout<<mystr.data<<endl; | |
return 0; | |
} |
二十一、強制類型轉換const_cast、static_cast、reinterpert_cast、dynamic_cast#
注意:以上,如果轉換失敗的時候會返回空
1、const_cast把常量轉為變量#
using namespace std; | |
intmain() | |
{ | |
const int a = 10; | |
const int *p = &a; | |
int *ptr = (int*)(&a);//c語言轉換(在c語言可以這樣寫:int *ptr=&a,只是會警告,一樣可以操作,c++不允許) | |
*ptr = 1; | |
cout<<a<<endl; | |
cout<<*ptr<<endl; | |
int &ra = const_cast<int&>(a); | |
ra = 2; | |
cout<<a<<endl; | |
cout<<ra<<endl; | |
int *x = const_cast<int*>(p); | |
*x = 3; | |
cout<<a<<endl; | |
cout<<*x<<endl; | |
return 0; | |
} | |
輸出結果: | |
10 | |
1 | |
10 | |
2 | |
10 | |
3 | |
解釋:因為a是const修飾的,此時a的值會存在符號表中,也就是改變a地址所指向的值,也不會改變a的值,當調用a的時候,編譯器回到符號表中取值,而不是從a的地址取值。 |
(1)為何要去除const限定
原因(1)是,我們可能調用了一個參數不是const的函數,而我們要傳進去的實際參數確實const的,但是我們知道這個函數是不會對參數做修改的。於是我們就需要使用const_cast去除const限定,以便函數能夠接受這個實際參數。
例如:
using namespace std; | |
voidPrinter(int* val,string seperator = "\n") | |
{ | |
cout << val<< seperator; | |
} | |
intmain(void) | |
{ | |
const int consatant = 20; | |
//Printer(consatant);//Error: invalid conversion from 'int' to 'int*' | |
Printer(const_cast<int *>(&consatant)); | |
return 0; | |
} |
原因(2):
還有一種我能想到的原因,是出現在const對象想調用自身的非const方法的時候,因為在類定義中,const也可以作為函數重載的一個標示符。
2、static_cast靜態轉化#
static_cast < type-id > ( expression )該運算符把expression轉換為type-id類型,但沒有運行時類型檢查來保證轉換的安全性。它主要有如下幾種用法:
- ①用於類層次結構中基類(父類)和派生類(子類)之間指針或引用的轉換,不允許不相關的類進行轉換。
進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;
進行下行轉換(把基類指針或引用轉換成派生類表示)時,由於沒有動態類型檢查,所以是不安全的。 - ②用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。
- ③把空指針轉換成目標類型的空指針。
- ④把任何類型的表達式轉換成void類型。
注意: static_cast不能轉換掉expression的const、volatile、或者__unaligned屬性
例如
using namespace std; | |
intmain() | |
{ | |
char a = 'a'; | |
int b = (int)a; | |
double g = static_cast<int>(a); | |
//為什么不能轉換普通類型指針,卻能轉換對象指針和void指針(規定的) | |
void *pp; | |
double *pp1 = static_cast <double*>(pp); | |
int *xx; | |
void *xx1 = static_cast <void*>(xx); | |
//double *xx2 = static_cast <double*>(xx);//錯誤寫法 | |
return 0; | |
} |
3、reinterpret_cast強制類型轉換符#
reinterpret_cast (expression)
type-id 必須是一個指針、引用、算術類型、函數指針或者成員指針。它可以把一個指針轉換成一個整數,也可以把一個整數轉換成一個指針(先把一個指針轉換成一個整數,再把該整數轉換成原類型的指針,還可以得到原先的指針值)。
- reinterpret_cast可以轉換任意一個32bit整數,包括所有的指針和整數。可以把任何整數轉成指針,也可以把任何指針轉成整數,以及把指針轉化為任意類型的指針。但不能將非32bit的實例轉成指針。總之,只要是32bit的東東,怎么轉都行!
- 因為任何指針可以被轉換到void,而void可以被向后轉換到任何指針(對於static_cast<> 和 reinterpret_cast<>轉換都可以這樣做),如果沒有小心處理的話錯誤可能發生。
例如1:
using namespace std; | |
classA { | |
public: | |
int m_a; | |
}; | |
classB { | |
public: | |
int m_b; | |
}; | |
classC : public A, public B {}; | |
intmain() | |
{ | |
int n= 1231651 ; | |
double *d; | |
cout << d << endl; | |
d=reinterpret_cast<double*> (&n); | |
//為什么d和n的地址一樣但為什么地址里面的值不一樣? | |
//是因為double類型數據存儲的方式不一樣,用*d訪問時, | |
//系統會以讀取double類型數據來讀取。 | |
cout << d << " " << &n << endl; | |
cout << *d << " " << n << endl; | |
cout << "---------------------------\n"; | |
//將一個32位的整數轉換成一個指針 | |
char *n_p = reinterpret_cast<char*>(10); | |
//reinterpret_cast和static_cast的主要區別在於多繼承 | |
C c; | |
printf("%p, %p, %p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));//前兩個的輸出值是相同的,最后一個則會在原基礎上偏移4個字節,這是因為static_cast計算了父子類指針轉換的偏移量,並將之轉換到正確的地址(c里面有m_a,m_b,轉換為B*指針后指到m_b處),而reinterpret_cast卻不會做這一層轉換。 | |
return 0; | |
} |
例如2:
//強制類型轉換//編寫一個程序跳轉到地址0x12345678運行 | |
typedefvoid(*Fun)(void);//定義一個函數指針數據類型 | |
Fun fun = reinterpret_cast<Fun>( 0x12345678 ); | |
fun(); |
4、dynamic_cast類轉換#
dynamic_cast < type-id > ( expression )
說明: 該運算符把expression轉換成type-id類型的對象。Type-id必須是類的指針、類的引用或者void ;如果type-id是類指針類型,那么expression也必須是一個指針,如果type-id是一個引用,那么expression也必須是一個引用。
使用場景: dynamic_cast主要用於類層次間的上行轉換和下行轉換,還可以用於類之間的交叉轉換。在類層次間進行上行轉換時,dynamic_cast和static_cast的效果是一樣的;在進行下行轉換時,dynamic_cast具有類型檢查的功能,比static_cast更安全。
注意:
① dynamic_cast是動態轉換,只有在基類指針轉換為子類指針時才有意義。
② dynamic_cast<>需要類成為多態,即包括“虛”函數,並因此而不能成為void*。
③ static_cast和dynamic_cast可以執行指針到指針的轉換,或實例本身到實例本身的轉換,但不能在實例和指針之間轉換。static_cast只能提供編譯時的類型安全,而dynamic_cast可以提供運行時類型安全。
例如:
//dynamic---用於繼承過程中把父類指針轉換為子類指針 | |
using namespace std; | |
classA | |
{ | |
public: | |
A() {} | |
virtual ~A(){} | |
}; | |
classB:public A | |
{ | |
public: | |
B() {} | |
~B(){} | |
}; | |
//調用 | |
intmain() | |
{ | |
A *p = new B(); //用戶子類指針初始化父類指針 | |
A *a = new A();//創建一個A類對象 | |
//以下兩句必須在基類有虛析構的情況下才正確,否則編譯的時候報錯 | |
B *bptr = dynamic_cast<B*>(a);//把新的父類指針賦值子類指針(nullptr) | |
B *bptr1 = dynamic_cast<B*>(p);//p是A類型指向B對象空間,把p轉回B類 | |
if(bptr1 == nullptr)//為nullptr時轉換不成功 | |
{ | |
cout<<"fail"<<endl; | |
} | |
else { | |
cout<<"success"<<endl; | |
} | |
return 0; | |
} |
二十二、異常捕捉和處理#
在閱讀別人開發的項目中,也許你會經常看到了多處使用異常的代碼,也許你也很少遇見使用異常處理的代碼。那在什么時候該使用異常,又在什么時候不該使用異常呢?在學習完異常基本概念和語法之后,后面會有講解。
(1)異常拋出和捕捉語句#
//1.拋出異常 | |
throw 異常對象 | |
//2.異常捕捉 | |
try{ | |
可能會發生異常的代碼 | |
}catch(異常對象){ | |
異常處理代碼 | |
} |
- throw子句:throw 子句用於拋出異常,被拋出的異常可以是C++的內置類型(例如: throw int(1);),也可以是自定義類型。
- try區段:這個區段中包含了可能發生異常的代碼,在發生了異常之后,需要通過throw拋出。
- catch子句:每個catch子句都代表着一種異常的處理。catch子句用於處理特定類型的異常。catch塊的參數推薦采用地址傳遞而不是值傳遞,不僅可以提高效率,還可以利用對象的多態性。
(2)異常的處理規則#
- throw拋出的異常類型與catch抓取的異常類型要一致;
- throw拋出的異常類型可以是子類對象,catch可以是父類對象;
- catch塊的參數推薦采用地址傳遞而不是值傳遞,不僅可以提高效率,還可以利用對象的多態性。另外,派生類的異常捕獲要放到父類異常撲獲的前面,否則,派生類的異常無法被撲獲;
- 如果使用catch參數中,使用基類捕獲派生類對象,一定要使用傳遞引用的方式,例如catch (exception &e);
- 異常是通過拋出對象而引發的,該對象的類型決定了應該激活哪個處理代碼;
- 被選中的處理代碼是調用鏈中與該對象類型匹配且離拋出異常位置最近的那一個;
- 在try的語句塊內聲明的變量在外部是不可以訪問的,即使是在catch子句內也不可以訪問;
- 棧展開會沿着嵌套函數的調用鏈不斷查找,直到找到了已拋出的異常匹配的catch子句。如果拋出的異常一直沒有函數捕獲(catch),則會一直上傳到c++運行系統那里,導致整個程序的終止。
(3)實例#
- 實例1:拋出自定義類型異常。
classData | |
{ | |
public: | |
Data() {} | |
}; | |
voidfun(int n) | |
{ | |
if(n==0) | |
throw 0;//拋異常 int異常 | |
if(n==1) | |
throw "error"; //拋字符串異常 | |
if(n==2) | |
{ | |
Data data; | |
throw data; | |
} | |
if(n>3) | |
{ | |
throw 1.0; | |
} | |
} | |
intmain() | |
{ | |
try { | |
fun(6);//當異常發生fun里面,fun以下代碼就不會再執行,調到catch處執行異常處理代碼,后繼續執行catch以外的代碼。當throw拋出異常后,沒有catch捕捉,則整個程序會退出,不會執行整個程序的以下代碼 | |
cout<<"*************"<<endl; | |
}catch (int i) { | |
cout<<i<<endl; | |
}catch (const char *ptr) | |
{ | |
cout<<ptr<<endl; | |
}catch(Data &d) | |
{ | |
cout<<"data"<<endl; | |
}catch(...)//抓取 前面異常以外的所有其他異常 | |
{ | |
cout<<"all"<<endl; | |
} | |
return 0; | |
} |
- 實例2:標准出錯類拋出和捕捉異常。
using namespace std; | |
intmain() | |
{ | |
try { | |
char* p = new char[0x7fffffff]; //拋出異常 | |
} | |
catch (exception &e){ | |
cout << e.what() << endl; //捕獲異常,然后程序結束 | |
} | |
return 0; | |
} |
輸出結果:
當使用new進行開空間時,申請內存失敗,系統就會拋出異常,不用用戶自定義異常類型,此時捕獲到異常時,就可告訴使用者是哪里的錯誤,便於修改。
- 實例3:繼承標准出錯類的派生類的異常拋出和捕捉。
using namespace std; | |
classFileException :public exception | |
{ | |
public: | |
FileException(string msg) { | |
this->exStr = msg; | |
} | |
virtualconstchar*what()constnoexcept//聲明這個函數不能再拋異常 | |
{ | |
return this->exStr.c_str(); | |
} | |
protected: | |
string exStr; | |
}; | |
voidfun() | |
{ | |
int fd = ::open("./open.txt",O_RDWR); | |
if(fd<0) | |
{ | |
FileException openFail("open fail"); //創建異常對象 | |
throw openFail;//拋異常 | |
} | |
} | |
intmain( ) | |
{ | |
try { | |
fun(); | |
} catch (exception &e) {//一般需要使用引用 | |
cout<<e.what()<<endl; | |
} | |
cout<<"end"<<endl; | |
return 0; | |
} |
如果在Linux上運行,上述代碼需要根據環境修改:
98標准寫法
~FileException()throw(){}//必須要 | |
virtual const char*what() const throw()//聲明這個函數不能再拋異常 | |
{ | |
return this->exStr.c_str(); | |
} | |
//編譯 | |
g++ main.cpp |
2011標准寫法
~FileException()noexcept{}//必須要 | |
virtualconstchar*what() constnoexcept//聲明這個函數不能再拋異常 | |
{ | |
return this->exStr.c_str(); | |
} | |
//編譯 | |
g++ main.cpp -std=c++11 指定用c++11標准編譯 |
(4)總結#
1. 使用異常處理的優點:
- 傳統錯誤處理技術,檢查到一個錯誤,只會返回退出碼或者終止程序等等,我們只知道有錯誤,但不能更清楚知道是哪種錯誤。使用異常,把錯誤和處理分開來,由庫函數拋出異常,由調用者捕獲這個異常,調用者就可以知道程序函數庫調用出現的錯誤是什么錯誤,並去處理,而是否終止程序就把握在調用者手里了。
2. 使用異常的缺點:
- 如果使用異常,光憑查看代碼是很難評估程序的控制流:函數返回點可能在你意料之外,這就導致了代碼管理和調試的困難。啟動異常使得生成的二進制文件體積變大,延長了編譯時間,還可能會增加地址空間的壓力。
- C++沒有垃圾回收機制,資源需要自己管理。有了異常非常容易導致內存泄漏、死鎖等異常安全問題。 這個需要使用RAII來處理資源的管理問題。學習成本較高。
- C++標准庫的異常體系定義得不好,導致大家各自定義各自的異常體系,非常的混亂。
3. 什么時候使用異常?
- 建議:除非已有的項目或底層庫中使用了異常,要不然盡量不要使用異常,雖然提供了方便,但是開銷也大。
4. 程序所有的異常都可以catch到嗎?
- 並非如此,只有發生異常,並且又拋出異常的情況才能被catch到。例如,數組下標訪問越界的情況,系統是不會自身拋出異常的,所以我們無論怎么catch都是無效的;在這種情況,我們需要自定義拋出類型,判斷數組下標是否越界,然后再根據自身需要throw自定義異常對象,這樣才可以catch到異常,並進行進一步處理。
二十三、STL標准模板庫#
容器:
- vector---順序存儲---順序表 (訪問遍歷查詢)
- list ------鏈式存儲 ----鏈表 (適合數據長度不確定, 經常改變)
- map ----鍵值對存儲 (key:value) (適合數據成對存儲)
- set ------容器-----------------------(存儲數據是唯一的)
1、vector(順序表)#
using namespace std; | |
intmain() | |
{ | |
//創建vector對象 | |
//大小變化:1024 2048 4096(開始會以2倍數增加,后面慢慢以1/3、1/5等的形式增加) | |
vector<string> names; | |
//賦值,3個jack | |
names.assign(3,"Jack"); //Jack, Jack, Jack | |
//插入數據 | |
//創建一個迭代器 | |
vector<string>::iterator it = names.begin(); | |
//insert之后迭代器it已經改變,返回值為插入值的位置 | |
it = names.insert(++it, "Rose"); //結果:Jack Rose Jack Jack | |
it = names.insert(++it,"Jim");//結果:Jack Rose Jim Jack Jack | |
it = names.insert(++it, "lcg"); //結果:Jack Rose Jim lcg Jack Jack | |
it = names.insert(names.end(), "Jack"); //結果:Jack Rose Jim lcg Jack Jack Jack | |
//查詢數據/刪除數據---迭代器遍歷使用vector, list, map, set | |
//names.end()為順序表最后一個元素的下一個地址 | |
for(it = names.begin(); it != names.end(); ++it) | |
{ | |
if(*it == "Jack") | |
{ | |
cout<<*it<<" "; | |
//擦除,返回擦出元素的下一個位置 | |
//例如a b c,刪除b后,返回迭代器指向c | |
it=names.erase(it); | |
--it; | |
} | |
} | |
cout<<endl; | |
//遍歷---順序表 | |
for(int i=0; i<names.size(); i++) | |
{ | |
cout<<names[i]<<" "; | |
} | |
cout<<endl; | |
return 0; | |
} |
作者:李春港