• C++: 类和对象(下) (初始化列表, 隐式类型转换, static成员, 友元, 内部类, 匿名对象)


    一. 再谈构造函数

    1. 构造函数体赋值

    在创建对象时, 编译器通过调用构造函数, 给对象中的各个成员一个合适的初始值.

    class Date
    {
    public:
      Date(int year, int month, int day)
      {
        _year = year;
        _month = month;
        _day = day;
      }
    
    private:
      int _year;
      int _month;
      int _day;
    };
    
    int main()
    {
      Date d1(2023, 11, 2);   //定义加初始化
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    类相当于声明, main 函数中的 Date d1(2023, 11, 2);相当于定义加初始化.

    虽然上述构造函数调用一次后, 对象中已经有了个初始值, 但是不能将其称为对对象中成员变量进行初始化.

    构造函数体中的语句只能将其称为赋初值, 而不能称为初始化.因为初始化只能初始化一次, 而构造函数体内可以多次赋值.

    如果 Date 中有一个引用成员变量或者是 const 成员变量, 也可以用构造函数体初始化吗?

    显然是不可以的, 类中成员变量声明和定义分离, 但是引用和 const 常变量都要求在声明的时候同时需要初始化.

    在这里插入图片描述

    编译器提示引用和 const 常变量没有初始化. 因为构造函数体并不是真正的初始化定义的地方, 只是一个赋值的地方.

    真正类成员定义的地方是在构造函数初始化列表的地方.

    2. 初始化列表

    初始化列表: 以一个冒号开始, 接着是一个以逗号分隔的数据成员列表, 每个"成员变量"后面跟一个放在括号中的初始值或表达式.

    class Date
    {
    public:
      Date(int year, int month, int day)
      : _year(year)
      , _month(month)
      , _day(day)
      , x(_year)
      , y(month)
      {}
    
    private:
      int _year;
      int _month;
      int _day;
    
      int& x;
      const int y;
    };
    
    int main()
    {
      Date d1(2023, 11, 2);   //定义加初始化
    
      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

    初始化列表是每个成员定义的地方, 这样引用和 const 成员变量在初始化列表位置进行初始化就不会出现错误了.


    还有几点关于初始化列表需要注意:

    1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

    编译器会直接报错
    在这里插入图片描述


    1. 类中包含以下成员, 必须在初始化列表进行初始化:
    • 引用成员变量
    • const 成员变量
    • 自定义类型成员(且该类没有默认构造函数时)
    class A       // A类 没有默认构造函数
    {
    public:
      A(int a)
      : _a(a)
      {}
    private:
      int _a;
    };
    
    class B
    {
    public:
      B(int x, int a, int ref, int n)
      : _x(x)       // 普通成员变量初始化
      , _aobj(a)    // 自定义成员初始化
      , _ref(ref)   // 引用成员初始化
      , _n(n)       // const 成员初始化
      {}
    private:
      int _x;       // 普通成员变量
      A _aobj;      // 没有默认构造函数
      int& _ref;    // 引用
      const int _n; // const
    };
    
    • 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

    1. 没有在初始化列表中的成员也会定义, 但是内置类型是随机值, 自定义类型会调用其默认构造函数.
      如果自定义类型没有默认构造函数, 则会编译报错.

    内置成员变量没有在初始化列表定义初始化, 也不在构造函数函数体内部赋值:

      B(int x, int a, int ref, int n)
      : _aobj(a)    // 自定义成员初始化
      , _ref(ref)   // 引用成员初始化
      , _n(n)       // const 成员初始化
      {}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    如果成员没有在初始化列表进行定义, 也没有在构造函数体内部进行赋值, 最终成员是随机值.

    注意, 这里的引用也是随机值, 是因为 _ref 引用了一个栈帧开辟的临时变量, 虽然不会因为未初始化报错, 但是这样的引用肯定也是不正确的.

    自定义类型没有默认构造函数, 也没有在初始化列表中进行定义, 会直接编译报错:

    在这里插入图片描述

    如果将 A 类的构造函数改成默认构造函数, 就不会出现编译错误.


    1. 初始化列表优先于缺省值, 只有初始化列表没有显示初始化的时候, 才会使用成员变量的缺省值.
    class Date
    {
    public:
      Date(int year, int month, int day)
      : _year(2)        // 初始化列表也定义了值
      , _month(month)
      , _day(day)
      {}
    
    private:
      int _year = 1;    // 缺省值
      int _month;
      int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    虽然输入了 4, 并没有用到 4. 同时也没有用到缺省值 1, 最终还是初始化列表中的初始化值 2.

    在这里插入图片描述


    1. 尽量使用初始化列表初始化, 因为不管是否使用初始化列表, 对于自定义类型成员变量, 一定会先使用初始化列表初始化.
    class Time
    {
    public:
      Time(int t = 4)
      {
        cout << "Time(int t = 4)" << endl;
        _t = t;
      }
    private:
      int _t;
    };
    
    class Date
    {
    public:
      Date()
      {}
    private:
      int _day;
      Time _t;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    如果类中有自定义成员, 且该自定义类有默认构造函数, 即使类中没有初始化列表, 编译器也会自动调用该自定义成员的默认构造函数.

    初始化列表是一个成员定义的地方, 即使没有显示写, 程序也会走隐含的初始化列表, 最终结果就是: 内置类型是随机值, 自定义类型调用它的默认构造函数.
    在这里插入图片描述


    1. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序, 与其在初始化列表中的先后次序无关

    问下面的代码最后打印什么?

    class A
    {
    public:
      A(int a)
        :_a1(a)
        ,_a2(_a1)
      {}
    
      void Print()
      {
        cout << _a1 << ' ' << _a2 << endl;
      }
    
    private:
      int _a2;
      int _a1;
    };
    
    int main()
    {
      A aa(1);
      aa.Print();
    
      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

    答案是 1 随机值

    虽然初始化列表中 _a1_a2 的前面, 但是实际初始化的顺序是按照类中成员定义的顺序的.

    在类中 _a2 先于 _a1, 所以先初始化 _a2, 但是此时 _a1 尚未初始化, 是随机值, 即 _a2 是随机值. 接着用 1 初始化 _a1.

    在这里插入图片描述


    总结
    初始化列表解决的问题

    • 必须在定义初始化的成员(引用成员变量, const成员变量, 没有默认构造函数的自定义成员)
    • 有些自定义成员想要自己显示初始化, 自己来控制

    80%-100%用初始化列表进行定义

    • 有些事初始化列表并不能做, 比如 _a((int*)malloc(sizeof(int) * capacity)), 虽然这样给 _a 初始化了, 但是却不能在初始化列表进行检查 malloc 是否成功, 检查只能在构造函数函数体进行.

    3. explitcit 关键字

    下述的类, 会有一种特殊的构造

    class A
    {
    public:
      A(int a)
      :_a(a)
      {
        cout << "A(int a)" << endl;
      }
    
      A(const A& aa)
      {
        _a = aa._a;
        cout << "A(const A aa)" << endl;
      }
    private:
      int _a;
    };
    
    int main()
    {
      A aa1 = 1;  //会发生隐式类型转换
    
      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

    表达式 A aa1 = 1; 看起来会出错, 将一个 int 类型的对象赋值给了一个 A 类型的对象.

    但是却自动发生了隐式类型转换, 首先会用 1 构造了一个临时对象, 再用这个临时对象拷贝构造 aa1
    在这里插入图片描述

    在这里插入图片描述

    需要注意的是, 这种隐式转换只适用于内置类型或者单参数(传一个参数的半缺省)构造函数支持

    如果类的构造函数需要两个参数, 或者隐式转换不了对应的参数类型, 会直接编译报错.

    在这里插入图片描述


    如果不想隐式转换, 可以在构造函数前加 explicit 关键字.

    构造函数不仅可以构造和初始化对象, 对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数, 还具有隐式类型转换的作用
    加上 explicit 可以防止隐式类型转换

    在这里插入图片描述


    如果构造函数有一个参数, 且使用了 explicit 关键字, 强制转换依旧是可以的

    A aa1 = A(1);
    
    • 1

    这样就相当于先使用 1 调用构造函数构造了一个匿名对象, 随后用这个匿名对象拷贝构造 aa1.


    补充

    C++98 不支持多参数隐式转换
    C++11 支持多参数隐式转换

    例如:

    Date d1 = {2023, 1, 3}; //列表初始化
    //不是下面这种写法
    //Date d1 = (2023, 1, 3); //相当于 Date d1 = 3;
    
    • 1
    • 2
    • 3

    先用 2023, 1, 3 作为构造函数参数创建了一个临时变量, 随后用临时变量拷贝构造 d1.

    如果想要良好的代码可读性, 推荐使用 explicit 关键字
    例如智能指针是不希望隐式类型转换的

    二. static 成员

    1. 概念

    声明为 static 的类成员称为类的静态成员.
    static 修饰的成员变量, 称为静态成员变量, 静态成员变量一定要在类外进行初始化.
    static 修饰的成员函数, 称为静态成员函数.


    需要实现一个类, 计算程序中创建出了多少个类对象.

    第一种想法是创建全局变量 count, 在类的所有构造函数中, 加上 count++.
    但是全局变量很容易被修改, 容易出错.

    第二种想法是为类添加一个普通成员变量.
    但是这个成员变量是属于对象的, 每创建一个对象, 就会多一个计数的成员, 数值一直为 1.

    第三种想法就是为类添加一个静态成员变量.
    静态成员变量是属于每个类的, 类实例化的所有对象都共有这一个成员.

    class A
    {
    public:
      A() { ++_scount; }
      A(const A& a) { ++_scount; }
      ~A() { --_scount; }
      static int getCount() { return _scount; }
    
    private:
      static int _scount; // 声明静态成员
    };
    
    int _scount = 0; // 初始化静态成员
    int main()
    {
      cout << A::getCount() << endl;
      A a1, a2;
      A a3(a1);
      cout << A::getCount() << endl;
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    结果为:

    在这里插入图片描述


    2. 特性

    声明静态成员

    通过在成员的声明之前加上关键字 static 使其与类关联在一起.
    和其他成员一样, 静态成员可以是 publicprivate 的.
    静态数据成员的类型可以是常量, 引用, 指针, 类类型等.

    类的静态成员为所有类对象共享, 不属于某个具体的对象, 存放在静态区.
    类似的, 静态成员函数也不予任何对象绑定在一起, 它们不包含 this 指针. 所以, 静态成员函数是不能声明成 const 的.

    使用类的静态成员

    可以使用作用域运算符直接访问静态成员

    cout << A::getCount() << endl;  //使用域作用限定符可以直接访问静态成员
    
    • 1

    虽然静态成员不属于类的某个对象, 但是仍然可以使用类的对象, 引用或指针来访问静态成员.

    A a1;
    A* a2 = &a1;
    
    cout << a1.getCount() << endl;    //通过对象或者引用调用静态成员函数
    cout << a2->getCount() << endl;   //通过指向对象的指针调用静态成员函数
    
    • 1
    • 2
    • 3
    • 4
    • 5

    成员函数不需要使用域作用限定符就可以直接使用静态成员. 反之, 静态成员函数不可以使用非静态成员.

    A() { ++_scount; }  //成员函数直接使用静态成员
    
    • 1

    定义静态成员

    和其他的成员函数一样, 静态成员函数既可以在类中也可以在类外定义.
    在类外定义的时候, 不能重复 static 关键字, 该关键字只出现在类内部的声明语句

    int A::getCount()
    {
      return _scount;
    }
    
    • 1
    • 2
    • 3
    • 4

    和类的所有成员一样, 当我饿们指向类外不得静态成员时, 必须指明成员所属的类名. static 关键字只出现在类内部的声明语句中.


    因为静态成员变量不属于类的任何一个对象, 所以它们并不是在创建类的时候被定义的.
    这就意味着静态成员变量不是由类的构造函数初始化的, 必须在类的外部定义和初始化每一个静态成员.

    int A::_scount = 0;   //类外定义并且初始化静态成员变量
    
    • 1

    要想确保对象只定义一次, 最好的方法是把静态成员的变量的定义与其他非内联函数的定义放在同一个文件中.

    静态成员函数和静态成员变量就相当于受限制的全局函数和全局变量, 不存在于对象里, 属于所有对象, 受类域和访问限定符限制.


    总结

    • 静态成员所有类对象共享, 不属于某个具体的对象, 存放在静态区.
    • 静态成员变量必须在类外定义, 定义时不添加 static 关键字, 类中只是声明.
    • 类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问
    • 静态成员函数没有隐藏的 this 指针, 不能访问任何非静态成员
    • 静态成员也是类的成员, 受 public, private, protected 访问限定符的限制
    • 静态成员函数不可以调用非静态成员函数
    • 非静态成员函数可以调用类的静态成员函数

    三. 友元

    友元提供了一种突破封装的方式, 有时提供了遍历. 但是友元会增加耦合度, 破坏了封装, 所以友元不宜多用.

    友元分为: 友元函数友元类

    1. 友元函数

    在之前的博客由详细讲解, 用友元函数处理 <<>> 重载的问题

    友元函数可以直接访问类的私有成员, 它是定义在类外部普通函数, 不属于任何类, 但需要在类的内部声明, 声明需要加 friend 关键字.

    说明:

    • 友元函数可访问类的私有和保护成员, 但不是类的成员函数
    • 友元函数不能用 const 修饰
    • 友元函数可以在类定义的任何地方声明, 不受类访问限定符限制
    • 一个函数可以是多个类的友元函数
    • 友元函数的调用与普通函数的调用原理相同

    2. 友元类

    友元类的所有成员函数都可以是另一个类的友元函数, 都可以访问另一个类中的非公有成员.

    • 友元是单向的, 不具有交换性.
      比如 Time 类和 Date 类, 在 Time 类中申明 Date 类为其友元类, 那么可以在 Date 类中直接访问 Time 类的私有成员变量, 但想在 Time 类中访问 Date 类中私有的成员变量则不行.
    • 友元关系不能传递
      如果 C 是 B 的友元, B 是 A 的友元, 则不能说明 C 是 A 的友元.
    • 友元关系不能继承.
    class Time
    {
      friend class Date; //声明 Date 类为 Time 类的友元类, 则在 Date 类中可以直接访问 Time 类的私有成员变量
      public:
        Time (int hour = 0, int minute = 0, int second = 0)
          :_hour(hour)
          ,_minute(minute)
          ,_second(second)
        {}
    
      private:
        int _hour;
        int _minute;
        int _second;
    };
    
    class Date
    {
      public:
        Date (int year = 1970, int month = 1, int day = 1)
          :_year(year)
          ,_month(month)
          ,_day(day)
        {}
    
        void SetTimeOfDate(int hour, int minute, int second)
        {
          // 直接访问 Time 类的私有成员变量
          _t._hour = hour;
          _t._minute = minute;
          _t._second = second;
        }
    
    private:
      int _year;
      int _month;
      int _day;
      Time _t;
    };
    
    • 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

    四. 内部类

    概念: 如果一个类定义在另一个类的内部, 这就叫做内部类.
    内部类是一个独立的类, 它不属于外部类, 更不能通过外部类的对象去访问内部类的成员.
    外部类对内部类没有任何优越的访问权限.

    注意: 内部类就是外部类的友元类, 参见友元类的定义, 内部类可以通过外部类的对象参数来访问外部类的所有成员. 但是外部类不是内部类的友元.

    特性:

    • 内部类可以定义在外部类的 public, protected, private
    • 内部类可以直接访问外部类的 static 成员, 不需要外部类的对象/类名
    • sizeof(外部类) = 外部类, 和内部类没有任何关系.
    class A
    {
      private:
        static int _k;
        int _h;
      public:
        class B // B 天生是 A 的友元
        {
          public:
            void func(const A& a)
            {
              cout << _k << endl;   // ok
              cout << a._h << endl; // ok
            }
        };
    };
    
    int A::_k = 1;
    
    int main()
    {
      A::B b;
      b.func(A());
    
      cout << sizeof(A) << endl;
      cout << sizeof(A::B) << endl;
    
      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

    B 就是一个普通类, 只是受 A 的类域和访问限定符限制, 本质相当于被封装了一下
    在这里插入图片描述

    五. 匿名对象

    class A
    {
      public:
        A(int a = 0)
          :_a(a)
        {
          cout << "A(int a)" << endl;
        }
    
        ~A()
        {
          cout << "~A()" << endl;
        }
    
      private:
        int _a;
    };
    
    class Solution
    {
      public:
        int Sum_Solution(int n)
        {
          //..
          return n;
        }
    };
    
    int main()
    {
      A();    // 定义匿名对象, 生命周期只有这一行, 下一行就会自动调用析构函数
    
      A aa1(2); // 先用 2 构造匿名对象, 再用匿名对象拷贝构造 aa1
    
      // 匿名对象在这样的场景下就很好用
      Solution().Sum_Solution(10);
    
      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

    在这里插入图片描述

    本章完.

  • 相关阅读:
    我把 CPU 三级缓存的秘密,藏在这 8 张图里
    Java入门------static关键字和静态属性、方法
    RabbitMQ常见问题及其解决方案
    Win10 + VS017 编译SQLite3.12.2源码
    一文梳理SpringCloud常见知识点
    如何画一棵树
    如何实现设备可视化系统建设?
    npm 包的命名空间介绍,以及@typescript-eslint/typescript-eslint
    (转载自新华网)蓄势数载业初就 | 多智能体协同控制科学研究一瞥
    GitHub暴涨4W下载量!阿里内网的大型分布式技术手册到底有多强
  • 原文地址:https://blog.csdn.net/Kuzuba/article/details/134292952