三大函數——拷貝構造、拷貝指派、析構函數

拷貝構造——接受的是自己這種東西
ctor和dtor構造函數和析構函數
字元串有兩種:
一種是前面有一個常數,用于記錄字元串的長度,此字元串的末尾沒有結束符号。
另一種是字元串的末尾有結束符号,字元串的開頭沒有用于記錄字元串長度的常數。
new就是配置設定記憶體,配置設定了一個字元的記憶體。
配置設定了一個字元的記憶體,然後把結束符傳進來,這樣就形成了一個空字元串
strlen是一個函數,擷取字元串的長度(strlen是計算機C語言函數,計算字元串s的(unsigned int型)長度,不包括'\0'在内)你的class裡面有指針,你多半是要做動态配置設定。是以你要在他生命結束前,調用析構函數,把記憶體釋放掉)
拷貝指派函數
如圖中的紅框①②③,是要把右手裡面的東西拷貝指派給左邊的步驟:
a)清空左邊的東西
b)申請和右邊一樣大的記憶體空間
c)拷貝
如果沒有上面那句檢測自我指派(
),會出現如下情況:
檢測是否為自我指派,不僅僅是為了效率,還是為了安全性。
output 函數
為了列印類中的東西,我們要重載操作符 "<<",由于成員函數有預設this指針,如果将重載"<<"設定成成員函數,那麼變量的位置要發生改變,這不符合人們的使用規範,是以,重載"<<"要設定成全局函數。
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
任何東西,隻要你能直接丢給cout,你就直接丢給他輸出好了。先看一下整體代碼:
#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 = '\0';
}
}
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
注意到 get_c_str函數的傳回值是char *,剛剛好可以直接給cout進行輸出,是以我們寫了get_c_str函數來進行輸出。
堆棧與記憶體管理
stack object 的生命周期
static local object
global object 的生命周期
heap object 的生命期
new——先配置設定記憶體,後調用構造函數
new的動作分解:
a)調用 operator new 函數來配置設定記憶體(operator new 底層調用的是malloc)。對應的,上圖配置設定出
b)第二個動作把我們建立的變量做一個類型轉換
c)通過指針調用構造函數Complex(注意:構造函數在類裡面,是以是成員函數,會有this指針。誰調用成員函數,this指針就指向誰。是以,上圖中的第三步完整的寫法應該是如圖所示的形式:
。
這裡的this指針指向了pc)
delete:先調用析構函數,再釋放記憶體
動态配置設定記憶體塊 in VC
根據上圖第一個矩形(debug模式下的情況):
1、new 一個複數會獲得的記憶體是 8 byte(上圖中第一個矩形草綠的部分)。
2、在調試模式下,你會得到灰色的部分,上面每一格是4byte(即
),一共 4*8=32個位元組。
3、還會得到草綠色矩形下面的那一個 4byte(即
)
4、上下兩個磚紅色的矩形區域是cookie
記憶體一共需要 8+(32+4)+(4*2)=52,而VC給你配置設定的記憶體塊一定是16的倍數(現在不提為什麼),是以填補了三個深綠色的填補物pad
看起來配置設定很浪費,但是這是必要的浪費,因為回收的時候,作業系統需要依據這裡的資訊來進行回收。
根據上圖中第二個矩形(release 模式):
配置設定記憶體8byte,加上上下cookie剛剛好16byte,是以無需調整,無需添加填補物pad。
上下cookie的作用
記錄整塊給你的記憶體大小,便于系統回收,讓系統知道回收多大記憶體。
讓我們看一下cookie上記錄的數字——000000041,對于第一個矩形,記憶體一共配置設定了64byte,64的16進制表示是40,而cookie上的數字是41,為什麼呢?40借助最後一個bit,最後一位,标志我這塊記憶體是給出去了還是收回來了。這裡是給出去了,對于程式來說是獲得了,是以最後一位标志位1,是以是41。
同理,對于第二個矩形,系統給出的記憶體是16byte,16的16進制是10,這裡cookie上寫的是11,因為這是程式獲得的記憶體。
為什麼可以借最後一個bit來标志這一塊記憶體是給出了還是收回了?
因為配置設定的記憶體都是16的倍數,16的倍數最後四個bit都是0,我們就可以借一位來表示記憶體的狀态。
為什麼array new 要搭配 array delete
分析上圖中第一個矩形(debug模型下的array new配置設定的記憶體分析)
1、複數申請的數組長度是3,是以申請了三塊記憶體(
2、在調試模式下要加上那個header,上面是32下面是4(即
和
3、加上向下cookIe(
4、在VC中(别的編譯器不明),會用一個4byte的記憶體記錄數組的長度,即圖中的
是以記憶體配置設定為 8*3+32+4+4*2+4=72 ,湊16的倍數,是以配置設定記憶體為80。
array new要搭配array delete
否則會造成記憶體洩漏。讓我們看看是哪一種記憶體洩漏
記憶體洩漏的是動态配置設定的記憶體。
寫不寫 [],清空的記憶體大小是相等的,因為cookie上有記錄。但是不寫 [] ,編譯器不知道要對每一個對象進行分别的析構。析構的時候,先分别析構各個記憶體對象,然後再析構母體的那個地方(
)。發生記憶體洩漏的是這裡(
複習string的實作過程
設計一個class,我總是要去思考我需要什麼樣的資料。由于不知道字元串的長度,是以大部分人設計字元串這種類中的資料都是在裡面放一根指針,将來要配置設定多大的字元串内容,就動态地去配置設定字元串的大小,用new的方式去動态配置設定一塊記憶體(在32位的平台裡面,一根指針占記憶體是4位元組,是以不管你裡面字元有多長,字元串本身就4個位元組的記憶體)
Class裡面帶指針,是以我要關注三個重要的函數:
拷貝構造:他是一個構造函數,是以沒有傳回值。他要有一個拷貝藍本,藍本就是他自己(傳入reference是可以的,又因為我們不會改變藍本,是以前面可以加一個const)
拷貝指派:指派是要把來源段的拷貝到目的端,是以涞源段的内容和拷貝構造是相同的(是以他傳入的參數和拷貝構造的參數是相同的)
因為傳入的值我們不打算去改變他,是以前面加一個const。
拷貝指派的傳回值(要不要return by reference,要看函數執行所傳回的結果是不是放在裡local object中,隻要不是local object,就可以傳reference)
析構函數:
輔助函數:我們希望把最後的結果丢給cout來輸出到螢幕上(加了const是因為不會改變資料)
拷貝指派函數:
涞源段拷貝到目的端,目的端是已經存在的東西,是以
第一個動作應該是把目的端的記憶體清空
第二個動作是重新配置設定一塊夠大的空間:
第三個動作是把來源端拷貝到目的端:
接下來要思考指派之後的傳回值(如果不寫傳回值的話,連串的指派行為就會受限)
&str得到的是一根指針
String&是引用
擴充補充:類模闆、函數模闆以及其他
進一步補充:static
誰調用我,誰就是那個this pointer,是以c的位址就是this pointer
成員函數有一個隐藏的參數this pointer,但是我們不能寫進去,這個是編譯器幫我們寫進去
靜态資料:加入了static的資料,就跟對象脫離了,他不屬于對象,他在記憶體的某一個區域單獨有一份,我們不必知道他在那裡,反正後面的代碼能夠找得到就好了
靜态函數:他的身份跟一般的成員函數字記憶體中是相同的,我們所指的相同指的是他也在記憶體中隻有一份。函數在記憶體中當然隻有一份,不可能因為你建立了好幾個對象,就有好幾個函數
靜态函數跟一般函數的差别就在于,靜态函數沒有this pointer。是以靜态函數如果去處理資料,他隻能去處理靜态的資料。
例子:
進一步補充:把ctors(構造函數)放在private區
當我們希望寫的class隻産生一個對象的時候,可以這麼用。
把構造函數寫在private裡面,這樣外界就無法再建立對象。
這麼寫有個缺陷,就是如果外界不需要這個資料,這個資料依然存在,這樣會造成記憶體的浪費。更好的寫法如下:
進一步補充:cout
為什麼cout可以接受任何類型的資料,因為裡面重載了很多
進一步補充:類模闆
模闆會造成代碼的膨脹,但是這并不是缺點,因為你确實是需要這種類型的函數,即使不用模闆,你也要寫出來
進一步補充:function template,函數模闆
類模闆在用的時候要明确指出類型(
),函數模闆則不需要,因為編譯器會做實參的推導(argument deduction)
進一步補充:namespace
namespace等同于你把你的東西都封鎖在這個命名空間裡了,這樣就不會打架。
Using directive(使用指令):等同于你把封鎖打開,調用的時候就用寫全名(e.g std::cin)了,可以直接寫cin
Using declaration:一行一行的打開,不是全開,因為裡面東西可能會很多
或者是都不打開,就每一步都規規矩矩的寫全名