class xxx { xxx (const xxx &that) {...} };
&that是引用,拷貝構造函數推薦使用引用,如果直接傳遞值,那麼會導致無限遞歸;
如果一個類沒有定義拷貝構造函數,系統會提供一個缺省拷貝構造函數;
缺省拷貝構造函數對於基本類型的成員變量,按字節複製;
對於類類型成員變量,調用相應類型的拷貝構造函數,如 string;
class A {
public:
A (int data) {...}
// 當執行構造函數時,相當於執行下面這條缺省拷貝構造函數:
// A (const A &that) { data(that.data) {...} };
private:
int m_data;
};
void foo (A data) {...}
A bar() { A i; return i; }
int main(){
A a(10);
A b(a); // 拷貝構造,a 和 b 的值一樣;ok
A c = a; // 這裏用 a 初始化 c,也是拷貝構造;相當於 A c(a); ok
foo(a); // 也構成拷貝;ok
A d = bar(); // error,雖然這裏的 bar 返回的也是 A 類型,但由於編譯器自身的優化原則,這裏不屬於拷貝構造;
// 只是把 bar 的匿名對象起個新名字 d,並沒有調用拷貝構造
A e(bar()); // 同上;error
A f(e); // 此時就調用拷貝構造;
return 0;
}
2. 在某些情況下,缺省拷貝構造函數只能實現淺拷貝,
如果需要獲得深拷貝的複製效果,就需要自己定義拷貝構造函數;
一般下面兩種情況需要自定義拷貝構造函數:
如果成員裏面包含指針或者引用;
如果成員對象指向 new 地址;
class A {
public:
A (int data) : m_data(new int(data)) { delete m_data; }
private:
int *m_data; // 含有指針
};
int main(){
A a(10);
A b(a);
return 0;
}
編譯時,會提示地址兩次釋放錯誤;
原因:創建 a 的時候,構造函數通過 new 在堆裏面分配空間,並把地址給 a;
當把 a 賦值給 b 的時候,調用拷貝構造函數,同時也就把 new 的地址給了 b;
也就是說,a 和 b 指向同一個堆內存;當 a 執行完,調用析構,釋放了那個內存;
b 執行完也要析構那個內存,但是此時內存已經不存在了,所以編譯器報錯,重複釋放堆內存;
這裏 a 和 b 地址一樣,屬於淺拷貝;
當 a 的值改變的時候,b 也跟着改變,這不符合拷貝原則;
拷貝原則是,拷貝完以後,a 和 b 是兩個獨立的元素;
解決這個問題的辦法是:自己寫一個拷貝構造函數,拷貝時,給 b 單獨分配一個內存:
A (const A &that) { new int data(that.data) {...} }; // 深拷貝
3. 拷貝賦值運算符:
class X { X& operator= (const X& that) {...} };
如果一個類沒有定義拷貝賦值運算符,系統會提供一個缺省拷貝賦值運算符;
缺省拷貝賦值運算符對於基本類型的成員變量,按字節複製;
對於類類型成員變量,調用相應類型的拷貝賦值運算符函數;
class A {
public:
A (int data) : m_data(new int(data)) { delete m_data; }
// A& operator= (const A& that) { m_data = that.m_data); }
int set (int data) {
return *m_data = data;
}
private:
int *m_data;
};
int main(){
A a(10);
A c(20);
A c(a); // 拷貝構造,c 值變爲 10;
A c.set(30); // 給 c 重新賦值 30;
cout << a; // 最後 a 的值變爲 30;而不是原來的 10;
return 0;
}
分析:上面另定義了 c 初值爲 20,
當調用拷貝構造函數,把 a 複製給 c 後,
重新給 c 賦值,那麼這個時候,a 也就被改變了,變成 30;
因爲系統默認調用下面函數:
A& operator= (const A& that) { m_data = that.m_data); }
4. 缺省拷貝賦值運算符只能實現淺拷貝;
如果需要獲得深拷貝的複製效果,就需要自己定義拷貝賦值運算符函數;
解決上面的辦法,可以用下面的代碼:(也是自定義拷貝賦值運算符函數的固定格式)
A& operator= (const A& that){ // 參數裏的 & 是引用符號
if (&that != this){ // 防止自賦值,這裏 & 是取地址,和 this 匹配;
delet m_data; // 釋放舊資源
m_data = new int (*that.m_data); // 申請新空間,拷貝新數據
}
return *this; // 返回自引用
}
如果一個類什麼都沒寫,那麼編譯器會幫我們寫下下面的內容:
class A {
int x;
public:
A() {} // 構造
A(const A&) {} // 拷貝構造
A& oprator= (const A& that) {} // 拷貝賦值
~A() {} // 析構
};
5. 拷貝構造和拷貝賦值私有化:
class A {
public:
A (void) {...}
private:
A (const A&);
A operator= (const A&);
};
6. 靜態成員變量和靜態成員函數是屬於類的,而非屬於對象;
靜態成員變量,可以被多個對象所共享,只有一份實例;
可以通過對象訪問,也可以通過類訪問;
由於靜態成員變量不屬於對象,通過對象訪問,也就是通過對象訪問類,再由類訪問靜態成員變量;
必須在類的外部定義,並初始化;
class A {
public:
static int m_i; // 聲明
};
int A::m_i; // 定義,切記:給靜態成員變量分配內存空間;也可以給其初始化;
int main() {
A::m_i = 10; // 類訪問
A a1, a2;
++a1.m_i; // 對象訪問,並 +1
a2.m_i; // m_i 爲 11
return 0;
}
分析:雖然 m_i 被多個對象訪問,但是這裏只是一個 m_i;
首先通過類 A 訪問 m_i 並賦值爲 10;這時,所有 m_i 都爲 10;
然後 a1 訪問 m_i,並增加 1,這時,所有 m_i 都變爲 11;
所以雖然最後 a2 調用 m_i,沒作任何操作,但是此時的 m_i 值爲 11;
靜態成員變量本質上和全局變量沒有區別;
只是多了作用域和訪控屬性的限制;
我們推薦對靜態成員的訪問用類來訪問,這樣可以增加可讀性,且不用構造對象;
8. 靜態成員函數,區別是沒有 this 指針;
所以無法訪問非靜態的成員;
普通的成員函數可以訪問靜態成員變量;
class A {
public:
int i;
static int j;
void bar() {
j++; // ok, 非靜態函數可以訪問靜態成員
foo(); // ok
}
static void foo() {
j++; // ok, 靜態訪問靜態
i++; // error, 靜態不能訪問非靜態
bar(); // error, 無 this 指針,無法訪問非靜態
cout << this; // error
}
};
int main(){
A::foo(); // ok, 類名調用,推薦
A a;
a.foo(); // ok, 對象調用
}
當功能不需要對象訪問,只用類直接就可以訪問,那麼就定義爲靜態的;
9. 單例模式(靜態實現)
客戶只能創建一個對象;
思路:把構造私有化,不讓創建對象;
在類裏面自己建立一個對象,也私有化;
然後用靜態類,創建一個引用出來,不是對象;
這時需要把拷貝構造也私有化,要不然會通過靜態類創建多個對象;
餓漢模式:(hungry.cpp)
#include <iostream>
using namespace std;
class Single {
private:
Single () {} // 構造寫到私有裏
Single (const Single&); // 拷貝構造
static Single s_inst; // 內部聲明
public:
static Single& getInst(){ return s_inst; } // 和全局函數一樣,不能用 const,因爲無 this 指針;
};
Single Single::s_inst; // 外部定義,分配空間;
int main() {
//Single s; // error,Single被私有
Single& s1 = Single::getInst(); // 拷貝構造被私有,所以這裏只能用引用,不創造新對象;
Single& s2 = Single::getInst();
Single& s3 = Single::getInst();
return 0;
}
分析:雖然這裏有 s1,s2,s3,但是用的是同一個 getInst();
所以s1,s2,s3 的地址是一樣的;
懶漢模式:(lazy.cpp)
#include <iostream>
using namespace std;
class Single {
public:
static Single& getInst() {
if(!m_inst) // 如果爲空
m_inst = new Single;
++m_cn; // 雖然只new一次,但是每引用一次,就加 1
return *m_inst;
}
void releaseInst(){ // 釋放資源
if(m_cn && --m_cn == 0)
delete this; // 完了調用析構,所以在析構裏置空
}
private:
static Single* m_inst;
static unsigned int m_cn;
Single () {} // 構造
Single (const Single&); // 拷貝構造
~Single () { m_inst = NULL; }
};
Single* Single::m_inst = NULL;
unsigned int Single::m_cn = 0;
int main() {
Single& s1 = Single::getInst(); // 調用構造
Single& s2 = Single::getInst();
Single& s3 = Single::getInst();
s3.releaseInst(); // 釋放
s2.releaseInst();
s1.releaseInst();
return 0;
}
結果:只構造了一次,三個地址一樣;
同樣,析構也只析構了一次,最後一次調用的時候析構;
10. 成員變量指針
是一個相對地址,具體參考下面的舉例
1)定義:
成員變量類型 類名 ::*指針變量名;
Student s1; // Student 類假設之前定義過了,裏面有 m_name 成員;
sring* p = &s.m_name; // 這個p不是成員指針,因爲它只指向 s1;
string Student::*p_name; // p_name 指向 Student 類中所有 string 類型的成員
2)初始化:
指針變量名 = &類名 ::成員變量名;
p_name = &Student::m_name;
定義和初始化寫在一起:
string Student::*p_name = &Student::m_name;
3)解引用
對象 .* 指針變量名;
對象指針 ->* 指針變量名;
Student s, *p = &s;
s .* p_name = "zhangfei";
cout << p ->* p_name << endl;
這裏 “點星” 和 “箭頭星” 中間不能空格,這是一個完整操作符,c++ 特有;
成員變量指針(c++03b.avi)
是一個相對地址,具體參考下面的舉例:
class Date {
public:
int year;
int month;
int day;
};
int main() {
Date d = {2012, 2, 23};
int Date::* p1 = &Date::year;
int Date::* p2 = &Date::monty;
int Date::* p3 = &Date::day;
cout << d.*p1; // 輸出 2012;d.*p1 相當於 d.year;
cout << p1 << p2 << p3; // 輸出 1 1 1
printf("%d%d%d", p1, p2, p3); // 輸出 0 4 8
}
分析:cout 輸出 1 1 1,是由於 c++ 自身原因導致的,1 代表 ture,不是真實結果;
利用 printf 輸出 0 4 8,是真正的相對地址;year 相對於 Date 地址爲 0,下面每個相差一個 int 大小;
11. 成員函數指針:
1)定義:
成員函數返回類型 (類名 ::*指針變量名)(參數表)
假如Student類裏面有learn函數:
void learn(const string& lesson) const {}
則定義如下:
void (Student::*p_learn)(const string&) const;
2)初始化:
指針變量名 = &類名 ::成員函數名;
p_learn = &Student::learn;
3)解引用
(對象 . *指針變量名)(實參表);
(對象指針 -> *指針變量名)(實參表);
(s.*p_learn)("C++");
(p->*p_learn)("Linux");
如果是靜態成員函數:
static void hello(void){...}
那麼聲明一樣:
void (*phello)(void) = Student::hello;
但是調用不需要對象,可以直接用,類似於C:
phello();
12. 運算符重載,可以實現不同類型數據間的運算;
運算符分類:
1)雙目操作符:L#R
成員函數形式操作符:L.operator# (R)
左調右參
全局函數形式操作符::: operator# (L, R)
左一右二,左操作數作第一個參數,右操作數作第二個參數
2)單目運算符:#O/O#
成員函數形式操作符:O.operator# ()
全局函數形式操作符::: operator# (O)
3)三目操作符:不考慮,無法重載;
13. 雙目運算符
加、減、乘、除
操作數在計算前後不變;
表達式的值是右值,不可被賦值;
(a + b) = c; // error,編譯錯
複數舉例:實現輸出類似於 3 + 4 i 效果
class Complex{
public:
Complex (int r = 0, int i = 0):m_r(r), m_i(i){}
void print(void) const { // 輸出
cout << m_r << '+' << m_r << 'i' << endl;
}
Complex add(Complex& c) { // 實現加法
return Complex(m_r + c.m_r, m_i + c.m_i);
}
private:
int m_r;
int m_i;
};
int main(){
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1.add(c2);
c.print(); // 輸出:4+6i
return 0;
}
上面這個複數例子,可以作以下修改:
可以把 add 換爲 operator+,輸出結果一樣;
那麼 Complex c3 = c1.add(c2);
可以改爲:Complex c3 = c1.operator+(c2);
還可以進一步修改爲:Complex c3 = c1 + c2; // 運算符重載
這裏 operator 是關鍵字;
class Complex{
public:
Complex (int r = 0, int i = 0):m_r(r), m_i(i){}
void print(void) const { // 輸出
cout << m_r << '+' << m_r << 'i' << endl;
}
const Complex operator+(const Complex& c) const { // 儘量使用 const,提高安全性
// Complex c3 = c1.operator+(c2);
// 第一個 const 返回右值,或者說函數返回值爲常量,目的是讓 c1+c2 不可以再被賦值;對應 c3
// 第二個 const 支持常量型右操作數,或者說支持傳入常量實參;對應 c2,
// 第三個 const 支持常量型左操作數,或者說允許常量(this)調用此函數;對應 c1
// 第一個縮小作用域,第二第三都是擴大作用域;
return Complex(m_r + c.m_r, m_i + c.m_i);
}
private:
int m_r;
int m_i;
friend const Complex operator-(const Complex&, const Complex&); // 友員聲明,爲了訪問私有成員變量
};
const Complex operator-(const Complex& l, const Complex& r) { // 這三個 const 和上面的功能一樣
// Comple c3 = operator-(c1, c2);
// 另外這個是友員(全局)函數,無 this 指針,沒有最後一個 const
return Complex(l.m_r - r.m_r, l.m_i - r.m_i);
}
int main(){
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3 = c1 + c2;
c3.print(); // 輸出:4+6i
c3 = c1 - c2;
return 0;
}
這裏加法重載用的是成員函數,減法重載用的是全局(友員)函數;
我們推薦用全局寫,原因如下:
c3 = c1 + 200; // ok, c3 = c1.operator+(200);
c3 = 200 + c1; // error, c3 = 200.operator+(c1);
友員函數會隱式的把 200 轉換爲類類型,具體看後期的類型轉換;