本章學習內容:
- 1.const
- 2.指針const
- 3.inline內聯函數
- 4.函數重載
- 5.extern “C”
- 6.new/delete聲明與釋放
- 7.namespace命名空間
- 8.C++中的4種轉換
- 9.拷貝構造函數
- 10.構造函數初始化列表
- 11.析構函數
- 12.const成員函數
- 13.const對象
- 14.棧、堆、靜態存儲區的區別
- 15.靜態成員變量/靜態成員函數
- 16.友元friend
- 17.operator操作符重載函數
- 18. 通過()操作符重載實現:函數對象
- 19. 操作符重載實現:類型轉換函數
- 20.explicit顯式調用(用來阻止隱式轉換)
- 21.父類和子類中的同名成員/函數
- 22.子類對象初始化父類對象
- 23.父類對象初始化子類對象
- 24.純虛函數vertual
- 25.泛型函數模板(兼容不同類型)
- 26.泛型類模板(兼容不同類型)
- 27.數值型函數模板和數值型類模板(兼容不同數值)
- 28.C++智能指針
- 29.Qt中的智能指針
1.const
const和define宏區別
- const常量: 由編譯器處理,它會對const常量進行類型檢查和作用域檢查
- define宏定義: 由預處理器處理,直接進行文本替換,不會進行各種檢查
const在C++中為真正常量.示例:
const int c = 0; //const局部變量 int* p = (int*)&c; //會給p重新分配空間,而c還是處於常量符號表中 *p = 5; //此時修改的值是新的地址上,對於c而言,依舊為0 printf("c = %d,*p=%d\n", c,*p); //打印: c = 0, *p=5
2.指針const
1) 底層const(位於*左側)
const int *p : const修飾*p為常量,也就是說該指針指向的對象內容是個常量,只能改變指向的地址.但是可以通過其他方式修改對象內容
例如:
int a=1,a=2; const int *p = &a; *p = 2; //error, 不能直接修改 a=2; //right,通過對象本身修改內容 *p = &b; //right,可以指向其它地址
2) 頂層const(位於*右側)
int * const p : const修飾指針p是個常量,也就是說p指向的地址是個常量
例如:
int a=1,a=2; int * const p = &a; p = &b; //error,p指向的地址是常量,永遠為a地址,不能修改
注意:頂層const變量可以替代mutable變量
3.inline內聯函數
示例如下:
inline int MAX(int a, int b) { return a > b ? a : b ; }
- 普通函數:每次調用前,CPU都會保存現場(入棧),調用完后還要恢復現場(出棧)等額外開銷.
- 內聯函數:就會在每次調用的地方,將內聯函數里的代碼段”內聯地”展開,所以省去了額外的開銷
注意:當內聯函數里的代碼過多,且流程復雜時,編譯器可能會拒絕該函數的內聯請求,從而變成普通函數
4.函數重載
參數表不同主要有以下幾種
- 1) 參數個數不同
- 2) 參數類型不同
- 3) 參數順序不同
注意:
- 重載函數需要避免使用參數默認值
- 調用重載函數時,只會匹配函數參數表,與函數返回值無關
- 函數重載必須發生在同一個作用域中
- 重載函數的入口地址,不能直接通過函數名來獲取
5.extern “C”
可以實現調用C庫代碼.
示例:
#ifdef __cplusplus extern "C" //通過C方式來編譯add.h,也就是add()函數 { #include "add.h" } #endif
6.new/delete聲明與釋放
示例如下:
int *p = new int(); //默認值為0 int *p1= new int(1); //動態分配一個int空間給p1,並賦值為1 float *p2=new float(2.0f); //2.0后面加f,表示2.0是個float類型 int *p3 = new int; //默認值為隨機值 string *p4 = new string[10]; delete p; delete p1; delete p2; delete p3; delete[] p4;
注意:
• 釋放數組的空間時,必須使用delete[],而不是delete,避免內存泄漏
7.namespace命名空間
示例:
#include <stdio.h> namespace First //定義First命名空間 { int i = 0; } namespace Second //定義Second命名空間 { int i = 1;namespace Internal //在Second里,再次定義一個Internal空間(實現嵌套) { struct Position { int x; int y; }; } } int main() { using namespace First; //使用First整個命名空間,成為該main()的默認空間 using Second::Internal::Position; //使用Second->Internal空間里的Position結構體 printf("First::i = %d\n", i); printf("Second::i = %d\n", Second::i); Position p = {2, 3}; printf("p.x = %d\n", p.x); printf("p.y = %d\n", p.y); return 0; }
輸出結果:
First::i = 0 Second::i = 1 p.x = 2 p.y = 3
8.C++中的4種轉換
static_cast(靜態類型轉換)
用於變量和對象之間的轉換,比如(bool,char,int等)
用於有繼承關系的類對象指針轉換,可以通過父類對象去初始化子類對象(注意只能初始化父類的那部分)
const_cast(去常類型轉換)
常用於去除const類對象的只讀屬性
強制轉換的類型必須是指針*或者引用&
示例-去除const對象的只讀屬性:
class Test { public: int mval; Test():mval(10) { } }; int main() { const Test n1; //n1.mval = 100; //error,不能直接修改常量對象的成員 Test *n2 = const_cast<Test *>(&n1); //通過指針*轉換 Test &n3 = const_cast<Test &>(n1); //通過引用&轉換 n2->mval = 20; cout<<n1.mval<<endl; //打印20 n3.mval = 30; cout<<n1.mval<<endl; //打印30 }
dynamic_cast(動態類型轉換)
只能用在有虛函數的類中,一般在多重繼承下用的比較多,比如:
class BaseA { public: virtual void funcA() { cout<<"BaseA: funcA()"<<endl; } }; class BaseB { public: virtual void funcB() { cout<<"BaseB: funcB()"<<endl; } }; class Derived : public BaseA,public BaseB { }; int main() { Derived d; BaseA *pa=&d; pa->funcA(); //打印 BaseA: funcA() /*通過強制轉換執行*/ BaseB *pb=(BaseB *)pa; pb->funcB(); //還是打印BaseA: funcA(), 因為pb還是指向pa,執行的還是pa的虛函數表 /*通過dynamic_cast執行*/ pb = dynamic_cast<BaseB *>(pa); pb->funcB(); //打印 BaseB: funcB() //編譯器會去檢測pa所在的地址,發現有多個虛函數表,然后根據 <BaseB *>來修正指針pb return 0; }
reinterpret_cast(解讀類型轉換)
對要轉換的數據重新進行解讀,適用於所有指針的強制轉換
9.拷貝構造函數
一般用於當類對象中有成員指針時,才會自己寫拷貝構造函數,因為編譯器自帶的默認拷貝構造函數只支持淺拷貝
class Test { //... ... Test(const Test& t) { //copy... ... } };
10.構造函數初始化列表
- 當類中有const成員變量時,則必須要用初始化列表進行初始化.
- 對於其它普通變量如果不初始化的話則為隨機值.
- 初始化列表位於構造函數名右側,以一個冒號開始,接着便是需要初始化的變量,以逗號隔開
示例如下:
class Example { private: int i; float j; const int ci; int *p; public: Test(): j(1.5),i(2),ci(10),p(new int(3)) //初始化i=2,j=1.5,ci=10 *p=3 { } };
11.析構函數
注意:
- 在類里,當定義了析構函數,編譯器就不會提供默認的構造函數了,所以還要自己定義一個構造函數。
- 使用new創建的對象變量,在不使用時,需要使用delete,才能調用析構函數
構造函數的調用順序
- 1. 首先判斷父類是否存在,若存在則調用父類構造函數
- 2. 其次判斷該對象的是否有類成員,若有則調用類成員的構造函數(調用順序按照聲明的順序來構造)
- 3. 最后調用對象本身的構造函數
12.const成員函數
- cosnt成員函數里只能調用const成員函數
- const成員函數中不能直接修改成員變量的值
- 只有對mutable成員變量或者頂層const成員是可以修改的
- 如果用const修飾的函數,那么該函數一定是類的成員函數
13.const對象
- const對象的成員變量不允許被改變,
- const對象只能調用const成員函數,而非const對象可以訪問const成員函數
- const對象是編譯階段的概念,運行時無效
- const對象可以通過const_cast強制轉換來實現改變其中成員變量的值
14.棧、堆、靜態存儲區的區別
棧
用來存放函數里的局部變量,當調用某個函數時(執行某個代碼段),會將該函數的變量(從數據段讀出)入棧,然后退出函數的時候,會將該局部變量出棧進行銷毀.
一般如果局部變量未初始化的話,都是隨機值
堆
堆由程序員分配釋放new/delete,所以需要注意內存泄漏問題
一般new分配的對象變量,其成員都是隨機值
靜態存儲區
用來存放全局變量,一直會存在的,一般編譯器為自動將未賦值的全局變量進行一次清0
15.靜態成員變量/靜態成員函數
- 在類里定義時直接通過static關鍵字修飾
- 靜態成員變量需要在類外單獨分配空間,而靜態成員函數則不需要
- 靜態成員變量在程序內部位於靜態存儲區
- 對於public公有的靜態成員變量/函數時,可以直接通過類名進行直接訪問
- 靜態成員函數中不能訪問非靜態成員變量,因為它屬於整個類的,沒有隱含this指針
示例如下:
class Test{ private: static int mval; public: Test() { print(); } static int print() //靜態成員函數是存在代碼段中,所以不在類外定義也可以 { cout<<"mval="<<mval<<endl; } };
int Test::mval=4; //靜態成員變量存在靜態存儲區中,所以需要在類外定義 int main() { Test::print(); //通過類名直接訪問靜態成員函數,打印: mval=4 }
16.友元friend
- 友元的好處在於,方便快捷.可以通過friend函數來訪問這個類的私有成員
- 友元的壞處在於,破壞了面向對象的封裝性,在現代已經逐漸被遺棄
示例:
#include "stdio.h" class Test{ private: static int n; int x; int y; public: Test(int x,int y) { this->x = x; this->y = y; } friend void f_func(const Test& t); //聲明Test的友元是f_func()函數 }; int Test::n = 3; void f_func(const Test& t) { printf("t.x=%d\n",t.x); printf("t.y=%d\n",t.y); printf("t.n=%d\n",t.n); //訪問私有靜態成員變量 } int main() { Test t1(1,2); f_func(t1); return 0; }
17.operator操作符重載函數
使'+,-,*,/'等操作符擁有了重載能力,能夠實現對象之間的操作,而不再單純的普通變量之間的操作了.
示例如下,實現一個加法類:
class Add { double mval; public: explicit Add(double t=0) { mval = t; } Add& operator +(const Add& t) //實現相同類對象相加 { this->mval += t.mval; cout<<"operator +(const Add& t)"<<endl; return *this; //返回該對象,表示可以重復使用 }
Add& operator +(int i) //實現int型對象相加 { this->mval += i; cout<<"operator +(int i)"<<endl; return *this; } Add& operator +(double d) //實現double型對象相加 { this->mval += d; cout<<"operator +(double d)"<<endl; return *this; } Add& operator = (const Add& t) //重載賦值操作符 { cout<<"operator =(const Add& t)"<<endl; if(this!=&t) { mval = t.mval; } return *this; } double val() { return mval; } };
int main() { Add a1(11.5); Add a2(1.25); a1=a1+a2; //相當於調用兩步: a1.operator =(a1.operator +(a2)); cout<< a1.val() <<endl; }
運行打印:
18.通過()操作符重載實現:函數對象
- 函數對象是指該對象具備函數的行為
- 函數對象,是通過()調用操作符聲明得到的,然后便能通過函數方式來調用該對象了.
- ()調用操作符可以定義不同參數的多個重載函數
- ()調用操作符只能通過類的成員函數重載(不能通過全局函數)
示例:
class Test{ public: void operator () (void) //通過()重載操作符,來使對象具備函數的行為 { cout<<"hello"<<endl; } }; int main() { Test t; t(); //來調用t這個函數對象打印"hello" }
PS:好處在於可以封裝自己的成員以及其它函數,所以能夠更好的面向對象.
19.操作符重載實現:類型轉換函數
示例如下:
class Test{ int mValue; public: Test(int i=0) { mValue=i; } operator int() //重載int類型 { return mValue; } }; int main() { Test t(1000); int i=t; //等價於: i=t.operator int(); cout<<i<<endl; //i=1000 }
20.explicit顯式調用(用來阻止隱式轉換)
示例:
class Test{ public: explicit Test(unsigned int i) { cout<<"unsigned i= "<<i<<endl; } }; int main() { short num=3; //Test t1=num; //Error,因為explicit阻止short類型 轉換為unsigned int 類型 /*只能有以下3個方法實現*/ Test t2=(Test)num; //C方式強制轉換,不推薦 Test t3=static_cast<Test>(num); //C++方式強制轉換 Test t4(num); //手工調用構造函數 return 0; }
21.父類和子類中的同名成員/函數
- 子類可以定義父類中的同名成員和同名函數
- 子類中的成員變量和函數將會隱藏父類的同名成員變量和函數
- 父類中的同名成員變量和函數依然存在子類中
- 通過作用域分辨符(::)才可以訪問父類中的同名成員變量和函數
示例1-通過子類訪問父類同名函數和同名成員:
class Parent{ public: int mval; Parent():mval(100) { } void print() { cout<<"Parent: mval="<<mval<<endl; } }; class Child :public Parent { public: int mval; Child():mval(20) { } void print() { cout<<"Child: mval="<<mval<<endl; } }; int main() { Child c; c.Parent::print(); //調用父類的同名成員函數 cout<<c.mval<<endl; cout<<c.Parent::mval<<endl; //打印父類的同名成員變量 }
22.子類對象初始化父類對象
以上示例的Parent類和Child類為例,在編譯器中,可以將子類對象退化為父類對象,從而實現子類來初始化父類,比如:
Parent p1(Child()); //Child()構造函數會返回一個臨時對象,從而通過子類初始化父類 Child c; Parent & p2 = c ; //定義p2是C對象的別名
23.父類對象初始化子類對象
只能使用static_cast或者C方式轉換,以上示例的Parent類和Child類為例:
Parent p;
Child *c = static_cast<Child *>(&p);
24.純虛函數vertual
- 在父類中用virtual聲明的成員函數便為虛函數
- 虛函數的作用在於,能夠正確調用某個同名函數是哪個類的對象
- 比如:當某個子類被強制轉換為父類時,則父類的虛函數也會被替代為子類的,從而實現程序靈活性
一個典型的示例,如下所示:
class Base //父類 { public: virtual void func() //聲明func為虛函數 { cout<<"Base: func()"<<endl; } }; class BaseA : public Base //子類A { public: void func() { cout<<"BaseA: funcA()"<<endl; } }; class BaseB : public Base //子類B { public: void func() { cout<<"BaseB: funcB()"<<endl; } }; void print(class Base& b) { b.func(); } int main() { BaseA bA; BaseB bB; print(bA); print(bB); return 0; }
運行打印:
如上圖可以看到,我們以print(bA)為例:
再調用print()函數時,會將BaseA bA轉換為父類Base,由於父類Base有個func()虛函數,所以會被動態替換為bA子類的func()函數.所以會打印funcA()
如果將上面代碼virtual void func()改為void func()重新編譯運行后,打印:
如上圖可以看到,沒有虛函數后,整個代碼都變得沒有靈活性,不適合類的擴展.
PS:在QT中,virtual用的非常多,比如QWidget的showEvent函數:
virtual void showEvent ( QShowEvent * event );
假如我們需要在窗口顯示時加點特效時,只需要重寫它即可,而QT庫只需要根據vertual特性來自動調用我們重寫的函數,非常靈活.
25.泛型函數模板(兼容不同類型)
函數模板是C++中重要的代碼復用方式, 可通過不同類型進行調用
- 通過template關鍵字來聲明使用模板
- 通過typename關鍵字來定義模板類型
示例:
template <typename T> //聲明使用模板,並定義T是一個模板類型 void Swap(T& a, T& b) //緊接着使用T { T c = a; a = b; b = c; }
int main() { int a=0; int b=1; Swap(a,b); //自動調用,編譯器根據a和b的類型來推導 float c=0; float d=1; Swap<float>(c,d); //顯示調用,指定T是float類型 }
為什么函數模板能夠執行不同的類型參數?
答:
- 其實編譯器對函數模板進行了兩次編譯
- 第一次編譯時,首先去檢查函數模板本身有沒有語法錯誤
- 第二次編譯時,會去找調用函數模板的代碼,然后通過代碼的真正參數,來生成真正的函數。
- 所以函數模板,其實只是一個模具,當我們調用它時,編譯器就會給我們生成真正的函數.
函數模板也支持多參數,示例如下(如果定義了返回值模板,則必須要顯示指定返回值類型,因為編譯器不知道到底返回什么類型):
#include <iostream> using namespace std; template<typename T1,typename T2,typename T3> T1 Add(T2 a,T3 b) { return static_cast<T1>(a+b); } int main() { // int a = add(1,1.5); //該行編譯出錯,沒有指定返回值類型 int a = Add<int>(1,1.5); //指定T1為int形 cout<<a<<endl; //打印2 float b = Add<float,int,float>(1,1.5); //指定T1,T2,T3類型 cout<<b<<endl; //2.5 return 0; }
26.泛型類模板(兼容不同類型)
類模板和函數模板一樣,都是進行2次編譯,需要注意的是定義對象必須顯示指定所有類型
示例:
template<typename t1,typename t2,typename t3> class Operator{ public: t1 add(t2 num1,t3 num2) { return num1+num2; } };
int main() { Operator<float,int,float>t; cout<<t.add(11,11.5)<<endl; //11+11.5 = 22.5 return 0; }
27.數值型函數模板和數值型類模板(兼容不同數值)
數值型和泛型類似,但是數值型模板必須在編譯時被唯一確定
示例1-數值型函數模板:
template <typename T,int N > //定義一個泛型值T,還有個int型的數值 void func() { T arr[N]; //使用模板參數T和N定義局部數組 } int main() { func<int,10>(); //相當於實現 int arr[10] }
示例2-數值型類模板(實現1+2+3+....+N值):
template < int N > class Sum { public: static const int VALUE = Sum<N-1>::VALUE + N; //通過Sum<N-1>::VALUE實現遞歸調用,並返回該臨時對象 }; template < > //完全特化,因為我們知道N為1,所以不需要寫< int N > class Sum < 1 > //重載Sum類(類似於函數重載),當N==1時調用該類 { public: static const int VALUE = 1; }; int main() { cout << "1 + 2 + 3 + ... + 10 = " << Sum<10>::VALUE << endl; cout << "1 + 2 + 3 + ... + 100 = " << Sum<100>::VALUE << endl; return 0; }
28.C++智能指針
頭文件<memory>
1)auto_ptr
- 生命周期結束時,自動摧毀指向的內存空間
- 不能指向堆數組(因為auto_ptr的析構函數刪除指針用的是delete,而不是delete[])
- auto_ptr的構造函數為explicit類型,所以只能顯示初始化
- 提供get()成員函數,可以用來查看類里的指針地址
- 一個堆空間永遠只屬於一個對象(比如auto_ptr被拷貝/賦值,則自身的指針指向的地址會被搶占)
示例如下:
#include <iostream> #include <memory> using namespace std; class Test{ int mvalue; public: Test(int i=0) { mvalue = i ; cout<< "Test("<<mvalue<<")"<<endl; } ~Test() { cout<< "~Test("<<mvalue<<")"<<endl; } }; int main() { auto_ptr<Test> p1(new Test(1)); auto_ptr<Test> p2(new Test(2)); cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; p2 = p1; cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; return 0; }
運行打印:
如上圖所示,當我們執行p2=p1后,便執行了p2的析構函數進行自動釋放了.並且p1.get()=0,所以auto_ptr具備自動釋放功能以及同塊堆空間下只能有一個指針對象特性
2) shared_ptr (需要C++11支持)
- 帶有引用計數機制,支持多個指針對象指向同一片內存(實現共享)
- 提供swap()成員函數,用來交換兩個相同類型的對象指針地址
- 提供unique()成員函數, 判斷該指針對象地址是否被其它指針對象引用
- 提供get()成員函數,用來獲取指針對象指向的地址
- 提供reset()成員函數,將自身指針對象地址設為NULL,並將引用計數-1(當計數為0,會自動去delete內存)
- 提供use_count()成員函數,可以用來查看引用計數個數
示例如下所示:
class Test{ public: int mvalue; Test(int i=0) { mvalue = i ; cout<< "Test("<<mvalue<<")"<<endl; } ~Test() { cout<< "~Test("<<mvalue<<")"<<endl; } }; int main() { shared_ptr<Test> p1(new Test(1)); shared_ptr<Test> p2(new Test(2)); cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; p1.swap(p2); //互換p1和p2指針指向的地址 cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; p1 = p2; //使p1指向p2指向的地址,並且釋放p1之前指向的地址 cout<<"p1:addr="<<p1.get()<<", p2:addr="<<p2.get()<<endl; cout<<"p1: unique="<<p1.unique()<<endl; //p1和p2指向同一片內存,所以為0 cout<<"p1: count="<<p1.use_count()<<endl; return 0; }
運行打印:
29.Qt中的智能指針
-QPointer
- 當其指向的對象被銷毀時,本身會自動賦值為NULL(從而避免被多次釋放和野指針)
- 缺點在於,該模板類析構時,不會自動摧毀所指向的對象(需要手工delete)
-QSharedPointer
- 帶有引用計數機制,支持多個指針對象指向同一片內存(實現共享)
- 可以被自由地拷貝和賦值
- 當引用計數為0(最后一個指針被摧毀)時,才刪除指向的對象(和shared_ptr類似)
-QScopedPointer
- 優點在於生命期結束后會自動刪除它所指的對象(不需要手工delete)
- 不支持多個QScopedPointer指針對象指向同一片內存(不能共享)
示例:
QScopedPointer<QPushButton> p1(new QPushButton);