C++

表达式

"表达式的知识点"

Posted by JosonChan on 2020-10-31

[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 = &num;
double* ptr = static_cast<double*> (p);

任何编写程序时能够明确的类型转换都可以使用static_cast(static_cast不能转换掉底层const,volatile和__unaligned属性)。由于不提供运行时的检查,所以叫static_cast,因此,需要在编写程序时确认转换的安全性。

主要在以下几种场合中使用:

  1. 用于类层次结构中,父类和子类之间指针和引用的转换;进行上行转换,把子类对象的指针/引用转换为父类指针/引用,这种转换是安全的;进行下行转换,把父类对象的指针/引用转换成子类指针/引用,这种转换是不安全的,需要编写程序时来确认;
  2. 用于基本数据类型之间的转换,例如把int转char,int转enum等,需要编写程序时来确认安全性;
  3. 把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. 继承中的转换

    (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,基类一定需要虚函数。

    因为类中存在虚函数,说明它可能有子类,这样才有类型转换的情况发生,由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表。

  2. void*的转换

    一些情况下,我们需要将指针转换为void*,然后再合适的时候重新将void*转换为目标类型指针。

    class A{
    	virtual void fun(){};// 注意,这里一定需要虚函数
    };
    
    int main(){
        A* a = new A;
        void* b = dynamic_cast<void*>(a);
        delete a;
    }
    
  3. 菱形继承中的上行转换

    首先,定义一组菱形继承的类:

    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

非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。非极端情况不要使用。