Linux C编程一站式学习笔记3

news2024/9/30 21:28:16

lLinux C编程一站式学习笔记 chap3 简单函数

文章目录

  • lLinux C编程一站式学习笔记 chap3 简单函数
  • 一.数学函数
    • C标准库和glibc
  • 二.自定义函数
  • 三.形参和实参
    • Man Page
    • 习题
  • 四.全局变量、局部变量和作用域
    • 局部变量 local variable
    • 全局变量 global variable
    • 全局变量和局部变量重名的情况
    • 局部变量和全局变量初始化的不同
    • 如何证明“局部变量的存储空间在每次函数调用时分配,在函数返回时释放”?
  • 相关资源、参考资料

一.数学函数

  • 建立一个C语言文件second.c

    #include <math.h>
    #include <stdio.h>
    
    int main(void)
    {
    	double pi = 3.1416;
    	printf("sin(pi/2)=%f\nln1=%f\n", sin(pi/2), log(1.0));
    	return 0;
    }
    

    ln函数在C标准库中叫做log

    • 编译运行这个程序,结果如下

      $ gcc second.c -lm
      $ ./a.out
      sin(pi/2)=1.000000
      ln1=0.000000
      
      • 这里需要加上-lm,否则会报错

        在这里插入图片描述

      • 参考:在编译时为什么要加上 –lm ?

        • 这篇博客详细多角度解答了这个问题

        image-20221217001402353

        • 以下摘自上面那篇博客:
          • 在Linux平台上最广泛使用的C函数库是glibc,其中包括C标准库的实现。几乎所有C程序都要调用glibc的库函数,所以glibc是Linux平台C程序运行的基础。glibc提供一组头文件和一组库文件,最基本、最常用的C标准库函数和系统函数在libc.so库文件中,几乎所有C程序的运行都依赖于libc.so (例如 printf)
          • 数学函数位于libm.so库文件中(这些库文件通常位于/lib目录下),-lm选项告诉编译器,我们程序中用到的数学函数要到这个库文件里找
        • 以下摘自本书:
          • 使用math.h中声明的库函数还有一点特殊之处,gcc命令行必须加-lm选项,因为数学函数位于libm.so库文件中(这些库文件通常位于/lib目录下),-lm选项告诉编译器,我们程序中用到的数学函数要到这个库文件里找。本书用到的大部分库函数(例如printf)位于libc.so库文件中,使用libc.so中的库函数在编译时不需要加-lc选项,当然加了也不算错,因为这个选项是gcc的默认选项。关于头文件和库函数目前理解这么多就可以了,到第 20 章 链接详解再详细解释。
  • 上面那个例子中

    • log(1.0)。在C语言的术语中,1.0参数(Argument)log是函数**(Function)log(1.0)是函数调用(Function Call)**

      在数学中写一个函数有时候可以省略括号,而C语言要求一定要加上括号

    • sin(pi/2)log(1.0)这两个函数调用在我们的printf语句中处于写表达式的位置

    • 函数调用也是一种表达式,这个表达式由函数调用运算符(()括号)和两个操作数组成

      • 操作数log是一个函数名(Function Designator),它的类型是一种函数类型(Function Type),操作数1.0double型的。log(1.0)这个表达式的值就是对数运算的结果,也是double型的,在C语言中函数调用表达式的值称为函数的返回值(Return Value)
  • 总结一下我们新学的语法规则:

    表达式 → 函数名
    表达式 → 表达式(参数列表)
    参数列表 → 表达式, 表达式, ...
    
  • printf也是一个函数,上例中的printf("sin(pi/2)=%f\nln1=%f\n", sin(pi/2), log(1.0))是带三个参数的函数调用,而函数调用也是一种表达式,因此printf语句也是表达式语句的一种。

    • 但是printf感觉不像一个数学函数,为什么呢?因为像log这种函数,我们传进去一个参数会得到一个返回值,我们调用log函数就是为了得到它的返回值,至于printf,我们并不关心它的返回值(事实上它也有返回值,表示实际打印的字符数),我们调用printf不是为了得到它的返回值,而是为了利用它所产生的副作用(Side Effect)--打印C语言的函数可以有Side Effect,这一点是它和数学函数在概念上的根本区别
      • Side Effect这个概念也适用于运算符组成的表达式。
        • 比如a + b这个表达式也可以看成一个函数调用,把运算符+看作函数,它的两个参数是ab,返回值是两个参数的和,传入两个参数,得到一个返回值,并没有产生任何Side Effect。
        • 而赋值运算符是有Side Effect的,如果把a = b这个表达式看成函数调用,返回值就是所赋的值,既是b的值也是a的值,但除此之外还产生了Side Effect,就是变量a被改变了,改变计算机存储单元里的数据或者做输入输出操作都算Side Effect。

下面这段话是作者写的学习理念,我觉得很棒!

  • 回想一下我们的学习过程,一开始我们说赋值是一种语句,后来学了表达式,我们说赋值语句是表达式语句的一种;一开始我们说printf是一种语句,现在学了函数,我们又说printf也是表达式语句的一种。随着我们一步步的学习,把原来看似不同类型的语句统一成一种语句了。学习的过程总是这样,初学者一开始接触的很多概念从严格意义上说是错的,但是很容易理解,随着一步步学习,在理解原有概念的基础上不断纠正,不断泛化(Generalize)。比如一年级老师说小数不能减大数,其实这个概念是错的,后来引入了负数就可以减了,后来引入了分数,原来的正数和负数的概念就泛化为整数,上初中学了无理数,原来的整数和分数的概念就泛化为有理数,再上高中学了复数,有理数和无理数的概念就泛化为实数。坦白说,到目前为止本书的很多说法都是不完全正确的,但这是学习理解的必经阶段,到后面的章节都会逐步纠正的。

  • 程序第一行的#号(Pound Sign,Number Sign或Hash Sign)和include表示包含一个头文件(Header File),后面尖括号(Angel Bracket)中就是文件名(这些头文件通常位于/usr/include目录下)。头文件中声明了我们程序中使用的库函数,根据先声明后使用的原则,要使用printf函数必须包含stdio.h,要使用数学函数必须包含math.h,如果什么库函数都不使用就不必包含任何头文件,例如写一个程序int main(void){int a;a=2;return 0;},不需要包含头文件就可以编译通过,当然这个程序什么也做不了。

C标准库和glibc

  • C标准主要由两部分组成,一部分描述C的语法,另一部分描述C标准库。

  • C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义。要在一个平台上支持C语言,不仅要实现C编译器,还要实现C标准库,这样的实现才算符合C标准。不符合C标准的实现也是存在的,例如很多单片机的C语言开发工具中只有C编译器而没有完整的C标准库。

  • 在Linux平台上最广泛使用的C函数库是glibc,其中包括C标准库的实现,也包括本书第三部分介绍的所有系统函数。几乎所有C程序都要调用glibc的库函数,所以glibc是Linux平台C程序运行的基础

    • glibc提供一组头文件和一组库文件,最基本、最常用的C标准库函数和系统函数在libc.so库文件中,几乎所有C程序的运行都依赖于libc.so

      • 有些做数学计算的C程序依赖于libm.so

      • 多线程的C程序依赖于libpthread.so

        以后作者说libc时专指libc.so这个库文件,而说glibc时指的是glibc提供的所有库文件。

  • glibc并不是Linux平台唯一的基础C函数库,也有人在开发别的C函数库,比如适用于嵌入式系统的uClibc

二.自定义函数

  • 我们不仅可以调用C标准库提供的函数,也可以定义自己的函数,事实上我们已经这么做了:我们定义了main函数。例如:

    int main(void)
    {
    	int hour = 11;
    	int minute = 59;
    	printf("%d and %d hours\n", hour, minute / 60);
    	return 0;
    }
    
  • main函数的特殊之处在于执行程序时它自动被操作系统调用,操作系统就认准了main这个名字,除了名字特殊之外,main函数和别的函数没有区别。我们对照着main函数的定义来看语法规则:

    函数定义 → 返回值类型 函数名(参数列表) 函数体
    函数体 → { 语句列表 }
    语句列表 → 语句列表项 语句列表项 ...
    语句列表项 → 语句
    语句列表项 → 变量声明、类型声明或非定义的函数声明
    非定义的函数声明 → 返回值类型 函数名(参数列表);
    
  • 给函数命名也要遵循标识符命名规则。

  • 由于我们定义的main函数不带任何参数,参数列表应写成void

    这里是书上的说法,但我感觉自我学习C语言以来,好像没怎么见过这个写法,我还得再去查查资料看

    • main函数括号里面里面需要写东西吗? - 菜鸟的回答 - 知乎 https://www.zhihu.com/question/444589396/answer/1731526236
    • main函数括号里面里面需要写东西吗? - 卡卡超人的回答 - 知乎 https://www.zhihu.com/question/444589396/answer/1732162507
  • 函数体可以由若干条语句和声明组成,C89要求所有声明写在所有语句之前(本书的示例代码都遵循这一规定),而C99的新特性允许语句和声明按任意顺序排列,只要每个标识符都遵循先声明后使用的原则就行。

  • main函数的返回值是int型的,return 0;这个语句表示返回值是0,main函数的返回值是返回给操作系统看的,因为main函数是被操作系统调用的,通常程序执行成功就返回0,在执行过程中出错就返回一个非零值。比如我们将main函数中的return语句改为return 4;再执行它,执行结束后可以在Shell中看到它的退出状态(Exit Status):

    image-20221217110436633

    运行这个可执行文件,然后使用echo $?可以得到结果4

    • $?是Shell中的一个特殊变量,表示上一条命令的退出状态

main函数的注意点👇

在这里插入图片描述

  • 最简单的自定义函数:不带参数也没有返回值的函数

    #include <stdio.h>
    
    void newline(void)
    {
    	printf("\n");
    }
    
    int main(void)
    {
    	printf("First Line.\n");
    	newline();
    	printf("Second Line.\n");
    	return 0;
    }
    

    在这里插入图片描述

    • 我们定义了一个newline函数给main函数调用,它的作用是打印一个换行,所以执行结果中间多了一个空行。

    • newline函数不仅不带参数,也没有返回值,返回值类型为void表示没有返回值,这说明我们调用这个函数完全是为了利用它的Side Effect。如果我们想要多次插入空行就可以多次调用newline函数。

      • 如果函数newline没有返回值,那么表达式newline()不就没有值了吗?然

      • 任何表达式都有值和类型两个基本属性。其实这正是设计void这么一个关键字的原因:首先从语法上规定没有返回值的函数调用表达式有一个void类型的值,这样任何表达式都有值,不必考虑特殊情况,编译器的语法解析比较容易实现;然后从语义上规定void类型的表达式不能参与运算,因此newline() + 1这样的表达式不能通过语义检查,从而兼顾了语法上的一致和语义上的不矛盾。

        妙啊!

  • 较简单的自定义函数

    #include <stdio.h>
    
    void newline(void)
    {
    	printf("\n");
    }
    
    void threeline(void)
    {
    	newline();
    	newline();
    	newline();
    }
    
    int main(void)
    {
    	printf("Three lines:\n");
    	threeline();
    	printf("Another three lines.\n");
    	threeline();
    	return 0;
    }
    

    image-20221217121913480

    • 通过此例我们可以知道
      • 同一个函数可以被多次调用
      • 可以用一个函数调用另一个函数,后者再去调第三个函数
      • 通过自定义函数可以给一组复杂的操作起一个简单的名字,例如threeline。对于main函数来说,只需要通过threeline这个简单的名字来调用就行了,不必知道打印三个空行具体怎么做,所有的复杂操作都被隐藏在threeline这个名字后面。
      • 使用自定义函数可以使代码更简洁,main函数在任何地方想打印三个空行只需调用一个简单的threeline(),而不必每次都写三个printf("\n")
  • 函数调用的执行顺序

    在这里插入图片描述

    读代码的过程就是模仿计算机执行程序的过程,我们不仅要记住当前读到了哪一行代码,还要记住现在读的代码是被哪个函数调用的,这段代码返回后应该从上一个函数的什么地方接着往下读

  • 函数声明和函数定义初步辨析

    比如void threeline(void)这一行,声明了一个函数的名字、参数类型和个数、返回值类型,这称为函数原型(Prototype)。在代码中可以单独写一个函数原型,后面加;号结束,而不写函数体,例如:

    void threeline(void);
    

    这种写法只能叫函数声明而不能叫函数定义,只有带函数体的声明才叫定义。

    只有分配存储空间的变量声明才叫变量定义,函数也是一样,编译器只有见到函数定义才会生成指令,而指令在程序运行时当然也要占存储空间。

    那么没有函数体的函数声明有什么用呢?它为编译器提供了有用的信息,编译器在翻译代码的过程中,只有见到函数原型(不管带不带函数体)之后才知道这个函数的名字、参数类型和返回值,这样碰到函数调用时才知道怎么生成相应的指令,所以函数原型必须出现在函数调用之前,这也是遵循“先声明后使用”的原则

  • 在上面的例子中,main调用threelinethreeline再调用newline,要保证每个函数的原型出现在调用之前,就只能按先newlinethreelinemain的顺序定义了。如果使用不带函数体的声明,则可以改变函数的定义顺序:

    #include <stdio.h>
    
    void newline(void);
    void threeline(void);
    
    int main(void)
    {
    	...
    }
    
    void newline(void)
    {
    	...
    }
    
    void threeline(void)
    {
    	...
    }
    

    这样仍然遵循了先声明后使用的原则

  • 由于有Old Style C语法的存在,并非所有函数声明都包含完整的函数原型,例如void threeline();这个声明并没有明确指出参数类型和个数,所以不算函数原型这个声明提供给编译器的信息只有函数名和返回值类型。如果在这样的声明之后调用函数,编译器不知道参数的类型和个数,就不会做语法检查,所以很容易引入Bug。读者需要了解这个知识点以便维护别人用Old Style C风格写的代码,但绝不应该按这种风格写新的代码。

    如果在调用函数之前没有声明会怎么样呢?比如按上面的顺序定义这三个函数,但是把开头的两行声明去掉:

    不推荐这种写法。

    #include <stdio.h>
    
    int main(void)
    {
    	printf("Three lines:\n");
    	threeline();
    	printf("Another three lines.\n");
    	threeline();
    	return 0;
    }
    
    void newline(void)
    {
    	printf("\n");
    }
    
    void threeline(void)
    {
    	newline();
    	newline();
    	newline();
    }
    

    编译时会报警告

    image-20221217123213439

    但仍然能编译通过,运行结果也对。这里涉及到的规则称为函数的隐式声明(Implicit Declaration),在main函数中调用threeline时并没有声明它,编译器认为此处隐式声明了int threeline(void);,隐式声明的函数返回值类型都是int,由于我们调用这个函数时没有传任何参数,所以编译器认为这个隐式声明的参数类型是void,这样函数的参数和返回值类型都确定下来了,编译器根据这些信息为函数调用生成相应的指令。然后编译器接着往下看,看到threeline函数的原型是void threeline(void),和先前的隐式声明的返回值类型不符,所以报警告。好在我们也没用到这个函数的返回值,所以执行结果仍然正确。

三.形参和实参

  • 带参数的自定义函数

    我们需要在函数定义中指明参数的个数和每个参数的类型,定义参数就像定义变量一样,需要为每个参数指明类型,参数的命名也要遵循标识符命名规则。例如:

    #include <stdio.h>
    
    void print_time(int hour, int minute)
    {
    	printf("%d:%d\n", hour, minute);
    }
    
    int main(void)
    {
    	print_time(23, 59);
    	return 0;
    }
    
    
  • 形参和实参

    • 我们调用print_time(23, 59)时,函数中的参数hour就代表23,参数minute就代表59。确切地说,当我们讨论函数中的hour这个参数时,我们所说的“参数”是指形参(Parameter),当我们讨论传一个参数23给函数时,我们所说的“参数”是指实参(Argument)

    • 形参相当于函数中定义的变量,调用函数传递参数的过程相当于定义形参变量并且用实参的值来初始化例如这样调用:

      void print_time(int hour, int minute)
      {
      	printf("%d:%d\n", hour, minute);
      }
      
      int main(void)
      {
      	int h = 23, m = 59;
      	print_time(h, m);
      	return 0;
      }
      

      下面这段解释真清楚

      相当于在函数print_time中执行了这样一些语句:

      int hour = h;
      int minute = m;
      printf("%d:%d\n", hour, minute);
      

      main函数的变量hprint_time函数的参数hour是两个不同的变量,只不过它们的存储空间中都保存了相同的值23,因为变量h的值赋给了参数hour。同理,变量m的值赋给了参数minute

      C语言的这种传递参数的方式称为Call by Value在调用函数时,每个参数都需要得到一个值,函数定义中有几个形参,在调用时就要传几个实参,不能多也不能少,每个参数的类型也必须对应上

      • 为什么我们每次调用printf传的实参个数都不一样呢?因为C语言规定了一种特殊的参数列表格式,用命令man 3 printf可以查看到printf函数的原型:
      int printf(const char *format, ...);
      

      第一个参数是const char *类型的,后面的…可以代表0个或任意多个参数,这些参数的类型也是不确定的,这称为可变参数(Variable Argument),第 6 节 “可变参数”将会详细讨论这种格式。总之,每个函数的原型都明确规定了返回值类型以及参数的类型和个数,即使像printf这样规定为“不确定”也是一种明确的规定,调用函数时要严格遵守这些规定,有时候我们把函数叫做接口(Interface),调用函数就是使用这个接口,使用接口的前提是必须和接口保持一致

    在这里插入图片描述

这本书真是融入了很多学习编程的思想!比如下面这段 启发我们要多去思考Rationale(基本原理)

在这里插入图片描述

Man Page

Man Page是Linux开发最常用的参考手册,由很多页面组成,每个页面描述一个主题,这些页面被组织成若干个Section。FHS(Filesystem Hierarchy Standard)标准规定了Man Page各Section的含义如下:

Section描述
1用户命令,例如ls(1)
2系统调用,例如_exit(2)
3库函数,例如printf(3)
4特殊文件,例如null(4)描述了设备文件/dev/null/dev/zero的作用
5系统配置文件的格式,例如passwd(5)描述了系统配置文件/etc/passwd的格式
6游戏
7其它杂项,例如bash-builtins(7)描述了bash的各种内建命令
8系统管理命令,例如ifconfig(8)

注意区分用户命令和系统管理命令,用户命令通常位于/bin/usr/bin目录,系统管理命令通常位于/sbin/usr/sbin目录,一般用户可以执行用户命令,而执行系统管理命令经常需要root权限。

Man Page中有些页面有重名,比如敲man printf命令看到的并不是C函数printf,而是位于第1个Section的系统命令printf,要查看位于第3个Section的printf函数应该敲man 3 printf,也可以敲man -k printf命令搜索哪些页面的主题包含printf关键字。

本书会经常出现类似printf(3)这样的写法,括号中的3表示Man Page的第3个Section,或者表示“我这里想说的是printf库函数而不是printf命令”

  • man printf如下

    image-20221217130940988

习题

  • 如果在一个程序中调用了printf函数却不包含头文件,例如int main(void) { printf(“\n”); },编译时会报警告:warning: incompatible implicit declaration of built-in function ‘printf’。请分析错误原因。

    没有包含头文件就没有printf函数的声明,编译器要做隐式声明,返回值int,参数一个,而真正的printf原型返回值是int,参数是可变个数的,所以incompatible。

    参考:https://www.zybuluo.com/ChristopherWu/note/72463

四.全局变量、局部变量和作用域

局部变量 local variable

我们把函数中定义的变量称为局部变量(Local Variable),由于形参相当于函数中定义的变量,所以形参也是一种局部变量。在这里“局部”有两层含义:

  • 一个函数中定义的变量不能被另一个函数使用。

    • 例如print_time中的hourminutemain函数中没有定义,不能使用,同样main函数中的局部变量也不能被print_time函数使用。如果这样定义:

      void print_time(int hour, int minute)
      {
      	printf("%d:%d\n", hour, minute);
      }
      
      int main(void)
      {
      	int hour = 23, minute = 59;
      	print_time(hour, minute);
      	return 0;
      }
      
    • main函数中定义了局部变量hourprint_time函数中也有参数hour虽然它们名称相同,但仍然是两个不同的变量,代表不同的存储单元。main函数的局部变量minuteprint_time函数的参数minute也是如此

  • 每次调用函数时局部变量都表示不同的存储空间。

    • 局部变量在每次函数调用时分配存储空间,在每次函数返回时释放存储空间,例如调用print_time(23, 59)时分配hourminute两个变量的存储空间,在里面分别存上23和59,函数返回时释放它们的存储空间,下次再调用print_time(12, 20)时又分配hourminute的存储空间,在里面分别存上12和20。

全局变量 global variable

  • 与局部变量的概念相对的是全局变量(Global Variable),全局变量定义在所有的函数体之外,它们在程序开始运行时分配存储空间,在程序结束时释放存储空间,在任何函数中都可以访问全局变量,例如:

    #include <stdio.h>
    
    int hour = 23, minute = 59;
    
    void print_time(void)
    {
    	printf("%d:%d in print_time\n", hour, minute);
    }
    
    int main(void)
    {
    	print_time();
    	printf("%d:%d in main\n", hour, minute);
    	return 0;
    }
    
  • 正因为全局变量在任何函数中都可以访问,所以在程序运行过程中全局变量被读写的顺序从源代码中是看不出来的,源代码的书写顺序并不能反映函数的调用顺序。程序出现了Bug往往就是因为在某个不起眼的地方对全局变量的读写顺序不正确,如果代码规模很大,这种错误是很难找到的。而对局部变量的访问不仅局限在一个函数内部,而且局限在一次函数调用之中,从函数的源代码很容易看出访问的先后顺序是怎样的,所以比较容易找到Bug。因此,虽然全局变量用起来很方便,但一定要慎用,能用函数传参代替的就不要用全局变量

全局变量和局部变量重名的情况

下面这个纸张覆盖的比喻也讲的好好!

在这里插入图片描述

则第一次调用print_time打印的是全局变量的值,第二次直接调用printf打印的则是main函数局部变量的值。在C语言中每个标识符都有特定的作用域,全局变量是定义在所有函数体之外的标识符,它的作用域从定义的位置开始直到源文件结束,而**main函数局部变量的作用域仅限于main函数之中**。

如上图所示,设想整个源文件是一张大纸,也就是全局变量的作用域,而**main函数是盖在这张大纸上的一张小纸,也就是main函数局部变量的作用域**。

在小纸上用到标识符hourminute时应该参考小纸上的定义,因为大纸(全局变量的作用域)被盖住了,如果在小纸上用到某个标识符却没有找到它的定义,那么再去翻看下面的大纸上有没有定义,例如上图中的变量x

局部变量和全局变量初始化的不同

  • 到目前为止我们在初始化一个变量时都是用常量做Initializer,其实也可以用表达式做Initializer

  • ⚠ 注意:局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常量表达式(Constant Expression)初始化

    • 例如,全局变量pi这样初始化是合法的:

      double pi = 3.14 + 0.0016;
      
    • 但这样初始化是不合法的:

      double pi = acos(-1.0);
      

    然而局部变量这样初始化却是可以的。程序开始运行时要用适当的值来初始化全局变量,所以初始值必须保存在编译生成的可执行文件中,因此初始值在编译时就要计算出来,然而上面第二种Initializer的值必须在程序运行时调用acos函数才能得到,所以不能用来初始化全局变量

    请注意区分编译时和运行时这两个概念为了简化编译器的实现,C语言从语法上规定全局变量只能用常量表达式来初始化,因此下面这种全局变量初始化是不合法的:

    int minute = 360;
    int hour = minute / 60;
    

    虽然在编译时计算出hour的初始值是可能的,但是minute / 60不是常量表达式,不符合语法规定,所以编译器不必想办法去算这个初始值。

    如果全局变量在定义时不初始化则初始值是0,如果局部变量在定义时不初始化则初始值是不确定的。所以,局部变量在使用之前一定要先赋值,如果基于一个不确定的值做后续计算肯定会引入Bug

如何证明“局部变量的存储空间在每次函数调用时分配,在函数返回时释放”?

  • 如何证明“局部变量的存储空间在每次函数调用时分配,在函数返回时释放”?当我们想要确认某些语法规则时,可以查教材,也可以查C99,但最快捷的办法就是编个小程序验证一下:

  • 验证局部变量存储空间的分配和释放

    #include <stdio.h>
    
    void foo(void)
    {
    	int i;
    	printf("%d\n", i);
    	i = 777;
    }
    
    int main(void)
    {
    	foo();
    	foo();
    	return 0;
    }
    

    第一次调用foo函数,分配变量i的存储空间,然后打印i的值,由于i未初始化,打印的应该是一个不确定的值,然后把i赋值为777,函数返回,释放i的存储空间。第二次调用foo函数,分配变量i的存储空间,然后打印i的值,由于i未初始化,如果打印的又是一个不确定的值,就证明了“局部变量的存储空间在每次函数调用时分配,在函数返回时释放”。分析完了,我们运行程序看看是不是像我们分析的这样:

    21922
    777
    

    在这里插入图片描述

    结果不太对劲啊?

    第二次调用打印的i值正是第一次调用末尾赋给i的值777。

    下面是作者写的,作者太懂初学者了哈哈哈!
    有一种初学者是这样,原本就没有把这条语法规则记牢,或者对自己的记忆力没信心,看到这个结果就会想:哦那肯定是我记错了,改过来记吧,应该是“函数中的局部变量具有一直存在的固定的存储空间,每次函数调用时使用它,返回时也不释放,再次调用函数时它应该还能保持上次的值”。

    还有一种初学者是怀疑论者或不可知论者,看到这个结果就会想:教材上明明说“局部变量的存储空间在每次函数调用时分配,在函数返回时释放”,那一定是教材写错了,教材也是人写的,是人写的就难免出错,哦,连C99也这么写的啊,C99也是人写的,也难免出错,或者C99也许没错,但是反正运行结果就是错了,计算机这东西真靠不住,太容易受电磁干扰和宇宙射线影响了,我的程序写得再正确也有可能被干扰得不能正确运行。

    这是初学者最常见的两种心态。不从客观事实和逻辑推理出发分析问题的真正原因,而仅凭主观臆断胡乱给问题定性,“说你有罪你就有罪”。

    • 先不要胡乱怀疑,我们再做一次实验,在两次foo函数调用之间插一个别的函数调用,结果就大不相同了:

      int main(void)
      {
      	foo();
      	printf("hello\n");
      	foo();
      	return 0;
      }
      
      • 我在Linux上做的结果

        21857
        hello
        21857
        
      • 我在windows上做的结果

        0
        hello
        1983827136
        

        这里我还是有些疑惑的,来看看作者的讲解吧!

        image-20221217134517940

  • 从第 2 节 “自定义函数”介绍的语法规则可以看出,非定义的函数声明也可以写在局部作用域中,例如:

    int main(void)
    {
    	void print_time(int, int);
    	print_time(23, 59);
    	return 0;
    }
    

    这样声明的标识符print_time具有局部作域,只在main函数中是有效的函数名,出了main函数就不存在print_time这个标识符了。

写非定义的函数声明时参数可以只写类型而不起名,例如上面代码中的void print_time(int, int);,只要告诉编译器参数类型是什么,编译器就能为print_time(23, 59)函数调用生成正确的指令。另外注意,虽然在一个函数体中可以声明另一个函数,但不能定义另一个函数,C语言不允许嵌套定义函数[5]。


改变函数又做了些实验,感觉自己还是不能很好地解释,先往下学吧,之后再回来解释

在这里插入图片描述

相关资源、参考资料

  • 豆瓣评价

  • 开源电子书

  • 《Linux C编程一站式学习》这书写得很不错,为什么都买不到了呢? - echo1937的回答 - 知乎 https://www.zhihu.com/question/34069391/answer/544825938

  • [大佬们的学习笔记]

    • 习题答案整理

    • https://blog.csdn.net/weixin_44576779/article/details/87443584

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/96025.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【linux】linux centos 6 日志服务、rsyslogd日志服务

1.概述 在CentOS 6.x中日志服务已经由rsyslogd取代了原先的syslogd服务。rsyslogd日志服 务更加先进,功能更多。但是不论该服务的使用,还是日志文件的格式其实都是和 syslogd服务相兼容的,所以学习起来基本和syslogd服务一致。 rsyslogd的新特点: 基于TCP网络协议传输日志…

测试人,为什么建议你要去大厂看一下,绝不仅仅是为了薪资和面子

&#x1f4cc; 博客主页&#xff1a; 程序员二黑 &#x1f4cc; 专注于软件测试领域相关技术实践和思考&#xff0c;持续分享自动化软件测试开发干货知识&#xff01; &#x1f4cc; 公号同名&#xff0c;欢迎加入我的测试交流群&#xff0c;我们一起交流学习&#xff01; 之前…

python数据容器分类

目录 一.数据容器可以从以下视角进行简单的分类: 1.是否支持下标索引 2.是否支持重复元素: 3.是否可以修改 二.数据容器特点对比 三.数据容器的通用操作 1.遍历 2.len(容器)、max(容器)、min(容器) 3.类型转换 注意 4.排序 一.数据容器可以从以下视角进行简单的分类:…

Clipper库 | 坐标圆整和精度控制

坐标圆整造成的问题 在Clipper库中顶点&#xff08;IntPoint&#xff09;的坐标使用的是整数类型&#xff0c; 目的是为了保持数字的鲁棒性&#xff0c;所以用整数类型来存储坐标&#xff0c;而不是我们常见的浮点数类型&#xff08;浮点存在不精确性&#xff09;。然而坐标圆…

Kerberos身份验证在ChunJun中的落地实践

Kerberos&#xff0c;在古希腊神话故事中&#xff0c;指的是一只三头犬守护在地狱之门外&#xff0c;禁止任何人类闯入地狱之中。 那么在现实中&#xff0c;Kerberos指的是什么呢&#xff1f; 一、Kerberos介绍 01 Kerberos是什么 根据百度词条释义&#xff0c;Kerberos是一…

Java——并查集

概念 当我们将多个元素分配到不同的集合中&#xff0c;这些集合有的是相关的&#xff0c;有的是不相关的。并查集就是用来查找两个元素是否在同一个集合中的 其主要实现方式是&#xff1a;将所有的元素以下标的形式存储在数组中。例如一共有十个人&#xff0c;那么就将这些人…

C语言tips-野指针

0.写在最前 最近因为工作需要开始重新学c语言&#xff0c;越学越发现c语言深不可测&#xff0c;当初用python轻轻松松处理的一些数据&#xff0c;但是c语言写起来却异常的复杂&#xff0c;这个板块就记录一下我的c语言复习之路 1. 什么是野指针&#xff1f; 如果一个指针指向的…

一文带你深入理解【Java基础】· IO流(上)

写在前面 Hello大家好&#xff0c; 我是【麟-小白】&#xff0c;一位软件工程专业的学生&#xff0c;喜好计算机知识。希望大家能够一起学习进步呀&#xff01;本人是一名在读大学生&#xff0c;专业水平有限&#xff0c;如发现错误或不足之处&#xff0c;请多多指正&#xff0…

机器学习100天(五):005 数据预处理之划分训练集

机器学习100天,今天讲的是:数据预处理之划分训练集。 在上一节,我们对类别特征进行了编码,X 和 y 已经变成了机器学习可以理解和处理的数据格式。 下面我们就要对数据集进行划分,划分成训练集和测试集。 在监督式机器学习中,我们一般使用训练集的数据来训练模型,然后…

【LVGL学习笔记】(二) 基础概念

LVGL全程LittleVGL&#xff0c;是一个轻量化的&#xff0c;开源的&#xff0c;用于嵌入式GUI设计的图形库。并且配合LVGL模拟器&#xff0c;可以在电脑对界面进行编辑显示&#xff0c;测试通过后再移植进嵌入式设备中&#xff0c;实现高效的项目开发。 LVGL中文教程手册&#…

心理健康网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 管理员功能&#xff1a; 1、管理关于我们、联系我们 2、管理文章类型、添加心理文章 3、审核咨询师注册信息 4、查看咨询…

我的创作纪念日——为什么要写博客

文章目录收获意义憧憬收获 转载和摘抄只是对知识的搜集&#xff0c;不仅不会起到扩充知识体系的作用&#xff0c;反而会让人陷入盲目的自信或者自卑。一些人会把收藏当作底蕴&#xff0c;例如看到一个如何快速学Python的标题&#xff0c;就会逢人说自己会Python&#xff1b;另…

哈夫曼树,哈夫曼编码及应用——(代码实现)

哈夫曼树&#xff0c;哈夫曼编码及应用1.哈夫曼树1.1 什么是哈夫曼树2.如何构造哈夫曼树&#xff08;哈夫曼算法&#xff09;2.1 举例实现哈夫曼树2.1.1手动实现具体步骤2.1.2代码实现具体步骤3.哈夫曼编码3.1 什么是哈夫曼编码3.2哈夫曼编码的具体实现END!!!1.哈夫曼树 路径长…

零基础可以学习Python吗?转行小白怎么学Python?

ython学习常用的技巧有这些&#xff1a;一是要明确自己的学习目的;二是从易到难&#xff0c;循序渐进;三是合理的选择资料&#xff0c;有所取舍;四是坚定自己的信念。以下是关于Python学习技巧的具体介绍。 1、明确自己的学习目的 每个人学Python的初衷绝对是不一样的&#xf…

【观察】Akamai:向分布式云迈出坚实一步,让云和边缘“无处不在”

近年来&#xff0c;云正如同日常生活中的水、电那样&#xff0c;融入到社会的各个层面&#xff0c;它不再是一种单纯的架构或者技术&#xff0c;而是千行百业走向数字化的核心基础设施&#xff1b;云也正在变成一种融合剂&#xff0c;无论是大数据、人工智能、物联网等&#xf…

多目标背包问题:MOJAYA求解多目标背包问题(Multi-objective Knapsack Problem,MOKP)提供Matlab代码

一、多目标背包问题 1.1多目标背包问题描述 多目标背包问题(Multi-objective Knapsack Problem&#xff0c;MOKP)是一种重要的组合优化问题&#xff0c;在生活的许多领域都有着十分广泛的应用。多目标背包问题可以描述为&#xff1a;给定一个背包和n种物品&#xff0c;其中&a…

docker-compose安装gogs

1.gogs是什么 Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发&#xff0c;并且支持 Go 语言支持的 所有平台&#xff0c;包括 Linux、Mac OS X、Windows 以及 ARM 平台。 2.准备工作gogs 在安装软…

论文精读:《FCOS3D: Fully Convolutional One-Stage Monocular 3D Object Detection》

文章目录论文精读摘要&#xff08;Abstract&#xff09;1. 介绍&#xff08;Introduction&#xff09;2. 相关工作&#xff08;Related Work&#xff09;3. 方法&#xff08;Approach&#xff09;3.1 框架总览(Framework Overview)3.2 2D引导的多层次3D预测(2D Guided Multi-Le…

【java】javac 相关API JavaCompiler StandardJavaFileManager AbstractProcessor

1.概述 转载并且补充:Java 编译器 javac 笔记:javac API、注解处理 API 与 Lombok 原理 看这个文章之前首先看:【java】java JSR 269 自定义注解实战 Lombok @Data注解 java版本直接调用 javac 是 Java 代码的编译器 [openjdk, oracle ],初学 Java 的时候就应该接触过。…

马上跨年了,如何用代码写一个“跨年倒计时”呢?

前言 大家好&#xff0c;我是陈橘又青&#xff0c;再过两周就是新的一年了&#xff0c;作为一名有仪式感的程序员&#xff0c;今天我们就来制作一个简单的跨年倒计时小网页&#xff0c;祝看到的所有人新年快乐&#xff01;&#xff08;附上完整源码&#xff0c;需要的小伙伴自取…