構造函數:
拷貝構造函數
就類對象而言,相同類型的類對象是通過拷貝構造函數來完成整個復制過程的
拷貝構造函數是一種特殊的構造函數,函數的名稱必須和類名稱一致,它必須的一個參數是本類型的一個引用變量。
如
[cpp]
#include <iostream>
using namespace std;
class CExample {
private:
int a;
public:
//構造函數
CExample(int b)
{ a = b;}
//拷貝構造函數
CExample(const CExample& C)
{
a = C.a;
}
//一般函數
void Show ()
{
cout<<a<<endl;
}
};
int main()
{
CExample A(100);
// 把A對象值拷貝給B
//CExample B(A); 也是一樣的
CExample B = A;
B.Show ();
return 0;
}
#include <iostream>
using namespace std;
class CExample {
private:
int a;
public:
//構造函數
CExample(int b)
{ a = b;}
//拷貝構造函數
CExample(const CExample& C)
{
a = C.a;
}
//一般函數
void Show ()
{
cout<<a<<endl;
}
};
int main()
{
CExample A(100);
// 把A對象值拷貝給B
//CExample B(A); 也是一樣的
CExample B = A;
B.Show ();
return 0;
}
淺拷貝
所謂淺拷貝,指的是在對象復制時,只對對象中的數據成員進行簡單的賦值,默認拷貝構造函數執行的也是淺拷貝。
深拷貝
在“深拷貝”的情況下,對於對象中動態成員,就不能僅僅簡單地賦值了,而應該重新動態分配空間
C++拷貝構造函數詳解
析構函數:
它不能帶任何參數,也沒有返回值(包括void類型)
只能有一個析構函數,不能重載。
析構函數名也應與類名相同,只是在函數名前面加一個位取反符~,例如~stud( ),以區別於構造函數。
如果用戶沒有編寫析構函數,編譯系統會自動生成一個缺省的析構函數(即使自定義了析構函數,編譯器也總是會為我們合成一個析構函數,並且如果自定義了析構函數,編譯器在執行時會先調用自定義的析構函數再調用合成的析構函數),它也不進行任何操作。所以許多簡單的類中沒有用顯式的析構函數。
C++基本概念----構造函數和析構函數
內聯函數:
內部不能使用for while switch等耗時操作,推薦在操作簡單,內部代碼簡單的時候使用(1~5行)。
編譯時函數會被內部代碼取代,增加程序的空間度,減少時間度。
內斂函數的定義必須出現在第一次被調用前
[cpp]
內聯函數:
定義
內聯函數從源代碼層看,有函數的結構,而在編譯後,卻不具備函數的性質。編譯時,類似宏替換,使用函數體替換調用處的函數名。一般在代碼中用inline修飾,但是否能形成內聯函數,需要看編譯器對該函數定義的具體處理。
實現
有兩種實現方式:
1.在類聲明的內部聲明,而在類聲明外部定義叫做顯式內聯函數,如:
class display
{
int t;
public:
void output(void);
};
display object;
inline void display::output(void)
{
cout << "i is " << i <<"\n";
}
2.在類聲明的內部定義,叫做隱式內聯函數,如:
class display
{
int t;
public:
inline void output(void)
{cout<<"i is "<< i << "\n";}
}
實際應用
引入內聯函數的目的是為了解決程序中函數調用的效率問題。
函數是一種更高級的抽象。它的引入使得編程者只關心函數的功能和使用方法,而不必關心函數功能的具體實現;函數的引入可以減少程序的目標代碼,實現程序代碼和數據的共享。但是,函數調用也會帶來降低效率的問題,因為調用函數實際上將程序執行順序轉移到函數所存放在內存中某個地址,將函數的程序內容執行完後,再返回到轉去執行該函數前的地方。這種轉移操作要求在轉去前要保護現場並記憶執行的地址,轉回後先要恢復現場,並按原來保存地址繼續執行。因此,函數調用要有一定的時間和空間方面的開銷,於是將影響其效率。特別是對於一些函數體代碼不是很大,但又頻繁地被調用的函數來講,解決其效率問題更為重要。引入內聯函數實際上就是為了解決這一問題。
在程序編譯時,編譯器將程序中出現的內聯函數的調用表達式用內聯函數的函數體來進行替換。顯然,這種做法不會產生轉去轉回的問題,但是由於在編譯時函數體中的代碼被替代到程序中,因此會增加目標程序代碼量,進而增加空間開銷,而在時間開銷上不象函數調用時那麼大,可見它是以目標代碼的增加為代價來換取時間的節省。
在程序中,調用其函數時,該函數在編譯時被替代,而不是像一般函數那樣是在運行時被調用。
注意事項
使用內聯函數應注意的事項
內聯函數具有一般函數的特性,它與一般函數所不同之處只在於函數調用的處理。一般函數進行調用時,要將程序執行權轉到被調用函數中,然後再返回到調用它的函數中;而內聯函數在調用時,是將調用表達式用內聯函數體來替換。在使用內聯函數時,應注意如下幾點:
1.在內聯函數內不允許用循環語句和開關語句。
如果內聯函數有這些語句,則編譯將該函數視同普通函數那樣產生函數調用代碼,遞歸函數(自己調用自己的函數)是不能被用來做內聯函數的。內聯函數只適合於只有1~5行的小函數。對一個含有許多語句的大函數,函數調用和返回的開銷相對來說微不足道,所以也沒有必要用內聯函數實現。
2.內聯函數的定義必須出現在內聯函數第一次被調用之前。
3.本欄目講到的類結構中所有在類說明內部定義的函數是內聯函數。
內聯函數:
定義
內聯函數從源代碼層看,有函數的結構,而在編譯後,卻不具備函數的性質。編譯時,類似宏替換,使用函數體替換調用處的函數名。一般在代碼中用inline修飾,但是否能形成內聯函數,需要看編譯器對該函數定義的具體處理。
實現
有兩種實現方式:
1.在類聲明的內部聲明,而在類聲明外部定義叫做顯式內聯函數,如:
class display
{
int t;
public:
void output(void);
};
display object;
inline void display::output(void)
{
cout << "i is " << i <<"\n";
}
2.在類聲明的內部定義,叫做隱式內聯函數,如:
class display
{
int t;
public:
inline void output(void)
{cout<<"i is "<< i << "\n";}
}
實際應用
引入內聯函數的目的是為了解決程序中函數調用的效率問題。
函數是一種更高級的抽象。它的引入使得編程者只關心函數的功能和使用方法,而不必關心函數功能的具體實現;函數的引入可以減少程序的目標代碼,實現程序代碼和數據的共享。但是,函數調用也會帶來降低效率的問題,因為調用函數實際上將程序執行順序轉移到函數所存放在內存中某個地址,將函數的程序內容執行完後,再返回到轉去執行該函數前的地方。這種轉移操作要求在轉去前要保護現場並記憶執行的地址,轉回後先要恢復現場,並按原來保存地址繼續執行。因此,函數調用要有一定的時間和空間方面的開銷,於是將影響其效率。特別是對於一些函數體代碼不是很大,但又頻繁地被調用的函數來講,解決其效率問題更為重要。引入內聯函數實際上就是為了解決這一問題。
在程序編譯時,編譯器將程序中出現的內聯函數的調用表達式用內聯函數的函數體來進行替換。顯然,這種做法不會產生轉去轉回的問題,但是由於在編譯時函數體中的代碼被替代到程序中,因此會增加目標程序代碼量,進而增加空間開銷,而在時間開銷上不象函數調用時那麼大,可見它是以目標代碼的增加為代價來換取時間的節省。
在程序中,調用其函數時,該函數在編譯時被替代,而不是像一般函數那樣是在運行時被調用。
注意事項
使用內聯函數應注意的事項
內聯函數具有一般函數的特性,它與一般函數所不同之處只在於函數調用的處理。一般函數進行調用時,要將程序執行權轉到被調用函數中,然後再返回到調用它的函數中;而內聯函數在調用時,是將調用表達式用內聯函數體來替換。在使用內聯函數時,應注意如下幾點:
1.在內聯函數內不允許用循環語句和開關語句。
如果內聯函數有這些語句,則編譯將該函數視同普通函數那樣產生函數調用代碼,遞歸函數(自己調用自己的函數)是不能被用來做內聯函數的。內聯函數只適合於只有1~5行的小函數。對一個含有許多語句的大函數,函數調用和返回的開銷相對來說微不足道,所以也沒有必要用內聯函數實現。
2.內聯函數的定義必須出現在內聯函數第一次被調用之前。
3.本欄目講到的類結構中所有在類說明內部定義的函數是內聯函數。
虛函數:
objective-c函數默認都是虛函數-子類會覆蓋父類實現。
只有聲明函數時需要virtual關鍵字
[cpp]
虛函數:
定義
虛函數必須是基類的非靜態成員函數,其訪問權限可以是protected或public,在基類的類定義中定義虛函數的一般形式:
virtual 函數返回值類型 虛函數名(形參表)
{ 函數體 }
作用
虛函數的作用是實現動態聯編,也就是在程序的運行階段動態地選擇合適的成員函數,在定義了虛函數後,可以在基類的派生類中對虛函數重新定義,在派生類中重新定義的函數應與虛函數具有相同的形參個數和形參類型。以實現統一的接口,不同定義過程。如果在派生類中沒有對虛函數重新定義,則它繼承其基類的虛函數。
當程序發現虛函數名前的關鍵字virtual後,會自動將其作為動態聯編處理,即在程序運行時動態地選擇合適的成員函數。
動態聯編規定,只能通過指向基類的指針或基類對象的引用來調用虛函數,其格式:
指向基類的指針變量名->虛函數名(實參表)
或 基類對象的引用名. 虛函數名(實參表)
虛函數是C++多態的一種表現
例如:子類繼承了父類的一個函數(方法),而我們把父類的指針指向子類,則必須把父類的該函數(方法)設為virtual(虛函數)。
使用虛函數,我們可以靈活的進行動態綁定,當然是以一定的開銷為代價。 如果父類的函數(方法)根本沒有必要或者無法實現,完全要依賴子類去實現的話,可以把此函數(方法)設為virtual 函數名=0 我們把這樣的函數(方法)稱為純虛函數。
如果一個類包含了純虛函數,稱此類為抽象類 。
示例
虛函數的實例
#include<iostream.h>
class Cshape
{ public: void SetColor( int color) { m_nColor=color;}
void virtual Display( void) { cout<<"Cshape"<<endl; }
private:
int m_nColor;
};
class Crectangle: public Cshape
{
public:
void virtual Display( void) { cout<<"Crectangle"<<endl; }
};
class Ctriangle: public Cshape
{
void virtual Display( void) { cout<<"Ctriangle"<<endl; }
};
class Cellipse :public Cshape
{
public: void virtual Display(void) { cout<<"Cellipse"<<endl;}
};
void main()
{
Cshape obShape;
Cellipse obEllipse;
Ctriangle obTriangle;
Crectangle obRectangle;
Cshape * pShape[4]=
{ &obShape, &obEllipse,&obTriangle, & obRectangle };
for( int I= 0; I< 4; I++)
pShape[I]->Display( );
}
本程序運行結果:
Cshape
Cellipse
Ctriangle
Crectangle
條件
所以,從以上程序分析,實現動態聯編需要三個條件:
1、 必須把動態聯編的行為定義為類的虛函數。
2、 類之間存在子類型關系,一般表現為一個類從另一個類公有派生而來。
3、 必須先使用基類指針指向子類型的對象,然後直接或者間接使用基類指針調用虛函數。
其他信息
定義虛函數的限制: (1)非類的成員函數不能定義為虛函數,類的成員函數中靜態成員函數和構造函數也不能定義為虛函數,但可以將析構函數定義為虛函數。實際上,優秀的程序員常常把基類的析構函數定義為虛函數。因為,將基類的析構函數定義為虛函數後,當利用delete刪除一個指向派生類定義的對象指針時,系統會調用相應的類的析構函數。而不將析構函數定義為虛函數時,只調用基類的析構函數。
(2)只需要在聲明函數的類體中使用關鍵字“virtual”將函數聲明為虛函數,而定義函數時不需要使用關鍵字“virtual”。
(3)當將基類中的某一成員函數聲明為虛函數後,派生類中的同名函數自動成為虛函數。
(4)如果聲明了某個成員函數為虛函數,則在該類中不能出現和這個成員函數同名並且返回值、參數個數、類型都相同的非虛函數。在以該類為基類的派生類中,也不能出現這種同名函數。
虛函數聯系到多態,多態聯系到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。
c++的虛函數
下面是對C++的虛函數這玩意兒的理解。
一, 什麼是虛函數
(如果不知道虛函數為何物,但又急切的想知道,那你就應該從這裡開始)
簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而采用不同的策略。下面來看一段簡單的代碼
class A{
public:
void print(){ cout<<”This is A”<<endl;}
};
class B:public A{
public:
void print(){ cout<<”This is B”<<endl;}
};
int main(){ //為了在以後便於區分,我這段main()代碼叫做main1
A a;
B b;
a.print();
b.print();
}
通過class A和class B的print()這個接口,可以看出這兩個class因個體的差異而采用了不同的策略,輸出的結果也是我們預料中的,分別是This is A和This is B。但這是否真正做到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操作對象。那現在就把main()處的代碼改一改。
int main(){ //main2
A a;
B b;
A* p1=&a;
A* p2=&b;
p1->print();
p2->print();
}
運行一下看看結果,喲呵,蓦然回首,結果卻是兩個This is A。問題來了,p2明明指向的是class B的對象但卻是調用的class A的print()函數,這不是我們所期望的結果,那麼解決這個問題就需要用到虛函數
class A{
public:
virtual void print(){ cout<<”This is A”<<endl;} //現在成了虛函數了
};
class B:public A{
public:
void print(){ cout<<”This is B”<<endl;} //這裡需要在前面加上關鍵字virtual嗎?
};
毫無疑問,class A的成員函數print()已經成了虛函數,那麼class B的print()成了虛函數了嗎?回答是Yes,我們只需在把基類的成員函數設為virtual,其派生類的相應的函數也會自動變為虛函數。所以,class B的print()也成了虛函數。那麼對於在派生類的相應函數前是否需要用virtual關鍵字修飾,那就是你自己的問題了。
現在重新運行main2的代碼,這樣輸出的結果就是This is A和This is B了。
現在來消化一下,我作個簡單的總結,指向基類的指針在操作它的多態類對象時,會根據不同的類對象,調用其相應的函數,這個函數就是虛函數。
二, 虛函數是如何做到的
(如果你沒有看過《Inside The C++ Object Model》這本書,但又急切想知道,那你就應該從這裡開始)
虛函數是如何做到因對象的不同而調用其相應的函數的呢?現在我們就來剖析虛函數。我們先定義兩個類
class A{ //虛函數示例代碼
public:
virtual void fun(){cout<<1<<endl;}
virtual void fun2(){cout<<2<<endl;}
};
class B:public A{
public:
void fun(){cout<<3<<endl;}
void fun2(){cout<<4<<endl;}
};
由於這兩個類中有虛函數存在,所以編譯器就會為他們兩個分別插入一段你不知道的數據,並為他們分別創建一個表。那段數據叫做vptr指針,指向那個表。那個表叫做vtbl,每個類都有自己的vtbl,vtbl的作用就是保存自己類中虛函數的地址,我們可以把vtbl形象地看成一個數組,這個數組的每個元素存放的就是虛函數的地址,請看圖
通過左圖,可以看到這兩個vtbl分別為class A和class B服務。現在有了這個模型之後,我們來分析下面的代碼
A *p=new A;
p->fun();
毫無疑問,調用了A::fun(),但是A::fun()是如何被調用的呢?它像普通函數那樣直接跳轉到函數的代碼處嗎?No,其實是這樣的,首先是取出vptr的值,這個值就是vtbl的地址,再根據這個值來到vtbl這裡,由於調用的函數A::fun()是第一個虛函數,所以取出vtbl第一個slot裡的值,這個值就是A::fun()的地址了,最後調用這個函數。現在我們可以看出來了,只要vptr不同,指向的vtbl就不同,而不同的vtbl裡裝著對應類的虛函數地址,所以這樣虛函數就可以完成它的任務。
而對於class A和class B來說,他們的vptr指針存放在何處呢?其實這個指針就放在他們各自的實例對象裡。由於class A和class B都沒有數據成員,所以他們的實例對象裡就只有一個vptr指針。通過上面的分析,現在我們來實作一段代碼,來描述這個帶有虛函數的類的簡單模型。
#include<iostream>
using namespace std;
//將上面“虛函數示例代碼”添加在這裡
int main(){
void (*fun)(A*);
A *p=new B;
long lVptrAddr;
memcpy(&lVptrAddr,p,4);
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);
fun(p);
delete p;
system("pause");
}
用VC或Dev-C++編譯運行一下,看看結果是不是輸出3,如果不是,那麼太陽明天肯定是從西邊出來。現在一步一步開始分析
void (*fun)(A*); 這段定義了一個函數指針名字叫做fun,而且有一個A*類型的參數,這個函數指針待會兒用來保存從vtbl裡取出的函數地址
A* p=new B; new B是向內存(內存分5個區:全局名字空間,自由存儲區,寄存器,代碼空間,棧)自由存儲區申請一個內存單元的地址然後隱式地保存在一個指針中.然後把這個地址附值給A類型的指針P.
.
long lVptrAddr; 這個long類型的變量待會兒用來保存vptr的值
memcpy(&lVptrAddr,p,4); 前面說了,他們的實例對象裡只有vptr指針,所以我們就放心大膽地把p所指的4bytes內存裡的東西復制到lVptrAddr中,所以復制出來的4bytes內容就是vptr的值,即vtbl的地址
現在有了vtbl的地址了,那麼我們現在就取出vtbl第一個slot裡的內容
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); 取出vtbl第一個slot裡的內容,並存放在函數指針fun裡。需要注意的是lVptrAddr裡面是vtbl的地址,但lVptrAddr不是指針,所以我們要把它先轉變成指針類型
fun(p); 這裡就調用了剛才取出的函數地址裡的函數,也就是調用了B::fun()這個函數,也許你發現了為什麼會有參數p,其實類成員函數調用時,會有個this指針,這個p就是那個this指針,只是在一般的調用中編譯器自動幫你處理了而已,而在這裡則需要自己處理。
delete p; 釋放由p指向的自由空間;
system("pause"); 屏幕暫停;
如果調用B::fun2()怎麼辦?那就取出vtbl的第二個slot裡的值就行了
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 為什麼是加4呢?因為一個指針的長度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 這更符合數組的用法,因為lVptrAddr被轉成了long*型別,所以+1就是往後移sizeof(long)的長度
三, 以一段代碼開始
#include<iostream>
using namespace std;
class A{ //虛函數示例代碼2
public:
virtual void fun(){ cout<<"A::fun"<<endl;}
virtual void fun2(){cout<<"A::fun2"<<endl;}
};
class B:public A{
public:
void fun(){ cout<<"B::fun"<<endl;}
void fun2(){ cout<<"B::fun2"<<endl;}
}; //end//虛函數示例代碼2
int main(){
void (A::*fun)(); //定義一個函數指針
A *p=new B;
fun=&A::fun;
(p->*fun)();
fun = &A::fun2;
(p->*fun)();
delete p;
system("pause");
}
你能估算出輸出結果嗎?如果你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是B::fun和B::fun2,如果你想不通就接著往下看。給個提示,&A::fun和&A::fun2是真正獲得了虛函數的地址嗎?
首先我們回到第二部分,通過段實作代碼,得到一個“通用”的獲得虛函數地址的方法
#include<iostream>
using namespace std;
//將上面“虛函數示例代碼2”添加在這裡
void CallVirtualFun(void* pThis,int index=0){
void (*funptr)(void*);
long lVptrAddr;
memcpy(&lVptrAddr,pThis,4);
memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);
funptr(pThis); //調用
}
int main(){
A* p=new B;
CallVirtualFun(p); //調用虛函數p->fun()
CallVirtualFun(p,1);//調用虛函數p->fun2()
system("pause");
}
CallVirtualFun方法
現在我們擁有一個“通用”的CallVirtualFun方法。
這個通用方法和第三部分開始處的代碼有何聯系呢?聯系很大。由於A::fun()和A::fun2()是虛函數,所以&A::fun和&A::fun2獲得的不是函數的地址,而是一段間接獲得虛函數地址的一段代碼的地址,我們形象地把這段代碼看作那段CallVirtualFun。編譯器在編譯時,會提供類似於CallVirtualFun這樣的代碼,當你調用虛函數時,其實就是先調用的那段類似CallVirtualFun的代碼,通過這段代碼,獲得虛函數地址後,最後調用虛函數,這樣就真正保證了多態性。同時大家都說虛函數的效率低,其原因就是,在調用虛函數之前,還調用了獲得虛函數地址的代碼。