1 操作符分类
1.1 引言
操作符是一种告诉编译器执行特定的数学或逻辑操作的符号。经常也能见到有运算符的说法,有些资料上会将运算符定义为数学运算(+、-、*、÷),将其他类似赋值(=)、逻辑运算(&&、||)等符号定义为操作符,实际上没有必要做如此的区分,操作符和运算符就是一个含义。
1.2 操作符分类
(1)算术操作符:+(加)、-(减)、(乘)、/(除)、%(取余)
(2)逻辑操作符:&&(逻辑与)、||(逻辑或)、!(逻辑非)
(3)比较操作符:>(大于)、<(小于)、>=(大于等于)、<=(小于等于)、==(等于)、!=(不等于)
(4)位操作符:&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)
(5)赋值操作符:=、+=、-=、=、/=、%=、<<=、>>=、&=、|=、^=
(6)其他操作符:?:(三目操作符)、,(逗号操作符)、sizeof(大小操作符)、[](下标引用操作符)、()(函数调用操作符)、&(取地址操作符)、*(解引用操作符)、.和->(结构成员操作符)、++(自增操作符)、–(自减操作符)、(new、delete)(内存操作)、(static_cast、dynamic_cast、const_cast、reinterpret_cast)(四种类型转换操作符)
1.3 算术操作符
如下为算术操作符的样例(假设变量 a 的值为 100,变量 b 的值为 80,两个变量都是 int 类型):
操作符 | 描述 | 结果 |
---|---|---|
+ | 把两个操作数相加 | a+b 返回 180 |
- | 从第一个操作数中减去第二个操作数 | a-b 返回 20 |
* | 把两个操作数相乘 | a* b 返回 8000 |
/ | 第一个操作数除以第二个操作数 | a/b 返回 1 |
% | 取余操作符,整除后的余数 | a%b 返回 20 |
重点关注
(1)取模与取余
取模运算与取余运算的区别如下:
假设有两个整数,a=-9 和 b=6。
求商(整数)c : c = a/b
计算模或者余数 d: d = a - c*b
不同之处在第一步,在计算c的值时
取模向负无穷方向舍入 (对应 floor() 函数)。此时 c=-2。
取余向 0 方向舍入 (对应 fix() 函数)。此时 c=-1。
所以,根据 c 的不同,取模的结果 d=3 ,取余的结果 d=-3 。
总结来说:当 a 和 b 符号一致时,取模运算和求余运算所得结果一致。当a和b符号不一致时,结果不一样。取模运算结果的符号和 b 一致,取余运算结果的符号和 a 一致。
C++ 的 % 操作符默认为取余运算。
int a = -9, b = 6;
int c = a / b;
int d = a % b;
该代码段执行后 d 的值为 -3 。
(2)超出范围的运算
超出数据范围的计算实际上是做循环计算,即 最大值+1 = 最小值,最小值+1 = 最大值。
以 int 为例,其他类型的行为类似。
代码:
#include <iostream>
#include <climits>
int main(int argc, char *argv[])
{
printf("INT_MAX = %d\n", INT_MAX);
printf("INT_MIN = %d\n", INT_MIN);
int val1 = INT_MAX;
val1 += 1;
printf("INT_MAX + 1 = %d\n",val1);
int val2 = INT_MIN;
val2 -= 1;
printf("INT_MIN - 1 = %d\n", val2);
return 0;
}
输出结果如下:
INT_MAX = 2147483647
INT_MIN = -2147483648
INT_MAX + 1 = -2147483648
INT_MIN - 1 = 2147483647
(3)浮点型计算
由于浮点数存入时可能发生数据截断,因此超过有效位数的浮点数进行加减运算是没有意义的。
float val1 = 1234567.12345f;
float val2 = 1234567.12344f;
if (val1 > val2)
{
printf("val1 > val2\n");
}
else
{
printf("val1 <= val2\n");
}
上述代码的输出为:
val1 <= val2
这是由于 val1 与 val2 在读入时都会被截断为 1234567.13000。从而导致最后的计算结果与预期不同。
1.4 逻辑操作符
如下为算术操作符的样例(假设变量 a 的值为 true,变量 b 的值为 false,两个变量都是 bool 类型):
操作符 | 描述 | 结果 |
---|---|---|
&& | 逻辑与操作符:如果两个操作数都为真 ,则条件为真。 | a&&b 返回 false |
|| | 逻辑或操作符:如果两个操作数中有任意一个假 ,则条件为真。 | a||b 返回 true |
! | 逻辑非操作符:逆转操作数的逻辑状态。如果条件为真则逻辑非操作符将使其为假。 | !a 返回 false |
重点关注
(1)短路法则
|| 从左向右开始计算:
当遇到为真的条件时停止计算,整个表达式为真。
所有条件为假时表达式才为假。
&& 从左向右开始计算:
当遇到为假的条件时停止计算,整个表达式为假。
所有条件为真时表达式才为真。
逻辑表达式中,&& 比 || 具有更高的优先级。
(2)隐式转换
在执行逻辑操作时,操作数被隐式转换为 bool 类型。
1.5 比较操作符
如下为比较操作符的样例(假设变量 a 的值为 100,变量 b 的值为 80,两个变量都是 int 类型):
操作符 | 描述 | 结果 |
---|---|---|
== | 相等操作符 | a==b 返回 false |
!= | 不等操作符 | a!=b 返回 true |
> | 大于操作符 | a>b 返回 true |
< | 小于操作符 | a<b 返回 false |
>= | 大于等于操作符 | a>=b 返回 true |
<= | 小于等于操作符 | a<=b 返回 false |
重点关注
判断两个浮点数是否相等:
由于浮点数存入时可能发生数据截断造成精度损失,所以两个浮点数直接用操作符进行比较很可能会得到不符合预期的结果。
浮点数的比较应该使用如下方式:
对于浮点数而言比较合适的精度为:0.000001
对于双进度浮点数而言比较合适的精度为:0.0000000000000001
因此可以定义两个宏:
#define ACCURACY_F 1e-6
#define ACCURACY_D 1e-16
判断浮点数是否等于 0 :
float 类型:if(fabs(f) <= ACCURACY_F );
double 类型:if(fabs(d) <= ACCURACY_D);
判断两个浮点数是否相等:
float 类型:if(fabs(f1 - f2) <= ACCURACY_F);
double 类型:if(fabs(d1 - d2) <= ACCURACY_D);
1.6 位操作符
如下为比较操作符的样例(假设变量 a 的二进制值为 01100100(十进制值位100),变量 b 的二进制值为 01010000(十进制值位为80),两个变量都是 uint8_t 类型):
操作符 | 描述 | 结果 |
---|---|---|
& | 与操作符 | a&b 返回 01000000(十进制值位64) |
| | 或操作符 | a|b 返回 01110100(十进制值位116) |
^ | 异或操作符 | a^b 返回 00110100(十进制值位52) |
~ | 补码操作符 | ~a 返回 10011011(十进制值位155) |
<< | 左移操作符 | a<<1 返回 11001000(十进制值位200) |
>> | 右移操作符 | a>>1 返回 00110010(十进制值位50) |
重点关注
(1)位操作符可以提高计算性能
位运算可以快速地进行一些算数运算,例如位移运算可以代替整数的乘除运算、按位异或可以代替加减、按位取反可以代替整数相反数的计算。
(2)位操作符可以做标志符叠加
假设要用很多布尔标志表示一个数据库连接对象的特征,是否加密,是否压缩等。通常情况是定义很多个布尔变量,要判断是否全部条件满足的时候, 需要很多个逻辑与,这样就比较复杂(同时因为需要很多 if else ,效率也比较低)。
位操作符可以使用一个 int 变量来叠加表示多种状态,如下:
// 标志位
#define CONNEECTION_ENCRYPTION 0x1 // 加密
#define CONNEECTION_COMPRESS 0x2 // 压缩
int flags=0;
flages & CONNEECTION_ENCRYPTION // 检查是否加密
flages |= CONNEECTION_COMPRESS // 设置压缩
flages &= ~(CONNEECTION_ENCRYPTION | CONNEECTION_COMPRESS) // 除去加密、压缩
1.7 赋值操作符
用于给变量赋值。
操作符 | 描述 | 结果 |
---|---|---|
= | 赋值操作符 | a = 2 : 把 2 赋给 a |
+= | 加后赋值操作符 | a+=2 : a=a+2 |
-= | 减后赋值操作符 | a-=2 : a=a-2 |
*=` | 乘后赋值操作符 | a*=2 : a=a*2 |
/=` | 除后赋值操作符 | a/=`2 : a=a/2 |
%= | 求余后赋值操作符 | a%=2 : a=a%2 |
<<=` | 左移后赋值操作符 | a<<=2 : a=a<<2 |
>>=` | 右移后赋值操作符 | a>>=2 : a=a>>2 |
&= | 按位与后赋值操作符 | a&=2 : a=a&2 |
^= | 按位异或后赋值操作符 | a^=2 : a=a^2 |
|= | 按位或后赋值操作符 | a|=2 : a=a|2 |
重点关注
不可以在定义变量时对变量进行连续赋值。如下代码所示:
int a = b = 2;
这是由于"="操作符是从右至左结合,先把 2 赋值给 b ,但此时 b 还没有定义,违反了 C++ 语法先定义后使用原则。
1.8 其他操作符
1.8.1 ?:(三目操作符)
语法规则:
condition ? expression1 : expression2;
其中,condition为条件表达式,expression1为满足条件时执行的表达式,expression2为不满足条件时执行的表达式。条件表达式的结果为真(非零值)时,执行expression1;否则,执行expression2。
三目操作符主要应用于简化 if else 语句:
int a = 1;
int b = 2;
int max = (a > b) ? a : b; //max 的值为 2
该语句相当于如下 if else 语句:
int a = 1;
int b = 2;
int max = 0;
if(a > b)
{
max = a;
}
else
{
max = b;
}
1.8.2 ,(逗号操作符)
常见于同时定义多个变量:
int a=1,b=2;
逗号操作符的实际含义是:将多个表达式串联在一起,这些表达式将从左到右依次执行。最后一个表达式的值将作为整个逗号表达式的值返回。
所以如下的操作是合法的:
int a = 1, b = 2;
int c = (a++, b++);
最后 c 的值为 3 。
1.8.3 sizeof(大小操作符)
sizeof 作用是返回一个对象或者类型所占的内存字节数。如下:
int len = sizeof(int);
该语句执行后,len 的值为 4 。
如下为使用 sizeof 需要注意的点:
(1)sizeof 不能求得 void 类型的长度
sizeof(void) 将导致编译错误:‘void’: illegal sizeof operand。
(2)sizeof 可以求得 void 类型的指针的长度
void* tmp=nullptr;
int len = sizeof(tmp);
上述代码在 32 位程序中,len 的值为 4 。在 64 位程序中,len 的值为 8 。
(3)sizeof 可以求得静态分配内存的数组的长度
char t[10] = { 0 };
int len = sizeof(t);
该语句执行后,len 的值为 10 。
需要注意的是,数组如果作为函数的参数传入,此时 sizeof 的计算会得到非预期的结果:
void testFunc(char t[10])
{
int len = sizeof(t);
}
在 32 位程序中,该函数调用时,len 的值为 4 。
(4)sizeof 不能求得动态分配内存的数组的长度
char* t = new char[10];
int len = sizeof(t);
在 32 位程序中,该语句执行后,len 的值为 4 (实际上就是指针 t 的大小)。
(5)sizeof 与 strlen 的区别
- sizeof 是算符,strlen是函数。
- strlen(char*) 函数求的是字符串的实际长度,直到遇到第一个 ‘\0’ ,然后就返回计数值,且不包括 ‘\0’ 。而 sizeof() 函数返回的是变量声明后所占的内存数,不是实际长度。
- sizeof 可以用类型或函数做参数,strlen 只能用 char* 做参数,且必须是以 ‘\0’ 结尾的。
(6)sizeof 计算结构体大小
如下面的数据结构:
struct s1{
int a;
char b;
int c;
};
用sizeof求该结构体的大小,发现值为 12 ,而非 4+1+4 = 9。原因是:当操作数是结构类型时,sizeof 是将所有成员字节对齐后的长度之和。
字节对齐的细节和编译器实现相关,但一般满足以下三个准则:
结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节。
结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节。
1.8.4 [](下标引用操作符)
用于获取一个数组中某索引位置的元素。
int vals[10] = { 1,2,3,4,5 };//定义数组
printf("数组 vals 的第1个元素为:%d\n", arr[0]); //通过下标操作符获取第 1 个元素
printf("数组 vals 的第2个元素为:%d\n", arr[1]); //通过下标操作符获取第 2 个元素
1.8.5 ()(函数调用操作符)
注:该部分内容涉及到 C++ 中类的相关知识。
函数调用操作符是一种特殊的成员函数,它使得一个对象可以像函数一样被调用。这种特性让 C++ 具有极高的灵活性和表达能力。如下为样例代码:
class MyFunc {
public:
void operator()() {
std::cout << "hello" << std::endl;
}
};
MyFunc func;
func(); // 输出 "hello"
func(); // 输出 "hello"
函数调用操作符经常用于标准模板库的自定义算法中。例如,在 std::sort 或 std::for_each 等算法中,可以传入一个仿函数作为自定义的比较或操作函数。如下为样例代码:
#include <algorithm>
#include <vector>
#include <iostream>
class MyCompare {
public:
bool operator()(int a, int b) {
return a < b;
}
};
int main() {
std::vector<int> vals = {1, 6, 8, 12, 2};
std::sort(vals.begin(), vals.end(), MyCompare());
for (const auto& val : vals) {
std::cout << val << " ";
}
return 0;
}
上面代码的输出为:
1 2 6 8 12
1.8.6 &(取地址操作符)
取地址操作符作用于内存中一个可寻址的数据(如变量、对象和数组元素等等),操作的结果是获得该数据的地址。如下:
using namespace std;
int x, y;
void f1() {};
void f2() {};
int main()
{
int a, b, c;
cout << "局部变量的地址:" << endl;
cout << &a << " " << &b << " " << &c << endl;
static int z;
cout << "全局(局部静态)变量的地址:" << endl;
cout << &x << " " << &y << " " << &z << endl;
cout << "函数代码的地址:" << endl;
cout << &main << " " << &f1 << " " << &f2 << endl;
return 0;
}
上面代码的输出为:
局部变量的地址:
000000285E0FF7E4 000000285E0FF804 000000285E0FF824
全局(局部静态)变量的地址:
00007FF6994B61F0 00007FF6994B61F4 00007FF6994B6244
函数代码的地址:
00007FF6993C2BDF 00007FF6993C5736 00007FF6993C573B
由程序运行结果可以看出:
- 局部变量与全局的地址相互相差很大,说明被分配在了不同的内存空间(局部变量在栈区,全局变量在全局数据区)。
- 全局变量和局部静态变量的地址相邻,说明二者被安排在了同一分区。
1.8.7 *(解引用操作符)
以如下代码为例:
#include<stdio.h>
int main()
{
int a = 1;
printf("before : a=%d\n", a);
int* p = &a;
*p = 2;
printf("after : a=%d\n", a);
return 0;
}
上面代码的输出为:
before : a=1
after : a=2
上面代码中的 *
p = 2; 这里的*
是解引用操作符,目的是间接访问 p 的地址即 a ,并将 a 的大小改成 2。
1.8.8 .和->
(结构成员操作符)
注:该部分内容涉及到 C++ 中类的相关知识。
以如下代码为例:
class MyObj {
public:
string getName()
{
return m_name;
}
private:
string m_name;
};
MyObj obj;
obj.getName(); //使用 . 结构成员操作符
MyObj* pObj = new MyObj;
pObj->getName(); //使用 -> 结构成员操作符
1.8.9 ++(自增操作符)与–(自减操作符)
这两个操作符关注的重点是操作符是前置还是后置,以具体代码为例:
#include<stdio.h>
int main()
{
int a = 1;
int b = a++; //先执行 b=a; 再执行 a=a+1
int c = ++a; //先执行 a=a+1; 再执行 c=a
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
}
上面代码的输出为:
3
1
3
1.8.10 (new、delete)(内存操作)
new 和 delete 是 C++ 用于管理堆内存的两个操作符。
new 操作符的内部实现分为两步:
第一步:内存分配
调用相应的 operator new(size_t) 函数,动态分配内存。如果 operator new(size_t) 不能成功获得内存,则调用 new_handler() 函数用于处理 new 失败问题。如果没有设置 new_handler() 函数或者 new_handler() 未能分配足够内存,则抛出 std::bad_alloc 异常。new 操作符所调用的 operator new(size_t) 函数,按照C++的名字查找规则,首先做依赖于实参的名字查找(即 ADL 规则),在要申请内存的数据类型 T 的内部(成员函数)、数据类型 T 定义处的命名空间查找;如果没有查找到,则直接调用全局的 ::operator new(size_t) 函数。
第二步:构造函数
在分配到的动态内存块上初始化相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用 operator delete(void*
, void*
) 函数释放已经分配到的内存。
delete 操作符的内部实现分为两步:
第一步:析构函数
调用相应类型的析构函数,处理类内部可能涉及的资源释放。
第二步:内存释放
调用相应的 operator delete(void *
) 函数。调用顺序参考上述 operator new(size_t) 函数( ADL 规则)。
注意 new 和 malloc 的区别
- new/delete 是操作符。malloc/free 属于库函数,需要头文件(stdlib.h)支持。
- new 申请内存无需指定内存大小,编译器会根据类型信息自行计算。除此之外,new会调用构造函数。malloc 需要指出所需内存的尺寸,并且其并不能对所得的内存进行初始化,所以得到的一片新内存中,值是随机的。
- new 操作符内存分配成功时,返回的是对象类型的指针,无须进行类型转换,故 new 是符合类型安全性的操作符。而 malloc 内存分配成功则是返回 void
*
,需要通过强制类型转换将 void*
指针转换成我们需要的类型。 - new 内存分配失败的时候,抛出 bad_alloc 异常 ;malloc 分配内存失败时返回 NULL 。
- C++允许重载 new/delete 操作符,而 malloc 是库函数不允许重载。
1.8.11 类型转换操作符
static_cast:用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用。该操作符不能用于两个不相关的类型进行转换。
int a = 22;
int b = static_cast<int>(a); //OK
int* c = &a;
int d = static_cast<int>(c); //错误:不能用于两个不相关的类型进行转换。
reinterpret_cast:通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型。
int a = 22;
int b = static_cast<int>(a); //OK
int* c = &a;
int d = reinterpret_cast<int>(c); //OK
const_cast:最常用的就是删除变量的 const 属性,方便赋值。
#include<stdio.h>
int main()
{
const int a = 1;
int* b = const_cast<int*>(&a);
*b = 2;
printf("%d\n", a);
printf("%d\n", *b);
}
上面代码的输出为:
1
2
a 是 const 修饰的常量,放在内存的数据段,不会被修改。b 去取 a 时会把 a 放进缓存或寄存器(内存的另一个空间),b 指向这段空间,b 把这段空间的值修改成2,最后打印 a 取的是内存的数据段里的1,*
b 打印的是寄存器里的2。
dynamic_cast:用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)
向上转型:子类对象指针/引用->
父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->
子类指针/引用(用 dynamic_cast 转型是安全的)
注意:
- dynamic_cast只能用于父类含有虚函数的类。
- dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回 nullptr 。
- dynamic_cast使用前提必须是多态,父类中必须有虚函数,无虚函数不可用。
2 操作符优先级
C++ 一共有 16 个优先级,运算中按优先级进行性计算,当优先级相同时,根据结合性规则来决定。
结合性:
1.从左到右(L-R):大部分是从左到右结合性的,例如()、单独的算术运算符
2.从右到左(R-L):最典型的是赋值运算符,当赋值符号与算术运算符结合后 ,整体也是 R-L 。
在使用的时候,如果不确定,或者运算符太多,尽量使用括号做分隔。
2.1 优先级1
操作符 | 描述 | 结合性 |
---|---|---|
() | 括号操作符 | 从左到右 |
[] | 下标操作符 | 从左到右 |
-> | 结构成员操作符(通过指针访问) | 从左到右 |
. | 结构成员操作符(通过对象访问) | 从左到右 |
:: | 作用域操作符 | 从左到右 |
++ | 后置自增操作符(如:i++) | 从左到右 |
– | 后置自减操作符(如:i–) | 从左到右 |
2.2 优先级2
操作符 | 描述 | 结合性 |
---|---|---|
! | 逻辑取反操作符 | 从左到右 |
~ | 按位取反操作符 | 从左到右 |
++ | 前置自增操作符(如:++i) | 从左到右 |
– | 前置自减操作符(如:–i) | 从左到右 |
- | 一元取负操作符 | 从左到右 |
+ | 一元取正操作符 | 从左到右 |
* | 解引用操作符 | 从左到右 |
& | 取地址操作符 | 从左到右 |
cast | 类型转换操作符 | 从左到右 |
sizeof | 取大小操作符 | 从左到右 |
2.3 优先级3
操作符 | 描述 | 结合性 |
---|---|---|
->* | 结构成员操作符(通过指针访问)(如:a->* m1=0) | 从左到右 |
.* | 结构成员操作符(通过对象访问)(如:a.* m1=0) | 从左到右 |
2.4 优先级4
操作符 | 描述 | 结合性 |
---|---|---|
* | 乘法操作符 | 从左到右 |
/ | 除法操作符 | 从左到右 |
`% | 求余操作符 | 从左到右 |
2.5 优先级5
操作符 | 描述 | 结合性 |
---|---|---|
+ | 加法操作符 | 从左到右 |
- | 减法操作符 | 从左到右 |
2.6 优先级6
操作符 | 描述 | 结合性 |
---|---|---|
<< | 按位左移操作符 | 从左到右 |
>> | 按位右移操作符 | 从左到右 |
2.7 优先级7
操作符 | 描述 | 结合性 |
---|---|---|
< | 小于比较操作符 | 从左到右 |
<= | 小于或等于比较操作符 | 从左到右 |
> | 大于比较操作符 | 从左到右 |
>= | 大于或等于比较操作符 | 从左到右 |
2.8 优先级8
操作符 | 描述 | 结合性 |
---|---|---|
== | 等于比较操作符 | 从左到右 |
!= | 不等于比较操作符 | 从左到右 |
2.9 优先级9
操作符 | 描述 | 结合性 |
---|---|---|
& | 按位与操作符 | 从左到右 |
2.10 优先级10
操作符 | 描述 | 结合性 |
---|---|---|
^ | 按位异或操作符 | 从左到右 |
2.11 优先级11
操作符 | 描述 | 结合性 |
---|---|---|
| | 按位或操作符 | 从左到右 |
2.12 优先级12
操作符 | 描述 | 结合性 |
---|---|---|
&& | 逻辑与操作符 | 从左到右 |
2.13 优先级13
操作符 | 描述 | 结合性 |
---|---|---|
|| | 逻辑或操作符 | 从左到右 |
2.14 优先级14
操作符 | 描述 | 结合性 |
---|---|---|
?: | 三元条件操作符 | 从右到左 |
2.15 优先级15
操作符 | 描述 | 结合性 |
---|---|---|
= | 赋值操作符 | 从右到左 |
+= | 加后赋值操作符 | 从右到左 |
-= | 减后赋值操作符 | 从右到左 |
*= | 乘后赋值操作符 | 从右到左 |
/= | 除后赋值操作符 | 从右到左 |
%= | 求余后赋值操作符 | 从右到左 |
<<= | 左移后赋值操作符 | 从右到左 |
>>= | 右移后赋值操作符 | 从右到左 |
&= | 按位与后赋值操作符 | 从右到左 |
^= | 按位异或后赋值操作符 | 从右到左 |
|= | 按位或后赋值操作符 | 从右到左 |
2.16 优先级16
操作符 | 描述 | 结合性 |
---|---|---|
, | 逗号操作符 | 从右到左 |