侧边栏壁纸
博主头像
LittleAO的学习小站 博主等级

在知识的沙漠寻找绿洲

  • 累计撰写 125 篇文章
  • 累计创建 27 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

C++之表达式与类型转换

LittleAO
2024-06-04 / 0 评论 / 0 点赞 / 24 阅读 / 0 字
温馨提示:
本文最后更新于2024-06-04,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

C++之表达式与类型转换

表达式是由多个运算对象组成,对表达式求值将得到一个运算结果,字面值和变量是最简单的表达式,运算符和多个运算对象组合起来可以生成复杂的表达式,例如下面这个例子:

a = val == 3 ? b : 2L << 4;

基本概念

左值和右值

左值和右值是C++中比较难以理解的概念。这两个名词都是从C语言中继承过来的,例如a=1a就是左值,1就是右值。

如果只是按赋值语句的左右来简单的判断左值和右值那就大错特错了,有的时候,左值也能出现在等号的右侧。但是右值绝对不能出现在等号的左侧(赋值语句中)。

int a = 2;  // a是左值,2是右值
int b;
b = a;  // 两个变量都是左值

到这里可能就糊涂了,左右值究竟怎么判断,为什么要分辨左右值?简单来说,左值表达式的求值结果是一个对象变量,左值是对象的身份,例如上面的语句中,a是身份;右值是具体的值,例如上面的2

至于为什么要区分左右值,是因为某些运算符是要用到左值的:

  • 赋值运算符

int val;
val = 2;    // 赋值运算符作用于左值,得到结果也是左值
// 3 = val; // 错误:赋值运算符不能作用于右值
  • 取地址符

int val = 2;
func(&val)  // 取地址符需要作用于左值,结果是地址,这是一个右值
func(&2)    // 错误,不能为右值
  • 解引用运算符,下标运算符

int val[3] = {1, 2, 3};
int *pval = val;
std::cout << *(pval + 1) << std::endl;  // pval + 1 是个左值,解引用需要左值
std::cout << val[1] << std::endl;    // 下标运算符需要左值
  • 内置类型,迭代器的递增递减运算符

int val[3] = {1, 2, 3};
int *pval = val;
std::cout << (*++pval)-- << std::endl;  // ++,--需要作用于左值

decltype作用于左右值的结果也有所不同,如果表达式的求值结果是左值,则为一个引用类型,如果表达式的求值结果是右值,则不是引用。

int a = 10;
decltype(a) b = a;  // b 的类型是 int,因为 a 运算结果是右值
int* ptr = &a;
decltype(*ptr) c = a;  // c 的类型是 int&,因为 *ptr 结果是左值

一元运算符和二元运算符

一元二元决定于运算对象的个数,例如取地址运算符`&`为一元运算符,加法运算符+为二元运算符。

优先级,结合律和求值顺序

  • 优先级:在复杂的表达式中,优先级决定了操作符的执行顺序。例如,在表达式 2 + 3 4 中,乘法操作符 `` 的优先级高于加法操作符 +,所以 3 * 4 会先被计算。

  • 结合律:结合律决定了当两个或更多的操作符具有相同的优先级时,操作符的执行顺序。大多数操作符从左到右结合(左结合),例如减法和除法。但是,有一些操作符从右到左结合(右结合),例如赋值 = 和条件 ?: 操作符。

a = 5 + 6 + 7 * 2;  
// 先考虑优先级,*的优先级较高,先计算7*2
// 再考虑结合律,+是左结合,先计算5+6,再计算11+14
  • 求值顺序:求值顺序是指在表达式中计算各个子表达式的顺序。在 C++ 中,这个顺序并不总是从左到右。例如,在函数调用中,函数参数的求值顺序是未指定的。这意味着如果函数参数之间有依赖关系,可能会导致未定义的行为。

int i = 0;
std::cout << i << " " << ++i << std::endl; 
// 求值顺序不知道,有可能先计算++i后再输出i,这是未定义的行为,取决于编译器
// 不要这样写!

逻辑运算符,条件运算符和逗号运算符确定了求值顺序,之后会讲到。

类型转换与提升

  • 类型转换:类型转换是将一个数据类型转换为另一个数据类型的过程。在 C++ 中,类型转换可以是隐式的,也可以是显式的。隐式类型转换是由编译器自动完成的,例如将 int 转换为 double。显式类型转换是由程序员使用转换运算符如 static_castdynamic_castconst_castreinterpret_cast 手动完成的,后面将会详细解释。

  • 类型提升:类型提升是一种特殊的类型转换,它发生在混合类型的表达式中。当一个表达式中的操作数具有不同的数据类型时,编译器会自动将"较小"的类型转换为"较大"的类型,以确保数据不会丢失。例如,如果你将一个 int 和一个 double 相加,int 会被提升为 double,然后进行加法运算。

重载运算符

有关重载运算符的详细解释,前面的文章有提到过。

C++之运算符重载-LittleAO的学习小站

算术运算符

算术运算符有以下几种:

  • + :一元运算时做正号,二元为加法运算

  • -:一元运算时做负号,二元为减法运算

  • *:乘法运算

  • / :除法运算

  • % :取余运算

注意事项

使用算术运算符时,需要注意以下方面:

  • 算术运算符的求值结果是右值;

  • 指针可以使用加法减法运算;

bool flag = true; bool flag2 = -flag; 请问flag2是多少,bool类型适合参与算术运算吗?

  • bool类型参与运算,会被提升为整型,只有整型的0才能被隐式转换为false;

  • 运算的范围不要超过类型支持的范围;

  • % 的运算对象只能为整数;

  • 除法运算结果若为整型,则直接切除小数部分(C++11);

逻辑和关系运算符

逻辑运算符有以下几种:

  • ! :逻辑非,一元运算符;

  • &&:逻辑与,二元运算符;

  • ||:逻辑或,二元运算符;

关系运算符有以下几种:

  • < :小于时为真;

  • <=:小于等于时为真;

  • > :大于时为真;

  • >= :大于等于时为真;

  • == :相等时为真;

  • != :不等时为真;

注意事项

特别注意的是,使用逻辑运算符,可以考虑求值顺序。逻辑与和逻辑或都是先求左边运算对象再求右边运算对象,如果左边的结果已经满足了判断的要求,那么右边的求值就可以忽略掉。这种策略称为短路求值

index != s.size() && is_space(s[index])	// 递增index时,这样写不用担心index会超出s的范围而引发报错

使用关系运算符也需要注意,关系运算符运算的结果是一个bool值,连续使用关系运算符极有可能出错。

0 < a < 1 在C++(Python是个例外)中代表着(0 < a) < 1 ,实际上是一个bool值和1作比较,这是不对的。正确的写法是0 < a && a < 1

小任务:能不能写一个Int 来满足链式比较?这里给一个示例

现在来考虑一下,if(val)if(val == true) 哪种写法更好?两者看似差不多,但是如果val 不为bool值时,会进行隐式转换,例如指针,如果指针不是一个空指针,那么if(val)会被判断为真,而if(val == true)会被判断为假。综上,if(val)更加常用也更好。

int *p = nullptr;
if (!p)
{
    std::cout << "pointer is null!" << std::endl;
}
if (p == false) // 编译器报错, 无法进行比较
{
    std::cout << "pointer is equals to false!" << std::endl;
}

赋值运算符

  • = :赋值运算

注意事项

  • 赋值运算的左侧必须是一个可以修改的左值,如果右侧运算对象不同,则转换成左侧运算对象相同的类型。

int iNum;
iNum = 3.4f;	// iNum为3
  • C++11 允许使用初始值列表作为右侧运算对象:

std::vector<int> a = {1, 2, 3, 4};
  • 赋值运算满足的是右结合律:

int a, b, c;
a = b = c = 1;	// 先计算c = 1,将结果1赋值到b,这个赋值操作得到结果1赋值给a
1 = a = b = c;	// 错误,赋值运算左侧运算对象不能为右值
  • 赋值运算优先级很低,如果有特别需求,建议将赋值运算加括号:

while (i = getvalue() > 3) { /**/ } // i被赋值getvalue() > 3
while ((i = getvalue()) > 3) { /**/ } // i被赋值getvalue(), 并判断i > 3

条件语句中赋值运算一般要加括号。

复合赋值运算符

将算数运算符或位运算符与赋值运算符组合,称为复合赋值运算符:

  • +=-=*=/=%= , 与算数运算符复合;

  • <<=>>=&=^=!=,与位运算符复合,后面会讲;

所有的复合运算a op= b 相当于 a = a op b。

递增递减运算符

  • ++ :递增运算符;

  • -- :递减运算符;

这两个运算符分为前置版本和后置版本,前置版本(++i)是将运算对象加1(减1)后将改变后的对象作为求值结果。后置版本(i++)是将改变前的对象作为求值结果。

注意事项

  • 尽量不要使用后置版本:后置版本需要将原始值存储下来返回,会导致性能的损耗,除非必须,否则不推荐使用后置版本。

  • 混用解引用和递增递减运算符:例如*iter++ ,可以让代码看起来更简洁,在循环的时候经常用到。

成员访问运算符

  • *:访问指针指向的内容;

  • ->:访问指针指向对象的成员;

注意事项

* 运算符的优先级较低,一般使用的时候需要加括号在访问其成员。

*p.size()	// 错误,优先级先处理点
(*p).size()	// 正确

条件运算符

  • ?: :可以把简单的if-else语句嵌套在表达式中,可以多层嵌套。

std::string grade = score >= 60 ? score >= 90 ? "excellent" : "pass" : "fail";  

嵌套不要超过2层,否则代码可读性大大下降。

位运算符

  • ~ :一元运算符,求反;

  • << :二元运算符,左移;

  • >> :二元运算符,右移;

  • & :二元运算符,位与;

  • ^ :二元运算符,位异或;

  • | :二元运算符,位或;

注意事项

  • 使用位运算符,可以是有符号的,可以是无符号的。但是有符号的会在移位操作中由正变负,或者其他情况,这取决于机器的处理,属于未定义的行为。因此使用位运算,一般使用无符号整型。

  • 运算对象位数小于32,位运算会自动提升至32位。

  • 无论是右移还是左移,超过边界的位数都会被舍弃掉。

  • bitset 可以表示任意大小的二进制位集合。

sizeof运算符

有两种形式:

  • sizeof(type) :返回type类型所占字节数;

  • sizeof expr :返回表达式类型所占字节数;

返回类型为const size_t类型。需要注意,sizeof expr 不返回表达式实际所占字节大小。

sizeof(int);    // 4, int类型的大小为4字节(int32)
sizeof "hello"; // 6,实际上为char[6]的大小
sizeof (2 + 1);  // 4,表达式的类型为int
int val = 2, *p = &val, &r = val;
sizeof p;  // 8,返回指针的大小,而不是所指向内容的大小
sizeof *p; // 4,返回所指向内容类型的大小
sizeof r;  // 4,引用返回被引用内容类型的大小
int a[8] = {1, 2, 3, 4, 5, 6, 7, 8};
sizeof a;  // 32,返回int[8]类型的大小
sizeof *a; // 4,返回int[8]第一个元素类型的大小(int)
sizeof std::string("This is a very long long sentence.");
// 32,string和vector都是固定长度,不会因为元素多少的改变而改变它们的sizeof值

扩展:内存对齐

逗号运算符

  • , :从左至右依次求值,规定了求值的顺序;

注意事项:

  • 逗号运算符真正的求值结果是逗号右边表达式的值;

a = (1 + 2, 3 + 4); // a的值为7

类型转换

隐式转换

如果两种类型有关联,并且程序需要其中一种类型的时候,就可以将某一种类型转换成需要的类型,这称作为相互转换。有些类型转换无需程序员的介入,是自动执行的,这称作为隐式转换

在运算过程中,这种隐式转换经常发生。例如:

int val = 3.14 + 3;	// 3先转换为double与3.14相加,赋值到val转换成了6

上面的类型转换程序员没有显式的指明,由程序自动进行的隐式转换。你可能会好奇这样的转换规则是什么,下面就来介绍。

算术转换

算术转换的含义就是将一种算术类型转换成另外一种算术类型,例如整型和浮点型相加时,整型就会被转化为浮点型。一个窄的类型和一个宽的类型做运算,窄的类型就会转化为宽的类型(提升)。

int窄的类型都会在运算过程中提升为int,或者是unsigned int

3.14159L + 'a';	// 'a'提升为int,接着提升为long double
bool_val = double_val;	// 赋值运算右边的转化为左边的值
short_val + char_val;	// 两个都提升为int

其他隐式转换

  • 数组转换为指针;

  • 指针之间的转换:任意非常量指针可以转换为void*,任意指针可以转换为const void*

  • 转换为布尔类型,常用于if语句,例如空指针就能隐式转换为false;

  • 转换为常量:可以将非常量指针和引用转换为常量指针和引用,但反过来不行。

  • 类类型定义的转换。

显式转换

有时希望将一个类型强制转换成另外一种类型,可以手动进行显式转换。

旧式强制类型转换

在C++早期版本,可以显式的进行类型转换:

(float)3	// 浮点类型的3.0
float(3) 	// 效果一样
double a = (double)3 / 2	// 经常用,包含一个显式转换和一个隐式转换

但是如果转换出现问题,追踪起来比较困难。一般推荐使用命名的强制类型转换。

强制类型转换非常危险,不到迫不得已不要用。

static_cast

static_cast 用于在编译时进行类型转换。它主要用于以下几种情况:

  • 基本数据类型之间的转换,例如 intdouble

  • 在继承层次结构中,上行转换(从派生类到基类)和下行转换(从基类到派生类,但不安全)。

  • void* 指针转换为其他类型的指针。

示例

int main() 
{
    int a = 10;
    double b = static_cast<double>(a); // int 转 double
    Base* base = new Derived();
    Derived* derived = static_cast<Derived*>(base); // 基类转派生类(不安全)
    void* ptr = &a;
    int* intPtr = static_cast<int*>(ptr); // void* 转 int*
    return 0;
}

dynamic_cast

dynamic_cast 主要用于在继承层次结构中进行安全的下行转换(从基类到派生类)。它依赖于运行时类型识别(RTTI),并且只能用于指向多态类型(即包含虚函数的类)的指针或引用。如果转换失败,指针类型会返回 nullptr,引用类型会抛出 std::bad_cast 异常。

示例

class Base 
{
    virtual void foo() {} // 必须有虚函数
};

class Derived : public Base 
{
    void foo() override {}
};

int main() {
    Base* base = new Derived();
    Derived* derived = dynamic_cast<Derived*>(base); // 安全的下行转换
    if (derived) 
	{
        // 转换成功
    } else 
	{
        // 转换失败
    }
    return 0;
}

const_cast

const_cast 用于添加或移除 constvolatile 修饰符。它不能用于其他类型的转换。主要用于需要修改常量对象或在函数重载中处理常量和非常量版本的情况。

示例

void foo(const int* p) 
{
    int* modifiable = const_cast<int*>(p);
    *modifiable = 10; // 修改常量对象,这是未定义的行为
}

int main() 
{
    const int a = 5;
    foo(&a);
    return 0;
}

reinterpret_cast

reinterpret_cast 用于执行低级别的、几乎不受限制的类型转换。这种转换通常会改变对象的比特表示,因此它非常危险,只有在非常特殊的情况下才应该使用。常见的用途包括将指针转换为整数类型,或将一个类型的指针转换为另一个类型的指针。

示例

int main() 
{
    int a = 42;
    void* ptr = reinterpret_cast<void*>(&a); // int* 转 void*
    int* intPtr = reinterpret_cast<int*>(ptr); // void* 转 int*
    return 0;
}

0

评论区