4.4.2:方法二
在介绍方法二之前大家需要直到异或的三个性质:
1:a^a=0 2:0^a=a 3:异或满足交换律和结合律,即:a^b^c=a^(b^c),a^b=b^a
int main() { int a = 3; int b = 5; printf("%d %d\n", a, b); a = a ^ b; b = a ^ b;//也就是:(a^b)^b=a^(b^b)=a^0=a,此时就把a的值放到b里面去了,b里面存的就是a a = a ^ b;//也就是:(a^b)^a=a^a^b=0^b=b,此时就把b的值存到a里面去了,a里面存的就是b printf("%d %d\n", a, b); return 0; } //结果: 3 5 5 3
五:赋值操作符
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
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;
5.1:复合赋值符
+= -= *= /= %= <<= >>= &= |= ^=
这些运算符都可以写成复合的效果。
int x = 10; x = x+10; x += 10;//复合赋值 //其他运算符一样的道理。这样写更加简洁。
六:单目操作符
单目操作符就只有一个操作数
! 逻辑反操作 - 负值 + 正值 & 取地址 sizeof 操作数的类型长度(以字节为单位) ~ 对一个数的二进制按位取反 -- 前置、后置-- ++ 前置、后置++ * 间接访问操作符(解引用操作符) (类型) 强制类型转换
补充:如果把一个负数放到了无符号类型的变量里会发生什么呢?比如:
int main() { unsigned int a = -10;//把-10放到无符号类型的变量a里面去 //因为内存中存的是补码,这里是-10为负数,要先求出-10的补码 //10000000000000000000000000001010(-10的原码) //11111111111111111111111111110101(-10的反码) //11111111111111111111111111110110(-10的补码) //此时a里面存的就是:11111111111111111111111111110110(-10的补码) //由于此时的a是无符号整型,所以对于a来说他就没有符号位这个概念,存在a里面的所有数都是正整数,原、反、补都一样,所以在a看来11111111111111111111111111110110就是一个原码,会直接把它转换成对应的数字 printf("%u\n", a);//无符号整型用格式符u打印 return 0; } //结果: 4294967286
6.1:取地址符&
//取地址符& int main() { int a = 10; printf("%p\n", &a); int* pa = &a;//取出变量a的地址放在整型指针变量pa里面 printf("%p\n", pa); char ch = 'w'; printf("%p\n", &ch); char* pc = &ch;//取出变量ch的地址放在字符型指针变量pc里面 printf("%p\n", pc); char* p = "abcdef";//字符串常量的值其实就是首字符的地址,所以这里我们用一个字符类型的指针变量来接收 printf("%p\n", p); printf("%c\n", *p); return 0; } //结果: 012FF6F8 012FF6F8 012FF6E3 012FF6E3 00F77BD0 a
6.2:解引用操作符*
//解引用操作 int main() { int a = 10; int* pa = &a;//把a的地址放在pa变量里 *pa=20;//通过解引用操作就可以取到a的值,并对a的值进行修改(注意:是对地址进行解引用) printf("%d\n", a); return 0; }
int main() { *(int*)0x0012ff40=10;//随便编写了一个地址对其进行解引用是非法的 return 0; }
6.3:sizeof操作符
sizeof是关键字也是操作符,是计算变量或者类型的大小单位是字节
int main() { int a = 10; printf("%d\n", sizeof(a));//计算一个变量的大小 printf("%d\n", sizeof a);//这里可以不带括号,说明sizeof不是函数,因为函数即使没有参数也要带括号 printf("%d\n", sizeof(int));//计算一个类型的大小,这里sizeof后面的括号不能省略 int arr[10] = { 0 }; printf("%d\n", sizeof arr);//计算一个数组的大小 printf("%d\n", sizeof(int[10]));//其中int [10]是数组arr的类型 return 0; }
注意:sizeof内部的表达式是不参与计算的。为什么呢?
因为我们写的是一个.c的代码,最终运行的是一个.exe可执行程序,在这之间会经过编译、链接,而对sizeof的操作是在编译阶段就进行了,而表达式是在最终的可执行程序里运行的,到最终运行的时候就不再有sizeof和它后面的表达式了,因此sizeof后面的表达式根本就没有执行。
int main() { int a = 10; short s = 5; printf("%d\n", sizeof(s = a + 3));//a+3=13赋给s的这个动作是不会发生的,如果一个整形的数据非要放到一个短整型里会发生截断,所以最终的大小还是s说了算 printf("%d\n", s); return 0; } //结果: 2 5
6.4:~按位取反操作符
//~按位取反 int main() { int a = 0; //00000000000000000000000000000000 //11111111111111111111111111111111(按位取反,得到的是一个补码) //10000000000000000000000000000000 //10000000000000000000000000000001(打印时需要的原码) printf("%d\n", ~a);//结果就是-1 return 0; }
6.5:综合练习
//综合练习 int main() { int a = 9; //00000000000000000000000000001001 //把9的二进制位第五位上的0变成1 //首先想到按位或操作 //00000000000000000000000000010000 其实就是:1<<4 //00000000000000000000000000011001(按位或的结果就是我们想要的),其实就是25 a |= (1 << 4); printf("%d\n", a);//打印出来的就是25 //把a的二进制中的第五位改回来,变0 //00000000000000000000000000011001 // 可以用异或操作 //00000000000000000000000000010000 其实就是:1<<4 //00000000000000000000000000001001(按位异或的结果就是我们想要的) a ^= (1 << 4); printf("%d\n", a); //还可以利用按位与操作 //00000000000000000000000000011001 //11111111111111111111111111101111 其实就是:~(1<<4) //00000000000000000000000000001001(按位与的结果就是我们想要的) a &= ~(1 << 4); printf("%d\n", a); return 0; } //结果: 25 9 9
6.6:++、–操作符
6.6.1:后置
先使用,再进行++或者–操作
int main() { int a = 10; int b = a++;//后置++,先使用,再++ //相当于:int b=a;a=a+1; printf("a=%d\n", a); printf("b=%d\n", b); return 0; } //结果: a=11 b=10
6.6.2:前置
先进行++或者–操作,再使用
int main() { int a = 10; int b = ++a;//先进行++操作,再把a加之后的值赋值给b printf("a=%d\n", a); printf("b=%d\n", b); return 0; } //结果: a=11 b=11
++和- -有副作用,会让自身的值也发生变化
int main() { int a = 10; int b = ++a;//b=11,a=11 int a = 10; int b = a + 1;//b=11,a=10 return 0; }
6.7:强制类型转化
强制类型转换是在迫不得已的时候才用,不要故意去强制类型转换,强扭的瓜不甜!
int main() { int a = (int)3.14;//把一个浮点型强制转化成整型,直接取整数部分,不会进行四舍五入 printf("%d\n", a); return 0; }
6.8:sizeof和数组
void test1(int arr[])//数组传参,形参可以写成数组,也可以写成指针 { printf("%zd\n", sizeof(arr));//(2)这里其实是计算整型指针的大小,在×86(32位机)的环境下指针的大小是4个字节 } void test2(char ch[]) { printf("%zd\n", sizeof(ch));//(4)这里的ch本质上也是指针,所以在×86(32位机)的环境下大小还是4个字节 } int main() { int arr[10] = { 0 }; char ch[10] = { 0 }; printf("%zd\n", sizeof(arr));//(1)计算的是数组的大小,单位是字节10*4=40 printf("%zd\n", sizeof(ch));//(3)计算的是数组的大小,单位是字节10*1=10 test1(arr);//数组名是首元素地址 test2(ch);//数组名是首元素地址 return 0; } //结果: 40 10 4 4
补充:
void test1(int arr[], int sz)//数组传参,形参用数组来接收 { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]); } printf("\n"); } void test2(int* arr, int sz)//数组传参,形参用指针来接收 { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", arr[i]);//这里的arr[i]其实就是*(arr+i),加i就是跳过i个整型,然后解引用 } printf("\n"); } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); test1(arr, sz); test2(arr, sz); return 0; } //结果: 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
七:关系操作符
> >= < <= != 用于测试“不相等” == 用于测试“相等”
需要注意:=和==,前者是赋值操作符
八:逻辑操作符
&& 逻辑与 || 逻辑或
逻辑与就相当于日常生活中说的并且,逻辑或就相当于日常生活中的或者,逻辑与和逻辑或只关注真假,在C语言中0表示假,非0表示真。逻辑表达式的值只有真或者假,真用1表示,假用0表示
a | b | a&&b | a l l b |
真 | 真 | 真 | 真 |
真 | 假 | 假 | 真 |
假 | 真 | 假 | 真 |
假 | 假 | 假 | 假 |
8.1:一道例题
int main() { int i = 0, a = 0, b = 2, c = 3, d = 4; i = a++ && ++b && d++;//其中a++的值是0,因为后面都是逻辑与,并且a++的值为0,对于逻辑与来说,只要有0结果就一定是0,所以此时后面是什么值已经不重要了,不会再执行后面的表达式了,因此就只执行了a++这个表达式 //对于逻辑与,只要左边为假,右边就不再计算 //对于逻辑与,只要左边为真,右边就不再计算 //i = a++||++b||d++; printf("a = %d\nb = %d\n c = %d\nd = %d\n", a, b, c, d); return 0; } //结果: a = 1 b = 2 c = 3 d = 4
九:条件操作符
exp1 ? exp2 : exp3
其中exp1、exp2、exp3分别对应三个表达式,它们三个组合成条件表达式,如果exp1为真,exp2计算,exp3不计算,条件表达式的值是exp2的结果,如果exp1为假,exp2不计算,exp3计算,条件表达式的值是exp3的结果。条件操作符也被叫做三目操作符
//把a和b的较大值赋值给m int main() { int a = 10; int b = 20; int m = 0; if (a > b) m = a; else m = b; m = (a > b ? a : b);//利用条件操作符来实现 return 0; }
十:逗号表达式
> exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
//逗号表达式 int main() { //代码1 int a = 1; int b = 2; int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式,从左到右依次计算,整个逗号表达式的结果是最后一个表达式的结果 printf("%d\n", c);//c最终是13 return 0; } //结果: 13 //代码2 if (a =b + 1, c=a / 2, d > 0)//最终决定if真假的是最后一个表达式d>0,但如果前面的表达式中出现了d,就会影响最终d>0的结果,因此还是要老老实实的从左往右计算 //代码3 a = get_val(); count_val(a); while (a > 0) { //业务处理 a = get_val(); count_val(a); } //如果使用逗号表达式,改写: while (a = get_val(), count_val(a), a>0) { //业务处理 }
、
十一:下标引用、函数调用和结构成员
11.1:下标引用操作符
[ ] 下标引用操作符
下标引用操作符有两个操作数:一个数组名 + 一个索引值
//下标引用操作符 int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", arr[4]);//[]就是下标引用操作符 return 0; }
11.2:函数调用操作符
( ) 函数调用操作符
函数调用操作符接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。当函数没有参数的时候就只有函数名这一个操作数
//函数调用操作符 int main() { int len = strlen("abcdef");//()就是函数调用操作符,操作数:strlen,"abcdef".对于函数调用操作符来说至少有一个操作数函数名(函数没有参数的时候), return 0; }
11.3:结构成员访问操作符
访问一个结构的成员
. 结构体变量.成员名 -> 结构体指针->成员名
//结构成员访问操作符 //结构体——自定义类型(聚合体) //生活中有些对象要被描述,不能简单的使用单个内置类型 //书:书名,作者,出版社,定价,…… //类型 struct Book { char name[20]; char author[30]; int price; }; void print1(struct Book* p) { printf("《%s》%s %d\n", (*p).name, (*p).author, (*p).price); printf("《%s》%s %d\n", p->name,p->author,p->price);//结构体指针->成员名 } int main() { struct Book b1 = { "大话数据结构","佚名",66 }; struct Book b2 = { "C语言","佚名",65 }; printf("《%s》%s %d\n", b1.name, b1.author, b1.price);//结构体变量.成员名 printf("《%s》%s %d\n", b2.name, b2.author, b2.price); print1(&b1); return 0; }
十二:表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定,同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.1:隐式类型转换
隐式类型转换通俗来说就是偷偷的发生类型转化,我们并没有感知到,但他确确实实在时刻发生着。
C的整型算术运算总是至少以缺省整型类型的精度来进行的。,为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
12.2:如何进行整型提升?
整型提升是按照变量的数据类型的符号位来提升的
//整型提升 //char short int long…… // 1 2 4 //整型提升针对的是类型的字节数小于整形的,char或者short类型的变量在参与运算的时候,首先会把它们提升为整型 int main() { //当前编译器(vs)char-->signed char(有符号的char,所以它的最高位就是符号位) char a = 3;//首先3是一个整数,那它一定是4个字节 //00000000000000000000000000000011,要把这个存到a里面去,但是a是一个字符型,就只有一个字节,肯定存不下这么多,只能存下最右边的八个比特位 //00000011 - a中存的 //四个字节的数据非要放到一个字节的变量里面去,这叫截断 char b = 127; //00000000000000000000000001111111 //11111111 - b中存的 char c = a + b;//在计算的时候,首先会把a和b转化为int类型,转化完之后再去加,加的结果也是一个int类型,最终把这个int类型的结果放到c里面 //00000011 - a中存的 //01111111 - b中存的 //接下来就要发生整型提升 //00000000000000000000000000000011 -- a经过提升后 //00000000000000000000000001111111 -- b经过提升后 //接下来就可以进行相加的操作了 //00000000000000000000000010000010 -- a+b的结果 //要把这个(四个字节)存到c(字符型,一个字节)里面去,会发生截断 //10000010 -- c中存的 printf("%d\n", c); //%d -- 打印十进制的整数,所以这里以%d来打印一个字符型变量c也会进行整型提升 //10000010 -- c中存的 //11111111111111111111111110000010 -- c经过提升后,注意:这还是内存中存储的,所以这一串二进制是补码,而我们打印需要的是原码 //10000000000000000000000001111101 -- 符号位不变,其他位按位取反、 //10000000000000000000000001111110 -- 原码(+1) //对应二进制:-126 return 0; } //结果: -126
补充:char - - 有符号的char的取值范围是:-128~127 无符号的char的取值范围是:0~255 short - - 有符号的short的取值范围是:-32768~32767 无符号的short的取值范围是:0~65535
char类型对应一个字节对应八个比特位,所以一个字符型变量它可以存储的二进制序列是从“00000000~11111111”,一共有256个序列。
12.3:整型提升的例子
//整型提升的例子: int main() { char a = 0xb6;//对应二进制:10110110 //00000000000000000000000010110110 -- 0xb6 //10110110 -- a中存的 short b = 0xb600; int c = 0xb6000000; if (a == 0xb6) //这里:a == 0xb6 是一个表达式,在对表达式进行计算的时候,就会进行整型提升 //10110110 -- a中存的 //接下来要对a进行整型提升 //11111111111111111111111110110110 -- a经过整型提升后 //00000000000000000000000010110110 -- 0xb6 //显然:a和0xb6并不相等,所以 表达式:a == 0xb6 的值位假,就不会执行下面的打印a printf("a\n"); if (b == 0xb600) printf("b\n"); if (c == 0xb6000000)//这里c本来就是整型,所以就不会进行整型提升 printf("c\n"); return 0; } //结果: c
//实例2 int main() { char c = 1; printf("%u\n", sizeof(c)); printf("%u\n", sizeof(+c));//这里+c就是一个表达式,只要是表达式,在参与运算的时候就会进行整型提升,提升之后就变成四个字节了 printf("%u\n", sizeof(-c)); return 0; } //结果: 1 4 4
当鼠标放到+c这个表达式上面的时候,可以看出:此时的c已经从字符型提升到无符号的整型了
12.4:算数转化
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。算数转换是针对字节数大于等于整型的类型来说的。下面的层次体系称为寻常算术转换。
long double double float unsigned long int long int unsigned int int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。也就是说:算数转换就高不就低,会把低的转换成高的,也就是把字节数小的转换成字节数大的。
12.5:操作符的属性
复杂表达式的求值有三个影响的因素。
1.操作符的优先级 2.操作符的结合性 3.是否控制求值顺序
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
12.6:一些问题表达式
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只看做单一的变量,它们各自也可以是一个表达式,并且可能有相同的变量,此时先算的可能就会影响到后算的),为了避免出现上面这种情情况,可以加括号来决定到底谁先算。
c + --c;
注释:同上,操作符的优先级只能决定自减–的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。也就是说+左边的c可能在- -c执行之前就准备好了,也可能在- -c执行之后才准备好
int c=3; c + --c; //情形一: //+左边的c在--c之前就准备好了,此时+左边的c就是3,--c的结果就是2,此时c + --c的结果就是5. //情形二: //+左边的c在--c之后才准备好,此时先执行--c,--c的结果是2,此时c变成2,然后+左边的c开始准备,所以+左边的c就是2,此时表达式c + --c的值就是4
//问题代码 int fun() { static int count = 1; return ++count; } //第一次调用返回2,第二次调用返回3,第三次调用返回4 int main() { int answer; answer = fun() - fun() * fun(); //这里我们只能知道 * 比 + 先算,但是不知道这三个fun函数到底哪一个最先调用 printf("%d\n", answer);//输出多少? return 0; }
上面这段代码在我的vs编译器上执行的结果是:-10。说明最左边的这个fun函数先调用结果是2,最后的两个fun函数到底谁先调用还是不能确定,因为不管谁先调用结果都是3*4=12.
//问题代码 int main() { int i = 1; int ret = (++i) + (++i) + (++i); printf("%d\n", ret); printf("%d\n", i); return 0; }
上面这段代码按照正常思维,我们算出来的ret值应该是9,也就是最左边的++i先算,结果是2,然后算中间的++i,结果是3,接着算最右边的++i,结果是4,最后计算:ret=2+3+4=9.但是!!!在我的vs编译器中这段代码的执行结果是:ret=12,i=4。!!!而这段代码在Linux环境的结果是:ret=10,i=4
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。
到这里,操作符的有关分享就结束啦,喜欢的话可以点赞、评论和收藏哟!