C++学习笔记4--表达式
概述:
1.表达式由一个或多个操作数通过操作符组合而成。最简单的表达式仅包含一个字面值常量或变量。较复杂的表达式则由操作符以及一个或多个操作数构成。每个表达式都会产生一个结果。如果表达式中没有操作符,则其结果就是操作数本身(例如,字面值常量或变量)的值。当一个对象用在需要使用其值的地方,则计算该对象的值。
2.操作符的含义——该操作符执行什么操作以及操作结果的类型——取决于操作数的类型。除非已知道操作数的类型,否则无法确定一个特定表达式的含义。
3.操作符对其操作数的类型有要求,如果操作符应用于内置或复合类型的操作数,则由C++语言定义其类型要求。
4.对于操作数为内置或复合类型的二元操作符,通常要求它的两个操作数具有相同的数据类型,或者其类型可以转换为同一种数据类型。
5.要理解由多个操作符组成的表达式,必须先理解操作符的优先级、结合性和操作数的求值顺序。每一个操作符都有自己的优先级别和结合性。优先级规定复合表达式中操作符结合的方式,而结合性则决定同一个优先级的操作符如何结合。
大多数操作符没有规定其操作数的求值顺序:由编译器自由选择先计算左操作数还是右操作数。通常,操作数的求值顺序不会影响表达式的结果。但是,如果操作符的两个操作数都与同一个对象相关,而且其中一个操作数改变了该对象的值,则程序将会因此而产生严重的错误——而且这类错误很难发现。
一、算术运算符:
A.算术操作符 +、-、* 和 / 具有直观的含义:加法、减法、乘法和除法。对两个整数做除法,结果仍为整数,如果它的商包含小数部分,则小数部分会被截除.
B.操作符 % 称为“求余(remainder)”或“求模(modulus)”操作符,用于计算左操作数除以右操作数的余数。该操作符的操作数只能为整型,包括 bool、char、short 、int 和 long 类型,以及对应的 unsigned 类型.
C.操作符 %或操作符/ ,如果只有一个操作数为负数,这两种操作的结果取决于机器;求模结果的符号也取决于机器,而除法操作的值则是负数(或零)。
二、关系操作符和逻辑操作符:
A.逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。我们常常称这种求值策略为“短路求值(short-circuit evaluation)”。
对于逻辑与操作符,一个很有价值的用法是:如果某边界条件使 expr2 的计算变得危险,则应在该条件出现之前,先让 expr1 的计算结果为 false。例如,编写程序使用一个 string 类型的对象存储一个句子,然后将该句子的第一个单词的各字符全部变成大写,可如下实现:
string s("Expressions in C++ are composed..."); string::iterator it = s.begin(); // convert first word in s to uppercase while (it != s.end() && !isspace(*it)) { *it = toupper(*it); // toupper covered in section 3.2.4 (p. 88) ++it; }B.bool 类型可转换为任何算术类型——bool 值 false 用 0 表示,而 true 则为 1。if (val == true) { /* ... */ }val 本身是 bool 类型,或者 val 具有可转换为 bool 类型的数据类型。如果 val 是 bool 类型,则该判断条件等效于:
if (val) { /* ... */ }更重要的是,如果 val 不是 bool 值,val 和 true 的比较等效于:
if (val == 1) { /* ... */ }
三、位操作符A.位操作符使用整型的操作数。位操作符将其整型操作数视为二进制位的集合,为每一位提供检验和设置的功能。B.位操作符操纵的整数的类型可以是有符号的也可以是无符号的。如果操作数为负数,则位操作符如何处理其操作数的符号位依赖于机器。于是它们的应用可能不同:在一个应用环境中实现的程序可能无法用于另一应用环境。对于位操作符,由于系统不能确保如何处理其操作数的符号位,所以强烈建议使用unsigned整型操作数。
C.<< 和 >> 操作符提供移位操作,其右操作数标志要移动的位数。这两种操作符将其左操作数的各个位向左(<<)或向右(>>)移动若干个位(移动的位数由其右操作数指定),从而产生新的值,并丢弃移出去的位。
左移操作符(<<)在右边插入 0 以补充空位。对于右移操作符(>>),如果其操作数是无符号数,则从左边开始插入 0;如果操作数是有符号数,则插入符号位的副本或者 0 值,如何选择需依据具体的实现而定。移位操作的右操作数不可以是负数,而且必须是严格小于左操作数位数的值。否则,操作的效果未定义。
D.位与操作(&)需要两个整型操作数,在每个位的位置,如果两个操作数对应的位都为 1,则操作结果中该位为 1,否则为 0。
位异或(互斥或,exclusive or)操作符(^)也需要两个整型操作数。在每个位的位置,如果两个操作数对应的位只有一个(不是两个)为 1,则操作结果中该位为 1,否则为 0。
位或(包含或,inclusive or)操作符(|)需要两个整型操作数。在每个位的位置,如果两个操作数对应的位有一个或者两个都为 1,则操作结果中该位为 1,否则为 0。
E.移位操作符具有中等优先级:其优先级比算术操作符低,但比关系操作符、赋值操作符和条件操作符优先级高。若 IO 表达式的操作数包含了比IO操作符优先级低的操作符,相关的优先级别将影响书写该表达式的方式。通常需使用圆括号强制先实现右结合:
cout << 42 + 10; // ok, + has higher precedence, so the sum is printed cout << (10 < 42); // ok: parentheses force intended grouping; prints 1 cout << 10 < 42; // error: attempt to compare cout to 42!
四、赋值运算符:赋值操作的右结合性,只要被赋值的每个操作数都具有相同的通用类型,C++语言允许将这多个赋值操作写在一个表达式中。
五、条件操作符:
cond ? expr1 : expr2;
其中,cond 是一个条件判断表达式,条件操作符首先计算 cond 的值,如果 cond 的值为 0,则条件为 false;如果 cond 非 0,则条件为 true。无论如何,cond 总是要被计算的。然后,条件为 true 时计算 expr1 ,否则计算 expr2 。和逻辑与、逻辑或(&& 和 ||)操作符一样,条件操作符保证了上述操作数的求解次序。expr1 和 expr2 中只有一个表达式被计算。
六、sizeof 操作符:
A、sizeof 操作符的作用是返回一个对象或类型名的长度,返回值的类型为 size_t,长度的单位是字节,size_t表达式的结果是编译时常量,该操作符有以下三种语法形式:
sizeof (type name); sizeof (expr); sizeof expr;B.将 sizeof 应用在表达式 expr 上,将获得该表达式的结果的类型长度.C.将 sizeof 用于 expr 时,并没有计算表达式 expr 的值。特别是在 sizeof *p 中,指针 p 可以持有一个无效地址,因为不需要对 p 做解引用操作。D.使用 sizeof 的结果部分地依赖所涉及的类型:
1.对 char 类型或值为 char 类型的表达式做 sizeof 操作保证得 1。
2.对引用类型做 sizeof 操作将返回存放此引用类型对象所需的内在空间大小。
string p; string &pr=p;cout<<sizeof pr; //将输出string类型的大小3.对指针做 sizeof 操作将返回存放指针所需的内在大小;注意,如果要获取该指针所指向对象的大小,则必须对指针进行解引用。4.对数组做 sizeof 操作等效于将对其元素类型做 sizeof 操作的结果乘上数组元素的个数。
E.逗号表达式是一组由逗号分隔的表达式,这些表达式从左向右计算。逗号表达式的结果是其最右边表达式的值。如果最右边的操作数是左值,则逗号表达式的值也是左值。
七、求值顺序:C++中,规定了操作数计算顺序的操作符还有条件(?:)和逗号操作符。除此之外,其他操作符并未指定其操作数的求值顺序。
如果一个子表达式修改了另一个子表达式的操作数,则操作数的求解次序就变得相当重要:
// oops! language does not define order of evaluation if (ia[index++] < ia[index])然而,C++ 语言不能确保从左到右的计算次序。事实上,这类表达式的行为没有明确定义。
一个表达式里,不要在两个或更多的子表达式中对同一对象做自增或自减操作。
八、new 和 delete 表达式
A、动态创建对象:定义变量时,必须指定其数据类型和名字。而动态创建对象时,只需指定其数据类型,而不必为该对象命名。取而代之的是,new 表达式返回指向新创建对象的指针,我们通过该指针来访问此对象:
int i; // named, uninitialized int variable int *pi = new int; // pi points to dynamically allocated, // unnamed, uninitialized int这个 new 表达式在自由存储区中分配创建了一个整型对象,并返回此对象的地址,并用该地址初始化指针 pi。
B、动态创建对象的初始化:动态创建的对象可用初始化变量的方式实现初始化:
int i(1024); // value of i is 1024 int *pi = new int(1024); // object to which pi points is 1024 string s(10, '9'); // value of s is "9999999999" string *ps = new string(10, '9'); // *ps is "9999999999"
C、动态创建对象的默认初始化:如果不提供显式初始化,动态创建的对象与在函数内定义的变量初始化方式相同。对于类类型的对象,用该类的默认构造函数初始化;而内置类型的对象则无初始化。
注意new后面的类型没有括号
string *ps = new string; // initialized to empty string int *pi = new int; // pi points to an uninitialized intD.通过在类型名后面使用一对内容为空的圆括号对动态创建的对象做值初始化。内容为空的圆括号表示虽然要做初始化,但实际上并未提供特定的初值。对于提供了默认构造函数的类类型(例如 string),没有必要对其对象进行值初始化:无论程序是明确地不初始化还是要求进行值初始化,都会自动调用其默认构造函数初始化该对象。而对于内置类型或没有定义默认构造函数的类型,采用不同初始化方式则有显著的差别:
int *pi = new int; // pi points to an uninitialized int int *pi = new int(); // pi points to an int value-initialized to 0E.撤销动态创建的对象:动态创建的对象用完后,程序员必须显式地将该对象占用的内存返回给自由存储区。C++ 提供了 delete 表达式释放指针所指向的地址空间。delete pi;如果指针指向不是用 new 分配的内存地址,则在该指针上使用 delete 是不合法的。
F.零值指针的删除:如果指针的值为 0,则在其上做 delete 操作是合法的,但这样做没有任何意义.C++ 保证:删除 0 值的指针是安全的。
G.在 delete 之后,重设指针的值:删除指针后,该指针变成悬垂指针。悬垂指针指向曾经存放对象的内存,但该对象已经不再存在了。悬垂指针往往导致程序错误,而且很难检测出来。一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清楚地表明指针不再指向任何对象。
H.下面三种常见的程序错误都与动态内存分配相关:
删除( delete )指向动态分配内存的指针失败,因而无法将该块内存返还给自由存储区。删除动态分配内存失败称为“内存泄漏(memory leak)”。内存泄漏很难发现,一般需等应用程序运行了一段时间后,耗尽了所有内存空间时,内存泄漏才会显露出来。
读写已删除的对象。如果删除指针所指向的对象之后,将指针置为 0 值,则比较容易检测出这类错误。
对同一个内存空间使用两次 delete 表达式。当两个指针指向同一个动态创建的对象,删除时就会发生错误。如果在其中一个指针上做 delete 运算,将该对象的内存空间返还给自由存储区,然后接着 delete 第二个指针,此时则自由存储区可能会被破坏
九、类型转换
如果两个类型之间可以相互转换,则称这两个类型相关。
A.何时发生隐式类型转换:
编译器在必要时将类型转换规则应用到内置类型和类类型的对象上。在下列情况下,将发生隐式类型转换:
1.在混合类型的表达式中,其操作数被转换为相同的类型:
int ival; double dval; ival >= dval // ival converted to double2.用作条件的表达式被转换为 bool 类型:
int ival; if (ival) // ival converted to bool while (cin) // cin converted to bool条件操作符(?:)中的第一个操作数以及逻辑非(!)、逻辑与(&&)和逻辑或(||)的操作数都是条件表达式。出现在 if、while、for 和 do while 语句中的同样也是条件表达式。
3.用一表达式初始化某个变量,或将一表达式赋值给某个变量,则该表达式被转换为该变量的类型:
int ival = 3.14; // 3.14 converted to int int *ip; ip = 0; // the int 0 converted to a null pointer of type int *B.算术转换:算术转换保证在执行操作之前,将二元操作符(如算术或逻辑操作符)的两个操作数转换为同一类型,并使表达式的值也具有相同的类型。
1.算术转换规则定义了一个类型转换层次,该层次规定了操作数应按什么次序转换为表达式中最宽的类型。在包含多种类型的表达式中,转换规则要确保计算值的精度。
例如,如果一个操作数的类型是 long double,则无论另一个操作数是什么类型,都将被转换为 long double。3.有符号与无符号类型之间的转换:若表达式中使用了无符号( unsigned )数值,所定义的转换规则需保护操作数的精度。unsigned 操作数的转换依赖于机器中整型的相对大小,因此,这类转换本质上依赖于机器。
2.最简单的转换为整型提升:对于所有比 int 小的整型,包括 char、signed char、unsigned char、short 和 unsigned short,如果该类型的所有可能的值都能包容在 int 内,它们就会被提升为 int 型,否则,它们将被提升为 unsigned int。如果将 bool 值提升为 int ,则 false 转换为 0,而 true 则转换为 1。long 和 unsigned int 的转换也是一样的。只要机器上的 long 型足够表示 unsigned int 型的所有值,就将 unsigned int 转换为 long 型,否则,将两个操作数均转换为 unsigned long 。
对于包含 signed 和 unsigned int 型的表达式,其转换可能出乎我们的意料。表达式中的 signed 型数值会被转换为 unsigned 型。例如,比较 int 型和 unsigned int 型的简单变量,系统首先将 int 型数值转换为 unsigned int 型,如果 int 型的值恰好为负数,其结果将以第 2.1.1 节介绍的方法转换,并带来该节描述的所有副作用。
C.指针转换:在使用数组时,大多数情况下数组都会自动转换为指向第一个元素的指针:
int ia[10]; // array of 10 ints int* ip = ia; // convert ia to pointer to first element 不将数组转换为指针的例外情况有:数组用作取地址(&)操作符的操作数或 sizeof 操作符的操作数时,或用数组对数组的引用进行初始化时,不会将数组转换为指针。C++ 还提供了另外两种指针转换:指向任意数据类型的指针都可转换为 void* 类型;整型数值常量 0 可转换为任意指针类型。D.转换为 bool 类型:算术值和指针值都可以转换为 bool 类型。如果指针或算术值为 0,则其 bool 值为 false ,而其他值则为 true.E.转换与枚举类型:C++ 自动将枚举类型的对象或枚举成员( enumerator )转换为整型,其转换结果可用于任何要求使用整数值的地方。将 enum 对象或枚举成员提升为什么类型由机器定义,并且依赖于枚举成员的最大值。无论其最大值是什么, enum 对象或枚举成员至少提升为 int 型。如果 int 型无法表示枚举成员的最大值,则提升到能表示所有枚举成员值的、大于 int 型的最小类型( unsigned int、long 或 unsigned long)。F.由标准库类型定义的转换:从 istream 中读取数据,并将此表达式作为 while 循环条件:string s; while (cin >> s) 这里隐式使用了 IO 标准库定义的类型转换。G.强制类型转换:因为要覆盖通常的标准转换,所以需显式使用强制类型转换。显式使用强制类型转换的另一个原因是:可能存在多种转换时,需要选择一种特定的类型转换。1、static_cast不进行错误检查,可以进行的转换有:数字类型、继承层次中的指针或引用、声明为explicit的单形参构造函数。只要用来进行替换编译器进行的隐式转换。
2、dynamic_cast进行错误检查,可以进行的转换:继承层次中的指针或者引用。
3、const_cast用来添加或者去掉一个变量的const或者volatile属性,这个变量必须是指针或引用类型。
4、旧时转型(C风格转型)有两种形式:(T) expression 或 T (expression)。两种形式并无差别,纯粹只是小伙考的位置不同。如:
class Widget { public: explicit Widget(int size); }; void doSomeWork (const Widget& w); doSomeWork( Widget(15) ); //旧风格 doSomeWork( static_cast(15) ); //新风格 5、const_cast 通常被用来将对象的常量性移除。它也是唯一有此能力的C++ style 转型操作符。static_cast 用来强迫隐式转换,例如将 non_const 对象转换为cosnt 对象(就像条款3所为),但它无法将const转换为非const。
6、static_cast 可以将一个derived class对象转换为base class对象。但static_cast返回一个derived class对象的base class部分的一个副本:
class Window { pubilc: virtual void onResize(){...} ... }; class SpecialWindow: public Window { public: virtual void onResize() { static_cast(*this).onResize(); //Error, 应该为Window::onResize(); 注意优先级相同、左结合 //这里进行SpecialWindow的专属行为 } ... };7、static_cast 可以将存放在 void* 中的指针值强制转换为原来的指针类型,此时我们应确保保持指针值。也就是说,强制转换的结果应与原来的地址值相等。
错误的行,实际上对*this并无任何改变。改正后可以正确对*this调用函数,产生改变。(Effective C++ 条款27)
8、如果可能,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。