[toc]
求值顺序
优先级规定了运算符的组合方式,结合律规定了同级别的运算符之间的组合方式,但是运算对象之间并没有按照什么顺序进行求值。
int i = f1() * f2();
f1()
和 f2()
一定会在执行乘法之间被调用,但是不能够确定 f1()
在 f2()
之前被调用还是 f2()
在 f1()
之前被调用。
运算对象的求值顺序与优先级和结合律无关,在一条形如 f() + g() * h() + j()
的表达式中:
- 优先级规定,
g()
与h()
相乘 - 结合律规定,
f()
的返回值与g()
和h()
的乘积相加,所得结果再与j()
的返回值相加。 - 对于这些函数的调用顺序没有明确规定。
如果这些函数,它们即不会改变同一个对象的状态也不执行IO任务,那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一个对象,则它是一条错误的表达式,将产生未定义的行为。
因此如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
C语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,C的设计思想是尽可能地“相信”程序员,将效率最大化。然而这种思想却有着潜在的危害,就是无法控制程序员自身引发的错误。因此 Java 的诞生也是必然,Java的思想就是尽可能地“不相信”程序员。
赋值运算符
赋值运算采用的是右结合律,同时从右开始转换类型赋给左值。
int i = 0;
double j = 0.0;
j = i = 3.5;// i 等于 3,j也是3
递增和递减运算符
前置递增运算符将对象本身作为左值返回,而后置递增运算符将对象原始值的副本作为右值返回。
int i = 0;
int j = ++i;
j = i++;
前置版本的递增操作避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本就浪费了。测试如下:
#include <iostream>
int main(){
int i = 0;
int j = i++;
j = ++i;
}
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 0 # 给 i 变量初始化
mov eax, DWORD PTR [rbp-4]
lea edx, [rax+1]
mov DWORD PTR [rbp-4], edx
mov DWORD PTR [rbp-8], eax# 以上四步是 i++,需要存储原来的值
add DWORD PTR [rbp-4], 1
mov eax, DWORD PTR [rbp-4]
mov DWORD PTR [rbp-8], eax# 以上三步是++i,需要存储原来的值,因此省了一条汇编语句
mov eax, 0
pop rbp
ret
位运算符
位操作符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。如果运算对象是 “小整型”,则它的值会被自动提升成较大的整数类型。
对于位运算符来说,符号位如何处理没有明确的规定,所以强烈建议仅将该位运算符用于处理无符号类型。
sizeof运算符
sizeof
运算符与 decltype
一样,并不实际计算其运算对象的值,而是在编译期间就被替换了。但是具体的实现查找了一下,并没有看懂,挖个坑吧。
引用:sizeof是怎么实现的
-
对数组执行
sizeof
运算得到整个数组所占空间的大小,等价于数组中所有的元素各执行一次sizeof
运算并将结果求和。注意:sizeof
并不会把数组转换成指针来处理int ia[10] = {0}; constexpr size_t ia_len = sizeof(ia)/sizeof(*ia); int nums[ia_len];
-
对 string 对象或者 vector 对象执行
sizeof
运算只返回该类型固定部分的大小,并不会计算对象中的元素占用了多少空间。
类型转换
隐式转换
- 在大多数表达式中,比 int 类型小的整数值首先提升为较大的整数类型。
- 在条件中,非布尔值转为布尔值。
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换。
- 尽量避免隐式转换
显式转换
尽量避免类型转换!不管是隐式或者是显式!除非万不得已,否则尽可能使用显示转换!
static_cast
double num = 10.0;
int temp = static_cast<int> (num);
void* p = #
double* ptr = static_cast<double*> (p);
任何编写程序时能够明确的类型转换都可以使用static_cast(static_cast不能转换掉底层const,volatile和__unaligned属性)。由于不提供运行时的检查,所以叫static_cast,因此,需要在编写程序时确认转换的安全性。
主要在以下几种场合中使用:
- 用于类层次结构中,父类和子类之间指针和引用的转换;进行上行转换,把子类对象的指针/引用转换为父类指针/引用,这种转换是安全的;进行下行转换,把父类对象的指针/引用转换成子类指针/引用,这种转换是不安全的,需要编写程序时来确认;
- 用于基本数据类型之间的转换,例如把int转char,int转enum等,需要编写程序时来确认安全性;
- 把void指针转换成目标类型的指针(这是极其不安全的),把任何类型的表达式转换成 void 类型。
const_cast
const_cast用于移除类型的const、volatile和__unaligned属性。而且 const_cast 只能改变底层的const,对象必须是指针、引用或者指向对象的指针。
- 常量指针被转换成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然引用原来的对象。-
const char *c = nullptr;
char *c_new = const_cast<char*> (c);
const int &num = 0;
int &num_new = const_cast<int&> (num);
- 在函数重载时使用,第六章补充。
dynamic_cast
dynamic_cast 转换仅适用于指针或引用,并且运行时会进行检查,在转换可能发生的前提下,dynamic_cast会尝试转换,若指针转换失败,则返回空指针,若引用转换失败,则抛出异常。
-
继承中的转换
(1)上行转换
在继承关系中 ,dynamic_cast由子类向父类的转换与static_cast和隐式转换一样,都是非常安全的。
(2)下行转换
class A { virtual void f(){}; }; class B : public A{ public: int num; }; int main() { A* pA = new A; B* pB = dynamic_cast<B*>(pA); if(pB == nullptr){ cout << "s" << endl; }else{ cout << pB->num << endl; } }
**注意类A和类B中定义了一个虚函数,这是不可缺少的。**在基类以及派生类之间进行使用 dynamic_cast,基类一定需要虚函数。
因为类中存在虚函数,说明它可能有子类,这样才有类型转换的情况发生,由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表。
-
void*的转换
一些情况下,我们需要将指针转换为void*,然后再合适的时候重新将void*转换为目标类型指针。
class A{ virtual void fun(){};// 注意,这里一定需要虚函数 }; int main(){ A* a = new A; void* b = dynamic_cast<void*>(a); delete a; }
-
菱形继承中的上行转换
首先,定义一组菱形继承的类:
class A { virtual void f() {}; }; class B :public A { void f() {}; }; class C :public A { void f() {}; }; class D :public B, public C { void f() {}; };
B继承A,C继承A。
D继承B和C。
这样的情况:D对象指针不能安全的转换为A类型指针,因为B和C都继承了A,并且都实现了虚函数f(),导致在进行转换时,无法选择一条转换路径。一种可行的方法是,自行指定一条转换路径。
void main() { D *pD = new D; A *pA = dynamic_cast<A *>(pD); // pA = NULL // 可行的方法 B* pB = dynamic_cast<B*>(pD); A* pA = dynamic_cast<A*>(pB); }
reinterpret_cast
非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。非极端情况不要使用。