c++筆記04---構造拷貝函數,拷貝賦值運算符函數,靜態成員變量

1.    構造拷貝函數:用一個已有的對象,構造和它同類型的副本;
    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 轉換爲類類型,具體看後期的類型轉換;
發佈了57 篇原創文章 · 獲贊 19 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章