目录
[1. 操作符的分类](#1. 操作符的分类)
[2. 二进制和进制转换](#2. 二进制和进制转换)
[2.1 二进制转十进制](#2.1 二进制转十进制)
[2.1.1 十进制转二进制](#2.1.1 十进制转二进制)
[2.2 二进制转八进制和十六进制](#2.2 二进制转八进制和十六进制)
[2.2.1 二进制转八进制](#2.2.1 二进制转八进制)
[2.2.2 二进制转十六进制](#2.2.2 二进制转十六进制)
[3. 原码,反码,补码](#3. 原码,反码,补码)
[4. 移位操作符](#4. 移位操作符)
[4.1 左移操作符](#4.1 左移操作符)
[4.2 右移操作符](#4.2 右移操作符)
[5. 位操作符:&,|,^, ~](#5. 位操作符:&,|,^, ~)
[6. 单目操作符](#6. 单目操作符)
[7. 逗号表达式](#7. 逗号表达式)
[8. 下标访问[],函数调用()](#8. 下标访问[],函数调用())
[8.1 []下标引用操作符](#8.1 []下标引用操作符)
[8.2 函数调用操作符](#8.2 函数调用操作符)
[9. 结构成员访问操作符](#9. 结构成员访问操作符)
[9.1 结构体](#9.1 结构体)
[9.1.1 结构的声明](#9.1.1 结构的声明)
[9.1.2 结构体变量的定义和初始化](#9.1.2 结构体变量的定义和初始化)
[9.2 结构成员访问操作符](#9.2 结构成员访问操作符)
[9.2.1 结构体成员的直接访问](#9.2.1 结构体成员的直接访问)
[9.2.2 结构体成员的间接访问](#9.2.2 结构体成员的间接访问)
[10. 操作符的属性:优先性,结合性](#10. 操作符的属性:优先性,结合性)
[10.1 优先级](#10.1 优先级)
[10.2 结合性](#10.2 结合性)
[11.2 算术转换](#11.2 算术转换)
[11.3 问题表达式解析](#11.3 问题表达式解析)
[11.3.1 表达式1](#11.3.1 表达式1)
[11.3.2 表达式2](#11.3.2 表达式2)
[11.3.3 表达式3](#11.3.3 表达式3)
[11.3.4 表达式4](#11.3.4 表达式4)
[11.3.5 表达式5](#11.3.5 表达式5)
[11.4 总结](#11.4 总结)
1. 操作符的分类

2. 二进制和进制转换
其实我们经常能听到 2进制,8进制,16进制是数值的不同表示形式而已。
比如:数值15的各种进制的表示形式:
15的2进制:1111
15的8进制:17 017
15的16进制:F 0xf
//16进制的数值之前写:0x
//8进制的数值之前写:0
我们重点介绍一下二进制:
首先我们还是得从10进制讲起,其实10进制是我们生活中经常使用的,我们已经形成了很多常识:
·10进制中满10进1
·10进制的数字每一位第是0~9的数字组成
其实二进制也是一样的
·2进制中满2进1
·2进制的数字每一位都是0~1的数字组成
那么1101就是二进制的数字了
2.1 二进制转十进制



2.1.1 十进制转二进制


2.2 二进制转八进制和十六进制
2.2.1 二进制转八进制
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.2.2 二进制转十六进制
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

3. 原码,反码,补码
整数的 2 进制表示方法有三种,即原码、反码和补码
有符号整数的三种表示方法均有符号位和数值位**两部分,2 进制序列中,最高位的 1 位是被当做符号位,剩余的都是数值位。
符号位都是用 0 表示 "正",用 1 表示 "负"。

正整数的原码,反码,补码都相同
负整数的三种表示方式各不相同


无符号整型没有符号位,全都是数值位,因此无符号整型的储存范围比有符号整型存储范围大

原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码
反码:将原码的符号位不变,其他位次依次按位取反就可以得到反码
补码:反码+1就得到补码
补码得到原码也是可以使用:取反,+1的操作。
无符号整数的三种2进制表示相同,没有符号位,每一位都是数值位
整数在内存中存储的是补码的二进制序列,计算的时候使用的也是补码~





对于整型来说:数据存放内存中其实存放的是补码。
为什么呢?
++在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU 只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路++

4. 移位操作符
<< 左移操作符
\>> 右移操作符
注:移位操作符的操作数只能是整数




4.1 左移操作符
移位规矩:左边抛弃,右边补0
cs
// 引入标准输入输出头文件,用于使用printf等输入输出函数
#include <stdio.h>
// 主函数,程序的入口,程序从这里开始执行
int main()
{
// 定义一个整型变量num,并将其初始化为10
int num = 10;
// 将num左移1位(左移1位等价于num乘以2),计算结果赋值给整型变量n
int n = num << 1;
// 打印输出变量n的值
printf("n = %d\n", n);
// 打印输出变量num的值(左移运算不会修改原变量num本身的值)
printf("num = %d\n", num);
// 主函数返回0,表示程序正常执行结束
return 0;
} // 主函数的函数体结束





4.2 右移操作符
移位规则:首先右移运算分两种:
1.逻辑右移:左边用0填充,右边丢弃
2.算数右移:左边用原该值的符号位填充,右边丢弃
cs
// 引入标准输入输出头文件,使程序可以使用printf等输入输出函数
#include <stdio.h>
// 主函数,是C程序的入口,程序从main函数开始执行
int main()
{
// 定义整型变量num,并将其初始值设置为10
int num = 10;
// 将num右移1位(对于正数,右移1位等价于除以2取整),计算结果赋值给整型变量n
// 注意:右移运算不会修改原变量num本身的值
int n = num >> 1;
// 输出变量n的计算结果
printf("n = %d\n", n);
// 输出原变量num的值(验证num未被右移操作修改)
printf("num = %d\n", num);
// 主函数返回0,表示程序正常执行完毕
return 0;
}


警告:对于移位运算符,不要移动负数位,这个是标准未定义的

5. 位操作符:&,|,^, ~
位操作符有:
& //按位与 对应的二进制位上,两个同时为1才是1,只要有0就是0
| //按位或 对应的二进制位上,只要有1就是1,同为0才是0
^ //按位异或 对应的二进制位上,相同为0,相异为1
~ //按位取反 1变0,0变1,符号位也算
注:他们的操作数必须是整数

直接上代码:
cs
// 引入标准输入输出头文件,用于使用printf函数输出结果
#include <stdio.h>
// 主函数,C程序的执行入口
int main()
{
// 定义整型变量num1,初始值为-3(计算机中以补码形式存储)
int num1 = -3;
// 定义整型变量num2,初始值为5
int num2 = 5;
// 执行"按位与"运算:对num1和num2的补码对应位进行与操作(都为1才得1),输出结果
printf("%d\n", num1 & num2);
// 执行"按位或"运算:对num1和num2的补码对应位进行或操作(有1就得1),输出结果
printf("%d\n", num1 | num2);
// 执行"按位异或"运算:对num1和num2的补码对应位进行异或操作(不同得1,相同得0),输出结果
printf("%d\n", num1 ^ num2);
// 执行"按位取反"运算:对0的补码(全0)按位取反(变全1),有符号int下结果为-1,输出该结果
printf("%d\n", ~0);
// 主函数返回0,表示程序正常结束
return 0;
}





一道变态的面试题:
> 不能创建临时变量(第三个变量),实现两个整数的交换
cs
// 引入标准输入输出头文件,用于后续的printf输出操作
#include <stdio.h>
// 主函数,C程序的执行入口
int main()
{
// 定义整型变量a,初始值设为10
int a = 10;
// 定义整型变量b,初始值设为20
int b = 20;
// 第一步:将a和b的"按位异或"结果存到a中
// 此时a = 原a ^ 原b
a = a ^ b;
// 第二步:用当前a(原a^原b)和原b做异或,结果存到b中
// 原a^原b ^ 原b = 原a,因此b被赋值为"原a"
b = a ^ b;
// 第三步:用当前a(原a^原b)和当前b(原a)做异或,结果存到a中
// 原a^原b ^ 原a = 原b,因此a被赋值为"原b"
a = a ^ b;
// 输出交换后的a和b的值(此时a=20,b=10)
printf("a = %d b = %d\n", a, b);
// 主函数返回0,表示程序正常执行结束
return 0;
}

练习一:编写代码实现:求一个整数存储在内存中的二进制中1的个数
cs
// 方法1:统计正数二进制中1的个数(注意:此方法不适合负数)
#include <stdio.h>
int main()
{
int num = 10; // 定义待统计的整数(此处是正数10,二进制为1010)
int count = 0; // 计数器:用于记录二进制中"1"的个数,初始化为0
while(num) // 循环条件:num不为0时(num为0则二进制无1,循环结束)
{
if(num % 2 == 1) // 判断num的二进制"最低位"是否为1(取余2得1则是)
count++; // 若最低位是1,计数器加1
num = num / 2; // num除以2 → 等价于二进制"右移1位",处理下一位
}
// 输出统计结果(10的二进制是1010,含2个1)
printf("二进制中1的个数 = %d\n", count);
return 0; // 程序正常结束
}
// 方法1的问题:
// 若num是负数(比如num=-1),计算机中负数以"补码"存储,num/2等价于"算术右移"(会补符号位1),
// 导致num永远不会变为0,循环会进入死循环。因此方法1仅适用于正数。
cs
// 方法2:遍历int的32个二进制位,统计1的个数(支持任意整数)
#include <stdio.h>
int main()
{
int num = -1; // 待统计的整数(-1的补码是32个连续的1)
int i = 0; // 循环变量:用于遍历32个二进制位(从第0位到第31位)
int count = 0; // 计数器:记录二进制中"1"的个数,初始化为0
// 循环32次(int占4字节=32位),遍历每一个二进制位
for(i = 0; i < 32; i++)
{
// 1 << i:生成"第i位为1、其余位为0"的掩码
// num & (1 << i):按位与运算,结果非0 → 说明num的第i位是1
if( num & (1 << i) )
{
count++; // 若当前位是1,计数器加1
}
}
// 输出结果(-1的补码含32个1,所以结果为32)
printf("二进制中1的个数 = %d\n", count);
return 0; // 程序正常结束
}
// 方法2的特点:无论num里1的数量多少,都固定循环32次,逻辑简单但效率不够灵活
cs
// 方法3:利用"num & (num-1)"消除最右的1,统计1的个数(效率更优)
#include <stdio.h>
int main()
{
int num = -1; // 待统计的整数(-1的补码是32个1)
int count = 0; // 计数器:记录1的个数,初始化为0
// 循环条件:num不为0(当num的1被全部消除后,num会变为0,循环结束)
while(num)
{
count++; // 每循环一次,说明存在1个1,计数器加1
// 核心操作:num & (num - 1)
// 原理:num-1会把num"最右边的1"变成0,同时右边的0变成1;
// 两者按位与后,会消除num最右边的那个1
num = num & (num - 1);
}
// 输出结果(-1的补码有32个1,所以循环32次,count=32)
printf("二进制中1的个数 = %d\n", count);
return 0; // 程序正常结束
}
// 方法3的优化点:循环次数等于num中1的实际数量(比如num=10(二进制1010)仅循环2次),
// 相比方法2的固定32次循环,效率更高(尤其是1的数量较少时)
练习二:二进制位置0或者1
编写代码将13二进制序列的第五位修改为1,然后再改回0
cs
提取的文字如下:
1 13的2进制序列:00000000000000000000000000001101
2 将第5位置为1后:00000000000000000000000000011101
3 将第5位再置为0:00000000000000000000000000001101
cs
// 引入标准输入输出头文件,用于使用printf函数输出结果
#include <stdio.h>
// 主函数,C程序的执行入口
int main()
{
// 定义整型变量a,初始值为13(对应的二进制序列:00000000 00000000 00000000 00001101)
int a = 13;
// 核心操作1:将a的"第5位"(二进制位从0开始计数)置为1
// 原理:
// 1 << 4 → 将数字1左移4位,得到二进制:00000000 00000000 00000000 00010000(对应十进制16)
// "|"(按位或)运算:只要对应位有1,结果就为1 → 此操作会将a的第5位置为1,其他位保持不变
a = a | (1 << 4);
// 输出置位后的a:此时a=13+16=29,二进制为00000000 00000000 00000000 00011101
printf("a = %d\n", a);
// 核心操作2:将a的"第5位"重新置为0(复位)
// 原理:
// 1 << 4 → 先得到00000000 00000000 00000000 00010000
// ~(1 << 4) → 对其按位取反,得到11111111 11111111 11111111 11101111
// "&"(按位与)运算:只有对应位都为1,结果才为1 → 此操作会将a的第5位置为0,其他位保持不变
a = a & ~(1 << 4);
// 输出复位后的a:此时a变回13,二进制恢复为00000000 00000000 00000000 00001101
printf("a = %d\n", a);
// 主函数返回0,表示程序正常结束
return 0;
}
6. 单目操作符
单目操作符有这些:

单目操作符的特点是只有一个操作数,在单目操作符中只有&和*没有介绍,这两个操作符,我们放在学习指针的时候学习。
7. 逗号表达式

逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
cs
//代码1
#include <stdio.h>
int main()
{
// 定义整型变量a,初始赋值为1
int a = 1;
// 定义整型变量b,初始赋值为2
int b = 2;
// 核心:逗号表达式的规则------
// 1. 从左到右依次执行每个子表达式;
// 2. 整个逗号表达式的最终结果 = 最后一个子表达式的计算结果;
// 3. 执行过程中会修改变量的实际值(如a、b会被更新)
int c = (a > b, a = b + 10, a, b = a + 1);
// 逐个子表达式执行解析:
// 1. 第一个子表达式:a > b → 1 > 2 → 结果为假(值为0),无变量修改,继续执行;
// 2. 第二个子表达式:a = b + 10 → b=2 → a = 2+10=12(a被更新为12),继续执行;
// 3. 第三个子表达式:a → 直接取当前a的值(12),无变量修改,继续执行;
// 4. 第四个子表达式(最后一个):b = a + 1 → a=12 → b=12+1=13(b被更新为13);
// 最终:逗号表达式的结果 = 最后一个子表达式的结果(13),因此c被赋值为13
// 打印c的取值,验证结果
printf("c = %d\n", c); // 输出结果:c = 13
return 0;
}
cs
// 代码2:if条件中使用逗号表达式
if (a = b + 1, c = a / 2, d > 0)
// 这里的括号内是"逗号表达式",规则:
// 1. 从左到右依次执行每个子表达式;
// 2. 整个逗号表达式的"最终结果" = 最后一个子表达式的结果(即此if的判断条件)
// 逐个子表达式解析:
// ① 第一个子表达式:a = b + 1 → 将"b+1"的结果赋值给变量a(会修改a的实际值)
// ② 第二个子表达式:c = a / 2 → 用刚更新的a的值,计算"a/2"并赋值给变量c(会修改c的实际值)
// ③ 第三个子表达式(最后一个):d > 0 → 判断d是否大于0,结果为"真(非0)"或"假(0)"
// 最终:if的判断条件是"d > 0"的结果,前两个子表达式仅执行"赋值操作",不影响if的判断
{
// 若"d > 0"为真,则执行这里的代码块;否则跳过
}
cs
// 代码3:原逻辑(先获取值、计数,再循环处理)
a = get_val(); // 调用get_val()函数,获取一个值并赋值给变量a
count_val(a); // 调用count_val()函数,统计/处理变量a的值
// 循环条件:当a的值大于0时,进入循环体
while (a > 0)
{
// 业务处理:这里是具体的功能逻辑(比如对a的计算、操作等)
// ...
a = get_val(); // 再次调用get_val(),更新a的取值
count_val(a); // 再次统计/处理新的a值
}
// 原代码的特点:
// 1. 循环外、循环内都重复写了"a = get_val(); count_val(a);",代码有冗余;
// 2. 只有当a>0时,才会进入循环执行业务处理
cs
// 用逗号表达式改写:将"获取a、统计a"整合到while的循环条件中,消除冗余
// while的条件是一个逗号表达式,执行规则:
// 1. 从左到右依次执行前两个子表达式;
// 2. 用最后一个子表达式的结果,作为while的循环判断条件
while (a = get_val(), count_val(a), a>0)
{
// 业务处理:逻辑和原代码一致,只保留核心业务逻辑
// ...
}
// 改写后的逻辑等价于原代码:
// 每次循环前,先执行"a = get_val()"(更新a)、"count_val(a)"(统计a),
// 再判断"a>0":如果成立则进入循环执行业务,否则结束循环;
// 优势:消除了原代码中"获取a+统计a"的重复代码,让逻辑更简洁
8. 下标访问[],函数调用()
8.1 []下标引用操作符
操作数:一个数组名+一个索引值(下标)
cs
// 代码说明:数组定义与下标引用操作
1 int arr[10];// 创建数组
// 注释:定义一个"整型数组",数组名为arr,长度为10(即包含10个int类型的元素)
// 注意:C语言数组的下标从0开始,所以arr的合法下标范围是 0~9(共10个元素)
2 arr[9] = 10;// 实用下标引用操作符。
// 注释:使用"下标引用操作符[]"访问数组arr的元素
// arr[9] 表示访问数组arr的"第10个元素"(因为下标从0开始,9是最后一个合法下标)
// 此语句将数值10赋值给arr的最后一个元素
3 []的两个操作数是arr和9。
// 注释:下标引用操作符"[]"是一个二元操作符,它有两个操作数:
// 左边操作数是"数组名arr",右边操作数是"下标值9"
// 通过这两个操作数,[]可以定位到数组中对应的元素
8.2 函数调用操作符
接受一个或者多个操作符:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
cs
// 引入标准输入输出头文件,用于使用printf函数实现输出功能
#include <stdio.h>
// 定义无返回值的函数test1:函数名是test1,无参数
void test1()
{
// 打印字符串"hehe"并换行
printf("hehe\n");
}
// 定义无返回值的函数test2:
// 参数是const char* str → const修饰表示"str指向的字符串内容不能被修改",str是字符串指针
void test2(const char *str)
{
// 打印传入的字符串str的内容并换行
printf("%s\n", str);
}
// 主函数,程序的执行入口
int main()
{
// 调用函数test1:这里的"()"是【函数调用操作符】
// 函数调用操作符的操作数是"函数名test1"+"参数列表(此处无参数)"
test1(); // 这里的()就是作为函数调用操作符。
// 调用函数test2:传入字符串常量"hello bit."作为参数
// 此处的"()"同样是函数调用操作符,操作数是"函数名test2"+"参数"hello bit.""
test2("hello bit.");// 这里的()就是函数调用操作符。
// 主函数返回0,标识程序正常结束
return 0;
}
9. 结构成员访问操作符
9.1 结构体
提取的文字如下: C语言已经提供了内置类型, 如: char、short、int、long、float、double等, 但是只有这些内置类型还是不够的, 假设我想描述学生, 描述一本书, 这时单一的内置类型是不行的。
描述一个学生需要名字、年龄、学号、身高、体重等;
描述一本书需要作者、出版社、定价等。C语言为了解决这个问题, 增加了结构体这种自定义的数据类型, 让程序员可以自己创造适合的类型。


9.1.1 结构的声明
cs
// 结构体的通用定义语法模板(用于自定义复合数据类型)
1 struct tag // struct:C语言定义结构体的关键字(固定写法);tag:结构体的"标签名"(自定义名称,用于标识这个结构体类型)
2 { // 大括号:开始定义结构体的"成员列表"(即结构体包含的属性集合)
3 member-list; // member-list:结构体的成员列表(可包含多个不同类型的变量,比如int、char*等,每个成员对应结构体的一个属性)
4 }variable-list; // 大括号:结束成员列表的定义;variable-list:定义的结构体变量(可在定义结构体时,直接创建若干个该类型的变量)
9.1.2 结构体变量的定义和初始化


cs
// 代码1:结构体的定义与变量声明
2 struct Point // 定义名为Point的结构体(用于表示"坐标点",包含x、y两个整型成员)
3 {
4 int x; // 结构体Point的成员:表示x坐标(整型)
5 int y; // 结构体Point的成员:表示y坐标(整型)
6 }p1; // 声明结构体类型的同时,直接定义该类型的变量p1
// 这里的"p1"是struct Point类型的变量
7 struct Point p2; // 先声明struct Point类型,再单独定义该类型的变量p2
// 代码2:结构体变量的初始化(两种方式)
9 // 代码2: 初始化。
10 struct Point p3 = {10, 20}; // 按"结构体成员的声明顺序"初始化:x=10,y=20
12 struct Stu // 定义名为Stu的结构体(用于表示"学生")
13 {
14 char name[15];// 结构体Stu的成员:名字(字符数组,最多存14个字符+1个结束符)
15 int age; // 结构体Stu的成员:年龄(整型)
16 };
17 struct Stu s1 = {"zhangsan", 20};// 按成员顺序初始化:name="zhangsan",age=20
19 struct Stu s2 = {.age=20, .name="lisi"};// 指定成员名初始化(可打乱顺序):age=20,name="lisi"
// 代码3:结构体嵌套的定义与初始化
21 // 代码3
22 struct Node // 定义名为Node的结构体(可用于链表节点,包含嵌套结构体)
23 {
24 int data; // 节点数据域:存储整型数据
25 struct Point p; // 嵌套结构体:Node包含一个Point类型的成员(表示该节点关联的坐标)
26 struct Node* next; // 节点指针域:指向另一个Node类型的指针(链表节点的核心,用于链接下一个节点)
27 }n1 = {10, {4,5}, NULL}; // 结构体嵌套初始化:
// data=10;p(Point类型)的x=4、y=5;next=NULL(表示无下一个节点)
29 struct Node n2 = {20, {5, 6}, NULL};// 同理:data=20;p的x=5、y=6;next=NULL
9.2 结构成员访问操作符
9.2.1 结构体成员的直接访问

结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所示:
cs
// 引入标准输入输出头文件,用于使用printf函数实现输出功能
#include <stdio.h>
// 定义名为Point的结构体(用于表示"坐标点"),包含两个整型成员x、y
struct Point
{
int x; // 结构体Point的成员:表示x坐标(整型)
int y; // 结构体Point的成员:表示y坐标(整型)
}p = {1,2}; // 声明结构体类型的同时,定义该类型的变量p,并按成员顺序初始化:p.x=1,p.y=2
// 主函数,程序的执行入口
int main()
{
// 使用"点操作符(.)"访问结构体变量p的成员:
// p.x 表示访问p的x成员,p.y 表示访问p的y成员,最终打印这两个成员的值
printf("x: %d y: %d\n", p.x, p.y);
// 主函数返回0,表示程序正常结束
return 0;
}
使用方式:结构体变量.成员名

9.2.2 结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。如下所示
cs
// 引入标准输入输出头文件,用于使用printf函数输出内容
#include <stdio.h>
// 定义名为Point的结构体类型(表示坐标点,包含x、y两个整型成员)
struct Point
{
int x; // 结构体成员:x坐标(整型)
int y; // 结构体成员:y坐标(整型)
};
// 主函数,程序执行入口
int main()
{
// 定义struct Point类型的变量p,并按成员顺序初始化:p.x=3,p.y=4
struct Point p = {3, 4};
// 定义struct Point类型的指针ptr,并将p的地址(&p)赋值给ptr
// 此时ptr指向结构体变量p(ptr是p的地址的别名)
struct Point *ptr = &p;
// 通过结构体指针访问成员的操作符"->",修改ptr指向的结构体的x成员为10
// 等价于修改p.x = 10(因为ptr指向p)
ptr->x = 10;
// 同理,通过ptr->y修改p的y成员为20
ptr->y = 20;
// 通过ptr->x、ptr->y访问成员值,打印修改后的结果
printf("x = %d y = %d\n", ptr->x, ptr->y);
// 主函数返回0,标识程序正常结束
return 0;
}
使用方式:结构体指针->成员名
综合举例:
cs
// 引入标准输入输出头文件,用于printf等输入输出操作
#include <stdio.h>
// 引入字符串处理头文件,因为需要用strcpy函数(字符数组的字符串复制)
#include <string.h>
// 定义名为Stu的结构体,用于描述"学生"的信息
struct Stu
{
char name[15];// 结构体成员:名字(字符数组,最多存14个字符+1个字符串结束符'\0')
int age; // 结构体成员:年龄(整型)
};
// 函数:打印学生信息(传值调用)
// 传值调用的特点:函数接收的是实参的"副本",函数内修改参数不会影响原实参
void print_stu(struct Stu s)
{
// 用"点操作符(.)"访问结构体变量s的成员,打印名字和年龄
printf("%s %d\n", s.name, s.age);
}
// 函数:修改学生信息(传址调用)
// 传址调用的特点:函数接收的是实参的"地址",通过指针可直接修改原实参的内容
void set_stu(struct Stu* ps)
{
// 用"->操作符"访问结构体指针ps指向的成员:
// 字符数组不能直接用"="赋值,需用strcpy函数将"李四"复制到name数组中
strcpy(ps->name, "李四");
// 修改ps指向的age成员,将其设为28
ps->age = 28;
}
// 主函数,程序的执行入口
int main()
{
// 定义struct Stu类型的变量s,并初始化:name为"张三",age为20
struct Stu s = { "张三", 20 };
// 调用print_stu(传值调用),打印s的初始信息(输出:张三 20)
print_stu(s);
// 调用set_stu(传址调用),传入s的地址&s,修改s的信息
set_stu(&s);
// 再次调用print_stu,打印修改后的s(输出:李四 28)
print_stu(s);
// 主函数返回0,标识程序正常结束
return 0;
}
更多关于结构体的知识,后期在《第20讲:自定义类型:结构体》中讲解
10. 操作符的属性:优先性,结合性
C语言的操作符有2个重要的属性:优先级,结核性,这两个属性决定了表达式求值的计算顺序。
10.1 优先级
优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不同的
cs
// 算术表达式语句:遵循C语言的"运算符优先级"规则
3 + 4 * 5;
// 注释:
// 1. C语言中,乘法运算符(*)的优先级高于加法运算符(+),因此先执行"4 * 5",结果为20;
// 2. 再执行"3 + 20",整个表达式的最终计算结果为23;
// (注:此语句仅计算表达式的值,未将结果赋值给变量,所以计算结果不会被保存)
提取的文字如下: 上面示例中,表达式3 + 4 * 5里面既有加法运算符(+),又有乘法运算符(*)。由于乘法的优先级高于加法,所以会先计算4 * 5,而不是先计算3 + 4。
10.2 结合性
提取的文字如下: 如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(=)。
cs
// 算术表达式语句:涉及乘法(*)和除法(/)运算符
5 * 6 / 2;
// 注释:
// 1. 乘法(*)和除法(/)的优先级相同,此时需遵循"左结合性"(从左到右执行运算);
// 2. 先计算"5 * 6",结果为30;
// 3. 再计算"30 / 2",整个表达式的最终计算结果为15;
提取的文字如下: 上面示例中,*和/的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算5 * 6,再计算/ 2。
运算符的优先级顺序很多,下面是部分运算符的优先级顺序(按照优先级从高到低排列),建议大概记住这些操作符的优先级就行,其他操作符在使用的时候查看下面表格就可以了。

由于圆括号的优先级最高,可以使用它改变其他运算符的优先级


参考https://zh.cppreference.com/w/c/language/operator_precedence
11.表达式求值
11.1整体提升
C 语言中整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。

整型提升的意义:
表达式的整型运算要在 CPU 的相应运算器件内执行,CPU 内整型运算器 (ALU) 的操作数的字节长度一般就是 int 的字节长度,同时也是 CPU 的通用寄存器的长度。
因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度。
通用 CPU(general-purpose CPU)是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入 CPU 去执行运算。
cs
int main()
{
// 定义有符号char类型变量a,赋值为4(char通常占1字节,8位,有符号char范围:-128~127)
char a = 4;
// 定义有符号char类型变量b,赋值为127(有符号char的最大值)
char b = 127;
// 计算a + b:触发"整型提升"
// 过程:a、b先被提升为int类型,计算4 + 127 = 131(int类型的结果)
// 再将int类型的131赋值给char类型的c:触发"溢出截断"(char仅能存8位)
char c = a + b;//整型提升
// 打印c的值:
// 131的8位二进制是10000011,对于有符号char,这是补码形式,对应的十进制是-125
printf("%d\n", c);
return 0;
}
如何进行整体提升呢?
有符号整数提升是按照变量的数据类型的符号位来提升的
无符号整数提升,高位补0


11.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名靠后,那么首先要转换为另外一个操作数的类型后执行运算。



11.3 问题表达式解析
11.3.1 表达式1
// 表达式的求值顺序是由操作符的优先级来决定的
// 表达式1
a * b + c * d + e * f;
// 注释:
// 1. 乘法运算符(*)的优先级高于加法运算符(+),因此会先分别计算三个乘法操作:a*b、c*d、e*f;
// 2. 加法运算符(+)是左结合性,所以完成所有乘法后,会从左到右依次执行加法:先算"(a*b) + (c*d)",再将结果与"e*f"相加。
表达式 1 在计算的时候,由于 * 比 + 的优先级高,只能保证,*的计算是比 + 早,但是优先级并不能决定第三个*比第一个 + 早执行。

所以表达式的计算顺序就可能是:

或者
11.3.2 表达式2

同上,操作符优先级只能决定自减 -- 的运算在 + 的运算的前面,但是我们并没有办法得知,+ 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。

11.3.3 表达式3
cs
// 表达式3(注意:该表达式存在C语言的"未定义行为",最终结果会因编译器/环境不同而变化)
int main()
{
int i = 10; // 定义整型变量i,初始赋值为10
// 危险表达式:同一个表达式中对变量i进行了多次"副作用操作"(i--、--i、i=-3、i++、++i)
// C语言标准未规定这些操作的执行顺序,这种情况属于"未定义行为"------不同编译器会按照不同逻辑执行,结果无法预测
i = i-- - --i * (i = -3) * i++ + ++i;
printf("i = %d\n", i); // 打印i的值,但结果不固定(依赖具体编译器的处理逻辑)
return 0;
}

11.3.4 表达式4
cs
// 引入标准输入输出头文件,用于后续printf函数的输出操作
#include <stdio.h>
// 定义返回整型的函数fun
int fun()
{
// 定义静态局部变量count,初始值为1
// 静态变量(static)仅在第一次进入函数时初始化,后续调用会保留上次的取值(存储在静态存储区)
static int count = 1;
// 前置自增:先将count的值加1,再返回自增后的结果
return ++count;
}
// 主函数,程序执行入口
int main()
{
int answer; // 定义整型变量answer,用于存储表达式的计算结果
// 表达式:fun() - fun() * fun()
// 1. 运算符优先级:乘法(*)高于减法(-),因此先计算"fun() * fun()",再用左侧fun()的结果减该乘积
// 2. 关键问题:C语言未规定多个函数调用作为操作数时的执行顺序(属于"未指定行为"),因此三个fun()的调用顺序依赖编译器
// 常见调用顺序(如"从右到左")的过程:
// - 最右侧fun():count从1→2,返回2
// - 中间fun():count从2→3,返回3
// - 左侧fun():count从3→4,返回4
// 最终计算:4 - (3 * 2) = -2
// 若调用顺序不同(如"从左到右"),结果会变化(例如:2 - (3 * 4) = -10)
answer = fun() - fun() * fun();
printf("%d\n", answer);// 输出结果(依赖编译器的调用顺序,常见结果为-2)
return 0;
}

这个代码有没有实际的问题?有问题!
虽然在大多数的编译器上求得结果都是相同的。
但是上述代码 answer = fun () - fun () * fun (); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。
函数的调用先后顺序无法通过操作符的优先确定。
11.3.5 表达式5
// 表达式5(该表达式存在C语言的"未定义行为",结果因编译器不同而变化)
#include <stdio.h> // 引入标准输入输出头文件,用于printf输出
int main()
{
int i = 1; // 定义整型变量i,初始赋值为1
// 危险表达式:同一个表达式中对变量i进行了多次前置自增(++i)操作
// C语言标准未规定多个++i在表达式中的执行顺序,这种情况属于"未定义行为"------不同编译器会按不同逻辑执行
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret); // 打印ret的值(结果不固定,依赖编译器)
printf("%d\n", i); // 打印i的最终值(同样因编译器而异)
return 0; // 主函数返回0,程序结束
}
// 提示:尝试在Linux环境的GCC编译器、VS2013环境下执行,会得到不同结果

看看同样的代码产生了不同的结果,这是为什么?
简单看一下汇编代码,就可以分析清楚.
这段代码中的第一个 + 在执行的时候,第三个 ++ 是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。
11.4 总结
即使有了操作符的优先和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在潜在风险的,建议不要写出特别复杂的表达式。
完。