您的位置:首页 > 编程语言 > C语言/C++

C++ 表达式

2015-11-23 21:46 441 查看
《C++ Primer 4th》读书摘要

C++ 提供了丰富的操作符,并定义操作数为内置类型时,这些操作符的含义。除此之外,C++ 还支持操作符重载,允许程序员自定义用于类类型时操作符的含义。标准库正是使用这种功能定义用于库类型的操作符。

操作符的含义——该操作符执行什么操作以及操作结果的类型——取决于操作数的类型。除非已知道操作数的类型,否则无法确定一个特定表达式的含义。

按优先级来对操作符进行分组——一元操作符优先级最高,其次是乘、除操作,接着是二元的加、减法操作。高优先级的操作符要比低优先级的结合得更紧密。这些算术操作符都是左结合,这就意味着当操作符的优先级相同时,这些操作符从左向右依次与操作数结合。

逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。我们常常称这种求值策略为“短路求值(short-circuit evaluation)”。

由于 true 转换为 1,因此要检测某值是否与 bool 字面值true 相等,其等效判断条件通常很难正确编写:如果 val 不是 bool 值,val 和 true 的比较等效于:

if (val == 1) { /* ... */ }

这与下面的条件判断完全不同:

// condition succeeds if val is any nonzero value

if (val) { /* ... */ }

此时,只要 val 为任意非零值,条件判断都得 true。如果显式地书写条件比较,则只有当 val 等于指定的 1 值时,条件才成立。

位操作符使用整型的操作数。位操作符将其整型操作数视为二进制位的集合,为每一位提供检验和设置的功能。另外,这类操作符还可用于bitset 类型的操作数,该类型具有这里所描述的整型操作数的行为。

操作符

功能

用法

~

bitwise NOT(位求反)

~expr

<<

left shift(左移)

expr1 << expr2

>>

right shift(右移)

expr1 >> expr2

&

bitwise AND(位与)

expr1 & expr2

^

bitwise XOR(位异或)

expr1 ^ expr2

|

bitwise OR(位或)

expr1 | expr2

移位操作的右操作数不可以是负数,而且必须是严格小于左操作数位数的值。否则,操作的效果未定义。位异或(互斥或,exclusive or)操作符(^)也需要两个整型操作数。在每个位的位置,如果两个操作数对应的位只有一个(不是两个)为 1,则操作结果中该位为 1,否则为 0。

一般而言,标准库提供的 bitset 操作更直接、更容易阅读和书写、正确使用的可能性更高。而且,bitset 对象的大小不受 unsigned 数的位数限制。通常来说,bitset 优于整型数据的低级直接位操作。

输入输出标准库(IO library)分别重载了位操作符 >> 和 << 用于输入和输出。移位操作符具有中等优先级:其优先级比算术操作符低,但比关系操作符、赋值操作符和条件操作符优先级高。

与其他二元操作符不同,赋值操作具有右结合特性。当表达式含有多个赋值操作符时,从右向左结合。多个赋值操作中,各对象必须具有相同的数据类型,或者具有可转换为同一类型的数据类型。

复合赋值操作符的一般语法格式为:

a op= b;

其中,op= 可以是下列十个操作符之一:

+= -= *= /= %= // arithmetic operators

<<= >>= &= ^= |= // bitwise operators

这两种语法形式存在一个显著的差别:使用复合赋值操作时,左操作数只计算了一次;而使用相似的长表达式时,该操作数则计算了两次,第一次作为右操作数,而第二次则用做左操作数。除非考虑可能的性能价值,在很多(可能是大部分的)上下文环境里这个差别不是本质性的。

自增操作使其操作数加1,操作结果是修改后的值。它的后置形式同样对其操作数加 1(或减 1),但操作后产生操作数原来的、未修改的值作为表达式的结果:

int i = 0, j;

j = ++i; // j = 1, i = 1: prefix yields incremented value

j = i++; // j = 1, i = 2: postfix yields unincremented value

因为前置操作返回加1 后的值,所以返回对象本身,这是左值。而后置操作返回的则是右值。

建议:只有在必要时才使用后置操作符

有使用 C 语言背景的读者可能会觉得奇怪,为什么要在程序中使用前自增操作。道理很简单:因为前置操作需要做的工作更少,只需加 1 后返回加 1 后的结果即可。而后置操作符则必须先保存操作数原来的值,以便返回未加 1 之前的值作为操作的结果。对于 int 型对象和指针,编译器可优化掉这项额外工作。但是对于更多的复杂迭代器类型,这种额外工作可能会花费更大的代价。因此,养成使用前置操作这个好习惯,就不必操心性能差异的问题。

vector<int>::iterator iter = ivec.begin();

// prints 10 9 8 ... 1

while (iter != ivec.end())

cout << *iter++ << endl; // iterator postfix increment

由于后自增操作的优先级高于解引用操作,因此 *iter++ 等效于*(iter++)。子表达式 iter++ 使 iter 加 1,然后返回 iter 原值的副本作为该表达式的结果。因此,解引用操作 * 的操作数是 iter 未加 1 前的副本。

C++ 语言为包含点操作符和解引用操作符的表达式提供了一个同义词:箭头操作符(->)。点操作符用于获取类类型对象的成员

假设有一个指向类类型对象的指针(或迭代器),下面的表达式相互等价:

(*p).foo; // dereference p to get an object and fetch its member named foo

p->foo; // equivalent way to fetch the foo from the object to which p points

sizeof 操作符的作用是返回一个对象或类型名的长度,返回值的类型为size_t,长度的单位是字节。

结合性

操作符

功能

用法

L

::

global scope(全局作用域)

:: name

L

::

class scope(类作用域)

class :: name

L

::

namespace scope(名字空间作用域)

namespace :: name

L

.

member selectors(成员选择)

object . member

L

->

member selectors(成员选择)

pointer -> member

L

[]

subscript(下标)

variable [ expr ]

L

()

function call(函数调用)

name (expr_list)

L

()

type construction(类型构造)

type (expr_list)

R

++

postfix increment(后自增操作)

lvalue++

R

--

postfix decrement(后自减操作)

lvalue--

R

Typeid

type ID(类型 ID)

typeid (type)

R

typeid

run-time type ID(运行时类型 ID)

typeid (expr)

R

explicit cast(显式强制类型转换)

type conversion(类型转换)

cast_name<type>(expr)

R

sizeof

size of object(对象的大小)

sizeof expr

R

sizeof

size of type(类型的大小)

sizeof(type)

R

++

prefix increment(前自增操作)

++ lvalue

R

--

prefix decrement(前自减操作)

-- lvalue

R

~

bitwise NOT(位求反)

~expr

R

!

logical NOT(逻辑非)

!expr

R

unary minus(一元负号)

-expr

R

+

unary plus(一元正号)

+expr

R

*

dereference(解引用)

*expr

R

&

address-of(取地址)

&expr

R

()

type conversion(类型转换)

(type) expr

R

new

allocate object(创建对象)

new type

R

delete

deallocate object(释放对象)

delete expr

R

delete[]

deallocate array(释放数组)

delete[] expr

L

->*

ptr to member select(指向成员操作的指针)

ptr ->* ptr_to_member

L

.*

ptr to member select(指向成员操作的指针)

obj .*ptr_to_member

L

*

multiply(乘法)

expr * expr

L

/

divide(除法)

expr / expr

L

%

modulo (remainder)(求模(求余))

expr % expr

L

+

add(加法)

expr + expr

L

subtract(减法)

expr - expr

L

<<

bitwise shift left(位左移)

expr << expr

L

>>

bitwise shift right(位右移)

expr >> expr

L

<

less than(小于)

expr < expr

L

<=

less than or equal(小于或等于)

expr <= expr

L

>

greater than(大于)

expr > expr

L

>=

greater than or equal(大于或等于)

expr >= expr

L

==

equality(相等)

expr == expr

L

!=

inequality(不等)

expr != expr

L

&

bitwise AND(位与)

expr & expr

L

^

bitwise XOR()

expr ^ expr

L

|

bitwise OR(位异或)

expr | expr

L

&&

logical AND(逻辑与)

expr && expr

L

||

logical OR(逻辑或)

expr || expr

R

?:

conditional(条件操作)

expr ? expr : expr

R

=

assignment(赋值操作)

lvalue = expr

R

*=, /=, %=,

+=, -=,

<<=, >>=,

&=,|=, ^=

compound assign(复合赋值操作)

lvalue += expr, etc.

R

throw

throw exception(抛出异常)

throw expr

L

,

comma(逗号) expr ,

expr

以什么次序求解操作数通常没有多大关系。只有当操作符的两个操作数涉及到同一个对象,并改变其值时,操作数的

计算次序才会影响结果。如果一个子表达式修改了另一个子表达式的操作数,则操作数的求解次序就变得相当重要:

// oops! language does not define order of evaluation

if (ia[index++] < ia[index])

此表达式的行为没有明确定义。问题在于:< 操作符的左右操作数都使用了index 变量,但是,左操作数更改了该变量的值。假设 index 初值为 0,编译器可以用下面两种方式之一求该表达式的值:

if (ia[0] < ia[0]) // execution if rhs is evaluated first

if (ia[0] < ia[1]) // execution if lhs is evaluated first

一个表达式里,不要在两个或更多的子表达式中对同一对象做自增或自减操作。

下面两个指导原则有助于处理复合表达式:

1. 如果有怀疑,则在表达式上按程序逻辑要求使用圆括号强制操作数的组合。

2. 如果要修改操作数的值,则不要在同一个语句的其他地方使用该操作数。如果必须使用改变的值,则把该表达式分割成两个独立语句:在一个语句中改变该操作数的值,再在下一个语句使用它。

第二个规则有一个重要的例外:如果一个子表达式修改操作数的值,然后将该子表达式的结果用于另一个子表达式,这样则是安全的

而动态创建对象时,只需指定其数据类型,而不必为该对象命名。取而代之的是,new 表达式返回指向新创建对象的指针,我们通过该指针来访问此对象:

int i; // named, uninitialized int variable

int *pi = new int; // pi points to dynamically allocated,unnamed, uninitialized int

如果不提供显式初始化,动态创建的对象与在函数内定义的变量初始化方式相同。对于类类型的对象,用该类的默认构造函数初始化;而内置类型的对象则无初始化。

string *ps = new string; // initialized to empty string

int *pi = new int; // pi points to an uninitialized int

正如我们(几乎)总是要初始化定义为变量的对象一样,在动态创建对象时,(几乎)总是对它做初始化也是一个好办法。

int *pi = new int; // pi points to an uninitialized int

int *pi = new int(); // pi points to an int value-initialized to 0

动态创建的对象用完后,程序员必须显式地将该对象占用的内存返回给自由存储区。C++ 提供了 delete 表达式释放指针所指向的地址空间。

delete pi;

如果指针指向不是用 new 分配的内存地址,则在该指针上使用delete 是不合法的。

删除指针后,该指针变成悬垂指针。悬垂指针指向曾经存放对象的内存,但该对象已经不再存在了。悬垂指针往往导致程序错误,而且很难检测出来。

一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清楚地表明指针不再指向任何对象。

C++ 允许动态创建 const 对象:

// allocate and initialize a const object

const int *pci = new const int(1024);

与其他常量一样,动态创建的 const 对象必须在创建时初始化,并且一经初始化,其值就不能再修改

警告:动态内存的管理容易出错

下面三种常见的程序错误都与动态内存分配相关:

1. 删除( delete )指向动态分配内存的指针失败,因而无法将该块内存返还给自由存储区。删除动态分配内存失败称为“内存泄漏(memory leak)”。内存泄漏很难发现,一般需等应用程序运行了一段时间后,耗尽了所有内存空间时,内存泄漏才会显露出来。

2. 读写已删除的对象。如果删除指针所指向的对象之后,将指针置为 0 值,则比较容易检测出这类错误。

3. 对同一个内存空间使用两次 delete 表达式。当两个指针指向同一个动态创建的对象,删除时就会发生错误。如果在其中一个指针上做 delete 运算,将该对象的内存空间返还给自由存储区,然后接着 delete 第二个指针,此时则自由存储区可能会被破坏。

尽管程序员不能改变 const 对象的值,但可撤销对象本身。如同其他动态对象一样, const 动态对象也是使用删除指针来释放的:

delete pci; // ok: deletes a const object

如果两个类型之间可以相互转换,则称这两个类型相关。

包含 short 和 int 类型的表达式, short 类型的值转换为 int 。如果int 型足够表示所有 unsigned short 型的值,则将 unsigned short 转换为int,否则,将两个操作数均转换为 unsigned int 。

指向任意数据类型的指针都可转换为void* 类型;整型数值常量 0 可转换为任意指针类型。

算术值和指针值都可以转换为 bool 类型。如果指针或算术值为 0,则其bool 值为 false ,而其他值则为 true

当使用非 const 对象初始化 const 对象的引用时,系统将非 const 对象转换为 const 对象。此外,还可以将非 const 对象的地址(或非 const 指针)转换为指向相关 const 类型的指针:

int i;

int ci = 0;

const int &j = i; // ok: convert non-const to reference to const int

const int *p = &ci; // ok: convert address of non-const to address of a const

显式转换也称为强制类型转换(cast),包括以下列名字命名的强制类型转换操作符:static_cast、dynamic_cast、const_cast 和 reinterpret_cast。

虽然有时候确实需要强制类型转换,但是它们本质上是非常危险的。显式使用强制类型转换的另一个原因是:可能存在多种转换时,需要选择一种特定的类型转换。

命名的强制类型转换符号的一般形式如下:

cast-name<type>(expression);

其中 cast-name 为 static_cast、dynamic_cast、const_cast 和reinterpret_cast 之一,type 为转换的目标类型,而 expression 则是被强制转换的值。强制转换的类型指定了在 expression 上执行某种特定类型的转换。

const_cast ,顾名思义,将转换掉表达式的 const 性质.只有使用 const_cast 才能将 const 性质转换掉。在这种情况下,试图使用其他三种形式的强制转换都会导致编译时的错误。类似地,除了添加或删除const 特性,用 const_cast 符来执行其他任何类型转换,都会引起编译错误。

const char *pc_str;

char *pc = string_copy(const_cast<char*>(pc_str));

编译器隐式执行的任何类型转换都可以由 static_cast 显式完成:

double d = 97.0;

// cast specified to indicate that the conversion is intentional

char ch = static_cast<char>(d);

如果编译器不提供自动转换,使用 static_cast 来执行类型转换也是很有用的。例如,下面的程序使用 static_cast 找回存放在 void* 指针中的值:

void* p = &d; // ok: address of any data object can be stored in avoid*

// ok: converts void* back to the original pointer type

double *dp = static_cast<double*>(p);

可通过 static_cast 将存放在 void* 中的指针值强制转换为原来的指针类型,此时我们应确保保持指针值。

建议:避免使用强制类型转换

强制类型转换关闭或挂起了正常的类型检查(第 2.3 节)。强烈建议程序员避免使用强制类型转换,不依赖强制类型转换也能写出很好的 C++程序。

这个建议在如何看待 reinterpret_cast 的使用时非常重要。此类强制转换总是非常危险的。相似地,使用 const_cast 也总是预示着设计缺陷。设计合理的系统应不需要使用强制转换抛弃 const 特性。其他的强制转换,如 static_cast 和 dynamic_cast,各有各的用途,但都不应频繁使用。每次使用强制转换前,程序员应该仔细考虑是否还有其他不

同的方法可以达到同一目的。如果非强制转换不可,则应限制强制转换值的作用域,并且记录所有假定涉及的类型,这样能减少错误发生的机会。

旧式强制类型转换

在引入命名的强制类型转换操作符之前,显式强制转换用圆括号将类型括起来实现:

char *pc = (char*) ip;

虽然标准 C++ 仍然支持旧式强制转换符号,但是我们建议,只有在 C 语言或标准 C++ 之前的编译器上编写代码时,才使用这种语法。

旧式强制转换符号有下列两种形式:

type (expr); // Function-style cast notation

(type) expr; // C-language-style cast notation
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: