1. 算术 *** 作符
2. 移位 *** 作符
2.1 左移 *** 作符
2.2 右移 *** 作符
2.3 总结移位 *** 作符的计算
3. 位 *** 作符
4. 赋值 *** 作符
5. 单目 *** 作符
5.1 单目 *** 作符介绍
5.2 sizeof 和 数组
6. 关系 *** 作符
7. 逻辑 *** 作符
8. 条件 *** 作符(也叫三目 *** 作符)
9. 逗号表达式
10. 下标引用、函数调用和结构成员
11. 表达式求值
11.1 隐式类型转换
11.2 算术转换
11.3 *** 作符的属性
内容:
1. 各种 *** 作符的介绍。
2. 使用各种 *** 作符怎么进行表达式求值
*** 作符分类:
- 算术 *** 作符
- 移位 *** 作符
- 位 *** 作符
- 赋值 *** 作符
- 单目 *** 作符
- 关系 *** 作符
- 逻辑 *** 作符
- 条件 *** 作符
- 逗号表达式
- 下标引用、函数调用和结构成员
+ - * / %
注意:
%——>是取模或取余,得到的是相除之后的余数。
1. 除了 % *** 作符之外,其他的几个 *** 作符都可以作用于整数和浮点数。
2. 对于 / *** 作符如果两个 *** 作数都为整数,执行整数除法;而当除号两端只要有一个浮点数执行的就是浮点数除法。
3. % *** 作符的两个 *** 作数必须为整数。返回的是整除之后的余数。
4.n%(常量)的结果范围是1~(常量-1)。
5.+ - * / 这些 *** 作符两端只要有一个是浮点数,其计算结果也是浮点数。
#include
int main()
{
int ret = 10 / 3;//3
int len = 10 % 3;//1
printf("%d %d\n", ret, len);
double sub = 10 / 3.0;//3.333333
double rmb = 10.0 / 3;//3.333333
double qub = 10.0 / 3.0;//3.333333
printf("%lf %lf %lf\n", sub, rmb, qub);
return 0;
}
2. 移位 *** 作符
2.1 左移 *** 作符<< 左移操作符
>> 右移 *** 作符
注:移位 *** 作符的 *** 作数只能是整数。
移位规则:左边抛弃、右边补0
a << 2:
意思是把a在内存中存储的二进制位向左移动2位再赋值给b。
整数有3种二进制的表示形式:
- 原码
- 反码
- 补码
任何一种整数都能写出原码、反码和补码。
对于正整数来说,原码、反码、补码相同;
对于负整数来说,原码、反码、补码不同,需要进行计算。
举例:
①第一种情况:int a = 5 ;是正整数
a是整型,a占4个字节——>32个比特位(32bit)
00000000000000000000000000000101——原码
依据二进制直接写出的二进制序列就是原码。
(怎么证明它是5呢?)
规定二进制序列的最高位表示符号位:0表示是正数,1表示是负数。
最高位即符号位后面的一系列的数字相加之和就是5。
因为5是正数,所以它的原码、反码和补码相同,所以:
00000000000000000000000000000101——原码
00000000000000000000000000000101——反码
00000000000000000000000000000101——补码
②第二种情况:int a = -5 ; 是负整数
因为直接按照该数的正负数写出的二进制序列就是原码的表现形式:
10000000000000000000000000000101——原码
原码的符号位不变,其他位按位取反(原来1变成0,原来0变成1),得到反码:
11111111111111111111111111111010——反码
反码的二进制序列(最低位)+1,得到补码:
11111111111111111111111111111011——补码
整数在内存中存储的是补码,(整数在内存中存储的二进制位是补码)。
对于正整数来说,原码、反码、补码都相同,不能直接验证内存中存储的是哪一个,则接下来用负整数来验证:
举例:-1:
10000000000000000000000000000001——原码
111111111111111111111111111111111110——反码
111111111111111111111111111111111111——补码
在内存中存储-1:
-1的地址是 ff ff ff ff,是16进制形式展示的;
(注:VS编译器在内存窗口是16进制展示的,并不代表内存里存储的形式就是16进制。)
16进制的范围是:1 2 3 4 5 6 7 8 9 A B C D E F (小写也可以)
因为4个二进制都是1才表示15(二进制1111表示成十进制就是15),15就是一个F,即8个F就是32个1。
即完成验证对于负整数来说在内存中存储的是它的补码。
上述讲的都是对移位 *** 作符的铺垫,再回到
a << 2:
意思是:a在内存中存储的二进制位向左移动2位再赋值给b。
即b=20;
#include
int main()
{
int a = 5;
int b = a << 2;
printf("%d ", b);
printf("%d\n", a);
return 0;
}//20 5
a的值还是5,没有发生变化。所以a<<2,a向左移动两位只是一种运算,不会真的把a给变了。只是把a里的值向左移动两位的效果展示出来,即把计算结果放到b里去,a本身不会发生变化。(a在没被赋值的情况下自身的值不会发生变化)
就相当于:b = a + 1;只是把a+1的结果放到b里去,a不变。
若此时打印 : printf("%d\n",b) :
内存中存储的是补码;
计算时用的也是补码;
但是打印或者使用的时候用的是原码的值,所以移位之后的结果还算是它的原码。
内存中是补码,现在要倒着算它的原码。 因为此时是知道b的补码,而要打印的是b的原码:所以计算b的原码:
知道补码——>补码-1=反码,符号位不变,其他位按位取反——>得到原码
#include
int main()
{
int a = -5;
int b = a << 2;
printf("%d ", b);
printf("%d\n", a);
return 0;
}//-20 -5
2.2 右移 *** 作符
移位规则:
首先右移运算分两种:
1. 逻辑右移
左边用0填充,右边丢弃
2. 算术右移
左边用原该值的符号位填充,右边丢弃到底是算数右移还是逻辑右移,是取决于编译器的,常见的编译器下都是算术右移。
举例:
①第一种情况:int a = 5 ;是正整数,其原码、反码和补码相同(即不用计算到补码再进行移位)
#include
int main()
{
int a = 5;
int b = a >> 2;
printf("%d\n", b);
return 0;
}//2
②第二种情况:int a = -5 ; 是负整数
#include
int main()
{
int a = -5;
int b = a >> 2;
printf("%d\n", b);
return 0;
}//-2
所以,放的是原符号位,编译器采用的算术右移。
注意:移位的时候不要移负数位。移位 *** 作符的右 *** 作数不能是负数。
#include
int main()
{
int a = -5;
int b = a >> -3;//这是标准未定义行为,即C语言自己都不知道怎么办
//没有必要这样做
/*printf("%d\n", b);
return 0;*/
}
2.3 总结移位 *** 作符的计算
对于正整数来说,写出原码可以直接进行移位,移位后计算结果就是打印的值。
(因为正整数的原码、反码和补码是相同的)
对于负整数来说,写出原码后,需要由此原码计算到补码,对补码移位,然后对移位的结果再计算到它的原码(因为规定是打印的原码),计算原码就是打印的值。
3. 位 *** 作符& 按位与
| 按位或
^ 按位异或这里的位都是指二进制位。
注:它们的 *** 作数必须是整数。
举例:
int a = 3;
int b = -5;
int c = a & b;
a按位与b——=——>a按二进制位与b——=——>这个二进制位是补码的二进制序列,a和b在计算时计算的都是存在内存里面的补码。
int a = 3;
int b = -5;
int c = a | b;
int a = 3;
int b = -5;
int c = a ^ b;
计算规则:
写出二进制序列,有原码计算到补码,对补码进行按位与、按位或、按位异或,进行完运算后再由相应的补码计算到原码,打印原码的值。
对这些位 *** 作符的使用:
练习1:不能创建临时变量(第三个变量),实现两个数的交换。
⑴方法1:这是常用(实际开发工作中)的方法,简单,效率高,不存在问题
#include
int main()
{
int a = 3;
int b = 5;
printf("交换前:a=%d,b=%d\n", a, b);
int c = 0;//创建临时变量
c = a;
a = b;
b = c;
printf("交换后:a=%d,b=%d\n",a, b);
return 0;
}
//交换前:a = 3, b = 5
//交换后:a = 5, b = 3
但是创建了临时变量,不符合要求。
⑵方法2:存在溢出的问题
#include
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
有Bug:若a和b都相对较大,则相加之和可能超出整型的最大值,相加之和放到a里就会丢一些二进制位,就不会还原原来的a和b。所以这种方法不能解决所有的情况。
⑶方法3:代码的可读性差,而且只适用于整型
用^位 *** 作符,遵循其计算规则:相同为0,相异为1。
0 ^ 任何数 = 这个任何数,如 0 ^ 5 = 5;
任何数 ^ 这个任何数 = 0,如3 ^ 3 = 0;
#include
int main()
{
int a = 3;
int b = 5;
printf("交换前:a=%d,b=%d\n", a, b);
a = a ^ b;
b = a ^ b;//相当于a^b^b=a^0=a,即把a赋值给b
a = a ^ b;//相当于a^b^a=a^a^b=0^b=b,(注意这里的b是a的值,上面赋值了)即把b赋值给a
printf("交换后:a=%d,b=%d\n",a, b);
return 0;
}
//交换前:a = 3, b = 5
//交换后:a = 5, b = 3
这种方法不存在溢出的问题。
练习2:编写代码实现求一个整数存储在内存中的二进制中1的个数。
解题:一个整数存储在内存中的是它的补码,即求补码中1的个数。
怎么得到二进制位的序列呢?
例如:5,它的二进制位:00000000000000000000000000000101
若得到它的二进制序列的每一位:
00000000000000000000000000000101 & 00000000000000000000000000000001
得到的二进制序列是00000000000000000000000000000001代表的是十进制数字1
(若
00000000000000000000000000000100 & 00000000000000000000000000000001
得到的二进制序列是00000000000000000000000000000000代表的是十进制数字0
)
所以:
5 & 1得到的结果是几,它二进制序列最低位就是几
a & 1得到的结果是几,它二进制序列最低位就是几
00000000000000000000000000000101中最低位的1统计完之后没有用了则把二进制序列整体向右移一位:00000000000000000000000000000010,让这个结果再 & 1 就可以得到原来二进制序列的倒数第二位。
综上:
若看a的二进制位里有多少个1,则看 a & 1 看结果是几;
用完之后:a = a >> 1(把上一位丢掉)
这两步循环(其实因为有32位,所以执行32次),统计结果是1的个数即可。
4. 赋值 *** 作符赋值 *** 作符是一个很棒的 *** 作符,可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
int weight = 120; 体重
weight = 89; 这里等号就是赋值 *** 作符,不满意就赋值
double salary = 10000.0;
salary = 20000.0; 使用赋值 *** 作符赋值。
赋值 *** 作符可以连续使用,比如:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1; 这里是连续赋值:第一步把y+1的值赋给x,第二步是把x的值赋给a
这样写不够好,两个动作一步到位,调试时不能体会过程。
同样的语义:
x = y+1;
a = x;
这样的写法更加清晰爽朗而且易于调试。
连续赋值是可以的,但是必须保证左边是可以被赋值的;
左边是变量就可以被赋值,因为变量里面有一块空间,空间里是可以放值的,但是左边是常量就不可以。如3 = 100,编译器会报错:左 *** 作数必须是左值。
什么是左值什么是右值呢?
左值是可以放在等号左边的,一般是一块空间,以此可以保证里面放值;
右值是可以放在等号右边的,一般是一块空间,或者是一块空间的内容。
如:int a = 20;
这里a是左值,因为它是一块空间,是4个字节的空间里放了个20;20是右值。
如:int b = 0; b = a;
这里b是左值,它表现的是一块空间;a是右值,是把a里面放的20赋值给b,这里右值是一块空间的内容。
赋值 *** 作符里有一种复合赋值符:
复合赋值符
+=
-=
*=
/=
%=
>>=
<<=
&=
|=
^=
这些运算符都可以写成复合的效果,可以使代码更加简洁
#include
int main()
{
int a = 0;
a = a + 10;
a += 10;
a = a - 10;
a -= 10;
a *= 10;
a /= 10;
a %= 10;
a ^= 10;
a |= 10;
a &= 10;
a <<= 10;
a >> 10;
return 0;
}
5. 单目 *** 作符
举例:3 + 5 :
3是左 *** 作数; 5是右 *** 作数; +是一个 *** 作符,是双目 *** 作符。
有三个 *** 作数叫三目 *** 作符,如:?:
有一个 *** 作数叫单目 *** 作符。
5.1 单目 *** 作符介绍! 逻辑反 *** 作
- 负值
+ 正值
& 取地址
sizeof *** 作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问 *** 作符(解引用 *** 作符)
(类型) 强制类型转换
1、! 逻辑反 *** 作
可以把一个表达式的结果是真就变为假,是假就变为真。
当把一个假变为真的时候,变真的结果是1,这是规定的。
即 !(表达式)的结果要么是0要么是非0,即要么是假要么是真。
#include
int main()
{
int flag = 0;
if (!flag)//flag为假(则!flag为真)的时候就打印hehe
{
printf("hehe\n");
}
return 0;
}//hehe
2、- 负值
+ 正值
#include
int main()
{
int i = 0;
int flag = 1;
int a = -10;//负数
int b = +10;//
b = + -10;//还是负数,正号对于正、负数基本没作用
for (i = 0; i < 10; i++)
{
printf("%d ", i * flag);
flag = -flag;
}
return 0;
}//0 -1 2 -3 4 -5 6 -7 8 -9
有一个函数是绝对值函数abs(),对整数求绝对值;fabs()函数是对小数求绝对值
3、& 取地址
#include
int main()
{
int a = 10;
&a;//拿取在内存中的a(变量)的地址
//&也能拿到数组的地址
return 0;
}
4、sizeof *** 作数的类型长度(以字节为单位)
它总是用来计算内存的大小。
#include
int main()
{
//用sizeof计算变量的大小
int a = 10;
printf("%d ", sizeof(a));//计算a所占内存空间的大小
printf("%d ", sizeof(int));//因为a的类型是int
//用sizeof计算数组的大小
int arr[10] = { 1,2,3,4,5,6 };
printf("%d ", sizeof(arr));//数组名单独放到sizeof内部表示计算整个数组的大小
//若写的是数组arr的类型:int[10],是去掉数组名剩下的就是它的类型
printf("%d\n", sizeof(int[10]));
return 0;
}//4 4 40 40
数组也是有类型的,是数据类型 + [ ];
sizeof是 *** 作符,不是函数。(strlen()函数是库函数)
注意一种写法:
#include
int main()
{
int a = 10;
printf("%d ", sizeof(a));
printf("%d ", sizeof a);
printf("%d ", sizeof(int));//这里圆括号不能省略
return 0;
}//4 4 4
//若sizeof去掉圆括号也对,说明sizeof不是函数,是函数就不能省略括号就不能运行
//所以sizeof在一定程度上可以省略圆括号,但不建议省略,降低代码可读性
5、~ 对一个数的二进制位按位取反
#include
int main()
{
int a = 0;
//00000000000000000000000000000000——0的原码,也是反码和补码
//~a:
//11111111111111111111111111111111——按位取反之后这也是补码
//因为打印的结果是原码
//11111111111111111111111111111110——反码
//10000000000000000000000000000001——原码,是-1
printf("%d\n", ~a); //~按(内存中补码的二进制)位取反
return 0;
}//-1
对~按位取反的使用:
#include
int main()
{
int a = 10;
//000000000000000000000000000001010——a的原码、反码也是补码
//如果要把它的二进制序列倒数第三位0改成1:则仅仅对该位|1(按位或1):
//即:
// 000000000000000000000000000001010
// | 000000000000000000000000000000100——这个二进制序列的得到:把1向左移动2位:1 << 2
// 000000000000000000000000000001110
a = a | (1 << 2);//也可以写:a|=(1 << 2)
printf("%d ", a);
//若此时想改回原来的序列,即把000000000000000000000000000001110的倒数都三位把1改成0
// 000000000000000000000000000001110
// & 111111111111111111111111111111011——这个二进制序列的得到:是~000000000000000000000000000000100(对该序列按位取反),而这个序列是:1 << 2(把1向左移动两位)得到的
// 000000000000000000000000000001010
//即:~(1 << 2)
a = a & ~(1 << 2);//等价于:a & = ~(1 << 2)
printf("%d\n", a);
return 0;
}//14 10
6、
-- 前置、后置--
++ 前置、后置++
⑴对于前置++、后置++:
#include
int main()
{
int a = 10;
int b = ++a;//前置++:先++,后使用
printf("a=%d b=%d\n", a, b);//a=11 b=11
//注意因为++a是对a进行了改变,a变成了11
a = 10;
b = a++;//后置++:先使用,后++
printf("a=%d b=%d\n", a, b);//a=11 b=10
a = 10;
printf("%d\n", ++a);//结果为11;注意这里的++a也是表达式,也是前置++,是先++后打印(使用)
a = 10;
printf("%d\n", a++);//结果是10,打印完之后a再自增
return 0;
}
⑵对于前置--、后置--:
#include
int main()
{
int a = 10;
int b = --a;//前置--:先--,后使用
printf("a=%d b=%d\n", a, b);//a=9 b=9
a = 10;
b = a--;//后置--:先使用,后--
printf("a=%d b=%d\n", a, b);//a=9 b=10
a = 10;
printf("%d\n", --a);//9
a = 10;
printf("%d\n", a--);//10
return 0;
}
7、* 间接访问 *** 作符(解引用 *** 作符)
* 是和指针一起使用的。只要是地址就可以对其进行解引用 *** 作。
#include
int main()
{
int a = 10;
int* pa = &a;//地址是编号,pa存放地址则pa是指针变量,pa的类型是int*,因为pa指向的a的类型是int类型
//若通过指针变量pa找到a,则是:*pa,*是解引用 *** 作符或者说是间接引用 *** 作符,*pa=a
*pa = 20;
printf("%d\n", a);//20,即实现了改变a的值
return 0;
}
8、(类型) 强制类型转换
当类型不准确或不对的时候可以对其类型进行调整,进行强制转换。
#include
int main()
{
int a = 0;
//a = 3.14;
//3.14是doble类型,如果放到a中,a的类型是int,则存在类型不同一
//则需要把double类型转化为int类型:(不转化可能会丢失数据)
//把3.14变成整型再放到a中:
a = (int)3.14;//说明要把3.14强制转化为整型(取整数部分)再赋值给a
printf("%d\n", a);//3
//再如:
int b = (int)5.67;
printf("%d\n", b);
return 0;
}
强制类型一般不建议使用,尽量把类型和数据设置的相匹配。
注意是在圆括号里放的是类型的时候才是强制类型转换:(类型)
int a = int (3.14);
这是错误的写法。
5.2 sizeof 和 数组#include
void test1(int arr[])//这里可以写成数组也可以写成指针,但本质上都是指针,所以这里是int* arr指针
{
//因为一个指针的大小是4或8字节,32平台是4,64平台是8(X86是32平台)
printf("%d\n", sizeof(arr));//(2)4
}
void test2(char ch[])//所以这里实质上是char *ch[]
{
printf("%d\n", sizeof(ch));//(4)4
//1个字符的大小是1个字节,但是这个字符的地址编号是一系列二进制序列,二进制序列是32位,它的大小是4个字节,这个地址编号指向的空间是一个字节,char*指针的解引用访问权就是1个字节
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));//(1)40
//这里没有对数组进行传参,此时sizeof内部单独放一个数组名,数组名表示整个数组,计算的是整个数组的大小:40
printf("%d\n", sizeof(ch));//(3)10
test1(arr);//这里是数组传参,数组名前没有sizeof,没有取地址,所以这里的数组名就是首元素的地址,即传的是数组首元素的地址
test2(ch);
return 0;
}
注意:
指针的大小和类型没有关系,就是4或8个字节;
32平台是4,64平台是8(X86是32平台,X64是62平台)
6. 关系 *** 作符>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
注意一些运算符使用时候的陷阱:在编程的过程中== 和=不小心写错,是导致的错误。
关系 *** 作符是用来比较大小的。注意 *** 作的对象,例如两个日期是不能相加的。
为了避免错误,常把常量写在等号的左面,因为即使把==写成=,编译器会报错从而及时发现错误:
#include
int main()
{
int a = 5;
if (5 = a)
printf("hehe\n");
return 0;
}//报错:“=”: 左 *** 作数必须为左值
7. 逻辑 *** 作符
&& 逻辑与 表示“并且”
|| 逻辑或 表示“或者”
逻辑与:
#include
int main()
{
int age = 0;
scanf("%d", &age);
if (0 < age < 18)
//这种写法是错误的,因为若输入-15,不大于0,为假,则前面结果是0,0<18也会进入if语句;
//若输入20也会:因为20>0,为真,前面结果是1,1<18则也会进入
{
printf("未成年\n");
}
//正常写法:
if (age > 0 && age < 18)
{
printf("未成年\n");
}
return 0;
}
逻辑或:
#include
int main()
{
//输入月份
int month = 0;
scanf("%d",&month);
//注意:如果输入字母是小于1的
if (month < 1 || month>12)
{
printf("输入错误\n");
}
else
{
;
}
return 0;
}
一道面试题:
#include
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//这里a++是先使用,则为0,&&前面是0,那么无论后面是几它的结果都是0,
//所以后面的++b和d++根本没进行计算,只有a发生变化了
printf("a = %d b = %d c = %d d = %d\n", a, b, c, d);
return 0;
}//a = 1 b = 2 c = 3 d = 4
//而若是:
#include
int main()
{
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//这里a++是先使用,则为1,&&前面是1,b是先自增是3,所以 1 && 3结果为真,再看下一个&&后面的表达式,这里三个表达式都计算了,则a、b和c相应发生变化
//d++,先使用是4,所以:1 && 3 && 4结果为真,所以i是1
printf("a = %d b = %d c = %d d = %d\n", a, b, c, d);
return 0;
}//a = 2 b = 3 c = 3 d = 5
#include
int main()
{
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++||++b||d++;
//这里a++是先使用,则为1,&&前面是1,此时a自增为2;||前面是真,后面就没算,后面又是||,所以||后面也没算
//所以只有a变化了
printf("a = %d b = %d c = %d d = %d\n", a, b, c, d);
return 0;
}//a =2 b = 2 c = 3 d = 4
所以对于:a && b 如果&&前面的a的表达式为假,则后面的b表达式不用看更不用计算。
*** 作符前面不满足要求,就不用算 *** 作符后面的,后面的表达式也就不会产生效果。
*** 作符是从左向右计算的。
对于 || 来说,前面左 *** 作数为真,右面就不用计算了;
对于 && 来说,前面左 *** 作数为假,右面就不用计算了。
注意区分:
区分逻辑与 && 和按位与 &
区分逻辑或 || 和按位或 |
按位与 & 和按位或 | 是用来 *** 作二进制位的;
逻辑与 && 和逻辑或 ||只关注变量或表达式的真和假。
1 & 2 -----> 0
1 && 2 ----> 1
1 | 2 -----> 3
1 || 2 ----> 1
exp1 ? exp2 : exp3
表达式1 ? 表达式2 : 表达式3
执行逻辑:
如果表达式1为真,执行表达式2,整个表达式是表达式2的结果;
如果表达式1为假,执行表达式3,整个表达式是表达式3的结果。
//计算两个数的较大值:
#include
int main()
{
int a = 100;
int b = 200;
int max = 0;
/*if (a > b)
{
max = a;
}
else
{
max = b;
}*/
max = (a > b ? a : b);//都可以
printf("%d\n",max);
return 0;
}//200
if (a > 5)
b = 3;
else
b = -3;
转换成条件表达式:
#include
int main()
{
int a = 0;
scanf("%d", &a);
int b = (a > 5 ? 3 : -3);
printf("%d %d\n", a, b);
return 0;
}
9. 逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
#include
int main()
{
int a = 3;//5
int b = 5;//-1
int c = 6;
int d = (a += 2, b = a - c, c = a + 2 * b);
printf("%d\n", d);
return 0;
}//3
#include
int main()
{
//代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
//逗号表达式c是多少?13
//a>b是判断语句,没用,不会对a或者b产生修改。a=12。a也没用。b=13。
//代码2
if (a = b + 1, c = a / 2, d > 0)
//真正起到if语句判断作用的只有最后一句话,看最终d是否大于0,大于0就进入If语句,不大于0就不进入
//代码3,是伪代码,不是真实的代码
a = get_val();//用get_val()函数获取一个值放到a里
count_val(a);//count_val()函数对a进行处理,不关心具体是什么,只关心逻辑
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
//如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a > 0)
//这是逗号表达式,真正起到判断条件的是最后一个表达式:a>0
{
//业务处理
}
return 0;
}//多一种写法
10. 下标引用、函数调用和结构成员
1. [ ] 下标引用 *** 作符
*** 作数:一个数组名 + 一个索引值
int arr[10]; 创建数组
arr[9] = 10; 实用下标引用 *** 作符。
[ ]的两个 *** 作数是arr和9。
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", arr[7]);//[]就是下标引用 *** 作符,这里arr和7是它的 *** 作数
//注:编译器处理的时候:arr[7]—=—>*(arr+7)
//意思是数组首元素地址+7跳过7个地址就是第8个元素的地址,对其解引用就找到了第8个元素
//扩展:arr[7]—=—>*(arr+7)—=—>*(7+arr)—=—>7[arr]
printf("%d\n", 7[arr]);//8,这种写法也可以,但是这种写法比较奇怪
return 0;
}//8
//所以[]就是一个 *** 作符,只要满足语法它都可以写。
2. ( ) 函数调用 *** 作符
接受一个或者多个 *** 作数:第一个 *** 作数是函数名,剩余的 *** 作数就是传递给函数的参数。
#include
void test()
{
printf("hehe\n");
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
test();//这里的()就是函数调用 *** 作符,不能随便省略,这里的 *** 作数是test函数名
int ret = Add(2, 3);//()——函数调用 *** 作符,这里的 *** 作数是Add函数名和2、3
return 0;
}
3. 访问一个结构的成员
. 结构体 . 成员名
-> 结构体指针->成员名
. *** 作符的语法规则:
左 *** 作数是结构体变量 . 右边是结构体成员
#include
//创建学生这个结构体类型
struct Stu
{
char name[20];
int age;
double score;
};//3个成员
int main()
{
//这里用上面创建的结构体类型创建一个变量s:
//struct Stu s;
//给这个变量初始化:结果体里放的是多个元素,初始化的时候用大括号)
struct Stu s = { "zhangsan",20,85.5 };
printf("%s %d %.1lf\n", s.name, s.age, s.score);
//所以访问结构体变量的成员的时候用的是 . 这个 *** 作符
return 0;
}//zhangsan 20 85.5
//小数点默认保有6位
//%.1lf是保留1位小数
或者是:
#include
struct Stu
{
char name[10];
int age;
double score;
};
int main()
{
struct Stu s = { "zhangsan",20,85.5 };
struct Stu *ps=&s;//s是结构体变量,因为ps指向struct Stu结构体类型的变量,所以ps是struct Stu类型的指针
//前面放*说明ps是指针变量,放struct Stu说明ps指向的对象的类型是struct Stu
//ps的类型是struct Stu*,是一种结构体指针
//ps是指向s的,*ps就是s,要通过ps来打印s里面的内容,即把ps指向对象的s的里面的内容打印出来
printf("%s %d %.1lf\n", (*ps).name, (*ps).age, (*ps).score);
return 0;
}//zhangsan 20 85.5
这里是先解引用再找到它的成员,但是写法较啰嗦
-> *** 作符的语法规则:
左 *** 作数是结构体指针-> 右边是结构体成员
#include
struct Stu
{
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { "zhangsan",20,85.5 };
struct Stu* ps = &s;
printf(" %s %d %.1lf\n", ps->name, ps->age, ps->score);
//ps->name意思是:ps指向对象s成员的name,就等价于(*ps).name
}// zhangsan 20 85.5
即是:
#include
struct Stu
{
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { "zhangsan",20,85.5 };
printf("%s %d %.1lf\n", s.name, s.age, s.score);
struct Stu* ps = &s;
printf("%s %d %.1lf\n", (*ps).name, (*ps).age, (*ps).score);
printf("%s %d %.1lf\n", ps->name, ps->age, ps->score);
return 0;
}//3种方式都可以访问结构体成员
//zhangsan 20 85.5
//zhangsan 20 85.5
//zhangsan 20 85.5
当知道结构体变量的时候用 . *** 作符访问结构体成员,当得不到变量本身得到的是变量地址的时候用—>也可以找到它所指向对象的内容。
11. 表达式求值表达式求值的顺序一部分是由 *** 作符的优先级和结合性决定。
同样,有些表达式的 *** 作数在求值的过程中可能需要转换为其他类型。
一个表达式是怎么计算的,首先要看的是它的优先级和结合性。
一个表达式求值的时候到底可能会经历哪些事情呢?
11.1 隐式类型转换隐式类型转换意思是:有一些表达式在计算的时候偷偷的就发生了类型转换,而自己不知道的情况。
#include
int main()
{
char a = 5;
char b = 126;
char c = a + b;
printf("%d\n", c);//131?
return 0;
}//-125
sizeof(char)和sizeof(short)都小于sizeof(int)
当看到某一个表达式的 *** 作数自身的大小(所占空间的大小)没有达到一个整型大小的时候,它首先会发生提升,提升成整型。
char a = 5;
char b = 126;
char c = a + b;
即这里会把a提升为整型,b提升为整型,然后进行运算;
计算完之后,再把值放到c中。
所以这里发生了整型提升。 字符也是一种特殊的整型,a + b也算是一种整型运算
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型 *** 作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的 *** 作数的字节长度
一般就是int的字节长度,同时也是CPU的通用寄存器的长度。(所以说即使是char,在CPU运算的时候,计算 *** 作数的字节长度也是int,为了精度更高可以把整个整型都用掉。)
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型 *** 作数的标准长
度来计算。(CPU是中央处理器,其功能是解释计算机的指令和处理相关数据)。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令
中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转
换为int或unsigned int,然后才能送入CPU去执行运算。
实例1:
char a,b,c;
...
a = b + c;
b和c的值被提升为普通整型,然后再执行加法运算,加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的。
#include
int main()
{
char a = 5;//这里是整型数字5,不是字符‘5’,字符需要单引号引起来
//5的二进制序列:00000000000000000000000000000101——5的原码、反码、补码
//5要放到a里面去,a是char类型,是一个字节,8个比特位,所以它只能存低位的8个比特位:00000101
//a里存的真实值:00000101,这是截断(空间不够)
char b = 126;
//00000000000000000000000001111110
//b里存的真实值是:01111110
//a:00000101
//b:01111110
//当a和b相加的时候,a和b都是char类型
//表达式计算时就会发生整型提升
//因为整型提升是按照变量的数据类型的符号位来提升的
//a和b的类型都是char类型,都是有符号的char
//要对a进行提升,最高位就是符号位是0,所以高位全部补0,提升完之后就是:00000000000000000000000000000101
//对b进行整型提升,b的类型是char,是有符号的char,最高位是0,所以高位全部补0,提升完之后就是:00000000000000000000000001111110
//所以,对a、b整型提升之后的结果是:
//a:00000000000000000000000000000101
//b:00000000000000000000000001111110
//得a+b:00000000000000000000000010000011
//最高位的符号位也会参与运算。
//因为a+b要存到c中,c是char类型,只能存8个比特位
//所以c存的8个比特位是:10000011
char c = a + b;
//因为内存中放的是补码,打印出来应该是原码,所以需要由补码计算到原码
//c:10000011
//以%d的形式打印,因为%d形式表示打印整数,而c是char类型,所以会发生一次整型提升
//c要提升,看它的类型是char类型,意味着高位是符号位,是1,所以高位全补1,所以c整型提升之后的结果是:11111111111111111111111110000011
//所以c:11111111111111111111111110000011——在内存中,是补码有,接下来计算原码
//11111111111111111111111110000011——补码
//11111111111111111111111110000010——反码
//10000000000000000000000001111101——补码
//-125
printf("%d\n", c);
//这里若是以%c形式打印,像-125这种ASCII码值是不存在的,把-125作为ASCII码值打印字符的时候就是不可打印字符所以会打印问号
return 0;
}
//一个整型本来是4个字节,非要放进一个字节里就会发生截断。
上述是计算机的计算过程。
注意:
整型提升的对象二进制序列是内存中的补码。
只有在表达式求值的时候,才有可能发生整型提升,而且整型提升发生的前提是参与运算的 *** 作数的大小是达不到一个整型大小的。char和char计算,short和short计算这都会发生整型提升,只要有char和short都会提升为整型来进行运算。
①负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
②正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
③无符号整形提升,高位补0
整形提升的例子:
1、
#include
int main()
{
char a = 0xb6;//a是char a,会发生整型提升,提升之后就和0xb6不同了
short b = 0xb600;//b也是
int c = 0xb6000000;//c不会发生整型提升,不会发生截断
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}//c
//另一种理解:0xb6真实放到a里的不是这个数值,放不下会发生截断,截断之后的值和原来的值不同
例1中的a,b要进行整形提升,但是c不需要整形提升
a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真,所以程序输出的结果是:c
2、
#include
int main()
{
char c = 1;
printf("%u\n", sizeof(c));//1个字节,计算的是一个char类型的大小
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
//+c和-c都是表达式,(没有进行真的运算,但是会知道表达式结果的类型)会发生整型提升,一个字符提升到整型
//+c表达式的结果计算是个整型,-c这个表达式的结果计算也是个整型,提升完之后的结果就是4个字节
return 0;
}//1 4 4
例2中的,c只要参与表达式运算,就会发生整形提升,表达式 +c 就会发生提升,所以 sizeof(+c) 是4个字节。表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节。
补充相关知识:
#include
int main()
{
int a = 10;
int b = 20;
a + b;//表达式有两个属性,值属性和类型属性
//计算结果30就是值属性;a是整型,b是整型,算出的结果是整型int就是类型属性
//值属性是计算后才知道的,而类型属性不用计算就知道,
//如果一个是float和一个int,会把int转换为float,两个结果一定是float,所以根本不需要计算这个表达式就可以知道表达式的类型属性
//所以sizeof(+c或-c)内部的表达式不进行计算就知道它的大小,因为它有类型属性,所以根据它的类型就可以判断它最终的结果是几个字节,根本不需要计算。
return 0;
}
sizeof内部的表达式是不参与运算的(不计算)
#include
int main()
{
short s = 20;
int a = -5;
printf("%d\n", sizeof(s = a + 4));
printf("%d\n", s);
return 0;
}//2 20
对于printf("%d\n", sizeof(s = a + 4));
a+4的类型属性是整型,这个整型硬要放到short类型中,sizeof()内部表达式s=a+4,最终s说的算;
由于4个字节放到2个字节中会发生截断,去掉2个字节,只能放2个字节,所以表达式最终的类型属性结果就是short类型,2个字节;
即是最终结果放到谁中,表达式的类型属性谁说了算;
sizeof内部的表达式是不参与运算的,不会去真实的计算。
sizeof在处理的时候是在编译器里处理的,表达式的类型属性是short,所以计算的是sizeof(short)的大小,是2,即编译器就计算好了是2;
在编译器看来sizeof括号内部放的就是一个short,就是sizeof(short),如果要执行s=a+4,这样的表达式是在运行期间才执行的,编译和链接是不会执行表达式的;
因为在编译期间已经内部的表达式是不会参与运算的把表达式换成short了(2),sizeof整个代码在编译完之后变成2了,没有这一串代码了,所以在运行的时候就没法执行a+4了;
所以在sizeof内部的表达式是不参与运算的。
数据类型有:
char,1字节,参与运算时会发生整型提升
short,2字节,参与运算时会发生整型提升
int
long
long long
float
double
如果一个long和一个int类型计算、一个long和一个long long计算、一个int和一个float计算,一个int和double类型变量的计算等等,int,long,long long,float,double这些类型的变量在计算的时候它们会发生算术转换。
如果某个 *** 作符的各个 *** 作数属于不同的类型,那么除非其中一个 *** 作数的类型转换为另一个 *** 作数的类型,否则 *** 作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
这里面没有大小小于char还是short类型的,没有提及到小于4字节的,都是4字节以上的。
意思是当计算的时候有一个int类型的变量和一个unsigned int类型的变量的时候,会把int转换为unsigned int,这种类型转换就叫算术转换。如果一个int类型的变量和一个float类型的变量,会把int类型的变量转换为float类型的变量,这也称算术转换。
所以,小于4字节的两个类型运算时会发生整型提升,而算术转换发生在4字节及4字节以上。
谁转换到谁——在上面层次体系中由下面的类型向上面的类型转换。如一个float类型变量和一个double类型的变量运算时会把float类型转换为double类型;一个unsigned long Int类型变量和一个float类型的变量运算时会把unsigned long int 类型转换为float类型。
#include
int main()
{
int a = 3;
float f = 5.5;
float r = a + f;
//两个类型不同的变量进行运算时会发生算术转换,需要把a的int转换为float类型,然后才可以参与运算
return 0;
}
如果某个 *** 作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个 *** 作数的类型后执行运算。
警告:算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f; 隐式转换,会有精度丢失
复杂表达式的求值有三个影响的因素。
1. *** 作符的优先级;
2. *** 作符的结合性;
3. 是否控制求值顺序。
两个相邻的 *** 作符先执行哪个——取决于他们的优先级。优先级高的先计算,优先级低的后计算。
如果两者的优先级相同,取决于他们的结合性。
#include
int main()
{
int a = 10;
int b = 20;
int c = a + b * 20;//这里两个相邻的 *** 作符,先计算*,因为*比+的优先级要高
int d = a + b + 10;//左边+,右边+,两者的优先级相同,相邻 *** 作符优先级相同的情况下,结合性说了算,
//因为+的结合性是从左向右结合,即左边计算完之后计算右边
return 0;
}
*** 作符优先级部分表格:
因为有些 *** 作符本身就可以控制求值顺序。
如
1、 *** 作符:&&——左边为假时,右边就不算了。
2、||——左边为真,右边就不用算了。
3、条件 *** 作符也是进行选择的,如果表达式1成立,表达式2执行表达式3不执行,表达式1不成立,表达式2不执行,表达式3执行;
4、逗号 *** 作符也会控制求值顺序的,整个逗号表达式从左向右依次执行计算,但真正起到最终决定性的因素是只有最后一个表达式结果。
所以它们会在代码里选择部分进行执行,控制了表达式的求值、执行顺序。
知道了表达式求值: *** 作符的优先级, *** 作符的结合性和是否控制求值顺序这三个影响的因素之后,那么由此根据这三个因素给定一个表达式就能确定这个表达式的求值的唯一计算路径呢?
——不是
表达式求值有了优先级,结合性,是否控制求值顺序这三个特性之后都有可能没办法确定一个唯一的计算路径。
一些问题表达式:
1、表达式的求值部分由 *** 作符的优先级决定。
如何确定这个代码的计算:
a* b + c * d + e * f;
在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不能决定第三个*比第一个+早执行。
所以表达式的计算机顺序就可能是:
a*b + c*d + e*f
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
同一个表达式计算路径已经产生不同,没有确定唯一的计算路径——代码有问题。
虽然计算顺序不同结果相同(这是把a,b,c,d,e,f都想象成是变量),而把a,b,c,d,e,f想象成是各自的表达式,而这些表达式先计算后计算会互相产生影响,所以当一个表达式没办法确定唯一的计算路径的时候,代码就存在潜在的问题。
所以,如果使表达式产生唯一的一条明确的计算路径,代码就会没有问题。
2、表达式要能够确定计算的唯一结果。
int c = 5;
c + --c;
计算(顺序)路径明确唯一,但是取值有问题(计算结果有差异)——代码有问题。
+和-- *** 作符的优先级:查阅表格,--在+的上面,所以--的优先级比+高,即先算--再算+。
--和+的左右 *** 作数都是c,是有关联的,取值时机(左 *** 作数有可能在--c之前已经取值为5了,也有可能在--c之后取值就为4了)不同结果不同。
同上, *** 作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+ *** 作符的左 *** 作数的获取在右 *** 作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
3、非法表达式
#include
int main()
{
int i = 10;
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
在不同编译器中测试结果截然不同——编译器凌乱,错误代码。
这里所有 *** 作符几乎都有左右 *** 作数,而这些左右 *** 作数又互相干扰。
4、
#include
int fun()
{
static int count = 1;//count这个变量出了这个大括号(作用域)是不销毁的,所以下次进来是上一次计算的值。
return ++count;
}
//fun第一次调用返回的是2,第二次调用返回的是3,第三次调用返回的是4……
//即每次调用返回结果不一样,因为coount变量会产生累计的效果
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);//输出多少?
return 0;
}
虽然在大多数的编译器上求得结果都是相同的:-10(2-3*4得到)
但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过 *** 作符的优先级得知:先算乘法,
再算减法。但是函数的调用先后顺序无法通过 *** 作符的优先级确定。它没有因为*而先调用后面fun,不知道这三个函数是哪一个函数先调用的,可能是4-2*3,可能是2-3*4,也肯能是3-2*4等调用顺序不同结果不同——问题代码。
5、代码相同,执行结果不同,因为计算顺序不一样。
#include
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
//尝试在linux 环境gcc编译器,VS2013环境下都执行,看结果。
Linux环境的结果:10 4;
Vs2019环境的结果:12 4。
简单看一下汇编代码,就可以分析清楚:
计算机中常见的寄存器:eax,ebx,ecx,edx,ebp;后面两个一般用来存放地址。
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠 *** 作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
总结:我们写出的表达式如果不能通过 *** 作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)