📝目录
🪧 前言
本章主要介绍由语言本身定义、并用于内置类型运算对象的运算符,同时简单介绍几种标准库定义的运算符。
表 达 式 由 一 个 或 多 个 运 算 对 象 组 成 , 对 表 达 式 求 值 将 得 到 一 个 结 果。 宇 面 值 和 变 量 是 最 简 单 的 表 达 式 , 其 结 果 就 是 字 面 值 和 变 量 的 值 。 把 一 个 运 算符和一个或多个运算对象组合起来可以生成较复杂的表达式。
一、基础
1️⃣ 基本概念
\qquad
C++定义了一元运算符
和二元运算符
。作用于一 个 运 算 对 象 的 运 算 符 是 一 元 运 算 符 , 如 取 地 址 符 (& ) 和 解 引 用 符 (* ) ;作 用 于 两 个 运 算 对 象 的 运 算 符 是 二 元 运 算 符 , 如 相 等 运 算 符 (==) 和 乘 法 运 算 符 ( * )。 除 此 之 外 , 还 有 一 个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的 数量没有限制。
\qquad 一些符号既能作为一元运算符也能作为二元运算符。以符号 * 为例,作为一元运算符 时执行解引用操作,作为二元运算符时执行乘法操作。一个符号到底是一元运算符还是二 元 运 算 符 由 它 的 上 下 文 决 定 。 对 于 这 类 符 号 来 说 , 它 的 两 种 用 法 互 不 相 干, 完 全 可 以 当 成 两个不同的符号。
✈️运算对象转换
- 在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。例如,尽管 一般的二元运算符都要求两个运算对象的类型相同,但是很多时候即使运算对象的类型不相同也没有关系,只要它们能被转换成同一种类型即可。
- 类型转换的规则虽然有点复杂,但大多数都合乎情理、容易理解。例如,整数能转换成浮点数 ,浮点数也能转换成整数,但是指针不能转换成浮点数。让人稍微有点意外的是, 小整数类型(如bool、char、short 等)通常会被
提升
(promoted)成较大的整数类型, 主要是int。后面会讲。
✈️重载运算符
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实 上是为已存在的运算符賦予另外一层含义,所以称之 为
重载运算符
。 IO库 的 >> 和 << 运 算 符 以 及 string 对 象 、 vector 对 象 和 迭 代 器 使 用 的 运 算符都是重载的运算符。
我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定 义的:但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
简单来说,就是将一个运算符赋予新的作用,比如 =
可以根据自己的意愿重新赋给它作用,但是基础作用要和它一样,在此基础上进行优化和设计,具体在之后会讲,这是很后面的知识了。
✈️左值和右值
左值(lvalue):指向内存位置的表达式被称为左值表达式。左值可以出现在赋值号的左边或右边。
右值(rvalue):术语右值指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。下面是一个有效的语句:
int g = 20;
//变量g 就是左值,而常量20就是右值
20 = 18;
//这样就会犯错,因为常量不能被改变,所以不能用作左值
2️⃣ 优先级与结合律
复合表达式是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理地组合在一起,
优先级
与结合律
决定了运算对 象组合的方式
高优先级运算符的运算对 象要比低优先级运算符的运算对象更为紧密地组合在一起。如果优先级相同,则其组合规 则由结合律确定, 这一点和数学上的优先级相似。
- 根 据 运 算 符 的 优 先 级 , 表 达 式 3 + 4 * 5 的 值 是 23, 不 是 35 。
- 根据运算符的结合律,表达式20-15- 3的值是2,不是8。
6 + 3 * 4 / 2 +2
按照优先级和结合律应该先算乘法,再算除法,最后从左到右再算加法。
总之,编程的优先级和结合律和数学上的类似。
二、算术运算符
+ | 正号,为正数(+1) |
---|---|
- | 负号,为负数(-1) |
* | 乘号,相乘 |
/ | 除号,相除 |
% | 求余数,也叫取模 |
+ | 加号,相加 |
- | 减号,相减 |
int a = 12;
int b = 6;
int c = a + b; // 12 + 6 = 18
c = a - b; //12- 6 = 6;
c = a * b; //12 * 6 = 72
c = a / b; //12 / 6 = 2;
c = a % 10; //取模,12 / 10 = 1 ---》余得 2, c = 2
三、逻辑和关系运算符
! | 逻辑非 | ! expr |
---|---|---|
< | 小于 | expr < expr |
<= | 小于等于 | expr <= expr |
> | 大于 | expr > expr |
>= | 大于等于 | expr >= expr |
== | 相等 | expr == expr |
!= | 不等 | expr != expr |
&& | 逻辑与 | expr && expr |
|| | 逻辑或 | expr||expr |
✈️逻辑与和逻辑或运算符
-
对于逻辑与运算符(&&)来说,当且仅当两个运算对象都为真时结果为真;对于逻辑或运算符 ( || )来说,只要两个运算对象中的一个为真结果就为真。
-
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值, 当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为
短路求值
对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。需要两个条件都为真,才为真,只要有一个条件为假,整体就为假。
对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。只要有其中一个条件为真就为真。
int a = 10;
int b = 5;
int c = 10;
if(a + b && a + c) //两个条件都大于0,为真,所以会输出
{
cout << a + b << " " << a + c << endl;
}
if(a + b && a - c) //a -c=0,又一个条件为假,所以不会输出
{
cout << a + b << " " << a - c << endl;
}
if(a + b || a + c) //只要有一个为真就输出
{
cout << a + b << " " << a + c << endl;
}
if(a + b || a - c)//a+b为真,所以不会去验证a-c是否为真,直接输出了
{
cout << a + b << " " << a - c << endl;
}
if(a -c || a + b)//和第二条一样,先判断 a-c是否为假,为假就判断下一个条件,下一个条件为真就进入分支输出
{
cout << a -c << " " << a + b << endl;
}
if(a -c || a - b)//逻辑或要两个都为假才为假,不会输出
{
cout << a -c << " " << a - b << endl;
}
✈️逻辑非
如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if 语句的 条件:
int a= 1;
if(a) //因为a大于0,所以为true,会输出
{
cout << "hello"<<endl;
}
if(!a) //逻辑非 是将true 变成false,将false变成true,所以 !a = 0,所以不能输出
{
cout << "hello"<<endl;
}
四、赋值运算符
赋值运算符的左侧运算对象必须是一个可修改的左值。
int i = 0; j = 0; k = 0; //初始化而非赋值
const int a = i; //初始化而非赋值
//下面的赋值语句都是非法的
1024 = k; //错误:字面值是右值
i + j = k; //错误:算术表达式是右值
a = k; //错误:a是常量,不可被修改
//赋值:
i = 10; //这就是赋值,已经被定义过了,重新给值就叫赋值
j = 4;
C++11新标准允许使用花括号括起来的初始值列表作为赋 值语句的右侧运算对象:
vector<int> vi;
vi = {0, 1, 2, 3,4, 5, 6,7, 8, 9}; //vi现在含有10个元素了,值从0到9
✈️赋值满足右结合律
int i, j;
i = j = 0; //都被赋值为0
string str1, str2;
str1 = str2 = "hi"; //都被赋值为hi
满足右结合律,先是 j=0 ,然后 i = j ,因为j是0,所以 i 也是0。
✈️复合赋值运算符
+= | -= | *= | /= | %= | 算术运算符 |
---|---|---|---|---|---|
<<= | >>= | &= | ^= | |= | 位运算符 |
int a = 10, b = 20;
a += b; //相当于a = a + b;
b -= a; //相当于b = b - a;
//以此类推
五、递增和递减运算符
递 增 运 算 符 (++ ) 和 递 减 运 算 符 ( – ) 为 对 象 的 加 1 和 减 1 操 作 提 供 了一 种 简 洁 的 书 写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支特算术运算,所以此 时递增和递减运算符除了书写简洁外还是必须的。
int i = 0;
int a = 0;
int j;
j = ++i; //j等于1,相当于j = i + 1,i也等于1
j = a++; //j 等于0,a = 1
为什么上面都是 ++ 操作,而j的值却不一样呢,因为当++操作符在变量前面,是先进行自身的增加,然后在赋值给左边的变量;而 ++ 操作符在变量右边时,是先把自身的值赋值给左边的变量,然后在自身进行增加。-- 运算也是这样的。
口诀:++在前,先增加再使用;++在后,先使用后增加,--同理
j = ++i;
//++ 在前,先进行自身的增加,i+1 = 1;然后在赋值给j,j就等于1
j = a++;
//++ 在后,先使用,先把a=0的值赋给j,然后再进行自身a加1,所以j等于0,a = 1;
六、成员访问运算符
点 运 算 符 和 箭 头 运 算 符都 可 用 于访问成员,其中,点运算待获取类对象的一个成员:箭头运算符与点运算符有关,表达 式 ptr->mem 等 价 于(*pt).mem :
string s1 = "hello";
string *p = &s1;
auto n = s1.size(); //运行string对家s1的size成员函数
n = (*p).size(); //运行p所指对象的size成员
n = p->size(); //等价 于 (*p).size();
(*p).size() 的括号必须要括起来,不然根据结合律,* 的比较低,p.size()就会先结合。
七、条件运算符
条件运算符: ? :
使用:
cond ? expr1 : expr2 ;
其中cond 是判断条件的表达式,而expr1 和expr2是两个类型相同或可能转换为某个公共 类型的表达式。条件运算符的执行过程是:首先求cond 的值,如果条件为真对expr1 求值 并返回该值,否则对expr2求值并返回该值。
//我们可以使用条件运算符判断成 绩是否合格:
int grade1 = 61; //定义一个成绩
int grade2 = 50;
string ctr = (grade1 >= 60) ? "及格" : "不及格";
//ctr 为 及格,括号里面的条件表达式判断为真,就返回第一个值:"及格"
string ctr = (grade2 >= 60) ? "及格" : "不及格";
//ctr 为 不及格,括号里面的条件表达式判断为假,就返回第二个值:"不及格"
//嵌套条件表达式
ctr = (grade > 90) ? "high pass" : (grade < 60)? "fai1" : "pass";
八、位运算符
位运算符作用 于整数类型的运算对象,并把运算对象看成是二进制位的集合。
运算符 | 功能 | 用法 |
---|---|---|
~ | 位求反 | ~expr |
<< | 左移 | expr1 << expr2 |
>> | 右移 | expr1 >> expr2 |
& | 位与 | expr1 & expr2 |
^ | 位异或 | expr1 ^ expr |
| | 位或 | expr1 |expr2 |
九、sizeof运算符
\qquad sizeof 运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得的值是一个size_t 类型的常量表达式。运 算 符 的 运 算 对 象 有 两 种 形 式 :
sizeof(type)
sizeof expr
在第二种形式中,sizeof 返回的是表达式结果类型的大小。与众不同的一点是, sizeof 并不实际计算其运算对象的值:
cout << sizeof(int) << endl; //获取int的大小,一般为4个字节
cout << sizeof(double) << endl; //获取double的大小,一般为8个字节
int arr[10];
sizeof(arr);
//得到的是数组所占内存的大小,10个int,一个int为4个字节,所占40个字节
//sizeof(arr)/sizeof(int) 所占空间除以一个int大小,就可以得到数组的元素个数
Sales_data data, *p; //Sales_data是一个类
sizeof(Sales_data); //存储Sales_data类型的对象所占的空问大小
sizeof data; //data的类型的大小,即sizeof(sales_diata)
sizeof p; //指针所占的空间大小
sizeof *p; //p所指类型的空间大小,即sizeof(sales_data)
sizeof data.revenue;//Sales_data的revenue成员对应类型的大小
sizeof 运算符的结果部分地依赖于其作用的类型:
- 对char或者类型为char的表达式执行sizeof运算,结果得1。
- 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
- 对指针执行sizeof运算得到指针本身所古空间的大小。
- 对解引用指针执行sizeof 运算得到指针指向的对象所占空间的大小,指针不需有效。
- 对数组执行sizeof 运算得到整个数组所占空间的大小,等价于对数组中所有的元 素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
- 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小, 不会计算对象中的元素占用了多少空间。
十、逗号运算符
逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序依次求值。 和逻辑与、逻辑或以及条件运算符一样,逗号运算符也规定了运算对象求值的顺序。
对于逗号运算符米说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算 符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。
int a = 0, b = 0;
//逗号运算符,从左到右执行,先定义a,再定义b
十一、类型转换
int ab = 3.14 + 3;
//因为3.14是double类型的,而最终需要得到int类型,所以编译器就会对3.14进行类型转换
//但是编译器会警告,3.14转成int,将会丢掉小数部分,会导致精度丢失
上述的类型转换是自动执行的,无须程序员的介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换。
何时发生隐式转换
在 下 面 这 些 情 况 下, 编 译 器 会 自 动 地 转 换 运 算 对 象 的 类 型 :
- 在大多数表达式中,比int 类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
- 初始化过程中,初始值转换成变量的类型:在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换
1️⃣ 算术转换
算 术 转 换的 含 义 是 把 一 种 算 术 类 型 转 换 成 另 外 一 种 算 术 类 型 。
✈️整型提升
整型提升负责把小整数类型转换成较大的整数类型。对于bool、 char、signed char、unsigned char、short 和unsigned short等类型来说, 只要它们所有可能的值都能存在int 里,它们就会提升成int 类型;否则,提升成 unsigned int类 型 。 就 如 我 们 所 熟 知 的 , 布 尔 值false 提 升 成 0、true 提 升 成 1 。
较 大 的 char 类 型 (wchar_t 、char15_t、char32_t 提 升 成 int、unsigned int、 long、unsigned long、long long和unsigned long long中最小的一种类型,前 提是转换后的类型要能容纳原类型所有可能的值。
2️⃣ 其他隐式转换类型
\qquad
除了算术转换之外还有几种隐式类型转换,包括如下几种。
数组转换成指针:在大多数用到数组的表达式中,数组自 动转换成指向数组首元素的指针:
int arr[10];
int *p = arr; // arr转换成指向数组首元素的指针
当数组被用作decltype 关键字的参数,或者作为取地址符&、sizeof 及typeid 等运算符的运算对象时,上述转换不会发生。
- 指针的转换:C++还规定了几种其他的指针转换方式,包括常量整数值0 或者字面值 nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对 象的指针能转换成const void*。
- 转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针 或算术类型的值为0,转换结果是false:否则转换结果是true;
- 转换 成 常 量 : 允许将指向非常 量类型的指针转换成指向相应的常量类型的指针, 对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向 T的指针或引用分别转换成指 向 const T的 指 针 或 引 用
- 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种 类 类型 的 转 换
3️⃣ 显示转换
✈️旧版本的强制转换
虽然是旧版本,但很实用。
int a = 10;
double b = 3.14;
int c = a + (int)b;
//向(int)b,在想要进行转换的对象前用括号添加数据类型,或者这样写也行:int(b)
✈️命名的强制类型转換
一个命名的强制类型转换具有如下形式:
const-name< type> (expression)
其中,type 是转换的目标类型而expression 是要转换的值。如果type 是引用类型,则结果是左值。cast-name 是 static_cast
、dynamic_cast
、const_cast
和 reinterpret_cast
中的一种。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
int a = 10;
double b = 3.14;
//int c = a + (int)b; 可以改成
int c = a + static_cast<int>(b);
//将double类型改为int类型
使用这个的好处:让编译器知道我们要对它进行强制类型转换,编译器就不会报警告,之前的可能会报警告说精度会丢失。
static_cast 对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用 static_cast 找回存在于void *指 针中 的 值 :
void *p = &d; //正确:任何非常量对象的地址都能存入void*
double *pd = static_cast<double*>(p);
//正确:将void* 转换回初始的指针类型
const_cast
是用来改变常量的属性的,比如:去掉const属性
const_cast只能改变运算对象的底层const:
const char *pd;
char *p = const_cast<char*>(pd); //正确:但是通过p写值是未定义的行为
对于将常量对象转换成非常量对象的行为,我们一般称其为
去掉const 性质
,一旦我们去掉了某个对象的const 性质,编译器就不再阻止我们对该对象进 行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。 然而如果对象是一个常量,再使用const_cast 执行写操作就会产生未定义的后果。
\qquad 只有const_cast 能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast 改变表达式 的类型:
const char *pd;
char *p = static_cast<char*>(pd);
//错误:static_cast 不能转换掉const性质,他只能改变类型
static_cast<string> (pd); //正确:宇符串宇面值转换成string类型
const_cast<string>(pd); //錯误:const_cast 只改变常量属性,不改变类型
reinterpret_cast
reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。举个例子,假设有如下的转换:
int *p;
char *pc = reinterpret_cast<char *>(ip);
我们必须年记pc 所指的真实对象是一个int 而非字符,如果把pc 当成普通的字符指针 使用就可能在运行时发生错误。例如:
string str (pc) ;
可能导致异常的运行时行为。
\qquad 使用reinterpret_cast 是非常危险的,用pc 初始化str的例子很好地证明了这 一点。 其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。 当我们用一个int 的地址初始化pc 时,由于显式地声称这种转换合法,所以编译器不会 发出任何警告或错误信息。接下来再使用pc 时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int 的指针。最终的结果就是,在上面的例子中虽然用pc 初 始化str 没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。查找这类问题的原因非常困难,如果将 ip 强制转换成pc 的语句和用 pc 初始 化string 对象的语句分属不同文件就更是如此。
十二、运算符优先级表