登录  | 立即注册

游客您好!登录后享受更多精彩

查看: 85|回复: 0

C++Primer学习笔记04.表达式

[复制链接]

44

主题

-24

回帖

30

积分

新手上路

积分
30
发表于 2025-1-1 22:47:18 | 显示全部楼层 |阅读模式

C++ 语言提供了一套丰富的运算符,并定义了这些运算符作用于内置类型的运算对象时所执行的操作。对应自己定义的类型,可以使用运算符重载来为它们定义各种运算规则。

基础

- -
一元运算符 作用于一个运算对象的运算符是一元运算符,如取地址符(&)和解引用符(*)
二元运算符 作用于两个运算对象的运算符是二元运算符,如相等运算符(==)和乘法运算符(*)
三元运算符

<b>运算对象转换</b>

整数能转换成浮点数,浮点数也能转换成整数,但是指针不能转换成浮点数。

再进行计算的时候,小整数类型(如bool、char、short等)通常会被提升(promoted)成较大的整数类型,主要是 int。可以暂时认为和 Java 的类型提升规则一样。

<b>重载运算符</b>

C++ 语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自行定义其含义。

<span style="color:red">典型的运算符重载有:IO 库的 >> 和 << 运算符以及 string 对象、vector 对象和迭代器使用的运算符都是重载的运算符。</span>

<b>左值和右值</b>

C++ 的表达式要不然是右值(rvalue,读作 “are-value”),要不然就是左值(lvalue,读作 “ell-value”)。这两个名词是从 C 语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。

....

优先级和结合律

使用 () 确保正确的优先级。

求值顺序

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。

int i = f1() * f2();

我们知道 f1 和 f2 一定会在执行乘法之前被调用,因为毕竟相乘的是这两个函数的返回值。但是我们无法知道到底 f1 在 f2 之前调用还是 f2 在 f1 之前调用。

但是括号的优先结合在这里也是适用的。

// 先计算 f1 和 f2 的和,再计算和 f3 的乘积
int i = (f1()+f2()) * f3(); 

<b>建议:处理复合表达式</b>

  • 拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
  • 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

<span style="color:orange">C++ 语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡。</span>

算术运算符

<div align="center"><h6>算术运算符(左结合律)</h6></div>

运算符 功能 用法
+ 一元正号 + expr
- 一元负号 - expr
* 乘法 expr* expr
/ 除法 expr / expr
% 求余 expr % expr
+ 加法 expr + expr
- 减法 expr - expr

<b>溢出和其他算术运算异常</b>

算术表达式有可能产生未定义的结果。一部分原因是数学性质本身:例如除数是 0 的情况;另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。

假设某个机器的 short 类型占 16 位,则最大的 short 数值是 32767。在这样一台机器上,下面的复合赋值语句将产生溢出

short short_val = 32767;
short_val +=1;
cout<<short_val<<endl;

给 short_value 赋值的语句是未定义的,这是因为表示一个带符号数 32768 需要 17 位,但是 short 类型只有 16 位。很多系统在编译和运行时都不报溢出错误,像其他未定义的行为一样,溢出的结果是不可预知的。在我们的系统中,程序的输出结果 -32768

逻辑和关系运算符

<div align="center"><h6>逻辑运算符和关系运算符</h6></div>

结合律 运算符 功能 用法
右 ! 逻辑非 ! expr
左 < 小于 expr < expr
左 <= 小于等于 expr <= expr
左 > 大于 expr > expr
左 >= 大于等于 expr >= expr
左 == 相等 expr == expr
左 != 不相等 expr != expr
左 && 逻辑与 expr && expr
左|| 逻辑或 expr|| expr

与其他语言类似。唯一的区别是,C++ 中非 0 的在条件表达式中都表示 true。

#include<iostream>
using std::cout;
int main(){
    int a = -1;
    // true
    if(a) cout<<"true";
}
#include<iostream>
using std::cout;
int main(){
    const char *cp = "hello world";
    // true
    if(cp && *cp) cout<<"true";
}

赋值运算符

int num = 10; // 赋值

C++11 新标准允许使用花括号括起来的初始值列表

vector<int> vi;
vi = {0, 1, 2, 3, 4};

<b>赋值运算满足右结合律</b>

int val1, val2;
val1 = val2 = 0;

递增和递减运算符

递增运算符(++)和递减运算符(--)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算,所以此时递增和递减运算符除了书写简洁外还是必须的。

递增和递减运算符有两种形式:前置版本和后置版本。

int i=0, j;
j = ++i; // j = 1, i = 1; 前置版本得到递增之后的值
j = i++; // j = 1, i = 2; 后置版本得到递增之前的值

<b>除非必须,否则不用递增递减运算符的后置版本</b>

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

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

后置递增运算符的优先级高于解引用运算符,<span style="color:red">因此*pbeg++ 等价于*(pbeg++)。</span>pbeg++ 把 pbeg 的值加 1,然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。最终,这条语句输出 pbeg 开始时指向的那个元素,并将指针向前移动一个位置。

成员访问运算符

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

string s1="hello", *p = &s1;
auto n = s1.size();
n = (*p).size();
n = p->size(); // 等价于 (*p).size();

条件运算符

条件运算符(? :)允许我们把简单的 if-else 逻辑嵌入到单个表达式当中,条件运算符按照如下形式使用

string ans = (grade < 60) ? "fail" : "pass";

嵌套条件运算符

string ans = (grade > 90) ? "high pass" 
                            : (grade < 60) ? "fail" : "pass";

第一个条件检查成绩是否在 90 分以上,如果是,执行符号 ?后面的表达式,得到 "high pass";如果否,执行符号:后面的分支。这个分支本身又是一个条件表达式,它检查成绩是否在 60 分以下,如果是,得到 "fail" ;否则得到 "pass"。

条件运算执行前会检查条件,然后跳转到内存的不同地方,然后再执行指令,这也意味着更大的开销。许多代码优化策略也会去消除部分 if 操作。

位运算

<div align="center"><h6>位运算</h6></div>

运算符 功能 用法
~ 位求反 ~ expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
& 位与 expr & expr
^ 位异或 expr ^ expr
| 位或 expr| expr

一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型。

<span style="color:red">注意:位运算符如何处理运算对象的“符号位”依赖于机器!因此,强烈建议仅将位运算符用于处理无符号类型。</span>

#include<iostream>

using std::cout;
using std::cin;
using std::endl;

// 被移动的数字放位运算符左边,移动的位数在运算符右边
int main() {
    int num = 10;
    // num 右移一位,缩小为原来的 1/2
    cout << (num >> 1) << endl; // 5
    // num 左移一位,扩大为原来的 2 倍
    cout << (num << 1) << endl; // 20
}

刷题的时候再补充其他的内容

sizeof运算符

sizeof 运算符返回一条表达式或一个类型名字所占的字节数。

#include<iostream>

using std::cout;
using std::cin;
using std::endl;
using std::begin;
using std::end;

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    char *c = new char;

    cout << sizeof(arr) << endl; // 36 
    cout << sizeof(arr) / sizeof(int) << endl; // 9 计算出它有多少个元素
    cout << sizeof(c) << endl; // 8 字节,指针的大小都是一样的。在我的机器上是 8 字节
}

<b>sizeof 运算符的结果部分地依赖于其作用的类型</b>

  • 对 char 或者类型为 char 的表达式执行 sizeof 运算,结果得 1。
  • 对引用类型执行 sizeof 运算得到被引用对象所占空间的大小。
  • 对指针执行 sizeof 运算得到指针本身所占空间的大小。
  • 对解引用指针执行 sizeof 运算得到指针指向的对象所占空间的大小,指针不需有效。
  • 对数组执行 sizeof 运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次 sizeof 运算并将所得结果求和。注意,sizeof 运算不会把数组转换成指针来处理。
  • 对 string 对象或 vector 对象执行 sizeof 运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

因为 sizeof 的返回值是一个常量表达式,所以我们可以用 sizeof 的结果声明数组的维度。

类型转换

与 Java 类似。char short 计算时会先提升为 int,再做计算。隐式类型转换也是 “大类型+小类型” 会先都转换成大类型然后再计算。

// 先都提升为 double 然后再计算,再把 6.5 赋值给 int
// 编译器可能会警告该运算损失了精度
int val = 3.5 + 3; 

<b>何时发送了隐式类型转换</b>

在下面这些情况下,编译器会自动地转换运算对象的类型:

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

算术类型转换

<b>无符号的类型转换</b>

如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是 unsigned int 和 int,则 int 类型的运算对象转换成 unsigned int 类型。

<b>理解算术中的类型转换</b>

bool    flag;       char    cval;
short   sval;       unsigned short  usval;
int ival;           unsigned int uival;
long lval;          unsigned long ulval;
float fval;         double dval;

3.14L + 'a';    // 'a' 提升为 int, int 转成 long double
dval + ival;    // ival 转成 double
dval + fval;    // fval 转成 double
ival = dval;    // dval 转成 int
flag = dval;    // 如果 dval 是 0,则 flag 是 false,否则 flag 是 true
cval + fval;    // cval 提升成 int,然后该 int 值转换成 float
sval + cval;    // sval 和 cval 都提升成 int
cval + lval;    // cval 转换成 long
ival + ulval;   // ival 转换成 unsigned long
usval + ival;   // 根据 unsigned short 和 int 所占空间的大小进行提升
uival + lval;   // 根据 unsigned int 和 long 所占空间的大小进行转换

其他隐式类型转换

<b>数组转换成指针:</b>在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针

int ia[10]; // 含有 10 个整数的数组
int* ip = ia;   // ia 转换成指向数组首元素的指针

当数组被用作 decltype 关键字的参数,或者作为取地址符(&)、sizeof 及 typeid 等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。

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

<b>转换成常量:</b>允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果 T 是一种类型,我们就能将指向 T 的指针或引用分别转换成指向 const T 的指针或引用

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

显示转换

显示的将对象强转转换成另一种类型。

#include<iostream>

using std::cout;
using std::cin;
using std::endl;
using std::begin;
using std::end;

int main(){
    int i=1,j=3;
    double slope = i/j;
    cout<<slope<<endl; // 0
}

<b>命名的强制类型转换</b>

一个命名的强制类型转换具有如下形式 cast-name<type>(expression);

其中,type 是转换的目标类型而 expression 是要转换的值。如果 type 是引用类型,则结果是左值。cast-name 是 static_cast、dynamic_cast、const_cast 和 reinterpret_cast 中的一种。dynamic_cast 支持运行时类型识别。

1️⃣static_cast

任何具有明确定义的类型转换,只要不包含底层 const,都可以使用 static_cast。

double slope = static_cast<double>(j) / i;

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

void *p = &d;
// 将 void* 转换回初始的指针类型,可以用它实现多态吧
double *dp = static_cast<double*>(p);

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

2️⃣const_cast

将常量对象转换为非常量对象,即去掉 const 性质。去掉了某个对象的 const 性质,编译器就不再阻止我们对该对象进行写操作了。

#include<iostream>
#include<string>

using std::cout;
using std::cin;
using std::endl;
using std::begin;
using std::end;
using std::string;

int main() {
    const char *cp = "hello";
    // 错误,static_cast 不能转换掉 const 性质
    // char *q = static_cast<char*>(cp);
    // 正确,字符串字面值转换成 string 类型。
    auto sc = static_cast<string>(cp);
    // 正确 const_cast 只改变常量属性
    char *sc2 = const_cast<char *>(cp);
    // 错误 const_cast 只改变常量属性
    string sc3 = const_cast<char *>(cp);
    cout << sc << ":" << sc2 << endl;
}

3️⃣reinterpret_cast

reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。不是很理解,但是它是依赖于机器的,尽量不要去使用。除非对涉及的类型和编译器实现转换的过程都非常了解。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2025-1-19 02:31 , Processed in 0.067359 second(s), 27 queries .

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

快速回复 返回顶部 返回列表