《C++11Primer》阅读随记 -- 四、表达式;五、语句

第四章 表达式

左值和右值

在 C++ 语言中,一个左值表达式的求值结果是一个对象或者一个函数,然而通常以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。

简单的归纳:当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)

当一个左值被当成右值使用时,实际上使用的是它的内容(值)。到目前为止,已经有几种我们熟悉的运算符是要用到左值的

  • 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值
  • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值
  • 内置解引用运算符、下标运算符、迭代器解引用运算符、stringvector 的下标运算符的求值结果都是左值
  • 内置类型和迭代器的底层递减运算符作用域左值运算对象,其强制版本所得到的结果也是左值

使用关键字 decltype 的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype 作用于该表达式( 不是变量 ) 得到一个引用类型。举个例子,假定 p 的类型是 int*, 因为解引用运算符生成左值,所以 decltype(*p) 的结果是 int&。另一方面,因为取地址运算符生成右值,所以 decltype(&p)的结果是 int**,也就是说,结果是一个指向整型指针的指针。

赋值运算符

赋值运算符的左侧运算对象必须是一个可修改的左值。如果给定

int i = 0, j = 0, k = 0;		// 初始化而非赋值
const int ci = i;				// 初始化而非赋值

则下面的复制语句都是非法的

1024 = k;		// 错误:字面值是右值
i + j = k;		// 错误:算术表达式是右值
ci = k;			// 错误:ci 是常量(不可修改)左值

C++11 新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象

k = {3.14};			// 错误:窄化转换
vector<int> vi;		// 初始为空
vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且改值即使转换的话其所占空间也不应该大于目标类型的空间

p39: 对于内置类型的变量,列表初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器报错:
long double ld = 3.1415926536;
int a[ld], b = {ld}; // 错误:转换未执行,因为存在丢失信息的风险
int c(ld), d = ld; // 正确:转换执行,且确实丢失了不分枝

在一条语句中混用解引用和递增运算符

auto pbeg = v.begin();
while( pbeg != v.end() && *pbeg >= 0 )
	cout << *pbeg++ << endl; // 输出当前值并将 pbeg 向前移动一个元素

后置递增运算符的优先级高于解引用运算符,因此 *pbeg++ 等价于 *(pbeg++)

成员访问运算符

点运算符和箭头运算符都可用于访问成员,其中,点运算符获取类对象的一个成员;箭头运算符与点运算符有关,表达式 ptr->mem 等价于 (*ptr).mem

string s1 = "a string", *p = &s1;
auto n = s1.size();			// 运行 string 对象 s1 的 size 成员
n = (*p).size();			// 运行 p 所指对象的 size 成员
n = p->size();				// 等价于上一行

位运算符

位与、位或、位异或运算符
与(&)、或(|)、异或(^)运算符在两个运算对象上逐位执行相应的逻辑操作:

unsigned char b1 = 0145;			0 1 1 0 0 1 0 1
unsigned char b2 = 0257;			1 0 1 0 1 1 1 1
b1 & b2	 /* 24 个高阶位都是 0 */	    0 0 1 0 0 1 0 1
b1 | b2  /* 24 个高阶位都是 0 */      1 1 1 0 1 1 1 1
b1 ^ b2  /* 24 个高阶位都是 0 */      1 1 0 0 1 0 1 0

对于 位与运算符( & ) 来说,如果两个运算对象的对应位置都是 1 则运算结果中该位为 1,否则为 0.对于**位或运算符( | )来说,如果两个运算对象的对应为止至少有一个为 1 则运算结果中该位为 1.否则为 0。对于位异或运算符( ^ )**来说,如果两个运算对象的对应为止有且只有一个为 1 则运算结果中该位为 1,否则为 0。

类型转换

在 C++ 语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型得运算对象时,可以用另一种关联类型得对象或值来替代。换句话说,如果两种类型可以互相转换( conversion ),那么它们就是关联得

int ival = 3.514 + 3; // 编译器可能会井盖该运算损失了精度, 但这句代码得目的是将 ival 初始化为 6
上述类型转换是自动执行得,无须程序员的介入,甚至不需要程序员了解,因此被称作隐式转换( implicit conversion )

算数类型之间的隐式转换被设计得京可能避免损失精度,所以上面的类型 3 转换成 double 类型。
但接下来。在初始化过程中,因为被初始化的对象的类型无法改变,所以初始值被转换成该对象的类型。用上例说明,加法运算得到的 double 类型的结果又转换成了 int 类型的值,这个值,这个值被用来初始化 ival。由 doubleint 转换时忽略小数部分。

何时发生隐式类型转换

  • 在大多数表达式中,比 int 类型小的整数值首先提升为较大的整数类型
  • 在条件中,非布尔值转换陈布尔类型
  • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右值运算对象转换成左侧运算对象的类型
  • 如果算数运算或关系运算的运算对象有多种类型,需要转换成同一种类型
  • 函数调用时也会发生类型转换

其他隐式类型转换

数组转换成指针 在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针。当数组被用作 decltype 关键字的参数,或者作为取地支付(&)、sizeof 以及 typeid 等运算符的运算对象时,上述转换不会发生。同样,如果用一个引用来初始化数组,上述转换也不会发生。

指针的转换 C++ 规定了几种其他的指针转换方式:包括常量整数值 0 或者字面值 nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成 void*;指向任意对象的指针能转换成 const void*

转换成常量 允许将指向非常量类型的指针阻焊换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果 T 是一种类型,我们就能将指向 T 的指针或引用分别转换成指向 const T 的指针或引用。相反的转换并不存在,因为它试图删除掉底层 const

int i;			
const int&j = i;			// 非常量转换成 const int 的引用
const int* p = &i;			// 非常量的地址转换成 const 的地址
int& r = j, *q = p;			// 错误:不允许 const 转换成非常量

转换成布尔类型 存在一种从概算书类型或指针类型向布尔类型自动转换的机制。如果指针或算数类型为 0,转换结果是 false,否则 true

char* cp = get_string();
if(cp) /* ... */			// 如果指针 cp 不是 0,条件为真
while(*cp) /* ... */		// 如果 *cp 不是空字符,条件为真

类类型定义的转换 类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
之前的例子:

  1. 需要在标准库 string 类型的地方使用 C 风格字符转
  2. 在条件部分读入 istrem
string s, t = "a value";		// 字符串字面值转换成 String 类型
while(cin >> s)					// while 的条件部分把 cin 转换成布尔值

条件 (cin >> s) 读入 cin 的内容并将 cin 作为其求值结果。条件部分本来需要一个布尔类型的值,但这里实际检查的是 istream 类型的值。而 IO 库定义了从 istream 向布尔值类型转换的规则,根据这一规则,cin 自动转换成布尔值。所得到的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是 true;相反 false

显示转换

有时会出现希望显示地将对象强制转换成另一种类型。例如想在下面地代码中执行浮点数除法

int i, j;
double slope = i / j;

命名的强制类型转换
一个命名的强制类型转换具有如下形式:
cast-name<type>(expression);
其中,type 是转换的目标类型而 expression 是要转换的值。如果 type 是引用类型,则结果是左值。cast-namestatic_cast、dynamic_cast、const_cast 和 reinterpret_cast 中的一种。dynamic_cast 支持运行时类型识别,

static_cast

任何具有明确定义的类型转换,只要不包含底层 const,都可以使用 static_cast。例如,通过将一个运算对象强制转换成 double 类型就能使表达式执行浮点数除法

// 进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;

当需要一个较大的算数类型复制给较小的类型时,static_cast 非常有用。用强制类型转换告诉程序的读者和编译器:知道并且不在乎潜在的精度损失。当执行了显示的类型转换后,警告信息被关闭。

static_cast 对于编译器无法自动执行的类型转换也非常有用。例如,可以使用 static_cast 找回存在于 void* 指针中的值

void* p = &d;	// 正确:任何非常量对象的地址都能存入 void*
double* dp = static_cast<double*>(p);	// 赵青雀:将 void* 转换回初始的指针类型

当我们把指针存放在 void* 中,并且使用 static_cast 将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的候过

const_cast

const_cast 只能改变运算对象的底层 const

const char* pc;
char* p = const_cast<double*>(p);

对于将常量对象转换成非常量对象的行为,我们一般将其称为“去掉 const 性质( cast away the const )。一旦去掉某个对象的 const 性质,编译器就不会再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限时合法行为。然,如果对象是一个常量,再使用 const_cast 执行写操作就会产生未定义的后果

只有 const_cast 能改变表达式的常量属性,其他的不可以,同样,也不能用 const_cast 改变表达式的类型:

const char* pc;
char* q = static_cast<char*>(pc); 	// 错误:static_cast 不能转换 const 属性
static_cast<string>(pc);	// 正确:字符串字面值转换成 string 类型
const_cast<string>(pc);		// 错误:const_cast 只改变常量属性

const_cast 常常用于有函数重载的上下文中。( 第六章 p208 详细解释 )

reinterpret_cast

reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。假设有如下转换:

int* ip;
char* pc = reinterpret_cast<char*>(ip);

要知道,pc 所指的真实对象是一个 int 而非字符,如果把 pc 当作普通的字符指针使用就可能在运行时发生错误,如:
string str(pc);
可能导致异常的运行时行为。

使用 reinterpret_cast 非常危险。其中的关键问题ishi类型改变了,但编译器没有给出任何警告或者错误的提示信息。当我们用一个 int 的地址初始化 pc 时,由于显示地声称这种转换合法,所以编译器不会发出任何警告或错误信息。最终地解雇哦就是,在上面地例子中虽然用 pc 初始化 str 没有什么实际意义,甚至还会引发更糟糕地结果,但仅从语法上而言,这种草所没有问题。

旧式的强制类型转换

type(expr);		// 函数形式的强制类型转换
(type) expr;	// C 语言风格的强制类型转换

根据所涉及的类型不同,旧式的强制类型转换分别具有与 const_cast、static_cast 或 reinterpret_cast 相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成 const_caststatic_cast 也合法,则其行为与对应的命名转换已知。如果替换后不合法,则旧式强制类型转换执行与 reinterpret_cast 类似的功能:
char* pc = (char*) ip; // ip 是指向整数的指针
的效果与使用 reinterpret_cast 一样

第五章 语句

try 语句块和异常声明

  • throw 表达式( throw expression ) 异常检测部分使用 throw 表达式来表达它遇到了无法处理的问题。说 throw 引发( raise ) 了异常
  • try 语句块( try block ) 异常处理部分使用 try 语句块处理异常。try 语句块以关键字 try 开始,并以一个或多个 catch 子句( catch clause ) 结束。try 语句块中代码抛出的异常通常会被某个 catch 子句处理。因此 catch 子句“处理”异常,所以它们也被称为异常处理代码(ecception handler)
  • 一套异常类( exception class ) 用于在 throw 表达式和相关的 catch 子句之间传递异常的具体信息

throw 表达式

例子:

Sales_item item1, item2;
cin >> item1 >> item2;
// 首先检查 item1 和 item2 是否表示同一种数据
if( item1.isbn() == item2.isbn() ){
	cout << item1 + item2 << endl;
	return 0; // 表示成功
} else {
	cerr << "Data muste refer to same ISBN" << endl;
	return -1; // 表示失败
}

在真实的程序中,应该把对象相加的代码和用户交互的代码分离开来。因此,改写程序使得检查完成后不再直接输出一条信息,而是抛出一个异常

// 首先检查两条数据是否是关于同一种书籍的
if( item1.isbn() != item2.isbn() )
	throw runtime_error( "Data must refer to same ISBN" );
// 如果程序执行到了这里,表示两个 ISBN 是相同的 
cout << item1 + item2 << endl;

类型 runtime_error 是标准库异常类型的一种,定义在 stdexcept 头文件中

try 语句块

try 语句块的通用语法形式是

try{
	program-statements
} catch ( exception-declaration ){
	handler-statements
} catch ( exception-declaration ){
	handler-statments
} // ...

try 语句块的一开始是关键字 try ,随后紧跟着一个块。
跟着 try 块之后的一个或多个 catch 子句。catch 子句包括三部分:关键字 catch 、括号内一个( 可能未命名 )对象的声明( 称为 异常声明,exception declaration )以及一个块。当选中了某个 catch 子句处理异常之后,执行与之对应的块。catch 一旦完成,程序跳到 try 语句块最后一个 catch 子句之后的那条语句继续执行

try 语句块中的 program-statements 组成程序的正常逻辑,想其他任何块一样,program-statements 可以有包括声明在内的任意 C++ 语句。

try 语句块内声明的变量在外部无法访问,特别是在 catch 子句内也无法访问

编写处理代码
对于上面的那个例子,假设执行 Sales_item 对象加法的代码是与用户交互的代码分离开来的。其中与用户交互的代码负责处理发生的异常,它的形式可能如下:

while( cin >> item1 >> item2 ){
	try{
		// 执行添加两个 Sales_item 对象的代码
		// 如果添加失败,代码抛出一个 runtime_error 异常
	} 
	catch ( runtime_error err ){
		cout << err.what()
			 << "\nTry Again? Enter y or n" << endl;
			 char c;
			 cin >> c;
			 if( !cin || c == 'n' )
			 	break; // 跳出循环
	}
}
函数在寻找处理代码的过程中退出

在复杂系统中,程序在遇到抛出异常的代码前,其执行路径可能已经经过了多个 try 语句块。例如,一个 try 语句块可能调用了包含另一个 try 语句块的函数,新的 try 语句块可能调用了包含又一个 try 语句块的新函数,以此类推

寻找处理代码的过程与函数调用刚好相反。当异常被抛出,首先搜索抛出该异常的函数。如果没有找到匹配的 catch 子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的 catch 子句,这个新函数也被终止,继续搜索调用它的函数。以此类推,言这程序的执行路径逐层回退,自横刀找到适当类型的 catch 子句

如果最终还是没能找到任何匹配的 catch 子句,程序转到名为 terminate 的标准库函数。该函数的行为与系统有关,执行该函数将导致程序非正常退出

对于那些没有任何 try 语句块定义的异常,也按照类似的方法处理:毕竟,没有 try 语句块就意味着没有匹配的 catch 子句。

标准异常

  • exception 头文件定义了最通用的异常类 exception。它只报告异常的发生,不提供任何额外信息

  • stdexception 头文件定义了几种常用的异常类,如下表

    stdexcept定义的异常类
    exception最常见的问题
    runtime_error只有在运行之才能检测出的问题
    range_error运行时错误:生成的结果超粗黑了有意义的值域范围
    overflow_error运行时错误:计算上溢
    underflow_error运行时错误:计算下溢
    logic_error程序逻辑错误
    domain_error逻辑错误:参数对应的结果值不存在
    invalid_argument无效参数
    length_error逻辑错误:试图创建一个超出该类型最大长度的对象
    out_of_range逻辑错误:使用一个超出有效范围的值
  • new 头文件定义了 bad_alloc 异常类型(p407 第十二章)

  • type_info 头文件定义了 bad_cast 异常类型 (p731 第十九章)

标准库异常类只定义了几种运算,包括创建或拷贝异常类新给的对象,以及为异常类型的对象赋值。

我们只能以默认初始化的方式初始化 exception、bad_allocbad_cast 对象,不允许为这些对象提供初始值

其他异常类型的行为恰好相反:应该使用 string 对象或 C 风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息。

异常类型只定义了一个名为 what 的成员函数,该函数没有任何参数,返回值是一个指向 C 风格字符串的 const char*what 函数返回的 C 风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则 what 返回该字符串。对于其他无初始值的异常类型来说,what 返回的内容由编译器决定

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值