2.1 拷貝構造函數 拷貝賦值運算符 析構函數
2.1.1拷貝構造函數
第一個參數必須是自身類類型的引用,且任何額外參數都有默認值。(為什么必須是引用?見后解釋)
合成拷貝構造函數:如果我們沒有為一個類定義拷貝構造函數,則編譯器會為我們定義一個。同合成的默認構造函數不同的是,即使我們定義了其他構造函數,編譯器也會為我們合成一個拷貝構造函數。(一旦自己定義了構造函數,則不會合成默認構造函數)
拷貝初始化與直接初始化
直接初始化:要求編譯器使用普通的函數匹配來選擇與我們提供的參數最匹配的構造函數。
拷貝初始化:要求編譯器將右側運算對象拷貝到正在創建的對象中,如果需要的話,還要進行類型轉換。
string dots(10, '.');? ? ? ? ? ? ? //直接初始化
string s(dots);? ? ? ? ? ? ? ? ? ? //直接初始化
string s2 = dots;? ? ? ? ? ? ? ? ? //拷貝初始化
string null_book = "9-999-8999";? ? //拷貝初始化
string nines = string(100, '9');? ? //拷貝初始化
使用‘=’號的是拷貝初始化,不使用等號的是直接初始化。
拷貝初始化發生在以下情況
1. 用 = 定義變量時發生。
2. 將一個對象作為實參傳遞給一個非引用類型的形參。
3. 從一個返回類型為非引用類型的函數返回一個對象。
4. 用花括號列表初始化一個數組中的元素或一個聚合類中的成員。(聚合類是指沒有用戶定義的構造函數,沒有私有和保護的非靜態數據成員,沒有基類,沒有虛函數)。
拷貝構造函數第一個參數必須是引用原因:由于拷貝構造函數被用來初始化非引用類類型的參數。如果其自身參數不是引用類型,則調用永遠也不會成功——為了調用拷貝構造函數,我們必須拷貝它的實參,但為了拷貝實參,我們又必須調用拷貝構造函數,如此無限循環。
2.1.2 拷貝賦值運算符
與類控制其對象如何初始化一樣,類也可以控制器對象如何賦值:
Sales_data trans, accum;
trans = accum;? //使用Sales_data的拷貝賦值運算符
與拷貝構造函數一樣,如果類未定義自己的拷貝賦值運算符,編譯器也會為它合成一個。
重載賦值運算符
重載運算符本質上是函數,其名字由operator關鍵字后接表示要定義的運算符的符號組成。因此,賦值運算符就是一個名為operator=的函數。類似于任何其他函數,運算符函數也有一個返回類型和一個參數列表。
如果是一個運算符是一個成員函數,其左側運算對象就綁定到隱式的this參數。對于一個二元運算符,例如賦值運算符,其右側運算對象作為顯式參數傳遞。
拷貝賦值運算符接受一個與其類相同類型的參數:
class Foo{
public:
Foo& operator=(const Foo&);? //賦值運算符
//...
};
為了與內置類型的賦值保持一致,賦值運算符通常返回一個指向其左側運算對象的引用。注意,標準庫通常要求保存在容器中的類型要有其賦值運算符,且其返回值是左側運算對象的引用。
2.1.3 析構函數
析構函是類的一個成員函數,名字由波浪號接類名構成。它沒有返回值,也不接受參數:
class Foo{
public:
~Foo();? //析構函數
//...
};
由于析構函數不接受參數,因此它不能被重載。對于一個給定類,只會由唯一一個析構函數。
在一個構造函數中,成員的初始化時在函數體執行之前完成的,且按照它們在類中出現的順序進行初始化。在一個析構函數中,首先執行函數體,然后銷毀成員。成員按初始化順序的逆序進行銷毀。
無論何時一個對象被銷毀,就會自動調用其析構函數:
1. 變量在離開其作用域時被銷毀
2. 當一個對象被銷毀時,其成員被銷毀
3. 容器(無論是標準容器還是數組)被銷毀時,其元素被銷毀
4. 對于動態分配的對象,當對指向它的指針應用delete運算符時被銷毀
5. 對于臨時對象,當創建它的完整表達式結束時被銷毀
2.2 堆 棧和內存管理
a) 棧:內存由編譯器在需要時自動分配和釋放。通常用來存儲局部變量和函數參數。(為運行函數而分配的局部變量、函數參數、返回地址等存放在棧區)。棧運算分配內置于處理器的指令集中,效率很高,但是分配的內存容量有限。
b) 堆:內存使用new進行分配,使用delete或delete[]釋放。如果未能對內存進行正確的釋放,會造成內存泄漏。但在程序結束時,會由操作系統自動回收。
c) 自由存儲區:使用malloc進行分配,使用free進行回收。和堆類似。
d) 全局/靜態存儲區:全局變量和靜態變量被分配到同一塊內存中,C語言中區分初始化和未初始化的,C++中不再區分了。(全局變量、靜態數據、常量存放在全局數據區)
e) 常量存儲區:存儲常量,不允許被修改。
這里,在一些資料中是這樣定義C++內存分配的,可編程內存在基本上分為這樣的幾大部分:靜態存儲區、堆區和棧區。他們的功能不同,對他們使用方式也就不同。
a)靜態存儲區:內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。它主要存放靜態數據、全局數據和常量。
b)棧區:在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。
c)堆區:亦稱動態內存分配。程序在運行的時候用malloc或new申請任意大小的內存,程序員自己負責在適當的時候用free或 delete釋放內存。動態內存的生存期可以由我們決定,如果我們不釋放內存,程序將在最后才釋放掉動態內存。 但是,良好的編程習慣是:如果某動態內存不再使用,需要將其釋放掉,否則,我們認為發生了內存泄漏現象。
2.3 string類的實現
String里涉及動態內存的管理,默認的拷貝構造函數在運行時只會進行淺復制,即只復制內存區域的指針,會造成兩個對象指向同一塊內存區域的現象。如果一個對象銷毀或改變了該內存區域,會造成另一個對象運行或者邏輯上出錯。這時就要求自己實現這些函數進行深復制,即不止復制指針,需要連同內存的內容一起復制。
//代碼參考C++primer.//String類的實現,
#include using namespace std;
class String{
friend ostream& operator<< (ostream&,String&);
public:
String(const char* str=NULL);??????????????? //賦值構造兼默認構造函數(char)
String(const String &other);???????????????? //賦值構造函數(String)
String& operator=(const String&other);?????? //operator=
String operator+(const String &other)const;? //operator+
bool operator==(const String&);????????????? //operator==
char& operator[](unsigned int);????????????? //operator[]
size_t size(){return strlen(m_data);};
~String(void) {delete[] m_data;}
private:
char *m_data;
};
inline String::String(const char* str)
{
if (!str) m_data=0;
else
{
m_data = new char[strlen(str)+1];
strcpy(m_data,str);
}
}
inline String::String(const String& other)
{
if(!other.m_data) m_data=0;
else
{
m_data=new char[strlen(other.m_data)+1];
strcpy(m_data,other.m_data);
}
}
inline String& String::operator=(const String& other)
{
if (this!=&other)
{
delete[] m_data;
if(!other.m_data) m_data=0;
else
{
m_data = new char[strlen(other.m_data)+1];
strcpy(m_data,other.m_data);
}
}
return *this;
}
inline String String::operator+(const String &other)const
{
String newString;
if(!other.m_data)
newString = *this;
else if(!m_data)
newString = other;
else
{
newString.m_data = new char[strlen(m_data)+strlen(other.m_data)+1];
strcpy(newString.m_data,m_data);
strcat(newString.m_data,other.m_data);
}
return newString;
}
inline bool String::operator==(const String &s)
{
if ( strlen(s.m_data) != strlen(m_data) )
return false;
return strcmp(m_data,s.m_data)?false:true;
}
inline char& String::operator[](unsigned int e)
{
if (e>=0&&e<=strlen(m_data))
return m_data[e];
}
ostream& operator<<(ostream& os,String& str)
{
os << str.m_data;
return os;
}
void main()
{
String str1="Hello!";
String str2="Teacher!";
String str3 = str1+str2;
cout<<str3<<"/n"<<str3.size()<<endl;
}
2.4 函數模板 類模板
2.4.1函數模板
重載函數通常用于對不同的數據類型執行相似的操作,不同數據類型的程序邏輯可能有所不同。如果每種數據類型的程序邏輯和操作都相同,則可以使用函數模板來更緊湊、更方便地實現函數重載。
就本質而言,定義一個函數模板就定義了一群重載函數。
所有的函數模板定義都從關鍵字template開始,后接它的模板參數表,列表位于一對尖括號(<和>)中。表示類型的每個模板參數,其前面都必須帶關鍵字class或template(二者可以互換),例如:
template?<?typename?T>???或
template?<?class??ElementType>??或
template?<?typename??BorderType,?typename??FillType>
函數模板定義中的類型模型參數,用于指定函數實參的類型。函數的返回類型或聲明函數內部的變量。除此之外,函數定義與其他任何函數定義的形式相同。
2.4.2 類模板
類模板類模板稱為參數化類型,因為它需要一個或者多個類型參數來指定如何定制一個“泛型類”模板,以形成一個類模板特殊化。為了產生各種類模板特殊化,只需編寫一個類模板定義。每次需要一個額外的類模板特殊化時,只需要使用一種清晰、簡單的方法,編譯器就能為所需要的類模板特殊化編寫源代碼。
Demo:例如一個Stack類模板,可以是程序中創建許多Stack類的基礎,如:"Stack of? double"、"Stack of? int"、"Stack of? char"、"Stack of? Employee"等等創建類模板Stackstack.h :[cpp] view plain copy #ifndef STACK_H? #define STACK_H? ? ? template//指定一個使用參數類型T的類模板定義
class?stack