Bootstrap

C语言笔记(第n版):函数

在上文,我介绍了几个基本的I/O函数,不知道你是否记得你初次使用函数的心情(我是不记得了),不过,你会很容易体会到函数的神奇之处——无需知道具体实现细节,便可方便快捷取得那些可能要经历复杂环节才能实现的效果。

​ 这样的编程机制在诸多编程语言中普遍存在(有的也叫Methods,方法),并且如果不出意外的话,应该所有的一些现代编程语言在它们入门级的“Hello World!”程序中都会给你一个function,比如

S y s t e m . o u t . p r i n t l n ( " H e l l o   W o r l d ! " ) ; —— J a v a System.out.println("Hello\ World!");——Java System.out.println("Hello World!");——Java
C o n s o l e . l o g ( " H e l l o   w o r l d ! " ) —— j a v a s c r i p t Console.log("Hello\ world!") ——javascript Console.log("Hello world!")——javascript
p r i n t ( " H e l l o   W o r l d ! " ) —— p y t h o n print("Hello \ World!")——python print("Hello World!")——python
s t d : : c o u t < < " H e l l o   W o r l d ! " < < s t d : : e n d l ; —— C + + std::cout << "Hello \ World! " << std::endl; ——C++ std::cout<<"Hello World!"<<std::endl;——C++
C o n s o l e . W r i t e L i n e ( " H e l l o , W o r l d ! " ) ; —— C # Console.WriteLine("Hello, World!");——C\# Console.WriteLine("Hello,World!");——C#

​ 不过,我觉得这样输出的效果,不太惊艳,好歹也是作为自己这门编程语言的第一个程序,我想要一个不一样的”Hello World!”,比如在屏幕上用字符组合拼出一个大大的”Hello World“程序。

​ 诶,现在我可以实现一个了,首先拼出一个轮廓,然后 p r i n t f printf printf输出,所以,结果是这样的:

pPDF(rintf(" _     _     _________      __             __                 ______\n");Sleep(500);
printf("| |   | |   |  ______|    |  |           |  |              /   ___ \\   \n");Sleep(500);
printf("| |   | |   | |           |  |           |  |             /  /    \\  \\  \n");Sleep(500);
printf("| |___| |   | |______     |  |           |  |            /  /      \\  \\  \n");Sleep(500);
printf("|  ___  |   |  ______|    |  |           |  |            |  |       |  | \n");Sleep(500);
printf("| |   | |   | |           |  |           |  |            \\ \\     /  /\n");Sleep(500);
printf("| |   | |   | |______     |  |______     |  |______       \\ \\___/  /\n");Sleep(500);
printf("|_|   |_|   |________|    |_________|    |_________|       \\ _____ /\n");Sleep(500);

在这里插入图片描述

​ 这个O有点丑,不过也是很努力了,嗯,加了点”特效“,让它延时输出。不过,我还是觉得不得劲,

​ 第一,我现在只能输出Hello,没有World,扩展会很麻烦;

​ 第二,就是如果我要把这个放在以后的代码中,我需要把整个代码相关部分强移植,这势必会有点麻烦,甚至破坏阅读性;

​ 第三,如果有一天,我把这个O弄完美了,我想要用到这个功能的部分全部更新,我就得一个一个的去找,这会很浪费时间。

​ 我必须要找到一个东西能够解决这个问题,显然 p r i n t f printf printf是一个完美的范本,它能解决这些问题。所以,Here we go! 让我们逐步做成一个我们的惊艳文字输出,姑且称之为 p r e t t y P r i n t prettyPrint prettyPrint

​ 既然 p r i n t f printf printf是一个函数,显然我们也需要一个函数,那么我们也弄一个函数,那怎么定义一个函数?

函数入门

返回值类型 函数名(参数列表...)
{
    // 函数体
}

​ 一个函数的框架如上所示,我们需要的是填空

void prettyPrint()
{
	
}

​ 注意,返回值类型,我写的是 v o i d void void,意思是,我不打算让我的函数返回什么,既然不返回什么就没有类型,就是 v o i d void void。第二,我的参数列表也没写,参数列表是什么呢?可能有些小白不知道,所以不知道就不写。

​ OK,接下来,把我的伟大作品放在里面,整个程序,看起来就是这个样子的:

#include <stdio.h>
#include <windows.h>

void prettyPrint()
{
    pPDF(rintf(" _     _     ________      __             __                 ______\n");
    Sleep(500);
    printf("| |   | |   |  ______|    |  |           |  |              /   ___ \\   \n");
    Sleep(500);
    printf("| |   | |   | |           |  |           |  |             /  /    \\  \\  \n");
    Sleep(500);
    printf("| |___| |   | |______     |  |           |  |            /  /      \\  \\  \n");
    Sleep(500);
    printf("|  ___  |   |  ______|    |  |           |  |            |  |       |  | \n");
    Sleep(500);
    printf("| |   | |   | |           |  |           |  |            \\ \\     /  /\n");
    Sleep(500);
    pPDF("|rintf("| |   | |   | |______     |  |makewa______     |  || makewa______       \\ \\___/  /\n");
    Sleep(500);
    pPDF("|_|rintf("|_|   |_|   ||_-|________|    |_________|    |_________|       \\ _____ /\n");
    Sleep(500);
}

int main()
{
   
    return 0;
}

​ 不错,不过怎么用呢?诶,学学 p r i n t f printf printf,我们这样:

int main()
{
	prettyPrint();
	return 0;
}

​ 不出意外,我们会成功,不过还是觉得不行,只能输出Hello,要是能够通过给它什么就能打印什么就好了。那么怎么在使用函数时给它数据呢?

传递参数

​ 在使用 p r i n t f printf printf时我们通过使用函数在其括号中添加数据,就可以在屏幕上显示我们想要的数据,这说明,这就是与函数数据交换的通道,那传些什么呢?不如传一个数字看看

int main()
{
    prettyPrint(3);
    return 0;
}

​ 传递后,再次运行,不会有任何问题,嗯,那函数里怎么得到3呢?这时就要用到参数列表了,这就像一个取菜的窗口,传了什么菜 拿什么碗装。

​ 既然传了一个3,那就用int,所以 p r e t t y P r i n t prettyPrint prettyPrint应该这样

void prettyPrint(int number)
{
	// 省略其它
}

​ 这就相当于 i n t   n u m b e r = 3 ; int\ number = 3; int number=3;,我们可以打印看看,是不是3:
在这里插入图片描述

​ 诶,就是3,不过,有个问题,如果传字母,那不是得有好多,不如我们简单点,我们弄个倒计时,传一个数字,从这个数字倒计时到0。不错,一下子工程量就少了。

​ 一个简单的先行版本是这样的

#include <stdio.h>
#include <windows.h>

void prettyPrint(int number)
{
    if (number < 3 || number > 9)
    {
        printf("搞不了!");
    }
    else
    {
        do
        {
            Sleep(500);
            system("cls");
            switch (number)
            {
            case 0:
                figure0();
                break;
            case 1:
                figure1();
                break;
            case 2:
                figure2();
                break;
            case 3:
                figure3();
                break;
            case 4:
                figure4();
                break;
            case 5:
                figure5();
                break;
            case 6:
                figure6();
                break;
            case 7:
                figure7();
                break;
            case 8:
                figure8();
                break;
            case 9:
                figure9();
                break;
            default:
                break;
            }

        } while (--number >= 0);
    
    }
    return 0;
}

void figure0()
{

    printf("******\n");
    printf("******\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("******\n");
    printf("******\n");
}

void figure1()
{
    printf("****  \n");
    printf("****  \n");
    printf("  **  \n");
    printf("  **  \n");
    printf("  **  \n");
    printf("  **  \n");
    printf("  **  \n");
    printf("  **  \n");
    printf("******\n");
    printf("******\n");
}

void figure2()
{
    printf("******\n");
    printf("******\n");
    printf("    **\n");
    printf("    **\n");
    printf("******\n");
    printf("******\n");
    printf("**    \n");
    printf("**    \n");
    printf("******\n");
    printf("******\n");
}

void figure3()
{
    printf("******\n");
    printf("******\n");
    printf("    **\n");
    printf("    **\n");
    printf("******\n");
    printf("******\n");
    printf("    **\n");
    printf("    **\n");
    printf("******\n");
    printf("******\n");
}

void figure4()
{
    printf("** ** \n");
    printf("** ** \n");
    printf("** ** \n");
    printf("** ** \n");
    printf("******\n");
    printf("******\n");
    printf("   ** \n");
    printf("   ** \n");
    printf("   ** \n");
    printf("   ** \n");
}

void figure5()
{
    printf("******\n");
    printf("******\n");
    printf("**    \n");
    printf("**    \n");
    printf("******\n");
    printf("******\n");
    printf("    **\n");
    printf("    **\n");
    printf("******\n");
    printf("******\n");
}

void figure6()
{
    printf("******\n");
    printf("******\n");
    printf("**    \n");
    printf("**    \n");
    printf("******\n");
    printf("******\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("******\n");
    printf("******\n");
}

void figure7()
{
    printf("******\n");
    printf("******\n");
    printf("    **\n");
    printf("    **\n");
    printf("    **\n");
    printf("    **\n");
    printf("    **\n");
    printf("    **\n");
    printf("    **\n");
    printf("    **\n");
}

void figure8()
{
    printf("******\n");
    printf("******\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("******\n");
    printf("******\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("******\n");
    printf("******\n");

}

void figure9()
{
    printf("******\n");
    printf("******\n");
    printf("**  **\n");
    printf("**  **\n");
    printf("******\n");
    printf("******\n");
    printf("    **\n");
    printf("    **\n");
    printf("******\n");
    printf("******\n");

}

int main()
{

    prettyPrint(10);
    return 0;
}

​ 成功了,也报错了
在这里插入图片描述

不过没关系,添加些东西就可以了

在这里插入图片描述
在这里插入图片描述

你看一个简单的倒计时就完成了(分分钟百行上下)。但是有个小问题,就是如果传入的参数非法,就会这样

在这里插入图片描述

嗯,似乎有些鲁莽,要是给别人用,还以为我有点小气呢。那怎么办?必须要给其一个回馈以防止认为函数没有响应,又不能就这样输出,因为这都程序结束了。所以,我们必须要设置一个可以在使用函数后返回给使用者的一个”状态码“。

设置返回值

​ 返回值就是通过return返回的值,初学者应该不陌生,因为在main函数中就有这个。为了让使用函数的人知道是否成功启动了函数,规定返回0成功,返回非0失败,如此,我们有

在这里插入图片描述

​ 请注意,在函数的最后,我添加了 r e t u r n   0 ; return \ 0; return 0;,一个函数在任何可能导致函数结束的地方的返回行为与值类型应该相同,此外函数的返回值类型我修改为了int,因为此时函数不再没有返回。

​ 接收也很容易,例如,可以这样:
在这里插入图片描述

​ 完结,撒花🎉!经过以上的一个小的函数设计,你会很容易发现,一切都是那么的从容,我不用在主函数中写大块的代码,或者说处理一切逻辑,我只需要在main中得到特定的函数里面实现特定的功能然后使用,彼此独立。

​ 在现实生活就像人们各自分工合作一样,领导负责分配,不负责生产,当某一个函数调用失败时,出错的往往是调用方,程序的错误也很快发现。

main函数

​ main作为C程序的入口,其重要地位不言而喻,无论用不用其它函数,你都必须用main,这也是最常见的一个函数,所以接下来我将通过展开讨论main,更进一步讲解函数(更高级之处将留在以后),并一起领略不一样的main。

关于main的定义

​ 为什么要谈论main的定义呢?因为在不同的场景中,你可能碰到过不同的main:

int main()  void main() void main(void) int main(int argc, char * argv[])  int main(int argc, char **argv)
{			{			{				{								   {
  //....     //...      // ...				//...    						 //...
  return 0;									return 0;						  return 0;
}			}			}				}								    }

​ 而官方,是这么定义的

int main (void) { body }	(1)	
int main (int argc, char *argv[]) { body }	(2)	
/* another implementation-defined signature */ (since C99)	(3)

​ 也就是说,只给了两种标准版,但是允许其它定义,嗯,那怎么确定我写的是不是可行呢?其实最好的判断就是不判断,建议使用标准的写法,因为肯定都适用,就比如第二种就是比较标准的,常见的写法(就我认为)。

调用main函数

​ 既然main函数也是函数,那么就能像普通函数一样使用吧,使用函数,规范说称之为调用(call,function call),调用函数,我们要使用一个叫做函数调用运算符(Function call operator)的东西,语法如下:

expression ( argument-list (可选) )
表达式-函数指针类型的任何表达式(左值转换后)
参数列表-任何完整对象类型的表达式(不能是逗号运算符)的逗号分隔列表。调用不带参数的函数时可能会省略。

​ 你可能不懂什么是函数指针,不过没关系,现在你只需把它当作是函数的名字即可,左值转换以后再谈。

​ OK,现在我们尝试调用main,那就有一个问题了,怎么调用呢?在哪里调用呢?一个标准的main函数可能如下:

int main (int argc, char *argv[]) { 
    body 
}

​ 如果对于一个普通的函数,我们可能会这样

main(....);

​ 如果是这样,在那里里,有两种可能:

​ 这样

int main (int argc, char *argv[]) { 
    main(...);
}

​ 或者是再定义一个函数,在那个函数中调用main

void function(){
	main(....);
}

int main(int argc, char * argv[]){

	function();
}

​ 显然都不合理的,因为这会不可避免的导致函数调用的死循环,或者说无限递归(在编程中,函数本身调用自身的操作称之为递归,上述第一种可称之为直接递归,第二种称之为间接递归)。而究其原因是main函数本身的特殊性,它在程序启动之时就被调用,也就是说,我们无法在C语言的源码中使main函数调用其自身,这属于禁止行为。

​ 但是既然main函数能够被调用,那肯定有调用方法,不能再程序中调用,那就在程序外调用,这就体现了main与其他函数的不同之处。

​ 在程序外调用main(通常意义上,应该说运行程序,因为程序一旦启动,main函数就会被系统调用),我们能做的就是在系统调用main时可选性地提供参数(这时叫做命令行参数),但这其实和直接调用也差不多,不过就是间接地,具体语法如下

程序名 参数列表(可选)

​ 同时,不得不提到的就是main函数的参数到底是什么意思,当然这是基于以上的标准main的定义形式,这两个值,在文档中,是这么描述的

argc	-	代表程序所运行的环境传递给程序的实参数量。
argv	-	指向 argc + 1 个指针的数组的首元素的指针。数组末元素为空指针,而若前面有元素,则它们指向表示从宿主环境传递给程	序的参数的字符串。若 argv[0] 不是空指针(或等价地 argc > 0),则它指向表示程序名的字符串。若程序名从宿主环境不可用则该字符串为空。

	名称 argc 和 argv 表示“argument count 实参计数”和“argument vector 实参向量”,使用它们是一项传统,但也可以为这些形参选用其他名字,也可以为它们的类型选用不同但等价的声明:int main(int ac, char** av) 同样合法。

​ 对于小白,这可能难以理解,但只需要记住的是:

  • ​ argc用于接收一个值,这个值指明argv所包含的信息数。并且正常情况下,这个值>=1,因为一般argv的”第一块存储单元“是这个程序的名字(编译成二进制形式后的程序名)

举个例子,在可执行程序后通过空格间隔不同的参数,你可以看到即使是简单的运行,不加任何参数,这时的argc依然是1。​ 在这里插入图片描述

  • ​ argv,一个空间用于存储信息。

    一个官方文档的例子(略修改是这样的),这个程序对小白可能不太友好,但是通过程序结果,你可以确实验证文档的说法。

#include <stdio.h>
 
int main(int argc, char *argv[])
{
    printf"argc = %d\n", argc);
    for (int ndx = 0; ndx != argc; ++ndx){
        printf("argv[%d] --> %s\n", ndx, argv[ndx]);
    }

}

在这里插入图片描述
​ 不过以上对于小白来讲似乎还是有些难了,所以我们点到为止,仅仅是作为一个main函数的特殊说明。以下做一个总结

在C编程中,main函数在托管环境中作为程序执行的起点,在所有静态存储器对象初始化后启动执行。它是程序控制开始的地方,并可根据传递给它的命令行参数执行命令。main函数通常接受两个参数:argc,一个整数,表示传递的参数数量(包括程序的名称),以及argv,一个字符串数组,参数的存储空间(char *argv[]),其中每个元素指向一个命令行参数。argv[0]保存程序的名称或其表示形式,后续元素(从argv[1]到argv[argc-1])指向包含实际参数的字符串。argv中的每个字符串都以空字符(‘\0’)结尾,确保程序内的字符串操作清晰且正确终止。

从main到mine

​ 了解完main后,就应该是我们自己的函数了。

​ C语言的函数大致可分为两种,一种是库函数,包含在一些预定义的头文件中,可以直接调用;一般而言库函数是指C语言的标准函数库(广义上,你可以说是一些先成的写好的第三方库函数),标准库不是 C 语言本身的构成部分,但是支持标准 C 的实现会提供该函数库中的函数声明、类型以及宏定义。在另一种是用户自定义函数,由用户在C语言程序中编写调用。事实上,用户也可以将自己编写的函数放进自定义的头文件中,并包含在程序中。用户自定义的函数可以直接在程序中编写,也可以放进一个自定义头文件中(这点以后讨论)。

​ 下面,我将会逐步说明自定义函数中的那些细节。

函数定义

​ 函数的定义,尽管在前面说过,但是不够细致,现在让我们系统来看一下

  1. 函数定义语法:

    return_type function_name(parameter_list) {
        // Function body
        statements;
        return expression; // Optional return statement
    }
    
    
    • return_type(返回值类型): 指明函数的返回值类型。如果函数不返回任何值,这可以是 void
    • function_name(函数名): 在程序中独一无二的函数名称
    • parameter_list(参数列表): 函数接收的参数列表 (可选),参数是用于向函数传递值的变量。
    • Function body(函数体): 若干条语句,用于决定函数是如何工作的。它封装( encapsulates)了函数执行的逻辑和操作。
    • return statement(返回语句): 返回给调用者一个值的可选语句。如果返回值类型是 void这将不是必要的。
  2. 返回与接收类型:

    以下面函数为例

    int add(int op1, int op2){
    	return op1 + op2;
    }
    
    int main(int argc, char* argv[]){
        
        int result = add(2, 3);
    }
    

    上面函数的功能一目了然,我们设计了一个add函数用于将两个整数相加并返回结果,在调用处,我使用一个resultint类型变量进行接收。

    ​ 整个过程相当简单,以至于简直相当于以下C语句

    int a = 2;
    int b = 3;
    int result = (int)(a + b);
    

    ​ 尽管其它函数可能不会这么简单,但这却透露出了函数调用中三处可能的数据类型转换过程:

    ​ 1、实参与形参的数据类型转换:实参(Argument,传递给函数的实际值)与形参(Parameter,函数定义或声明时提供的变量,是调用函数时将传递或提供给函数的实际值(或实参)的占位符)的数据类型的转换,如果有读者记得的话,在之前曾提过,有一个叫做默认参数提升(Default Argument Promotion)的隐式转换。

    ​ 具体来说,这是指实参在和形参数据类型不符(形参的数据类型更”高“)经历的的一次数据类型“扩大”,比如:

    • charshort 提升为int.
    • float提升为double.

    这保证了实参和形参的数据类型能够兼容。不过不可否认的是,会存在形参类型更低的情况,这时候和直接赋值的情况是一样的,甚至可以说,实参与形参的数据类型转换仅仅是隐式转换的一种实例,实际上和赋值转换的规则是一样的(如有不同,可能是笔者经验不够)。

    ​ 2、返回值与返回类型转换

    略,如有特别情况会补充,大致上可以理解为赋值转换的一种类型,可以有通用的数据类型转换操作:

    float calculateAverage(int total, int count) {
        return (float) total / count;  // Explicitly cast total to float
    }
    

    ​ 3、返回类型与接收值类型转换

    同上。

  3. 无返回与空参数函数

    上面提到过,如果函数不返回任意值,则可以为void,这在之前的数据类型中提到过,意味着空类型,但是空类型不意味着没有,在后面这甚至可以作为“任意类型”,不过目前,你仍然可以把它当作“没有”。比起符号上的没有,有一种事实上的没有,如

    print(){
    	printf( have no return type!\n");
    }
    

    我没有在return-type这里写任何东西,你可能会想这不会报错吗?事实上,如果使用gcc命令,你会收到一个警告(在一些IDE中,可能不明显提醒),但是这依然能够运行,从警告中,你可以发现这经历了什么,编译器默认函数的返回值为int,所以你可以不写函数的返回值类型,但这不是一个好的编程风格。
    在这里插入图片描述

    推一及二,形参不写和写void一样吗?如果读者还能记得前面写的倒计时函数,你会发现,在一次编译中,我在调用函数时向 p r e t t y P r i n t prettyPrint prettyPrint函数传递了一个实参3,但是那时函数的参数列表什么也没有,程序没有任何问题。难道也是默认int,我们验证一下
    在这里插入图片描述
    传递字符串3.0没有任何问题,那试着多几个
    在这里插入图片描述

    传递了4个不同类型的参数,没有任何问题,看来没有就是“所有”,不过如果函数的参数列表是这样的,那么不用运行就错了
    在这里插入图片描述

    至于为什么函数参数列表不写时可以“无限传”参数,这留在后面讲,不过可以确定的是,你在函数中得不到它们!

  4. 独一无二的函数名:

​ 在上面,曾提到过函数名的独一无二,这可能并不奇怪,因为如果函数名相同,那程序就不知道调用哪一个了?不过在其他一些编程语言中却可以存在函数名相同的情况,在其他语言中,这称之为函数的重载(overload)。编译器将通过调用者调用函数的行为(如传入的参数类型)来推测出具体要调用哪一个函数,不过C语言的编译器不支持,具体为什么,读者有兴趣可以去了解。

​ 那这就会导致一个问题,诶,我怎么保证我的函数名一定是独一无二的的呢?毕竟有那么多库,答案是,有办法,后面再说。

​ 另一个,在之前设计倒计时函数时曾经遇到过,那就是函数的“undefined reference”,那时我说只需在前面加点东西就可以了,不知道读者是否知道为什么,如果不知道,倘若注意到函数的定义与调用的顺序你就会知道,“在前面加东西”是因为调用在前,定义在后。而加上的那些东西,我们称之为**“前向声明(forward declaration)”,具体点叫”函数声明(function declaration,有时也说函数原型,function prototype)“,函数声明在形式上是函数定义(function definition)**的一部分,但也有差别,以下引用一段教程的原文作辨析(供参考):

​ A forward declaration allows us to tell the compiler about the existence of an identifier before actually defining the identifier.

前置声明允许我们在实际定义标识符之前告诉编译器标识符的存在。

​ In the case of functions, this allows us to tell the compiler about the existence of a function before we define the function’s body. This way, when the compiler encounters a call to the function, it’ll understand that we’re making a function call, and can check to ensure we’re calling the function correctly, even if it doesn’t yet know how or where the function is defined.

​ 在函数的情况下,这允许我们在定义函数体之前告诉编译器函数的存在。这样,当编译器遇到对函数的调用时,它会理解我们正在进行函数调用,并可以检查以确保我们正确调用函数,即使它还不知道函数是如何定义的或在哪里定义的。

​ To write a forward declaration for a function, we use a function declaration statement (also called a function prototype). The function declaration consists of the function’s return type, name, and parameter types, terminated with a semicolon. The names of the parameters can be optionally included. The function body is not included in the declaration.

​ 要为函数编写前置声明,我们使用函数声明语句(也称为函数原型)。函数声明由函数的返回类型、名称和参数类型组成,以分号结尾。可以选择包含参数的名称。函数体不包含在声明中。

​ 有一个在其他语言可能用得比较多的概念,叫做函数签名(function Signature),这是一个仅仅包括函数名及其参数类型的组合,函数签名通常是具有重载机制的编程语言用于鉴别是否合法重载的关键。

​ 以下的一张图可以简要展示这些术语的包括范围:
在这里插入图片描述

另一个值得说明的是函数声明的写法,例如对于以下函数

int add(int op1, int op2){
	return op1 + op2;
}

以下声明均合法、等效:

int add(int op1, int op2);// 和定义的函数头一样
int add(int, int); // 不写形参名
int add(int a, int b);// 和定义的形参名不同
int add(int a, int); // 其它

因为函数声明只需告知编译器返回值类型、函数名、参数的数据类型即可。不过注意声明不要写多了,以上声明取一即可,不过写多了也没事,不要少写了就行。

函数调用

​ 之前的函数调用,我们一直是只关注调用的结果,没注意调用的过程,现在我们来简单聊聊

函数调用的过程

在C中调用函数时,通常会发生以下事件序列:

1.函数调用
-当调用函数时,控制权从调用者(代码中调用函数的点)转移到被调用者(被调用的函数)。

2.参数传递
-如果函数有参数,则将函数调用中传递的值分配给这些参数。在C中,参数默认按值传递,这意味着参数值的副本被传递给函数。

3.局部变量分配
-调用函数时在堆栈上分配空间(用于局部变量)。这是函数执行期间局部变量和参数所在的位置。你可能不懂局部变量是什么,不过没关系,我在后文说明。

4.函数体执行
-函数体中的语句按顺序执行。这包括任何计算、I/O操作或对局部变量的修改。

5.返回声明
-如果函数的返回类型不是“void”,则使用带有表达式(计算为函数的返回类型)的“back”语句将值返回给调用者。

6.控制返回
-函数完成执行后(通过到达函数体的末尾或遇到“返回”语句),控制权返回给调用者。执行从函数调用后立即恢复。

以上的一个流程相当简要,不过要去具体的实践可能还要涉及一些难一点的东西,这里不先展开了。

递归

递归是一个强大的概念,其中函数直接或间接调用自身,属于函数调用的一种特殊情况。当函数调用自身时,每次都会创建该函数的新实例。递归是这样进行的:

1.基线条件(base case)
-递归函数通常包括一个基线条件-函数停止递归调用自身的条件。这可以防止无限递归并定义递归的端点。

2.递归条件(recursive case)
-除了基线条件之外,递归函数还包括一个或多个递归条件-函数使用修改的参数再次调用自身的条件。

3.调用堆栈
-每个递归调用都会在调用堆栈上创建一个新的激活记录(堆栈帧)。此堆栈帧保存参数、局部变量和返回地址等信息。

4.堆栈展开
-当达到基线条件时,函数调用开始返回。这个过程称为堆栈展开,每个返回都会从调用堆栈中弹出顶部堆栈帧,并在前一个函数调用的返回点继续执行。

#include <stdio.h>

// 递归函数计算阶乘
int factorial(int n) {
    // 基线条件: 0的阶乘是1
    if (n == 0) {
        return 1;
    } else {
        // 递归条件:  n 的阶乘是 n * factorial(n-1)
        return n * factorial(n - 1);
    }
}

int main() {
    int num = 5;
    int result = factorial(num);
    printf"Factorial of %d is %d\n", num, result);
    return 0;
}

在这个例子中:

  • factorial(5) 将会被 main调用。
  • factorial(5) 调用factorial(4), 后者调用 factorial(3), 依依此类推次类推。
  • 当调用 factorial(0) 即达到基线条件时,开始返回 1.
  • 递归调用依次展开: factorial(1) 返回 1, factorial(2) 返回 2, 依此类推, 直到 factorial(5) 返回120 (即 5!).

什么是递归,通过这篇文章,让你彻底搞懂递归 - 知乎

生存期与作用域

​ 生存期与作用域是对变量两个维度的量度,生存期是指一个变量从合法到非法也即(undefined)的过程,比如

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​ 而作用域是指一个变量的可访问程度,即在多大的范围能够访问到这个变量,如果不能访问到,无外乎,作用域触及不到或者变量已经消亡,而表现和生存期的一样,都一般是(undefined)。并且一般两者贴合得很紧,比如上面得例子,也可以说是作用域触及不到。

​ 以下的表格概括生存期和作用域的一般对应关系

生存期(Lifetime)作用域(scope)说明
自动生存期(auto)函数作用域、代码块作用域一般的(在函数体内,包括在函数体里面的代码块定义的)变量就属于自动生存期,或者显示使用auto修饰的变量
静态生存期(static)文件作用域在源文件内函数外,定义的变量或者用staticextern修饰的变量
寄存器生存期(register)~由编译器决定或者手动添加register的变量
函数调用期函数原型作用域函数原型声明中形参(如果有)的作用范围

生存期(Lifetime)

​ 生存期,也称之为存储期(Storage duration),换言之就是程序存储多久这个对象。C语言有五种存储类型修饰符(Storage-class specifiers),用于显示指定对象的存储期限。

​ 不过在介绍这五种修饰符之前先阐明一些名词:

​ 全局变量与局部变量:这两个是比较相对的两个概念,所以非全局就是局部了,而全局一般是指那些在程序的任何地方都能访问并且和程序共存亡的一类变量,这类变量在程序启动时就被初始化并分配了空间。在C中没有特别地注明全局变量与局部变量。

存储类型修饰符

  1. auto

    • 说明:默认情况下,局部变量具有自动存储期,这意味着它们在函数调用开始时被创建,在函数调用结束时被销毁。auto关键字通常不需要显式使用,因为它是默认的存储类型。
    • 修饰范围:只允许修饰块级作用域,不包括参数列表里面的变量
    #include <stdio.h>
    
    void func() {  
        auto int a = 10; // 'a' is an automatic variable
        int b = 20;      // 'b' is also automatic variable
        printf("%d\n", a);
    }
    
    int main() {
        func();
        return 0;
    }
    
  2. register

    • 说明:这个修饰符用于建议编译器将变量存储在寄存器中,以提高访问速度。但是,这种类型的变量必须是基本数据类型,而且数量有限,另外建议不等于编译器同意优化。
    • 修饰范围:只允许修饰块级作用域里面的变量,换句话说,不允许修饰不在花括号里面的,或者参数列表里面的
    #include <stdio.h>
    
    void func() {
        register int a = 10; // 'a' is a register variable
        printf("%d\n", a);
    }
    
    int main() {
        func();
        return 0;
    }
    

    ​ 并且不管有没有被优化,都不能被取地址(地址运算符以后说明)。
    在这里插入图片描述

  3. static

    • 说明:当应用于局部变量时,static关键字表示变量在整个程序执行期间都存在,即使函数调用结束,其值也会保留。应用于全局变量时,static关键字限制变量的作用域仅限于当前文件。
    • 修饰范围:除了参数列表里面的变量,其它皆可
    #include <stdio.h>
    
    static int count = 0;
    
    void func() {
        static int a = 10; // 'a' is a static local variable
        count++;           // 'count' is a static global variable
        a++;
        printf("%d %d\n", a, count);
    }
    
    int main() {
        func();
        func();
        return 0;
    }
    
  4. extern

    • 说明:extern关键字用于声明一个在其他文件中定义的变量。这使得多个文件可以共享同一个全局变量。指明静态存储期
    • 修饰范围:除了参数列表里面的变量
    // 文件 extern_var.c
    extern int shared_var;
    
    int main() {
        printf("%d\n", shared_var);
        return 0;
    }
    
    // 文件 var_def.c
    int shared_var = 42;
    
  5. _Thread_local(until C23)thread_local(since C23)

    涉及到多线程,这里不多展开

存储期

  1. 自动存储期(Automatic Storage Duration)
    自动存储期的变量在进入包含其声明的代码块时被分配内存,并在退出该代码块时释放内存。变量长度数组(Variable Length Arrays, VLA)是一个例外,这点以后说。所有函数参数和非static的块作用域对象都有自动存储期。

    #include <stdio.h>
    
    void testAutoStorage() {
        int localVar = 10; // 自动存储期
        {
            int blockVar = 20; // 自动存储期
            printf("Block variable: %d\n", blockVar);
        }
        // printf("Block variable outside block: %d\n", blockVar); // 错误:'blockVar' 已超出作用域
    }
    
    int main() {
        testAutoStorage();
        return 0;
    }
    
  2. 静态存储期(Static Storage Duration)
    静态存储期的变量在整个程序执行期间都存在,且只在主函数之前初始化一次。所有用static声明的对象,以及所有具有内部或外部链接但没有声明为_Thread_local(直到C23)或thread_local(自C23/C11起)的对象都有静态存储期。

    #include <stdio.h>
    
    static int staticVar = 5; // 静态存储期
    
    void testStaticStorage() {
        static int staticBlockVar = 10; // 静态存储期
        staticBlockVar++;
        printf("Static block variable: %d\n", staticBlockVar);
    }
    
    int main() {
        testStaticStorage();
        testStaticStorage();
        return 0;
    }
    
  3. 线程存储期(Thread Storage Duration)

  4. 动态分配存储期(Allocated Storage Duration)
    动态分配存储期的变量根据请求分配和释放内存,使用动态内存分配函数如malloc, calloc, reallocfree

    Linkage(链接)

    ​ linkage是指标识符(变量或函数)在其他范围内被引用的能力。任何标识符都应属于以下之一:

    • 没有链接。标识符只能在它所在的局部范围内被引用。所有函数参数和所有非extern修饰的块级变量(包括声明为static的变量)都有此链接。
    • 内部链接。可以从当前翻译单元中的所有范围引用标识符。所有具有文件作用域的(函数和变量)都有此链接。
    • 外部链接。标识符可以从整个程序中的任何其他翻译单元中引用。所有非static函数、所有外部变量(除非前面声明为static)和所有文件范围的非静态变量都有这个链接。

作用域(scope)

​ 在C种存在4种作用域

  1. 文件作用域(File Scope)
    文件作用域的变量在整个文件内都是可见的,所以某变量的作用域是文件作用域,那么在某种程度上可以作为广义上的全局变量。只要变量定义出现在变量使用之前。这类变量通常在所有函数外部声明。

    #include <stdio.h>
    
    int globalVar = 5;
    
    void printGlobal() {
        printf("Global variable: %d\n", globalVar);
    }
    
    int main() {
        printGlobal();
        return 0;
    }
    
  2. 函数作用域(Function Scope)
    函数作用域的变量只在定义它们的函数内部可见。这些变量在函数被调用时创建,在函数执行完毕后销毁。

    #include <stdio.h>
    
    void printLocal() {
        int localVar = 10;
        printf("Local variable: %d\n", localVar);
    }
    
    int main() {
        printLocal();
        // printf("Local variable outside function: %d\n", localVar); // 错误:'localVar' undeclared
        return 0;
    }
    
  3. 代码块作用域(Block Scope)
    代码块作用域的变量只在一个特定的代码块(由花括号 {} 包围的区域)内可见。这些变量在进入代码块时创建,在离开代码块时销毁。

    #include <stdio.h>
    
    int main() {
        {
            int blockVar = 20;
            printf("Block variable: %d\n", blockVar);
        }
        // printf("Block variable outside block: %d\n", blockVar); // 错误:'blockVar' undeclared
        return 0;
    }
    
  4. 参数作用域(Parameter Scope)
    函数的参数在函数体内具有局部作用域。它们在函数调用时被创建,当函数返回时被销毁。

    #include <stdio.h>
    
    void printParam(int param) {
        printf("Parameter value: %d\n", param);
    }
    
    int main() {
        printParam(30);
        // printf("Parameter value outside function: %d\n", param); // 错误:'param' undeclared
        return 0;
    }
    

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;