[C++进阶]容易被忽视的细节之-基础篇

1,844次阅读
没有评论

变量与运算

基本数据类型

变量的声明类型

我们可以用一个类型声明多个变量,比如:

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_tuint16_tint32_tuint32_tint64_tuint64_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个字节的位置,也会出问题。

如何避免野指针的出现?

  1. 初始化指针。我们最好在指针被定义时就初始化它,或者初始化为nullptr。
  2. 注意不要让指针越界。
  3. 当指针指向的对象被释放时,将指针设为nullptr。
  4. 确保字符数组有"\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 !=

Lyzen
版权声明:本站原创文章,由 Lyzen 2022-08-16发表,共计1893字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码