【C++精华铺】6.C++类和对象(下)类与对象补充及编译器优化

简介: 构造函数的初始化列表及其行为、static成员(函数,变量)、友元(函数,类)、内部类、匿名对象、对象拷贝时的编译器优化

 目录

1. 再谈构造

1.1 成员变量的初始化(初始化列表)

1.2 初始化列表的行为

1.3 explicit关键字

2. 类中的static成员

2.1 静态成员变量

2.2 静态成员函数

3. 友元

3.1 友元函数

3.1 友元类

4. 内部类

5. 匿名对象

6. 对象拷贝时候的编译器优化

1. 再谈构造

1.1 成员变量的初始化(初始化列表)

       为什么还要去看初始化的问题呢,因为这里有一个比较大的误区,我们都知道创建对象的时候会调用构造函数对成员进行初始化,所以我们会把下面的代码看作初始化,但其实下面的构造函数代码只能叫做赋值。

class Date
{
public:
  Date(int year,int month,int day)
  {
    _year = year;   //因为初始化只有一次,
    _month = month; //而在函数里面可以多次赋值,不能叫做初始化
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

image.gif

       而真正的初始化是在初始化列表中进行的,初始化列表的位置是在构造函数”{}“的前面,是由一个冒号后面跟着多个用逗号隔开的成员变量,每个成员变量后面都有括号,括号里面就是初始值,初始化的次序与成员列表次序无关,是按照成员的声明顺序进行初始化,如下:

Date(int year, int month, int day)
    :_year(year)
    ,_month(month)
    ,_day(day)
  {}

image.gif

       而且像下面三种成员必须使用初始化列表进行初始化:

    1. const 成员变量:因为const成员具有常性不能进行赋值,而函数体是进行赋值的,所以只能在初始化列表进行初始化(具体与初始化列表的行为有关,待会叙述
    2. 引用成员变量:因为引用在定义的时候必须初始化,并且引用的实体不能更改,如果在函数体里进行,本质上是一个赋值重载。
    3. 没有默认构造的自定义类型:因为自定义类型会调用默认构造进行初始化,如果没有构造就必须显示的初始化。

    1.2 初始化列表的行为

           我想大家应该都很困惑,为什么在初始化列表里面是初始化而在函数体里面就是赋值?初始化列表的执行时间是在函数体之前的,当我们调用构造函数的时候,编译器会首先去按照声明顺序进行默认的初始化,如果在初始化列表中有构造初始值则不会执行默认初始化,而去执行显式的初始化,所以无论我们有没有去写初始化列表,都是会在函数体执行之前初始化,这也就解释了为什么有些成员只能在初始化列表里面进行初始化和初始化列表的顺序与初始化顺序无关。而初始化列表对于编译器只是一个参考,可以认为编译器在执行的时候如果你(列表)有,我就按你的来,如果没有我就按照我自己的来。而const成员一旦执行了初始化就不能进行赋值,所以在语法上规定了const必须人为的给构造初始值,这也就是为什么const必须在初始化列表进行初始化了。

           而当我们执行到函数体的时候所有成员其实早就执行默认初始化初始化完成了,这个时候去进行所谓的”初始化“其实都是赋值(拷贝或者进行赋值重载)。

           无论什么时候我们都建议使用初始化列表进行初始化,首先就是不容易出错,其次就是规则上的统一。

    1.3 explicit关键字

           如果构造函数只有单个参数或者除第一个参数无默认值其余均有默认值,在一些情况下会存在隐式类型转换。比如

    class A
    {
    public:
      A(int a)
        :_a(a)
      {}
    private:
      int _a;
    };
    //
    class B
    {
    public:
      B(int a, int b = 1, int c = 1)
        :_a(a)
            ,_b(b)
            ,_c(c)
      {}
    private:
      int _a;
      int _b;
      int _c;
    };
    int main()
    {
      A a1 = 1;  //这俩种情况都会发生隐式类型转换 转换成一个无名类型的对象进行拷贝
      B b1 = 1;
        //C++11
        B b2 = { 1, 2, 3}; //c++11支持的括号初始化,将数组进行类型转换再构造,            
                           //想了解可以看我的c++11专栏
    }

    image.gif

    image.gif编辑

            对于代码中提到的括号初始化可以看我的这一篇文章:【C++11】 统一的列表初始化( {}初始化 )_子亦半截诗的博客-CSDN博客

           回归正题,上述的这种类型转换有的时候我们并不希望发生,所以就有了explicit关键字,用这个关键字修饰构造函数,将不会发生这种类型转换,同时C++11的括号初始化特性也会被禁用。

    class A
    {
    public:
      explicit A(int a)
        :_a(a)
      {}
    private:
      int _a;
    };
    //
    class B
    {
    public:
      explicit B(int a, int b = 1, int c = 1)
        :_a(a)
        , _b(b)
        , _c(c)
      {}
    private:
      int _a;
      int _b;
      int _c;
    };
    int main()
    {
      A a1 = 1;  //报错:E0415 不存在从 "int" 转换到 "A" 的适当构造函数
      B b1 = 1;  //报错:E0415 不存在从 "int" 转换到 "B" 的适当构造函数
      //C++11
      B b2 = { 1, 2, 3 };   //报错:E2334  复制列表初始化不能使用标记为“显式”的构造函数
    }

    image.gif

    报错:image.gif编辑

    2. 类中的static成员

           类中的静态成员和类中其他的成员有很大的区别,非静态成员变量的初始化在初始化列表里面进行,但是静态成员变量不能在初始化列表中初始化,因为static成员并不属于某个对象,static成员被存放在静态区,被同类型的所有对象共享

    2.1 静态成员变量

           静态成员变量属于同类型的所有对象,它的定义和初始化在类外进行。在定义的时候需要在变量名前面加上 类名:: 。

    int A::_a = 0;
    class A
    {
    public:
    private:
      static int _a;
    };

    image.gif

    2.2 静态成员函数

           由于静态函数属于每一个类对象,而不属于某个对象,所以静态成员函数也就没有this指针,所以也就不能访问类的非静态成员。非静态成员函数可以调用静态成员函数,但是静态成员函数不能调用非静态函数。可能有人要疑问了:非静态成员函数不也是被共享的吗,为什么不能被静态成员函数访问呢?这是因为非静态成员函数需要传入this指针,而静态成员函数是没有this指针的。

    int A::_a = 0;
    class A
    {
    public:
      static void test() {  }
      void test1() { test(); }
    private:
      static int _a;
    };

    image.gif

    3. 友元

           友元是一种突破封装的方式,让外部成员访问自己私有成员的一种方式,但过度使用会破坏封装。

    3.1 友元函数

           友元函数的声明就是在类中声明函数时在前面加上friend关键字,声明成友元函数这个函数就能访问对象的私有成员。在一些特殊情况下需要使用,就比如”<<“流写入符号重载,由于"<<"的左操作数必须是ostream对象,所以第一个参数就必须是ostream类型,但是如果定义成成员函数,第一个参数就变成this指针了,这会导致我们使用”<<“的时候就变成” 对象<<cout  “,这与我们的习惯就不符,所以流写入的符号重载必须定义成全局函数,而且还必须要能访问类的私有成员,这个时候友元函数就可以发挥作用了。如下:

    class Date
    {
      friend ostream& operator<<(ostream& _cout, const Date& d);
      friend istream& operator>>(istream& _cin, Date& d);
    private:
      int _year;
      int _month;
      int _day;
    };
    ostream& operator<<(ostream& _cout, const Date& d)
    {
      _cout << d._year << ' ' << d._month << ' ' << d._day << endl;
      return _cout;
    }
    istream& operator>>(istream& _cin, Date& d)
    {
      _cin >> d._year;
      _cin >> d._month;
      _cin >> d._day;
      return _cin;
    }

    image.gif

      • 友元函数可以访问类的所有成员,但不是类的成员
      • 友元函数不可以用const修饰
      • 友元函数不受访问限定符限制

      3.1 友元类

             如果一个类A声明了一个友元类B,那么B类中的所有成员都可以访问A类的所有成员,但A类不能访问B类的成员。友元关系不能传递和继承(继承以后再说),比如C是D的友元,D是E的友元,但C不是E的友元。

      class A
      {
        friend class B;
      private:
        int _aa;
        int _ab;
        int _ac;
      };
      class B
      {
        void print()
        {
          cout << _atest._aa << ' ' << _atest._ab << ' ' << _atest._ac << endl;
        }
      private:
        A _atest;
      };

      image.gif

      4. 内部类

             当一个类定义在另一个类的内部,这个类就被称之为内部类。内部类相当于外部类的友元类,可以通过对象参数访问内部类中的所有成员,访问静态成员的时候不需要对象参数就可以直接访问,但是外部类不能访问内部类的成员。内部类是一个独立的类,对外部类的大小没有任何影响。外部成员对内部类的访问会受到访问限定修饰符的限制。

      class A
      {
      public:
        class B
        {
          void print(const A& a)
          {
            cout << a._a;
          }
        };
      private:
        int _a;
      };

      image.gif

      5. 匿名对象

             匿名对象的定义就是在对象后面加上一个括号,匿名对象不用取名字,声明周期也只有一行,使用完后就自动调用析构函数销毁了。

      class A
      {
      public:
        static int _a;
      };
      int A::_a = 0;
      int main()
      {
        A();   //匿名对象
        cout << A()._a;
      }

      image.gif

      6. 对象拷贝时候的编译器优化

             在我们传参或者传值返回的时候可能会进行频繁的构造和拷贝构造,如果对象较大,会造成极大的资源消耗,所以为此编译器做出一些优化。(只针对构造,赋值重载不会进行优化)

      class A
      {
      public:
        A(int a = 0)
        {
          cout << "A(int a = 0)" << endl;
        }
        A(const A& aa)
        {
          cout << "A(const A & aa)" << endl;
        }
        ~A()
        {
          cout << "~A()" << endl;
        }
      };
      void f1(A a)
      {}
      A f2()
      {
        A a;
        return a;
      }
      int main()
      {
        A a;
        f1(1);  //连续构造+拷贝构造->直接构造
        f1(A(2)); //连续构造+拷贝构造->直接构造
        A a1 = f2(); //拷贝构造+拷贝构造->优化为一个拷贝构造
      }

      image.gif

      image.gif编辑


      相关文章
      |
      2天前
      |
      存储 编译器 C语言
      【c++丨STL】string类的使用
      本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
      15 2
      |
      8天前
      |
      存储 编译器 C++
      【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
      本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
      33 5
      |
      14天前
      |
      存储 编译器 C++
      【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
      本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
      46 4
      |
      15天前
      |
      存储 编译器 Linux
      【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
      本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
      43 4
      |
      1月前
      |
      存储 编译器 对象存储
      【C++打怪之路Lv5】-- 类和对象(下)
      【C++打怪之路Lv5】-- 类和对象(下)
      28 4
      |
      1月前
      |
      编译器 C语言 C++
      【C++打怪之路Lv4】-- 类和对象(中)
      【C++打怪之路Lv4】-- 类和对象(中)
      25 4
      |
      1月前
      |
      存储 安全 C++
      【C++打怪之路Lv8】-- string类
      【C++打怪之路Lv8】-- string类
      22 1
      |
      1月前
      |
      存储 编译器 C++
      【C++类和对象(下)】——我与C++的不解之缘(五)
      【C++类和对象(下)】——我与C++的不解之缘(五)
      |
      1月前
      |
      编译器 C++
      【C++类和对象(中)】—— 我与C++的不解之缘(四)
      【C++类和对象(中)】—— 我与C++的不解之缘(四)
      |
      1月前
      |
      C++
      C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
      C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
      54 1