第一節(jié) 導讀
知識清單:
首先來列一張清單,清點一下后面課程將會深入的細節(jié):
- operator type()const;
- explicit complex(...):initialization list{}
- pointer-like object
- function-like object
- Namespace
- template specialization
- Standard Library
- variadic template(since C++11)
- move ctor(since C++11)
- Rvalue reference(since C++11)
- auto(since C++11)
- lambda(since C++11)
- range-base for loop(since C++11)
- unordered containers(cince C++11)
目標:
- 在先前的基礎課程所培養(yǎng)的正規(guī)、大氣的編程素養(yǎng)上,繼續(xù)探討更多技術。
- 泛型編程(Generic Programming)和面向對象編程(Object-Oriented Programming)雖然分屬不同思維,但它們正是C++的技術主線,所以本課程也討論template(模板)。
- 深入探索面向對象之繼承關系(inheritance)所形成的對象模型(Object Model),包括隱藏于底層的this指針,vptr(虛指針),vtbl(虛表),virtual mechanism(虛機制),以及虛函數(virtual functions)造成的polymorphism(多態(tài))效果。
推薦書目
- 《C++ Primer》
- 《The C++ Programming Language》
- 《Effective Modern C++》
- 《Effective C++》
- 《The C++ Standard Library》
- 《STL源碼剖析》
第二、三節(jié) non-explicit one argument constructor & Conversion Function(轉換構造與類型轉換函數)
為了方便對照學習,記憶,決定把二三節(jié)內容放在一起講解。
轉換構造函數
定義
在CPP中,類的構造函數可以省略不寫,這時CPP會為它自動創(chuàng)建一個隱式默認構造函數(implicit default constructor);也可以由用戶定義帶參數的構造函數,構造函數也是一個成員函數,他可以被重載;當一個構造函數只有一個參數,而且該參數又不是本類的const引用時,這種構造函數稱為轉換構造函數(non-explicit ont argument constructor)。(該段引自百度百科)
class Complex
{
private:
double real,imag; //復數的實部和虛部
public:
Complex(double x)
{
real=x;
imag=0;
}
//與下方等價
/*
Complex(double x,double y=0):real(x),imag(y)
{
}
*/
};
這個構造函數即 轉換構造函數。
如上文。構造函數只有一個參數 double x,它也不是本類的const引用。
應用
通過轉換構造函數可以將一個指定類型的數據轉換為類的對象。
1.用于定義
轉換構造函數一般由系統(tǒng)自動調用(當然代碼里自己調用完全沒問題),這點很利于編程。
例如:
- Complex t=5.0;
- Complex t(5.0);
- Complex t=Complex(5.0);
- Complex t=(Complex)5.0;
這時系統(tǒng)就自動調用了 Complex(double x)將 5.0轉換成Complex類,再賦值給t。
2.用于計算
通常來講,轉換構造函數更多搭配運算符重載用來計算。
class Complex
{
public:
Complex(double x,double y=0)//轉換構造
:real(x),imag(y){}
Complex operator+(const Complex& f)//操作符重載
{
return Complex(......);
}
private:
double real;
double imag;
};
Complex t=5.0;
Complex b=t + 4.8;
編譯器會隱式調用轉換構造函數將5.0轉換為Complex成員并賦值給t。第二步同理,將4.8轉換成Complex成員后調用'+'的重載函數完成計算。
類型轉換函數
通過轉換構造函數可以將一個指定類型的數據轉換為類的對象。但是不能反過來將一個類的對象轉換為一個其他類型的數據(例如將一個Complex類對象轉換成double類型數據)。
C++提供類型轉換函數(type conversion function)來解決這個問題。類型轉換函數的作用是將一個類的對象轉換成另一類型的數據。如果已聲明了一個Complex類,可以在Complex類中這樣定義類型轉換函數:
operator double() const//類型轉換函數
{
return real;
}
從函數結構來看,與重載函數類似,都需要關鍵字operator,只不過這里的轉換的是類型而已,double在Complex類中經過重載后,Complex就被賦予了一種新的含義,既可以當做Complex類型本身使用,也可以當做double類型來使用。
我們來舉一個簡單的例子:
class Complex
{
public:
Complex():real(0),imag(0)
{}
Complex(double x,double y):real(x),imag(y)
{}
operator double() const
{
return real;
}
private:
double real;
double imag;
};
Complex t(5,0);
Complex b=t + 4.8;
此時我們的b=t+4.8運算有了另一種解法,即將Complex對象t通過隱式調用類型轉換函數轉換為double對象完成計算。
小結:
- 轉換構造函數可以將一個指定類型的數據轉換為類的對象。
- 類型轉換函數可以將一個類的對象轉換為一個其他類型的數據。
我們了解了轉換構造函數與類型轉換函數可以為Complex b=t+4.8這樣的運算提供兩個不同角度的解法,那么如果Complex類同時擁有了兩種函數,又會怎樣呢?
class Complex
{
public:
Complex():real(0),imag(0)
{}
Complex(double x,double y=0)//轉換構造
:real(x),imag(y){}
Complex(double x,double y):real(x),imag(y)
{}
Complex operator+(const Complex& f)//操作符重載
{
return Complex(......);
}
operator double() const//
{
return real;
}
private:
double real;
double imag;
};
Complex t(5,0);
Complex b=t + 4.8;
編譯器會提示ambiguous(歧義),即有多重解。當編譯器可選擇的方案不止一種,會出現(xiàn)這種提示。在案例中,編譯器既可以通過轉換構造函數將4.8轉換為Complex對象,與可以通過類型轉換函數將t轉換為double對象完成計算。
但是在實際使用的過程中,難免會遇到這種情況,好在CPP為我們提供解決的辦法:explicit。
explicit關鍵字多用在轉換構造函數之前,其作用是指定該構造函數只能被顯示調用(即創(chuàng)建實例時的調用,如Complex a(5,0)),而不可以再被隱式調用,這樣就解決了程序的ambiguous問題。
第四節(jié) pointer-like classes(關于智能指針)
我們知道智能指針能夠比原生指針做更多事情,例如處理線程安全,提供寫時復制,確保協(xié)議,并且提供遠程交互服務等等等等許許多多強大的功能。但其實不論是多牛的智能指針,在它的內部一定至少有一個原生指針在工作。現(xiàn)在就我們從語法的角度來初窺智能指針。
以shared_ptr為例:
template<class T>
class shared_ptr
{
public:
T& operator*() const//重載*
{return *px;}
T* operator->() const//重載->
{return px;}
shared_ptr(T* p):px(p){}
private:
T* px;
long* pn;
}
struct Foo
{
void method(){}
};
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);
sp->method();
//px->method();
解析:
智能指針的本質,其實就是把一個原生指針包裝在類中,再向類中寫進各種對指針操作符的重載,使用戶在使用類時可以完全按照指針的語法去使用。這樣做的優(yōu)勢很明顯,我們可以根據自己的需求向類中寫入各種功能,相當于“組裝一個無所不能的指針”。
語法其實不難理解,只是重載一下指針的操作符"*"與"->",但是其中有一個小細節(jié)很容易被忽略,及時是有多年經驗的工程師也未必能解釋清楚,在這里再次感謝侯老師。我舉個例子:
我們已經對操作符“*”和“->”進行了重載,當編譯器在執(zhí)行*sp時,返回值是*px,這很好理解,可是在執(zhí)行sp->時,返回值是sp,為什么能起到和sp->一樣的效果呢?
原來,CPP為了支持這種做法,在這里進行了特殊的處理,使得->可以無限次的使用,即在sp之后自動補齊->。(注:只有在這種情況下)
看完了shared_ptr,我們再來看看迭代器。
迭代器(iterator)是一種對象,它能夠用來遍歷標準模板庫容器中的部分或全部元素,每個迭代器對象代表容器中的確定的地址。迭代器修改了常規(guī)指針的接口,所謂迭代器是一種概念上的抽象:那些行為上像迭代器的東西都可以叫做迭代器。然而迭代器有很多不同的能力,它可以把抽象容器和通用算法有機的統(tǒng)一起來。
迭代器提供一些基本操作符:*、++、==、!=、=。這些操作和C/C++“操作array元素”時的指針接口一致。不同之處在于,迭代器是個所謂的復雜的指針,具有遍歷復雜數據結構的能力。其下層運行機制取決于其所遍歷的數據結構。因此,每一種容器型都必須提供自己的迭代器。事實上每一種容器都將其迭代器以嵌套的方式定義于內部。因此各種迭代器的接口相同,型號卻不同。這直接導出了泛型程序設計的概念:所有操作行為都使用相同接口,雖然它們的型別不同。(以上定義摘自百度百科)
下面我們來看一下迭代器的實現(xiàn):
template<class T>
struct __list_node
{
void* prev;
void* next;
T data;
}
template<class T,class Ref,class Ptr>
struct __list_iterator
{
typedef __list_iterator<T,Ref,Ptr> self;
typedef Ptr pointer;
typedef Ref reference;
typeder __list_node<T>* link_type;
link_type node;
bool operator==(const self& x)const{return node==x.node;}
bool operator!=(const self& x)const{return node!=x.node;}
reference operator*()const{return (*node).data;}
pointer operator->()const{return &(operator*());}
self& operator++(){node=(link_type)((*node).next);return *this;}
self operator++(int){self tmp=*this;++*this;return tmp;}
self& operator--(){node=(link_type)((*node).prev);return *this;}
self& operator--(int){self tmp=*this;--*this;return tmp;}
};
我們把其中隔開的兩個函數抽出,簡單的分析一下。
從使用者的角度來講,只會通過右上角的方式來調用,然而實際的處理過程如左側所示:
- 當執(zhí)行*ite時,會獲得(*node).data;其中*node為一個object,data為其中的成員。
- 當執(zhí)行ite->method()時,會調用上方的operator*()獲得(*node).data,返回其地址。
這樣一來就完美的將原生指針node包裹在了迭代器__list_iterator中。
第五節(jié) function-like classes (仿函數)
仿函數在標準庫中有著廣泛的應用,這節(jié)課我們將從標準庫中抽取一個案例來探討仿函數的用法,對于為什么要讓一個類模仿函數行為,這節(jié)我們不做討論。
通常來講,如果一個東西可以接收小括號這種操作符我們就叫它函數,或者像函數的東西。
上面是標準庫中的一段代碼(有省略)。
select1st與select2st分別通過對()的重載提取pair對象的第一個元素和第二個元素。
圖片中灰色處省略了部分代碼,展開如下:
再來看看標準庫中其它的仿函數:
我們發(fā)現(xiàn)標準庫中的仿函數通常要繼承一些古怪的base,下面是base的原型:
在這里我們不對base做任何討論,在后面有專門講解STL的課程會深入講解。
第六節(jié) namespace 經驗談
#include<iostream>
namespace lalala
{
int a=5;
}
namespace lalala1
{
int a=10;
}
int main()
{
std::cout<<lalala::a<<std::endl;
std::cout<<lalala1::a<<std::endl;
return 0;
}
很小的話題,給出一段示例代碼,相信有一定C++基礎的人都可以理解。
第七節(jié) class template
template<typename T>
class complex
{
public:
complex(T r=0,T i=0)
:re(r),im(i)
{}
complex& operator +=(const complex&);
T real () const { return re; }
T imag () const { return im; }
private:
T re,im;
friend complex& _doapl(complex*,const complex&);
};
complex<double> c1(2.5,1.5);
complex<int> c2(2,6);
解析:
template的基本使用方法,不做贅述。
在定義object時指明模板類型,傳入后與符號T綁定。
PS:在template<...>的尖括號中,class與typename是等價的。
第八節(jié) Function Template(函數模板)
class stone
{
public:
stone(int w,int h,int we)
:_w(w),_h(h),_weight(we)
{}
bool operator< (const stone& rhs) const
{ return _weight < rhs._weight; }
private:
int _w,_h,_weight;
}
template<class T>
inline
const T& min(const T& a,const T& b)
{
return b<a?b:a;
}
stone r1(2,3),r2(3,3),r3;
r3=min(r1,r2);
解析:
stone兩個object r1,r2,傳入min函數。min函數在接收參數后將參數類型與模板類型T綁定(實參引導),確認類型后a,b進行'<'操作,編譯器會進入T類(stone)類內尋找對應的重載函數來執(zhí)行。
PS:該用法在“(GeekBand)C++面向對象高級編程(上)第二周筆記(1)”中有過介紹。