创建对象时()和{}用法


  • 几种初始化方式

      {
          int nX(0);
          int nY = 0;
          int nZ{ 0 };
          int nZz = { 0 };
      }
    

    至于int nZz = { 0 };这种方式的初始化通常会把它按照int nZ{ 0 };这种方式处理

    C++98对于初始化的方式有很多种,但是对于一些场景还是没有办法表达某些想要进行的初始化,例如,没有办法直接指定一个STL容器在创建时持有一个特定集合的值,而C++11引入了统一初始化方式——大括号{}。至少从概念上可以用于一切场合、表达一切意思的初始化。例如解决刚刚提到的在C++98中无法表达之事:std::vector<int> v{1,2,3,4,5}

    大括号{}同样适用于类class的非静态成员指定默认值,也可以使用”=“初始化,但不能使用”()“,对于不可复制的对象可以采用大括号{}和小括号()进行初始化,却不能使用”=“


  • 大括号{}的特性

    使用大括号初始化时它会禁止内建型别之间进行隐式的高精度转换低精度转换

    如果大括号内的表达式无法保证能够采用进行初始化的对象来表达,则编译不同通过。例如编译不通过的例子:

      {
          double d1 = 10.0;
          double d2 = 11.0;
          double d3 = 12.0;
    
          int sum = { d1 + d2 + d3 };
      }
    

    我们知道采用小括号()和“=”的初始化则不会进行高精度转换低精度检查(相信很多程序员都走过丢失精度的坑),如果C++11中小括号()和“=”的初始化按照大括号{}进行这样的检查,就会破坏很多现有代码

    对于C++的苦恼的解析语法免疫

    C++规定:任何能够解析为声明的都要解析为声明。按照上面所述的初始化方式,例如对于一个带有形参的构造函数的调用方式:Person p1(10),如果我们按照同等语法调用一个默认构造函数的方式是Person p2(),这样就会变成了函数声明而非对象。由于函数的声明不能使用大括号来指定形参列表,所以使用大括号{}来完成对象的默认构造就没有问题,就像这样Person p3{}


  • 大括号初始化的缺陷

    如果我们使用auto推导变量的类型时,对于这样的auto a = {1,false};那么变量a的类型被推导成std::initializer_list类型,而如果一个类没有重载以这样类型为参数的构造函数时,其实以大括号{}和小括号()初始化其实没什么太多区别。 而问题出自“如果一个类重载了以这样类型为参数的构造函数时”。如果一个类重载了以std::initializer_list类型为参数的构造函数时,编译器会优先选择这样的构造函数,只有在找不到任何办法把大括号{}中的实参转换成std::initializer_list模板中的型别时,编译器才会退而去检查普通的重载决议。通过下面代码说明:

      class Person
      {
      public:
          Person()
          {
              cout << "Person()" << endl;
          }
          Person(int nAge, double nWeight)
          {
              cout << "Person(int nAge, double nWeight)" << endl;
          }
          Person(std::string strName)
          {
              cout <<"Person(std::string strName)"<< endl;
          }
          Person(std::initializer_list<int> il)
          {
              cout << "Person(std::initializer_list<int> il)" << endl;
          }
      private:
          int m_nAge;
          double m_fWeight;
      };
    
      {
          int i = 10;
          int j = 100;
          short s = 500;
          double d = 20.0;
          string strName = "zhangsan";
    
          //Person p1{ i,d };	// 报出高精度向低精度转换的错误,无法编译,即使有完全匹配的构造函数
    
          Person p2{ i,j };		// 正确
    
          Person p3{ i,s };		// 正确
    
          Person p4{ strName };	// 正确
    
          Person p5{};			// 正确
      }
    

    输出:

      Person(std::initializer_list<int> il)
      Person(std::initializer_list<int> il)
      Person(std::string strName)
      Person()
    

    从上面的输出及编译可以进一步总结出,如果以大括号{}的形式初始化时,可以进行实参向std::initializer_list模板中的型别进行隐式转换则优先选择重载了以std::initializer_list类型为参数的构造函数, 但是隐式转换要遵循上面提到的一点——禁止内建型别之间进行隐式的高精度转换低精度转换,只有在找不到任何办法把大括号{}中的实参转换成std::initializer_list模板中的型别时,编译器才会退而去检查普通的重载决议

    语言也同时规定,以一个空大括号{}进行初始化则表示的是“没有实参”而不是“空的std::initializer_list”,进而调用默认构造函数


  • 影响

    通过上面的测试发现,对于一个类的设计者最好把构造函数设计成无论使用小括号()还是大括号{}都不会影响调用的重载版本。而如果对于一个已存在的类添加一个带有std::initializer_list型别形参的构造函数的话,有可能客户端之前使用的大括号{}初始化方式会决议到这个新构造函数

    而对于选择大括号{}的程序员大多是看中了它的特性,选择大括号{}还是小括号()究竟哪个更好,例如下面使用可变模板函数进行参数转发的例子使这个问题更突出

      template<typename T,typename... Ts>
      void TemplateFun(Ts&&... params)
      {
          T obj(std::forward<Ts>(params));
      }
    

    对于上面的例子这样的类型std::vector如果以小括号()形式初始化和以大括号{}结果完全不一样。所以必须将以什么样的形式(大括号{}还是小括号())初始化则必须有调用者决定,而标准库函数std::make_uique和std::make_shared也有这样的问题,它们解决的办法是在内部使用小括号()


  • 最后总结

    • 大括号初始化可以应用的语境最为宽泛,可以阻止隐式高精度向低精度转换,还有对解析语法免疫

    • 在构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有std::initializer_list型别形参相匹配,即使其他重载版本有更匹配的形参表

    • 使用小括号还是大括号,可能造成完全不一样的结果,所以使用时要做到详细了解

    • 在模板内容进行对象创建时,要注意小括号()和大括号{}所带来的问题