#include #include "complex.h"
using namespace std;
ostream& operator << (ostream& os, const complex& x) { return os << '(' << real (x) << ',' << imag (x) << ')'; }
int main() { complex c1(2, 1); complex c2(4, 0);
cout << c1 << endl; cout << c2 << endl;
cout << c1+c2 << endl; cout << c1-c2 << endl; cout << c1*c2 << endl; cout << c1 / 2 << endl;
cout << conj(c1) << endl; cout << norm(c1) << endl; cout << polar(10,4) << endl;
cout << (c1 += c2) << endl;
cout << (c1 == c2) << endl; cout << (c1 != c2) << endl; cout << +c2 << endl; cout << -c2 << endl;
cout << (c2 - 2) << endl; cout << (5 + c2) << endl;
return 0; }
如上圖所示,在重載「=」賦值運算子時需要檢查自我賦值,原因如下:
如果沒有自我賦值偵測,那麼自身物件的m_data將會被釋放,m_data指向的內容將不存在,所以該拷貝會出問題。
這裡以兩個類別做出說明:
class complex
{
public:
complex (double r = 0, double i = 0)
: re (r), im (i)
{ }
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex*,
const complex&);
};
class String
{
public:
String(const char* cstr = 0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
建立這兩個物件後,編譯器(VC)給兩個物件分配記憶體如下:
左邊兩個是類別complex在調試模式和release模式下的編譯器記憶體分配。在debug模式下,編譯器給complex物件記憶體插入了頭和尾(紅色部分),4*8 + 4大小的資訊部分(灰色部分),綠色部分是complex物件實際佔用的空間,計算後只有52字節,但VC以16位元組對齊,所以52最近的16倍數是64,還應該填補12位元組的空缺(青色pad部分)。對於release部分的complex對象,只增加了資訊頭和偉部分。 string類別的分析基本一樣。
接下來看對於數組對象,VC編譯器是如何分配的:
類似的,編譯器為對象增加了一些冗餘資訊部分,對於complex類別對象,由於數組有三個對象,則存在8個double,然後編譯器在3個complex對象前插入“3”用於標記對象個數。 String類別的分析方法也類似。
下面這張圖說明了為何刪除陣列物件需要使用delete[]方法:
String.h
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
public:
String(const char* cstr=0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
#include <cstring>
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = ' ';
}
}
inline
String::~String()
{
delete[] m_data;
}
inline
String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
#include <iostream>
using namespace std;
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
#endif
string_test.cpp
#include "string.h"
#include <iostream>
using namespace std;
int main()
{
String s1("hello");
String s2("world");
String s3(s2);
cout << s3 << endl;
s3 = s1;
cout << s3 << endl;
cout << s2 << endl;
cout << s1 << endl;
}
C++中, 一個參數的建構函式(或除了第一個參數外其餘參數都有預設值的多參建構子), 承擔了兩個角色。 1 是個建構器,2 是個預設且隱含的型別轉換操作符。
所以, 有時候在我們寫下如AAA = XXX, 這樣的程式碼, 且剛好XXX的型別剛好是AAA單參數建構器的參數型別, 這時候編譯器會自動呼叫這個建構器, 建立一個AAA的物件。
這樣看起來好像很酷, 很方便。 但在某些情況下(見下面權威的例子), 卻違背了我們(程式設計師)的本意。 這時候就要在這個構造器前面加上explicit修飾, 指定這個構造器只能被明確的呼叫/使用, 不能作為型別轉換操作子被隱含的使用。
explicit的建構函式是用來防止隱式轉換。請看下面的程式碼:
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}
Test1的建構子帶一個int型的參數,程式碼23行會隱式轉換成呼叫Test1的這個建構子。而Test2的建構函數被宣告為explicit(顯式),這表示不能透過隱式轉換來呼叫這個建構函數,因此程式碼24行會出現編譯錯誤。
普通構造函數能夠被隱式呼叫。而explicit建構函數只能被明確地呼叫。
任何Fraction需要被轉換為double類型的時候,自動呼叫double()函數來轉換。如上圖所示,編譯器在分析double d = 4 + f過程中判斷4為整數,然後繼續判斷f,觀察到f提供了double()函數,然後會對f進行double()操作,計算得到0.6 ,再與4相加,最後得到double類型的4.6。
上圖中定義了一個類,叫Fraction,類別裡面重載了「+」運算符,在f+4操作過程中,「4」被編譯器隱式轉換(建構子)為Fraction對象,然後經由Fraction重載的“+”運算符參與運算。
如上圖所示,在Fraction中增加了double()函數,將Fraction的兩個成員變數進行除法運算,然後強制轉換為double類型並傳回結果,在f+4重載過程中編譯器將報錯,可以做出如下分析:
1.首先4被隱式轉換(建構子)為Fraction對象,再透過重載的「+」運算子與「f」進行運算傳回一個Frction物件;
2.首先4被隱式轉換(建構子)為Fraction對象,再透過重載的「+」運算子與「f」運算後對進行double運算,最返回一個Frction物件;
3、。 。 。
所以編譯器有至少兩條路可以走,於是產生了二義性,報錯。 如上圖所示,在建構函數Franction前加入explict關鍵字,隱式轉換會取消,所以在執行d2 = f + 4過程中,f將呼叫double函數轉換為0.6,然後與4相加變成4.6,由於建構函數取消隱公式轉換,4.6無法轉換為Fraction,於是將報錯。
下圖為C++ stl中操作符重載與轉換函數的一個應用:
下面這張圖很好地說明了智慧指針的內部結構和使用方法: 智慧指針在語法上有三個很關鍵的地方,第一個是保存的外部指針,對應於上圖的T* px,這個指標將代替傳入指標進行相關傳入指標的操作;第二個是重載“*”運算符,解引用,傳回一個指標所指向的物件;第三個是重載“->”運算符,傳回一個指針,對應上圖就是px。
迭代器也是一種智慧指針,這裡也存在著上述的智慧指針的三個要素,分別對應於下圖的紅色字體和黃色標註部分:
以下將仔細分析迭代器重載的“*”和“->”重載符:
建立一個list迭代器對象,list::iterator ite;這裡的list用來保存Foo對象,也就是list模板定義裡的class T,operator*()回傳的是一個(*node).data對象,node是__link_type類型,然而__link_type又是__list_node<T>*類型,這裡的T是Foo,所以node是__list_node<Foo >*類型,所以(*node).data得到的是Foo類型的一個對象,而&(operator*())最終得到的是該Foo對象的一個地址,即返回Foo*類型的一個指標。
重上圖可以看到,每個仿函數都是某個類別重載“()”運算符,然後變成了“仿函數”,實質還是一個類,但看起來具有函數的屬性。每個仿函數其實在背後都整合了一個奇怪的類,如下圖所示,這個類不用程式設計師手動顯式聲明。 標準庫中的仿函數也同樣繼承了一個奇怪的類別: 這個類別的內容如下圖所示,只是聲明了一些東西,裡面沒有實際的變數或函數,具體的內容將在STL中討論。
與類別模板不同的是,函數模板在使用是不需要明確地聲明傳入參數類型,編譯器將自動推導類型。
成員模板在泛型程式設計裡用得較多,為了有更好的可擴展性,以上圖為例,T1往往是U1的基類,T2往往是U2的基類,可以看下面這個例子: 透過這種方法,只要傳入的U1和U2的父類別或祖類是T1和T2,那麼透過這樣的方式可以實現繼承和多態的巧妙利用,但反之就不行了。這樣的方式在STL中用得很多:
如其名,在模板偏化指的是模板中指定特定的資料類型,這和泛化是不同的: 當然,模板偏化也有程度之分,可以部分類型指定,稱之為偏特化:
太多內容將在C++11課程講解,這裡暫時只做介紹。
太多內容將在C++11課程講解,這裡暫時只做介紹
reference可以看做是某個被引用變數的別名。
如上圖所示,定義了三個類,A、B和C,B繼承於A,C繼承於B,A中有兩個虛函數,B中有一個,C中也有一個。編譯器將A的物件a在記憶體中分配如上圖所示,只有兩個成員變數m_data1和m_data2,同時,由於A類別有虛函數,編譯器將給a物件分配一個空間用於保存虛函數表,這張表維護著該類別的虛函數位址(動態綁定),由於A類有兩個虛函數,於是a的虛函數表中有兩個空間(黃藍空間)分別指向A::vfunc1()和A::vfunc2();同樣的,b是B類的一個對象,由於B類重寫了A類的vfunc1()函數,所以B的虛函數表(青色部分)將指向B::vfunc1(),同時B繼承了A類的vfunc2(),所以B的虛函數表(藍色部分)將指向父類A的A: :vfunc2()函數;同樣的,c是C類的一個對象,由於C類別重寫了父類別的vfunc1()函數,所以C的虛函數表(黃色部分)將指向C::vfunc1(),同時C繼承了超類別A的vfunc2(),所以B的虛函數表(藍色部分)將指向A::vfunc2()函數。同時上圖也用C語言程式碼說明了編譯器底層是如何呼叫這些函數的,這就是物件導向繼承多態的本質。
this指針其實可以認為是指向當前物件記憶體位址的指針,如上圖所示,由於基底類別和子類別中有虛函數,this->Serialize()將動態綁定,等價於(*(this- >vptr)[n])(this)。可以結合上節虛指標和虛函數表來理解,至於最後為什麼這樣寫是正確的,下面小結將會解釋。