跳转到内容

C 编程/过程和函数

来自维基教科书,开放的书籍,开放的世界
前一个:控制 C 编程 下一个:标准库

在 C 编程中,所有可执行代码都驻留在一个函数中。请注意,其他编程语言可能会区分“函数”、“子例程”、“子程序”、“过程”或“方法”——在 C 中,这些都是函数。函数是任何高级编程语言的基本特性,它使我们能够通过将任务分解成更小、更易于管理的代码块来处理大型复杂的任务。

在更低级别,函数不过是在计算机内存中存储函数相关指令的内存地址。在源代码中,此内存地址通常会赋予一个描述性的名称,程序员可以使用此名称调用函数并执行从函数起始地址开始的指令。与函数相关的指令通常被称为代码。在函数指令执行完毕后,函数可以返回一个值,代码执行将从紧接在函数初始调用之后的指令处继续。如果您现在还不能理解,别担心。一开始理解计算机内部最底层发生的事件可能会让人困惑,但随着您 C 编程技能的提升,最终会变得非常直观。

现在,您只需要知道函数及其相关代码块通常会多次执行(调用),从程序执行期间的几个不同位置调用。

举个简单的例子,假设您正在编写一个计算给定 (x,y) 点到 x 轴和 y 轴距离的程序。您需要计算整数 x 和 y 的绝对值。我们可以这样写(假设我们在任何库中都没有预定义的绝对值函数)

#include <stdio.h>

/*this function computes the absolute value of a whole number.*/
int abs(int x)
{
        if (x>=0) return x;
        else return -x;
}

/*this program calls the abs() function defined above twice.*/
int main()
{
        int x, y;
        
        printf("Type the coordinates of a point in 2-plane, say P = (x,y). First x=");
        scanf("%d", &x);
        printf("Second y=");
        scanf("%d", &y);
        
        printf("The distance of the P point to the x-axis is %d. \n Its distance to the y-axis is %d. \n", abs(y), abs(x));

        return 0;
}

下面的例子说明了函数作为过程的使用。这是一个简单的程序,它要求学生提供三个不同课程的成绩,并告诉他们是否通过了课程。在这里,我们创建了一个名为 check() 的函数,可以根据需要调用任意次数。该函数使我们不必为学生上的每门课都编写相同的指令集。

#include<stdio.h>

/*the 'check' function is defined here.*/
void check(int x)
{
        if (x<60)
                printf("Sorry! You will need to try this course again.\n");
        else
                printf("Enjoy your vacation! You have passed.\n");
}

/*the program starts here at the main() function, which calls the check() function three times.*/
int main()
{
        int a, b, c;
        
        printf("Type your grade in Mathematics (whole number). \n");
        scanf("%d", &a);
        check(a);
        
        printf("Type your grade in Science (whole number). \n");
        scanf("%d", &b);
        check(b);
        
        printf("Type your grade in Programming (whole number). \n");
        scanf("%d", &c);
        check(c);

        /* this program should be replaced by something more meaningful.*/
        return 0;
}

请注意,在上面的程序中,'check' 函数没有结果值。它只执行一个过程。

这就是函数的真正用途。

更多关于函数的信息

[edit | edit source]

将函数概念化为工厂中的机器很有用。在机器的输入侧,您输入“原材料”或要由机器处理的输入数据。然后机器开始工作,并将完成的产品,“返回值”,吐到机器的输出侧,您可以收集并用于其他目的。

在 C 中,您必须告诉机器预期处理哪些原材料以及您希望机器返回什么类型的完成产品。如果您为机器提供了与预期不同的原材料,或者试图返回与您告诉机器生产的产品不同的产品,C 编译器将抛出错误。

请注意,函数不需要接收任何输入。它也不需要返回任何内容给我们。如果我们将上面的例子修改为在 check 函数中询问用户他们的成绩,则无需将成绩值传递到函数中。并且请注意,check 函数不会将值传递回来。该函数只是在屏幕上打印一条消息。

您应该熟悉与函数相关的某些基本术语

  • 使用另一个函数 g 的函数 f 被称为调用 g。例如,f 调用 g 来打印十个数的平方。f 被称为调用者函数,g 被称为被调用者
  • 我们发送给函数的输入称为其参数。当我们声明函数时,我们描述参数,这些参数决定了哪些类型的参数可以传递到函数中。我们在函数名称旁边的括号中向编译器描述这些参数。
  • 返回某种答案给 f 的函数 g 被称为返回该答案或值。例如,g 返回其参数的总和。

在 C 中编写函数

[edit | edit source]

通过例子学习总是好的。让我们编写一个返回数字平方的函数。

int square(int x)
{
   int square_of_x;
   square_of_x = x * x;
   return square_of_x;
}

要了解如何编写这样的函数,看看这个函数整体执行的操作可能会有所帮助。它接收一个int, x,并对其进行平方,将其存储在变量 square_of_x 中。现在此值将被返回。

函数声明开头的第一个 int 是函数返回的数据类型。在本例中,当我们对整数进行平方时,我们得到一个整数,我们正在返回此整数,因此我们写int作为返回值类型。

接下来是函数的名称。使用有意义且描述性的函数名称是一个良好的习惯。将函数命名为它所编写的操作可能会有所帮助。在本例中,我们将函数命名为“square”,因为它就是做这件事的——它对数字进行平方。

接下来是函数的第一个也是唯一一个参数,一个int, 它将在函数中被称为 x。这是函数的输入

花括号之间的部分是函数的实际核心。它声明了一个名为 square_of_x 的整数变量,用于保存 x 的平方值。请注意,变量 square_of_x 只能在函数内部使用,而不能在外部使用。我们将在后面学习更多关于这类内容的信息,并且我们会看到这种属性非常有用。

然后,我们将 x 乘以 x,或者 x 的平方,赋值给变量 square_of_x,这就是这个函数的全部内容。接下来是一个return语句。我们想要返回 x 的平方值,因此我们必须说这个函数返回变量 square_of_x 的内容。

我们的花括号要关闭,我们已经完成了声明。

以更简洁的方式编写,这段代码与上面的代码执行完全相同的功能

int square(int x)
{
   return x * x;
}

请注意,这应该看起来很熟悉——您实际上一直在编写函数——main 是一个始终编写的函数。

一般而言

[edit | edit source]

一般来说,如果我们想声明一个函数,我们写

 type name(type1 arg1, type2 arg2, ...)
 {
   /* code */
 } 

我们之前说过,函数可以不接收任何参数,也可以不返回任何内容,或者两者兼而有之。如果我们想要让函数不返回任何内容,我们应该写什么呢?我们使用 C 的void关键字。void基本上意味着“无”——所以如果我们想要编写一个不返回任何内容的函数,例如,我们写

void sayhello(int number_of_times)
{
  int i;
  for(i=1; i <= number_of_times; i++) {
     printf("Hello!\n");
  }
}

请注意,上面函数中没有return语句。由于没有,我们写void作为返回值类型。(实际上,可以在过程中使用return关键字在过程结束之前返回调用者,但不能像函数一样返回值。)

不接收任何参数的函数呢?如果我们想要这样做,我们可以例如写

float calculate_number(void)
{
  float to_return=1;
  int i;
  for(i=0; i < 100; i++) {
     to_return += 1;
     to_return = 1/to_return;
  }
  return to_return;
}

请注意,此函数不接收任何输入,而只是返回此函数计算出的一个数字。

当然,您也可以将 void 返回和 void 参数组合在一起以获得有效的函数。

递归

[edit | edit source]

这是一个简单的函数,它执行无限循环。它打印一行并调用自身,这再次打印一行并再次调用自身,这种情况会一直持续下去,直到堆栈溢出并且程序崩溃。函数调用自身称为递归,通常您会有一个条件语句在有限的小步数后停止递归。

      // don't run this!
void infinite_recursion()
{
    printf("Infinite loop!\n");
    infinite_recursion();
}

可以这样进行简单的检查。请注意 ++depth 用于在将值传递到函数之前进行递增。或者,您可以在递归调用之前在单独的一行上进行递增。如果您说 print_me(3,0);,该函数将打印 Recursion 行 3 次。

void print_me(int j, int depth)
{
   if(depth < j) {
       printf("Recursion! depth = %d j = %d\n",depth,j); //j keeps its value
       print_me(j, ++depth);
   }
}

递归最常用于目录树扫描、查找链表的结尾、解析数据库中的树结构以及分解数字(和查找素数)等任务。

静态函数

[edit | edit source]

如果函数只能从声明它的文件中调用,则将其声明为静态函数是合适的。当函数被声明为静态时,编译器将知道以一种防止从其他文件中的代码调用函数的方式编译目标文件。示例

static int compare( int a, int b )
{
    return (a+4 < b)? a : b;
}

使用 C 函数

[edit | edit source]

我们现在可以编写函数,但我们如何使用它们呢?当我们编写 main 时,我们将函数放在包含 main 的大括号之外。

当我们想使用该函数时,比如,使用我们的calculate_number函数,我们可以写类似下面这样的代码

 float f;
 f = calculate_number();

如果函数接受参数,我们可以写类似下面这样的代码

 int square_of_10;
 square_of_10 = square(10);

如果函数不返回值,我们可以简单地写

 say_hello();

因为我们不需要变量来捕获它的返回值。

C 标准库中的函数

[edit | edit source]

虽然 C 语言本身不包含函数,但它通常与 C 标准库链接在一起。要使用此库,您需要在 C 文件的顶部添加一个 #include 指令,它可能是来自 C89/C90 的以下指令之一

可用的函数是

<assert.h> <limits.h> <signal.h> <stdlib.h>
  • assert(int)
  • (常量)
  • int raise(int sig)。这个
  • void* signal(int sig, void (*func)(int))
  • atof(char*), atoi(char*), atol(char*)
  • strtod(char * str, char ** endptr ), strtol(char *str, char **endptr), strtoul(char *str, char **endptr)
  • rand(), srand()
  • malloc(size_t), calloc (size_t elements, size_t elementSize), realloc(void*, int)
  • free (void*)
  • exit(int), abort()
  • atexit(void (*func)())
  • getenv
  • system
  • qsort(void *, size_t number, size_t size, int (*sortfunc)(void*, void*))
  • abs, labs
  • div, ldiv
<ctype.h> <locale.h> <stdarg.h> <string.h>
  • isalnum, isalpha, isblank
  • iscntrl, isdigit, isgraph
  • islower, isprint, ispunct
  • isspace, isupper, isxdigit
  • tolower, toupper
  • struct lconv* localeconv(void);
  • char* setlocale(int, const char*);
  • va_start (va_list, ap)
  • va_arg (ap, (type))
  • va_end (ap)
  • va_copy (va_list, va_list)
  • memcpy, memmove
  • memchr, memcmp, memset
  • strcat, strncat, strchr, strrchr
  • strcmp, strncmp, strccoll
  • strcpy, strncpy
  • strerror
  • strlen
  • strspn, strcspn
  • strpbrk
  • strstr
  • strtok
  • strxfrm
errno.h math.h stddef.h time.h
  • (errno)
  • sin, cos, tan
  • asin, acos, atan, atan2
  • sinh, cosh, tanh
  • ceil
  • exp
  • fabs
  • floor
  • fmod
  • frexp
  • ldexp
  • log, log10
  • modf
  • pow
  • sqrt
  • offsetof 宏
  • asctime (struct tm* tmptr)
  • clock_t clock()
  • char* ctime(const time_t* timer)
  • double difftime(time_t timer2, time_t timer1)
  • struct tm* gmtime(const time_t* timer)
  • struct tm* gmtime_r(const time_t* timer, struct tm* result)
  • struct tm* localtime(const time_t* timer)
  • time_t mktime(struct tm* ptm)
  • time_t time(time_t* timer)
  • char * strptime(const char* buf, const char* format, struct tm* tptr)
  • time_t timegm(struct tm *brokentime)
float.h setjmp.h stdio.h
  • (常量)
  • int setjmp(jmp_buf env)
  • void longjmp(jmp_buf env, int value)
  • fclose
  • fopen, freopen
  • remove
  • rename
  • rewind
  • tmpfile
  • clearerr
  • feof, ferror
  • fflush
  • fgetpos, fsetpos
  • fgetc, fputc
  • fgets, fputs
  • ftell, fseek
  • fread, fwrite
  • getc, putc
  • getchar, putchar, fputchar
  • gets, puts
  • printf, vprintf
  • fprintf, vfprintf
  • sprintf, snprintf, vsprintf, vsnprintf
  • perror
  • scanf, vscanf
  • fscanf, vfscanf
  • sscanf, vsscanf
  • setbuf, setvbuf
  • tmpnam
  • ungetc

可变长参数列表

[edit | edit source]

具有可变长参数列表的函数是可以接受可变数量参数的函数。C 标准库中的一个例子是printf函数,它可以根据程序员的使用方式接受任意数量的参数。

C 程序员很少需要编写新的具有可变长参数列表的函数。如果他们想传递大量内容给一个函数,他们通常会定义一个结构来保存所有这些内容——也许是一个链表,或者一个数组——并将这些数据放在参数中调用该函数。

但是,您偶尔会发现需要编写支持可变长参数列表的新函数。要创建一个可以接受可变长参数列表的函数,您必须首先包含标准库头文件stdarg.h. 接下来,像往常一样声明函数。接下来,将省略号("...")作为最后一个参数添加。这指示编译器,接下来将是一个可变列表的参数。例如,以下函数声明用于返回数字列表的平均值的函数

  float average (int n_args, ...);

请注意,由于可变长参数的工作方式,我们必须在参数中以某种方式指定可变长参数部分中元素的数量。在average函数中,这是通过一个名为n_args的参数来实现的。在printf函数中,这是通过您在为该函数提供的参数中第一个字符串中指定的格式代码来实现的。

现在函数已经被声明为使用可变长参数,我们接下来必须编写执行函数中实际工作的代码。要访问存储在我们average函数的可变长参数列表中的数字,我们必须首先声明一个变量来表示该列表本身

  va_list myList;

va_list类型是在stdarg.h头文件中声明的类型,它基本上允许您跟踪您的列表。但是,要开始实际使用myList,我们必须首先为它分配一个值。毕竟,仅仅声明它本身并不会做任何事情。为此,我们必须调用va_start,它实际上是stdarg.h中定义的宏。在va_start的参数中,您必须提供您计划使用的va_list变量,以及出现在您函数声明中省略号之前的最后一个变量的名称

#include <stdarg.h>
float average (int n_args, ...)
{
    va_list myList;
    va_start (myList, n_args);
    va_end (myList);
}

现在myList已经准备好使用,我们终于可以开始访问存储在其中的变量。为此,使用va_arg宏,它会从列表中弹出下一个参数。在va_arg的参数中,提供您正在使用的va_list变量,以及您要访问的变量应具有的原始数据类型(例如int, char

#include <stdarg.h>
float average (int n_args, ...)
{
    va_list myList;
    va_start (myList, n_args);
    
    int myNumber = va_arg (myList, int);
    va_end (myList);
}

通过从可变长参数列表中弹出n_args个整数,我们可以设法找到数字的平均值

#include <stdarg.h>
float average (int n_args, ...)
{
    va_list myList;
    va_start (myList, n_args);
    
    int numbersAdded = 0;
    int sum = 0;
     
    while (numbersAdded < n_args) {
        int number = va_arg (myList, int); // Get next number from list
        sum += number;
        numbersAdded += 1;
    }
    va_end (myList);
     
    float avg = (float)(sum) / (float)(numbersAdded); // Find the average
    return avg;
}

通过调用average (2, 10, 20),我们得到1020的平均值,也就是15.

参考文献

[edit | edit source]
前一个:控制 C 编程 下一个:标准库
华夏公益教科书