编程语言中的函数
数学上的函数是指一种映射关系,通常用来将一个输入(或多个输入)映射到一个输出。对于每一个特定的输入,总是有一个确定的输出。例如,函数 f(x) = x² 将输入 x 映射为其平方。
编程语言中的函数与之类似,具有输入和输出的概念,但其本质上更广泛和灵活。
在编程中,函数不仅可以执行计算,还可以执行一系列操作,如修改数据、输出信息、读取文件等。函数在编程中是用来封装一段代码,使得该代码可以复用,并且能够通过调用函数来执行特定的任务。
编程语言中的函数通常具有以下的特点:
-
定义与调用:
- 定义函数时,需要给出函数名、参数(如果有)和函数体。
- 调用函数时,通过函数名和传递的参数来执行函数。
-
参数与返回值:
- 函数通常接受参数(inputs),并可能返回一个值(output)。
- 某些函数可能不接受参数或不返回值(如在 C 语言中使用
void
关键字)。
-
封装与抽象:
- 函数将代码封装起来,使得复杂操作可以通过简单的函数调用来实现。
- 通过函数的使用,可以更容易地理解和管理代码结构。
-
作用域:
- 函数的内部变量通常具有局部作用域,即它们只能在函数内部访问。
- 这种作用域规则有助于避免变量命名冲突和意外的值修改。
Windows C 的函数编程
函数声明与定义
在使用函数前需要进行声明(或称为函数原型声明)。例如,int add(int a, int b);
。
函数的定义包括函数的返回类型、函数名、参数列表和函数体。
返回类型
每个函数必须有一个返回类型,表明该函数返回的数据类型。
如果函数不返回任何值,使用 void
作为返回类型。
参数传递
C 语言中的函数参数通常是按值传递的,即传递的是参数的副本,函数内部的修改不会影响原参数。
如果需要函数内部修改外部变量的值,可以使用指针进行参数传递。
变量的作用域
局部变量只在离自己最近的大括号内有效。形式参数也是局部变量。
全局变量在源文件中定义,占用的内存不会因为程序结束而释放,二是在整个进程的执行过程中一直有效。
变量的存储方式分为:
-
静态存储方式:程序运行期间,由系统分配固定的存储空间,
-
动态存储方式:程序运行时根据需要进行动态的分配存储空间。
外部变量
全局变量在源程序文件(一个编译单位)中,从定义位置到文件末尾一直有效。可以使用 extern
扩展变量的作用域。
#include <stdio.h>
extern int k;
//扩展全局变量k的作用域
void print()
{
printf("k = %d\n", k);
}
int k = 10;//全局变量k的定义与生效,向下↓
int main()
{
print();
}
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。全局变量不能定义在头文件中
#include <stdio.h>//标准库
int add();//函数声明
#include "func.h"
int i = 5;//全局变量
int j = 6;//全局变量
int main()
{
int sum;
sum = add();//在头文件中声明
printf("%d\n",sum);
}
//程序执行输出:11
extern int i;
extern int j;
//extern借用其他文件中的全局变量
int add()
{
return i+j;
}
静态局部变量
static
静态局部变量存在数据区,不会在函数结束随栈空间释放,可用于统计函数的调用次数。
#include <stdio.h>
extern int k;
void print()
{
static int i = 0;
//i存储在数据区,只初始化1次
i++;
//每次调用函数时,对i加1
printf("k = %d, i = %d\n", k, i);
}
int k = 10;
int main()
{
print();
print();
print();
}
静态全局变量不能被借用,静态函数不能被其他文件调用.
寄存器变量
register
关键字用于提示编译器将变量尽可能存储在 CPU 的寄存器中,而不是内存中,以提高访问速度。寄存器变量通常用于需要频繁访问的局部变量,因为寄存器访问比内存访问要快得多。
现代编译器通常已经非常优化,能够自动决定哪些变量应该存储在寄存器中。因此,手动使用 register
关键字在今天的编程实践中已经很少见,通常依赖于编译器的优化机制。即使指定了register
,编译器也可能将变量存储在内存中,特别是在寄存器资源紧张时。
-
存储位置:
register
变量提示编译器将其存储在寄存器中,但实际存储位置由编译器决定。编译器可能会忽略这个建议,尤其是在寄存器有限的情况下。
-
使用限制:
register
变量不能直接获取其地址(不能使用取地址符&
),因为寄存器不具有内存地址。
-
局部变量:
register
关键字只能用于局部变量或函数的形参,不能用于全局变量。
-
提高访问速度:
- 在一些需要高效执行的循环中,使用
register
变量可能会提高程序的运行效率。
- 在一些需要高效执行的循环中,使用
#include <stdio.h>
int main() {
register int counter; // 尽量将 counter 存储在寄存器中
for (counter = 0; counter < 10; counter++) {
printf("%d\n", counter);
}
return 0;
}
在这个例子中,counter
被声明为寄存器变量。编译器可能会将其存储在CPU寄存器中,而不是内存中,以提高循环中的访问速度。
内存管理
栈内存(Stack Memory):栈内存用于存储函数的局部变量和函数调用信息。栈上的内存在函数结束时自动释放。
- 访问速度快
- 空间有限
- 自动分配和释放
#define N 100000
int main()
{
int *a=(int *)malloc(4*N);
a[N-1] = 10;
printf("%d\n",a[N-1]);
}
Windows C 中,函数栈的大小为 1M,申请动态空间能避免出现访问越界的问题。
堆内存(Heap Memory):堆内存是通过 malloc
、calloc
、realloc
等函数动态分配的内存。堆内存不会在函数结束时自动释放,必须通过 free
函数显式释放。
- 访问速度相对较慢
- 空间较大
- 需要手动管理
在函数中,返回局部变量的指针存在未定义行为(Undefined Behavior)风险。返回栈上分配的局部变量指针是错误的做法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 函数 print_stack 使用栈内存存储字符串并返回
char* print_stack()
{
char c[] = "THIS IS STACK."; // 在栈上分配一个字符数组 c,并初始化为 "THIS IS STACK."
return c; // 返回字符数组 c 的首地址
}
// 函数 print_malloc 使用堆内存存储字符串并返回
char* print_malloc()
{
char *pc = (char*)malloc(20); // 在堆上分配 20 字节的内存
strcpy(pc, "THIS IS MALLOC"); // 将字符串 "THIS IS MALLOC" 复制到堆内存中
return pc; // 返回指向堆内存的指针 pc
}
int main()
{
char *p;
// 调用 print_stack 并尝试输出返回的字符串
p = print_stack();
puts(p); // 输出结果可能不可预期
// 调用 print_malloc 并输出返回的字符串
p = print_malloc();
puts(p); // 输出 "THIS IS MALLOC"
// 暂停程序,等待用户输入,防止程序立即退出
system("pause");
}
在 print_stack
函数中,返回的是一个局部变量的地址。当函数结束后,栈内存被释放(弹栈),栈上的字符数组 c
(表示字符串的首地址)所占用的内存被释放,返回的指针指向的是无效的内存位置(可能是“脏数据”),puts(p)
将输出不可预期的结果。
而堆空间不会因为函数执行释放,print_malloc
使用 malloc
函数分配了20字节的内存,并返回指向该内存的指针。这段内存直到手动释放 free
之前都可以安全地使用。
堆空间释放时 free
必须使用申请时返回的指针值,不能有任何偏移。当申请动态内存的指针指向发生改变时,无法对申请的堆空间进行释放。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int i;
char *p;
scanf("%d", &i);
p = (char*)malloc(i); //初始化,申请堆空间
strcpy(p, "malloc success");
puts(p);
free(p);
}
若动态分配的内存没有及时释放,导致内存占用越来越多,最终可能使程序崩溃,称为内存泄漏。
多次申请动态内存时,需注意避免下一次申请动态内存时,分配到同一块堆空间。例如:
int main()
{
int* p, * p1, * p2;
p = (int*)malloc(4);
*p = 1;
p1 = (int*)malloc(4);
*p1 = 2;
free(p);
p = NULL;
p2 = (int*)malloc(4);
*p2 = 3;
*p = 100;
printf("p = %d, p1 = %d, p2 = %d", *p, *p1, *p2);
}
函数指针
C 语言支持函数指针,即可以将函数的地址赋给指针变量,通过指针调用该函数,就像直接调用函数一样。
- 例如,如果有一个函数
void b()
,你可以定义一个函数指针void (*p)()
并让它指向b
。
(函数名本身存储的即为函数入口的首地址)
#include <stdio.h>
void b()
{
printf("i am b\n");
}
int main()
{
void (*p)();
// 定义函数指针
p = b;
// 让函数指针p指向b
p();
//等价于 b();
return 0;
}
通过函数指针,可以将函数指针作为参数传递给其他函数,从而实现“行为传递”的功能。例如,可以通过传递不同的函数指针来实现不同的计算方式。
C 语言本身不支持函数重载,但通过函数指针的灵活性,在运行时,可以根据需要选择不同的函数指针,从而调用不同的函数,达到动态选择功能的目的,不需要显式地为每种功能重写代码。
#include <stdio.h>
// 定义两个不同的函数
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
// 定义一个函数,它接受一个函数指针作为参数
void executeOperation(int (*operation)(int, int), int x, int y) {
int result = operation(x, y); // 调用传入的函数
printf("Result: %d\n", result);
}
int main() {
// 定义函数指针
int (*op)(int, int);
// 指向add函数
op = add;
executeOperation(op, 5, 3); // 输出 "Result: 8"
// 指向multiply函数
op = multiply;
executeOperation(op, 5, 3); // 输出 "Result: 15"
return 0;
}
-
int (*operation)(int, int)
:- 这是一个函数指针,表示指向返回类型为
int
,且接受两个int
参数的函数。
- 这是一个函数指针,表示指向返回类型为
-
传递行为:
- 在
main
函数中,op
可以指向不同的函数 (add
或multiply
)。 - 然后通过
executeOperation(op, 5, 3)
调用不同的行为(即函数add
或multiply
),实现了类似函数重载的效果。
- 在
面向接口的思想:通过使用函数指针,可以在 C 语言中动态传递不同的函数(行为),从而实现类似于其他编程语言中的函数重载,这在实现不同算法、回调函数、策略模式等方面非常有用。
增量编译
增量编译(incremental compilation):增量地处理源代码的变化,避免重复进行整个程序的完全编译。
理解增量编译首先需了解 C 语言编译及执行过程中的特点:
-
一个 C 程序由一个或多个程序模块组成,一个源程序文件可以被多个 C 程序公用。即源程序文件可以分别编写、编译,有利于提高调试效率。
-
一个源程序文件由一个或多个函数以及其他相关内容组成。一个源程序文件是一个编译单位。多个编译单位链接成为可执行文件
.exe
。 -
C 程序的执行是从
main
函数开始和结束的。 -
函数不能嵌套定义。函数之间的关系是平行的,定义函数时应分别进行,相互独立。函数之间可以相互调用(嵌套调用)。
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
jmp_buf envbuf;
void b()
{
printf("i am func b\n");
longjmp(envbuf,5);
//跳转回main函数,a不继续执行
}
void a()
{
printf("before b(), i am func a\n");
b();
printf("after b(), i am func c\n");
}
void main()
{
int i;
i = setjmp(envbuf);//保存进程执行的上下文
if(i==0)
{
a();
}
system("pause");
return;
}
递归与迭代
C 语言中的函数可以调用自身。
#include <stdio.h>
int factorial(int i)
{
if(i == 0 || i == 1)
{
return 1;
}
else
{
return i * factorial(i-1);
}
}
int main()
{
int i;
scanf("%d",&i);
printf("i! = %d\n",factorial(i));
}
通过找规律(公式)/ 结束条件实现递归,在处理某些算法时非常有用,但需要注意递归深度,以防止栈溢出。