变量与运算
基本数据类型
变量的声明类型
我们可以用一个类型声明多个变量,比如:
int a, b;
这里,基本数据类型是int。
当我们需要指针时:
int* pa, b;
这里的pa就是int型指针。但你可能会把b也错看成一个int指针,但实际上b只是一个int。pa和b的基本数据类型都是int,*只修饰pa。这也是为什么声明指针型变量时常常把星号和变量名连接在一起。同时声明两个指针的写法是:
int *pa, *pb;
尝试解释以下变量的含义:
int* pa, a, &ra, func1(), *func2();
a是一个int型变量,pa是一个int型指针,ra是一个int型引用,func1是返回值类型为int的函数的声明,func2是返回值类型为int*的函数的声明。
int和long有什么区别?
对于如今大部分编译器(Windows环境下)来说,int和long这两种类型都是占用4字节,为什么会这样呢?
对于没有经历过16位时代的人来说,可能会好奇这个问题。
其实,在早期16位环境下,int的大小并非如今的4字节,而是2字节。那时候,4字节的long就已经很“长”了。
到了32位乃至64位时代,int也和long一样进化到4字节,此时就有了long long这种类型,为8字节。
但在64位的Linux环境下,long为8字节的情况却很常见。
其实,int和long是多少字节并不是取决于系统的位数或者是在什么系统下,而是取决于编译器。
目前,最稳定的类型是char(1字节)、short(2字节)、long long(8字节)。
int也是比较稳定的,可以放心使用,除非你的环境是16位的。如果不确定当前环境某个数据类型的大小,可用使用sizeof运算符,如sizeof(int)
。
另外,long int和long是一样的,long long int和long long也是。
C++标准规定,int的大小大于等于short,long的大小大于等于int,long long大于等于long。
对于字面常量,你可以用字母标注:
1e-10F; //单精度浮点数
9999999999LL; //long long
9999999999ULL; //unsigned long long
9999999999LLU; //同上
3.14159265358979L; //long double
既可以用小写字母,也可以用大写字母,但不建议使用小写字母“l”,它容易与数字“1”弄混。
对于浮点类型,大部分情况下double就已经够用了。long double也是一个不稳定的类型,可能是10字节,可能是12字节,也有可能是16字节,用它来计算开销也会比较大。float的精度太小,不建议使用,而且目前的计算机大部分都对双精度浮点数的计算做了优化,运算速度甚至比单精度还快。
long long是C++11标准新增的,long double是C++99标准新增的。
stdint
我们常用char表示一个字符,但它只能表示ASCII字符表内的字符,因为它只有1字节的大小。由于C++没有“byte”类型(即占用1字节的整型),我们常用char来代替它。由于C++是弱类型语言,因此以下代码是合法的:
char a = 1;
当然,为了让这个类型好看一些,你可以使用typedef:
typedef signed char byte;
byte a = 1;
或者使用stdint.h
提供的int8_t
来代表有符号的8比特位整数,uint8_t
代表无符号的。
不过呢,不建议直接使用char来记录数值,因为char不一定是signed char,它也有可能是unsigned char,具体由编译器决定。数值建议使用int8_t,而char则用来表示字符。
另外,它也提供了int16_t
、uint16_t
、int32_t
、uint32_t
、int64_t
、uint64_t
。
慎用无符号类型
如果一个数是非负整数,使用unsigned
可以让变量表示更大的数值。
对于大部分情况你完全可以用一个更大的数据类型来代替它,比如用long long代替int。
这是因为使用unsigned有可能会导致很多奇怪的bug。比如:
unsigned int i;
for (i = 3; i >= 0; --i){
std::cout << i << " ";
}
以上代码将会造成无限循环。
尽管我们不会故意给无符号赋负值,但我们依然可能在无意中写出这样的代码:
unsigned int a = 1;
int b = -2;
std::cout << b + a << std::endl;
输出:4294967295
b是一个有符号的类型,给他加1,不应该是-1吗?实际上,如果一个算术表达式中既有无符号类型又有有符号数时,会把有符号数转化为无符号数,无论他们谁在前谁在后。
另外,一些你没有意识到的隐形转换也会让你寻找bug时痛苦不已。
除非你是在表示一个位组(bit pattern),即使用整型来保存一系列二进制的0和1,而不是表示一个真实的数值,不然请慎用无符号类型。
如果非要使用无符号类型来表示数值,一定要注意保护数据,例如使用断言来阻止负数的进入。
char、wchar_t、char16_t、char32_t
char只占1个字节,也就是8bits的空间,那么它能够表示的只有有符号的0-127或无符号的0-255。我们一般是使用有符号的char,也就是只能表示ASCII字符集中的内容。
由于char只能表示8bits的内容,所以如果我们想表示中文或其他不在ASCII范围内符号时,char就无能为力了。
wchar_t用来表示宽字符,但一般Windows普通下它是16位(2字节),Linux下是32位(4字节),是不稳定的。
用wchar_t
来表示宽字符或字符串时,需要在前面加上L
,如:
wchar_t c = L'啊';
wchar_t str[] = L"确实";
对于宽字符,cout无法直接输出,对此iostream提供了wcout来输出它。但大部分情况下直接输出它会发现什么都没有输出:
std::wcout << L"无语" << std::endl;
(啥都没有的)输出:
为此,你需要设置一下locale,让它能够支持中文环境:
std::setlocale(LC_ALL, "zh-CN");
std::wcout << L"无语" << std::endl;
输出:无语
或者...(还是啥都没有的)输出:
为什么会这样呢?这是因为L"..."
是宽字符,但未必是Unicode字符。在不同的编码字符集中,同一个数字代表的可能是不同的字符,比如专门用来支持中文的GBK编码,和为了支持所有语言字符而制定的Unicode编码等。这也是为什么在不同编码环境下中文可能会变成乱码,而英文却不会(因为几乎所有编码都向下兼容ASCII,英文的编码都是相同的)。即便是Unicode编码,虽然它也在不断迭代升级的,但是否能永远成为以后的标准还是未知数。
出于历史原因,C++输出非ASCII范围的字符并没有后来的一些编程语言那么轻松。对于如何正确输出他们,不在本文的讨论范围内,感兴趣的可以使用搜索引擎查询。
这里给的建议是,无论目标设备的编码是什么,都使用Unicode作为中间格式,字面常量、文件都使用Unicode编码,到输出设备中再做转换。
C++11开始有了char16_t和char32_t,分别用于表示16位(2字节)和32位(4字节)的字符,而且是固定位数的,不像wchar_t那样不稳定。但在标准输出中,缺少输出他们的方法。不过如果使用第三方库或者为图形化界面编写程序的话,大部分还是会提供相应的方法输出他们的。
C++11还引入了一种新的转义字符,用来表示Unicode字符:
const char *c = "\u597d";
以及说明字符串是以UTF-8格式编码(只能用于字符串字面常量):
const char *c = u8"好";
除此之外还有前缀u(Unicode 16位字符,对应char16_t)、U(Unicode 32位字符,对应char32_t)。
当函数指针与数组相遇
对于常见的变量类型,你一定能一眼认出来他们:
int a;
:int型变量a
int *a;
:int型指针变量a
int a[5];
:长度为5的数组a,每个元素是指向int,即
a : [int, int, int, int, int]
但当变量类型复杂起来时,就没那么好认了:
int *a[5];
:长度为5的数组a,每个元素是指向int的指针,即
a : [int*, int*, int*, int*, int*]
因为[]
的优先级高于*
,故先a是数组,再数组中的元素是int*类型指针。
int (*a)[5];
:一个指针,指向长度为5的数组,数组的每个元素是指向int,即
a -> [int, int, int, int, int]
这两个的区别,一个是指针的数组,另一个是数组的指针。如果我们对他们分别做自增操作,会怎么样呢?这个到下一章节数组与指针再讲。
再来看点更复杂的:
int (*f[5])();
:长度为5的数组a,每个元素是指向函数的指针,函数返回值类型是int,即
a : [int (*f)(), int (*f)(), int (*f)(), int (*f)(), int (*f)()]
int (*(*f)[5])();
:一个指针f,指向长度为5的数组,数组的每个元素是指向函数的指针,函数返回值类型为int,即
a -> [int (*f)(), int (*f)(), int (*f)(), int (*f)(), int (*f)()]
int* (*f[5])();
:长度为5的数组f,每个元素是指向函数的指针,函数返回值类型是int*,即
a : [int* (*f)(), int* (*f)(), int* (*f)(), int* (*f)(), int* (*f)()]
int* (*(*f)[5])();
:一个指针,指向长度为5的数组,数组的每个元素是指向函数的指针,函数返回值类型为int*,即
a -> [int* (*f)(), int* (*f)(), int* (*f)(), int* (*f)(), int* (*f)()]
如果用上二级指针,情况可能就没那么好对付了。比如:
int (*(*f[5]))()
相当于int (**ff3[5])()
。
但这已经脱离了实用价值,虽然可以通过编译,但实际情况很难像这样复杂,且这样的代码对可阅读性将会是灾难性的破坏。
运算符
i++ 与 ++i
i++
和++i
单独出现时的作用都是自增。但i++
返回的是i自增前的数值,而++i
返回自增后的值,如:
int i = 1;
std::cout << i++ << std::endl;
i = 1;
std::cout << ++i << std::endl;
输出:
1
2
由于i++
的特性,便可以这样复制字符串:
char str1[16] = "Hello World";
char str2[16];
char *p1 = str1, *p2 = str2;
while(*p2++ = *p1++);
同时,++i
可以理解为返回i自增后i的引用,所以你可以给他赋值:
int i = 1;
++i = 5;
std::cout << i << std::endl;
i++ = 5; //错误!
输出:5
int i = 1;
int &a = ++i;
a = 5;
std::cout << i << std::endl;
int &b = i++; //错误!
输出:5
在性能上,++i
会比i++
效率高,但大多数编译器都会对代码进行优化,当他们单独出现时(普通变量),是没有什么区别的。但对于某些类对象,obj++
会产生一个新的临时对象,故此时++obj
性能要高一些。
运算符优先级
在计算正整数乘法时,如果乘数是2的N次幂,可以使用左移运算符(<<
),左移的位数为N。
比如,3 * 16
可表示为3 << 4
。
实际开发中,运算的表达式可能会比较复杂,比如,在上面的乘法之后再加一个数:3 << 4 + 1
,在这个例子中,期望得到的结果是49,但实际上得到的是96,这是因为左移运算符(<<
)的优先级比加号(+
)要低,所以会先计算4 + 1
,然后再3 << 5
。因为乘号(*
)的优先级比加号(+
)高,因此把乘号改为左移符号时就容易犯这种错误。加上括号即可解决该问题:(3 << 4) + 1
。
对于难以分辨的运算符优先级,建议加上括号,这样不仅不容易出问题,也能更容易看出运算的前后顺序,提高可阅读性。
“i+=i++”、“a+=a+=a+=(a++)+(++a)”
对于这种谭式(没有贬低的意思)题目,我的回答是:去问编译器。
看起来像是一个考运算符优先级的题,但在考虑这个表达式的结果之前,不妨考虑一下为什么要写出这样的代码。
以i+=i++
为例,实际上(严格上来说是C++17之前),这种写法属于未定义的行为,不同编译器对该行为可能会有不同的解释,从而造成结果的不同,所以这样的写法毫无意义。
与其去纠结回字的多种写法,不如脚踏实地采用最简单的写法。对于绝大部分开发规范来说,以上写法是不允许出现的,比如像+=
这样的赋值运算符后面不允许有复杂表达式,应该杜绝两个或以上的自增(++
)、自减(--
)进行合成等。这样不仅可以提高程序的可阅读性,还能避免出现不可预料的结果。
左值与右值
左值可看作内存中有明确地址的数据,它是可寻址的。
右值是可提供数据值的数据,不一定可寻址。
比如以下代码
int x = 1;
x是左值,它是可寻址的,生命周期较长,我们可以对它进行其他操作;1是右值,它是一个临时的常量,不可寻址,生命周期较短。
左值可以作为右值使用,如:
int a, b;
a = b = 1;
上面的b = 1
中,1是右值,b是左值,而在a = b
中,b又作为了右值,此时a为左值。
左值引用和右值引用
左值引用(lvalue-reference)
左值引用是我们最熟悉的给变量起别名:
int a = 1;
int &b = a;
b = 2;
std::cout << a << std::endl;
输出:2
左值引用必须指向一个已经存在的地址,下面的代码是非法的:
int &b = 1; //错误
这是因为1是右值,没有指向它的内存地址。
这便是常见报错:“非常量引用的初始值必须是左值”的原因。
如果需要将常量赋给左值引用,可以把左值声明为const,下面的代码是合法的:
const int &a = 1;
编译器会创建一个隐藏的变量储存这个右值的字面常量,然后再把这个隐藏的变量与引用绑定。
由于函数使用引用形参时,传值不会复制一个新的对象,可以节省开销。但为了既能够传入变量也能够传入常量,且告诉用户不会修改实参的值,那么形参的声明就可以这样写:
void func(const int &a);
再来看这个例子:
void func(int *&a);
int main(){
int a, *p = &a;
func(p); //合法
func(&a); //非法
}
第1行代码函数形参int *&a
指的是int类型的指针的引用,要理解这个,首先int &a
表示a的引用,只能传入变量不能传入常量。因为是引用,即变量的别名,假设函数func内改变了a的值,那么在main中调用func时,传入的实参a也会被改变,因为传入func的实参a的地址,和func处理的a的地址,是相同的。而int *a
是指针,传入的是地址。而int *&a
就是指针类型的引用,传入的指针实参可能被修改,同时不能传入常量只能传入变量。
第3行p是a的指针,且p是变量(左值),故可以把p传入func,且p可能被修改。而第5行的&a
为右值,不能赋给左值引用,故无法传入func。
如果把第一行的int *&a
改为int * const &a
,则以下调用均合法:
void func(int * const &a);
int main(){
int a, *p = &a;
func(p); //合法
func(&a); //合法
int * const p2 = &a;
func(p2); //合法
}
注:const放在星号*左边表示变量指向的数据不可修改,即指向的数据是常量;const放在星号*右边表示指针的指向不能修改,即指针本身是常量。所以上面要把const放*右边。
右值引用(rvalue-reference)
C++11引入了右值引用,用&&
表示,表示一个临时变量即右值,而且可以修改这个临时变量的值:
int &&a = 1;
a = 2;
不能将右值引用赋给左值,以下代码是非法的:
int a = 1;
int &&b = a; //错误
右值引用的作用
右值引用的概念看起来很迷惑,它有什么用呢?其实它大有用处。
如果你希望对传入的左值和右值做不同的操作,那么你可以这样重构函数:
void func(const int &a){
std::cout << "lvalue" << std::endl;
}
void func(int &&a){
std::cout << "rvalue" << std::endl;
}
int main(){
int a = 1;
func(a); //输出lvalue
func(2); //输出rvalue
}
再来看这个:
int func(const int &a){
std::cout << "lvalue" << std::endl;
return a;
}
int& func(int &&a){
std::cout << "rvalue" << std::endl;
return a;
}
int main(){
int a = func(func(1));
}
输出:
rvalue
lvalue
这是因为在func(func(1));
中,func(1)中的1是右值,故调用int& func(int &&a)
,而该函数又返回了一个左值,故func(返回的左值);
时调用了int func(const int &a)
。
但这又有什么用呢?其实这个特性对移动语义十分有用。
思考这样一个问题:如何将一头大象装进冰箱中。我们可以自然地想到,打开冰箱门,放入大象,关闭冰箱门。
不幸的是,在C++11之前是这样做的:复制一头大象,装入冰箱,删除冰外的大象:
class Refrigerator{
public:
std::string content;
Refrigerator(std::string s) : content(s){}
};
int main(){
Refrigerator r("Elephant");
}
main中,"Elephant"
会复制一份传入Refrigerator的构造函数,这复制操作明显增加了开销。
为了避免这种开销,需要:
class Refrigerator{
public:
std::string content;
Refrigerator(const std::string &s) : content(s){}
};
int main(){
std::string s = "Elephant";
Refrigerator r(s);
}
有了右值引用,我们就可以:
class Refrigerator{
public:
std::string content;
Refrigerator(std::string &&s) : content(s){}
};
int main(){
Refrigerator r("Elephant");
}
这样,临时的"Elephant"
就可以废物利用,避免了复制大象的额外开销。
上述代码投入的是右值。对于左值,因为构造函数形参为左值和右值这两种情况都可以分别被重载,所以只需再加一个Refrigerator(const std::string &s) : content(s){}
,但如果我们可以放弃对左值资源的所有权,那么可以使用std::move
来将其转化为右值使用:
class Refrigerator{
public:
std::string content;
Refrigerator(std::string s) : content(std::move(s)){
std::cout << "s:" << s << std::endl;
}
};
int main() {
std::string s = "123";
Refrigerator r1(s);
Refrigerator r2("456");
}
输出:
s:
s:
可以看到无论是左值还是右值均可传入该构造函数内,且在构造函数中由于s被移动了,所以输出s的值会发现啥都没有。
查看std::move()
的源码,可以发现它其实是static_cast<T&&>()
的简单封装。
引用作为函数返回值
对于一个值类型的函数返回值,它返回的是一个右值:
int func();
int main(){
int && a = func(); //合法,a只能赋右值
}
当引用作为函数的返回值时,你不能返回一个函数内的临时变量或者右值,因为的生命周期只在该函数的作用域内,出了函数就被销毁了:
int& func(){
return 2; //错误,不能返回右值
}
int& func(){
int i = 1;
return i; //错误,i的生命周期只在该函数内,不能返回局部变量的引用
}
引用作为函数返回值,可以避免复制对象产生的额外开销:
不使用引用作为返回值时:
int globalVar;
int func(){
globalVar = 1;
std::cout << &globalVar << std::endl;
return globalVar;
}
int main(){
int a = func();
std::cout << &a << std::endl;
}
输出:
0x7ff6b8078040
0xaede1ff7ec
使用引用作为返回值:
int globalVar;
int& func(){ //返回值是引用
globalVar = 1;
return globalVar;
}
int main(){
std::cout << "globalVar地址:" << &globalVar << std::endl;
int a = func(); //将返回值赋给普通变量
std::cout << "a地址:" << &a << std::endl;
int& b = func(); //将返回值赋给引用
std::cout << "b地址:" << &b << std::endl;
}
输出:
globalVar地址: 0x7ff77a438040
a地址: 0x1b22bff814
b地址: 0x7ff77a438040
我们可以看到,func返回的是引用,可以理解为返回的是左值。
main中的a是一个普通变量,声明一个普通的变量会在内存中开辟一个新的地址,然后才把func得到的值赋给它,相当于int a = globalVar;
。
main中的b是一个引用,func返回的是左值,是可寻址的,所以可以赋给b,而且它指向globalVar,b是globalVar的别名,相当于int& b = globalVar;
。
因为func返回的是左值,于是我们可以给他赋值:
int globalVar;
int& func(){ //返回值是引用
return globalVar;
}
int main(){
func() = 2;
std::cout << globalVar << std::endl;
}
输出:2
另外,不应该去返回函数内由new分配的变量的引用,这是不好的习惯,因为如果返回的变量作为临时变量而不是赋给实际变量时,就会导致申请的内存无法被释放,造成内存泄露。
比如:
int& func();
int main(){
int result = func() + 1;
}
这个func看着人畜无害的样子,但实际上它返回的是函数内使用new分配的值。用户在不看源码或文档的情况下,怎么知道它分配了内存呢?
你可能不知道的const
常量指针?指针常量?
对于指针来说,const在不同的位置会有不同的效果。
- const在*前:被指物是常量
- const在*后:指针本身是常量
- const在*两边:被指物和指针都是常量
如
const char * c;
:指针指向的字符是常量,不能修改这个字符的值。而指针本身是变量,可以修改指针的指向。char const * c;
:同上,因为const还是在*的前面,与char的相对位置无关。char * const c;
:指针本身是常量,不能修改指针的指向,但可以修改指针指向的字符。const char * const c;
:指针是常量,指针指向的值也是常量。char const * const c;
:同上。
实现常量的方式
const的作用是实现一个语义约束,让一个对象不可改动。
除了const,也有其他实现常量的方式。
宏定义是其中一种方法,但非常不建议使用这种方法,因为宏定义的常量并不一定会被编译器发现,而且一旦出现错误,你将会收获匪夷所思的错误:
// error_is_here.h
#define MY_CONSTANT 1.463
func(MY_CONSTANT); // 假设这里错误使用了宏常量
得到的错误:Error:1.463
你会发现,得到的错误是1.463,而不是MY_CONSTANT
,更不是error_is_here.h
,你就会疑惑这个1.463是哪来的,从而难以定位到错误的位置。如果这个头文件是你写的还好,但如果它是由别人写的,你就更难直到这个变量是哪来的了。
Effective C++给出的建议是,使用const、enum、inline代替#define。
TODO
NULL和nullptr
在C++11之前,我们常用NULL表示空指针。但NULL只是一个宏,而且C和C++中对此的定义均不相同。
C:
#define NULL (void*)0
C++:
#define NULL 0
可见在C++中,NULL是整数0。
如果int* p = NULL;
,由于C++中当一个指针为0时,视作空指针,所以这是没问题的。
但如果有这样的情况:
void func(int);
void func(int*);
int main(){
func(NULL);
}
则会报错,因为并不知道这里的NULL究竟指的是数值0,还是空指针,因此产生了二义性。
为了解决这一问题,C++11引进了nullptr,它是一个特殊的字面常量,类型为std::nullptr_t,用于代表空指针,可以转换为其他不同类型的指针。
如果把上面的代码中的NULL改为nullptr,则调用的函数原型为void func(int*)
。
野指针的出现原因和避免方式
野指针是一种指向不明确的、随机的指针。
当我们声明一个局部变量时,它会在内存中寻找一个空闲的位置。这这个位置上可能原先是有内容的,当这个内容不再被使用后,它并不会被清理掉。
所以如果我们声明一个变量后没有去初始化它,那么它的值将会是随机的:
int a;
std::cout << a;
输出:32759(随机)
我们知道,指针存放的内容是一个地址,那么当一个局部指针被声明却没被初始化时,恰巧这片空间中有内容,那么此时这个指针就会指向一个随机的地址。
因为野指针的指向是不明确的,所以当我们操作这个指针时,就会产生各种意料之外的事情(去修改内存中随机位置的数据的值?)。
当指针越界访问时,也会变成野指针。比如我们操作数组时,不小心操作过头了,C++并不会像其他语言那些报数组越界错误,而是去访问越界后对应内存的数据:
int arr[] = {1, 2, 3, 4, 5};
std::cout << arr[5] << std::endl; // 访问“数组中的5”后面内存区域的数据。
int *p = arr;
p += 5;
std::cout << *p << std::endl; // 同理,越界访问
当指针指向的数据被释放时,也会成为野指针:
int* func(){
int a = 1;
return &a;
}
int main(){
int *p = func();
std::cout << p << std::endl;
}
在函数func中,a的作用域只在函数func中,当func运行结束时,a被释放,a所在的内存空间变为空闲空间,随时可能被其他数据占领。此时在main函数中的指针p,指向的就是未知的内容,成为了野指针。同样,如果p指向使用new或malloc分配的空间,当这个空间被释放后,p也会成为野指针。
还有一种情况,指针一般指向某个对象的第一个字节的地址。假设这个对象有4个字节,而指针却指向了这个对象的第2个字节的位置,也会出问题。
如何避免野指针的出现?
- 初始化指针。我们最好在指针被定义时就初始化它,或者初始化为nullptr。
- 注意不要让指针越界。
- 当指针指向的对象被释放时,将指针设为nullptr。
- 确保字符数组有"\0"结尾。
初始化对象时的疑惑
首先要区别初始化和赋值的区别。很多语言使用等号“=”来给对象进行初始化,比如Java:
Object obj = new Object();
而在C++中初始化是很复杂的过程。初始化和赋值是两个不同的操作,初始化不是赋值,初始化是在创建变量时赋予一个初始值,而赋值则是把当前值擦除,然后用一个新的值代替。
在C++中,假设有个类Widget,并初始化一个obj:
class Widget{
public:
Widget();
Widget(std::string);
};
int main() {
Widget w("123");
}
Widget类中有两个构造函数,调用第二个构造函数时给形参传入值“123”,这没什么问题吧?但当我们调用第一个无参构造函数时,就有可能写下这样的代码:
Widget w();
看似创建了一个对象w,但实际上却是声明了一个函数。实际上你应该这样写:
Widget w; // 调用默认构造函数
//或者
Widget ww = Widget();
但这看起来又像是声明了一个变量,不像是初始化的样子。
实际上,C++的初始化相当的混乱不堪,你可以用等号“=”和括号“()”初始化对象:
int a = 1;
int b(2);
int c = int(3);
Widget w;
Widget ww = Widget();
于是,C++11提供了一种统一的初始化方式,即大括号初始化:
int a{1};
Widget w{};
//也可以
int b = {2};
Widget ww = Widget{};
而且,如果存在信息丢失的风险,则会报错:
int a = 1;
short b = a, c(a); // 可以通过编译,实际上存在信息丢失风险
short d = {a}, e{a}; // 不能通过编译
这有利于提醒你注意信息丢失风险,而不会因为编译器偷偷摸摸的转换而头疼。
大括号初始化对列表初始化也有作用,这个到后面再讲。
大括号初始化也不是没有缺点,比如当类的构造函数的形参中有std::initializer_list
时,就容易被劫持:
class Widget {
public:
Widget(int, int);
Widget(std::initializer_list);
};
int main(){
Widget w{1, 2};
}
调用的是Widget(std::initializer_list<int>);
这个构造函数。
此时使用圆括号就没有问题:Widget w(1, 2);
。
要想用圆括号调用initializer_list的构造函数,则需要:Widget w({1, 2});
另外一种情况,如果类中既有无参构造函数,也有initializer_list,那么用空大括号初始化对象时,调用哪一个构造函数呢?
class Widget {
public:
Widget();
Widget(std::initializer_list);
};
int main(){
Widget w{};
}
答案是Widget();
,如果需要调用含initializer_list的构造函数,则需要这样初始化:Widget w({});
。
位运算
位运算替代名
除了使用符号&
、|
等来进行位运算,C++还提供了保留字用来代替他们,他们在C++98就已经存在了:
- compl:~
- bitand:&
- bitor:|
- xor:^
- and_eq:&=
- or_eq:|=
- xor_eq:^=
注意和逻辑与和逻辑或区分:
- and:&&
- or:||
- not:!
- not_eq !=