summary
type
status
date
slug
tags
category
文件和媒体
文本
icon
password
变量定义
变量是一个保存数据的地方,当需要在程序中保存数据时,比如上面例子中要记录用户输入的价格,就需要一个变量来保存它,用一个变量保存了数据,它才能参加到后面的计算中,比如计算找零
变量定义的一般形式就是<类型名称><变量名字>;
一行可以定义多个变量,用逗号隔开,分号代表结束
变量需要名字,是一种标识符,意思它是用来区分不同的名字,标识符基本原则是只用用字母、数字、下划线组成,数字不能出现在第一个位置,C语言的关键字也不能用来做标识符
变量赋值和初始化
price=0是一个式子,这里的=是一个赋值运算符,表示将=右边的值赋给左边的变量
<类型名称><变量名称>=<初始值>
所以的变量在使用之前必须定义或声明,所有的变量必须具有确定的数据类型,数据类型表示在变量中可以存放什么样的数据,变量中只能存放指定类型的数据,程序运行过程中也不能改变变量的类型
常量和变量
不变的量是常量,是直接写在程序里的
定义一个常量:const int AMOUNT=100;
const是修饰符,加在int前面,用来给这个变量加上一个(不变的)的属性。这个const表示这个变量的值一旦初始化,就不能再修改,一般const后面的常量需要是大写
浮点数
整数运算的结果只有整数部分,不然就要用浮点数
带小数点的数值叫浮点数
当浮点数和整数放在一起运算时,C就会将整数转换为浮点数,然后进行浮点数的运算,%d要改成%f
如果把int换成double,就可以把变量改成double类型的浮点数变量了;double本来是双精度浮点数的第一个单词,表示浮点数类型,除了double,还有float(浮点)表示单精度浮点数
用double时,输入时要把%d改成%lf
数据类型:
整数 | 带小数点的数 |
int | double |
printf(”%d”,…) | printf(”%f”,…) |
scanf(”%d”,&…) | scanf(”%lf”,&…) |
表达式
运算符是指进行运算的动作,比如加法运算符”+“,减法运算符”-“
算子是指参与运算的值,这个值可能是常数,也可能是变量,还可能是一个方法的返回值
取余计算%
hour1*60+minute1 ——>转换为分钟为单位
t/60 ——>小时部分;t%60 ——>分钟
程序就是数据计算
运算符的优先级
优先级 | 运算符 | 运算 | 结合关系 | 举例 |
1 | + | 单目不变 | 自左向右 | a*+b |
1 | - | 单目取负 | 自右向左 | a*-b |
2 | * | 乘 | 自左向右 | a*b |
2 | / | 除 | 自左向右 | a/b |
ㅤ | % | 取余 | 自左向右 | a%b |
3 | + | 加 | 自左向右 | a+b |
3 | - | 减 | 自左向右 | a-b |
4 | = | 赋值 | 自右向左 | a=b |
有一个算子的运算符就是单目运算,两个算子就是双目运算
赋值也是运算符
a=b=6 ——>a=(b=6)
交换变量
程序表达的是顺序执行的动作,而不是关系
a=6 b=5
t=a
a=b
b=t
提供一个地方存放东西:t
复合赋值和递增递减
5个算术运算符可以和赋值运算符=结合起来,形成复合赋值运算符
+ | += |
- | -= |
* | *= |
/ | /= |
% | %= |
total+=5
total=total+5
注意两个运算符中间不要有空格
total+=(sum+100)/2
total=total+(sum+100)/2
total*sum+12
total=total*(sum+12)
“++”和1”--”是单目运算符,这个算子还必须是变量。他们分别叫递增和递减运算符,作用是给这个变量+1或-1
count++
count+=1
count=count+1
前缀后缀
++和--放在变量的前面,叫做前缀形式,放在变量后面叫做后缀形式
a++的值是a加1以前的值,++a的值是加了1以后的值,无论哪个,a自己的值都加了1
表达式 | 运算 | 表达式的值 |
count++ | 给count加1 | count原来的值 |
++count | 给count加1 | count+1以后的值 |
count-- | 给count减1 | count原来的值 |
--count | 给count减1 | count-1以后的值 |
可以单独使用,但是不要组合进表达式
做判断:if语句根据条件决定做还是不做
if(条件成立){
……
}
如果条件成立,就做这件事情,如果不成立,就不做这件事情
判断的条件
条件
计算两个值之间的关系,所以叫做关系运算
运算符 | 意义 |
== | 相等 |
!= | 不相等 |
> | 大于 |
>= | 大于或等于 |
< | 小于 |
<= | 小于或等于 |
关系运算的结果
当两个值的关系符合关系运算符的预期时,关系运算的结果为整数1,否则为整数0
表达式 | 结果 |
printf(”%d”,5==3 | 0 |
printf(”%d”,5>3) | 1 |
printf(”%d”,5<=3) | 0 |
优先级
所有的关系运算符的优先级比算术运算的低,但是比赋值运算的高
判断是否相等的==和!=的优先级比其他的低,而连续的关系运算是从左到右进行的
实例:找零计算器、年龄计算


if如果条件不成立,否则
如果if后面还有语句,它们在if结束后会执行,无论条件如何
我们可以在if后面跟上一个else
else=否则的话

if语句2
一个基本的if语句由一个关键字if开头,跟上在括号里的一个表示条件的逻辑表达式,然后是一对大括号”{}“之间的若干条语句。如果表示条件的逻辑表达式的结果不是个零,那么就执行后面跟着的这对大括号中的语句,否则就跳过这些语句不执行,而继续下面的其他语句
if语句还有另外一种形式,就是可以没有后面的大括号
if语句这一行结束的时候并没有表示语句结束的“;”,而后面的赋值语句写在if的下一行,并且缩进了,在这一行结束的时候有一个表示语句结束的”;”。这表明这条赋值语句是if语句的一部分,if语句拥有和控制这条赋值语句,决定它是否要被执行。
else同理
循环
写程序写的不是关系,不是说明,而是步骤!
/*如果我们把while翻译成“当 ”,那么一个while循环的意思就是:当条件满足时,不断地循环重复循环体内的语句。
循环执行之前判断是否继续循环,所以有可能循环一次也没有被执行
while循环

数位数的算法:
1.用户输入x;
2.初始化n为0;
3.x=x/10,去掉个位;
4.n++;
6.如果x>0,回到3;
6.否则n就是结果。
if和while的区别仅仅在于一次性还是反复多次的做,后者包括反复多次的判断条件
循环体内要有改变条件的机会,否则会出不来
如果把while循环翻译为“当”,那么一个while循环的意思就是:当条件满足时,不断地重复循环体内的语句
循环执行之前判断是否继续循环,所以有可能循环一次也没有被执行
条件成立是循环继续的条件
do-while
先做循环内的句子
在进入循环的时候不做检查,而是在执行完一轮循环体的代码之后,再来检查循环的条件是否满足,如果满足则继续下一轮循环,不满足则结束循环

结束语句要么是大括号要么是分号
do-while循环和while循环很像,区别是在循环体执行结束的时候才来判断条件。也就是说,无论如何循环都会至少一遍,然后再来判断条件。与while循环相同的是,条件满足时执行循环,条件不满足时结束循环
for循环
阶乘
n!=1x2x3x4x……xn
4!=1x2x3x4=24

for循环 像一个计数循环:设定一个计数器,初始化它,然后计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步骤进行调整,比如加1或者减1
for=对于
for(count=10;count>0;count--)
就读成:“对于一开始的count=10,当count>0时,重复做循环体,每一轮循环在做完循环体内语句后,使得count--。”
小套路 :做求和的程序时,记录结果的变量应该初始化0,而做求积的变量时,记录结果的变量应该初始化为1
C99循环控制变量i只在循环里被使用,在循环外没有如何用处,所以可以把变量i的定义写到for语句里面 for(int i=1)
循环的选择
for(i=0;i<n;i++)
则循环的次数时n,而循环结束以后,i的值是n。循环的控制变量i,是选择从0开始还是从1开始,
是判断i<n还是判断i<=n,对循环的次数和循环结束后变量的值都有影响
for(初始动作;条件;每轮的动作){
}
for中的每一个表达式都是可以省略的for(;条件;)==while(条件)
Tips
如果有固定次数,用for
如果必须执行一次,用do while
其他情况用while
逻辑类型
bool:
#include<stdbool.h>
之后就可以使用bool和true、false
逻辑运算
运算符 | 描述 | 示例 | 结果 |
! | 逻辑非 | !a | 如果a是true结果就是false,如果a是false结果就是true |
&& | 逻辑与 | a&&b | 如果a和b都是ture,结果就是true;否则就是false |
|| | 逻辑或 | a||b | 如果a和b有一个是true,结果为true;两个都是false,结果为false |
如果要表达4<x<6,应该x>4&&x<6,因为4<x的结果是一个逻辑值(0或1)
理解:
age>20&&age<30
index<0||index>99
!age<20会先算!age,然后就两个结合在一起,比如age如果是0,那么!age就会变成1,如果是1就变零,永远小于20,如果想不小于20,就要加括号
逻辑运算符普遍低于比较运算符,!是单目运算符,单目运算符高于双目运算符
优先级:
!>&&>||
!done&&(count>MAX):!done会先算,如果count>MAX并且done是0,那么整个结果才会是1
优先级 | 运算符 | 结合性 |
1 | () | 从左到右 |
2 | ! + - ++ -- | 从右到左(单目的+和-) |
3 | * / % | 从左到右 |
4 | + - | 从左到右 |
5 | < <= > >= | 从左到右 |
6 | == != | 从左到右 |
7 | && | 从左到右 |
8 | || | 从左到右 |
9 | = += -= *= /= %= | 从右到左 |
赋值永远是最低的
短数:
逻辑运算是自左向右进行的,如果左边的结果已经能够决定结果了,就不会做右边的计算
a==6&&b==1
a==6&&b+=1
对于&&,左边是false时就不做右边了
对于||,左边时true时就不做右边了
不要把赋值,复合赋值组合进表达式里
条件运算

条件运算符的优先级高于赋值运算符,但是低于其他所有运算符
逗号运算符
逗号用来连接两个表达式,并以它右边的表达式的值作为它的结果,逗号的优先级是所有运算符中最低的,所以它两边的的表达式会先计算;逗号的组合关系是自左向右,所以左边的表达式会先算,而右边的表达式的值就会留下来作为逗号运算的结果
逗号主要在for中使用,如:
for(i,j=10;i<j;i++,j--)………
嵌套的if-else
当if的条件满足或不满足的时候要执行的语句也可以是一条if或if-else语句,这就是嵌套的i语句
else一般会和最近的if匹配,如果加了大括号,则根据大括号匹配,缩进格式不能暗示else的匹配
tips
在if或else后面总用{}大括号
即使只有一条语句的时候
级联的if-else
多路分支
switch-case
如果输入的3给到了type,那么就会直接跳到case 3,不用像else-if一个一个做判断。然后break把运行带到了return。
switch表达式必须是int类型:只能是整数型的结果
常量可以是常数,也可以是常数计算的表达式
switch语句可以看作是一种基于计算的跳转,计算控制表达式的值后,程序会跳转到相应的case(分支标号)的地方,分支标号只是说明switch内部位置的路标,在执行完分支中的最后一条语句后就,如果后面没有break,就会按顺序执行下面的case,直到遇到一个break或者switch结束为止。
/取整 %取余
计算循环
tips:
如果要模拟运行一个很大次数的循环,可以模拟较小的循环次数,然后推断
如果某个变量要做累加,那么一般初始值为0
如果某个变量要做除法,那么可以根据终点给判断值
循环算平均数
让用户输入一系列的正整数,最后输入-1表示输入结束,然后程序计算出这些数字的平均数,输入输出的数字的个数和平均数
首先需要有三个变量:
一个记录读到的整数的变量,这里用number
一个计算平均数的变量,每读到一个数,就把它加到一个累加的变量里去,这里用sum
一个用来储存平均数个数的变量,这里用count
一个变量计数累加的结果,一个记录读到的数的个数,到全部数据读完,再拿sum去除count读到的数的个数
算法:

算法:
1.初始化变量sum和count为0;
2.读入number
3.如果number不是-1,就将number加入到sum,并将count加1,回到步骤2
4.如果numbr是-1,那就计算和打印出sum/count(注意要换成浮点数计算
猜数字
1.计算机随机想一个数,记在变量number里
2.一个负责记录次数的变量count初始化为0
- 用用户输入一个数字a
4.count递增(加1)
5.判断a和number的大小关系,如果a大,就输出“大”;如果a小就输出“小”
6.如果a和number是不相等的(无论大还是小),程序转回到第3步
7.否则,程序输出“猜中”和次数,然后结束
随机数:
每次召唤rand()就得到一个随机的整数
因为要进入循环,让用户输入一个数,然后在循环结束的时候再判断是否继续循环,所以do while更合适
整数求逆
整数的分解:
整数是由1至多位数字组成
对一个数字做%10的操作,就得到它的个位数
对于一个整数做/10的操作,就去掉了它的个位数
然后再对2的结果做%10,就得到原来数的十位数了
以此类推
数的逆序:
输入一个正整数,输出逆序的数
if常见错误
错误1:忘记加大括号
养成好习惯,永远在if、else……后面加大括号,哪怕只有一个语句
错误2:if后面的分号
分号即代表结束,哪怕里面没有内容,空白也代表内容,c语言会认为if里面也有一条语句了,分号下面大括号的内容就变成普通语句,跟if就没有关系了。所以if、else后面不要加分号
错误3:错误使用==和=
if只要求()里的值是零或非零
只敲了一个等号就是赋值,赋值是表达式,只会把b给a,而不会判断a和b是否相等
尊重warning:根据warning解决问题
0对于if来说条件不成立
错误3:代码风格
在if和else之后必须加上大括号形成语句块
大括号内的语句缩进一个tab的位置
循环控制
素数:只能被1和自己整除的数,不包括1
2,3,5,7,9,11,13,15,17,19
break:跳出循环
continue:跳出循环这一轮剩下的语句进入下一轮

嵌套的循环
循环里面还是循环,可以是for和for的嵌套也可以是for和while的嵌套,没有任何关系,不用特别去理解,当你需要嵌套的时候就放个嵌套进去,需要注意的是:两重循环里面的控制变量不能是一样的,如果是一样的就会混淆,比如第一个循环初始值是x=2,里面的循环的变量就要是i=2
找出100以内的素数
找出前50个的素数
从嵌套的循环中跳出
break只能跳出其所在的循环
break和continue只能对它所在的那层循环做
exit:接力break:
goto:
goto的后面要跟一个标号,比如out。第二个标号要有冒号结尾,上面代码的意思是满足条件就跳到out的位置去
前n项求和

注意因为有分数,所以用浮点数,可用double也可用float
加减n项求和:

加sign使分子不断从正负转换达到加减n项求和的目的
整数分解
正序分解整数
此处以x为123为例
开始int定义了一个x,用于存放用户输入的数,然后定义变量mask初始值为1,再下一行将x的值赋给变量t,当t(也就是用户给的值)大于9时,t/10等于12,mask*10变成10,继续while循环,t现在是12仍大于9,t/10等于1,mask10*10等于10,进入下一步输出x=123(注意123不是t,x一直没动过)输出mask等于00,进入下一步do while定义d等于123/100等于1,输出1,if判断mask100大于9,输出空格,然后123取余100等于23,mask100/10等于10,进入while循环判断条件,mask10大于0,继续循环do,d等于23/10等于2,输出2,if判断mask10大于9,输出空格。x23%10等于3,mask10/10等于1,满足while循环条件,继续do,d等于3/1等于3,输出3。if判断条件不满足,不再输出空格。后面的取余和除已经不重要了,while循环条件也不满足,至此结束
x=123
123/100 →1
123%100 →23
100/10 →10
23/10 →2
23%10 →3
10/10 →1
3/1 →3
3%1 →3
1/10 →0
求最大公约数
枚举
1.设t为2
2如果u和v都能被t整除,则记下这个t
3.t加1后重复第2步,直到t等于u或v
- 曾经记下的最大的可以同时整除u和v的t就是gcd(最大公约数)
此处以a4,b2为例
开始定义了a和b,用于保存用户输入的参数。定义了min,用于存放最小参数那个常量a或b,if判断如果a小于b,那么最小数min就是a,else否则b就是最小的,这里a是4,b是2,所以最小的是b。定义ret初始值为0,ret用来保存最大公约数。定义i。进入for循环,i初始值为1;如果i小于等于最小的min(a和b其中一个),则进行for循环内的动作,每次进入循环时i加1,现在i是1,min是b(b的值是2),所以满足i<=min的条件,进入if判断条件,4%1=0,满足条件进入下一个if判断条件2取余1同样能整除,下一步把i的值也就是1赋给ret。继续for循环,直到不满足条件为止。满足条件,进入for循环,i+1=2,如果满足a取余2能整除的条件(已满足),进入下一个循环满足2取余2能整除的条件,把i(2)赋予ret。继续for循环,满足2小于等于2,i加1=3,如果4取余3能整除则满足条件,这里不满足,结束循环,进入printf输出
辗转相除法
1.如果b等于0,计算计算,a就是最大公约数
- 否则,计算a除以b的余数,让a等于b,而b等于哪个余数
- 回到第一步
此处以a4,b2为例
只讲解while部分,进入while循环,当b不等于0时,开始循环,现在b是2,满足条件,下一步将4取余2等于2的值赋值给t,将2赋值给a,将t(2)赋值给b,重复循环直到b等于0,循环结束
初试数组
int number[100]
scanf(”%d”,&number[i]);
第6行定义了一个int类型的number变量,这个变量是数组,这个数组里面每一个单元都是一个int,这个数组大小是100,这个数组里头可以放100个int,然后再读进来的时候除了做原来做的事情之外,再让number的cnt等于那个x,因为刚好cnt一开始是0,等到它加完是1,所以cnt会不断递增,这里就是在说number这个数组里头的cnt那个位置上的那个单元等于x,所以在while当中随着x的不断读入,我们都会把它放到number这个数组里头去,放进去的位置随着cnt的递增每一次再往后放,第一次放的是0的那个位置,第二次放的是number1那个位置
第十七行做一个循环,i等于0,i小于cnt,因为此时cnt表示的是在这个数组中有多少个数, 接下来数组根据循环的i++不断变大,数组也在不断遍历,遍历到符合判断条件的数组的其中一个单元时,就输出,接着继续遍历,直到i≥cnt
定义数组
<类型>变量名称[元素数量];
int number[100];//叫做number的数组,每一个单元都是int类型,它有100个这样的int
double weight[20];
元素数量之间必须是整数
C99之前数组的大小必须是一个常数,C99开始已经可以用变量定义数组的大小了
数组是什么
是一种容器,特点是:
其中所有的元素具有相同的数据类型
一旦创建,不能改变大小
数组中的元素在内存中是连续依次排序的
int[10]
一个int的数组
10个单元:a[0],a[1],……a[9]

每个单元都是一个int类型的变量
可以出现在赋值的左边或右边
a[2]=a[1]+6,右读左写,将a[1]读出来加上6,赋给a[2]
在赋值左边的叫做左值,右则右值
数组的单元
数组的每个单元就是数组类型的以提高变量
使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数:
grades[0]
grades[99]
average[5]
最大的下标是数组个数减1,而不是数组的个数
编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
出现segmenta faul可能就是数组下标越界了
使用有效的下标值[0,数组的大小-1]
用数组做散列计算
关键设计点
- 常量使用:
number
作为常量便于维护,若需统计更多数字只需修改一处
- 数组索引的巧妙利用:直接用输入数字作为数组下标
- 输入终止条件:使用-1作为结束标志(约定俗成的EOF替代方案)
- 防御性编程:通过
if(x >=0 && x <=9)
过滤无效输入
输入:
5 3 2 5 7 9 -1
输出:
0:0
1:0
2:1
3:1
4:0
5:3
6:0
7:1
8:0
9:1
这个程序是典型的「直方图统计」实现,如果需要扩展功能(如处理更大范围的数字),可以通过修改
number
常量和判断条件来实现。为什么需要count[x]++
?
- 这行代码是核心逻辑:
- 当输入数字3时 →
count[3]
的值+1 ++
运算符专门用于"加1"操作
初识函数
为什么从k=2开始?
- 素数定义:只能被1和自身整除
- 检查2到i-1之间的所有数字
- 循环条件 k < i-1 的疑问
- 教学目的:更直观展示判断逻辑
- 示例:i=6时检查k=2,3,4,5(虽然检查到2时就会退出)
(优化提示:循环可改为 k <= sqrt(i),后续会说明)
- m=1的特殊处理:因为1既不是素数也不是合数,直接跳过
- 循环结构:遍历区间[m,n]的每个数字,通过isPrime()判断是否为素数
- 累加器设计:sum和cnt分别记录素数的总和与数量
执行过程示例(输入m=10,n=13):
i值 | isPrime(i)结果 | sum变化 | cnt变化 |
10 | 0(非素数) | 不变(sum=0) | 0 |
11 | 1(素数) | 0+11=11 | 0→1 |
12 | 0 | 不变 | 1 |
13 | 1 | 11+13=24 | 1→2 |
为什么m=1时要改成2?
- 1的特殊性:
- 只能被1整除
- 根据定义,素数需要恰好两个不同的除数(1和自身)
- 因此1既不是素数也不是合数
素数检测逻辑:先假设是素数(ret=1),发现任何能整除的数就推翻假设
函数的定义和使用
函数是一块代码,接收零个或多个参数,做一件事情,并返回零个或一个值
可以先想象成数学中的函数:
y=f(x),x是它的自变量,y得到了它的结果,f是这个函数的名字
第一行是函数头,函数头以下括号内是函数体,void是返回值,void的意思就是没有,sum是函数名,void类型的意思就是说这个sum函数不返回任何东西,int begin,int end是参数表,在参数表里面每一个参数都是一个类型和一个名字的一对,这里表明有两个参数,第一个参数是int,第二个参数也是int。如果没有圆括号,那sum可能表示的是变量,有了圆括号就表示说前面的sum是一个函数
调用函数:
函数名(参数值);
()起到了表示函数调用的重要作用
即使没有参数也需要()
如果有参数,则需要给出正确的数量和顺序
这些值会被按照顺序依次用来初始化函数中的参数
比如sum(1,10),1会初始化到begin,10会初始化到end里面
函数知道每一次是哪里调用它,会返回到正确的地方
从函数中返回
如果函数要返回一个结果,那就需要用return把那个结果交给调用它的地方
return停止函数的执行,并送回一个值
函数一旦遇到return就不再往下执行了,如果return后面有值,那么就要送回去一个值,所以return有两种写法:
直接写:return;
return 表达式;
变量值变化表 | ㅤ | ㅤ | ㅤ | ㅤ |
执行步骤 | a值 | b值 | c值 | 说明 |
初始状态 | 5 | 6 | 未初始化 | ㅤ |
步骤①后 | 5 | 6 | 12 | max(10,12)=12 |
步骤②后 | 5 | 6 | 6 | max(5,6)=6 |
步骤③后 | 5 | 6 | 23 | max(6,23)=23 |
输出时 | 5 | 6 | 23(但未使用) | printf打印的是max(5,6)=6 |
变量作用域
变量位置 | 作用域 | 示例说明 |
max函数a,b | 仅在max函数内 | main中的a=5与max的a无关 |
main函数a,b | 仅在main函数内 | max函数无法访问main的a,b |
c的作用:接收返回值
一个函数里可以有多个return语句,如:
函数的返回值可以赋给变量,也可以再传递给函数:max(max(c,a),5);
没有返回值的函数
void函数名(参数表)
不能使用带值的return
可以没有return
调用的时候不能做返回值的赋值
如果函数有返回值,则必须使用带值的return
函数原型
函数先后关系
C的编译器自上而下顺序分析代码
比如看到sum(1,10)的时候,它要知道sum()的样子,也就是sum()要几个参数,每个参数的类型如何,返回什么类型
所以只有把函数放在上面它才能检查对sum()的调用是否正确
原型声明
将函数头放到第一行,结尾放个分号。这个叫函数的原型声明,下面void开始到结尾叫函数定义,声明不是函数,声明不仅仅是用来让编译器检查函数调用是不是对的,也会用来让编译器检查对函数的定义是不是一致的
函数头,以分号“;”结尾,就构成了函数的原型
函数原型的目的是告诉 编译器这个函数长什么样
名称
参数(数量及类型)
返回类型
参数传递
调用函数
如果函数有参数,调用函数时必须传递给它数量、类型正确的值
可以传递给函数的值是表达式的结果,这包括:
字面量
变量
函数的返回值
计算的结果
类型不匹配
调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞
编译器会自动把类型转换好,但这很可能不是我们所期望的
C语言在调用函数的时候,永远只能传值给函数
以上的交换a和b的值的方法是不可行的
问题核心图解:内存状态变化
步骤 | main的a | main的b | swap的a | swap的b | t |
main的初始化 | 5 | 6 | - | - | - |
调用swap时 | 5 | 6 | 5 | 6 | ㅤ |
swap执行后 | 5 | 6 | 6 | 5 | 5 |
swap返回后 | 5 | 6 | 已销毁 | 已销毁 | 已销毁 |
传值
每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
过去,对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”
无法通过纯值传递实现,但可以通过其他方式,比如指针、全局变量,、返回结构体
本地变量(局部变量、自动变量)
函数的每一次运行就会产生一个独立的变量空间,在这个变量空间当中的变量是函数的这一次运行所独的,那么把它们称作本地变量
定义在函数内部的变量就是本地变量
参数也是本地变量
变量的生存期和作用域
生存期:什么时候这个变量开始出现了,到什么时候它消亡了
作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
对于本地变量,这两个问题的答案是统一的:大括号内————块
本地变量的规则
本地变量是定义在块内的
它可以是定义在函数的块内
也可以定义在语句的块内
甚至可以随便拉一对大括号来定义变量
程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
块外面定义的变量在里面仍然有效
块里面定义了和外面同名的变量则掩盖了外面的
不能在同一个块内定义同名的变量
本地变量不会被默认初始化
参数在进入函数的时候被初始化了,调用函数的时候一定要给参数对应的值,那个值就会在进入函数的时候用来初始化参数
本地变量特性详解
1.作用域限制
输出结果:
游泳馆柜号:123
健身房柜号:456
重点解析:
• 同名变量
locker
在不同函数中是独立存在的
• main
函数无法访问其他函数内的locker
2.生命周期图解:
内存变化:
调用次数 | num值 | 说明 |
第一次调用 | 0→1 | 函数结束时num被销毁 |
第二次调用 | 新建num=0→1 | 完全独立的新变量 |
第三次调用 | 新建num=0→1 | 同上 |
3.多层作用域示例
运行结果:
内层a=20
外层a=10
4.常见错误类型:
错误1:跨函数访问
错误2:误以为会保留值
正确做法:用static修饰符
函数杂事
函数没有参数时
void f(void):在参数表里放void就是明确告诉编译器,这个函数不接受任何参数
void f():参数表里不放任何东西,在传统C中,它表示f函数的参数表未知,并不表示没有参数,不建议写出这样的原型,如果确定没有参数的,就把void写上去
逗号运算符
调用函数时的圆括号里的逗号是标点符合,不是运算符
f(a,b)
如果给它再加一层括号,它就表明我们要先做括号里面的运算,这个时候它就是逗号运算符了
f((a,b))
所以这两个表达式的区别就是到底传了两个还是一个参数进去的问题
函数里的函数
C语言不允许函数嵌套定义
可以在一个函数里面放另一个函数的声明,但是不能放另一个函数的body,不能放它的定义
关于main
int main()也是一个函数
必须存在:每个程序有且只有一个main
执行起点:程序运行时第一个执行的函数(就像打开书先看封面)
返回类型:
int
表示返回一个状态码(0通常表示成功)
基础结构解析:代码部分 | 作用声明 | 类比 |
int main() | 声明主函数 | 书的封面标题 |
{ } | 函数体范围 | 书的内容页面 |
return 0; | 向操作系统返回状态 | 故事结尾的"完"字 |
main函数的三种标准写法
1.最简形式(适合新手)
2.带void参数(明确表示无输入)
3.命令行参数形式(高级用法)
即使不需要返回值也要写return
main函数的特殊性质
特性 | 说明 | 与其他函数的区别 |
自动调用 | 由操作系统直接调用 | 其他函数需要手动调用 |
必须存在 | 程序无法编译没有main的文件 | 其他函数可有可无 |
返回值影响系统 | return 0表示正常退出 | 普通函数返回值由程序处理 |
main函数返回值的意义:
返回值 | 常见含义 |
0 | 成功执行(EXIT_SUCCESS) |
1 | 一般错误(EXIT_FAILURE) |
其他数字 | 自定义错误码 |
特殊环境中的main
- 嵌入式系统:可能没有传统意义上的main
- 操作系统内核:启动方式完全不同
- C++中的main:语法相同,但类构造函数先执行
二维数组
一维数组:只有一个下标
数组可以看作是一个线性的东西,从a0到a10,从a0到a9,都是一个线性的,除了线性的一维数组之外,还可以做二维的,除了二维还可以做三维四维。。。。。。
二维数组
int a[3][5];
通常理解为a是一个3行5列的矩阵
a[0][0] | a[0][1] | a[0][2] | a[0][3] | a[0][4] |
a[1][0] | a[1][1] | a[1][2] | a[1][3] | a[1][4] |
a[2][0] | a[2][1] | a[2][2] | a[2][3] | a[2][4] |
二维数组的遍历
二维数组遍历需要用两重循环,外面一重遍历行号,里面一重遍历列号
和一维数组一样,二维数组当中的每一个元素是一个整数,如果是double,那么每一个元素都是double
a[i][j]是一个int
表示第i行第j列上的单元
二维·数组的初始化
列数是必须给出的,行数可以由编译器来数
每行一个{},逗号分隔
最后的逗号可以存在,是传统
如果省略,可以补零
也可以用定位(*C99 ONLY)
代码定义:
数据类型 数组名[行数][列数];
基础操作三部曲:
1.声明与初始化
2.访问元素
3.遍历数组
内存存储原理
实际存储方式(重要)
虽然我们逻辑上看成表格,但内存中是连续线性存储:
matrix[0][0] → matrix[0][1] → matrix[0][2] →
matrix[1][0] → matrix[1][1] → matrix[1][2]
验证代码:
常见错误:
错误1:越界访问
错误2:错误初始化
错误3:错误遍历
数组运算
数组的集成初始化
int a[]={2,4,6,7,1,3,5,9,11,13,23,14,32};
直接用大括号给出数组的所有元素的初始值
不需要给出数组的大小,编译器替你数数
集成初始化时的定位
用[n]在初始化数据中给出定位
没有定位的数据接在前面的位置后面
其他位置的值补零
也可以不给出数组的大小,让编译器算
特别适合初始数据稀疏的数组
数组的大小
sizeof给出整个数组所占据的内容的大小,单位是字节
有一个数组,可以用size of那个数组去除以size of那个数组的第一个单元,得到的就是这个数组有多少个元素
sizeof(a)/sizeof(a[0])
sizeof(a[0])给出数组中单个元素的的大小,于是相处就得到了数组的单元个数
这样的代码,一旦修改数组中初始的数据,不需要修改遍历的代码
数组的赋值
数组变量本身不能被赋值
要把一个数组的所有元素交给另一个数组,必须采用遍历,写个循环,这是唯一的方法
遍历数组
做遍历的时候通常都是使用for巡回,让循环变量i从0到<数组的长度,这样循环体内最大的i正好是数组最大的有效下标
常见错误:
循环结束条件是<=数组长度
离开循环后,继续用i的值来做数组元素的下标
数组作为函数参数时,往往必须再用另一个参数来传入数组的大小
数组作为函数的参数时:
不能在[]中给出数组的大小
不能再利用sizeof来计算数组的元素个数
内存与执行过程图解
内存状态示例(输入x=6)
变量名 | 内存地址 | 存储值 | 说明 |
a[] | 0x1000 | [2,4,6,...] | 数组首地址 |
x | 0x2000 | 6 | 用户输入值 |
loc | 0x2004 | 2 | search返回值 |
search函数执行流程
Start[开始查找] --> Loop{是否检查完所有元素?}
Loop -- 否 --> Compare{当前元素等于key?}
Compare -- 是 --> Record[记录位置并结束]
Compare -- 否 --> Next[检查下一个元素]
Loop -- 是 --> Return[返回-1]
Record --> Return
关键语法特写
数组长度计算
函数参数传递
常见错误:
错误1:数组越界访问
错误2:忘记传递长度
错误3:错误理解索引
// 用户输入1,程序输出"在第0个位置"
// 因为数组索引从0开始!
数组例子:素数
内存状态表(初始状态):
变量名 | 值 | 说明 |
number | 4 | 目前生成4个素数 |
prime[0] | 2 | 已知素数 |
prime[1] | 0 | 未初始化 |
prime[2] | 0 | 未初始化 |
count | 1 | 当前素数数量 |
i | 3 | 待检测候选数 |
循环执行过程追踪:
第一次循环(count=1<3)
1. 调用
isPrime(3, [2], 1)
:- 参数传递:
x=3
knownPrimes[] = {2}
numberOfKnownPrimes=1
函数执行:
2. 更新数组和计数器:
prime[1] = 3
→ 数组变为[2,3,0,0]
count++
→ count=2
内存状态更新:
变量名 | 值 | 变化说明 |
prime[1] | 3 | 新增素数3 |
count | 2 | 计数器+1 |
i | 4 | 检测下一个数 |
第二次循环(count=2<3)
1. 调用
isPrime(4, [2,3], 2)
:检查过程:
2. 跳过存储操作,直接递增i。
内存状态更新:
变量名 | 值 | 变化说明 |
i | 5 | 检测下一个数 |
第三次循环(count=2<3)
1. 调用
isPrime(5, [2,3], 2)
:检查过程:
2. 更新数组和计数器:
prime[2] = 5
→ 数组变为[2,3,5,0]
count++
→ count=3
内存状态更新:
变量名 | 值 | 变化说明 |
prime[2] | 5 | 新增素数5 |
count | 3 | 计数器+1 |
i | 6 | 检测下一个数 |
输出阶段
关键概念深度解析
1. 数组的动态更新机制
- 逐步填充:数组初始只有2,随着循环逐个添加新素数
- 依赖关系:每个新素数的判断都基于之前已确认的素数
- 空间预分配:数组大小固定(3),实际应用应动态扩展
2. 候选数选择策略
- 从3开始:2是唯一的偶素数,后续只需检测奇数(可优化为
i+=2
)
- 顺序递增:简单但低效,实际应用可结合数学规律优化
3. 函数参数传递
- 数组传参:
prime
数组通过指针传递,函数内可访问原始数组
- 长度控制:
count
参数确保只检查有效素数,避免越界
线性搜索
在一个数组中找到某个数地位置(或确认是否存在)
基本方法:遍历
函数定义部分
参数详解:
参数名 | 技术说明 |
key | 需要查找的目标值 |
a[] | 被搜索的数组 |
len | 数组长度 |
主程序部分:
关键知识点:
- 数组长度计算:
sizeof(a)/sizeof(a[0])
sizeof(a)
→ 数组总字节数(10个int×4字节=40)sizeof(a[0])
→ 单个元素字节数(int为4字节)- 计算结果:40/4=10 → 数组长度
执行过程追踪
函数调用
search(12, a, 10)
参数传递:
参数 | 传递内容 | 内存示意图 |
key | 12 | 直接传递数值 |
a | 数组首地址 | 0x1000(假设地址) |
len | 10 | 直接传递数值 |
查找过程:
循环次数 | i值 | 当前a[i] | 比较结果 | ret值变化 | 动作说明 |
1 | 0 | 1 | 1≠12 | 保持-1 | 继续检查下一个单元号 |
2 | 1 | 3 | 3≠12 | 保持-1 | 继续 |
3 | 2 | 2 | 2≠12 | 保持-1 | 继续 |
4 | 3 | 5 | 5≠12 | 保持-1 | 继续 |
5 | 4 | 12 | 12=12 | -1→4 | 记录位置并停止检查 |
结果返回
return 4; // 返回单元号4(实际是数组索引)
输出4
结果说明:
- 在数组
a[]
中,数字12位于索引4的位置
- 索引从0开始计数,对应第5个元素
关键概念深度解析:
数组传递本质:
• 实际传递的是指针:函数接收的是数组首地址,不是数组副本
break的重要性:
- 若无break:会继续检查后续元素,但结果可能被覆盖
- 示例:若数组中有多个12,只返回第一个出现的位置
返回值设计
- 1的特殊含义:国际惯例用-1表示未找到
- 自然数表示位置:符合数组索引规范
搜索的例子
程序实现了一个硬币面值查询系统:
- 硬币信息库:存储硬币的面值和对应名称
- 查询功能:根据输入面值返回对应的硬币名称
- 数据结构优化:使用结构体替代原始的双数组方案
结构体定义:
内存示意图:
索引 | amount | name | 总字节数(假设 |
0 | 1 | 0x1000 | 4+8=12 |
1 | 5 | 0x1008 | 12 |
2 | 10 | 0x1010 | 12 |
3 | 25 | 0x1018 | 12 |
4 | 50 | 0x1020 | 12 |
主程序逻辑:
关键操作解析:
代码片段 | 功能说明 | 示例值(k=10时 |
sizeof(coins) | 计算数组总字节数 | 5元素×12字节=60 |
sizeof(coins[0]) | 单个元素字节数 | 12 |
sizeof(coins)/sizeof(coins[0]) | 计算数组长度 | 60/12=5 |
coins[i].amount | 访问面值字段 | 当i=2时,amount=10 |
coins[i].name | 访问名称字段 | 当i=2时,name="dime” |
执行过程追踪(k=10
循环次数 | i值 | coins[i].amount | 比较结果 | 动作 |
1 | 0 | 1 | 1≠10 | 继续 |
2 | 1 | 5 | 5≠10 | 继续 |
3 | 2 | 10 | 10=10 | 输出"dime"并退出 |
关键概念详解
结构体的优势
方案对比项 | 原始双数组方案 | 结构体方案 |
代码可读性 | 需要维护两个数组的索引对应关系(较差 | 数据自然关联(更好,语义明确 |
扩展性 | 添加新属性需新增数组 | 只需修改结构体定义 |
内存局部性 | 数据分散存储 | 数据连续存储,缓存更友好 |
错误风险 | 高(易出现索引错位) | 低 |
sizeof计算原理
- 数组总大小:
sizeof(coins) = 元素个数×单个元素大小
- 元素个数计算:
总大小 / 单个元素大小
- 示例验证:假设系统环境为:
int
占4字节- 指针占8字节
- 结构体总大小:4 + 8 = 12字节
- 5个元素总大小:5×12=60字节
- 计算结果:60/12=5 → 正确数组长度
代码演进对比
改进优势:
- 数据耦合:面值与名称天然绑定
- 错误预防:避免索引错位风险
- 扩展方便:新增字段只需修改结构体
struct
的作用1. 数据封装
将硬币的两个属性(面值
amount
和名称 name
)绑定为一个整体
• 意义:面值和名称不再是独立的数组,而是逻辑上关联的实体。2. 提高可维护性
- 添加新属性(如发行年份)时,只需修改结构体定义
• 旧方案(注释中的双数组)需维护多个数组的同步性,容易出错。
3. 保证数据一致性
- 结构体确保每个硬币的
amount
和name
天然对应,避免以下问题:
struct在技术层面是非必须的,但在设计层面强烈推荐
核心价值:
1. 数据建模
- 将现实世界的实体(如硬币)映射为代码中的数据结构。
- 每个硬币是一个完整的对象,而非分散的数据片段。
2. 减少耦合
- 函数操作以结构体为单位,无需关注多个数组的同步问题。
- 例如,交换两个硬币的位置只需操作结构体,无需分别调整
amount
和name
数组。
3. 面向对象思维的基础
- 结构体是C语言中实现封装和抽象的核心工具。
- 为后续学习C++/Java等面向对象语言奠定基础。
二分搜索
主程序部分
关键知识点:
sizeof(amount)/sizeof(amount[0])
:计算数组长度
- 数组必须有序:二分查找的前提条件
二分查找函数
参数说明
参数名 | 类比说明 | 技术说明 |
key | 要查找的数 | 目标值 |
a[] | 电话簿页面 | 有序数组 |
len | 电话簿总页数 | 数组长度 |
执行过程追踪(k=10
初始状态
第一轮循环
第二轮循环
第三轮循环
关键代码详解
1.中间值计算技巧:
优势:避免
left + right
可能超过整数最大值的情况
2.循环条件while (left <= right) // 允许左右边界重合
• 如果写成
left < right
,当目标在最后一位时会漏查3.边界调整
• 必要性:确保每次循环都能缩小搜索范围,避免死循环
二分查找通过比较中间值决定搜索方向,无序数组无法保证目标值在某一侧。
选择排序
实现选择排序算法,将数组元素按升序排列。程序分为两个主要部分:
max()
函数:查找数组中最大元素的索引
- 排序主逻辑:通过不断交换将最大值移动到数组末尾
代码逐行解析
1.最大值找函数
关键变量说明:
maxid
:当前找到的最大值索引
len
:参与比较的数组长度
执行示例
2.主函数排序逻辑
关键变量说明:
i
:当前要确定的位置(从数组末尾向前推进)
maxid
:每次循环找到的最大值索引
执行过程追踪
初始数组
索引:0 1 2 3 4 5 6 7 8 9 10
数值:2 45 6 12 87 34 90 24 23 11 65
第1次循环(i=10)
- 查找范围:0-10
- 找到最大值90(索引6)
- 交换a[6]和a[10]
结果:2 45 6 12 87 34 65 24 23 11 |90|
第2次循环(i=9)
- 查找范围:0-9
- 找到最大值87(索引4)
- 交换a[4]和a[9]
结果:2 45 6 12 11 34 65 24 23 |87 90|
第3次循环(i=8)
- 查找范围:0-8
- 找到最大值65(索引6)
- 交换a[6]和a[8]
结果:2 45 6 12 11 34 23 24 |65 87 90|
(后续循环依此类推,最终完成排序)
排序过程可视化:
关键算法特性:
特性 | 说明 |
算法类型 | 选择排序(每次选最大值 |
时间复杂度 | O(n²) |
空间复杂度 | O(1) |
稳定性 | 不稳定(交换可能改变相同元素顺序) |
优化点 | 可同时记录最大值和最小值 |
1. 为什么 i
从 len-1
递减?
- 每次确定数组末尾位置的值
- 已排序部分在数组尾部逐渐扩大
2. 为什么循环条件是 i > 0
?
- 当
i=0
时只剩一个元素无需处理
- 实际处理范围为
i = len-1
到i = 1
修改
max()
函数为查找最小值改为降序排序取地址运算:&运算符取得变量的地址
sizeof是一个运算符,给出某个类型或变量在内存中所占据的字节数
sizeof(int)
sizeof(i)
运算符&
scanf(”%d”,&i);里的&
scanf里面一定要加&,否则一定会出错
&符合的作用就是取得变量的地址,它的操作数必须是变量,它把变量的地址拿出来
因为C语言的变量是放在内存里头,所以要拿出来
int i;printf(”%x,&i)
地址的大小是否与int相同取决于编译器
int i;printf(”%p”,&i);
&不能取的地址
&不能对没有地址的东西取地址
&(a+)?
&(a++)?
&(++a)?
如果它的右边不是变量,那么就不能取地址,必须有一个明确的变量
说明:
&a
获取变量a
在内存中的地址。
%p
是格式化输出地址的占位符。
指针:指针变量就是记录地址的变量
指针就是保存地址的变量
int i;
int* p=&i;//*表示指针,它指向的是一个int,把i的地址交给了p:原本有一个变量i,现在又有了一个变量p,p这个变量是一个指针,它里面获得的值是i的地址,,p指向了i,实际的意思就是p里面的值是i变量的地址
int* p,q;
int *p,q;//这两个的意思是一样的
指针变量
变量的值是内存的地址
普通变量的值是实际的值
指针变量的值是具有实际值的变量的地址
作为参数的指针
void f(int *p)
在被调用的时候得到了某个变量的地址:
int i=0;f(&i);
在函数里面可以通过这个指针访问外面的这个i
访问那个地址上的变量*
*是一个单目运算符,用来访问指针的值所表示的地址上的变量
可以做右值也可以做左值
int k=*p;//放赋值号右边,读它的值
*p=k+1;//放赋值号左边,写它的值
执行流程与内存分析
1.main函数初始化
int i = 6;
- 内存分配:
- 变量
i
被分配在栈内存中(假设地址为0x7ffd4c3b456c
) - 存储值:
6
2.打印i的地址
printf("&i=%p\n", &i); // 输出:&i=0x7ffd4c3b456c
&i
获取变量i
的地址
%p
格式符用于打印指针(地址)
3.调用f(&i)
f(&i); // 传递i的地址
- 参数传递:
&i
的值为0x7ffd4c3b456c
- 函数
f
的形参p
接收该地址
4,函数f内部操作
p
存储的是i
的地址
p
解引用操作:访问p
指向的内存(即变量i
)
p = 26
实际修改了i
的值
5.调用g(i)
g(i); // 传递i的值(此时i=26)
- 参数传递:
- 将
i
的值26
复制给形参k
k
是独立变量,与i
无关联
6.函数g内部操作
关键概念详解
1. 指针的本质
- 指针变量:存储内存地址的变量(如
int *p
)
- 解引用操作:
p
表示访问指针指向的内存位置的值
2. 地址传递 vs 值传递
特征 | f(&i)(地址传递) | g(i)(值传递) |
传递内容 | i的地址 | i的值的副本 |
内存影响 | 可通过指针修改原始变量 | 无法修改原始变量 |
性能 | 高效(传递4/8字节地址) | 可能低效(大对象需拷贝) |
3. 内存变化图示
输出结果
常见问题
1. 为什么p
和&i
的地址相同?
p
存储的是i
的地址,因此p
的值等于&i
2. p
和i
的关系
p
是i
的别名,操作p
等同于操作i
3. 为什么g(i)
输出26?
f
函数通过指针修改了i
的值,g
函数接收到的是修改后的值
4. 能否修改p
的指向?
总结
- 指针核心操作:
&
取地址- *解引用
- 函数参数传递:
- 地址传递允许跨函数修改变量
- 值传递仅操作副本
- 内存直接操作:
- 指针提供了底层内存访问能力
- 需谨慎避免悬空指针和越界访问
指针与数组:为什么数组传进函数后的sizeof不对了
函数参数表中的数组实际上是指针
sizeof(a)==sizeof(int*)
但是可以用数组的运算符[]进行运算
数组参数
以下四种函数原型是等价的:
int sum(int *ar,int n);
int sum(int *,int);
int sum(int ar[],int n);
int sum(int [],int);
数组变量是特殊的指针
数组变量本身表达地址,所以:
int a[10]; int*p=a;//无需用&取地址
但是数组的单元表达的是变量,需要用&取地址
a==&a[0]
[]运算符可以对数组做,也可以对指针做:
p[0]<==>a[0]
运算符可以对指针做,也可以对数组做:
*a=25;
数组变量是const的指针,所以不能被赋值
int a[]<==>int *const a=……
关键概念详解
1. 数组传参的本质
- 传递指针:
int a[]
实际等价于int *a
- sizeof差异:
main
中的sizeof(a)
:计算数组总字节数minmax
中的sizeof(a)
:计算指针变量的大小(64位系统为8字节)
2. 指针参数操作
操作 | 等效写法 | 功能说明 |
*min=a[0] | *(min)=a[0] | 修改main中min变量的值 |
*max=a[i] | *(max)=a[i] | 修改main中max变量的值 |
3. 数组与指针的关系
表达式 | 等效形式 | 说明 |
a[0] | *(a+0) | 首元素 |
p[0] | *p | 指针指向的第一个元素 |
*a | a[0] | 数组首元素 |
内存操作追踪
数组内存布局
指针变量关系
输出结果分析
1. 为什么函数内修改a[0]会影响main中的数组?
数组名作为参数传递时,实际传递的是数组首地址指针。函数内通过指针直接操作原数组内存。
2. 为什么min最终是2而不是1?
初始时
*min = a[0] = 1000
,但遍历从i=1开始:- i=1时,a[1]=2 < 1000 → min=2
- 后续元素均大于2,最终min保持2
3. p[0]和*p有何区别?
无区别。
p[0]
等价于*(p+0)
,即*p
。4. 为什么max是1000?
初始设置
*max = a[0] = 1000
,后续遍历中所有元素均小于1000,因此未更新。错题
1.对于数组
int a[]={5,15,34,54,14,2,52,72};
int *p = &a[5];
1. 数组索引与内存布局
数组
a
的元素及索引如下:2. 指针初始化
p
指向a[5]
(即索引5的位置),其值为2
。
- 此时
p
的逻辑位置对应数组的第六个元素。
3. 指针运算规则
p[-n]
等价于(p - n)
。
- 指针减法基于数据类型大小。对于
int
指针,p - n
会向前移动n × sizeof(int)
字节。
4. 计算 p[-2]
p
初始指向a[5]
(索引5)。
p - 2
向前移动2个int
的位置,即索引5 - 2 = 3
。
- 因此,
p[-2]
对应a[3]
,其值为 54。
结论
p[-2]
的值为 54。指针运算
p[-2]
通过地址回退两个 int
单位,最终访问到 a[3]
。2.如果int a[]={0};
int *p=a;以下表达式的结果为什么成立,为什么不成立
a:p==&a[0].b:*p==a[0].c:p[0]==a[0].c:p==a[0]
表达式分析
a. p == &a[0]
- 数组名
a
的本质:
在大多数情况下,数组名
a
会被隐式转换为指向数组首元素(a[0]
)的指针,即 a
等价于 &a[0]
。- 指针初始化:
int *p = a;
等价于 int *p = &a[0];
,因此 p
存储的是 a[0]
的地址。- 结论:
p
的值等于 &a[0]
,故 p == &a[0]
成立。b. p == a[0]
- 解引用操作:
p
表示访问p
指向的内存位置的值。由于p
指向a[0]
,因此p
等价于a[0]
。
- 示例验证:
- 结论:
p
的值与a[0]
相同,故p == a[0]
成立。
c. p[0] == a[0]
- 指针的下标操作:
C语言中,指针可以使用下标操作符
[]
,其定义为:p[n]
等价于 *(p + n)
。- 下标为0的情况:
p[0]
等价于 *(p + 0)
,即 *p
,而 *p
已证明等于 a[0]
。- 结论:
p[0]
和 a[0]
的值相同,故 p[0] == a[0]
成立。内存布局图示
总结:
表达式 | 等价形式 | 成立原因 |
p==&a[0] | p=&a[0] | 数组名隐式转换为首元素地址 |
*p==a[0] | *(&a[0])==a[0] | 解引用指针直接访问数组首元素值 |
p[0]==a[0] | *(p+0)==a[0] | 指针下标操作符的本质是地址偏移与解引用 |
表达式
p == a[0]
不成立,因为它涉及类型不匹配的问题。1. 类型分析
- 指针变量
p
:
p
是 int*
类型(指向整型的指针),存储的是内存地址。- 数组元素
a[0]
:
a[0]
是 int
类型(整型),存储的是整数值(本例中为 0
)。2. 直接比较的错误性
- 地址 vs 值:
指针
p
保存的是 a[0]
的地址(如 0x7ffd4c3b456c
),而 a[0]
的值是 0
。两者本质上是不同类型的数据,直接比较没有意义。
错误原因:
p是地址,如(0x7ffd4c3b456c),而a[0]是整数0
它们的类型不同(
int*
vs int
),无法直接比较。表达式 | 类型 | 是否合法 | 说明 |
p==&a[0] | int*==int* | 是 | 合法,比较两个指针地址 |
*p==a[0] | int==int | 是 | 合法,比较两个整数值 |
p==a[0] | int*==int | 否 | 非法,类型不匹配 |
3.变量定义int* p,q;是指针吗
p
的类型:int*
(指向整型的指针)
q
的类型:int
(普通整型变量)
int *p; // p是指针
int q; // q是普通int变量
很多人初学者会认为
int*
是一个整体类型,但C语言的声明语法规则是:- 类型修饰符(如)仅作用于紧邻的变量名,而非逗号分隔的所有变量。
因此,若想声明两个指针,必须为每个变量单独添加*:
int *p, *q; // p和q都是int指针
代码验证
- 语法规则: 仅修饰紧邻的变量名。
- 建议写法:将 靠近变量名而非类型,以提高可读性
避免混淆:分行声明指针变量:
int *p; // 指针
int *q; // 指针
字符类型
char是一种整数,也是一种特殊的类型:字符。这是因为:
用单引号表示的字符字面量:’a’,’1’
’‘也是一个字符
printf和scanf里用%c来输入输出字符
字符的输入输出
输入’1‘这个字符给char c
scanf(”%c”,&c);——>1
scanf(”%d”,&i);c=i;——>49
‘1’的ASCII编码是49,所以当c==49时,它代表’1‘
大小写转换
字母在ASCII表中时顺序排序的
大写字母和小写子母是分开排序的,并不在一起
’a’-’A’可以得到两段之间的距离,于是:
a+’a’-’A’可以把一个大写字母变成小写字母,而
a+’A’-’a’可以把一个小写字母变成大写字母
每一个字符在计算机内部都有一个值去表达它,这个值是可以直接以整数的形式得到的
程序功能:
- 比较字符变量:验证直接赋值整数与字符字面量的区别
- 输出变量值:以十进制整数形式显示字符变量的实际存储值
关键概念详解
1. 字符变量的本质
- 存储形式:
char
类型变量实际存储的是ASCII码的整数值(1字节)。
- 赋值方式:
c = 1;
:直接存储整数1(ASCII码对应控制字符SOH)。d = '1';
:存储字符'1'的ASCII码值49。
2. ASCII码表片段
字符 | ASCII码(十进制) | 说明 |
SOH | 1 | 控制字符(不可打印) |
'1’ | 49 | 数字字符1 |
3. 比较操作的底层逻辑
c == d
等价于1 == 49
:结果为假(false),因此输出“不相等”。
为什么c=1
和d='1'
不相等?
c
存储的是整数1(ASCII码SOH),d
存储的是整数49(ASCII码字符'1')。
- 两者的存储值不同,因此比较结果为不相等。
为什么用%d
输出字符变量?
%d
将char
类型提升为int
类型输出十进制值,便于观察实际存储的ASCII码。
字符字面量的本质
'1'
在编译时会被替换为其ASCII码值49,等价于直接写d = 49
。
- 字符变量的本质是整数:存储ASCII码值。
- 直接赋值与字符字面量的区别:
1
是整数,对应ASCII码控制字符。'1'
是字符字面量,对应ASCII码49。
- 比较操作基于实际存储值:不同类型赋值会导致值不同。
ASCII码的可打印性
ASCII范围 | 字符类型 | 示例输出 |
0~31 | 控制字符(不可见) | 无显示或特殊符号 |
32~126 | 可打印字符 | 字母、数字、符号 |
127~255 | 扩展字符集 | 因系统而异 |
示例
逃逸字符
用来表达无法打印出来的控制字符或特殊字符,它由一个反斜杠“\”开头,后面跟上另一个字符,这两个字符合起来,组成了一个字符
\”的意思就是说反斜杠加上双引号成为一个字符,而不是两个字符这个字符表示的就是那个双引号,之所以这么做是因为在双引号里面不能直接出现双引号,否则会认为后一个双引号和前一个双引号是一个字符串,所以要用反斜杠加双引号来表达双引号,这种就叫逃逸字符,就是两个符号连在一起算一个字符
逃逸字符:
字符 | 意义 | 字符 | 意义 |
\b | 回退一格 | \” | 双引号 |
\t | 到下一个表格位 | \’ | 单引号 |
\n | 换行 | \\ | 反斜杠本身 |
\r | 回车 | ㅤ | ㅤ |
制表位
每行的固定位置
一个\t使得输出从下一个制表位开始
用\t才能使得上下两行对齐
字符串
字符数组
char word={‘H’,’e’,’l’,’l’,’o’,’!’,’0’};
word[0] | H |
word[1] | e |
word[2] | l |
word[3] | l |
word[4] | o |
word[5] | ! |
word[6] | \0 |
字符串:
以0(整数0)结尾的一串字符
0或‘\0’是一样的,但是和‘0’不同
0标志字符串的结束,但它不是字符串的一部分
计算字符串长度的时候不包含这个0
字符串以数组的形式存在,以数组或指针的形式访问
更多的是以指针的形式
string.h里有很多处理字符串的函数
字符串变量
char *str=”Hello”;
cahr word[]=”Hello”;
char line[10]=”Hello”;
字符串常量
“Hello”
“Hello”会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0
两个相邻的字符串常量会被自动连接起来
C语言的字符串是以字符数组的形式存在的
不能用运算符对字符串做运算
通过数组的方式可以遍历字符串
唯一特殊的地方是主席付出字面量可以用来初始化字符数组
以及标准库提供了一系列字符串函数
字符数组形式
字符串变量
字符串常量
char* s=”Hello,world!”;
s是一个指针,初始化为指向一个字符串常量
由于这个常量所在的地方,所以实际上s是const char *s,但由于历史的原因,编译器接受不带const的写法
但是试图对s所指的字符串做写入会导致严重的后果
如果需要修改字符串,应该用数组:
char s[]=”HEllo,world!”;
char *str=”Hello”;
cahr word[]=”Hello”;
数组:这个字符串在这里,作为本地变量空间自动被回收
指针:这个字符串不知道在哪里,处理参数,动态分配空间
如果要构造一个字符串:用数组
如果要处理一个字符串:用指针
字符串可以表达为char*,char*不一定是字符串
本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
只有当‘char*指向的字符数组有结尾的0,才能说它所指的是字符串
关键概念详解
1.内存区域划分
内存区域 | 存储内容 | 可修改性 | 示例 |
栈(stack) | 局部变量、函数参数 | 可读可写 | i,s3 |
只读数据段 | 字符串常量 | 只读 | s和s2指向的内容 |
堆(Heap) | 动态分配的内存 | 可读可写 | 未涉及 |
2,变量类型与行为对比
特性 | char *s=”…” | char s3[]=”…” |
存储位置 | 指针变量在栈,内容在只读数据段 | 整个数组在栈内存 |
可修改性 | 内容不可修改 | 内容可修改 |
sizeof行为 | 返回指针大小(如8字节) | 返回数组总字节数(含'\0') |
初始化方式 | 存储字符串常量的地址 | 拷贝字符串到栈内存 |
为什么printf("s=%p\n", &s)
输出的是指针地址而非字符串地址?
&s
获取的是指针变量s
本身的栈地址,而非字符串的地址。
- 若要打印字符串地址:应使用
printf("s=%p\n", s)
。
为什么s
和s2
可能指向同一地址?
- 编译器优化技术会合并相同的字符串常量,因此多个指针可能指向同一内存区域。
- 验证方法:直接比较指针值:
printf("s == s2 ? %d\n", s == s2); // 可能输出1(true)
为什么s3
可以修改而s
不能?
s3
是栈上的字符数组,程序运行时完整拷贝字符串到栈内存。
s
指向只读数据段,操作系统会禁止写入。
字符串输入输出
字符串赋值
char *t=”title”;
char *s;
s=t;
并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的
字符串输入输出
char string[8];
scanf(”%s”,string);
printf(”%s”,string);
scanf读入一个单词(到空格、tab或回车为止)
scanf是不安全的,因为不知道要读入的内容的长度
安全的输入
char string[8];
scanf(”%7s”,string);
在%和s之间的数字表示最多允许读入的字符的数量,这个数字应该比数字的大小小一
如果它就读到了那么多个,那么后面的空格有没有就没有关系了,不是一句空格来区分单词了,而是根据这个个数来划定这个单词,所以下面的内容会交给下一个百分号s或下面其他的这个scanf去阅读
常见错误
char *string;//只是定义了一个指针变量,string是一个将来要去指向某个字符串数组的某一块内存空间的一个指针(未初始化)
scanf(”%s”,string);
以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了
由于没有对string初始化为0,所以不一定每次运行都出错
空字符串
char buffer[100]=””;
这是一个空的字符串,buffer[0]==’\0’
char buffer[]=””;
这个数组的长度只有1!
字符串的输出
步骤说明:
printf
使用%s
格式符输出字符串。
- 字符串以
\0
(ASCII 0)结尾,printf
会自动识别终止符。
字符串的输入
步骤说明:
scanf
遇到空格停止,可能溢出(不推荐直接使用)。
fgets
需指定最大读取长度,格式:fgets(数组名, 数组长度, stdin)
。
stdin
表示标准输入流(键盘输入)。
核心概念:字符串的本质与操作
字符串的存储结构:
字符串本质是字符数组,以
\0
结尾:字符串的两种定义方式
方式 | 示例 | 可修改性 | 内存区域 |
字符数组 | char s[]=”Hello”; | 可修改 | 栈内存 |
指针指向字符串常量 | const char *s=”Hello”; | 不可修改 | 只读段 |
输入输出函数对比
函数 | 行为特性 | 安全性 | 换行符处理 |
scanf | 遇到空格停止,不检查长度 | 危险 | 不保留 |
fgets | 读取整行,包含换行符 | 安全 | 保留 |
gets | 已废弃(无长度限制) | 极度危险 | 保留 |
- 字符串本质:以
\0
结尾的字符数组。
- 输入输出函数:
- 优先使用
fgets
,避免scanf
和gets
。 - 始终检查输入长度。
- 安全编程:
- 字符数组预留
\0
空间。 - 处理输入后的换行符。
- 实战技巧:
- 使用
strlen
、strcpy
等函数时需明确长度。 - 操作字符串常量时使用
const
修饰。
执行流程
- 内存分配:
word
和word2
各分配8字节栈内存(可存储7字符 +\0
)。
- 输入阶段:
scanf("%s", word)
:从键盘读取输入,遇到空格或换行停止,自动添加\0
。scanf("%s", word2)
:同上。
- 输出阶段:将两个字符串以
##
分隔输出。
核心概念:字符串输入的关键机制
1. scanf("%s")
的行为
- 读取规则:从非空白符开始,到空白符(空格、换行、Tab)结束。
- 存储方式:自动在末尾添加
\0
。
- 示例输入:
2. 内存布局示例
假设输入为
ABCDEFGH
和 1234
:改进点:
%7s
:限制最多读取7字符(预留\0
位置)。
安全规范与总结
1. 必须遵守的规则
- 预留
\0
空间:数组长度 ≥ 最大输入字符数 + 1。
- 限制输入长度:
scanf
使用%7s
格式,fgets
直接指定数组大小。
- 清空输入缓冲区:防止残留字符影响后续输入。
2. 最佳实践总结
场景 | 推荐方法 |
读取单个单词 | scanf(”%7s”,arr)+清空缓冲区 |
读取含空格的句子 | fgets(arr,sizeof(arr),stdin) |
输入验证 | 检查strlen(arr)是否合法 |
字符串函数
string.h
strlen
size_strlen(const char *s)
返回s的字符串长度(不包括结尾的0)
strcmp
int strcmp(const char *s1,const char *s2);
比较两个字符串,返回:
0:s1==s2
1:s1>s2
-1:s1<s2
strcpy
char *strcpy(char *restrict dst,const char *restrict src);
把src的字符串拷贝到dst
restrict表明src和dst不重叠(C99)
返回dst
为了能链起代码来
strcat
char *strcat(char *restrict s1,const char *restrict s2);
把s2拷贝到s1的后面,接成一个长的字符串
返回s1
s1必须具有足够的空间
strcpy和ctrcat都可能出现安全问题,可能目的地没有足够的空间
安全版本:在函数中间多了一个n,参数表里多了一个n
字符串里中找字符
strchr
char *strchr(const char *s,int c);
在字符串里寻找C第一次出现的位置,从左边数过来,返回指针
char *strrchr(const char *s,int c);
从右边数过来
返回NULL表示没有找到
strstr
分步教程:常用字符串函数详解
1. 字符串长度:strlen
步骤说明:
strlen
遍历字符直到遇到\0
。
- 返回值类型为
size_t
(建议使用%zu
格式化输出)。
2. 字符串复制:
strcpy
与strncpy
3. 字符串连接:
strcat
与strncat
4. 字符串比较:
strcmp
与strncmp
核心概念:字符串操作的本质
1. 字符串函数的底层原理
函数 | 实现逻辑 | 时间复杂度 |
strlen | 遍历直到\0 | O(n) |
strcpy | 逐字符复制(含\0) | O(n) |
strcat | 先找到目标字符串末尾,再追加内容 | O(n+m) |
strcmp | 逐个字符比较ASCII码 | O(n) |
2. 内存管理要点
- 目标数组必须足够大:
strcpy
和strcat
不检查目标容量。
- 安全函数优先:
strncpy
替代strcpy
strncat
替代strcat
strncmp
替代strcmp
3. 返回值规则
函数 | 返回值意义 |
strlen | 字符串有效长度(不含\0) |
strcpy | 返回目标字符串地址 |
strcmp | 0(相等)/负数(s1<s2)/正数(s1>s2) |
对比分析:安全函数 vs 基础函数
1. strcpy
vs strncpy
特性 | strcpy | strncpy |
安全性 | 可能溢出 | 可限制最大复制长度 |
终止符处理 | 自动添加\0 | 不会自动添加\0(需手动处理) |
适用场景 | 标数组明确足够大时 | 需要防止溢出的场景 |
2.
strcmp
vs strncmp
特性 | strcmp | strncmp |
比较范围 | 比较到任意一个字符串结束 | 只比较前n个字符 |
典型应用 | 完全匹配验证 | 前缀匹配(如文件扩展名检查) |
安全规范与总结
1. 必须遵循的规则
检查目标数组大小:
输入验证:
2. 扩展函数列表
函数 | 功能描述 |
strchr | 查找字符首次出现位置 |
strstr | 查找子串首次出现位置 |
memset | 内存填充(常用于初始化) |
memcpy | 内存块复制(不限字符串) |
- Author:Anten
- URL:https://www.anten.ysepan.com/article/example-4
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!