栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Effective Modern C++[实践]->在创建对象时注意区分()和{}

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Effective Modern C++[实践]->在创建对象时注意区分()和{}

优先使用大括号初始化语法
  • 何谓初始化
  • 何谓赋值
    • 非类类型
    • 类类型
      • 赋值运算符
      • 移动赋值运算符
  • `()`、`{}` `=`
    • 就地初始化
    • 不可复制对象不能使用`=`
    • `{}`禁止内建类别的隐式窄话转换
    • 任何能解析为声明的都要解析为声明
  • `{}`陷阱
    • 没有`std::initializer_list`的情形
    • 存在`std::initializer_list`,调用`{}`编译器会强烈优选`initializer_list`
    • 隐式窄化转换陷阱
    • 存在`std::initializer_list`,调用`{}`找不见`initializer_list`时的陷阱
    • `vector`中的初始化陷阱
  • 参考文献

  1. {}初始化是最广泛的初始化语法,它可以阻止窄化转换,并且避免了C++最复杂的语法解析
  2. 在构造函数做函数重载的时候,{}会优先匹配带有std::initializer_list参数的版本,即使其他构造函数看起来更匹配
  3. 对与std::vector两个参数的构造函数来说,其{}和()两种初始化方式有很大的不同
  4. 在模版中对于{}和()初始化如何进行选择是一个挑战
何谓初始化

变量的初始化会在构造时提供变量的初始值。
初始值可以由声明符或 new 表达式的初始化器部分提供。在函数调用时也会发生:函数形参及函数返回值也会被初始化。
对于每个声明符,初始化器必须是下列之一:

  • ( 表达式列表 ) 用逗号分隔的任意表达式列表和用括号括起来的大括号初始化列表
  • = 表达式 等号后面跟着一个表达式
  • { 初始化器列表 } 带括号的初始化列表:可能为空,逗号分隔的表达式列表和其他带括号的初始化列表

根据上下文的不同,初始化器可以调用:

  1. 值初始化, std::string s{};
  2. 直接初始化,std::string s("hello");
  3. 拷贝初始化,std::string s = "hello";
  4. 列表初始化,std::string s{'a', 'b', 'c'};
  5. 聚合初始化,char a[3] = {'a', 'b'};
  6. 引用初始化, char& c = a[0];

如果没有提供初始化式器,则使用默认初始化规则。

何谓赋值

定义变量后,可以使用=操作符(在单独的语句中)给它赋值。这个过程被称为拷贝赋值(或简称赋值)。之所以这样命名复制赋值,是因为它将=操作符右侧的值复制到操作符左侧的变量。=操作符称为赋值操作符。
赋值的一个缺点是,它至少需要两条语句:一条用于定义变量,另一条用于赋值。

非类类型
  1. 复制赋值运算符以 b 内容的副本替换对象 a 的内容(不修改 b)。
  2. 移动赋值运算符以 b 的内容替换对象a的内容,并尽可能避免复制(可以修改 b)
    直接赋值表达式的形式为
  • 左操作数 = 右操作数
  • 左操作数 = {} (C++11 )
  • 左操作数 = { 右操作数 } (C++11 )
#include 
int main()
{
    int n = 0;        // 不是赋值
    n = 1;            // 直接赋值
    n = {};           // 零初始化,然后赋值
    n = 'a';          // 整型提升,然后赋值
    n = {'b'};        // 显式转型,然后赋值
    n = 1.0;          // 浮点转换,然后赋值
//  n = {1.0};        // 编译错误(窄化转换)
    int& r = n;       // 不是赋值
    int* p;
    r = 2;            // 通过引用赋值
    p = &n;           // 直接赋值
    p = nullptr;      // 空指针转换,然后赋值
    struct { int a; std::string s; } obj;
    obj = {1, "abc"}; // 从花括号初始化器列表赋值
}
  1. 复合赋值(compound assignment)运算符以 a 的值和 b 的值间的二元运算结果替换对象a的内容。
    复合赋值表达式的形式为
  1. 左操作数 运算符 右操作数
  2. 左操作数 运算符 {} (C++11 起)
  3. 左操作数 运算符 { 右操作数 } (C++11 起)

运算符 : *=、/=、%=、+=、-=、<<=、>>=、&=、^=、|= 之一
左操作数: 对于内建运算符,左操作数 可具有任何算术类型,但如果 运算符 是 += 或 -=,那么它也接受指针类型,并与 + 和 - 有相同限制
右操作数:对于内建运算符,右操作数 必须可隐式转换成 左操作数

注:对复制与移动赋值不加以区分,它们都被称作直接赋值(direct assignment)。

类类型 赋值运算符

类 T 的复制赋值运算符是名为 operator= 的非模板非静态成员函数,它接受恰好一个 T、T&、const T&、volatile T& 或const volatile T&类型的形参。可复制赋值 (CopyAssignable) 类型必须有公开的复制赋值运算符。

类名 & 类名 :: operator= ( 类名 ) 	
类名 & 类名 :: operator= ( const 类名 & ) 	
类名 & 类名 :: operator= ( const 类名 & ) = default; (C++11 起)
类名 & 类名 :: operator= ( const 类名 & ) = delete;  (C++11 起)
#include 
#include 
#include 
#include 
 
struct A{
    int n;
    std::string s1;
    // 用户定义的复制赋值(复制交换法)
    A& operator=(A other)
    {
        std::cout << "A 的复制赋值n";
        std::swap(n, other.n);
        std::swap(s1, other.s1);
        return *this;
    }
};
struct B : A{
    std::string s2;
    // 隐式定义的复制赋值
};
struct C{
    std::unique_ptr data;
    std::size_t size;
    // 用户定义的复制赋值(非复制交换法)
    // 注意:复制交换法总是会重新分配资源
    C& operator=(const C& other){
        if (this != &other) // 非自赋值 {
            if (size != other.size) // 资源无法复用{
                data.reset(new int[other.size]);
                size = other.size;
            }
            std::copy(&other.data[0], &other.data[0] + size, &data[0]);
        }
        return *this;
    }
};
int main()
{
    A a1, a2;
    std::cout << "a1 = a2 调用 ";
    a1 = a2; // 用户定义的复制赋值
 
    B b1, b2;
    b2.s1 = "foo";
    b2.s2 = "bar";
    std::cout << "b1 = b2 调用 ";
    b1 = b2; // 隐式定义的复制赋值
    std::cout << "b1.s1 = " << b1.s1 << " b1.s2 = " << b1.s2 << 'n';
}
移动赋值运算符

类T的移动赋值运算符是名为 operator=的非模板非静态成员函数,它接受恰好一个 T&&、const T&&、volatile T&& 或 const volatile T&& 类型的形参。

类名 & 类名 :: operator= ( 类名 && ) 	 				(C++11 起)
类名 & 类名 :: operator= ( 类名 && ) = default; 	 	(C++11 起)
类名 & 类名 :: operator= ( 类名 && ) = delete; 		(C++11 起)
struct V{
    V& operator=(V&& other){
        // 这可能会被调用一或两次
        // 如果调用两次,那么 'other' 是刚被移动的 V 子对象
        return *this;
    }
};
struct A : virtual V {}; // operator= 调用 V::operator=
struct B : virtual V {}; // operator= 调用 V::operator=
struct C : B, A {};      // operator= 调用 B::operator=,然后调用 A::operator=
                         // 但可能只调用一次 V::operator=
int main(){
    C c1, c2;
    c2 = std::move(c1);
}
#include 
#include 
#include 
 
struct A{
    std::string s;
    A() : s("测试") {}
    A(const A& o) : s(o.s) { std::cout << "移动失败!n"; }
    A(A&& o) : s(std::move(o.s)) {}
    A& operator=(const A& other){
         s = other.s;
         std::cout << "复制赋值n";
         return *this;
    }
    A& operator=(A&& other){
         s = std::move(other.s);
         std::cout << "移动赋值n";
         return *this;
    }
};
 
A f(A a) { return a; }
 
struct B : A{
    std::string s2; 
    int n;
    // 隐式移动赋值运算符 B& B::operator=(B&&)
    // 调用 A 的移动赋值运算符
    // 调用 s2 的移动赋值运算符
    // 并进行 n 的逐位复制
};
 
struct C : B{
    ~C() {} // 析构函数阻止隐式移动赋值
};
 
struct D : B{
    D() {}
    ~D() {} // 析构函数本会阻止隐式移动赋值
    D& operator=(D&&) = default; // 无论如何都强制移动赋值
};
 
int main()
{
    A a1, a2;
    std::cout << "尝试从右值临时量移动赋值 An";
    a1 = f(A()); // 从右值临时量移动赋值
    std::cout << "尝试从亡值移动赋值 An";
    a2 = std::move(a1); // 从亡值移动赋值
 
    std::cout << "尝试移动赋值 Bn";
    B b1, b2;
    std::cout << "移动前,b1.s = "" << b1.s << ""n";
    b2 = std::move(b1); // 调用隐式移动赋值
    std::cout << "移动后,b1.s = "" << b1.s << ""n";
 
    std::cout << "尝试移动赋值 Cn";
    C c1, c2;
    c2 = std::move(c1); // 调用复制赋值运算符
 
    std::cout << "尝试移动赋值 Dn";
    D d1, d2;
    d2 = std::move(d1);
}
()、{} = 就地初始化

在C++11之前,只能对结构体或类的静态常量成员进行就地初始化,其他的不行。

class C{
  private:
   static const int a=10;	//yes
   int a1=10;					//warning: default member initializer for non-static data member is a C++11 extension
}

在C++11中,结构体或类的数据成员在申明时可以直接赋予一个默认值,初始化的方式有两种,一是使用等号“=”,二是使用大括号列表初始化的方式。注意,使用参考如下代码:

class C{
 private:  
   int a=7; 	//C++11 only
   int b{7}; //或int b={7}; C++11 only
   int c(7);	//error
};    

报错如下:

/home/insights/insights.cpp:7:10: error: expected parameter declarator
   int c(7);                    
         ^
/home/insights/insights.cpp:7:10: error: expected ')'
/home/insights/insights.cpp:7:9: note: to match this '('
   int c(7);                    
        ^
2 errors generated.
不可复制对象不能使用=

如std::atmoic对象是不可复制的,描述如下

  std::atomic a1(0);//ok
  std::atomic a2 {0};//ok
  std::atomic a3 = 0;//error

第三行代码的报错如下:

/home/insights/insights.cpp:15:20: error: copying variable of type 'std::atomic' invokes deleted constructor
  std::atomic a3 = 0;
                   ^    ~
/usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/atomic:821:7: note: 'atomic' has been explicitly marked deleted here
      atomic(const atomic&) = delete;
      ^
1 error generated.
{}禁止内建类别的隐式窄话转换
  double x,y,z;
  int sum = {x+y+z};

报错如下:

/home/insights/insights.cpp:7:14: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
  int sum = {x+y+z};
             ^~~~~
/home/insights/insights.cpp:7:14: note: insert an explicit cast to silence this issue
  int sum = {x+y+z};
             ^~~~~
             static_cast( )
1 error generated.

但是()和=没有问题

#include 
#include 
 
int main()
{
  double x=1.2,y=2,z=3;
  int sum1 = x+y+z;
  int sum2( x+y+z);
  std::cout<<"sum1="<sum1=6   sum2=6
任何能解析为声明的都要解析为声明
#include 
using namespace std;
 
class A{
    public :
        A(int y):m(y){
            cout<<"A 构造 "<
  A a(10);//ok
  A a1();//warning
  A a2{2};//ok
}

A a1()声明了一个名为a1,返回A类型对象的函数,会产生如下警告:

:16:7: warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
   16 |   A a1();
      |       ^~
{}陷阱 没有std::initializer_list的情形
#include 
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "< 

调用顺序见输出

A(int x,bool y) 构造 10    1
A(int x,bool y) 构造 10    1
A(int x,double y) 构造 10    5
A(int x,double y) 构造 10    50
存在std::initializer_list,调用{}编译器会强烈优选initializer_list
#include 
#include 
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "<
            cout<<"A(int x,double y) 构造 "<li){
            cout<<"A(initializer_listli) 构造 ";
            for(auto beg=li.begin();beg!=li.end();++beg)
                cout<<*beg<<" ";
            cout<
    A a1(10,true);
    A a2{10,true};
    A a3(10,5.0);
    A a4{10,50.00};
}

运行结果如下:

A(int x,bool y) 构造 10    1
A(initializer_listli) 构造 10 1 
A(int x,double y) 构造 10    5
A(initializer_listli) 构造 10 50 
隐式窄化转换陷阱
#include 
#include 
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "<
            cout<<"A(int x,double y) 构造 "<li){
            cout<<"A(initializer_listli) 构造 ";
            for(auto beg=li.begin();beg!=li.end();++beg)
                cout<<*beg<<" ";
            cout<
    A a1(10,true);
    A a2{10,true};
    A a3(10,5.0);
    A a4{10,50.00};
}

报错如下:

: In function 'int main()':
:27:17: error: narrowing conversion of '10' from 'int' to 'bool' [-Wnarrowing]
   27 |     A a2{10,true};
      |                 ^
:29:18: error: narrowing conversion of '10' from 'int' to 'bool' [-Wnarrowing]
   29 |     A a4{10,50.00};
      |                  ^
存在std::initializer_list,调用{}找不见initializer_list时的陷阱
#include 
#include 
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "<
            cout<<"A(int x,double y) 构造 "<li){
            cout<<"A(initializer_listli) 构造 ";
            for(auto beg=li.begin();beg!=li.end();++beg)
                cout<<*beg<<" ";
            cout<
    A a1(10,true);
    A a2{10,true};
    A a3(10,5.0);
    A a4{10,50.00};
}

运行结果如下:

A(int x,bool y) 构造 10    1
A(int x,bool y) 构造 10    1
A(int x,double y) 构造 10    5
A(int x,double y) 构造 10    50

此时若有A a();那么是一个函数声明。使用A a({});可以调用 A(initializer_listli)进行初始化
如下输出:

A(initializer_listli) 构造 
vector中的初始化陷阱

此项可搭配文章开头第4点食用效果更佳。

#include 
#include 
using namespace std;
int main()
{
  vector v(10,20);
  vector v1{10,20};
  cout<<"v size = "< 

编译后的函数为:

#include 
#include 
using namespace std;

int main()
{
  vector v = std::vector >(10, 20, std::allocator());
  vector v1 = std::vector >{std::initializer_list{10, 20}, std::allocator()};
  std::operator<<(std::operator<<(std::operator<<(std::cout, "v size = ").operator<<(v.size()), ";    "), "v1.size").operator<<(v1.size()).operator<<(std::endl);
  return 0;
}

运行结果如下:

v size = 10;    v1.size= 2
参考文献

[1] https://zh.cppreference.com/w/cpp/language/initialization
[2] C++11就地初始化与列表初始化

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/999017.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号