1.操作符分类
- 算数操作符:+ - * / %
- 移位操作符:<< >>
- 位操 作符:& | ^
- 赋值操作符:= += -=……
- 单目操作符:!+- & sizeof ~ ++ -- * (类型)
- 关系操作符:> >= < <= == !=
- 逻辑操作符:& ||
- 条件操作符:?:
- 逗号表达式:exp1,exp2,exp3…expN
- 下标引用、函数调用和结构成员:[] () . ->
2.算数操作符
+ - * / %
/:整型的除法(整数除法) eg 1/2=0 浮点型的除法(小数除法) eg 1.0/2=0.5想要进行浮点数的除法必须保证除法两端操作符至少有一个数是小数,eg 1/2.0,1.0/2.0
%:计算的是整除后的余数 取模操作符两端必须为整数
7%2=1 7/2=3
3.移位操作符
移位操作符与二进制有关,移位操作符移动的是二进制位
进制:二进制,每一位都是0~1数字组成 八进制:0~7 十进制:0~9 十六进制:0~9 A~F
整数的二进制表示有三种:原码 反码 补码;正整数的原码,反码,补码相同 ;负整数原码,反码,补码是要计算的。
eg
//7 111 整型4个字节 32个二进制位(比特位)
原码:00000000 00000000 00000000 00000111 根据二进制序列直接写出来
反码:00000000 00000000 00000000 00000111
补码:00000000 00000000 00000000 00000111
//-7 最高位为0表示正数,为1表示负数
原码:10000000 00000000 00000000 00000111
反码:11111111 11111111 11111111 11111000 最高位的符号位不变 其他位按位取反
补码:11111111 11111111 11111111 11111001 反码+1就是补码
整数在内存中存储的是补码 整数的移位中移位操作符移动的是存在内存中的补码
3.1 左移操作符
左移操作符计算规则:左边丢弃,右边补0
左移操作符的特点:每左移一位就有乘2的效果
//<<左移操作符 将二进制序列向左移动
int a = 7;
int b = a << 1;
printf("a=%d\nb=%d\n",a,b);//a=7 b=14
//分析:00000000 00000000 00000000 00000111 整体向左移动一位,左边丢失一位,右侧补0
//00000000 00000000 00000000 00001110 ——> 十进制:14
注意:a的值在移动前后都不改变,只是将a移位的值赋给b,但a自身不发生变化
//<<左移操作符
int a = -7;
int b = a << 1;
printf("a=%d\nb=%d\n",a,b);//a=7 b=14
//分析:
//11111111 11111111 11111111 11111001 整体向左移动一位,左边丢失一位,右侧补0
//11111111 11111111 11111111 11110010 ——> 十进制:-14
//打印的是原码的值:补码——>原码 先-1 保留最高位的符号位,其他位取反
//-1 11111111 11111111 11111111 11110001 反码
//取反 10000000 00000000 00000000 00001110 原码
//计算可知=> -14
注意:
- 左移有乘2的效果
- 移位操作符的操作数只能是整数 ,不能是浮点数
3.1 右移操作符
右移操作符计算规则:
- 算术移位:右边丢弃,左边补原符号位(正数补0负数补1)
- 逻辑移位:右边丢弃,左边补0
//>>右移操作符 将二进制序列向右移动
//两种
//1.算术移位:右边丢弃,左边补原符号位(正数补0负数补1)
//2.逻辑移位:右边丢弃,左边补0
//
int a = 7;
int b = a >> 1;
printf("a=%d\nb=%d\n",a,b);//a=7 b=3
//分析:00000000 00000000 00000000 00000111 整体向右移一位,右边丢一位
//因为是正数,所以无论是算术还是逻辑移位左边都补0
//00000000 00000000 00000000 00000011——>十进制:3
正数的算术移位和逻辑移位都是一样的,如何区别?
用负数就可以测试出来当前编译器采用的是算术移位还是逻辑移位(算术右移还是逻辑右移取决于编译器 大部分编辑器都采用算术右移)
//>>右移操作符
int a = -7;
int b = a >> 1;
printf("a=%d\nb=%d\n",a,b);//a=-7 b=-4
//分析:11111111 11111111 11111111 11111001 整体向右移一位,右边丢一位 因为是负数,算术移位左边补1,逻辑移位左边补0
//算术移位:11111111 11111111 11111111 11111100
//-1 反码:11111111 11111111 11111111 11111011
//取反 原码:10000000 00000000 00000000 00000100--> 十进制:-4
//逻辑移位:01111111 11111111 11111111 11111100 --> 十进制:很大的一个数
int c = a >> -2;//这种写法不合理 移位操作符的移动位数不能有负数
注意:
- 无符号数右移,理解成正数
- 移位操作符不能移动负数位,这个是标准未定义的行为(编译器不知道怎么处理)
4.位操作符
& | ^
按位与(&)的计算规则:
- 写出两个操作数的二进制的补码
- 让补码进行按位与,对应的二进制位中只要有0,则按位与结果为0;两个同时为1,按位与的结果才为1
//& 按位(二进制位)与
//| 按位(二进制位)或
//^ 按位(二进制位)异或
int a = 3;
int b = -5;
int c = a & b;
printf("c=%d\n",c);//c=3
//分析:整数在内存中存的是二进制的补码 写ab二进制补码,再按位与
//00000000 00000000 00000000 00000011 (正数:原码反码补码相同) 3的补码
//-5的原码:10000000 00000000 00000000 00000101
//取反 反码:11111111 11111111 11111111 11111010
//+1 补码:11111111 11111111 11111111 11111011
//3&-5
//00000000 00000000 00000000 00000011
//&(对应的二进制按位与,两个二进制中如果有0则为0,两个二进制位都为1才为1)
//11111111 11111111 11111111 11111011
//运算结果:00000000 00000000 00000000 00000011 ---> 补码 又是正数 也是原码--->3
//%d 意味着打印一个有符号的整数 对应的值是原码
注意:按位与的计算完的结果是补码(只要存放在内存中,就是补码)
按位或( | )的计算规则:
- 对应二进制位只要有1则为1
- 两个同时为0才为0
// | 按位或
int a = 3;
int b = -5;
int c = a | b;
printf("c=%d\n",c);//c=-5
//分析:
//00000000 00000000 00000000 00000011 3的补码
//11111111 11111111 11111111 11111011 -5的补码
//二进制位进行按位或运算:对应二进制位只要有1则为1 两个同时为0才为0
//运算结果:11111111 11111111 11111111 11111011 补码
//-1 11111111 11111111 11111111 11111010反码
//取反 10000000 00000000 00000000 00000101--->十进制-5
异或(^)的计算规则:相同为0,相异为1
异或(^)的特点:
- a^a=0
- 0^a=a
- 异或操作符支持交换律
// ^ 按位异或
int a = 3;
int b = -5;
int c = a ^ b;
printf("c=%d\n",c);//c=-8
//分析:
//00000000 00000000 00000000 00000011 3的补码
//11111111 11111111 11111111 11111011 -5的补码
//11111111 11111111 11111111 11111000 按位异或的运算结果 补码
//-1 11111111 11111111 11111111 11110111 反码
//取反 10000000 00000000 00000000 00001000 原码 ——>十进制-8
//异或(^)的特点
3^3=0 -----> a^a=0 (相同的两个数字异或结果为0)
011^011=000
0^5=5 -----> 0^a=a
000^101=101
3^3^5=5 3^3=0 0^5=5
--------->异或操作符支持交换律
3^5^3=5
011^101=110 110^011=101
例:不创建临时变量(第三个变量),实现两个数的交换
int a = 3;
int b = 5;
printf("交换前:a=%d b=%d\n",a,b);
//方法一
a = a + b;
b = a - b;//b=a(原来的a)
a = a - b;//a=b(原来的b)
//理论上有一点问题:当a,b数值非常大,两者相加可能会溢出,a中存放的就不是a+b
printf("交换后:a=%d b=%d\n",a,b);
注意:这个方法会有溢出的问题
int a = 3;
int b = 5;
printf("交换前:a=%d b=%d\n",a,b);
//方法二
a = a ^ b;//a=3^5
b = a ^ b;//3^5^5=3 (5^5=0 0^3=3)--> b=3
a = a ^ b;//3^5^3=5 (3^3=0 0^5=5)--> a=5
//按照相同为0 相异为1 不会产生进位的效果,所以肯定不会溢出
//负数也可以这样
printf("交换后:a=%d b=%d\n",a,b);
注意:
- 我们在实际开发中,还是会选择创建一个临时变量来交换两个变量,运算速度快,执行效率高
- & | ^操作符只能适用于整数,不能用于浮点数,用于变量交换有非常大的局限性,而且代码的可读性不高,所以我们通常还是采用创建临时变量来交换两个变量
练习:求一个整数存储在内存中的二进制中1的个数
//求补码的二进制中1的个数
a=3
00000000 00000000 00000000 00000011
a&1
00000000 00000000 00000000 00000011 3的补码
00000000 00000000 00000000 00000001 1的补码
运算结果:
00000000 00000000 00000000 00000001
--->a&1得到是二进制序列的最低位 运算结果为1则最低位为1,运算结果为0则最低位为0
二进制其他位的0/1怎么统计?
将二进制序列向右移动1位,丢失最右边一位(最低位),再继续 &1,所有位数都可以这样统计出来
5.赋值操作符
= 赋值操作符
// = 赋值操作符
int a = 3;//初始化(创建变量的同时给他一个值)
int b = 0;
b = 3; //赋值(创建好变量之后,给他新的值)
//赋值操作符可以连续使用
int a = 10;
int x = 0;
iny y = 20;
a = x = y+1;//连续赋值
//将y+1结果先赋给x,再将x的值赋给a
//x=y+1是一个表达式,表达式结果是x的结果
//不建议这样连续赋值,调试中不好观察,代码可读性不高
x = y+1;//写成这样更好
a = x;
复合赋值符
+= -= *= /= %= >>= <<= &= |= ^=
int a = 3;
a = a + 5;
a += 5;//与上面的等价
a = a >> 1;
a >>= 1;
6.单目操作符
6.1 单目操作符介绍
单目操作符:只有一个操作数 双目操作符:操作符有两个操作数 eg +
! 逻辑反操作
//! 把真的变成假的,假的变成真的
int flag=3
if(flag)//flag为真进入if语句
{}
if(!flag)//flag为假进入if语句
{}
+ - 正负号操作符
// - 负号 + 正号
int a = -10;
int b = +a;
int c = -a;
printf("%d\n",a);
printf("%d\n",b);
printf("%d\n",c);
//正号不改变原来的符号
& 取地址
//& 取地址操作符 取出的是变量在内存中的起始地址
int a = 10;
printf("%p\n",&a);
//a占4个字节 每个字节都有地址
//&a取出的是首字节的地址(起始地址:4个字节中第一个字节的地址)
sizeof 变量所占内存空间大小的操作符 单位:字节
//sizeof 计算类型所创建的变量(类型是不占内存的)/变量所占内存空间大小的操作符 单位:字节
int a = 10;
int n = sizeof(a);//计算的是a所占内存的大小 单位:字节
//int n = sizeof(int);
printf("n=%d\n",n);//4
//计算整个数组的大小 单位:字节 sizeof(arr)
int arr[5] = {0};
printf("%d\n",sizeof(arr));//20
~ 按位(二进制位)取反
// ~ 按位(二进制位)取反 (原来位上是1变成0 是0变成1)
int a = 0;
printf("%d\n",~a);
//分析:
//00000000 00000000 00000000 00000000 a的补码
//11111111 11111111 11111111 11111111 ~a的补码
//%d对应的是原码值
//-1 11111111 11111111 11111111 11111110 ~a的反码
//取反 10000000 00000000 00000000 00000001 ~a的原码---> 十进制:-1
int a = 3;
printf("%d\n",~a);
//00000000 00000000 00000000 00000011 a的补码
//11111111 11111111 11111111 11111100 ~a的补码
//-1 11111111 11111111 11111111 11111011 ~a的反码
//取反 10000000 00000000 00000000 00000100 ~a的原码---> 十进制:-4
注意:按位取反符号位也会取反,即所有二进制位都取反
进行二进制某一位的修改
0----->1 某一位0变为1,其他位不变
修改第n个比特位(0--->1)就将1的补码向左移动n-1位(移动到第n位) 再按位或( | )上原变量的补码
int a = 13;
a |= (1<<1);
printf("%d\n",a);//15
//00000000 00000000 00000000 00001101 13的补码
//怎样将13补码中第二个比特位0置为1?
//加法运算 比较麻烦 如果置位的位数靠中间,根本不知道加几 不采用
//将第二个比特位或1(| 1)
//| 对应二进制位只要有1则为1 两个同时为0才为0
//即:
//00000000 00000000 00000000 00001101 13的补码
//00000000 00000000 00000000 00000010---> 1的补码<<1得到这个数值的补码
//即:1 << 1
//只将第二个比特位置1(或1),其他位保持不变(或0)
//00000000 00000000 00000000 00001111
// 1的补码中的1需要出现在第n个比特位就向左移动n-1位 再按位或
//将00000000 00000000 00000000 00001101 13的补码 第五位0改成1
int a = 13;
a |= (1<<4);
printf("%d\n",a);//29
//分析:11101 --->29
1----->0
某一位1变为0,其他位不变
修改第n个比特位(1--->0),就将1的补码向左移n-1位再按位取反(~),再与原变量的补码按位与( & )
int a = 29;
a &= (~(1<<4));
printf("%d\n",a);//13
//将00000000 00000000 00000000 00011101 29的补码 --->13的补码
//分析:
//第五位1改成0 其他位不变 要修改的二进制位按位与0 其他位按位与1
//按位与&:只要有0,则按位与结果为0;两个同时为1,按位与的结果才为1
//00000000 00000000 00000000 00011101 29的补码
//11111111 11111111 11111111 11101111--->如何得到的
//00000000 00000000 00000000 00010000 按位取反可以得到--->如何得到的
//1的补码向左移动4位 1<<4
//运算的结果:00000000 00000000 00000000 00001101 13的补码
注意:负数也是一样的规则
前置/后置++ --
++
//前置++
int a = 3;
int b = ++a; //先++后使用 a=a+1,b=a; 先自增1再赋值
printf("%d\n",a);//4
printf("%d\n",b);//4
//后置++
int a = 3;
int b = a++; //先使用后++ b=a,a=a+1; 先赋值再自增1
printf("%d\n",a);//4
printf("%d\n",b);//3
--
//前置--
int a = 3;
int b = --a; //先--后使用 a=a-1,b=a; 先自减1再赋值
printf("%d\n",a);//2
printf("%d\n",b);//2
//后置--
int a = 3;
int b = a--; //先使用后-- b=a,a=a-1; 先赋值再自减1
printf("%d\n",a);//2
printf("%d\n",b);//3
其他应用场景
//打印输出
int a = 10;
printf("%d\n",a--);//10 先使用后-- 表达式结果是a的结果,先打印后自减1
int b = 10;
printf("%d\n",--b);//9 先--后使用 表达式结果是b的结果,先自减1后打印
//作为实参进行传参
void test(int n)
{
printf("%d\n",n);//10
}
int main()
{
int a = 10;
test(a--); //先传参再--
printf("%d\n",a);//9
}
//对于内置类型来说,++i 和 i++没有区别 都是加1效果
for(i=0;i<10;i++)
{}
for(i=0;i<10;++i)
{}
//C++ 自定义类型 前置和后者会有差异 前置效率会更高
*间接访问操作符(解引用操作符)
int a = 10;
int* p = &a;//此处int和*组合是指针类型
*p = 20;//a=20
//指针变量p加* 解引用操作:对p解引用,通过p中存放的地址找到他所指向的对象
//*p 就是他所指向的对象
printf("%d\n",a);//20
(类型) 强制类型转换
int a = (int)3.14;//去掉小数点后面的数 只要整数 3.14默认是double型
printf("%d\n",a); //3
练习:
int a = 0;
printf("%d\n",sizeof(a)); //4
printf("%d\n",sizeof a ); //4
//sizeof+变量()可以省略 说明了sizeof是操作符不是函数(函数的()不能省略)
printf("%d\n",sizeof(int));//4
//printf("%d\n",sizeof int);//sizeof+类型()不能省略
注意:strlen()是求字符串长度的库函数;sizeof是操作符不是函数
6.2 sizeof和数组
#include <stdio.h>
void test1(int arr[])//这里arr看似是数组 实则是指针变量
{
printf("%d\n",sizeof(arr));//4/8 sizeof(指针变量)--->4/8
}
void test2(char ch[])//这里ch看似是数组 实则是指针变量
{
printf("%d\n",sizeof(ch));//4/8 32位--->4 64位--->8
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%d\n",sizeof(arr));//4*10=40 sizeof(arr)表示整个数组所占内存大小
printf("%d\n",sizeof(ch)); //1*10=10
test1(arr);//数组名表示首元素地址
test2(ch);
return 0;
}
7.关系操作符
> >= < <= !=判断是否不相等 ==判断是否相等
注意:
- ==与=的区别,不要写错了
- 不是所有的对象都可以用关系操作符进行比较
if(3==5)//两个整数比较是否相等可以用==
{}
if(3.0==5.0)//浮点数比较相等用==会有点不准确,浮点数比较相等有一个精度
{}
if("abc"=="abcdef")//比较两个字符串内容是否相等不能用==
{} //这种写法不是在比较字符串内容
//而是在比较两个字符串的首字符的地址
//比较两个字符串内容是否相等应该使用strcmp()
8.逻辑操作符
&& 逻辑与
|| 逻辑或
注意:
- 区分逻辑与和按位与,逻辑或和按位或
- 逻辑与和逻辑或不关注二进制位,只关注真假
逻辑与&&
逻辑与特点:
- 操作数只要有一个为假(0)则为假(0)
- 两个操作数都为真(非0)才为真(1)
//逻辑与&& 并且
int a = 3;
int b = 5;
int c = a && b;
printf("c=%d\n",c);//c=1
//分析:3,5 非0 为真 &&左右都为真==>整体为真:规定为1
逻辑或||
逻辑或特点:
- 操作数只要有一个为真(非0)则为真(1)
- 两个操作符都为假(0)才为假(0)
//逻辑或|| 或者
int a = 3;
int b = 5;
int c = a || b;
printf("c=%d\n",c);//c=1
例题:根据规则写出if语句判断条件
闰年判断两个规则:
- 能被4整除,并且不能被100整除
- 能被400整除
if((year%4==0)&&(year%100!=0)||(year%400==0))
练习:下面代码输出?
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);//a=1 b=2 c=3 d=4
return 0;
}
//分析:i = a++ && ++b && d++ 表达式从左向右算,表达式的结果最后赋给i
//赋值操作符的优先级非常低,先算完再赋值给i,计算时从左向右算
//a++ 先使用后++ a++这个表达式结果在使用时为0(使用完之后再自增1)
//逻辑与&&特点: 操作数只要有一个为假(0)则为假(0); 两个操作数都为真(非0)才为真(1)
//0 && ++b && d++
//因为左边表达式结果为0,所以(左边操作数为0)&&右边操作数===>整个表达式为0
//右边不用计算了就可以得出整个表达式为0,所以后面的++b和d++都没有计算
//计算只进行到a++,表达式结果就有了 程序就不会往后计算了,直接打印输出
//a=1 b=2 c=3 d=4
变形1:
#include <stdio.h>
int main()
{
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);//a=2 b=3 c=3 d=5
return 0;
}
//逻辑与&&特点: 操作数只要有一个为假(0)则为假(0); 两个操作数都为真(非0)才为真(1)
//分析:
//(a++)&&(++b)&&(d++) 因为操作数都为非0所以计算会一直持续下去
//i=1&&3&&4=1 a=a+1 b=b+1 d=d+1
//总结:
//exp1&&exp2&&exp3 整个表达式计算停止条件:
//1.全部完全计算完(exp都为真)
//2.遇到某个exp为假
变形2:
#include <stdio.h>
int main()
{
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;
printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);//a=2 b=2 c=3 d=4
return 0;
}
//逻辑或特点: 操作数只要有一个为真(非0)则为真(1); 两个操作符都为假(0)才为假(0)
//分析:
//先计算表达式 a++ 1 为真
//1 || ++b || d++ 整个表达式为真 停止计算
//a=2 b=2 c=3 d=4
//总结:
//exp1||exp2||exp3 整个表达式计算停止条件:
//1.全部完全计算完(exp都为假)
//2.遇到某个exp为真
变形3:
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ || ++b || d++;
printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);//a=1 b=3 c=3 d=4
return 0;
}
//逻辑或特点: 操作数只要有一个为真(非0)则为真(1); 两个操作符都为假(0)才为假(0)
//分析:
//先计算表达式a++ 0 为假,继续计算表达式++b 3 为真 停止计算
//0 || 3 || d++ 整个表达式为真 停止计算
//a=1 b=3 c=3 d=4
//总结:
//exp1||exp2||exp3 整个表达式计算停止条件:
//1.全部完全计算完(exp都为假)
//2.遇到某个exp为真
总结:&& 左边为假,右边不计算;||左边为真,右边不计算
9.条件操作符
也叫三目操作符
exp1?exp2:exp3 //exp1结果为真,计算exp2,exp3不计算,整个表达式结果是exp2结果
//exp1结果为假,exp2不计算,计算exp3,整个表达式结果是exp3结果
例题:
if(a > 5)
b = 3;
else
b = -3;
转换为条件表达式,是怎样的?
(a > 5) ? (b = 3) : (b = -3);
b=(a > 5 ? 3 : -3)//第二种写法
例题:使用条件表达式求出两个数中的较大值
int a = 3;
int b = 5;
int max = (a > b ? a : b);
10.逗号表达式
exp1,exp2,exp3…expN
//由逗号隔开的一串表达式(逗号是操作符)
//从左往右依次计算,整个表达式的结果是最后一个表达式的结果
例题:
int a = 1;
int b = 2;
int c = (a > b,a = b + 10,a,b = a + 1);
printf("c=%d",c);
//分析:a>b(判断表达式),a 没有价值的 不影响后续计算
//a=2+10=12 b=12+1=13(最后一个exp) ===> c=13
其他应用场景
if(a = b + 1,c = a / 2,d > 0)
//从左往右依次计算
while(a=get_val(),count_val(a),a>0)
//最后一个表达式a>0用于判断是否进入循环
11.下标引用、函数调用和结构成员
[] 下标引用操作符
int arr[10] = {0};//定义时不能倒过来写,访问可以倒着写,但是不推荐
arr[7] = 8;//这里的[]就是下标引用操作符 操作数:arr 7
7[arr] = 9;//满足交换律可以将两个操作数交换 更加说明了[]是操作符
//虽然下面的写法也可以, 但是不推荐
//分析:
//arr[7]--->*(arr+7)加法支持交换律--->*(7+arr)反推--->7[arr]
//arr是首元素地址
//arr+7:跳过7个元素,指向第8个元素
//*(arr+7) 就是第8个元素
() 函数调用操作符
//函数定义
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 至少有一个操作数(函数名),参数可以没有
return 0;
}
结构成员访问操作符
. 结构体对象.成员
-> 结构体指针->成员
例题 统计学生信息
//错误示范
#include <stdio.h>
#include <string.h>
struct stu
{
char name[20];
int age;
double score;
};
void set_stu(struct stu ss)
{
strcpy(ss.name,"zhangsan");
//ss.name = "zhangsan";
//这样写不可以,name是数组名,即首元素地址,不可以直接存入字符串
//要将字符串存入地址所指向空间才可以 用 strcpy()
ss.age = 20;
ss.score = 100.0;
}
void print_stu(struct stu ss)
{
printf("%s %d %lf\n",ss.name,ss.age,ss.score);
}
int main()
{
struct stu s = {0};
set_stu(s);
print_stu(s);
return 0;
}
//分析:传参有问题,传的是实参的值。
//传值调用时,实参传给形参,形参是实参的一份临时拷贝,对形参的修改不会影响实参
//在上面代码中调用set_stu()将信息存入了形参中,并不会改变实参的值,所以调用打印函数会输出错误
//想要修改实参的值,需要对set_stu()传址调用
//正确示范
#include <stdio.h>
#include <string.h>
struct stu
{
char name[20];
int age;
double score;
};
void set_stu(struct stu* ps)
{//通过指针ps找到main()中创建的结构体变量并进行设置,不需要创建新的结构体变量
/*//第一种写法
strcpy((*ps).name,"zhangsan");
(*ps).age = 20;
(*ps).score = 100.0;*/
//第二种写法
strcpy(ps->name,"zhangsan");
//通过结构体指针ps找到它所指向的对象s的成员name
ps->age = 20;
ps->score = 100.0;
}
//(*ps).age与ps->age完全等价
/*void print_stu(struct stu ss)
//重新创建一个结构体变量ss,打印输出来自ss中的数据
{
printf("%s %d %lf\n",ss.name,ss.age,ss.score);
}*/
//优化 打印输出main()中s中数据
void print_stu(struct stu* ps)
{
printf("%s %d %lf\n",ps->name,ps->age,ps->score);
}
int main()
{
struct stu s = {0};
set_stu(&s);
//print_stu(s);//传结构体对象,在print_stu()会重新创建一个结构体对象
//ss是s一份临时拷贝,造成空间浪费
//优化
print_stu(&s);
return 0;
}
12.表达式求值
操作符是用于表达式中的,用各种操作符组成表达式
3 + 5 / 2;//用各种操作符写成一个表达式
表达式求值的顺序一般是由操作符的优先级和结合性决定的
//优先级
int a = 2 + 6 / 3;//先算+还是/是有优先级的 / 优先级高于 + 先算除法
//a=2+2=4
//结合性
int b = 2 + 3 + 4;//先算2+3还是先算3+4 都是+优先级相同 +结合性从左向右
//2+3=5 5+4=9
有些表达式的操作符在求值的过程中可能需要转换成其他类型才能进行运算
12.1 隐式类型转换
不是明面上的类型转换,没有看到进行了类型转换,实则已经完成了类型转换
整型提升
C语言的整型算术运算(总是)至少以缺省(默认)整数类型的精度来进行的,为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升(类型的大小小于一个整型大小 char short)
char a = 5;
char b = 126;
char c = a + b;
//字符型char数据 a b 在运算之前先要转换成普通整型int,然后参与运算,即整型提升
//表达式中某些变量(char 1byte)大小达不到一个整型大小(int 4byte),所以需要进行整型提升
printf("%d\n",c);
整型提升的意义
如何进行整型提升?
整型提升是按照变量的数据类型的符号位来提升的
//负数的整型提升
char c = -1;//-1是整数 4个字节
//10000000 00000000 00000000 00000001 -1的原码
//11111111 11111111 11111111 11111110 -1的反码 原码取反得到(符号位不变)
//11111111 11111111 11111111 11111111 -1的补码 反码+1得到
//char 类型只能放下一个字节(8bit)
//存进去时发生了截断,c变量中只放了低八位即11111111
//对c进行整型提升,怎么提升?
//char 有符号字符型 11111111 最高位解读为一个符号位 1表示负数 整型提升时拿符号位进行提升
//11111111 11111111 11111111 11111111 符号位为1 整型提升就是进行高位补1
//正数的整型提升
char c = 1;
//00000000 00000000 00000000 00000001 1的补码
//字符型变量c 只放了低8位 00000001
//整型提升 有符号型字符变量 符号位是0 整型提升就是在高位补0
//即:
//00000000 00000000 00000000 00000001
//无符号整型提升 : 高位补0
例题:求输出c变量的值
char a = 5;
char b = 126;
//5 126 都是整型 4byte
//00000000 00000000 00000000 00000101 5的补码
//00000000 00000000 00000000 01111110 126的补码
//存到char(1byte) 存不下产生截断只存了低8位
//a中存放00000101 b中存放01111110
char c = a + b;
//00000101-a
//01111110-b
//发现表达式中的变量a b大小达不到一个整型大小,需要进行整型提升
//整型提升是按照变量的数据类型的符号位来提升的
//有符号型字符变量 符号位(低8位中的最高位)是0 整型提升就是在高位补0
//整型提升后的结果
//00000000 00000000 00000000 00000101 -a
//00000000 00000000 00000000 01111110 -b
//相加后:
//00000000 00000000 00000000 10000011
//结果存放到有符号字符型变量c中存放不下,产生截断,只能存放低8位
//10000011-c
printf("%d\n",c);// -125
//字符型变量c以%d形式打印 需要进行整型提升
//有符号型字符变量 符号位(低8位中的最高位)是1 整型提升就是在高位补1
//整型提升后的结果
//11111111 11111111 11111111 10000011 -c 补码
//11111111 11111111 11111111 10000010 补码-1=反码
//10000000 00000000 00000000 01111101 反码(除符号位之外)取反=原码
//即:-125
//字符类型也是归结到整型家族,字符在底层存储时存的是ASCII码值
//ASCII码值也是整数,所以char类型在归类时也属于整型家族
//整型家族中的类型在内存中存放的都是补码
//char 有符号字符型 表示范围:-128~127 ASCII值表示范围:0~127
例题:
仅仅输出:c
分析:a,b发生了整型提升,与原来的值不相等了,a==0xb6 b=0xb600也是表达式,表达式中出现字符和短整型操作数(没有达到一个整型大小)就会发生整型提升。c就是一个整型变量,不会发生整型提升,所以还与原来值相等,打印输出c
char a = 0xb6;
//10110110 有符号字符型 符号位(低8位中的最高位)1 高位补1
//11111111 11111111 11111111 10110110 补码
//11111111 11111111 11111111 10110101 反码(补码-1)
//10000000 00000000 00000000 01001010 原码(符号位不变 其他位取反)
//整型提升后的值-74 原来的值:182
char b = 0xb600;//同理可知b也变化了
注意:只要是放在表达式里面的char short 在表达式使用前都会进行整型提升
改进:
unsigned char a = 0xb6;
// 10110110 因为是无符号数 进行整数提升 高位补0
// 不会对数值进行改变
unsigned short b = 0xb600;
例题:
#include <stdio.h>
int main()
{
char c = 1;
printf("%u\n",sizeof(c)); //1
printf("%u\n",sizeof(+c));//4
printf("%u\n",sizeof(-c));//4
return 0;
}
分析:为什么正c和负c所占空间大小为4? +c -c也是一个表达式,所以先进行整型提升之后再去参与运算,整型提升后的空间大小为4byte ,所以所占空间为4
表达式中操作数的char和short类型的大小达不到一个整型的大小,需要进行整型提升,那么如果表达式中操作数类型为float,double等,大小已经超过一个整型大小或者等于整型大小,又该怎么处理? 算术转换
12.2 算术转换
类型的大小大于等于一个整型大小的类型在使用时进行的转换叫算术转换
如果某个操作数的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就没法进行
long double
double
float
unsigned long int
long int
unsigned int
int
操作数类型转换时进行向上转换(靠下的转换成靠上的类型)
float和int int->float
double和float float->double
12.3 操作符的属性
复杂表达式的求值有三个影响因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
注意
- 相邻操作符先看优先级,如果操作符一样就看结合性
- 优先级讨论的是相邻操作符的优先级,两相邻操作符的执行先后取决于优先级,相邻操作符的优先级相同就取决于结合性
操作符的优先级结合性表
注意
- 操作符的优先级从上到下递减
- 操作符的结合性:N/A 不讨论结合性 L-R从左向右 R-L从右向左。据表观察可得规律:赋值类操作符和单目操作符(除后置++ --)的结合性是从右向左R-L,聚组()和条件操作符?:不讨论结合性N/A,其他都是L-R。
- 是否控制求值顺序:大部分都是否,但是也有是的 eg
&& || ?: , //控制求值顺序
&& 逻辑与
表达式左边为假右边不用计算,左边为真右边计算,会控制求值顺序
|| 逻辑或
左边为真右边不要算,左边为假右边计算,会控制求值顺序
? :条件操作符
exp1为真,exp2计算,整个表达式结果是exp2的结果
exp1为假,exp3计算,整个表达式结果是exp3结果,会控制求值顺序
, 逗号表达式
从左往右依次计算,最后一个exp是整个表达式的结果,会控制求值顺序
注意:即使懂了操作符优先级,结合性,是否控制求值顺序操作符属性,依然有可能没办法确认一个表达式的唯一计算路径
问题表达式
a*b+c*d+e*f;//不能确定表达式的唯一路径 13254 14253 两种计算顺序都可以
//abcdef是六个互相影响的表达式,计算路径不同会有很大区别
//此处称为问题表达式(编译器都不知道怎么计算)
//解决:1.加() 2.简化表达式,不要写的太复杂
//13254代表依次执行五个操作符分别对应表达式5个操作符
int c = 2;
c+--c; //表达式的计算路径 21 --c会影响c的值 会出现计算结果不一致
//c提前准备好2+1=3 c没有在计算--c前准备好 1+1=2
//解决:左右操作数尽量不要跟同一个变量有关
int main()
{
int i = 10;
i=i-- - --i*(i = -3)*i++ + ++i;
printf("i=%d\n",i);//不同编译器下不同结果
return 0;
}
不同编译器下不同结果
每次调用返回值不同,只有操作符的优先级,没法确定操作数种函数调用的先后,造成多值,计算结果不唯一
在vs中运行,结果为12 先执行++a--->a=2 再执行第二个++a--->a=3 再执行第三个++a--->a=4 此时a=4 最后执行4+4+4=12
在linux中运行,结果为10 先执行++a--->a=2 再执行第二个++a--->a=3 此时a=3,再执行 3+3=6 再执行++a--->a=4 最后执行 6+4=10
注意:我们写的表达式如果不能通过操作符属性确定唯一路径,则说明表达式存在问题,解决:简化表达式