目录
1.函数基本用法
1.1 定义和三要素
函数是一个完成特定功能的代码模块,其程序代码独立,通常要求有返回值,也可以是空值。
函数三要素: 功能、参数和返回值
功能:函数要实现的功能
参数:参数就是在函数声明时和函数调用时定义的变量。它用于传递信息给函数。
参数分为实参和形式参数:
【1】实参是在函数调用中实际传递给函数的参数;而形式参数是函数声明中接受实参的参数。实参是有具体的数据。
【2】形参只是一个声明的形式,可以理解为一个符号名字。
返回值:函数返回值是函数调用后唯一留下的右值。(右值:只能放在运算符右边)
1.2 函数的声明和定义
1.2.1 函数声明
存储类型 数据类型 函数名(数据类型 形参1, 数据类型 形参2,...);
1.2.2 函数定义格式
存储类型 数据类型 函数名(数据类型 形参1, 数据类型 形参2,...)
{
函数体;
}
其中:
函数名称:是一个标识符,要求符合标识符的名命规则。
数据类型: 是整个函数返回值类型,如果没有返回值应该写为void。
形式参数说明:是逗号分隔的多个变量的说明形式,通常简称为形参。
形参:形式参数就是声明函数时指函数名后括号中的变量,因为形式参数只有在函数调用的过程中才实例化(分配内存单元),所以就叫形式参数。
大括弧对 {语句序列 },称为函数体,语句序列是大于等于零个语句构成的。
注意:在函数体中,表达式语句里使用的变量必须事先已有说明,否则不能使用。
return后面加表达式,语句中表达式的值,要和函数的<数据类型>保持一致。
总结函数的数据:
- 没有参数:参数列表可以省略,也可以写成void。
- 没有返回值:数据类型为void, 函数内部没有return语句。
- 有返回值:要根据返回值的数据类型定义函数的数据类型
- 定义子函数时可以直接定义在主函数上面,如果定义在主函数下面需要提前声明函数。
类型为void可以省略或者无表达式结果返回。(即写成return;)
1.3 函数调用
- 没有返回值: 直接调用: 函数名(实参);
- 有返回值: 如果需要接收返回值,就要在函数内定义一个和返回值类型相同的变量用return 返回出去,如果不需要接收返回值,直接调用就可以了
实参:在调用有参函数时,函数名后面括号中的参数称为“实参”,是我们真实传给函数的参数,实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
注意:如果定义在main函数下面,要先在main函数上面声明这个函数。
1.4 函数传参
(1)值传递
单向传递,将实参这个数据传递给形参使用,函数内改变形参函数外的实参不会受影响,因为他们是属于不同的空间。
#include <stdio.h>
void fun(int a, int b) //a=10 b=30
{
a++; //a=11
b++; //b=31
printf("in fun: %d %d\n", a, b); //11 31
}
int main(int argc, char const *argv[])
{
int n1 = 10, n2 = 30;
fun(n1, n2);
printf("in main: %d %d\n", n1, n2); //10 30
return 0;
}
(2)地址传递
双向传递,在函数内通过传递的地址修改地址所指空间中的内容,实参会随之变化。
#include <stdio.h>
void fun(int *a, int *b) //a=&n1 b=&n2
{
(*a)++; //*a =*&n1 = n1
(*b)++; //*b = *&n2 = n2
printf("in fun: %d %d\n", *a, *b); //11 31
}
int main(int argc, char const *argv[])
{
int n1 = 10, n2 = 30;
fun(&n1, &n2); //n1=11 n2=31
printf("in main: %d %d\n", n1, n2); //11 31
return 0;
}
(3)数组传递
和地址传递一样,参数中存在地址,也就是指针。
#include <stdio.h>
void fun(int *p, int n) //p=a
{
p[2] = 100; //a[2] =100
printf("fun: ");
for (int i = 0; i < n; i++)
printf("%d ", p[i]); //或者用*(p+i)
printf("\n");
}
int main(int argc, char const *argv[])
{
int a[5] = {1, 2, 3, 4, 5};
fun(a, 5);
printf("main: ");
for (int i = 0; i < 5; i++)
printf("%d ", a[i]); //1 2 100 4 5
printf("\n");
return 0;
}
1.5 函数和栈区(stack)
1.5.1 栈区的概念
栈用来存储函数内部的变量(包括main()函数)。它是一个FILO(First In Last Out,先进后出)的结构。每当一个函数声明一个新的变量它将被压入栈中。当一个函数运行结束后,这个函数所有在栈中相关的变量都将被删除,而且它们所占用的内存将会被释放。这就产生了函数内部的局部变量。栈区是一段非常特殊的内存区,它由CPU自动管理,所以你不必手动申请和释放内存。
内存由系统自动申请,在变量生命周期结束时由系统释放,也就是说,在程序运行的时候,系统有多个任务,就是检测变量是否该释放了,简单来说,就是cpu要抽时间去执行这部分功能。所以,如果这种变量比较多,不加节制的定义的话,那CPU的额外的工作量就会加大,综合下来,程序的运行效率就会低下。
1.5.2 栈区(stack)的总结
- 栈由CPU管理,无法修改
- 变量自动地分配和释放
- 栈并非没有限制,大部分栈都有一个上边界
- 栈随着变量地产生和销毁生长和收缩
- 栈区变量只有在函数创建它们地时候存在
2.开辟堆区(heap)空间
2.1 堆区的概念
申请的空间种分为五个区域栈区(堆栈区),堆区,全局区,常量区,代码区,我们之前讲的这些定义变量、数组都是在内存的栈区存储。
堆区的特点:由我们程序员随时申请,由我们自己随时释放。
2.2 栈区的特点
堆区的特点:由我们程序员随时申请,由我们自己随时释放。
堆和栈的主要区别有:
1.栈由系统自动分配,而堆是人为申请开辟;
2.栈获得的空间较小,而堆获得的空间较大;
3.栈由系统自动分配,速度较快,而堆一般速度比较慢;
4.栈是连续的空间,而堆是不连续的空间是随机分配的。
3.malloc 函数
3.1 定义
用man手册查看:
#include <stdlib.h>
void *malloc(size_t size);
功能:在堆区开辟大小为size的空间
参数:size:开辟空间的大小(单位:字节)
返回值:
成功:返回开辟空间的首地址
失败:NULL;
malloc()要和free()搭配使用
size_t大小:printf("%d\n",sizeof(size_t)); //4字节
3.2 用法
malloc内的参数是需要动态分配的字节数,而不是可以存储的元素个数!
当动态分配内存时,存储的是字符型数据,每个元素1字节,所以字节数刚好等于需要存储的元素个数(字符数+1);
如果存储的是整型或浮点型数据,字节数等于需要存储的元素个数 * 一个元素的字节数
代码格式:
数据类型 *指针名 = (数据类型 *)malloc(n*sizeof(数据类型));
3.3 free()函数定义
#include <stdlib.h>
void free(void *ptr);
功能:释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。
参数:ptr:堆区空间首地址
返回值:无
可以释放完以后赋值为空指针:
free(p); //p成为野指针
p=NULL; //防止使用野指针
注意:
1.手动开辟堆区空间,要注意内存泄漏
当指针指向开辟堆区空间后,又对指针重新赋值,则没有指针指向开辟的堆区空间,就会造成内存泄漏。
2.使用完堆区空间后及时释放空间
3.4 函数中开辟堆区空间
报段错,原因: 函数调用结束后相当于销毁申请的栈区空间了,不会保留函数内部的变量,所以函数调用结束后p就没了。开始通过传参将main函数的q的值也就是NULL传递给p也就是p也指向NULL, 函数内改变了p的指向指向了堆区,但是不会影响到函数外的q。因为q和p是两个空间。所以函数调用结束后,q没改变还是指向NULL。打印空指针会报段错误。
解决方法1: 通过返回值
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *fun()
{
char *p = (char *)malloc(32);
strcpy(p, "hello");
return p;
}
int main(int argc, char const *argv[])
{
char *q = fun(); //q接收了函数的返回值也就是p保存的堆区地址
printf("%s\n", q);
free(q);
return 0;
}
解决方法2: 通过传参, 传递二级指针
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void fun(char **p) //p=&q
{
*p = (char *)malloc(32); //*p = *&q =q
strcpy(*p, "hello");
}
int main(int argc, char const *argv[])
{
char *q = NULL;
fun(&q); //&q类型是char **
printf("%s\n", q);
free(q);
return 0;
}
解释: 通过传递二级指针把&q也就是q的地址传递给了fun函数被p接收,也就是p=&q了,也就是p指向了q。通过*p可以访问到q,给*p重新赋值相当于给q重新赋值了,所以函数内将*p(也就是q)指向了堆区,函数调用结束后函数内的p销毁了,但是q已经指向了堆区。
4.string函数族
4.1 strcpy
#include <string.h>
char *strcpy(char *dest, const char *src);
功能:把整个字符串复制,包括\0。
参数: char *dest :目标字符串首地址
const char *src:源字符串首地址
返回值:目标字符串首地址
4.2 strcat
#include <string.h>
char *strcat(char *dest, const char *src);
功能:用于字符串拼接
参数:
char *dest:目标字符串首地址
const char *src: 源字符串首地址
返回值:目标字符串首地址
4.3 strlen
#include <string.h>
size_t strlen(const char *s);
功能:计算字符串长度
参数:s:字符串的首地址
返回值:返回字符串实际长度,不包括‘\0’在内。
4.4 strcmp
#include <string.h>
int strcmp(const char *s1, const char *s2);
功能:用于字符串比较
参数:s1 s2用于比较多字符串
返回值:
从字符串首个字符开始比较字符ASCII的大小,如果相等继续向后判断。
正数: s1>s2
0 : s1== s2
负数: s1<s2
5.递归函数
5.1 概念
什么是递归函数?
所谓递归函数是指一个函数的函数体中直接调用或间接调用了该函数自身的函数。这里的直接调用是指一个函数的函数体中含有调用自身的语句,间接调用是指一个函数在函数体里有调用了其它函数,而其它函数又反过来调用了该函数的情况。
😀如何理解:
(1)从调用自身层面:函数递归就是函数自己调用自己。
(2)从编程技巧层面:一种方法(把一个大型复杂的程序转换为一个类似的小型简单的程序),这种方法的主要思想就是把大事化小,化繁为简。
5.2 执行过程
递归函数调用的执行过程分为两个阶段:
- 递推阶段:从原问题出发,按递归公式递推从未知到已知,最终达到递归终止条件。
就是从最里层开始算,然后一层一层算,直到终止。
- 回归阶段:按递归终止条件求出结果,逆向逐步代入递归公式,回归到原问题求解。
例子:求5的阶乘5! //1*2*3*4*5
递归公式:
1 n<=1
fac(n)=
n*fac(n-1) n>1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int fac(int n) //n=5 //n=4 //n=3 //n=2 //n=1
{
if (n <= 1)
return 1; //结束递归
if (n > 1)
return n * fac(n - 1); //5*fac(4) //5*4*fac(3) //5*4*3*fac(2) //5*4*3*2*fac(1) //5*4*3*2*1
}
int main()
{
int n = 5, r = 0;
r = fac(n);
printf("%d\n", r); //120
return 0;
}