【重生之我要苦学C语言】操作符详解、结构体与表达式求值过程
操作符详解
操作符的分类
- 算术操作符:+、-、*、/、%
- 移位操作符:<<、 >>
- 位操作符::&、^
- 赋值操作符:=、+=、-=、=、/=*、%=、<<=、>>=、&=、|=、^=
- 单目操作符: !、++、–、&、*、+、-、~、sizeof、(类型)·
- 关系操作符:>、>=、<、<=、==、!=·
- 逻辑操作符:&&、||
- 条件操作符: ? 、:
- 逗号表达式: ,
- 下标引用: []
- 函数调用: ()
- 结构成员访问: . 、->
二进制和进制转换
2进制、8进制、10进制、16进制是数值的不同表示形式
比如:数值15的各种进制的表示形式:
- 15的2进制:1111
- 15的8进制:17
- 15的10进制:15
- 15的16进制:F5
16进制的数值之前写:0x
16进制:0~15(10 ~15分别用a ~f表示)
8进制的数值之前写:0
8进制:0~7
二进制:
.2进制的数字每一位都是0~1的数字组成
.2进制中满2进1
二进制转化成十进制
10进制的数字从右向左是个位、十位、百位.…,分别每一位的权重是10°,10’,10…如下图:
百位 十位 个位
10进制的位 1 2 3
权重 10^2 10^1 10^0
权重值 100 10 1
求值 1*100 + 2*10* + 3*1 = 123
.
2进制的每一位的权重,从右向左是: 2°,2, 2*2…
2进制的位 1 1
1 0 权重 2^3 2^2 2^1 2^0 权重值 8 4 2 1
2进制的位 1 1 0 1
权重值 8 4 2 1
权重 2^3 2^2 2^1 2^0
求值 1^8 + 1^4 + 0^2 + 1^1 = 13
十进制转二进制
2 125 余数为1
2 62 余数为0,
2 31 余数为1
2 15 余数为1.
2 7 余数为1,
2 3 余数为1
2 1 余数为1
0
由下往上依次所得的余数就是10进制转换出的2进制
10进制的125转换的2进制:1111101
2进制转8进制和16进制
2进制转8进制
8进制的数字每一位是0~ 7 的 ,0~7的数字,各自写成2进制,最多有3个2进制位就足够了,比如7的二进制是
111,所以在2进制转8进制数的时候,从2进制序列中右边低位开始向左每3个2进制位会换算一个8进制位,剩余不够3个2进制位的直接换算
如:2进制的 01101011,换成8进制:0153,0开头的数字,会被当做8进制。
2进制 0 1 1 0 1 0 1 1
8进制 1 5 3
如果想将八进制转换成二进制,将以上过程倒过来即可
2进制转16进制
16进制的数字每一位是0~ 9,a ~ f 的,0~ 9,a~f的数字,各自写成2进制,最多有4个2进制位就足够了,比如f的二进制是1111,所以在2进制转16进制数的时候,从2进制序列中右边低位开始向左每4个2进制位会换算一个16进制位,剩余不够4个二进制位的直接换算
如:2进制的01101011,换成16进制:0x6b,16进制表示的时候前面加0x
2进制 0 1 1 0 1 0 1 1
16进制 6 b
如果想将八进制和十六进制转化为十进制,有以下两种方法:
- 直接算:
如:0x6b
6 * 16+12 * 1 - 将八进制和十六进制转化为二进制后,再进行转化
(八进制转化为十六进制同理,可借助二进制来完成)
原码、反码、补码
整数的2进制表示方法有三种,即原码、反码和补码
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当做符号位,剩余的都是数值位
符号位都是用0表示“正”,用1表示“负”
正整数的原、反、补码都相同
例:
0 0000000 00000000 00000000 00001010 ——原码
0 0000000 00000000 00000000 00001010 ——反码
0 0000000 00000000 00000000 00001010 ——补码
负整数的三种表示方法各不相同,需要计算:
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码
补码:反码+1就得到补码
补码得到原码也是可以使用:取反,+1(或-1,取反)的操作
例:
1 0000000 00000000 00000000 00001010 ——原码
1 1111111 11111111 11111111 11110101——反码
1 1111111 11111111 11111111 11110110——补码
无符号整数的三种2进制表示相同,没有符号位,每一位都是数值位
原码:对外展示
反码:中间状态
补码:内存中存储
在计算机系统中,数值一律用补码来表示和存储。
原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)
此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
例:
原码:1+(-1)
00000000 00000000 00000000 00000001——1
10000000 00000000 00000000 00000001——(-1)
10000000 00000000 00000000 00000010——(-2)
显然是错误的
补码:
00000000 00000000 00000000 00000001——1
11111111 11111111 11111111 11111111——(-1)
00000000 00000000 00000000 00000000——0
移位操作符
<<左移操作符
>>右移操作符
注:移位操作符的操作数只能是整数,是双目操作符
左移操作符
移位规则:左边抛弃、右边补0
假设num=10
00000000 00000000 00000000 00001010——原码
00000000 00000000 00000000 00001010——反码
00000000 00000000 00000000 00001010——补码
将num向左移动一位:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int num = 10;
int n = num << 1;
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}
结果为:
移位具体过程如下:
由输出结果可知,移位并不改变num本身的值
如果想要num改变:
num = num << 1;
或
num <<= 1;
右移操作符
移位规则:
右移运算分两种:
- 1.逻辑右移:左边用0填充,右边丢弃
- 2.算术右移:左边用原该值的符号位填充,右边丢弃
- 常见的编译器上采用的都是算术右移
- 到底使用哪种右移方式,C语言标准没有规定,取决于编译器
num=10,将num向右移动一位,采用算术右移:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int num = 10;
int n = num >> 1;
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}
结果为:
num=-1时,采用逻辑右移:
移位后仍是补码,应变为原码
注意:对于移位运算符,不要移动负数位,这是标准未定义的
位操作符: &、I、^、~
位操作符有:
& //按位与——(双目操作符)二进制位
| //按位或
^ //按位异或
~ //按位取反——单目操作符
注: 他们的操作数必须是整数
区分:
&&逻辑与 ——双目操作符
||逻辑或
按位与&:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int a = 3;
int b = -5;
int c = a & b;//按位与
//00000000 00000000 00000000 00000011 3的原码
//00000000 00000000 00000000 00000011 3的反码
//00000000 00000000 00000000 00000011 3的补码
//
//10000000 00000000 00000000 00000101 -5的原码
//11111111 11111111 11111111 11111010 -5的反码
//11111111 11111111 11111111 11111011 -5的补码
//
//00000000 00000000 00000000 00000011 3的补码
//11111111 11111111 11111111 11111011 -5的补码
//00000000 00000000 00000000 00000011 补码
// 对应二进制位上有0,则为0;两个同时为1,则为1
printf("c= %d\n", c);
return 0;
}
输出为:
按位或| :
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int a = 3;
int b = -5;
int c = a | b;//按位或
//00000000 00000000 00000000 00000011 3的补码
//11111111 11111111 11111111 11111011 -5的补码
//11111111 11111111 11111111 11111011 补码
// 对应二进制位上有1,则为1;两个同时为0,则为0
printf("c= %d\n", c);
return 0;
}
结果为:
按位异或^ :
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int a = 3;
int b = -5;
int c = a ^ b;//按位异或
//00000000 00000000 00000000 00000011 3的补码
//11111111 11111111 11111111 11111011 -5的补码
//11111111 11111111 11111111 11111000 补码
// 对应二进制位上相同为0,相异为1
printf("c= %d\n", c);
return 0;
}
输出为:
按位取反~ :
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int a = 0;
int b = ~a;
printf("%d\n", b);//按位取反
printf("%d\n", a);//按位取反
//00000000 00000000 00000000 00000000
//11111111 11111111 11111111 11111111
//将补码的所有二进制位取反
return 0;
}
输出为:
练习1:不创建临时变量,实现两个整数的交换
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int a =3;
int b = 5;
//将a和b的内容交换一下
printf("交换后:a=%d b=%d\n", a, b);
a = a + b;
b = a - b;//得到原来的a
a = a - b;//得到原来的b
printf("交换后:a=%d b=%d\n", a,b);
return 0;
}
缺点:当a,b都很大的时候,这种方法可能存在溢出的风险
优点:效率高,代码的可读性更好
应改为:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int a =3;
int b = 5;
//将a和b的内容交换一下
printf("交换后:a=%d b=%d\n", a, b);
a = a ^ b;//a'=a^b
b = a ^ b;//得到原来的a b=a'^b=a^b^b=a
a = a ^ b;//得到原来的b a=a'^a=a^b^a=b
printf("交换后:a=%d b=%d\n", a,b);/
return 0;
}
a^a = 0
0^a = a
3^3^5 = 5
3^5^3 = 5
//异或是支持交换律的
缺点:
- 1.这种方法只能针对整数交换
- 2.这种方式可读性不高
练习2 :求一个正数存储在内存中的二进制中1的个数
方法一:
因为存储方式为二进制,故取出每一位数字应当%2
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int count = 0;
int n = 15;
while (n) {
if (n % 2 == 1) {
count++;
}
n /= 2;
}
printf("%d", count);
return 0;
}
当n为正数时,成立,但是当n为负数时,却不成立,故该方法有局限性
方法二:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int count = 0;
int n = -1;
int i = 0;
for(i = 0;i < 32;i++) {
if ((n >> i) & 1 == 1) {
count++;
}
}
printf("%d", count);
return 0;
}
n&1——>1 最低位是1
n&1——>0 最低位是0
假设n为:
00010101 00010101 01010001 00010011
1:
00000000 00000000 00000000 00000001
所以只需要在每次判断最低位之后,向右移动一位,使当前位置成为最低为即可
方法三:
先看一个式子:
n=n&(n-1)
假设n=14,即1110
n-1:1101
按位与后:n=1100
n-1: 1011
按位与后:n=1000
n-1: 0111
按位与后:n=0000
这个表达式会将n的二进制表示中最右边的1去掉
所以,在n变成0之前,这个表达式能执行几次,说明n的二进制表达式中有几个1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int count = 0;
int n = -1;
int i = 0;
while (n) {
count++;
n = n & (n - 1);
}
printf("%d", count);
return 0;
}
练习3:判断n是否为2的幂次方数
2:0010
4:0100
8:1000
16:10000
32:100000
找规律:凡是2的幂次方,其二进制表达式中只含一个1
即判断:
if ((n & (n - 1)) == 0)
即可
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int n = 8;
if ((n & (n - 1)) == 0) {
printf("yes");
}
else printf("no");
return 0;
}
练习4:二进制位置0或者置1
编写代码将13(n)二进制序列的第五位改为1,再改回为0
00001101——>00011101
按位或00010000(1向左移动四位)即可
n|(1<<4)
代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int n = 13;
n |= (1 << 4);
printf("%d", n);
return 0;
}
改回来:
00011101——>00001101
按位与11101111(1向左移动四位后按位取反)即可
n & ~(1 << 4);
总代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int n = 13;
n |= (1 << 4);
n &= ~(1 << 4);
printf("%d", n);
return 0;
}
逗号表达式
exp1,exp2,exp4,……expN
逗号表达式,就是用逗号隔开的多个表达式,从左到右依次执行,整个表达式的结果是最后一个表达式的结果
例1:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("%d", c);
return 0;
}
过程如下:
a>b 不成立,但对a,b的值没有影响
a=b+10,a变为12
a,对a,b的值没有影响
b=a+1,b=13
将b的值赋给c
所以c应该为13
例2:
if (a = b + 1, c = a / 2, d > 0) {
//真正为if语句判断的是d>0
}
例3:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(){
a = get_val();
count_val(a);
while (a > 0) {
a = get_val();
count_val(a);
}
return 0;
}
冗余
用逗号表达式改写:
while (a = get_val(), count_val(a),a > 0) {
}
下标访问操作符[]
操作数:一个数组名+一个索引值(下标)
双目操作符
arr[5] //arr和5是[]的两个操作数
函数调用操作符
接受一个或多个操作数:第一个操作数是函数名,第二个操作数就是传递给函数的参数
printf("hehe");//()为函数调用操作符
//printf和hehe是()的两个操作数
Add(3,5);//调用自定义函数
//Add,3,5为()的操作数
//()最少有一个操作数,就是函数名
结构成员访问操作符
结构体
C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学生,这时单一的内置类型是不行的,描述一个学生需要名字、年龄、学号、身高、体重等……
C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。
结构是一些值的集合,这些值称为成员变量
(数组是一组一些相同类型元素的集合)
结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至是其他结构体。
结构的声明,定义与初始化
struct tag{
member-list;
}variable-list;
struct:关键字
tag:名字
member-list:成员列表(1个/多个成员)
variable-list:变量列表(可有可无)
例:
描述一个学生:
名字——char name[20];
年龄——int age;
性别——char sex[6];//男/女/保密 一个汉字占两个字符
学号——char id[12];
#include <stdio.h>
//声明一个结构体类型
struct Stu {
char name[20];
int age;
char sex[6];
char id[12]
}s4;//s4是一个全局结构体变量
struct Stu s3;//s3是一个全局结构体变量
int main(){
struct Stu s1={"张三",20,男,2024010203};//s1是一个局部结构体变量
//按照默认顺序初始化
struct Stu s2={.age=22,.id="2024010203",.name="lisi",sex="女"};//s2是一个局部结构体变量
//指定顺序初始化
return 0;
}
结构体中是存放多个成员的,初始化也使用{}
结构体嵌套:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//声明一个结构体类型
struct Point {
int x;
int y;
};
struct Data {
int n;
struct Point p;
double d;
};
int main(){
struct Data data = { 100,{10,20},3.14 };
return 0;
}
结构成员访问操作符
结构体成员的直接访问
结构体成员的直接访问通过点操作符(.)进行访问,点操作符接受两个操作数
printf("%s\n", s1.name);//结构体变量.结构体成员的名字
s1.age=18;
printf("%d\n", s1.age);
输入
struct Stu s1 = { 0 };
scanf("%s %d %s %s", s1.name, &(s1.age), s1.sex, s1.id);
//name,sex,id为数组名,本来就是地址,不需要取地址,age是整型变量,需要取地址
结构体成员的间接访问
->是依赖指针的,会在后期指针部分进行说明
操作符的属性:优先级、结合性
优先级(相邻操作符)
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行
各种运算符的优先级是不一样的
https://zh.cppreference.com/w/c/language/operator_precedence
结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(=)
常见运算符优先级从高到低:
- 圆括号(())
- 自增运算符(++ )
- 自减运算符(–)
- 单目运算符(+和-)
- 乘法(*),除法(/)
- 加法(+),减法(-)
- 关系运算符(<、>等)·
- 赋值运算符(=)
圆括号优先级最高,可以改变其他运算符的优先级
表达式求值
整型提升
C语言中整型算术运算总是至少以缺省(默认)整型类型的精度来进行的
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
如何进行整型提升?
- 1.有符号整数提升是按照变量的数据类型的符号位来提升的
- 2.无符号整数提升,高位补0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int a = 10;
short s = 5;
int b = s + a;//short会被转化为int类型,再计算
char c1 = 5;
char c2 = 126;
char c3 = c1 + c2;//char会被转化为int类型,再计算
//char:-128~127 126+5=127+1+1+1+1=-125
//char类型也属于整型,因为字符在存储的时候,存储的是ASCII码值
//5:00000000 00000000 00000000 00000101
//c1:00000101
//126:00000000 00000000 00000000 01111110
//c2:01111110
//发生整型提升
//c1:00000000 00000000 00000000 00000101
//c2:00000000 00000000 00000000 01111110
//c1+c2:00000000 00000000 00000000 10000011
//c3:10000011
//%d打印十进制的有符号整数
//c3发生整型提升:11111111 11111111 11111111 10000011(补码)
//原码:10000000 00000000 00000000 01111101
//-125
return 0;
}
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算
问题表达式
虽然操作符又优先级和结合性,但是依然可能写出一些没办法确定唯一计算路径的表达式
例1:
a* b + c * d + e * f;
//只能确定先计算乘法再计算加法,但无法确定乘法之间的先后顺序
例2:
c + --c;
//操作符的优先级只能决定自减--的运算在+的运算的前面,但是没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的
例3:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
在VS(msvc)中,输出为:
而在devC++(gcc)中,输出为:
END……
不管前方的路有多苦,只要走的方向正确,
不管多么崎岖不平,都比站在原地更接近幸福。
—《千与千寻》