编程语言中的函数

数学上的函数是指一种映射关系,通常用来将一个输入(或多个输入)映射到一个输出。对于每一个特定的输入,总是有一个确定的输出。例如,函数 f(x) = x² 将输入 x 映射为其平方。

编程语言中的函数与之类似,具有输入和输出的概念,但其本质上更广泛和灵活。

在编程中,函数不仅可以执行计算,还可以执行一系列操作,如修改数据、输出信息、读取文件等。函数在编程中是用来封装一段代码,使得该代码可以复用,并且能够通过调用函数来执行特定的任务。

编程语言中的函数通常具有以下的特点:

  1. 定义与调用:

    • 定义函数时,需要给出函数名、参数(如果有)和函数体。
    • 调用函数时,通过函数名和传递的参数来执行函数。
  2. 参数与返回值:

    • 函数通常接受参数(inputs),并可能返回一个值(output)。
    • 某些函数可能不接受参数或不返回值(如在 C 语言中使用 void 关键字)。
  3. 封装与抽象:

    • 函数将代码封装起来,使得复杂操作可以通过简单的函数调用来实现。
    • 通过函数的使用,可以更容易地理解和管理代码结构。
  4. 作用域:

    • 函数的内部变量通常具有局部作用域,即它们只能在函数内部访问。
    • 这种作用域规则有助于避免变量命名冲突和意外的值修改。

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,编译器也可能将变量存储在内存中,特别是在寄存器资源紧张时。

#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):堆内存是通过 malloccallocrealloc 等函数动态分配的内存。堆内存不会在函数结束时自动释放,必须通过 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 语言支持函数指针,即可以将函数的地址赋给指针变量,通过指针调用该函数,就像直接调用函数一样。

(函数名本身存储的即为函数入口的首地址)

#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;
}
  1. int (*operation)(int, int)

    • 这是一个函数指针,表示指向返回类型为 int,且接受两个 int 参数的函数。
  2. 传递行为:

    • main 函数中,op 可以指向不同的函数 (addmultiply)。
    • 然后通过 executeOperation(op, 5, 3) 调用不同的行为(即函数 addmultiply),实现了类似函数重载的效果。

面向接口的思想:通过使用函数指针,可以在 C 语言中动态传递不同的函数(行为),从而实现类似于其他编程语言中的函数重载,这在实现不同算法、回调函数、策略模式等方面非常有用。

增量编译

增量编译(incremental compilation):增量地处理源代码的变化,避免重复进行整个程序的完全编译。

理解增量编译首先需了解 C 语言编译及执行过程中的特点:

  1. 一个 C 程序由一个或多个程序模块组成,一个源程序文件可以被多个 C 程序公用。即源程序文件可以分别编写、编译,有利于提高调试效率。

  2. 一个源程序文件由一个或多个函数以及其他相关内容组成。一个源程序文件是一个编译单位。多个编译单位链接成为可执行文件 .exe

  3. C 程序的执行是从 main 函数开始和结束的。

  4. 函数不能嵌套定义。函数之间的关系是平行的,定义函数时应分别进行,相互独立。函数之间可以相互调用(嵌套调用)。

#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));
}

通过找规律(公式)/ 结束条件实现递归,在处理某些算法时非常有用,但需要注意递归深度,以防止栈溢出。