• C++基础——类与对象


    1 概述

    C++是面向对象的语言,面向对象语言三大特性:封装、继承、多态。
    C++将万事万物抽象为对象,对象上有其属性和行为。

    2 封装

    2.1 封装的意义

    封装是面向对象的三大特性之一,封装将属性和行为作为一个整体,对属性和行为加以权限控制
    创建类语法:

    class 类名 {
    访问权限:
    	属性;
    	行为;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    示例:

    class Student {
    private:
        string m_name;
        int m_age;
    public:
        Student(string name, int age) {
            m_name = name;
            m_age = age;
        }
        string getName() {
            return m_name;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面除了定义属性和行为,也定义了权限访问控制符
    C++包含三种访问控制符:

    • public 可被类中函数、子类函数、友元函数和类对象访问
    • protected 可被类中函数、子类函数、友元函数访问
    • private 可被类中函数和友元函数访问

    2.2 struct和class区别

    二者的区别在于默认的访问权限不同,struct默认访问权限为public,class默认访问权限为private

    #include 
    using namespace std;
    
    class C1 {
        int m_A;
    };
    
    struct S1 {
        int m_A;
    };
    
    
    int main() {
        C1 c1;
        //c1.m_A; 报错,无法访问
        S1 s1;
        s1.m_A = 10;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.3 成员属性设置为私有

    通常,将成员属性设置为私有,并提供设置和获取的方法。
    这样的好处是可以控制成员属性的访问权限,对于写权限,能够检测数据的有效性。

    class Test {
    public:
        //num1提供读写接口
        void setNum1(int num1) {
            m_num1 = num1;
        }
    
        int getNum1() {
            return m_num1;
        }
    
        //num2只提供读接口
        int getNum2() {
            return m_num2;
        }
    
        //num3只提供写接口,并判定数据范围
        void setNum3(int num3) {
            if (num3 > 0) {
                m_num3 = num3;
            } else {
                m_num3 = 0;
            }
        }
    
    private:
        int m_num1;
        int m_num2;
        int m_num3;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    封装的思想通常会将属性设置为私有,暴露必要的修改行为给外部。

    3 对象创建和清理

    3.1 构造函数和析构函数

    一个类具有最基础的两个函数是构造函数和析构函数,即使程序员未添加这两个函数,编译器也会生成一个默认的版本。
    其中构造函数用于为类创建一个对象,并进行一些初始化的工作。
    析构函数与构造函数相反,是为了清理一个对象,并释放资源。
    构造函数和析构函数都是由编译器自动调用。

    class Test {
    private:
    	int m_num;
    public:
        Test();
        Test(int num) {
    		m_num = num;
    	}
    
        ~Test();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    构造函数可以重载,而析构函数只能有一个。构造函数可以通过传入不同的参数来重载,而析构函数没有参数。
    当未定义构造函数和析构函数时,编译器会自动生成以下两个实现:

    Test(){}
    ~Test(){}
    
    • 1
    • 2

    默认会生成两个空实现。
    而如果提供了有参构造函数,则编译器不会提供默认的空参构造函数,如果有此使用场景,则需要自己再提供一个空参构造函数。

    3.2 构造函数分类及调用方式

    构造函数有两种分类方式:
    按参数分为:有参构造和无参构造
    按类型分类:普通构造和拷贝构造
    三种调用方式:
    括号法
    显示法
    隐藏转换法

    #include 
    
    using namespace std;
    
    class Person {
    public:
        //无参构造函数
        Person() {
            cout << "无参构造函数" << endl;
        }
    
        //有参构造函数
        Person(int age) {
            cout << "有参构造函数" << endl;
            m_age = age;
        }
    
        //拷贝构造函数
        Person(const Person &person) {
            cout << "拷贝构造函数" << endl;
            m_age = person.m_age;
        }
    
        ~Person() {
            cout << "析构函数" << endl;
        }
    
    public:
        int m_age;
    };
    
    void test1() {
        //调用无参构造函数
        Person p1;
        //显示调用无参构造函数
        Person p2 = Person();
        //调用拷贝构造函数
        Person P3(p1);
    }
    
    void test2() {
        //括号法
        Person p1(10);
        //括号不能用于调用无参构造函数,这样定义会被认为是一个函数声明
        //Person p();
    
        //显示法
        Person p2 = Person(10);
        Person p3 = Person(p2);
    
        //创建匿名对象,无法使用,直接析构
        Person();
        Person(10);
    
        //隐式转换法,隐式调用有参构造函数和拷贝构造函数
        Person p4 = 10;
        Person p5 = p4;
    
        //不能利用拷贝构造初始化匿名对象,会被认为是对象声明
        //Person p6(p4);
    }
    
    int main() {
        test1();
        test2();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    输出:

    无参构造函数
    无参构造函数
    拷贝构造函数
    析构函数
    析构函数
    析构函数
    有参构造函数
    有参构造函数
    拷贝构造函数
    无参构造函数
    析构函数
    有参构造函数
    析构函数
    有参构造函数
    拷贝构造函数
    析构函数
    析构函数
    析构函数
    析构函数
    析构函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    比较需要注意的是隐藏转换法

    3.3 拷贝构造函数的调用时机

    调用拷贝构造函数有三种情况:

    • 使用一个已有的对象去初始化另一个对象
    • 值传递的方式给对象类型参数传值
    • 以值的方式返回局部对象
    #include 
    
    using namespace std;
    
    class Person {
    public:
        //无参构造函数
        Person() {
            cout << "无参构造函数" << endl;
        }
    
        //有参构造函数
        Person(int age) {
            cout << "有参构造函数" << endl;
            m_age = age;
        }
    
        //拷贝构造函数
        Person(const Person &person) {
            cout << "拷贝构造函数" << endl;
            m_age = person.m_age;
        }
    
        ~Person() {
            cout << "析构函数" << endl;
        }
    
    public:
        int m_age;
    };
    
    void test1() {
        //调用有参构造函数
        Person p1(10);
        //括号法调用拷贝构造函数
        Person p2(p1);
        //隐式转换法调用拷贝构造函数
        Person p3 = p1;
        //显示调用拷贝构造函数
        Person p4 = Person(p1);
    
        //赋值操作,不会调用拷贝构造函数
        Person p5;
        p5 = p1;
    }
    
    void doSomething1(Person p){}
    void test2() {
        Person p;
        //对象赋值给形参,调用拷贝构造函数
        doSomething1(p);
    }
    
    Person doSomething2() {
        Person p;
        return p;
    }
    void test3() {
        Person p = doSomething2();
    }
    
    int main() {
        test1();
        test2();
        test3();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    输出:

    有参构造函数
    拷贝构造函数
    拷贝构造函数
    拷贝构造函数
    无参构造函数
    析构函数
    析构函数
    析构函数
    析构函数
    析构函数
    无参构造函数
    拷贝构造函数
    析构函数
    析构函数
    无参构造函数
    析构函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    最后返回局部对象与预想中有差异是因为编译器优化,感兴趣可以搜索RVO了解。
    由于返回值和值传递传对象都会导致对象复制,对象大小会随着属性的增加而增加,所以一般对象作为参数的时候,都是使用引用或者指针进行传参或返回。

    3.4 构造函数调用规则

    默认情况下,C++编译器会至少给一个类添加3个函数:

    • 默认构造函数(无参构造函数,实现为空)
    • 默认析构函数(空实现析构函数)
    • 默认拷贝构造函数(对定义的属性进行拷贝)

    构造函数生成规则如下:

    • 如果用户定义有参构造函数,则编译器不会提供默认无参构造函数,但还是会提供默认拷贝构造函数
    • 如果用户定义拷贝构造函数,则编译器不会提供默认无参构造函数

    1、使用编译器默认函数

    class Person {
    public:
        int m_age;
    };
    
    int main() {
        Person p1;
        p1.m_age = 10;
        Person p2 = p1;
        cout << "p2.m_age = " << p2.m_age << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里调用了默认无参构造和拷贝构造函数。析构函数由于没有实现所以无法体现。

    2、提供有参构造的情况

    class Person {
    public:
        //无参构造函数
        //Person() {
        //    cout << "无参构造函数" << endl;
        //}
    
        //有参构造函数
        Person(int age) {
            cout << "有参构造函数" << endl;
            m_age = age;
        }
    
        //拷贝构造函数
        Person(const Person &person) {
            cout << "拷贝构造函数" << endl;
            m_age = person.m_age;
        }
    
        ~Person() {
            cout << "析构函数" << endl;
        }
    
    public:
        int m_age;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述
    没有匹配的构造函数调用,编译器不提供默认构造函数。
    3、提供拷贝构造函数

    class Person {
    public:
        //拷贝构造函数
        Person(const Person &person) {
            cout << "拷贝构造函数" << endl;
            m_age = person.m_age;
        }
    
        ~Person() {
            cout << "析构函数" << endl;
        }
    
    public:
        int m_age;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述
    提供了拷贝构造函数后,不会提供默认无参构造函数。

    3.5 深拷贝和浅拷贝

    浅拷贝:简单的赋值拷贝,指向同一个内存区域
    深拷贝:在堆中重新申请空间,进行拷贝

    class Person {
    public:
        //无参(默认)构造函数
        Person() {
            cout << "无参构造函数!" << endl;
        }
        //有参构造函数
        Person(int age ,int height) {
    
            cout << "有参构造函数!" << endl;
    
            m_age = age;
            m_height = new int(height);
    
        }
        //拷贝构造函数  
        Person(const Person& p) {
            cout << "拷贝构造函数!" << endl;
            //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
            m_age = p.m_age;
            m_height = new int(*p.m_height);
    
        }
    
        //析构函数
        ~Person() {
            cout << "析构函数!" << endl;
            if (m_height != NULL)
            {
                delete m_height;
            }
        }
    public:
        int m_age;
        int* m_height;
    };
    
    void test01()
    {
        Person p1(18, 180);
    
        Person p2(p1);
    
        cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
    
        cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
    }
    
    int main() {
    
        test01();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    由于类中的属性是在堆上分配的,如果使用浅拷贝,会导致两个对象的m_height都指向了同一个堆内存区域,而当一个对象析构之后,会释放该内存区域,而另一个对象还在使用该内存区域,会导致不确定的结果。而第二个对象析构时,则会导致重复释放堆区,会导致程序终止。所以如果有堆上分配内存的属性,需要使用深拷贝,重新申请堆内存区域进行赋值。
    示例如下:

    void test1() {
        Person p1(18, 180);
        {
            Person p2(p1);
            cout << "p2.height = " << *p2.m_height << endl;
        }
        cout << "p1.height = " << *p1.m_height << endl;
    }
    
    int main() {
        test1();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出:

    有参构造函数
    p2.height = 180
    析构函数
    p1.height = 887973408
    析构函数
    
    Process finished with exit code -1073740940 (0xC0000374)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面是注释了深拷贝的结果,p2正常访问,然后p2被析构,此时p1的m_height指向的内存实际已经释放,所以访问该内存会导致不确定的结果,然后p1会重复释放m_height指向的内存,导致程序出错终止。

    3.6 初始化列表

    C++提供了一种初始化列表的语法来初始化属性

    class Person {
    public:
    
        传统方式初始化
        //Person(int a, int b, int c) {
        //    m_A = a;
        //    m_B = b;
        //    m_C = c;
        //}
    
        //初始化列表方式初始化
        Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
        void PrintPerson() {
            cout << "mA:" << m_A << endl;
            cout << "mB:" << m_B << endl;
            cout << "mC:" << m_C << endl;
        }
    private:
        int m_A;
        int m_B;
        int m_C;
    };
    
    int main() {
        Person p(1, 2, 3);
        p.PrintPerson();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    初始化列表的语法可以省略原始构造函数中的一些模版代码。

    3.7 类对象作为类成员

    class Phone
    {
    public:
        Phone(string name)
        {
            m_PhoneName = name;
            cout << "Phone构造" << endl;
        }
        ~Phone()
        {
            cout << "Phone析构" << endl;
        }
        string m_PhoneName;
    };
    
    class Person
    {
    public:
        //初始化列表可以告诉编译器调用哪一个构造函数
        Person(string name, string pName) :m_Name(name), m_Phone(pName)
        {
            cout << "Person构造" << endl;
        }
    
        ~Person()
        {
            cout << "Person析构" << endl;
        }
    
        void playGame()
        {
            cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
        }
    
        string m_Name;
        Phone m_Phone;
    
    };
    void test01()
    {
        //当类中成员是其他类对象时,我们称该成员为 对象成员
        //构造的顺序是 :先调用对象成员的构造,再调用本类构造
        //析构顺序与构造相反
        Person p("张三" , "苹果X");
        p.playGame();
    
    }
    
    int main() {
        test01();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    先调用对象成员的构造函数,然后调用本类的构造函数。析构的时候正好与构造的时候顺序相反。

    3.8 静态成员

    静态成员就是在成员变量或者成员函数前加上static关键字

    • 静态成员变量
      • 所有对象共享同一份数据
      • 在编译阶段分配内存
      • 类内声明,类外初始化
    • 静态成员函数
      • 所有对象共享同一个函数
      • 静态成员函数只能访问静态成员变量
        1、静态成员变量
    class Person
    {
    
    public:
        static int m_A; //静态成员变量
    private:
        static int m_B; //静态成员变量也是有访问权限的
    };
    //类外初始化
    int Person::m_A = 10;
    int Person::m_B = 10;
    
    void test01()
    {
        //静态成员变量两种访问方式
    
        //1、通过对象
        Person p1;
        p1.m_A = 100;
        cout << "p1.m_A = " << p1.m_A << endl;
    
        Person p2;
        p2.m_A = 200;
        cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
        cout << "p2.m_A = " << p2.m_A << endl;
    
        //2、通过类名
        cout << "m_A = " << Person::m_A << endl;
    
        //cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
    }
    
    int main() {
        test01();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    静态成员变量也有访问权限,与非静态成员变量的访问权限一样。静态成员变量可以通过对象和类名访问。

    class Person
    {
    public:
        static void func()
        {
            cout << "func调用" << endl;
            m_A = 100;
            //m_B = 100; //错误,不可以访问非静态成员变量
        }
    
        static int m_A; //静态成员变量
        int m_B; // 
    private:
        //静态成员函数也是有访问权限的
        static void func2()
        {
            cout << "func2调用" << endl;
        }
    };
    int Person::m_A = 10;
    
    void test01()
    {
        //静态成员变量两种访问方式
    
        //1、通过对象
        Person p1;
        p1.func();
    
        //2、通过类名
        Person::func();
    
        //Person::func2(); //私有权限访问不到
    }
    
    int main() {
        test01();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    静态成员函数只能访问静态成员变量,也只能直接调用静态成员函数,可以在其中创建对象来调用非静态成员函数,因为非静态成员函数属于对象。静态成员函数也可以通过对象和类名两种方式调用。

  • 相关阅读:
    [认知能力]电子信息类专业新生暑假可以先预习一些什么专业内容?
    python没有重复数字的两位数统计 青少年编程电子学会python编程等级考试二级真题解析2021年6月
    CSS文本属性
    【数据结构】单链表详解
    PyQt5_股票最近均线状态工具
    尚硅谷 webpack高级配置 笔记
    一文带你搞懂OAuth2.0
    64位Linux系统上安装64位Oracle10gR2及Oracle11g所需的依赖包
    搭建本地私有仓库
    【RocketMq 系列】RocketMq 入门教程
  • 原文地址:https://blog.csdn.net/weixin_49274713/article/details/134127658