身为C++的零基础初学者,短期内把《C++Primer》啃下来是一个比较笨但是有效的方法,一方面可以掌握比较规范的C++语法(避免被项目中乱七八糟的风格带跑偏),另一方面又可以全面地了解C++语法以及C++11新标准(后续要做的事情就剩下查漏补缺,不断完善自己的知识体系)。
个人感觉从零学习一门新知识比较好的方法是快速了解知识的全貌,然后构建自己的知识地图,后续不断地补充相应的细节。
由于《C++Primer》和大多数的教科书一样废话连篇,因此想要精炼一下每篇文章的内容再打印成pdf,方便温故知新。
这两个名词原本是从
C
继承过来的,主要是为了帮助记忆,左值可以位于赋值表达式左侧,而右值不行。
C++
的表达式要不然就是右值r-value
,要不然就是左值l-value
。但是在C++
语言中,两者的区别没有那么简单:
在需要右值的地方可以用左值来替代,但是不能把右值当成左值(也就是内存中的位置)来使用。当一个左值被当做右值来使用的时候,实际上使用的是它的内容(值)。
优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值,比如:int i = f1() * f2();
,我们只能知道f1
和f2
会在执行乘法之前被调用,但是不清楚f1
和f2
两者的调用顺序。这种情况在f1
和f2
同时修改了同个对象的值时可能引发非预期的错误。
有四种运算符明确规定了运算对象的求值顺序:
&&
:先求左侧||
:先求左侧?:
:右结合律,
:先求左侧需要注意如下几点:
short
型数值为32767
,这时候+1
可能输出-32768
(这是因为符号位从0
变为1
,从而变成负值)。当然在别的系统程序的行为可能不同甚至崩溃。/
除法运算在运算对象都是整数时会将商的小数部分剔除,并且如果两个运算对象的符号相同则商为正,否则为负%
取余运算的两个运算对象必须是整数类型,如果m
和n
是整数且n
非零,则表达式(m/n)*n + m%n
的求值结果与m
相同。(这意味着如果m%n
不等于0
,则它的符号与m
相同)。具体示例如下:21 % 6; // 3
21 % 7; // 0
-21 % -8; // -5
21 % -5; // 1
总计一下,对于除法
/
而言,(-m) / n
和m / (-n)
都等于-(m / n)
;对于取余%
而言,m % (-n)
等价于m % n
,(-m) % n
等价于-(m % n)
逻辑与&&
和逻辑或||
都是先求左侧对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果才会计算右侧运算对象的值,这种策略被称为短路求值。基于短路求值的特点,我们可以通过左侧运算对象来确保右侧运算对象求值的正确性和安全性:
// 只能左侧运算对象为真则右侧运算对象才安全
index != s.size() && !isspace(s[index])
const int ci = i;
是一个初始化语句而非赋值语句,因为该左值是常量不可修改。C++11
新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象ival = jval = 0;
会将两个变量都赋值为0
后置版本也会将运算对象加/减一,但是求值结果是运算对象改变之前的值的副本。这两种运算符必须作用于左侧运算对象,其中前置版本呢将对象本身作为左值返回,后置版本将对象原始值的副本的作为右值返回。
除非必须,否则不用递增递减运算符的后置版本:前置版本的递增运算将值加1之后直接返回该运算对象,但是后置版本需要将原始值存储下来以便于返回这个未修改的内容,如果我们不需要修改前的值的话就是一种性能上的浪费。对于整数和指针类型而言,编译器可能对这种额外的工作进行优化,但是如果是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本习惯,这样不仅不需要担心性能问题,而且不会引入非预期的错误。
int i = 0, j;
j = ++i; // j = 1, i = 1: 前置版本得到递增之后的值
j = i++; // j = 1, i = 2:后置版本得到递增之前那的值
最常用的场景就是在一条语句中混用解引用和递增运算符的后置版本:
auto pbeg = v.begin();
// 输出元素直到遇到第一个负值
while (pbeg != v.end() && *pbeg >= 0)
cout << *pebg++ << endl; // 输出当前值并将pbeg向前移动一个元素
*pbeg++
这种写法非常普遍,会先把pbeg
的值加1,然后返回pbeg
的初始值的副本作为其求解结果,此时解引用的运算对象是pbeg
未增加之前的值。
点运算符和箭头运算符都可用于访问成员,ptr->mem
等价于(*ptr).mem
。需要注意的是解引用运算符优先级低于点运算符,所以必须加上括号。
条件运算符满足右结合律,意味着运算对象一般按照从右往左的顺序组合,因此我们使用嵌套条件运算符:
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass"
注意条件运算符的优先级非常低,所以一条长表达式中嵌套了条件运算子表达式时,通常需要在两端加上括号:
cout << ((grade < 60) ? "fail" : "pass"); // 输出pass或者fail
左移运算符<<
在右侧插入值为0
的二进制位,右移运算符>>
的行为则依赖其左侧运算对象的类型,如果该运算对象是无符号类型,在左侧插入值为0
的二进制位;如果该运算符是带符号类型,则在左侧插入符号位的副本或值为0的二进制位。
对于char
类型的运算对象首先提升为int
类型,提升时运算对象原来的位保持不变,往高位添加0
即可。接下来将提升后的值逐位求反。
1
则返回1
,否则为0
1
则返回1
,否则为0
1
则返回1
sizeof
返回一条表达式或者一个类型名字所占的字节数。
char
或者类型为char
的表达式执行sizeof
,返回1
sizeof
运算得到被引用对象所占空间的大小sizeof
得到指针本身所占空间的大小sizeof
运算得到指针你指向的对象所占空间的大小,指针本身不需要有效sizeof
运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof
运算并将所得结果求和string
对象或vector
对象执行sizeof
运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间因为
sizeof
的返回值是一个常量表达式,因此我们可以用sizeof
的结果声明数组的维度
int
类型小的整型值首先提升为较大的整型类型unsigned int
和int
运算时,int
类型转换为unsigned int
。但是需要注意如果int
类型为负,则可能带来一定的副作用(因为无符号类型无法显示负值)。unsigned int
和long
,并且int
和long
的大小相同,则long
类型转换为unsigned int
,如果long
类型占用空间大于int
,则unsigned int
类型转换为long
。0
或nullptr
可以转换为任意指针类型;指向任意非常量的指针可以转换为void*
;指向人以对象的指针能转换为const void*
static_cast
任何具有明确定义的类型转换,只要不包含底层const
就可以使用static_cast
,一种常用的方法是把一个较大的算术类型赋值给较小的类型,这种用法告诉编译器和读者:我们知道并不在乎潜在的精度损失。
int i, j;
double slope = static_cast<double> (j) / i;
另一种用法对于编译器无法自动执行的类型转换也非常有用,例如我们使用static_cast
召回存在与void*
指针中的值:
void* p = &d;
double *dp = static_cast<double*>(p);
const_cast
const_cast
只能改变运算对象的底层const
,一旦我们去掉了某个对象的const
性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,那么使用强制类型转换获得写权限是合法的行为,但是如果对象是一个常量,再使用const_cast
执行写操作就会产生未定义的后果:
const char *pc;
char *p = const_cast<char*>(pc); // 正确,但是通过p写值是未定义的行为
reinterpret_cast
使用
reinterpret_cast
是非常危险的,主要是因为类型改变了但是编译器没有给出任何警告或者错误的提示信息。
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释,例如:
int *ip;
int *pc = reinterpret_cast<char*>(ip);
// 必须牢记pc的真实对象时一个int而非字符,如果把pc当成普通的字符指针容易在运行时发生错误,例如使用string str(pc);
如果替换后不合法,则旧式的强制类型转换执行与
reinterpret_cast
具有类似的功能。因此使用旧式的强制类型转换是不被推荐的行为。
type (expr); // 函数形式的强制类型转换
(type) expr; // C语言风格的强制类型转换