四、 表达式
1. 算术运算符
高优先级的操作符要比低优先级的结合得更紧密。这些算术操作符都是左结合。
操作符 |
功能 |
用法 |
+ |
unary plus(一元正号) |
|
- |
unary minus(一元负号) |
|
* |
multiplication(乘法) |
|
/ |
division(除法) |
|
% |
remainder(求余) |
|
+ |
addition(加法) |
|
- |
subtraction(减法) |
|
2. 位操作符
左结合,移位操作符具有中等优先级:其优先级比算术操作符低,但比关系操作符、赋值操作符和条件操作符优先级高。
操作符 |
功能 |
|
~ |
位求反 |
|
<< |
左移 |
在右边插入 0 以补充空位 |
>> |
右移 |
如果其操作数是无符号数,则从左边开始插入 0;如果操作数是有符号数,则插入符号位的副本或者 0 值 |
& |
位与 |
如果两个操作数对应的位都为 1,则操作结果中该位为 1 |
^ |
位异或 |
在每个位的位置,如果两个操作数对应的位只有一个为 1,则操作结果中该位为 1,否则为 0。 |
| |
位或 |
|
1. 与其他二元操作符不同,赋值操作具有右结合特性。多个赋值操作中,各对象必须具有相同的数据类型,或者具有可转换为同一类型的数据类型。
2. 使用复合赋值操作时,左操作数只计算了一次;而使用相似的长表达式时,该操作数则计算了两次,第一次作为右操作数,而第二次则用做左操作数。
3. 只有在必要时才使用后置操作符:因为前置操作需要做的工作更少,只需加1后返回加1后的结果即可。而后置操作符则必须先保存操作数原来的值,以便返回未加1之前的值作为操作的结果。
4. 假设有一个指向类类型对象的指针(或迭代器),下面的表达式相互等价:
(*p).foo; // dereference p to get an object and fetch its member namedfoo
p->foo; // equivalent way to fetch the foo from the object to whichp points
3. sizeof
sizeof操作符的作用是返回一个对象或类型名的长度,返回值的类型为size_t,长度的单位是字节。
该操作符有以下三种语法形式:
sizeof (type name);
sizeof (expr);
sizeof expr;
将 sizeof 用于 expr 时,并没有计算表达式 expr 的值。特别是在 sizeof*p 中,指针 p 可以持有一个无效地址,因为不需要对p做解引用操作。
使用 sizeof 的结果部分地依赖所涉及的类型:
• 对 char 类型或值为 char 类型的表达式做 sizeof 操作保证得 1。
• 对引用类型做 sizeof 操作将返回存放此引用类型对象所需的内在空间大小。
• 对指针做 sizeof 操作将返回存放指针所需的内在大小;注意,如果要获取该指针所指向对象的大小,则必须对指针进行引用。
• 对数组做 sizeof 操作等效于将对其元素类型做 sizeof 操作的结果乘上数组元素的个数。
5. 逗号表达式是一组由逗号分隔的表达式,这些表达式从左向右计算。逗号表达式的结果是其最右边表达式的值。如果最右边的操作数是左值,则逗号表达式的值也是左值。
4. 操作符的优先级
操作符 |
结合性 |
功能 |
用法 |
:: |
L |
全局作用域 |
:: name |
:: |
L |
类作用域 |
class::name |
:: |
L |
命名空间作用域 |
namespace::name |
. |
L |
成员选择 |
|
-> |
L |
成员选择 |
pointer.member |
[] |
L |
下标 |
variable[expr] |
() |
L |
函数调用 |
name(expr_list) |
() |
L |
类型构造 |
type(expr_list) |
++ |
R |
后自增 |
|
-- |
R |
后自减 |
|
typeid |
R |
类型ID |
typeid (type/expr) |
explicit cast |
R |
显示类型转换 |
cast_name<type>(expr) |
sizeof |
R |
|
sizeof expr/(type) |
++ |
R |
前自增 |
|
-- |
R |
前自减 |
|
~ |
R |
位求反 |
|
! |
R |
逻辑非 |
|
- |
R |
一元负号 |
|
+ |
R |
一元正号 |
|
* |
R |
解引用 |
|
& |
R |
取地址 |
|
() |
R |
类型转换 |
(type) expr |
new |
R |
创建对象 |
|
delete |
R |
释放对象 |
|
delete[] |
R |
释放数组 |
|
->* |
L |
指向成员操作的指针 |
ptr ->* ptr_to_member |
.* |
L |
指向成员操作的指针 |
|
* |
L |
|
|
/ |
L |
|
|
% |
L |
|
|
+ |
L |
加法 |
|
- |
L |
减法 |
|
<< |
L |
位左移 |
|
>> |
L |
位右移 |
|
< |
L |
|
|
<= |
L |
|
|
> |
L |
|
|
>= |
L |
|
|
== |
L |
|
|
!= |
L |
|
|
& |
L |
位与 |
|
^ |
L |
位异或 |
|
| |
L |
位或 |
|
&& |
L |
|
|
|| |
L |
|
|
?: |
R |
|
|
= |
R |
|
|
*=,/=,%=,+=,-=,<<=,>>=, &=,|=,^= |
R |
复合赋值操作 |
|
throw |
R |
|
|
, |
L |
|
|
5. 求值顺序
&& 和 || 操作符计算其操作数的次序:当且仅当其右操作数确实影响了整个表达式的值时,才计算这两个操作符的右操作数。C++中,规定了操作数计算顺序的操作符还有条件(?:)和逗号操作符。除此之外,其他操作符并未指定其操作数的求值顺序。
6. new 与 delete
定义变量时,必须指定其数据类型和名字。而动态创建对象时,只需指定其数据类型,而不必为该对象命名。取而代之的是,new表达式返回指向新创建对象的指针,我们通过该指针来访问此对象。
int i; // named, uninitialized int variable int *pi = new int; // pi points to dynamically allocated,unnamed, uninitialized int
如果不提供显式初始化,动态创建的对象与在函数内定义的变量初始化方式相同。对于类类型的对象,用该类的默认构造函数初始化;而内置类型的对象则无初始化。
int *pi = new int(1024); // object to which pi points is 1024 string *ps = new string(10, '9'); // *ps is "9999999999" string *ps = new string; // initialized to empty string int *pi = new int; // pi points to an uninitialized int string *ps = new string(); // initialized to empty string int *pi = new int(); // pi points to an int value-initialized to 0
C++ 提供了 delete 表达式释放指针所指向的地址空间。
delete pi;该命令释放pi指向的int型对象所占用的内存空间。
如果指针指向不是用new分配的内存地址,则在该指针上使用delete是不合法的。C++ 保证:删除 0 值的指针是安全的。删除指针后,该指针变成悬垂指针。悬垂指针指向曾经存放对象的内存,但该对象已经不再存在了。悬垂指针往往导致程序错误,而且很难检测出来。一旦删除了指针所指向的对象,立即将指针置为0,这样就非常清楚地表明指针不再指向任何对象。
下面三种常见的程序错误都与动态内存分配相关:
1. 删除(delete)指向动态分配内存的指针失败,因而无法将该块内存返还给自由存储区。删除动态分配内存失败称为“内存泄漏(memory leak)”。内存泄漏很难发现,一般需等应用程序运行了一段时间后,耗尽了所有内存空间时,内存泄漏才会显露出来。
2. 读写已删除的对象。如果删除指针所指向的对象之后,将指针置为 0 值,则比较容易检测出这类错误。
3. 对同一个内存空间使用两次 delete 表达式。当两个指针指向同一个动态创建的对象,删除时就会发生错误。如果在其中一个指针上做 delete 运算,将该对象的内存空间返还给自由存储区,然后接着 delete 第二个指针,此时则自由存储区可能会被破坏。
7. 类型转换
1. 隐式类型转换
在混合类型的表达式中
用作条件的表达式被转换为 bool 类型
用一表达式初始化某个变量,或将一表达式赋值给某个变量
整型提升:对于所有比 int 小的整型,包括 char、signed char、unsigned char、short 和 unsigned short,如果该类型的所有可能的值都能包容在int内,它们就会被提升为int型,否则,它们将被提升为unsigned int。如果将bool值提升为int,则false转换为0,而true则转换为 1。
对于包含 signed 和 unsigned int 型的表达式,表达式中的 signed 型数值会被转换为 unsigned 型。
指针转换:在使用数组时,大多数情况下数组都会自动转换为指向第一个元素的指针。不将数组转换为指针的例外情况有:数组用作取地址(&)操作符的操作数或 sizeof 操作符的操作数时,或用数组对数组的引用进行初始化时,不会将数组转换为指针。C++ 还提供了另外两种指针转换:指向任意数据类型的指针都可转换为void* 类型;整型数值常量 0 可转换为任意指针类型。
2. 显示转换:static_cast、dynamic_cast、const_cast 和 reinterpret_cast
强制类型转换符号的一般形式如下:cast-name<type>(expression);
dynamic_cast:支持运行时识别指针或引用所指向的对象。
const_cast :将转换掉表达式的 const 性质。
static_cast:编译器隐式执行的任何类型转换都可以由 static_cast 显式完成。
reinterpret_cast:通常为操作数的位模式提供较低层次的重新解释。
五、 语句
1. 常用语句注意点
复合语句:通常被称为块,是用一对花括号括起来的语句序列。块标识了一个作用域,在块中引入的名字只能在该块内部或嵌套在块中的子块里访问。通常,一个名字只从其定义处到该块的结尾这段范围内可见。
switch语句中default后必须要有一个语句,case后必须是整型常量表达式。对于 switch 结构,只能在它的最后一个 case 标号或 default 标号后面定义变量。
While语句:在循环条件中定义的变量在每次循环里都要经历创建和撤销的过程。
可以在 for 语句的 init-statement 中定义多个对象;但是不管怎么样,该处只能出现一个语句,因此所有的对象必须具有相同的一般类型。
与 while 语句不同。do-while 语句总是以分号结束。
int a[]={1,2,3,4,5}; int *source=a; size_t sz=sizeof(a)/sizeof(*a); int *dest=new int[sz]; while(source!=a+sz){ *dest++=*source++; } int *p=dest-1; while(p!=dest-sz-1){ std::cout<<*p<<std::endl; --p; }
goto 语句不能跨越变量的定义语句向前跳转。
2. 异常处理
throw 语句使用一个表达式,该表达式是 runtime_error 类型的对象。runtime_error类型是标准库异常类中的一种,在 stdexcept 头文件中定义。
每一个标准库异常类都定义了名为 what 的成员函数。这个函数不需要参数,返回const char* 类型值,它返回的指针指向一个 C 风格字符串。
如果不存在处理该异常的 catch 子句,程序的运行就要跳转到名为terminate 的标准库函数,该函数在 exception 头文件中定义。这个标准库函数的行为依赖于系统,通常情况下,它的执行将导致程序非正常退出。
标准库异常类定义在四个头文件中:
1. exception 头文件定义了最常见的异常类,它的类名是 exception。这个类只通知异常的产生,但不会提供更多的信息。
2. stdexcept 头文件定义了几种常见的异常类
exception 最常见的问题,只定义了默认构造函数
runtime_error 运行时错误:仅在运行时才能检测到问题
range_error 运行时错误:生成的结果超出了有意义的值域范围
overflow_error 运行时错误:计算上溢
underflow_error 运行时错误:计算下溢
logic_error 逻辑错误:可在运行前检测到问题
domain_error 逻辑错误:参数的结果值不存在
invalid_argument 逻辑错误:不合适的参数
length_error 逻辑错误:试图生成一个超出该类型最大长度的对象
out_of_range 逻辑错误:使用一个超出有效范围的值
3. new 头文件定义了 bad_alloc 异常类型,提供因无法分配内在而由 new抛出的异常,只定义了默认构造函数
4. type_info 头文件定义了 bad_cast 异常类型,只定义了默认构造函数
#define NDEBUG
预处理器还定义了其余四种在调试时非常有用的常量:
__FILE__ 文件名
__LINE__ 当前行号
__TIME__ 文件被编译的时间
__DATE__ 文件被编译的日期
另一个常见的调试技术是使用 NDEBUG 预处理变量以及 assert 预处理宏。assert 宏是在 cassert 头文件中定义的,所有使用 assert 的文件都必须包含这个头文件。预处理宏有点像函数调用。assert 宏需要一个表达式作为它的条件:
assert(expr)
只要 NDEBUG 未定义,assert 宏就求解条件表达式 expr,如果结果为false,assert 输出信息并且终止程序的执行。如果该表达式有一个非零值,则 assert 不做任何操作。