C语言初阶--函数
目录
1. 函数是什么?
2. C语言中函数的分类
2.1 库函数
2.2 自定义函数
3. 函数的参数
3.1 实际参数(实参)
3.2 形式参数(形参)
4. 函数调用
4.1 传值调用
4.2 传址调用
练习:写一个函数判断一个数是不是素数
练习:写一个函数判断一年是不是闰年
练习:写一个函数,实现一个整型有序数组的二分查找
5. 函数的嵌套调用和链式访问
5.1 嵌套调用
5.2 链式访问
6. 函数的声明和定义
6.1 函数声明
6.2 函数的定义
7. 函数递归
7.1 什么是递归?
7.2 递归的两个必要条件
练习1
补充:函数栈帧的创建和销毁
练习2
7.3 递归与迭代
练习3
练习4
提示
总结
1. 函数是什么?
数学中我们常见到函数的概念。如f(x) = 2 * x + 1。
维基百科中对函数的定义:子程序。
-
在计算机科学中,子程序,是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
-
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2. C语言中函数的分类
-
库函数
-
自定义函数
2.1 库函数
为什么有库函数?
开发过程中每个程序员都可能用得到的一些基础功能。为了支持可移植性、提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员软件开发。
如何学习库函数?
https://cplusplus.com/reference/
简单的总结,C语言常用的库函数有:
-
IO函数
-
字符串操作函数
-
字符操作函数
-
内存操作函数
-
时间/日期函数
-
数学函数
-
其它库函数
1、先看文档学习语法(函数名、函数原型、详细介绍、形式参数、返回值、案例、输出、相关函数)
2、上手练习代码
案例strcpy():
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello bit";
strcpy(arr1, arr2);
printf("%s\n", arr1); //hello bit
return 0;
}
案例memset():
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello world";
memset(arr, 'x', 5);
printf("%s\n", arr); //xxxxx world
return 0;
}
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello world";
memset(arr+6, 'y', 3);
printf("%s\n", arr); //hello yyyld
return 0;
}
注意:使用库函数,必须包含 #include 对应的头文件。
学习库函数的工具?
-
MSDN(Microsoft Developer Network)
-
https://cplusplus.com/reference/
-
https://en.cppreference.com/w/(英文版)
-
https://zh.cppreference.com/w/(中文版)
2.2 自定义函数
自定义函数和库函数一样,有函数名、返回值类型和函数参数。
但是不一样的是,这些都由我们自己来设计,给了程序员很大的发挥空间。
函数的组成:
ret_type fun_name(para1, *)
{
statement; //语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数(可以有0个,1个或多个)
{}内 函数体/函数的实现
//求两个整数的最大值
#include <stdio.h>
//函数的定义
int get_max(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
//函数的调用
int m = get_max(a, b);
printf("%d\n", m);
return 0;
}
//写一个函数来交换整型变量的内容(下面代码存在问题,没有交换成功)
//当实参传递给形参时,形参是实参的一份临时拷贝。对形参的修改不会影响实参。
#include <stdio.h>
void Swap(int x, int y) //形式参数
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
//交换
Swap(a, b); //a和b叫实参
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
通过调试,没有交换的原因:
当实参传递给形参时,形参是实参的一份临时拷贝。对形参的修改不会影响实参。
引入修改值内容:
int main()
{
int a = 10;
int* p = &a;
a = 20; //直接修改
*p = 30; //间接修改
return 0;
}
再次修改存在问题的代码
#include <stdio.h>
void Swap(int* px, int* py)
{
int z = *px; //z=a
*px = *py; //a=b
*py = z; //b=z
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
//交换
//Swap(a, b);
Swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
报错如下:
LNK200_main 已经在test1.obj中定义
LNK116找到一个或多个重定义的符号
原因:
一个工程中,可以有多个.c文件,但是只能有一个main函数。
3. 函数的参数
3.1 实际参数(实参)
实参:真实传给函数的参数。
实参可以是常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
int c = Add(a + 3, b); //表达式
int c = Add(Add(a, 3), b); //函数
3.2 形式参数(形参)
形参:指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元)。形式参数当函数调用完成后就自动销毁了。因此形式参数只有在函数中有效。
形式参数和实际参数的名字可以相同,也可以不同。
4. 函数调用
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
4.2 传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
练习:写一个函数判断一个数是不是素数
未写函数:
//打印100-200之间的素数,素数是只能被1和它本身整除的数
//例如数字7,只能被1和7整除(2,3,4,5,6)
#include <stdio.h>
int main()
{
int i = 0;
int count = 0;
for (i = 100; i <= 200; i++)
{
//判断i是否为素数,是素数就打印(拿2~i-1之间的数字去尝试除以i)
int flag = 1; //假设:初始化变量flag=1,等于1时表示是素数
int j = 0;
for (j = 2; j <= i-1; j++)
{
if (i % j == 0)
{
flag = 0;
break;
}
}
if (flag == 1)
{
count++;
printf("%d ", i); //101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199
}
}
printf("\ncount = %d\n", count); //21
return 0;
}
优化上述代码
eg: m = a * b
eg: 16 = 2 * 8
= 4 * 4
a和b中一定有一个数字<= sqrt(m),sqrt是开平方的意思。
#include <stdio.h>
#include <math.h>
int main()
{
int i = 0;
int count = 0;
for (i = 101; i <= 200; i+=2) //偶数不可能是素数,所以优化一下,从奇数开始,每次+2,得到100-200的所有奇数
{
//判断i是否为素数,是素数就打印(拿2~i-1之间的数字去尝试除以i)
int flag = 1; //假设:初始化变量flag=1,等于1时表示是素数
int j = 0;
for (j = 2; j <= sqrt(i); j++) //sqrt是数学库函数(开平方的意思),引入头文件math.h
{
if (i % j == 0)
{
flag = 0;
break;
}
}
if (flag == 1)
{
count++;
printf("%d ", i); //101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199
}
}
printf("\ncount = %d\n", count); //21
return 0;
}
写函数:
//写一个函数判断一个数是不是素数
#include <stdio.h>
#include <math.h>
int is_prime(int n)
{
int j = 0;
for (j = 2; j <= sqrt(n); j++) //sqrt是数学库函数(开平方的意思),引入头文件math.h
{
if (n % j == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
int count = 0;
for (i = 101; i <= 200; i+=2) //偶数不可能是素数,所以优化一下,从奇数开始,每次+2,得到100-200的所有奇数
{
//判断i是否为素数,是素数就打印(拿2~i-1之间的数字去尝试除以i)
if (is_prime(i))
{
printf("%d ", i); //101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199
count++;
}
}
printf("\ncount = %d\n", count); //21
return 0;
}
练习:写一个函数判断一年是不是闰年
未写函数:
//打印1000-2000年之间的闰年,闰年的规则:1、能被4整除并且不能被100整除的是闰年。2、能被400整除的是闰年
#include <stdio.h>
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year++)
{
//判断year是不是闰年
if (year%4 == 0)
{
if (year%100 != 0)
{
printf("%d ", year);
}
}
if (year%400 == 0)
{
printf("%d ", year);
}
}
return 0;
}
//1004 1008 1012 1016 1020 1024 1028 1032 1036 1040 1044 1048 1052 1056 1060 1064 1068 1072 1076 1080 1084 1088 1092 1096 1104 1108 1112 1116 1120 1124 1128 1132 1136 1140 1144 1148 1152 1156 1160 1164 1168 1172 1176 1180 1184 1188 1192 1196 1200 1204 1208 1212 1216 1220 1224 1228 1232 1236 1240 1244 1248 1252 1256 1260 1264 1268 1272 1276 1280 1284 1288 1292 1296 1304 1308 1312 1316 1320 1324 1328 1332 1336 1340 1344 1348 1352 1356 1360 1364 1368 1372 1376 1380 1384 1388 1392 1396 1404 1408 1412 1416 1420 1424 1428 1432 1436 1440 1444 1448 1452 1456 1460 1464 1468 1472 1476 1480 1484 1488 1492 1496 1504 1508 1512 1516 1520 1524 1528 1532 1536 1540 1544 1548 1552 1556 1560 1564 1568 1572 1576 1580 1584 1588 1592 1596 1600 1604 1608 1612 1616 1620 1624 1628 1632 1636 1640 1644 1648 1652 1656 1660 1664 1668 1672 1676 1680 1684 1688 1692 1696 1704 1708 1712 1716 1720 1724 1728 1732 1736 1740 1744 1748 1752 1756 1760 1764 1768 1772 1776 1780 1784 1788 1792 1796 1804 1808 1812 1816 1820 1824 1828 1832 1836 1840 1844 1848 1852 1856 1860 1864 1868 1872 1876 1880 1884 1888 1892 1896 1904 1908 1912 1916 1920 1924 1928 1932 1936 1940 1944 1948 1952 1956 1960 1964 1968 1972 1976 1980 1984 1988 1992 1996 2000
优化
#include <stdio.h>
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year++)
{
//判断year是不是闰年
if (((year%4 == 0) && (year%100 != 0)) || (year%400 == 0))
{
printf("%d ", year);
}
}
return 0;
}
写函数:
//写一个函数判断一年是不是闰年
//是闰年返回1,非闰年返回0
#include <stdio.h>
int is_leap_year(int y)
{
if (((y%4 == 0) && (y%100 != 0)) || (y%400 == 0))
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year++)
{
//判断year是不是闰年
if (is_leap_year(year))
{
printf("%d ", year);
}
}
return 0;
}
练习:写一个函数,实现一个整型有序数组的二分查找
#include <stdio.h>
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left + (right-left)/2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid; //找到了返回下标
}
}
return -1; //找不到
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 7;
int sz = sizeof(arr) / sizeof(arr[0]);
//找到了,返回下标;找不到,返回-1
int ret = binary_search(arr, k, sz);
if (ret == -1)
{
printf("找不到");
}
else
{
printf("找到了,下标是:%d\n", ret); //找到了,下标是:6
}
return 0;
}
错误的示范:
数组传参实际上传递的是数组首元素的地址,而不是整个数组。所以在函数内部计算一个函数参数部分的数组元素个数是不靠谱的。
二分查找法不会的可以自行百度,或者后续有时间出个详解。
5. 函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合,也就是互相调用的。
5.1 嵌套调用
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
函数可以嵌套调用,但不能嵌套定义。
//不能嵌套定义
int Add(int x, int y)
{
return x + y;
int Sub(int x, int y) //error
{
return x - y;
}
}
int main()
{
return 0;
}
5.2 链式访问
把一个函数的返回值作为另一个函数的参数。
#include <stdio.h>
#include <string.h>
int main()
{
int len = strlen("abcdef");
printf("%d\n", len); //6
//链式访问
printf("%d\n", strlen("abcdef")); //6,将strlen()函数的返回值(size_t类型)作为参数传给printf()
return 0;
}
//经典案例
#include <stdio.h>
int main()
{
printf("%d, printf("%d", printf("%d", 43))"); //4321
return 0;
}
函数不写返回值的返回类型时,默认返回类型是int类型。
//不推荐,下面的代码在一些编译器上返回的是函数中执行过程中最后一条指令执行的结果,5
int Add(int x, int y)
{
printf("hehe\n");
}
函数定义时没有参数,就不用特意去传参。(传参,函数也不会调用)
//明确地说明,main函数不需要参数,本质上main函数是有参数的
int main(void)
{
return 0;
}
//main函数有3个参数
int main(int argc, char* argv[], char *envp[])
{
return 0;
}
6. 函数的声明和定义
6.1 函数声明
告诉编译器有一个函数叫什么,参数类型是什么,返回类型是什么。但是具体是否存在,函数声明决定不了。
函数的声明一般出现在函数的使用之前,要满足先声明后使用。
函数的声明一般要放在头文件中的。
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
//加法
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
//函数的定义
int Add(int x, int y)
{
return x + y;
}
将函数定义在main函数后,会报如下错误:
Warning:“Add”未定义;假设外部返回int
解决:
//一般也不如下使用
#include <stdio.h>
//函数的声明
int Add(int x, int y);
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
//加法
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
//函数的定义
int Add(int x, int y)
{
return x + y;
}
6.2 函数的定义
函数的定义是指函数的具体实现,交代函数的功能实现。
//真正情况下如下使用:
//add.h
#pragma once //防止头文件被重复包含
//函数的声明
int Add(int x, int y);
//sub.h
#pragma once //防止头文件被重复包含
//函数的声明
int Sub(int x, int y);
//add.c
//函数的定义
int Add(int x, int y)
{
return x + y;
}
//sub.c
int Sub(int x, int y)
{
return x - y;
}
//test.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b); //20 10
//加法
int sum = Add(a, b);
printf("%d\n", sum); //30
//减法
int ret = Sub(a, b);
printf("%d\n", ret); //10
return 0;
}
//其中的add.h与add.c分别为模块
初学编程时,觉得把所有的代码写到一个文件中最方便。但是在公司里不是这样写代码的。
原因:1、协作的角度;2、功能模块化
非本节重点内容:
假设场景(勿要当真,当然可以反编译):假如A程序员编写了一段加法代码,想变卖给B公司换钱,但并不想透露所有的源码,则可将自己的add.h和add.c编译后的静态库(add.lib)给B公司。
add.h:告诉B公司如何使用函数。
add.c :函数真正的实现。
A程序员
//add.h
#pragma once //防止头文件被重复包含
//函数的声明
int Add(int x, int y);
//add.c
//函数的定义
int Add(int x, int y)
{
return x + y;
}
//test.c
#include <stdio.h>
#include "add.h"
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b); //20 10
//加法
int sum = Add(a, b);
printf("%d\n", sum); //30
return 0;
}
B公司
//A程序员给B公司的add.h
#pragma once //防止头文件被重复包含
//函数的声明
int Add(int x, int y);
//将add.c编译成add.lib,里面的文件内容是编译后的二进制,达到隐藏的功能
//A程序员给B公司的add.lib
int Add(int x, int y)
{
return x + y;
}
//B公司自己实现的add.c
#include <stdio.h>
#include "add.h"
//导入静态库,不导入会报错(还会有依赖,暂且不说)
#pragma comment(lib, "add.lib")
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
//加法
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
上面场景的实现,编译需要使用VS,非VScode(编辑器)
-
VS Code是一个轻量级的代码编辑器,支持语法高亮、智能代码补全、自定义快捷键、代码片段以及代码重构等基本编辑功能,同时还支持调试和内置Git版本控制功能。它主要用于编写和调试代码,特别适合Web开发、Node.js开发以及各种脚本语言的支持。
-
VS是一个全功能的集成开发环境(IDE),除了包括VS Code的所有功能外,还提供了项目模板、项目管理、自动化构建、单元测试、性能分析等更全面的开发工具。它不仅支持前端和后端的开发,还支持数据库、云服务、移动应用开发等,是一个全面的开发解决方案。
7. 函数递归
7.1 什么是递归?
程序调用自身的编程技巧称为递归。
递归作为一种算法,在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
递归策略:只需少量的程序就可描述出解题过程中所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
7.2 递归的两个必要条件
存在限制条件,当满足这个限制条件时,递归便不再继续。
每次递归调用后越来越接近这个限制条件。
练习1
接收一个整型值(无符号),按照顺序打印它的每一位。例如:
输入1234,输出1 2 3 4
分析:
1234%10 余4
1234/10 商123
123%10 余3
123/10 商12
12%10 余2
12/10 商1
1%10 余1
1/10 商0
最后num 0
#include <stdio.h>
int main()
{
unsigned int num = 0;
scanf("%u", &num); //%d打印有符号的整数,会有正负数;%u打印无符号的整数
while (num)
{
printf("%d ", num%10); //4 3 2 1,未符合要求,使用另外的方法
num = num / 10;
}
return 0;
}
递归分析:
print(1234)
print(123) 4
print(12) 3 4
print(1) 2 3 4
//递归实现
#include <stdio.h>
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num); //%d打印有符号的整数,会有正负数;%u打印无符号的整数
print(num); //功能:接收一个整型值(无符号),按照顺序打印它的每一位
return 0;
}
如果缺少if条件语句,则会出现死递归(栈溢出Stack overflow)
如下图调试时报错
内存图:
如何解决栈溢出?(层次太深也会导致栈溢出)
1、将递归改为非递归。
2、使用static对象替代nonstatic局部对象。在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
补充:函数栈帧的创建和销毁
【函数栈帧的创建和销毁】函数栈帧的创建和销毁_哔哩哔哩_bilibili
科普概念如下:
寄存器:eax、ebx、ecx、edx、esp、ebp
esp、ebp(存放地址,用来维护栈区)
三者函数的调用关系:
练习2
编写函数不允许创建临时变量,求字符串长度。
模拟实现strlen,求字符串长度:
#include <stdio.h>
//int my_strlen(char str[]) //参数部分写成数组的形式
int my_strlen(char* str) //参数部分写成指针的形式
{
int count = 0; //计数,临时变量
while (*str != '\0')
{
count++;
str++; //找下一个字符
}
return count;
}
int main()
{
char arr[] = "abc"; //[a b c \0]
//char*,存放地址,指针变量接收
int len = my_strlen(arr);
printf("%d\n", len); //3
return 0;
}
递归求解:编写函数不允许创建临时变量,求字符串长度。
分析:
my_strlen("abc")
1 + my_strlen("bc")
1 + 1 + my_strlen("c")
1 + 1 + 1 + my_strlen("")
1 + 1 + 1 + 0
#include <stdio.h>
int my_strlen(char* str)
{
if (*str != '\0')
return 1 + my_strlen(str+1);
else
return 0;
}
int main()
{
char arr[] = "abc"; //[a b c \0]
//char*,存放地址,指针变量接收
int len = my_strlen(arr);
printf("%d\n", len); //3
return 0;
}
7.3 递归与迭代
练习3
求n的阶乘。
分析:
当n<=1,Fac(n) = 1;
当n>1,Fac(n) = n * Fac(n-1);
5! = 5*4*3*2*1 = 5*4!
//递归实现
#include <stdio.h>
int fac(int n)
{
if (n <= 1)
return 1;
else
return n * fac(n-1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fac(n);
printf("ret = %d\n", ret);
return 0;
}
//循环实现-非递归
#include <stdio.h>
int fac(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret *= i; //ret = ret * i
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fac(n);
printf("ret = %d\n", ret);
return 0;
}
练习4
求第n个斐波那契数
斐波那契数列:1 1 2 3 5 8 13 21 34 55····
分析:
当n<=2,Fib(n) = 1;
当n>2,Fib(n) = Fib(n-1) + Fib(n-2);
//递归实现,效率低
#include <stdio.h>
int Fib(int n)
{
if (n <=2 )
return 1;
else
return Fib(n-1) + Fib(n-2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
//迭代实现,效率高(均不考虑溢出问题)
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
提示
1、许多问题是以递归的形式来解释的,这只是因为它比非递归的形式更为清晰。
2、但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3、当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时的开销。
函数递归的经典题目:(自主研究)
-
汉诺塔问题(B站:比特大博哥)
-
青蛙跳台阶问题
总结
(最近比较忙,没有更新)
今天就暂且更新至此吧,期待下周再会。如有错误还请不吝赐教。如果觉得对您学习有所帮助,还请留下你的支持,以防下次失踪了嗷。
作者更新不易,免费关注别手软。