优先使用大括号初始化语法
- {}初始化是最广泛的初始化语法,它可以阻止窄化转换,并且避免了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 )
#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"}; // 从花括号初始化器列表赋值
}
- 复合赋值(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 <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就地初始化与列表初始化