「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
前言:
本章将对C语言操作符进行深度的讲解,将每种操作符都单独拿出来精讲。
一、算术操作符
0x00 概览
📌 注意事项:
① 除了 % 操作符之外,其他的几个操作符都可以作用于整数和浮点数;
② 对于 / 操作符,如果两个操作数 都为整数 ,执行整数除法;
③ 对于 / 操作符,只要有浮点数出现 ,执行的就是浮点数除法;
④ 对于 % 操作符的两个数 必须为整数;
0x01 整数除法
📚 定义:对于 / 操作数,如果两个操作数都为整数,执行整数除法;
❓ 整数除法:即一个整数除以另一个整数结果为只保留整数;
💬 代码演示:
int main()
{
int a = 5 / 2; // 5÷2 = 商2余1
printf("a = %d\n", a); // 👈 输出的结果是什么?
return 0;
}
🚩 运行结果: a = 2
0x02 浮点数除法
📚 定义:只要有浮点数出现,执行的就是浮点数除法;
❓ 浮点数除法:结果会保留小数部分( 给定对应的%前提下 );
💬 代码演示:
int main()
{
double a = 5 / 2.0; // 5÷2 = 2.5,有1个浮点数,条件就成立,执行浮点数除法
printf("a = %lf\n", a); // 👈 输出的结果是什么?
return 0;
}
🚩 运行结果: a = 2.500000
0x03 取模操作符
📚 定义:取模运算即 求两个数相除的余数 ,两个操作数必须为非0整数;
📌 注意事项:
① 两个操作数必须为整数;
② 两个操作数均不能为0(没有意义);
💬 代码演示:
int main()
{
int a = 996 % 10; // 996 mod 10 = 6
int b = 996 % 100; // 996 mod 100 = 96
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
🚩 运行结果:6 96
❌ 错误演示:
int main()
{
double a = 5 % 2.0; // ❌ 操作数必须为整数
printf("a = %lf\n", a);
return 0;
}
🚩 运行结果:error: invalid operands to binary % (have 'int' and 'double')
int main()
{
int a = 2 % 0; // ❌ 操作数不能为0
printf("%d\n", a);
return 0;
}
🚩 运行结果:warning: division by zero [-Wdiv-by-zero]
0x04 整除和浮点除的区分
💬 代码演示:我们想得到 1.2
int main()
{
int a = 6 / 5;
printf("%d\n", a);
return 0;
}
🚩 运行结果: 1 ( 但是运行结果为1 )
❓ 难道是因为我们用的是 %d 打印的原因吗?
int main()
{
float a = 6 / 5;
printf("%f\n", a);
return 0;
}
🚩 运行结果: 1.000000 ( 仍然不是想要的1.2,运行结果为1.000000 )
(气急败坏,无能狂怒)
💡 解析:其实问题不在于存到a里能不能放的下小数的问题,而是 6 / 5 得到的结果已经是为1了(执行的是整除);
🔑 解决方案:把6改成6.0,或把5改成5.0,也可以都改,让它执行浮点数除法;
int main()
{
float a = 6 / 5.0;
printf("%f\n", a);
return 0;
}
🚩 运行结果: 1.200000
❓ 虽然代码可以运行,但是编译器报了一个 warning,让我们来瞅瞅是咋回事:
🔑 解析:直接写出的这个数字(6.0或5.0),编译器会默认认为它是 double 类型
那么计算后a的结果也会是 double 类型(双精度浮点数);
如果双精度浮点数的值放到一个单精度浮点数里的话,可能会丢失精度,
好心的编译器就发出了这样的一个警告,这个是正常的;
💡 如果你不想看到这样的警告,你可以这么做:
int main()
{
float a = 6.0f / 5.0f; // 👈 “钦定” 为float单精度浮点数
printf("%f\n", a);
return 0;
}
int main()
{
double a = 6.0 / 5.0; // 👈 改成double
printf("%lf\n", a);
return 0;
}
📚 关于精度丢失的现象:
① 有效数字位数超过7位的时候,将会四舍五入,会丢失较多精度;
② 在运行较大数值运算的时候,将有可能产生溢出,得到错误的结果;
二、移位操作符
0x00 概览
📚 概念: 移位操作符分为 "左移操作符" 和 "右移操作符" ;
📌 注意事项:
① 移位操作符的 操作数必须为整数;
② 对于运算符,切勿移动负数位(这是标准为定义的行为);
③ 左移操作符有乘2的效果,右移操作符有除2的效果(左乘2,右除2);
0x01 左移操作符
📚 移位规则:左边丢弃,右边补0 ;(左边的数给👴爬,至于爬多远,还要看操作数是多少)
💬 代码演示:
int main()
{
int a = 2;
int b = a << 1; // 将a的二进制位向左移动1位;
printf("b = %d\n", b); // 4 (左移操作符有乘2的效果)
/*
00000000000000000000000000000010
0|000000000000000000000000000010+0 (左边丢弃,右边补0)
*/
return (0);
}
🚩 运行结果: b = 4
🔑 图解左移操作符:
0x02 右移操作符
📚 移位规则:两种移位规则;
① 算术右移:右边丢弃,左边补原符号位(通常为算术右移);
② 逻辑右移:右边丢弃,左边补0;
📌 注意事项:
① C编译器中默认为算术右移,如果是 signed 有符号类型时,需要注意;
② 使用 unsigned 无符号类型时,算术右移和逻辑右移的结果是一样的;
int main()
{
int a = 10;
int b = a >> 1; // 把a的二进制位向右移动一位
printf("b = %d\n", b); // 5 (右移操作符有除2的效果)
/*
00000000000000000000000000001010
0+0000000000000000000000000000101|0
*/
return 0;
}
🚩 运行结果: b = 5
🔑 解析: 为了搞懂什么是算术右移,什么是逻辑右移,我们不得不了解整数的二进制表示方式:
0x03 整数的二进制表示方式(初步了解)
📚 负数-1要存放在内存中,内存中存放的是二进制的补码;
📌 整数的二进制表示形式(原反补):
① 原码:直接根据数值写出的二进制序列,即为原码;
② 反码:原码的符号位不变,其他位置按位取反,即为反码(如果不知道什么是按位取反,后面会讲);
③ 补码:反码 + 1,即为补码; (内存中存放的是补码)
📜 -1 的原码、反码、补码:
💬 此时回到上述问题,如果右移时采用逻辑右移:
int main()
{
int a = -1;
int b = a >> 1;
printf("b = %d\n", b);
return 0;
}
🚩 运行结果: b = -1
🔑 图解逻辑右移与算数右移:
❌ 错误演示:操作数不能是负数!
int main()
{
int num = 10;
num >> -1; // ❌ a<<1 ?? 垃圾代码
return 0;
}
🚩 运行结果: warning: right shift count is negative [-Wshift-count-negative]
三、位操作符
0x00 概览
📚 位操作符:按位与、按位或、按位异或;
📌 注意事项:位操作符的 操作数必须为整数;
0x01 按位与 &
📚 定义:按2进制按位与,只有对应的两个二进位都为1时,结果位才为1;(必须都为真,结果才为真)
💬 代码演示:按位与的用法
int main()
{
int a = 3;
int b = 5;
int c = a & b; // a和b都为真
printf("%d", c);
return 0;
}
🚩 运行结果: 1
0x02 按位或
📚 定义:只要对应的两个二进位有一个为1时,结果位就为1;(只要有一个为真,结果就为真)
💬 代码演示:按位或的用法
int main()
{
int a = 0;
int b = 5;
int c = a | b; // a和b有一个为真
printf("%d\n", c);
return 0;
}
🚩 运行结果: 5
int main()
{
int a = 0;
int b = 0;
int c = a | b; // a和b都为假
printf("%d\n", c);
return 0;
}
🚩 运行结果: 0
0x03 按位异或 ^
📚 定义:相同为0,相异为1;(上下相同就为假,不同为真)
💡 巧记:觉得按位异或不好记? 试着这么记 👇
" 这对恋人是异性恋吗?是回1,不是回0 " 0 1 是, 1 0 是, 1 1 不是, 0 0 不是;
※ 异或:a⊕b = (¬a ∧ b) ∨ (a ∧¬b) 如果a、b两个值不相同,则异或结果为1,反之结果为0;
💬 代码演示:按位异或的用法
int main()
{
int a = 3;
int b = 5;
int c = a ^ b; // a和b不同
printf("%d\n", c);
return 0;
}
🚩 运行结果: 6
int main()
{
int a = 3;
int b = 3;
int c = a ^ b; // a和b相同
printf("%d\n", c);
return 0;
}
🚩 运行结果: 0
0x04 位操作符的应用
📃 面试题:交换两个 int 变量的值,不能使用第三个变量;
(即a=3,b=5,交换之后a=5,b=3)
1. 临时变量法 - 该题禁止了此方法,但是在工作中建议使用该方法;
int main()
{
int a = 3;
int b = 5;
printf("交换前: a = %d, b = %d\n", a, b);
int tmp = a; // 创建一个临时变量,存放a
a = b; // a变为b
b = tmp; // b变为原来的a
printf("交换后: a = %d, b = %d\n", a, b);
return 0;
}
🚩 运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3
2. 加减交换法 - 存在缺陷:可能会溢出(超过整型的存储极限)
int main()
{
int a = 3;
int b = 5;
printf("交换前: a = %d, b = %d\n", a, b)
a = a + b;
b = a - b;
a = a - b;
printf("交换后: a = %d, b = %d\n", a, b);
return 0;
}
🚩 运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3
🔑 解析:第一步: 3 + 5 = 8,第二步: 8 - 5 = 3,第三步: 8 - 3 = 5,此时,a = 5, b = 3 ;
3. 异或交换法 - 缺点:可读性差,执行效率低下;
int main()
{
int a = 3;
int b = 5;
printf("交换前: a = %d, b = %d\n", a, b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("交换后: a = %d, b = %d\n", a, b);
return 0;
}
🚩 运行结果: 交换前: a = 3, b = 5;交换后:a=5, b=3
🔑 解析:
💬 编写代码实现:求一个整数存储在内存中的二进制中1的个数
1. 一般解法 - 模除
int main()
{
int num = 0;
int count = 0;
scanf("%d", &num);
/* 统计num的补码中有几个1 */
while(num != 0) {
if(num % 2 == 1) {
count++;
}
num = num / 2;
}
printf("%d\n", count);
return 0;
}
🚩 运行结果: (假设输入3) 2
🔑 解析:
2. 移位操作符 + 按位与 结合的方式解决
💭 思路:
① 利用 for 循环,循环32/64次;
② 每次 if 判断,将 num 右移 i 位的结果与 1 按位与,为真则说明为1,count++;
③ 如果为假,进入下一次循环,最后打印出 count 即可;
int main()
{
int num = 0;
int count = 0;
scanf("%d", &num);
int i = 0;
/* 32位系统,至少循环32次 */
for(i=0; i<32; i++) {
if( ((num >> i) & 1) == 1 ) // 如果num右移i位的结果和1按位与,为真
count++;
}
printf("%d\n", count);
return 0;
}
🚩 运行结果: (假设输入3) 2
四、赋值操作符
0x00 概览
📚 用法:用来重新赋值一个变量的值;
0x01 一般赋值
📚 赋值方法:
💬 赋值操作符是个很棒的操作符,它可以让你得到一个你之前不满意的值:
int main()
{
int weight = 120; // 体重120,不满意,我要变瘦点!
weight = 89; // 不满意就赋值~
double salary = 10000.0; // 我:老板!我要加薪!
salary = 20000.0; // 老板:好的,没有问题!
return 0;
}
0x02 连续赋值
📚 定义:连续赋值(continuous assignment),即一次性赋多个值;
📜 建议:建议不要使用连续赋值,会让代码可读性变差,而且还不容易调试;
💬 代码演示:连续赋值的使用方法;
int main()
{
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;// 👈 连续赋值
x = y+1;
a = x;
// 👆 这样写更加清晰爽朗而且易于调试
return 0;
}
0x03 复合赋值符
📚 意义:复合赋值运算符是为了减少代码输入量而设计的;
📌 注意事项:
① x = x + 10 与 x += 10 的意义等价;
② 可以提高代码的整洁度,让代码更加整洁;
💬 代码演示:复合赋值符的使用方法
int main()
{
int x = 10;
x = x + 10;
x += 10; //复合赋值符的写法 (和上面是等价的)
return 0;
}
五、单目操作符
0x00 概览
❓ 什么是单目操作符?
💡 在运算中只有一个操作数的操作符,叫做单目操作符;
0x01 逻辑反操作 !
📚 作用:可以让真变为假,也可以让假变为真;
💬 逻辑反操作的用法:
int main()
{
int a = 10;
printf("%d\n", !a); // 将真变为假, 0
int b = 0;
printf("%d\n", !b); // 将假变为真, 1
return 0;
}
🚩 运行结果: 0 1
💬 最常用的用法:
int main()
{
int flag = 5;
if ( flag ) // flag != 0 -> hehe
printf("hehe\n"); // flag为真,打印hehe
if ( !flag ) // flag == 0 -> haha
printf("haha\n"); // flag为假,打印haha
return 0;
}
🚩 运行结果: hehe
0x02 负值 -
📚 作用:把一个数置为负数;
💬 负值的用法:
int main()
{
int a = 10;
a = -a; // 在a前面放一个负号
printf("%d", a);
return 0;
}
🚩 运行结果: -10
0x03 正值 +
📚 作用:一般都省略掉了,和数学里面一样;
💬 加号一般都不写的:
int main()
{
int a = +5; // 一般都省略掉了,和数学里一样
printf("%d", a);
return 0;
}
🚩 运行结果: 5
0x04 取地址操作符 & 与 解引用操作符 *
📚 理解:
① 取地址操作符可以理解为取快递;
② 解引用操作符可以理解为拆快递;
(指针章节会详解)
💬 用法演示:
int main()
{
int a = 10;
int* pa = &a; // 取地址操作符 ( 随后将地址存放在int* pa里 )
*pa = 20; // 解引用操作符 通过p里存的值找到它所指向的对象;
// *p就是a, 将*p赋值为20,a就会变为20;
return 0;
}
🔑 解析:
① 首先 int* pa 是一个指针变量(如果不知道什么是指针,可以暂且理解为是一个快递包裹);
② 快递包裹里装的是内存地址,我们使用 取地址操作符& 取出 a 的地址,存放到这个包裹里(int* pa = &a);
③ 这时,我们想修改 a 的值,我们要打开包裹进行修改,可以通过 解引用操作符* 将 a 修改为新的值(*pa = 20);
0x05 操作数的类型长度 sizeof( )
📚 作用:计算变量所占内存空间的大小,单位是字节;
📌 注意事项:
① sizeof 括号中的表达式不参与运算;
② sizeof 本质上不是函数,所以可以省略括号,但是 sizeof 后面是类型时不可以省略括号;
💬 sizeof 的用法:
int main()
{
int a = 10;
char c = 'a';
char* pc = &c;
int arr[10] = {0};
/* sizeof 计算的变量所占内存空间的大小,单位是字节 */
printf("%d\n", sizeof(a)); //4;
printf("%d\n", sizeof(int)); //4;
printf("%d\n", sizeof(c)); //1;
printf("%d\n", sizeof(char)); //1;
printf("%d\n", sizeof(pc)); //4; 32位系统中
printf("%d\n", sizeof(char*)); //4;
printf("%d\n", sizeof(arr)); //40; 4x10=40
printf("%d\n", sizeof( int [10] )); //40;
return 0;
}
💬 下列代码的运行结果为什么?
int main()
{
short s = 0;
int a = 10;
printf("%d\n", sizeof(s = a + 5));
printf("%d\n", s);
}
🚩 运行结果: 2 0
❓ 为什么是 s 还是 0 呢? s = a + 5,s 不应该是 15吗……
🔑 解析:15个🔨15,sizeof 括号中的表达式不参与运算!
💬 下列代码输出后 (1) (2) (3) (4) 分别是多少(32位)?
void test1(int arr[]) //传参传过来的是首元素
{
printf("%d\n", sizeof(arr)); // (3)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch)); // (4)
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%d\n", sizeof(arr)); // (1)
printf("%d\n", sizeof(ch)); // (2)
test1(arr);
test2(ch);
return 0;
}
💡 答案:(1)40 (2)10 (3)4 (4)4
🔑 解析:
① (1) 一个int型大小为4,数组大小为10,4x10 = 40,所以答案为40;
② (3) 一个char型大小为1,数组大小为10,1x10 = 10,所以答案为10;
③ (3) (4) 数组名传参,传过去的虽然是是首元素地址,因为首元素的地址也是地址
所以要拿一个指针来接收它。本质上,arr 和 ch 为指针,而指针的大小,
是4个字节或者8个字节(具体是几个字节看操作系统),题目中为32位,所以答案为4;
❌ 错误示范:
int main()
{
/* sizeof 后面是类型时不可以省略括号 */
int a = 10;
printf("%d\n", sizeof a ); // 可以省略 ✅
printf("%d\n", sizeof int); // error! 不可以省略 ❌
return 0;
}
🚩 运行结果: error: expected expression before 'int' printf("%d\n", sizeof int);
0x06 按位取反 ~
📚 作用:对一个数按位取反,0 变 1, 1 变 0;
📌 注意事项:
① 按位取反,1~0互换,包括符号位;
② 按位取反后,是补码;
💬 巧用按位取反:将某一个数的二进制位从右到左数的第三个数改为1;
int main()
{
int a = 11;
a = a | (1<<2);
// 00000000000000000000000000001011 11
// | 00000000000000000000000000000100 让他和“这个数字”按位或
//-------------------------------------
// 00000000000000000000000000001111 此时这一位变成了1
// 如何创造出“这个数字”呢?
// 1<<2;
// 00000000000000000000000000000001 1
// 00000000000000000000000000000100 把他向左移动两位时1就到这了
// a|(1<<2)
// 00000000000000000000000000001011
// | 00000000000000000000000000000100
//-------------------------------------
// 00000000000000000000000000001111
printf("%d\n", a); //15
a = a & ( ~ (1<<2) );
// 如何再改回去? ↓ 让这一位改成0
// 00000000000000000000000000001111 让他和0按位与
// | 11111111111111111111111111111011 给他按位与一个“这样的数字”
//-------------------------------------
// 00000000000000000000000000001011 把这一位又还原成0了
// 1<<2,同上
// 00000000000000000000000000000100 这个数字按位取反可以得到 ...1011
// ~
// 11111111111111111111111111111011
// a& ~
// 00000000000000000000000000001111 15
// & 11111111111111111111111111111011
//-------------------------------------
// 00000000000000000000000000001011 11
printf("%d\n", a); //11
return 0;
}
🚩 运行结果: 15 11
0x07 前置、后置++
📚 定义:
① 前置++:先加加,后使用;
② 后置++:先使用,再加加;
💬 代码演示:后置++的用法
int main()
{
int a = 10;
printf("%d\n", a++); // 后置++:先使用,再++
printf("%d\n", a); // a此时已变为11
return 0;
}
🚩 运行结果: 10 11
💬 代码演示:前置++的用法
int main()
{
int a = 10;
printf("%d\n", ++a); // 前置++:先++,再使用
printf("%d\n", a);
return 0;
}
🚩 运行结果: 11 11
0x08 前置、后置 --
📚 定义:
① 前置--:先减减,后使用;
② 后置++:先使用,再减减;
💬 代码演示:后置 - - 的用法
int main()
{
int a = 10;
printf("%d\n", a--);
printf("%d\n", a);
return 0;
}
🚩 运行结果: 10 9
💬 代码演示:后置 - - 的用法
int main()
{
int a = 10;
printf("%d\n", --a);
printf("%d\n", a);
return 0;
}
🚩 运行结果: 9 9
0x09 强制类型转换(type)
📚 作用:强制类型转换可以把变量从一种类型转换为另一种数据类型;
📌 注意事项:
💬 代码演示:强制类型转换的用法
int main()
{
int a = (int)3.14;
return 0;
}
参考资料:
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
比特科技. C语言基础[EB/OL]. 2021[2021.8.31]. .
📌 本文作者: 王亦优
📃 更新记录: 2021.6.9
❌ 勘误记录: 无
📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
本章完。