C++ 表达式
《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
C++ 表达式的更多相关文章
- 【.net 深呼吸】细说CodeDom(2):表达式、语句
在上一篇文章中,老周厚着脸皮给大伙介绍了代码文档的基本结构,以及一些代码对象与CodeDom类型的对应关系. 在评论中老周看到有朋友提到了 Emit,那老周就顺便提一下.严格上说,Emit并不是针对代 ...
- 你知道C#中的Lambda表达式的演化过程吗?
那得从很久很久以前说起了,记得那个时候... 懵懂的记得从前有个叫委托的东西是那么的高深难懂. 委托的使用 例一: 什么是委托? 个人理解:用来传递方法的类型.(用来传递数字的类型有int.float ...
- 再讲IQueryable<T>,揭开表达式树的神秘面纱
接上篇<先说IEnumerable,我们每天用的foreach你真的懂它吗?> 最近园子里定制自己的orm那是一个风生水起,感觉不整个自己的orm都不好意思继续混博客园了(开个玩笑).那么 ...
- Linq表达式、Lambda表达式你更喜欢哪个?
什么是Linq表达式?什么是Lambda表达式? 如图: 由此可见Linq表达式和Lambda表达式并没有什么可比性. 那与Lambda表达式相关的整条语句称作什么呢?在微软并没有给出官方的命名,在& ...
- 背后的故事之 - 快乐的Lambda表达式(一)
快乐的Lambda表达式(二) 自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜.它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能 ...
- Kotlin的Lambda表达式以及它们怎样简化Android开发(KAD 07)
作者:Antonio Leiva 时间:Jan 5, 2017 原文链接:https://antonioleiva.com/lambdas-kotlin/ 由于Lambda表达式允许更简单的方式建模式 ...
- SQL Server-表表达式基础回顾(二十四)
前言 从这一节开始我们开始进入表表达式章节的学习,Microsoft SQL Server支持4种类型的表表达式:派生表.公用表表达式(CTE).视图.内嵌表值函数(TVF).简短的内容,深入的理解, ...
- 立即执行函数表达式(IIFE)
原文地址:benalman.com/news/2010/11/immediately-invoked-function-expression/ 译者:nzbin 也许你还没有注意到,我是一个对术语比较 ...
- javascript:逆波兰式表示法计算表达式结果
逆波兰式表示法,是由栈做基础的表达式,举个例子: 5 1 2 + 4 * + 3 - 等价于 5 + ((1 + 2) * 4) - 3 原理:依次将5 1 2 压入栈中, 这时遇到了运算符 + ...
- C++11特性——变量部分(using类型别名、constexpr常量表达式、auto类型推断、nullptr空指针等)
#include <iostream> using namespace std; int main() { using cullptr = const unsigned long long ...
随机推荐
- Modbus协议
总体上来说,发送命令格式如下:模块号(1字节)功能码 起始地址(2字节) 数据(X字节) CRC(2位) 模块号(1字节)功能码(要读取的寄存器,读 01,设置05,Coil Status/Inp ...
- nginx伪静态
nginx 伪静态设置目录 /usr/local/nginx/conf/nginx/conf 1.域名绑定.server_name bbs.99vps.com; 2.网站根目录设置.root /ho ...
- tomcat各种问题汇总
1. 让Tomcat支持中文路径名和中文文件名 因为内置get协议中的URL编码都是ISO-8859-1,所以需要我们强制编码,在tomcat/conf/Server.xml中添加URIEncodin ...
- libcurl多线程超时设置不安全(转)
from http://www.cnblogs.com/kex1n/p/4135263.html (1), 超时(timeout) libcurl 是 一个很不错的库,支持http,ftp等很多的协议 ...
- html-5 --html5教程article、footer、header、nav、section使用
header header元素是一种具有引导和导航作用的辅助元素.通常,header元素可以包含一个区块的标题(如h1至h6,或者hgroup元素标签),但也可以包含其他内容,例如数据表格.搜索表单或 ...
- Selenium2+python自动化9-CSS定位语法
前言 大部分人在使用selenium定位元素时,用的是xpath定位,因为xpath基本能解决定位的需求.css定位往往被忽略掉了,其实css定位也有它的价值,css定位更快,语法更简洁.这一篇css ...
- 关于Android中混淆的问题
1.签名打包后库依赖报错,提示找不到依赖库的方法. 原因:混淆,依赖库的方法被混淆了. 解决方法:过滤混淆,即不要混淆这依赖库的文件. -keep class de.greenrobot.event. ...
- iconv命令 gbk 转 UTF-8
-----linux gbk 转 UTF-8-------- iconv 用法 iconv -f "gbk" -t "utf-8" < infile &g ...
- ArcGIS删除部分数据后全图范围不正确
我有一个全国地图的图层,现在删除图层中其他省份,只保留山东省的图形,但是点击全图后,全图范围仍然是全国地图时候的全图范围,使用的版本是ArcGIS9.3,数据存放在9.3的个人数据库中(Perso ...
- Tableview的更新和删除某一行
.tableView的刷新 > 数据刷新的总体步骤 * 修改模型数据 * 刷新表格(刷新界面) > 刷新表格(刷新界面)的方法 * 全局刷新(每一行都会重新刷新) - (void)relo ...