目录
1.操作符分类
- 算数操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
2.算术操作符
+ - * / %
- 除了%操作符之外,其他的几个操作符可以作用于整数和浮点数,计算的是整除之后的余数,比如,5%2,得到的结果是1,%取模操作符的两端必须是整数
- 对于 / 操作符,如果两个操作数都为整数,执行整数除法。并且只要有浮点数执行的就是浮点数除法,举个例子,5/2的结果是2
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数
3.移位操作符
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数,并左移和右移操作符移动的都是二进制位
数值可以用多种进制表示,进制只是一种表示形式而已
2进制
0~1
8进制
0~7
10进制
0~9
整数的二进制表示有三种
- 原码
- 反码
- 补码
正整数的原码、反码、补码相同
负整数的原码、反码、补码:
- 原码:先写它对应的正整数的原码,然后将符号位(第一位为符号位,符号位为0是正整数,符号位为0是负整数)改为1,就是它的原码
- 反码:符号位不变,其他位按位取反
- 补码:反码+1
整数在内存中存储的是补码
用printf打印的是原码的值
1.左移操作符:左边丢弃,右边补0(左移有×2的效果)
这里的移动不会改变a本来的值
整数占 4个字节=32个比特位
2.右移操作符:
- 算术移位:右边丢弃,左边补原符号位
- 逻辑移位:右边丢弃,左边补0
是算术移位还是逻辑移位取决于编译器,多数编译器进行的是算术移位
注:
对于移位操作符,不要移动负数位,这个是标准未定义的,比如,a<<-1,这样的写法是错误的
4.位操作符
& //按(二进制)位与
| //按(二进制)位或
^ //按(二进制)位异或
注:它们的操作数必须是整数
&:对应的二进制位,两个同时为1,才为1(对补码进行操作)
%d 意味着打印一个有符号的整数
| :对应的二进制位只要有1,就为1 ;两个同时为0,才为0;
^:对应的二进制位,相同为0,相异为1
一道面试题:
不能创建临时变量(第三个变量),实现两个数的交换
//方法1:
这个方法能实现两个数的交换,但是存在缺点,因为a和b都是在整型,当a和b的数太大时会发生栈溢出的现象
//方法2:这种方法不会出现栈溢出的现象,但是按位异或的方法只适用于整数,浮点数是没法进行按位异或
a^a=0;
a^1=a;
异或操作符支持交换律
但是在实际的开发中,多数还是会选择创建临时变量来交换两个数,多创建一个变量不会占用很大的空间,并且执行效率是最快的
5.赋值操作符
赋值操作符可以给变量重新赋值
#include<stdio.>
int main()
{
int a=3;//这里是初始化
a=5;//这里是赋值
}
赋值操作符可以连续使用,
#include<stdio.h> int main() { int a=10; int x=0; int y=20; a=x=y+1;//连续赋值 }
连续赋值是允许,但是我们一般不会选择连续赋值这种使用方法,不便于调试,在调试的过程不便于观察值的变化,比如上面的代码x的值就不便于观察,所以多数时候选择分开写更好
复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果
6.单目操作符
单目操作符是指:只有一个操作数的
整形占四个字节,取地址,取的是四个字节中的第一个字节的地址
sizeof操作符
#include<stdio.h> int main() { int a = 10; int n = sizeof(int); printf("%d\n", n); return 0; }
sizeof是一个操作符
计算的是变量所占内存空间的大小,单位是字节
计算类型所创建的变量占据空间的大小,单位是字节
sizeof()里面放数组名时,计算的是整个数组的大小
注意:
sizeof(a);
sizeof a;//这两种写法是一样,对计算a的大小没有影响
//这个()可以删去说明sizeof是操作符不是函数
但是
sizeof(int);//这里的()是不能删去的
strlen也是用来求字符串长度的,但是strlen是库函数
问下面这段代码(1)(2)(3)(4)分别的打印的是什么(这里有陷阱)
#include<stdio.h> void test1(int arr[]) { printf("%d\n",sizeof(arr));//(2) } 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));//(3) test1(arr); test2(ch); return 0; }
(1)40 (2)4 (3)10 (4)
这里的char ch[ ]和int arr [ ]都是指针,指针的大小都是4/8个字节
~ 按位取反,对二进制位进行取反,也是对补码进行操作
想要将二进制位中某个数由0变为1,由1变为0
前置++、后置++(++和--是一样的)
#include<stdio.h> int main() { int a=3; int b=++a; printf("%d\n",a);//4 printf("%d\n",b);//4 return 0; }
#include<stdio.h> int main() { int a=3; int b=a++; printf("%d\n",a);//4 printf("%d\n",b);//3 return 0; }
前置++,先++,后使用
后置++,先使用,再++
#include<stdio.h> int main() { int a=10; printf("%d\n",a--);//10 printf("%d\n",a);//9 return 0; }
for循环中前置和后置区别不大(控制循环变量的i)
*(解引用操作符)和指针一起用
#include<stdio.h> int main() { int a=10; int*p=&a;//这里的*告诉我们这个p是指针变量 *p=20;//*是解引用,通过p里存的a的地址找到它所指向的内容,将a的值改为20 // 相当于a=20 printf("%d\n",a);//20 }
(类型) 强制类型转换
这个其实我们之前就已经遇到,在猜数字游戏中,产生随机数就有遇到强制类型转换
#include<stdio.h> int main() { srand((unsigned int)time(NULL));//这里将time强制类型转换成unsigned int //这里time返回的类型是time_t,而srand函数的参数是unsigned int类型,所以需要强制类转换 }
#include<stdio.h> int main() { int a=(int)3,14f; printf("%d\n",a);//3 return 0; }
这里如果没有强制类型转换,编译器会默认转换为double类型
7.关系操作符
>
>=
<
<=
!= 用于测试”不相等“
== 用于测试“相等”
在编程的过程中==和=不小心写错,导致的错误
不是所有的东西都可以用==比较相不相等的
比如,字符串的比较应该使用库函数strcmp进行比较
"abc"=="abcdef"//这样比较的是两个字符串的首字符的地址
8.逻辑操作符
&& 逻辑与 //两边同时为真才是真
| | 逻辑或 //两边只要有一个是真就是真
逻辑操作符只关注真假
区分逻辑与和按位与
区分逻辑或和按位或
1&2------->0
1&&2----->1
1 | 2------>3
1 | | 2---->1
#include<stdio.h> int main() { int i=0,a=0,b=2,c=3,d=4; i = a++ && ++b && d++; printf("a=%d\n b=%d\n c=%d\n d=%d\n",a,b,c,d); return 0; }
问上面这段代码的结果是?
//1 2 3 4
a++先使用再++,此次a=0,此条式子为假,不再继续进行(右边的式子不再进行),然后a开始++
&& 左边为假,右边就不计算了
| | 左边为真,右边就不计算了
9.条件操作符
exp1?exp2:exp3
条件操作符又称为三目操作符(有三个操作数)
1.if ( a>5 )
b=3;
else
b=-3;
上面的代码改写为条件操作符的形式
b=( ( a>5 )? 3 : -3 );
2.使用条件表达式实现找两个数中的较大值
a > b ? a : b ;
10.逗号表达式
expt,exp2,exp3,...expN
逗号表达式,就是用逗号隔开的多个表达式
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果
11.下标引用操作符、函数调用操作符和结构成员
1)[ ] 下标引用操作符
操作数:一个数组名+一个索引值
int arr [10];//创建数组
arr[10]-->*(arr+10)-->*(10+arr)-->10[arr]
访问的时候可以这么写,但是定义的时候不能这么写
//arr是数组首元素的地址
//arr+10就是跳过10个元素,指向了第11个元素
//*(arr+10)就是第11个元素
arr [ 9 ];//使用下标引用操作符
[ ]的两个操作数是arr和9
//arr [9] 也可以写成 9 [arr]
2) 函数调用操作符
int Add(int x,int y)
{
return x+y;
}
int main()
{
int a=10;
int b=20;
int c=Add(a,b);//这里的()就是函数调用操作符,这个()不能省略掉
//操作数:Add,a,b,操作数至少是一个
}
3)结构成员
//不能将字符串赋给一个地址,name是数组名,代表首元素地址,所以不能将字符串直接赋给数组,应该使用库函数strcpy,将字符串拷贝放到name所指向的空间中
#include<stdio.h>
#include<string.h>
struct Stu
{
char name[20];
int age;
double score;
};
void set_stu(struct Stu* ps)
{
//strcpy(ss.name,"ming");
//ss.age=19;
//ss.score=100.0;
//这里只是临时拷贝,不能更改实参,写成指针的形式
//strcpy((*ps).name,"ming");
//(*ps).age=20;
//(*ps).score=100.0;
//但是上面这种写法还是有一些冗余
strcpy(ps->name,"ming");
ps->age=19;
ps->score=100.0;
}
void print_stu(struct Stu ss)
{
printf("%s %d %f\n",ss.name,ss.age,ss.score);
}
int main()
{
struct Stu s={0};
set_stu(&s);
print_stu(s);
return 0;
}
//s传给ss时,实参传给形参,形参只是实参一个临时拷贝并不会改变实参的内容,所以应该用指针struct Stu*ss
结构体指针->成员
结构体对象.成员
ps->age等价于(*ps).age
12.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型
12.1隐式类型转换
C的整型算术运算总是以缺省(默认)整型类型的精度来进行的
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
整型提升的意义:
如何进行整型提升?
整型提升是按照变量的数据类型的符号来提升的
整型提升的实例
字符类型也是整型家族,字符在存储的时候存储的是ASCII码值,只要是整形在内存中存储就是补码
这里的a和b都要发生整型提升,所以只能打印出c
c只要参与表达式运算,就会发生整型提升,表达式+c,就会发生提升,所以sizeof(+c)是4个字节,表达式-c也会发生整型提升,所以sizeof(-c)是4个字节,sizeof(c)没有参与表达式运算,所以计算的结果就是1个字节,虽然这里的+c,-c什么都没有干,但是参与了表达式的运算,就要发生整型提升
12.2算术转换
如果莫格操作数的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作数就无法进行。下面的层次体系称为寻常算术转换
long double
double
float
ussigned long int
long int
unsigned int
int
类型向上转换,向大的类型转换
当int和unsigned int相遇时,int转换为unsigned int
unsigned int 转换为long int
long int转换为 unsigned long int
....
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另一个操作数的类型后执行运算
警告:
算术转换要合理,不然会有一些潜在问题
12.3 操作符的属性
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
从上到下,优先级由高到低
N/A是没有结合性
L-R是从右向左结合
R-L是从左向右结合
一些问题表达式
//表达式的求值部分由操作符的优先级决定
//代码1
a*b+c*d+e*f
这里只是知道 * 的优先级高于 + , * 的计算比 + ,早,但是并不能决定第二个 * 比第一个 * 先执行
举个例子
3+2*4+5;
相邻的两个运算符只能说明+和*中*先计算 ,但是前面的+和后面的+谁先运算,优先级说了不算,优先级一定是相邻间的两个操作符,结合性是相邻操作符的优先级相同的前提下,这个时候是结合性说了算
如果我们已经确定里各种操作符的优先级,结合性和是否控制求值顺序这些属性,写出一个表达式并不一定计算表达式的唯一值
a*b+c*d+e*f这个表达式的计算顺序就可能是:
a*b c*d a*b+c*d e*f 最后a*b+c*d+e*f
这个计算顺序是不确定的,谁先执行可能会引起后面值得变化,这样得代码存在潜在问题
//代码2
c + --c;
注释:同上,操作符得优先级只能决定自减 - - 得运算在+得前面,但是我们并没有变法得知,+操作符得左操作符的获取在右操作数之前还是右操作数之后求值,所以结果是不可预测的,这里有歧义
c什么时候准备好的,这个是不知道的,比如,现在c=2,c可能进行--就准备好了,+前的c是2,然后进行--,c=1;然后再相加,此时表达式的结果是3
另外一种情况,c是--之后才准备好的,此时表达式的结果是2
//代码3-非法表达式
#include<stdio.h> int main() { int i=10; i=i-- - --i*(i=-3)*i++ + ++i; printf("i=%d\n",i); return 0; }
这段代码在不同的编译器下测试结果是不同的
//代码4
//输出结果是?
#include<stdio.h> int fun() { static int count = 1; return ++count; } int main() { int answer; answer = fun() - fun() * fun(); printf("%d\n",answer); return 0; }
我们只能确定*和-中*是先进行运算的,但是没办法确定fun函数哪个先调用
函数的调用顺序无法通过操作符的优先级确定
//代码5
#include<stdio.h> int main() { int i=1; int ret =(++i)+(++i)+(++i); printf("%d\n",ret); printf("%d\n",i); return 0; }
这段代码也是,在不同的编译器下,有不同结果
代码五转反汇编
ebp是寄存器
ebp中存放的是地址
ebp-8也是地址
把1放到ebp-8这块地址中,其实就是放到a中
eax、ebx、ecx、edx是寄存器
上面这段代码的第一个+在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+和第三个前置 ++的先后顺序
总结:我们写的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达就是存在问题的