文章目录
1.算数操作符 + - * / %
对于 + - * 就不多阐述了。
对于 /(除)操作符,若 / 有一边是浮点型,则会发生算数转换结果也是浮点型;当两边为整型时,计算结果向靠近0的方向取整
%(取模)操作符要求两边均为整型
printf("%d\n", 5 % 2); //1
printf("%f\n", 5 / 2.5);//2.000000
printf("%d\n", 5 / 2); //2——> 2.5向0的方向取整为2
printf("%d\n", -5 / 2); //-2——> -2.5向0的方向取整为-2
2.移位操作符<< >>
移位操作符的操作数只能是整数,是对其补码的二进制位进行操作。
首先我们知道一个整数在内存中是以二进制的补码形式存储的。
正整数的原码反码补码相同是其本身,负整数的反码补码需要进行转换
左移操作符<<
规则:左边丢弃,右边补零
int a = 10;
a = a << 1;//20
左移几位,就是乘以2的几次方
右移操作符>>
右移操作符分为两种:
逻辑右移:左边补零,右边丢弃
算数右移:左边补符号位,右边丢弃
一般编译器中,右移是指算数右移
int a = -10;
a = a >> 1;//-5
由于-1的二进制反码均为1,所以-1算数右移几位,结果都是-1,而逻辑右移则会成为一个很大的正数
在vs2019中是算数右移
3.位操作符& | ^
位操作符有:
&
(按位与)|
(按位或)^
(按位异或)
位操作符也是对二进制的补码进行操作
按位与&
二进制位只要存在一个0,该位
&
的结果就是0,同时为1该位&
的结果才为1
int num = 10 & 3;
按位或 |
二进制位只要存在一个1,该位
|
的结果就是1,同时为0该位|
的结果才为0
int num = 10 | 3;
按位异或 ^
二进制位相同为0,不同为1
int num = 10 ^ 3;
仔细思考可以得出:一个数异或其本身结果为0,任意一个数异或0结果还是其本身
练习题:
不创建临时变量来实现两个数的交换(这两个数可能会很大)
可以用过两个变量相加再相减实现交换
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a + b;
b = a - b;
a = a - b;
printf("a = %d b = %d\n", a, b);
return 0;
}
但是注意题目中的提示,假如这两个数都很大,那么相加,最终很有可能存不下这么大的数据,最后导致数据丢失,造成结果不准确的后果。下面这种方法可以保证不会造成数据丢失。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a = %d b = %d\n", a, b);
return 0;
}
4.赋值操作符
赋值操作符=
可以改变变量的值
int a = 10;
int b = 7;
b = 250;
a = b + 1;
复合赋值符:
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
a += 10;
a = a + 10;
a >>= 1;
a = a >> 1;
这两种意思是一样的,其他复合赋值符也是同样的用法。
5.单目操作符
! 逻辑反操作
- 负值
+ 正值
& 取地址
sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
! 逻辑反操作符
非零(真)变为0(假),0(假)变为1(真)
sizeof操作符
sizeof 后面可以加括号也可以不加,sizeof不是函数而是一个运算符
前置和后置++、–
前置++:先+1, 再使用
后置++:先使用,再+1
--
同理
*解引用操作符
int a = 10;
int* pa = &a;
printf("%d\n", a);
printf("%d\n", *pa);
pa存放a的地址,*(pa)就是通过地址访问其中的内容,也就是a
强制类型转换
int a = 10;
double b = (double)a;
将int类型的a强制类型转换成double类型,再赋值给b
6.关系操作符
>
>=
<
<=
!=
==
注:=
和 ==
一定要区分清楚,千万不要写错
表达式成立则为1(真),不成立则为0(假)
7.逻辑操作符&& ||
逻辑与&& :两边都为真则为真,一边为假则为假
逻辑或||:一边为真则为真,两边都为假则为假
逻辑与&&
#include<stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3;
i = a++ && ++b && c++;
printf("%d %d %d %d %d\n", a, b, c, d, i);
return 0;
}
a b c i 结果分别为1 2 3 0
结果分析:在执行a++时,后置++先使用再+1,所以a++的结果为0(假)。逻辑与&&只要有一边为假则为假,所以不管后面的表达式的真假,都不用再往后执行了,因为没有意义,所以程序只执行了a++,a的值+1,整个表达式为假,所以i 为0(假)。
逻辑或||
#include<stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3;
i = a++ || ++b || c++;
printf("%d %d %d %d %d\n", a, b, c, d, i);
return 0;
}
a b c i 结果分别为1 3 3 1
结果分析:执行a++的结果为0(假),再执行++b,结果为3(真)。逻辑或||只要有一边为真则为真,所以不管后面的表达式的真假,都不用再往后执行了,整个表达式为真,所以i 为1(真)。
8.条件操作符
条件操作符是C语言中唯一的三目运算符,优先级高于赋值运算符,但是低于关系运算符和算术运算符。
表达式1 ? 表达式2 : 表达式3
表达式1为真则执行表达式2,表达式1为假则执行表达式3,执行后的结果就是整个条件表达式的值。
求两个数最大值
int a = 1, b = 2;
int c = a > b ? a : b;
printf("%d\n", c);
9.逗号表达式
逗号表达式,就是用逗号隔开的多个表达式。从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
表达式1, 表达式2, 表达式3,...表达式N
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//a = 12, b = 13, c = 13
10.下标引用、函数调用和结构成员
[]
下标引用操作符
操作数:数组名 和 索引值
int arr[10];
arr[0] = 7;
//也可以这样写,但一般不这样写,只是演示这种写法也正确
0[arr] = 7;
()
函数调用操作符
这里不再详细说明,详情请移步另一篇文章C语言函数详解
接受一个或者多个操作数,第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include<stdio.h>
void test1()
{
printf("hello\n");
}
void test2(char* str)
{
printf("%s\n", str);
}
int main()
{
char arr[] = "abcd";
test1();
test2(arr);
return 0;
}
.
和->
访问结构体成员
.
结构体.成员名
->
结构体指针->成员名
#include<stdio.h>
struct S
{
int a;
int b;
};
int main()
{
struct S s;
struct S* ps = &s;
s.a = 10;
s.b = 20;
ps->a = 100;
ps->b = 200;
return 0;
}
11.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
在C 语言中,类型转换的方式一般可分为隐式类型转换和显示类型转换(也称为强制类型转换)。其中隐式类型转换只由编译器自动进行,不需要程序员干预。
隐式类型转换通常有两种情况: 赋值转换和运算转换
C语言中,隐式类型转换遵循以下规则:
1.在对变量赋值时,若等号两边的数据类型不同,需要把右边表达式的类型转换为左边变量的类型,这可能导致降低精度,丢失的部分按四舍五入向前舍入。
2.若参与运算变量的类型不同,则先转换成同一类型,然后进行运算。
3.**转换按照数据长度增加的方向进行,以保证精度不降低。**比如 int 类型的数据和 double 类型的数据相加时,int 类型的数据就会被隐式地转换成 double 类型,然后再进行运算。
4.若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型
5.char类型和short类型参与运算时,必须先转换成int类型。
整型提升
C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的char类型和short类型操作数在使用之前被转换为int类型,这种转换称为整型提升。
整型提升:有符号补符号位,无符号则补 0。
示例一:
int main()
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c);
return 0;
}
打印结果是 -126
示例二:
#include<stdio.h>
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
前面提到:char类型和short类型参与运算时,必须先转换成int型。
示例三:
#include <stdio.h>
int main()
{
char a = 1;
printf("%zd\n", sizeof(a)); // 1
printf("%zd\n", sizeof(+a)); // 4
printf("%zd\n", sizeof(-a)); // 4
// char 类型的变量 a 作为 +/- 的操作数参与运算时,需要进行整型提升
return 0;
}
a参与了+ -操作符的运算,整型提升成int类型
算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
从上往下,等级依次降低,转换按照数据长度增加的方向进行,比如int和float类型进行运算,int类型会转换成float类型再进行运算。
注:算数转换可能会存在一些潜在问题,比如会有精度丢失
double pi = 3.14;
int num = pi; //3
示例:
这是因为sizeof的返回值类型是unsigned int,int
类型的num隐式转换成unsigned int
无符号整型,最高位不是符号位。
-1的原码:10000000 00000000 00000000 00000001 即2^31-1 要远比4大
CPU寄存器的比特位是统一的,将内存中的数据放入寄存器中就会发生隐式转换。
c语言的操作符对多个操作数进行操作时,必须保证其类型一致。
操作符的属性
复杂表达式的求值有三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
两个相邻的操作符先执行优先级高的。如果两者的优先级相同,取决于他们的结合性。
操作符优先级从上往下依次递减,重点记住以下这几个:
操作符 | 描述 | 用法示例 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 聚组 | (表达式) | 与表达式相同 | N/A | 否 |
[ ] | 下标引用 | rexp[rexp] | rexp | L-R | 否 |
. | 访问结构成员 | lexp.member_name | rexp | L-R | 否 |
-> | 访问结构指针成员 | lexp->member_name | rexp | L-R | 否 |
++ | 后缀自增 | lexp ++ | rexp | L-R | 否 |
– | 后缀自减 | lexp - - | rexp | L-R | 否 |
! | 逻辑反 | ! rexp | rexp | R-L | 否 |
~ | 按位取反 | ~ rexp | rexp | R-L | 否 |
++ | 前缀自增 | ++ lexp | rexp | R-L | 否 |
- - | 前缀自减 | - - lexp | rexp | R-L | 否 |
* | 间接访问 | * rexp | lexp | R-L | 否 |
示例:
int* p1[10] = { 0 }; //指针数组
int (*p2)[10] = { 0 };//数组指针
因为[]
的优先级比*
的优先级高,所以p1先与[ ]结合,是数组类型,再与 *\ 结合,所以是指针数组,数组中有10个元素,每个元素是int*
类型。
而()
的优先级最高,所以p2先与*结合,是指针类型,再与[ ]结合,所以是数组指针,这个指针指向一个数组,指向的数组有10个元素,每个元素是int
类型。
详细内容等后面指针篇再整理。
问题表达式:
c + --c;
操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,在不同编译环境下结果不同。
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
同上,这种具有歧义的表达式在不同编译环境下的结果不同,这段代码中的第一个 +在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的,我们要避免写出这种问题表达式。