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

  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 <iostream>
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= 的非模板非静态成员函数,它接受恰好一个 TT&const T&volatile T&const volatile T&类型的形参。可复制赋值 (CopyAssignable) 类型必须有公开的复制赋值运算符。

类名 & 类名 :: operator= ( 类名 ) 	
类名 & 类名 :: operator= ( const 类名 & ) 	
类名 & 类名 :: operator= ( const 类名 & ) = default; (C++11)
类名 & 类名 :: operator= ( const 类名 & ) = delete;  (C++11)
#include <iostream>
#include <memory>
#include <string>
#include <algorithm>
 
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<int[]> 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 <string>
#include <iostream>
#include <utility>
 
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 << "尝试从右值临时量移动赋值 A\n";
    a1 = f(A()); // 从右值临时量移动赋值
    std::cout << "尝试从亡值移动赋值 A\n";
    a2 = std::move(a1); // 从亡值移动赋值
 
    std::cout << "尝试移动赋值 B\n";
    B b1, b2;
    std::cout << "移动前,b1.s = \"" << b1.s << "\"\n";
    b2 = std::move(b1); // 调用隐式移动赋值
    std::cout << "移动后,b1.s = \"" << b1.s << "\"\n";
 
    std::cout << "尝试移动赋值 C\n";
    C c1, c2;
    c2 = std::move(c1); // 调用复制赋值运算符
 
    std::cout << "尝试移动赋值 D\n";
    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<int> a1(0);//ok
  std::atomic<int> a2 {0};//ok
  std::atomic<int> a3 = 0;//error

第三行代码的报错如下:

/home/insights/insights.cpp:15:20: error: copying variable of type 'std::atomic<int>' invokes deleted constructor
  std::atomic<int> 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<int>( )
1 error generated.

但是()=没有问题

#include <iostream>
#include <atomic>
 
int main()
{
  double x=1.2,y=2,z=3;
  int sum1 = x+y+z;
  int sum2( x+y+z);
  std::cout<<"sum1="<<sum1<<"   sum2="<<sum2<<std::endl;
}

运行结果为

sum1=6   sum2=6

任何能解析为声明的都要解析为声明

#include <iostream>
using namespace std;
 
class A{
    public :
        A(int y):m(y){
            cout<<"A 构造 "<<m<<endl;
        }
    private:
        int m;
};
int main()
{
  A a(10);//ok
  A a1();//warning
  A a2{2};//ok
}

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

<source>:16:7: warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
   16 |   A a1();
      |       ^~

{}陷阱

没有std::initializer_list的情形

#include <atomic>
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "<<m<<"    "<<n<<endl;
        }
        A(int x,double y):m(x),h(y){
            cout<<"A(int x,double y) 构造 "<<m<<"    "<<h<<endl;
        }
    private:
        int m;
        bool n;
        double h;
};
int main()
{
    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

存在std::initializer_list,调用{}编译器会强烈优选initializer_list

#include <iostream>
#include <atomic>
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "<<m<<"    "<<n<<endl;
        }
        A(int x,double y):m(x),h(y){
            cout<<"A(int x,double y) 构造 "<<m<<"    "<<h<<endl;
        }
        A(initializer_list<double>li){
            cout<<"A(initializer_list<double>li) 构造 ";
            for(auto beg=li.begin();beg!=li.end();++beg)
                cout<<*beg<<" ";
            cout<<endl;
        }
    private:
        int m;
        bool n;
        double h;
};
int main()
{
    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_list<double>li) 构造 10 1 
A(int x,double y) 构造 10    5
A(initializer_list<double>li) 构造 10 50 

隐式窄化转换陷阱

#include <iostream>
#include <atomic>
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "<<m<<"    "<<n<<endl;
        }
        A(int x,double y):m(x),h(y){
            cout<<"A(int x,double y) 构造 "<<m<<"    "<<h<<endl;
        }
        A(initializer_list<bool>li){
            cout<<"A(initializer_list<bool>li) 构造 ";
            for(auto beg=li.begin();beg!=li.end();++beg)
                cout<<*beg<<" ";
            cout<<endl;
        }
    private:
        int m;
        bool n;
        double h;
};
int main()
{
    A a1(10,true);
    A a2{10,true};
    A a3(10,5.0);
    A a4{10,50.00};
}

报错如下:

<source>: In function 'int main()':
<source>:27:17: error: narrowing conversion of '10' from 'int' to 'bool' [-Wnarrowing]
   27 |     A a2{10,true};
      |                 ^
<source>:29:18: error: narrowing conversion of '10' from 'int' to 'bool' [-Wnarrowing]
   29 |     A a4{10,50.00};
      |                  ^

存在std::initializer_list,调用{}找不见initializer_list时的陷阱

#include <iostream>
#include <string>
using namespace std;
 
class A{
    public :
        A(int x,bool y):m(x),n(y){
            cout<<"A(int x,bool y) 构造 "<<m<<"    "<<n<<endl;
        }
        A(int x,double y):m(x),h(y){
            cout<<"A(int x,double y) 构造 "<<m<<"    "<<h<<endl;
        }
        A(initializer_list<string>li){
            cout<<"A(initializer_list<string>li) 构造 ";
            for(auto beg=li.begin();beg!=li.end();++beg)
                cout<<*beg<<" ";
            cout<<endl;
        }
    private:
        int m;
        bool n;
        double h;
};
int main()
{
    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_list<string>li)进行初始化
如下输出:

A(initializer_list<string>li) 构造 

vector中的初始化陷阱

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

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

编译后的函数为:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
  vector<int> v = std::vector<int, std::allocator<int> >(10, 20, std::allocator<int>());
  vector<int> v1 = std::vector<int, std::allocator<int> >{std::initializer_list<int>{10, 20}, std::allocator<int>()};
  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就地初始化与列表初始化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-西门吹雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值