news 2026/10/21 2:32:20

大一C语言学习记录-3

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大一C语言学习记录-3

自定义函数

函数的作用

打包,模块化

函数四要素

返回值类型
函数名
形式参数表
函数体

函数的定义

语法格式
返回值类型 函数名(形参表) {
// 函数体(具体执行的内容)
return 结果; // 如果返回值类型是 void,则不需要 return
}
示例

intadd(inta,intb){intsum=a+b;returnsum;// 把算好的结果交出去}

函数的声明

在 C 语言中,代码是从上往下按顺序编译的。
如果你把函数定义在 main 函数的下面,编译器在 main 里看到你调用 add 时,会不认识它,报错:“找不到 add”。
为了让编译器认识它,我们需要在 main 前面提前声明。
语法:把函数头抄一遍,末尾加个分号。
函数声明是一个语句,要加分号,而函数定义时函数首部不是语句,不加分号

函数的调用

在 main 或其他函数里,写上 函数名(参数) 就可以用了。

intmain(){intx=3,y=5;intans=add(x,y);// 调用 add,把 x 和 y 传进去,把返回值赋给 ansprintf("%d\n",ans);// 输出 8return0;}

执行流程:
1.程序运行到 add(x, y),暂停 main 函数。
2.把 x 的值(3)复制给 a,y 的值(5)复制给 b。
3.跳到 add 函数里,算 3 + 5 = 8。
4.遇到 return 8,把 8 带回刚才暂停的地方,并退出 add 函数。
5.main 接着往下走,将 8 赋给 ans。

函数的分类

依据有无参数,有无返回值分为四类
1.有参数,有返回值:最常见。比如 sqrt(x),传个 4 进去,返回 2.0。
2.有参数,无返回值:只做事,不交结果。(这样的函数也称过程)
注意void不能省略,否则函数被默认定义为int

voidprint_hello(intn){for(inti=0;i<n;i++)printf("Hello\n");}

3.无参数,无返回值:纯粹为了封装一段代码。

voidmenu(){printf("1. 开始游戏\n");printf("2. 退出\n");}

4.无参数,有返回值:比如获取系统当前时间。

易错点

1.形参表变量定义不能连着定义
错误形式:int example(int a,b);
正确形式:int example(int a,int b);

2.形参与实参的数据类型可以不一样,会发生自动转换
但是目前阶段不推荐,尽量保持数据类型相同

voidprintNum(intn){printf("接收到的数字是: %d\n",n);}intmain(){doublea=3.99;printNum(a);return0;}

返回结果是“接收到的数字是: 3”
这里3.99直接截掉小数部分**(不是四舍五入)**,发生了从double到int的自动转换
自动转换的方向是把实参转化为形参的类型
数值类型(int、double、char 等)互相传参是可以的,按形参类型自动转换(小心丢精度);但指针类型必须严格一致,否则后果很严重

3.形参与实参弄混
形参(形式参数):定义函数时括号里的变量(如 int a, int b),只在函数内部有效。
实参(实际参数):调用函数时传进去的具体值或变量(如 add(x, y) 里的 x, y)。
它们只是名字,完全可以不一样。

4.传参是值传递,在被调函数里面只改变形参,不改变实参

voidchange(intx){x=100;// 修改的只是副本}intmain(){inta=5;change(a);printf("%d",a);// 依然是 5,不是 100!}

原因:调用函数时,传进去的是 a 的复印件。函数里改复印件,原件不受影响。
(注:如果想改变外面的变量,需要传指针 &a,这属于进阶内容。)

5.return只能返回一个值
return的作用有两个,一是结束函数运行,而是为主调函数提供返回值
return只能返回一个值,若要返回多个,可通过全局变量或指针


局部变量与全局变量

内外定义

函数内部:main 函数或其他函数的大括号 {} 里面。
函数外部:所有函数的外面(通常在 #include 下面)。

局部变量

1.特点
1).私有性:只能在它所在的那个大括号 {} 里面使用。出了这个门,谁也不认识它
2).生命周期短:函数被调用时,它诞生;函数执行结束,它立刻被销毁(内存被回收)
3).未初始化会变“垃圾”:如果你写 int a; 没给初值,它的值是不确定的(往往是内存里残留的垃圾值,如8263232)

2.作用:避免变量互相干扰
eg.形参不会影响实参,分属不同函数的局部变量可以同名.

3.小范围局部变量
可以用在{}复合语句里面用局部变量,作用范围仅限于{}内

intmain(){inta=5;{inta=10;// 这是一个全新的局部变量printf("%d\n",a);// 输出 10}printf("%d\n",a);// 输出 5}

全局变量

特点
1). 共享性:从它定义的那一行开始,一直到文件结束,所有的函数都能直接使用和修改它。
2). 生命周期长:程序一开始运行它就诞生,直到整个程序彻底结束才销毁
3). 默认初始化为 0:如果你不给它赋初值(int g;),编译器会自动把它初始化为 0(不像局部变量那样是垃圾值)。

比较

特性局部变量全局变量
定义位置函数或 {} 内部所有函数外部
作用范围仅限其所在的 {} 内整个程序文件
生命周期函数执行时诞生,结束时销毁程序开始时诞生,结束时销毁
初始值随机的“垃圾值”自动初始化为 0
内存位置通常存在栈(Stack)区存在静态/全局数据区
数量可以很多(每次调用都在栈上分配)尽量少,整个程序只有一份

如果名字冲突了怎么办?(就近原则)

如果局部变量和全局变量同名,在函数内部,局部变量会“屏蔽”(覆盖)全局变量。

全局变量尽量少用

既然全局变量这么方便,所有函数都能用,是不是应该多用?
绝对不要多用!工程界有一句名言:“尽量少用全局变量。”

为什么?

1.破坏封装,难以排错:如果一个代码有 100 个函数,里面有个全局变量 score。程序运行出错了,score 变成了 -999。你根本不知道是这 100 个函数里哪一个把它改错了,排查难度极大。
2.牵一发而动全身:改个全局变量,可能影响其他地方。
3.多线程灾难:如果两个函数同时修改一个全局变量,会发生数据冲突(线程安全问题)。

正确用法:
1.只用来定义程序级别的常量(比如 #define MAX 100 或 const double PI = 3.14159;)。
2.用来定义各个函数都需要读取,但很少修改的状态。
3.函数之间传数据,首选“传参”和“返回值”,而不是靠全局变量。


变量声明周期与静态局部变量

待完善…


数组

数组是什么

数组就是一块连续的内存空间,里面存放着一组相同类型的数据

初始化的三种方式

int a[5] = {10, 20, 30, 40, 50};
int b[5] = {1, 2}; // 结果是:1, 2, 0, 0, 0
int c[] = {1, 2, 3}; // 编译器发现只有 3 个,自动确定长度为 3
注意:不能写成 int d[5]; d = {1,2,3,4,5}; 这是错的!必须在定义时初始化

下标

数组的下标从0开始(易错),如果数组的长度为5,那么有效下标为0,1,2,3,4,没有5
数组越界:如果使用了超出数组长度的下标,C 语言不会报错,它会默默地去读写这块连续内存旁边“别人”的数据,导致程序莫名其妙崩溃,或者把别的变量改掉。

字符数组

在 C 语言中,没有专门的“字符串”类型。字符串其实就是以 \0(空字符)结尾的字符数组

// 方式 1:字符数组(必须手动在末尾加 \0)charstr1[]={'H','e','l','l','o','\0'};// 方式 2:直接用双引号(编译器会自动帮你加上 \0)charstr2[]="Hello";

在内存中,str2 占 6 个字节(5 个字母 + 1 个 \0)。

常用的字符串操作(需要引入 <string.h>):

  • strlen(str):求字符串长度,遇 \0则停,返回长度
  • strcpy(str1, str2):把 str2 复制给 str1,作用是给字符串赋值,因为 C 语言里不能写 str1 = “hello”;(数组名是常量不能赋值),但注意容量不能超,否则会溢出
  • strcmp(str1, str2):比较两个字符串,相等返回0,str1在字典序上排在str2后面返回正整数,反之返回负整数
  • strcat(str1, str2):把str2 拼接到 str1 后面,返回str1的地址。注意:它会把 str2 追加到 str1 的 \0 处,并把最后的新结尾补上 \0。同样不检查越界,str1 必须足够大

函数与数组的结合

在 C 语言中,数组作为参数传递给函数时,传递的是数组的首地址(相当于指针),而不是把整个数组复制一份。所以函数内部修改数组,外面的原数组也会被改变。

#include<stdio.h>// 传递数组时,通常还要传一个长度 n,因为函数内部不知道数组有多长voidprintArray(intarr[],intn){for(inti=0;i<n;i++){printf("%d ",arr[i]);}printf("\n");}intmain(){intmyArr[5]={10,20,30,40,50};printArray(myArr,5);// 只需传数组名,不需要加 &return0;}

为什么传递数组必须传递长度n?
因为数组在传给函数时,会“退化”成一个指针(只记录了第一个元素的地址),它丢失了大小信息
如果写一个传递时不加长度n,函数内部不知道这个数组到底有几个元素,如果盲目遍历,就会越界(读到未知的垃圾数据,甚至崩溃)

如何避免数组被意外修改?
如果你希望函数只读取数组,绝对不许修改,可以在参数前加 const 关键字:

voidprintArray(constintarr[],intn){// arr[0] = 999; // ❌ 如果这里试图修改,编译器会直接报错printf("%d",arr[0]);// ✅ 只能读}

二维数组(矩阵)

intmatrix[2][3]={{1,2,3},// 第 0 行{4,5,6}// 第 1 行};// 访问第 1 行第 2 列(下标从 0 开始,所以是 5)printf("%d",matrix[1][1]);

遍历二维数组需要双重for循环

什么时候数组后面加[]

1.定义/声明时要加:告诉编译器这是一个数组
2.函数接收参数时:加 [] 但里面不写长度
因为数组传参会退化为指针,写了长度也没用(编译器会忽略)
// 以下两种写法完全等价,都表示接收一个指针
void printArr(int arr[], int n);
void printArr(int *arr, int n);
3.使用时:只操作单个元素要加 [],操作整个字符串不加
不加 []:当你把整个数组交给函数处理,或者用 scanf 读入字符串时
scanf(“%s”, str); // 注意:字符串这里不加 &
printf(“%s”, str);
strlen(str); // 传整个字符串

易错

用==比较两个数组
int a[3] = {1, 2, 3};
int b[3] = {1, 2, 3};
if (a == b) // ❌ 错误!比较的是它们的内存地址,结果永远是假
正确做法:用 for 循环逐个元素比较,或者用 memcmp();如果是字符串,用 strcmp()


从零开始的指针

按教学进度,本人暂未学到指针,只是目前的数组已经涉及到了指针的内容,故通过AI暂且进行了粗浅的学习,本节均由AI生成

一、核心比喻:酒店房间与房卡

  • 变量:就是酒店里的房间(用来住人,也就是存数据)。
  • 内存地址:就是房间的门牌号(比如 308 房,对应内存的编号 0x7ffee…)。
  • 指针:就是一张写着门牌号的纸条(或房卡)。

指针的本质,就是一个专门用来存“内存地址”的变量。

如果你写 int a = 5;,相当于在内存里开了一个叫 a 的房间,里面住了个数字 5。
如果你写 int *p = &a;,相当于拿了一张纸条 p,上面写下了 a 的房间号。

二、两个关键符号:& 和 *

  • &(取地址符):去查房间在哪。&a 就是拿到变量 a 的地址。
  • *(解引用符):拿着纸条上的地址,直接去开房间的门,操作里面的东西。*p 就是走到 p 记录的地址,拿出里面的值。
inta=5;int*p=&a;// p 存了 a 的地址printf("%d\n",a);// 输出 5printf("%d\n",*p);// 输出 5 (*p 就是拿房卡开门,看到里面的 5)*p=100;// 拿房卡进去,把里面的 5 换成了 100printf("%d\n",a);// 输出 100 (因为改的就是 a 的房间)

三、在上面的语境里,指针都有哪些意思?

我们在前面讲数组、字符串、函数传参时频繁提到指针。其实在这些语境下,指针就是“地址”、“门牌号”的意思。具体有以下 4 种表现:

数组名就是指针(首地址)

当你写 int arr[5] = {1, 2, 3, 4, 5}; 时,arr 这个数组名,本质上是它第一个元素的内存地址(也就是 &arr[0])。
所以,数组名不加 [] 直接拿去用的时候,它就是一个指针。
int arr[3] = {10, 20, 30};
// arr 就是指针,代表第一个元素 10 的地址。
// *arr 就是 10。
// *(arr + 1) 就是 20。(等价于 arr[1])

为什么函数传数组,外面也会变?(因为传的是指针)

前面说过,函数传参时,数组会自动“退化”成指针。
当你写 void changeArray(int arr[]) 时,编译器根本不看 [],它实际看到的是:
void changeArray(int *arr)。
你调用 changeArray(myArr) 时,传给函数的不是那一堆数据,而是第一个元素的地址。
函数拿到地址,就能顺藤摸瓜找到原数组并修改,所以外面的原数组变了。

为什么 scanf 必须加 &?

因为 scanf 的目的,是要把用户输入的数据存进你的变量里。
如果它不拿到变量的地址(门牌号),它怎么把数据送进对应的房间呢?
(特例:读字符串 scanf(“%s”, str) 不加 &,因为 str 本身是个字符数组,数组名本身就是地址,已经是一张写着门牌号的纸条了。)

字符指针 char *(字符串的本质)

在 C 语言里,你用双引号写的 “Hello”,它本质上是一个常量字符数组。如果你写 char *s = “Hello”;,s 就是一个指针,指向 ‘H’ 所在的内存地址。

为什么 strcpy(str1, str2) 能改变 str1?

strcpy 的官方原型是:
char *strcpy(char *dest, const char *src);
你看,它的参数是 char *(字符指针)。也就是说,你传给它的 str1 和 str2,实际上传的是两个字符串的起始地址。它顺着 str1 的地址,把 str2 地址里的内容一个一个抄过去,所以 str1 的内容就变了。


math库

1.幂,开方,指对

函数原型作用
sqrtdouble sqrt(double x)平方根 x​,x≥0
powdouble pow(double x, double y)平方根 x​,x≥0
expdouble exp(double x)e 的 x 次幂
logdouble log(double x)自然对数 lnx(底数 e,x>0)
log10double log10(double x)常用对数 lgx(底数 10,x>0)

2.绝对值(易错)

函数原型作用注意
absint abs(int x)整数绝对值在 stdlib.h,不是 math.h!
fabsdouble fabs(double x)浮点数绝对值在math.h,小数必须用这个!

3.取整函数

函数原型作用示例
floordouble floor(double x)向下取整,往小方向取floor(2.9)=2,floor(-2.1)=-3
ceildouble ceil(double x)向上取整,往大方向取ceil(2.1)=3,ceil(-2.9)=-2
rounddouble round(double x)四舍五入round(2.4)=2,round(2.5)=3

4.三角函数(参数是弧度)
5.求余函数fmod(a,b)


零零散散的知识

两变量对调

法一:引入临时变量(最安全最通用)
a=temp;
a=b;
b=temp;
法二:算术加减(仅限数值,有溢出风险)
a=a+b;
b=a-b;
a=a-b;
其他方法还有:位运算异或法、指针/引用交换(在函数中操作)等,此处不表

字典序

就是ASCⅡ的顺序,比如’A’=65 < ‘a’=87 < ‘b’=88
strcmp的时候就会比较两个数组的字典序
比如str1 = “zebra” str2 = “apple”
strcmp(str1,str2)就会返回正整数

持续处理一串数据的方法

(目前只会方法1和5,剩下的以后再学)
1.while (scanf(…) == 1)(最常用)
2.读一整行 + strtok 切割法(最强大)
3.纯手动读取 getchar() 逐字符解析(最底层、控制力最强)
4.按指针步进法(适合用 fgets 读入后,手动找空格)
5.数组存起来慢慢处理(适合需要排序、求最大/最小值等后续操作)

前置递减与后置递减

常用的是后置递减如x–,也有前置递减
规则:x–先使用,后-1,x–先-1,后使用

写法 名称 规则 例子(设 x=5)
++x 前置自增 先加 1,再使用 y = ++x; → x=6, y=6
x++ 后置自增 先使用,再加 1 y = x++; → x=6, y=5
–x 前置递减 先减 1,再使用 y = --x; → x=4, y=4
x-- 后置递减 先使用,再减 1 y = x–; → x=4, y=5

坑:while中的后置递减

intx=3;while(x--){printf("%d ",x);}// 输出:2 1 0

判断 x–:先用 3 判断(真),x 变成 2。进入循环打印 2。
判断 x–:先用 2 判断(真),x 变成 1。进入循环打印 1。
判断 x–:先用 1 判断(真),x 变成 0。进入循环打印 0。
判断 x–:先用 0 判断(假),x 变成 -1。退出循环。
所以最后 x = -1,输出 2 1 0。


一些好用的算法

1.寻找最大公因数:辗转相除法
方法一:while循环

inta=48,b=18;inttemp;while(b!=0){temp=a%b;// 算余数a=b;// 把除数变成被除数b=temp;// 把余数变成除数}printf("最大公约数是: %d\n",a);// 输出 6

方法二:递归

intgcd(inta,intb){returnb==0?a:gcd(b,a%b);}

2.减少乘法的数量:秦九韶算法

inta[]={3,2,5,1};// 系数,从最高次项到常数项intn=4;// 系数个数intx=2;// 要求值的 xintresult=0;// 核心逻辑:每次拿上一次的结果乘以 x,再加上下一个系数for(inti=0;i<n;i++){result=result*x+a[i];}printf("结果是: %d\n",result);// 输出 43

两个程序题

1.本题要求编写程序,对输入的一个非负整数,从低位开始逐位分割并输出它的各位数字。

输入格式:
输入在一行中给出一个长整型范围内的非负整数。

输出格式:
从低位开始逐位输出该整数的各位数字,每个数字后面有一个空格。

错误答案:

#include<stdio.h>intmain(){intinput,x;scanf("%d",&input);while(input){x=input%10;input=input/10;printf("%d ",x);}return0;}

正确答案:
添加了if(input==0)printf("0 "); 以处理0的情况(原文:非负整数)

#include<stdio.h>intmain(){intinput,x;scanf("%d",&input);if(input==0)printf("0 ");while(input){x=input%10;input=input/10;printf("%d ",x);}return0;}

2.本题目要求一元二次方程ax^2+bx+c=0的根,结果保留2位小数。

(注意:0.00会在gcc下被输出为-0.00,需要做特殊处理,输出正确的0.00。)

输入格式:
输入在一行中给出3个浮点系数a、b、c,中间用空格分开。

输出格式:
根据系数情况,输出不同结果:
1)如果方程有两个不相等的实数根,则每行输出一个根,先大后小;
2)如果方程有两个不相等复数根,则每行按照格式“实部+虚部i”输出一个根,先输出虚部为正的,后输出虚部为负的;
3)如果方程只有一个根,则直接输出此根;
4)如果系数都为0,则输出"Zero Equation";
5)如果a和b为0,c不为0,则输出"Not An Equation"。

初版程序:

#include<stdio.h>#include<math.h>intmain(){doublea,b,c,delta,x1,x2,R,I,x;scanf("%lf%lf%lf",&a,&b,&c);delta=b*b-4*a*c;if(!a&&!b&&!c)printf("Zero Equation");elseif(!a&&!b&&c)printf("Not An Equation");elseif(a==0){printf("%.2f",-c/b);}elseif(delta==0){printf("%.2f",-b*0.5/a);}elseif(delta>0){x1=(-1*b+sqrt(delta))/(a*2);x2=(-1*b-sqrt(delta))/(a*2);if(x1>x2)printf("%.2f\n%.2f",x1,x2);elseprintf("%.2f\n%.2f",x2,x1);}else{delta=-delta;R=-b*0.5/a;I=sqrt(delta)*0.5/a;printf("%.2f+%.2fi\n%.2f-%.2fi",R,I,R,I);}return0;}

问题:会遇到返回-0.00的情况

第二版:在最前面添加了一个新函数change()

voidchange(double*x){if(*x==-0.000000)*x=-*x;}

再对上面所有输出在输出之前使用change()
这里用到了指针,可以直接在被调函数里面修改主调函数的变量值

第三版:浮点数相等判断的优化

deepseek:浮点数不能用 == 去直接比较。如果算出来的值是 -0.000000000000001,它和 -0.000000
是不相等的,你的函数就不会生效

替换程序

voidchange(double*x){if(fabs(*x)<1e-6){// 如果绝对值小于一个极小数,视为0*x=0.0;}}

同样地,if(delta==0)也可以进行优化
写为if (fabs(delta) < 1e-6)
且注意到最后面没有判断I的正负性,需要加一行I=fabs(I);

补充:处理-0.00的方法

上面这个直接设一个函数,属于是初学者没有办法的办法,经过搜索,发现还有其他更优美的方法
1.加上0.0
在 IEEE 754 浮点数标准中,-0.0 + 0.0 = +0.0
eg.printf(“%.2f”, R + 0.0);
2.利用 math.h 的防抖动(加 EPS)
#define EPS 1e-9
// …
printf(“%.2f”, R + EPS);
(目前我还看不懂 #define这一行…)
3.打印前用三元运算符
printf(“%.2f”, fabs® < 1e-6 ? 0.0 : R);
(这个也看不懂…)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 2:31:25

开源项目停更≠死掉:判断维护状态与应对冻结代码的实用策略

如果你最近在Gitee上找开源组件&#xff0c;大概率会遇到一种情况&#xff1a;翻看一个仓库的提交记录&#xff0c;最后一次commit写的是2023年6月&#xff0c;然后又翻了几个仓库&#xff0c;情况惊人地相似。这不是某个项目的特例&#xff0c;而是一整片区域内的项目都停在了…

作者头像 李华
网站建设 2026/10/21 2:31:06

Claude Code Skills 放对目录却不生效:插件启用顺序与缓存的三个坑

本文摘要&#xff1a;Skill 放对目录仍不生效是静默失败&#xff0c;常被误判为模型问题。本文复现目录冲突、frontmatter 缺失与 instincts 抢占三种失败&#xff0c;用日志确认加载。 一、问题与结论 从 mattpocock/skills 复制一个 Skill 到项目根目录的 .agents/skills/&a…

作者头像 李华
网站建设 2026/10/21 2:30:36

【LLM技术全景】第41篇 边缘设备部署:手机与IoT设备上的LLM

系列:《LLM技术全景:从Token到部署》工程实践篇第9篇 承接:第40篇《本地部署实战(二):GPU部署与多卡推理》| 预告:第42篇《多模态模型部署:视觉语言模型的服务化》 摘要 核心问题:第 39、40 篇我们把模型装进了 PC/服务器的 GPU——但现实里更多推理发生在"口袋…

作者头像 李华
网站建设 2026/10/21 2:30:23

【虎鲸进化论 34】它还在进化:进行时与可迁移 checklist

【虎鲸进化论 34】它还在进化&#xff1a;进行时与可迁移 checklist 本文是「虎鲸进化论」系列第 35 篇&#xff08;正文第 34 篇&#xff09;&#xff0c;也是卷Ⅲ「自进化实录」的最后一篇。上一篇&#xff1a;Token 经济学&#xff1b;下一篇预告&#xff1a;产能几百倍&…

作者头像 李华
网站建设 2026/10/21 2:30:09

给Agent接代码沙箱:六个容易踩的坑

给Agent接代码沙箱&#xff1a;六个容易踩的坑原文&#xff1a;GitHub - 《SPACE&#xff1a;给智能体跑代码的沙箱平台&#xff08;仓库与架构文档&#xff09;》&#xff08;https://github.com/perplexityai/space&#xff09;前言 让 Agent 写代码不难&#xff0c;难的是让…

作者头像 李华