c++笔记04---构造拷贝函数,拷贝赋值运算符函数,静态成员变量

本文详细解析了C++中复数类的运算符重载过程,包括复数加减乘除的实现方式及注意事项。通过复数类的实例展示了如何正确地重载加法运算符,并提供了对友员函数的使用建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 转换为类类型,具体看后期的类型转换;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值