目录
1.操作符分类
2.算术操作符
3.移位操作符
4.位操作符
5.赋值操作符
6.单目操作符
6.1 单目操作符
7.关系操作符
8.逻辑操作符
9.条件操作符
10. 逗号表达式
11. 下标引用、函数调用和结构成员
11.1 下标引用操作符
11.2 函数调用操作符
11.3 成员访问操作符
12. 表达式求值
12.1 隐式类型转换(整型提升)
12.2 算术转换
12.3 操作符的属性
- 算术操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
算术操作符:+ - * / %
注意:
- 除号两端都是整数时是整数除法,除号两端有小数有才能进行小数除法
- 取模操作符不能作用与浮点数
- 移位操作符的操作数只能是整数;
- 移位移动的是整数的二进制位
整数的二进制表示形式有三种:原码,反码,补码
原码:按照数值的正负,直接写出的二进制序列就是原码
(一个整数有4个字节=32bit位,对于有符号整数,最高位的一位是符号位,符号位为1表示负数,符号位为0表示负数。对于无符号整数(unsigned int),所有位都是有效位,没有符号位)
注意:对于正的整数,原码反码补码相同;对于负的整数,原码反码补码需要计算
反码:源码符号位不变,其他位按位取反
补码:反码的二进制+1就是补码
(原码取反加一得到补码;补码减一取反,或取反加一得到原码)(计算方式相同——只需要一套硬件电路即可)
(0一般作为无符号数)
注意:整数在内存中存的都是补码的二进制序列,整数在计算中使用的也是补码
#include
int main() {
int m = 7;
int n = m << 1;//m不会改变
printf("%d\n", n);
return 0;
}
注意:对于移位运算符,不要移动负数位,这个是标准未定义的。
& :按位与,按二进制位与,有0则0,全1才1;(a&1:得到a的最低位,移位后可以得到a的每一位)| :按位或,按二进制位或,有1则1,全0才0;^ :按位异或,相同为0,相异为1;(异或支持交换律)(注意:a^a=0;a^0=a)注:他们的操作数必须是整数。
面试题:不能创建临时变量(第三个变量),实现两个数的交换
//法一:
int main() {
int a = 3;
int b = 5;
a = a + b;
b = a - b;
a = a - b;
//可以实现两个数的交换,但当a,b的值太大时,a+b会超过整数能表达的最大值,所以a+b不会是a与b的和
return 0;
}
// 法二:代入法
int main() {
int a = 3;
int b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("%d %d", a, b);
return 0;
}
//异或不会产生进位,则不会溢出
// 只适用于整型,可读性差,一般还是创建临时变量实现两个变量的值交换,效率较高
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
#include
//思路:按位与1+右移(作业)
//方法一:
//00000000000000000000000000001010
int main() {
int m = 0;
printf("请输入一个数:\n");
scanf("%d", &m);
int count = 0;
for (int i = 0; i < 32; i++) {
if (m & 1)
count++;
m >>= 1;
}
//此处也可以:
//for (int i = 0; i < 32; i++) {
// if ((m >> i) & 1)//注意:error:((m >>= i) & 1)
// count++;
//}
printf("%d\n", count);
return 0;
}
//方法二:十进制数%10/10可以得到每一位,那么二进制序列可以%2/2得到每一位
int count_num_of_1(unsigned int n) {//注意:如果用该方法需要使用无符号形参
int count = 0;
while (n) {
if (n % 2 == 1) {
count++;
}
n /= 2;
}
return count;
}
int main() {
int num = 0;
printf("请输入一个数:\n");
scanf("%d", &num);
int ret = count_num_of_1(num);
printf("%d\n", ret);
return 0;
}
//方法三:(重要)
int count_num_of_1(int n) {//注意:如果用该方法需要使用无符号形参
int count = 0;
while (n) {
n = n & (n - 1);
//该表达式每一次执行,n最右边的一个1会变为0,执行的次数就是n的个数
count++;
}
return count;
}
int main() {
int num = 0;
printf("请输入一个数:\n");
scanf("%d", &num);
int ret = count_num_of_1(num);
printf("%d\n", ret);
return 0;
}
注意:赋值是重新给一个值
int a = 1;//是初始化,不是赋值
a = 2;//是赋值
赋值操作符可以连续使用,但是不易调试,一步就跳过
复合赋值符
+= ,-= ,*= ,/= ,%= ,>>= ,<<= ,&= ,|= ,^=
- ! 逻辑反操作
- - 负值
- + 正值(注意:正号不改变符号)
- & 取地址(注意:数组地址的表示:int(*pa)[10] = &arr;)
- sizeof 操作数的类型长度(以字节为单位)
//sizeof是计算类型创建变量的大小或者变量的大小,单位是字节 //sizeof计算的结果是size_t类型的 //size_t是无符号整型的 //对size_t类型的数据进行打印,可以使用%zd //对于不支持%zd的编译器可以使用%d或者%u int main(){ int a = 10; //a的类型是整型,可以用sizeof计算其类型创建变量的大小也可以计算a的大小 printf("%zd\n",sizeof(a)); printf("%zd\n",sizeof a); //sizeof计算变量的类型大小时,变量可以不加() //说明sizeof是操作符而不是函数,不需要函数调用操作符 printf("%zd\n",sizeof(int)); //sizeof计算数组大小 int arr[10] = {0}; printf("%zd\n",sizeof(arr)); printf("%zd\n",sizeof(arr[0])); printf("%zd\n",sizeof(arr) / sizeof(arr[0]));//元素个数 return 0; }
- ~ 对一个数的二进制按位取反(0按位取反的结果是-1)
#include
//~的用途 int main() { int a = 10; //a在计算机中的存储:00000000000000000000000000001010 //将a的倒数第五位变为1得到:00000000000000000000000000011010 // 按位或00000000000000000000000000010000(1) a |= (1 << 4); printf("%d\n", a); //将a还原:按位与1111111111111111111111111111111111101111;由(1)按位取反得到 a &= ~(1 << 4); printf("%d\n", a); return 0; } //常用于单片机开发,嵌入式开发 - -- 前置、后置--
- ++ 前置、后置++(注意:无论前置还是后置,操作数都会进行自减/自增操作,但是操作数赋给其他变量时,前置后置的结果不同——前置:先变化再赋值;后置:先赋值再变化)
- * 间接访问操作符(解引用操作符)
//解引用操作符和取地址操作符是一对,通常搭配使用 int main(){ int m = 1; int* p = &m; *p;//对p进行解引用操作,*p是通过p中存放的地址,找到p指向的对象,*p就是a return 0; }
- (类型) 强制类型转换
<< >> & | ^ ~ 都是针对二进制位的操作
注意:
#include
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)、(2)两个地方分别输出多少? // 40,8 //(3)、(4)两个地方分别输出多少? // 10,8 // 注意:数组传参的本质传的是首元素的地址
>>=<<=!= 用于测试 “ 不相等 ”== 用于测试 “ 相等 ”
&& 逻辑与|| 逻辑或
1 & 2 -----> 01 && 2 ----> 11 | 2 -----> 31 || 2 ----> 1
注意:
&& 属于短路操作,即如果第一个操作数是 false ,那么就不会再对第二个操作数求值。
||也是短路操作符,如果第一个操作数的求值结果为 true ,就不会对第二个操作数求值了。
#include
int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ && ++b && d++; //i = a++||++b||d++; printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d); printf("i = %d\n", i); return 0; } //程序输出的结果是什么? //1 2 3 4 //0
exp1 ? exp2 : exp3真 计算 不计算假 不计算 计算
#include
//计算a,b的较大值
int main() {
int a = 10;
int b = 30;
int m = 0;
/*if (a > b) {
m = a;
}
else
m = b;*/
m = (a > b ? a : b);
printf("%d\n", m);
return 0;
}
逗号表达式,就是用逗号隔开的多个表达式。逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
前面的表达式可能会影响到最后一个表达式
int a = 1; int b = 2; int c = (a>b, a=b+10, a, b=a+1);//逗号表达式 //c:13
a = get_val(); count_val(a); while (a > 0) { //业务处理 a = get_val(); count_val(a); } //如果使用逗号表达式,改写: while (a = get_val(), count_val(a), a>0) { //业务处理 } //代码不在冗余,整个表达式从左向右以此计算,但起到作用的还是最后一个表达式的结果
#include
int main()
{
int arr[] = {1,2,(3,4),5};
printf("%d\n", sizeof(arr));
return 0;
}
//(3,4)逗号表达式的结果是最后一个表达式的结果:4
//16
#include
int main()
{
int a, b, c;
a = 5;
c = ++a;
b = ++c, c++, ++a, a++;
//b = ++c是逗号表达式中的一个表达式,逗号操作符优先级低于赋值操作符
b += a++ + c;
printf("a = %d b = %d c = %d\n:", a, b, c);
return 0;
}
操作数:一个数组名 + 一个索引值(下标)
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
至少为一个参数,函数可以无参,但必须有函数名
. 结构体.成员名(有变量)-> 结构体指针->成员名(有地址)
#include
//成员访问操作符
struct Book {
char name[20];
int price;
};
void Print(struct Book* pb) {
printf("%s %d\n", (*pb).name, (*pb).price);
printf("%s %d\n", pb->name, pb->price);
}
int main() {
struct Book b = { "c语言指南",55 };
printf("%s %d\n", b.name, b.price);
Print(&b);
return 0;
}
C 的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为 整型 提升 。
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int(默认为有符号)或unsigned,然后才能送入CPU去执行运算。注意:char到底是signed char还是unsigned int是不确定的,c语言标准没有明确规定,是取决于编译器的
整形提升是按照变量的数据类型的符号位(当前二进制序列的最高位)来提升的无符号数整型提升,高位补0
#include
int main() {
char a = 5;
//5的补码:00000000000000000000000000000101
//存入char类型中:00000101
char b = 126;
//126的补码:00000000000000000000000001111110
//存入char类型中:01111110
char c = a + b;
//a整型提升:00000000000000000000000000000101
//b整型提升:00000000000000000000000001111110
//加法运算c:00000000000000000000000010000011
//存入c中:10000011
printf("%d\n", c);
//按整型打印
//c整型提升:11111111111111111111111110000011——补码
//c的原码: 10000000000000000000000001111101——取反加一
//-125(01111111——127)
return 0;
}
实例二:
int main()
{
char a = 0xb6;//10110110——整型提升
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0; }
实例三:
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
//整型提升的前提:字符型或短整型进行整型运算(有操作符:+-*/%...)或打印
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换 。
long doubledoublefloatunsigned long intlong intunsigned intint
注意:向上转换
复杂表达式的求值有三个影响的因素。1. 操作符的优先级2. 操作符的结合性3. 是否控制求值顺序。
C语言运算符优先级:
优先级 |
运算符 |
名称或含义 |
使用形式 |
结合方向 |
说明 |
是否控制求值顺序 |
1 |
[] |
数组下标 |
数组名[常量表达式] |
左到右 |
-- |
否 |
() |
圆括号 |
(表达式)/函数名(形参表) |
-- |
否 | ||
. |
成员选择(对象) |
对象.成员名 |
-- |
否 | ||
-> |
成员选择(指针) |
对象指针->成员名 |
-- |
否 | ||
2 |
++ |
自增运算符 |
变量名++(后缀) |
左到右 | 单目运算符 |
否 |
-- |
自减运算符 |
变量名-- |
||||
- |
负号运算符 |
-表达式 |
右到左 |
|||
~ |
按位取反运算符 |
~表达式 |
||||
++ |
自增运算符 |
++变量名(前缀) |
||||
-- |
自减运算符 |
--变量名 |
||||
* |
取值运算符 |
*指针变量 |
||||
& |
取地址运算符 |
&变量名 |
||||
! |
逻辑非运算符 |
!表达式 |
||||
(类型) |
强制类型转换 |
(数据类型)表达式 |
-- |
否 | ||
sizeof |
长度运算符 |
sizeof(表达式) |
-- |
否 | ||
3 |
/ |
除 |
表达式/表达式 |
左到右 |
双目运算符 |
否 |
* |
乘 |
表达式*表达式 |
||||
% |
余数(取模) |
整型表达式%整型表达式 |
||||
4 |
+ |
加 |
表达式+表达式 |
左到右 |
双目运算符 |
否 |
- |
减 |
表达式-表达式 |
||||
5 |
<< |
左移 |
变量<<表达式 |
左到右 |
双目运算符 |
否 |
>> |
右移 |
变量>>表达式 |
||||
6 |
> |
大于 |
表达式>表达式 |
左到右 |
双目运算符 |
否 |
>= |
大于等于 |
表达式>=表达式 |
||||
< |
小于 |
表达式<表达式 |
||||
<= |
小于等于 |
表达式<=表达式 |
||||
7 |
== |
等于 |
表达式==表达式 |
左到右 |
双目运算符 |
否 |
!= |
不等于 |
表达式!= 表达式 |
||||
8 |
& |
按位与 |
表达式&表达式 |
左到右 |
双目运算符 |
否 |
9 |
^ |
按位异或 |
表达式^表达式 |
左到右 |
双目运算符 |
否 |
10 |
| |
按位或 |
表达式|表达式 |
左到右 |
双目运算符 |
否 |
11 |
&& |
逻辑与 |
表达式&&表达式 |
左到右 |
双目运算符 |
是 |
12 |
|| |
逻辑或 |
表达式||表达式 |
左到右 |
双目运算符 |
是 |
13 |
?: |
条件运算符 |
表达式1? 表达式2: 表达式3 |
右到左 |
三目运算符 |
是 |
14 |
= |
赋值运算符 |
变量=表达式 |
右到左 |
-- |
否 |
/= |
除后赋值 |
变量/=表达式 |
-- |
否 | ||
*= |
乘后赋值 |
变量*=表达式 |
-- |
否 | ||
%= |
取模后赋值 |
变量%=表达式 |
-- |
否 | ||
+= |
加后赋值 |
变量+=表达式 |
-- |
否 | ||
-= |
减后赋值 |
变量-=表达式 |
-- |
否 | ||
<<= |
左移后赋值 |
变量<<=表达式 |
-- |
否 | ||
>>= |
右移后赋值 |
变量>>=表达式 |
-- |
否 | ||
&= |
按位与后赋值 |
变量&=表达式 |
-- |
否 | ||
^= |
按位异或后赋值 |
变量^=表达式 |
-- |
否 | ||
|= |
按位或后赋值 |
变量|=表达式 |
-- |
否 | ||
15 |
, |
逗号运算符 |
表达式,表达式,… |
左到右 |
-- |
是 |
只有四个操作符可以控制关系式的求值顺序
根据操作符的优先级无法确定表达式的唯一计算路径
问题代码:
eg:a*b+c*d+m*n(尽量拆开,如果每个变量各自为表达式其中又用到相同的变量,那么计算路径将会对结果产生影响)
eg:c + --c(有问题,加号的左操作数什么时候准备会对结果有影响)
eg:i = i-- - --i * ( i = -3 ) * i++ + ++i(不同编译器有不同结果)
eg:三个函数的调用顺序不同,结果不同
int fun()//2,3,4... { static int count = 1; return ++count; } int main() { int answer; answer = fun() - fun() * fun(); printf( "%d\n", answer);//输出多少? return 0; }
#include
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
//尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。
// vs:12;linux:10
注意:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。