系列文章目录
目录
1. C语言中函数的分类
- 库函数:C标准库提供的函数,如
printf()
、scanf()
、strcpy()
等。这些函数已经预定义好,并且可以直接在任何 C 程序中使用。 - 用户定义函数:由程序员定义,以满足特定程序的需要。
1.1 函数的组成
ret_type fun_name(para1, * )
{
statement;//语句项
}
// ret_type 返回类型
// fun_name 函数名
// para1 函数参数
1.2 库函数
参考上面链接中的文档,可以知道主要的库函数分类:
-
输入/输出函数:
printf()
,scanf()
,putchar()
,gets()
,puts()
等函数用于处理屏幕输入和输出。fopen()
,fclose()
,fread()
,fwrite()
,fprintf()
等用于文件操作。
-
字符串处理函数:
strcpy()
,strcat()
,strcmp()
,strlen()
,strchr()
等函数用于字符串的复制、连接、比较、长度测定和搜索。
-
数学函数:
sin()
,cos()
,sqrt()
,log()
,exp()
等函数提供基本的数学运算功能,这些函数定义在<math.h>
头文件中。
-
内存管理函数:
malloc()
,free()
,calloc()
,realloc()
等函数用于动态内存分配和释放。
-
其他实用函数:
qsort()
,rand()
,srand()
,atof()
,atoi()
,system()
等提供了排序、随机数生成、字符串转换和执行系统命令的功能。
1.3 如何使用库函数
要在C程序中使用库函数,通常需要包含相应的头文件。例如,要使用 printf()
或 scanf()
,需要包含 <stdio.h>
头文件;要使用 malloc()
或 free()
,需要包含 <stdlib.h>
。
代码示例:
#include <stdio.h>
#include <string.h>
int main() {
const char correctPassword[] = "password123"; // 假设这是正确的密码
char input[256]; // 用于存储用户输入的密码
int attempts = 0; // 用于跟踪密码尝试次数
printf("您有三次机会输入密码。\n");
while (attempts < 3) {
printf("请输入密码:");
scanf("%255s", input); // 读取用户输入的密码
if (strcmp(input, correctPassword) == 0) {
printf("登录成功!\n");
return 0; // 密码正确,退出程序
} else {
printf("密码错误。\n");
attempts++; // 增加尝试次数
}
}
printf("三次密码尝试均错误,程序已退出。\n");
return 0; // 密码输入三次错误,退出程序
}
strcmp
函数用于比较两个字符串是否相等。它是 string.h
库中定义的函数之一
1.4 自定义函数
在 C 语言中,除了使用库函数外,程序员经常需要编写自己的函数来执行特定的任务,这些被称为自定义函数。自定义函数允许程序员将复杂的代码分解成更简单、更易于管理的部分,从而增强代码的可读性、可维护性和重用性。
自定义函数的基本要素
- 函数定义:确定函数的行为,包括它的名字、返回类型、参数列表(如果有的话),以及包含执行所需任务的代码块(函数体)。
- 函数声明(可选):也称为函数原型,通常在函数实现之前或在头文件中进行声明,以告知编译器函数的存在。
- 函数调用:在需要执行函数任务的地方,通过函数名和提供必要参数(如果有的话)来执行函数。
为什么使用自定义函数
- 模块化:函数允许将大的程序分割为小的、独立的部分,每部分完成特定的功能。
- 代码复用:通过函数,可以多次调用相同的代码,避免代码重复。
- 简化复杂任务:复杂的任务可以分解成更小的子任务,每个子任务由一个函数实现。
- 便于测试和维护:每个函数可以单独测试和修改,不影响整个程序的其他部分。
2. 函数的参数
实际参数(实参):
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参。
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数
中有效。
代码示例
#include <stdio.h>
// 函数定义:尝试通过值传递交换两个整数的值
void Swap1(int x, int y)
{
int tmp = 0; // 临时变量用于存储交换过程中的中间值
tmp = x; // 将x的值存储到tmp
x = y; // 将y的值赋给x
y = tmp; // 将tmp(原x的值)赋给y
// 注意:这只交换了函数内部的局部变量副本,不影响外部的实际变量
}
// 函数定义:通过指针传递交换两个整数的值
void Swap2(int *px, int *py)
{
int tmp = 0; // 临时变量用于存储交换过程中的中间值
tmp = *px; // 将px指向的值存储到tmp
*px = *py; // 将py指向的值赋给px指向的位置
*py = tmp; // 将tmp(原px指向的值)赋给py指向的位置
// 此函数通过操作地址,直接交换了实际变量的值
}
int main()
{
int num1 = 1; // 定义并初始化num1
int num2 = 2; // 定义并初始化num2
Swap1(num1, num2); // 调用Swap1尝试交换num1和num2
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
// 由于Swap1使用值传递,num1和num2的值不会改变
Swap2(&num1, &num2); // 调用Swap2并传递num1和num2的地址来真正交换它们
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
// Swap2使用指针传递,因此成功交换了num1和num2的值
return 0; // 程序成功结束
}
- Swap1 函数(形参):尽管它在内部执行了交换逻辑,但这种变化仅限于函数内部的局部变量。当控制权返回到调用函数后,原始变量(
num1
和num2
)保持不变。 - Swap2 函数(实参):通过接收变量的内存地址(即指针),直接在内存中交换值,从而确实改变了调用处的变量。
3. 函数的嵌套调用和链式访问
3. 1 嵌套调用
函数的嵌套调用是指在一个函数内部调用另一个函数。在C语言中,这是一种非常常见的做法,使得程序的模块化和代码的重用成为可能。嵌套调用可以极大地简化程序设计,使每个函数可以专注于一个具体的任务,而更高级别的逻辑可以通过调用这些函数来实现。
代码示例:
#include <stdio.h>
int multiply(int x, int y) {
return x * y; // 返回两个数的乘积
}
int square(int num) {
return multiply(num, num); // 调用multiply函数来计算平方
}
int main() {
int value = 5;
printf("The square of %d is %d\n", value, square(value)); // 输出5的平方
return 0;
}
在这个例子中,square
函数内部调用了 multiply
函数来计算一个数的平方,square
函数依赖 multiply
函数来完成其功能。
3. 2 链式访问
链式访问通常指的是一系列的函数调用,其中每个函数调用的返回值被用作下一个函数调用的参数,通过函数返回值直接传递到另一个函数的参数列表中来模拟链式调用的效果。
代码示例:
#include <stdio.h>
int add(int x, int y) {
return x + y;
}
int main() {
printf("Result: %d\n", add(add(5, 10), 20)); // 先计算 add(5, 10),然后结果作为参数与20再次调用add函数
return 0;
}
在这个例子中,add(5, 10)
的结果是15,然后这个结果被立即用作下一个 add
函数调用的参数,与20相加,最终输出35。
4. 函数定义
在 C 语言中,函数定义是实现函数功能的核心部分,它不仅告诉编译器函数的操作内容,还规定了函数的接口,即函数如何被调用。函数定义提供了完整的函数体,包括所有执行语句,以及必要的返回语句。
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
test.c:放置函数的实现
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x+y;
}
5. 函数递归
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
具有以下两个主要特征:
- 递归调用:函数在执行过程中调用自己。
- 终止条件:也称为基本情况,用于停止递归。如果没有终止条件,递归将无限进行下去,最终可能导致栈溢出错误。
代码示例:
#include <stdio.h>
// 函数定义:计算 n 的阶乘
int factorial(int n) {
if (n == 0) { // 基本情况:0 的阶乘为 1
return 1;
} else { // 递归调用
return n * factorial(n - 1);
}
}
int main() {
int num = 5;
printf("Factorial of %d is %d\n", num, factorial(num));
return 0;
}
在这个例子中,factorial
函数通过递归方式计算一个整数的阶乘。如果 n
为0,函数返回1(这是阶乘的基本情况)。如果不是0,函数通过调用自身 factorial(n - 1)
来计算 n-1
的阶乘,然后将其结果与 n
相乘。
#include <stdio.h> // 修正代码中的打字错误:应为 #include
// 函数定义:递归计算字符串长度
int Strlen(const char* str) {
// 检查当前字符是否为字符串结束符 '\0'
if (*str == '\0') {
// 如果是结束符,递归结束,返回0
return 0;
} else {
// 如果不是,返回1加上剩余字符串的长度
// 递归调用 Strlen 函数,str+1 将指针移动到下一个字符
return 1 + Strlen(str + 1);
}
}
int main() {
// 指向字符串的指针
char *p = "abcdef";
// 调用 Strlen 函数计算字符串长度
int len = Strlen(p);
printf("%d\n", len);
return 0;
}
运行过程
以字符串 "abcdef"
为例,Strlen
的调用过程如下:
Strlen("abcdef")
返回1 + Strlen("bcdef")
Strlen("bcdef")
返回1 + Strlen("cdef")
Strlen("cdef")
返回1 + Strlen("def")
Strlen("def")
返回1 + Strlen("ef")
Strlen("ef")
返回1 + Strlen("f")
Strlen("f")
返回1 + Strlen("")
(空字符串,指针指向结束符)Strlen("")
返回0
(基本情况)
每次递归调用都返回当前位置到字符串末尾的长度。当调用到 Strlen("")
时,返回 0,然后这些调用依次回溯并相加,最终得到总长度 6。
6. 函数迭代
迭代利用循环结构(如for循环、while循环)来重复执行代码块。迭代通常用于执行固定次数的任务或者直到满足特定条件时停止。
递归和迭代是两种在编程中解决问题和实现重复执行任务的基本方法。虽然它们都可以用来执行重复的任务,但它们在实现方式、性能和适用场景上有着明显的差异。
递归(Recursion)
优点:
- 简洁易懂:递归可以简化代码,使得处理复杂问题,如树遍历、图搜索、动态规划等更加直观。
- 减少代码量:对于某些问题,如快速排序和归并排序,递归方法可以减少需要编写的代码量。
缺点:
- 性能开销:每次函数调用都会在调用栈上增加一层,大量的递归调用可能导致性能问题,尤其是栈溢出。
- 递归深度:递归的深度受到系统栈大小的限制,“递归过深可能导致栈溢出”。
- 效率问题:递归可能会导致很多重复计算
递归过深可能导致栈溢出:在计算机科学中,栈溢出是指程序在执行过程中使用的内存超过了为其分配的栈区域的最大容量。这通常发生在递归调用过程中,尤其是递归层次过深时。
迭代(Iteration)
迭代利用循环结构(如for循环、while循环)来重复执行代码块。迭代通常用于执行固定次数的任务或者直到满足特定条件时停止。
优点:
- 性能优势:迭代不涉及额外的调用栈开销,因此通常比相应的递归实现更高效。
- 无栈溢出风险:迭代不会因为深度过大而导致栈溢出的问题。
- 控制简单:迭代的控制通常更直观,通过循环控制变量的修改即可实现循环的继续或终止。
缺点:
- 代码复杂度:对于某些递归适用的问题,如深度优先搜索,迭代实现可能需要手动模拟栈操作,代码会更复杂。
- 不够直观:对于自然递归的算法(如分治策略),迭代实现可能不那么直观易懂。