❤️博客主页: 小镇敲码人
欢迎关注:点赞 留言 收藏
任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。
❤️ 什么?你问我答案,少年你看,下一个十年又来了
数学中我们也了解过函数的概念,但是C语言中的函数你真的了解吗?
- 美国人将函数称之为function,意思是功能的意思,到了我们中国将其译为函数,是因为C语言里面的函数与数学里面的函数有相似之处。
- lenth = strlen(str)
- y = f ( x ) y =f(x) y=f(x)
- 看看他们是如此的相似,都是通过一份数据得到另一份数据,不过,严格来讲讲Function理解为功能似乎更加恰当,因为一个C语言函数所实现的功能常常是独立的,一个C语言程序由多个函数组成,可以理解为一个程序由多个小功能叠加。
- C语言函数一般有函数名、返回值类型、函数参数。
----以上内容借鉴于C语言中文网
库函数就是C语言基础库里面提供的函数,但是C语言只规定了该函数的参数、返回值、还有基本功能,具体的底层实现不同的编译器可能会不同。
那C语言为什么会有库函数的存在呢?
printf
)、字符串拷贝功能(strcpy
)等等。这种基础功能不是业务性的代码。我们在开发的过程中每个程序员都会使用到,为了提高可移植性和程序的效率,所以C语言的基础库提供了一系列类似的库函数,方便程序员进行软件开发。那我们在学习C语言的过程中该如何学习库函数呢?我们可以借助下面的文档网站进行学习库函数cplusplus.com.
C语言常见的库函数有:
我们通过使用上述文档,来演示一下如何通过文档学习库函数:
进入那个网站,我们需要点击旧版,因为只有旧版才有搜索的功能,如图:
也就是右上角位置,当你点击之后,该界面会变成这样:
我们如果搜索字符串打印函数strcpy
会出现这样的信息:
我们通过这个文档可以这样来学习函数strcpy
:
通过上面图片我们可以知道strcpy
它的返回类型是char*
类型,它有两个参数,第一个参数是一个char*
类型的指针,表示目的地字符串的起始地址,第
二个参数是const char*
类型的指针,表示源字符串的起始地址,且它不可修改,上面图片也是strcpy
函数的一个定义。
再看下面的内容:
通过以上信息,我们可以知道strcpy
函数的功能:进行字符串的拷贝,把源头字符串拷贝到目的地字符串数组中,包括终止字符\0
也要拷贝。同时为了避免溢出,目的地字符数组的大小应该大于等于源字符串的大小(包括终止字符\0
),它的内存和源字符串也应该不重叠。
当然你也可以使用edge浏览器的插件帮助你把英文翻译为中文,以此来帮助更好的去阅读文档,但它翻译出的信息可能会与本意有所偏差。
接着阅读信息:
上面图片主要介绍了strcpy
函数的两个参数,destination
是指向目的数组的地址,字符串将拷贝到这个字符数组中,source
代表被拷贝的源字符串。
上面的内容主要是介绍了这个函数的返回值,目的地字符数组的起始地址将被返回。
我们通过下面代码来演示一下如何使用库函数strcpy
:
#include
#include
int main()
{
char arr1[] = "hello bit";//源头
char arr2[] = "xxxxxxxxxxxxxxx";//目的地
//对于数组,数组名表示首元素地址,它的类型是char*
strcpy(arr2, arr1);//将字符串arr1的内容全部拷贝到arr2中,包括\0
printf("%s\n", arr2);//打印目的地字符数组
return 0;
}
运行结果为:
#include
对应的头文件。讲到这里大家可能就会有疑惑,既然库函数可以实现这么多功能,那还要程序员干什么呢?注意:库函数实现的功能一般都是较简单的,不足以满足所有的业务需求。
所以更加重要的是:自定义函数。
自定义函数和库函数一样,有函数名、返回值类型、函数参数。但是唯一不一样的地方是它需要我们自己去设计,而库函数给它传对应的参数就可以直接使用。
函数组成:
ret_type fun_name(paral,...)
{
statement;//语句项
}
ret_type:函数的返回类型
fun_name:函数名
para1:函数参数
我们举一个例子:
写一个函数可以求两个数的最小值。
#include
// get_min函数用于比较两个整数a和b的大小,返回较小的数
int get_min(int a, int b)
{
if (a > b) // 如果a大于b,则返回b,表示b是较小的数
return b;
else // 否则返回a,表示a是较小的数
return a;
}
int main()
{
int a = 0; // 声明并初始化整数变量a,用于存储用户输入的第一个数
int b = 0; // 声明并初始化整数变量b,用于存储用户输入的第二个数
scanf("%d%d", &a, &b); // 从用户输入读取两个整数,并分别存储到a和b中
printf("%d\n", get_min(a, b)); // 调用get_min函数比较a和b的大小,并输出较小的数
return 0; // 程序正常结束,返回0表示成功
}
我们可以给这个函数传一个初始化的局部变量和常量,或者传一个函数调用,这都行,前提是表达式通过计算必须是一个确定的值。
可以看到如果你使用一个未初始化的局部变量去比较时,编译器是会报错的,其它几种方式都没有什么问题。
我们再举一个例子:
写一个函数,可以交换两个变量的内容。
#include
// swap1函数用于交换两个整数a和b的值,但实际上并不起作用,因为形式参数x和y是值传递
void swap1(int x, int y)
{
int temp1 = x; // 声明临时变量temp1,并将x的值赋给temp1
x = y; // 将y的值赋给x,但由于x和y是形式参数,不会影响到main函数中的a和b
y = temp1; // 将temp1的值赋给y,同样也不会影响到main函数中的x和y
}
// swap2函数用于交换两个整数的值,通过指针传递来实现交换
1void swap2(int* px, int* py)
{
int temp2 = *px; // 声明临时变量temp2,并将px指针所指向的值赋给temp2
*px = *py; // 将px指针所指向的值赋给px指针所指向的位置,实现a和b的交换
*py = temp2; // 将temp2的值赋给pb指针所指向的位置,实现b和a的交换
}
int main()
{
int a = 3; // 声明并初始化整数变量a,初始值为3
int b = 4; // 声明并初始化整数变量b,初始值为4
printf("交换前:a=%d b=%d\n", a, b); // 输出交换前a和b的值8
swap1(a, b); // 调用swap1函数,但由于a和b是值传递,不会改变main函数中的a和b的值
printf("交换后:a=%d b=%d\n", a, b); // 输出交换后a和b的值,实际上并未交换
printf("交换前:a=%d b=%d\n", a, b); // 再次输出交换前a和b的值
swap2(&a, &b); // 调用swap2函数,通过传递a和b的地址来实现交换
printf("交换后:a=%d b=%d\n", a, b); // 输出交换后a和b的值,实际上已经交换成功
return 0; // 程序正常结束,返回0表示成功
}
运行结果:
可以看到swap1
函数是不能实现交换功能的,只有swap2
函数实现了我们的交换功能,这里涉及到传值调用和传址调用,我们后续会讲。
真实传给函数的参数叫做实际参数。
实参可以是:常量、变量、表达式、函数等。
无论实参是哪一种形式,它在函数传参时必须有确定的值,以便把这些值传给形参。
形式参数是指函数名括号里面的变量,因为形式参数只有在函数调用的时候才会被分配内存单元,所以叫形式参数。当函数调用结束之后,形式参数的内存单元就会被销毁了。除非是
static
关键字修饰的形式参数,生存周期更长。
上面代码的swap1
和swap2
中的函数里面的参数a
、b
、pa
、pb
都是形式参数(形参),而在main函数里面传给函数swap1
的a
,b
和传给swap2
的&a
,&b
是实际参数(实参)。
这里我们对函数的实参和形参进行分析:
代码对应的内存分配如下:
这里我们可以看到x
、y
有了自己的独立空间,所以我们可以简单的认为:此时的形参相当于实参的一份临时拷贝。
形式参数和实际参数分别占有不同的内存块,改变形参并不会影响实参,因为实参相当于实参的一份临时拷贝。
- 传址调用是将函数外部创建的变量的内存地址给函数参数的一种调用方式。
- 这种方式可以让函数和函数外面的变量建立起直接的联系,也就是函数内部通过对地址解引用操作可以直接改变函数外面变量的值。
#include
//下面函数将实现判断一个数是否是素数的功能。
int is_prime(int n){
if (n == 1)
return 0;
else{
int i = 2;
for (i = 2; i * i <= n; i++){
if (n % i == 0)
return 0;
}
return 1;
}
}
int main()
{
int i = 0;
int count = 0;
for (i = 100; i <= 200; i++){
//判断i是否为素数
if (is_prime(i)){
printf("%d ", i);
count++;
}
}
printf("\n%d", count);
return 0;
}
运行结果:
include<stdio.h>
//写一个函数判断一个年份是否为闰年
//如果是闰年返回1
//如果不是返回0
//2. 实现函数
int is_leap_year(int year)
{
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
int main()
{
int year = 0;
int count = 0;
for (year = 2000; year <= 3000; year++)
{
//1. 函数怎样使用
//TDD
// test driven development
//测试驱动开发
if (is_leap_year(year))
{
printf("%d ", year);
count++;
}
}
printf("\n%d", count);
return 0;
}
运行结果:
#include
// binary_search函数用于在有序整数数组arr中查找元素k,并返回其下标
// 参数arr为有序整数数组的首地址,k为要查找的元素,sz为数组的大小
// 如果找到了元素k,则返回其在数组中的下标,否则返回-1表示没找到
int binary_search(int arr[], int k, int sz)
{
int left = 0; // 定义左边界left,初始值为数组的第一个元素下标0
int right = sz - 1; // 定义右边界right,初始值为数组的最后一个元素下标sz-1
int mid = 0; // 定义中间位置mid
// 使用循环进行二分查找
while (left <= right)
{
// 计算中间位置mid,避免整数溢出使用(right-left)/2的方式
mid = left + (right - left) / 2;
if (arr[mid] < k) // 若中间元素小于要查找的元素k
{
left = mid + 1; // 缩小查找范围,将左边界left更新为mid+1
}
else if (arr[mid] > k) // 若中间元素大于要查找的元素k
{
right = mid - 1; // 缩小查找范围,将右边界right更新为mid-1
}
else // 若中间元素等于要查找的元素k,表示找到了
{
return mid; // 返回中间位置mid,表示找到了元素k的下标
}
}
// 若循环结束仍未找到元素k,则返回-1表示没找到
return -1;
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 声明并初始化有序整数数组arr
int k = 0; // 声明整数变量k,用于接收用户输入的要查找的元素
scanf("%d", &k); // 输入要查找的元素k
int sz = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小
// 调用binary_search函数在数组arr中查找元素k
int ret = binary_search(arr, k, sz);
// 判断查找结果并输出相应信息
if (ret == -1)
{
printf("没找到\n"); // 若返回值为-1表示没找到,输出"没找到"
}
else
{
printf("找到了下标是:%d", ret); // 若返回值不为-1表示找到了,输出找到的元素k的下标
}
return 0; // 程序正常结束,返回0表示成功
}
我们使用传值调用和传址调用两种方式都可以解决这个问题。
#include
void test(int* p)
{
(*p)++;
}
int main()
{
int num = 0;
test(&num);
test(&num);
printf("%d\n", num);
return 0;
}
#include
int test(int num)
{
return num + 1;
}
int main()
{
int num = 0;
num = test(num);
num = test(num);
printf("%d\n", num);
return 0;
}
嵌套调用就是指一个函数体内可以调用其它的函数。
下面我们通过一段代码来演示一下函数的嵌套调用
#include
int test()
{
int a = 0;
int b = 0;
return a + b;
}
void fun()
{
test();
printf("hehe\n");
}
int main()
{
fun();
return 0;
}
运行结果:
链式访问就是把一个函数的返回值作为另一个函数的参数。
下面我们通过几段代码来演示函数的链式访问,这段代码将使用字符串函数,如果你不太熟悉,请自己使用文档学习,或者看博主的这篇博文【C语言进阶技巧】探秘字符与字符串函数的奇妙世界。
#include
#include
int main()
{
int len = strlen("abcdef"); // 使用strlen函数获取字符串"abcdef"的长度,即字符个数(不包括空字符'\0')
printf("%d\n", len); // 打印获取到的字符串长度,输出为6(字符串"abcdef"有6个字符)
// 函数调用:使用strlen函数直接获取字符串"abcdef"的长度,并打印结果
printf("%d\n", strlen("abcdef")); // 直接打印字符串"abcdef"的长度,输出也为6
return 0;
}
运行截图:
这段代码中临时变量len
接收了strlen
函数的int
类型的返回值,然后其作为printf
函数的参数,这就是链式访问,或者不用临时变量接收,直接放入printf
中,结果是一样的效果。
再看下面一段代码:
#include
int main()
{
printf("%d ", printf("%d ", printf("%d ", 43)));
return 0;
}
想知道这段代码的打印结果,我们就得清楚printf
函数的返回值是什么,表示的是什么含义,这里我们查询文档,给出如下图片:
所以第三个printf
会打印三个字符:4
、3
和空格,返回值为3,然后第二个printf
函数以第三个printf
函数的返回值3作为参数,打印两个字符:3
和空格,返回值为2,接着第一个printf
函数以第二个printf
函数的返回值2为参数,打印两个字符:2
和空格,返回值为2,所以最后屏幕上打印的应该是43 3 2
。
运行结果:
- 函数的声明所实现的功能是告诉编译器一个函数的参数、返回值类型、名称。但函数的声明只是一个声明,它不能决定函数是否存在,也就是说,如果这个函数只存在声明,而没有定义在语法也是通过的。
- 函数的声明一般放在函数的使用之前,要满足先声明再使用。
- 函数的声明应该放在头文件中。
函数的定义是函数声明的一种特殊形式,它是指函数的具体实现,交代函数的功能实现。
下面我们通过具体的代码来介绍函数的声明与定义:
#include
//函数声明
//函数声明中函数只需要包含函数参数的类型就可以了,但是加上变量名也可以
int Add(int, int);
//int Add(int a, int b);
int main()
{
int a = 0;
int b = 0;
//输入两个相加的数
scanf("%d%d", &a, &b);
//函数调用
int c = Add(a, b);
//打印
printf("%d", c);
}
//函数定义
int Add(int a, int b)
{
return a + b;
}
我们也可以省略函数声明,直接在main
函数的前面写Add
函数的定义,因为定义是一种特殊的声明,在实现函数功能的同时它也包括了函数的声明:
#include
//函数定义是一种特殊的声明
int Add(int a, int b)
{
return a + b;
}
int main()
{
int a = 0;
int b = 0;
//输入两个相加的数
scanf("%d%d", &a, &b);
//函数调用
int c = Add(a, b);
//打印
printf("%d", c);
}
实际项目开发中我们一般会这样去进行Add
函数的声明和定义:
Add
函数的声明放在头文件Add.h
中Add
函数的定义放在Add.c
文件中Add.h
和包含函数定义的文件Add.c
在同一个项目(解决方案)中时我们想要使用Add
函数只需要包含一下头文件就行了,但因为这是我们自己创建的头文件,为了区分,用""
包含头文件:#include "Add.h"
运行结果:
下面一张图,希望能帮助你理解:
2. 将代码的实现与声明分离
还是以
Add
函数举例子,此时将Add.h
和Add.c
分开来写,如果程序员K想把这个项目卖了来获取收益,但是它不想让别人知道他是怎么写的,这个时候就可以把Add.c
文件转换为Add.lib
文件,这样就可以达到既让客户使用又不让客户知道代码是如何实现的目的,因为静态库是一种预先编译好的代码集合,我们是看不懂的。
下面我们来演示一下将Add.c
转换为静态库文件并使用它:
3. 新建一个解决方案,并创建新的Add.h
和Add.c
。
5. 鼠标右击Add.c
,打开所在文件夹,并进入Debug(调试)文件中,可以看见Add.lib
文件。
6. 我们打开我们的test.c
文件,并把刚刚生成的Add.lib
和我们写好的代码声明Add.h
放进这个项目的文件夹中。
#pragma comment (lib,"Add.lib")
程序调用自身的编程技巧叫做递归。
递归作为一种算法在程序设计语言中广泛的被应用。递归这种算法可以把一个复杂的问题层层转化为很多和原问题相似的子问题来求解,这种策略只需少量的程序就可描述出解题过程中所需要的多次重复计算,大大减少了代码量。
最简单的递归:
#include
int main()
{
printf("hehe");
main();
return 0;
}
但是这个递归没有限制条件,会导致栈溢出。
接受一个整形值(无符号),按照顺序打印它的每一位。
例如:
输入:1234
,输出1 2 3 4
。
参考代码:
#include
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%u ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);
print(num);
return 0;
}
运行结果:
编写函数,不允许使用临时变量,求字符串长度。
这里我们使用递归来实现,但是如果你对更多方法感兴趣,可以看博主这篇文章【C语言进阶技巧】探秘字符与字符串函数的奇妙世界。
递归实现,参考代码:
#include
// 自定义函数 my_strlen,用于计算字符串的长度(不包括结尾的 '\0')
// 参数 str:指向待计算长度的字符串的指针
// 返回值:字符串的长度,以 size_t 类型表示
size_t my_strlen(char* str)
{
// 判断字符串是否为空,即是否为 '\0' 结尾
if (*str == '\0')
return 0; // 若为空,返回长度 0
else
// 若不为空,递归调用 my_strlen 函数,计算剩余子串的长度,并加 1(包含当前字符)
return 1 + my_strlen(str + 1);
}
int main()
{
char arr[] = "abc"; // 定义一个字符数组 arr,存储字符串 "abc"(包含结尾的 '\0')
// 计算字符串的长度(不包括结尾的 '\0'),调用自定义的 my_strlen 函数
size_t len = my_strlen(arr);
printf("%d\n", len); // 输出字符串的长度,注意这里使用 %zu 作为格式化字符串,表示 size_t 类型
return 0;
}
递归和迭代有着极其高的相似性,可以这样来理解递归:
递:迭代
归:回归一般情况下:递归比迭代在性能上要差,但代码量比迭代要少,且如果一个程序你可以用迭代来实现,就相应的也可以使用递归来实现。
求n的阶乘。(不考虑溢出)
参考代码:
#include
// 自定义函数 factorial,用于计算 n 的阶乘
// 参数 n:需要计算阶乘的整数
// 返回值:n 的阶乘,以 int 类型表示
int factorial(int n)
{
// 判断 n 是否为 1,如果是,则返回 1(1 的阶乘为 1)
if (n == 1)
return 1;
else
// 若 n 不是 1,则递归调用 factorial 函数,计算 n-1 的阶乘,并与 n 相乘
return n * factorial(n - 1);
}
int main()
{
int n = 0; // 定义整数变量 n,用于存储用户输入的值
scanf("%d", &n); // 从标准输入读取用户输入的整数,存入 n 中
// 调用自定义的 factorial 函数,计算 n 的阶乘,并输出结果
printf("%d\n", factorial(n));
return 0;
}
分析:
运行结果:
求第n个斐波拉契数,不考虑溢出。
参考代码:
#include
// 定义递归函数Fib,用于计算斐波那契数列的第n项
// 参数n为要计算的斐波那契数列的项数
int Fib(int n)
{
// 当n小于等于2时,斐波那契数列的第n项为1,直接返回1
if (n <= 2)
{
return 1;
}
else
{
// 当n大于2时,斐波那契数列的第n项为前两项之和
// 调用Fib(n-1)和Fib(n-2)分别计算第n-1项和第n-2项
// 然后将它们相加并返回
return Fib(n - 1) + Fib(n - 2);
}
}
int main()
{
int n = 0; // 声明整数变量n,用于接收用户输入的斐波那契数列项数
scanf("%d", &n); // 输入要计算的斐波那契数列项数n
// 调用Fib函数计算斐波那契数列的第n项并输出结果
printf("%d", Fib(n));
return 0; // 程序正常结束,返回0表示成功
}
我们可以发现上面两道题目,如果我们使用递归的思路来实现的话:
这是为什么呢?
我们可以对Fib函数做一下修改,计算一下Fib(3)被调用了多少次,假设此时 n = 15 。 n=15。 n=15。
#include
//定义全局变量count来计数,F(3)重复计算的次数
int count = 0;
// 定义递归函数Fib,用于计算斐波那契数列的第n项
// 参数n为要计算的斐波那契数列的项数
int Fib(int n)
{
// 当n小于等于2时,斐波那契数列的第n项为1,直接返回1
if (n <= 2)
{
return 1;
}
if (n == 3)
{
count++;
}
else
{
// 当n大于2时,斐波那契数列的第n项为前两项之和
// 调用Fib(n-1)和Fib(n-2)分别计算第n-1项和第n-2项
// 然后将它们相加并返回
return Fib(n - 1) + Fib(n - 2);
}
}
int main()
{
int n = 0; // 声明整数变量n,用于接收用户输入的斐波那契数列项数
scanf("%d", &n); // 输入要计算的斐波那契数列项数n
// 调用Fib函数计算斐波那契数列的第n项并输出结果
printf("%d", Fib(n));
printf("\n%d", count);
return 0; // 程序正常结束,返回0表示成功
}
运行结果:
可以看到虽然 n = 50 n = 50 n=50时的斐波拉契额数太大导致数据溢出,但是Fib(3)被调用的次数被成功计算出来了,为一个9位数,所以我们也可以知道,之所以计算第50个斐波拉契数这么慢,是因为有很多值是被重复计算了的。
另外我们可以对参数很大时候的factorial
函数进行调试,程序会出现这样的问题:
这是因为n
太大,所以函数栈帧开辟的太多,导致空间不够,造成了栈溢出(Stack overflow)的情况。因为系统分配给程序的栈空间是有限的,如果出现了死循环,或者死递归,这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称之为栈溢出。
那我们该如何解决这种问题呢?
static
对象代替局部变量(即栈对象)。比如下面两个函数就采用了非递归的方法来实现求n
的阶乘和求第n
个斐波拉契数:
#include
// 求n的阶乘
// 参数n为要计算阶乘的数
// 返回值为n的阶乘结果
int factorial(int n)
{
int i = 0;
int ret = 1;
// 使用for循环计算n的阶乘
for (i = 1; i <= n; i++)
{
ret *= i;
}
return ret;
}
// 求第n个斐波拉契数
// 参数n为要计算的斐波拉契数列的项数
// 返回值为第n个斐波拉契数
int Fib(int n)
{
int a = 1; // 斐波拉契数列的第一项为1
int b = 1; // 斐波拉契数列的第二项为1
int c = 1; // 用于保存第n个斐波拉契数
// 使用for循环计算第n个斐波拉契数
for (int i = 3; i <= n; i++)
{
c = a + b; // 计算第i项的值,即前两项之和
a = b; // 更新第i-2项为第i-1项的值
b = c; // 更新第i-1项为第i项的值
}
return c;
}
int main()
{
int n = 0;
printf("请输入一个整数n:");
scanf("%d", &n);
// 调用factorial函数计算n的阶乘,并输出结果
printf("%d的阶乘结果为:%d\n", n, factorial(n));
// 调用Fib函数计算第n个斐波拉契数,并输出结果
printf("第%d个斐波拉契数为:%d\n", n, Fib(n));
return 0;
}
运行结果: