- 何谓初始化
- 何谓赋值
- 非类类型
- 类类型
- 赋值运算符
- 移动赋值运算符
- `()`、`{}` `=`
- 就地初始化
- 不可复制对象不能使用`=`
- `{}`禁止内建类别的隐式窄话转换
- 任何能解析为声明的都要解析为声明
- `{}`陷阱
- 没有`std::initializer_list`的情形
- 存在`std::initializer_list`,调用`{}`编译器会强烈优选`initializer_list`
- 隐式窄化转换陷阱
- 存在`std::initializer_list`,调用`{}`找不见`initializer_list`时的陷阱
- `vector`中的初始化陷阱
- 参考文献
何谓初始化
- {}初始化是最广泛的初始化语法,它可以阻止窄化转换,并且避免了C++最复杂的语法解析
- 在构造函数做函数重载的时候,{}会优先匹配带有std::initializer_list参数的版本,即使其他构造函数看起来更匹配
- 对与std::vector两个参数的构造函数来说,其{}和()两种初始化方式有很大的不同
- 在模版中对于{}和()初始化如何进行选择是一个挑战
变量的初始化会在构造时提供变量的初始值。
初始值可以由声明符或 new 表达式的初始化器部分提供。在函数调用时也会发生:函数形参及函数返回值也会被初始化。
对于每个声明符,初始化器必须是下列之一:
- ( 表达式列表 ) 用逗号分隔的任意表达式列表和用括号括起来的大括号初始化列表
- = 表达式 等号后面跟着一个表达式
- { 初始化器列表 } 带括号的初始化列表:可能为空,逗号分隔的表达式列表和其他带括号的初始化列表
根据上下文的不同,初始化器可以调用:
- 值初始化, std::string s{};
- 直接初始化,std::string s("hello");
- 拷贝初始化,std::string s = "hello";
- 列表初始化,std::string s{'a', 'b', 'c'};
- 聚合初始化,char a[3] = {'a', 'b'};
- 引用初始化, char& c = a[0];
如果没有提供初始化式器,则使用默认初始化规则。
何谓赋值非类类型定义变量后,可以使用=操作符(在单独的语句中)给它赋值。这个过程被称为拷贝赋值(或简称赋值)。之所以这样命名复制赋值,是因为它将=操作符右侧的值复制到操作符左侧的变量。=操作符称为赋值操作符。
赋值的一个缺点是,它至少需要两条语句:一条用于定义变量,另一条用于赋值。
- 复制赋值运算符以 b 内容的副本替换对象 a 的内容(不修改 b)。
- 移动赋值运算符以 b 的内容替换对象a的内容,并尽可能避免复制(可以修改 b)
直接赋值表达式的形式为
- 左操作数 = 右操作数
- 左操作数 = {} (C++11 )
- 左操作数 = { 右操作数 } (C++11 )
#includeint 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"}; // 从花括号初始化器列表赋值 }
- 复合赋值(compound assignment)运算符以 a 的值和 b 的值间的二元运算结果替换对象a的内容。
复合赋值表达式的形式为
- 左操作数 运算符 右操作数
- 左操作数 运算符 {} (C++11 起)
- 左操作数 运算符 { 右操作数 } (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::atomica1(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
#includeusing 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类型对象的函数,会产生如下警告:
{}陷阱 没有std::initializer_list的情形:16:7: warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse] 16 | A a1(); | ^~
#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就地初始化与列表初始化


![Effective Modern C++[实践]->在创建对象时注意区分()和{} Effective Modern C++[实践]->在创建对象时注意区分()和{}](http://www.mshxw.com/aiimages/31/999017.png)
