C语言进阶之路之函数、作用域与存储期篇

目录

一、学习目标

二、函数入门

函数的定义

如何设计自己的函数

如何在函数中传递参数

参数的地址传递:

函数调用的流程

局部变量与栈内存

静态函数

内联函数

递归函数

回调函数(钩子函数)

三、作用域与存储期

基本概念

函数声明作用域

局部作用域

全局作用域

static关键字

基本概念

自动存储期

静态存储期

自定义存储期

总结


一、学习目标

  • 掌握函数的命名、参数及返回值的设计
  • 理解静态函数、钩子函数、内联函数等函数的原理
  • 清楚代码运行时的作用域和生命周期等知识

二、函数入门

        在C语言中,函数意味着功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接起来的整体。也因为如此,C语言被称为模块化语言

对于函数的使用者,可以简单地将函数理解为一个黑箱,使用者只管按照规定给黑箱一些输入,就会得到一些输出,而不必理会黑箱内部的运行细节

        日常使用的电视机可以被理解为一个典型的黑箱,它有一些公开的接口提供给使用者操作,比如开关、音量、频道等,使用者不需要理会其内部电路,更不需要管电视机的工作原理,只需按照规定的接口操作接口得到结果。

        对于函数的设计者,最重要的工作是封装,封装意味着对外提供服务并隐藏细节。对于一个封装良好的函数而言,其对外提供服务的接口应当是简洁的,内部功能应当是明确的。

函数的定义

  • 函数头:函数对外的公开接口
    1. 函数名称:命名规则与跟变量一致,一般取与函数实际功能相符合的、顾名思义的名称。
    2. 参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,也可以不需要参数 0 - N
    3. 返回类型:即黑箱的输出数据类型,一个函数可不返回数据,但最多只能返回一个数据 0 - 1
  • 函数体:函数功能的内部实现代码 ,只用一对大括号包含 { 函数体 }
  • 语法说明:
返回类型 函数名称(参数1, 参数2, ……)
{
    函数体
}
  • 概念:
    • 函数调用过程中的被传递的参数,被称为实参,即 arguments
    • 函数定义语句中的参数,被称为形参,即 parameters
  • 实参与形参的关系:
    • 实参于形参的类型和个数必须一一对应。
    • 形参的值由实参初始化。
    • 形参与实参位于不同的内存区域,彼此独立。

如何设计自己的函数

    • 在设计自己的函数之前一定要想清楚该函数的功能
    • 第二步想清楚对外的接口,也就是用户如何使用该功能
    • 想清楚该共功能的执行步骤,并把所有的步骤用注释写清楚
    • 按照注释填写代码

如何在函数中传递参数

函数传参的过程中有两大类:

        参数的值传递:

  •            就是把参数具体的数值进行传递,实参的数据作为形参的初始化值。
    •     值传递后,形参与实参再也没有任何关联,他们分别在自己对应的函数栈空间中
int a = 123 ;
char b = 45 ;

func(a , b) ;

参数的地址传递:

        • 把参数的地址传递出去,实参的地址作为形参的初始值
        • 参数的地址传递,形参就指向实参实际的数据的地址,因此在子函数中可以直接操作实参所对应的地址
int a = 123 ; 
char b = 45 ; 
func( &a , &b) ;

函数调用的流程

函数调用时,进程的上下文会切换到被调函数,当被调函数执行完毕之后再切换回去。

局部变量与栈内存

  • 局部变量概念:凡是被一对花括号包含的变量,称为局部变量
  • 局部变量特点
    • 某一函数内部的局部变量,存储在该函数特定的栈内存
    • 局部变量只能在该函数内可见,在该函数外部不可见
    • 当该函数退出后,局部变量所占的内存立即被系统回收,因此局部变量也称为临时变量
    • 函数的形参虽然不被花括号所包含,但依然属于该函数的局部变量
  • 栈内存特点
    • 每当一个函数被调用时,系统将自动分配一段栈内存给该函数,用于存放其局部变量
    • 每当一个函数退出时,系统将自动回收其栈内存
    • 系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配的原则
int max(int x, int y) // 变量 x 和 y 存储在max()函数的栈中
{
    int z;            // 变量 z 存储在max()函数的栈中
    z = x>y ? x : y;
    return z;         // 函数退出后,栈中的x、y 和 z 被系统回收
}

int main(void)
{
    int a = 1; // 变量 a 存储在main()函数的栈中
    int b = 2; // 变量 b 存储在main()函数的栈中
    int m;     // 变量 m 存储在main()函数的栈中,未赋值因此其值为随机值
    
    m = max(a, b);
}

  • 技术要点:
    • 栈内存相对而言是比较小的,不适合用来分配尺寸太大的变量(较大尺寸可以放在或者数据段)。
    • return 之后不可再访问函数的局部变量,因此返回一个局部变量的地址通常是错误的

静态函数

  • 背景知识:普通函数都是跨文件可见的,即在文件 a.c 中定义的函数可以在 b.c 中使用。
  • 静态函数:只能在定义的文件内可见的函数,称为静态函数。
    • 静态函数就限制了函数的作用范围本文件可用
  • 语法:
staitc void f(void) // 在函数头前面增加关键字 static ,使之成为静态函数
{
    // 函数体
}
  • 要点:
    • 静态函数主要是为了缩小函数的可见范围,减少与其他文件中重名函数冲突的概率
    • 静态函数一般被定义在头文件中,然后被各个源文件包含

内联函数

  • 背景知识:当一个函数被调用的时候,进程会发生一个上下文切换的过程,而上下文切换是需要占用一定的执行时间的。
      • 如果执行的函数功能比较简单,所占用的执行时间较少并该函数会被多次重复地调用(成为一个热点代码)

inline void f(void) // 在函数头前面增加关键字 static ,使之成为静态函数
{
    // 函数体
}

递归函数

  • 递归概念:如果一个函数内部,包含了对自身的调用,则该函数称为递归函数
    • 一般递归的过程中会让问题的规模逐步缩小,缩小到能直接给出结果的情况下进行回归(返回)
  • 递归问题:
    • 阶乘。
    • 幂运算。
    • 字符串翻转。
  • 要点:
    • 只有能被表达为递归的问题,才能用递归函数解决。
    • 递归函数必须有一个可直接退出的条件,否则会进入无限递归最终会因为栈内存溢出导致段错误
    • 递归函数包含两个过程,一个逐渐递进的过程,和一个逐渐回归的过程。

打怪实战

        尝试使用递归函数的思路把一个给定的字符串进行翻转输出

        思路提示: 递进寻找字符串的末尾标记,找到后开始回归

回调函数(钩子函数)

  • 概念:函数实现方不调用该函数,而由函数接口提供方间接调用的函数,称为回调函数。
  • 示例:系统中的信号处理,是一个典型的利用回调函数的情形。

  • 要点:
    • 示例中函数 sighandler 是回调函数。
    • signal() 将函数回调函数传递给内核,使得内核可以在恰当的时机回调 sighandler。
    • 应用开发者和内核开发者只要约定好回调函数的接口,即可各自开发,进度互不影响。

打怪练习:

    • 使用递归函数实现一下两个操作:
      • 用户输入一个整型值 函数返回 该整型值的阶乘
      • 用户输入两个整型值,分别为底数和指数,该函数返回底数的幂次方结果

输入: 35 输出: 35的阶乘是:.... 输入: 5 6 输出: 5的6次方是:....

    • 尝试使用函数封装继续完成课堂上歌词分割的函数要求
      • 函数头: int StrTok( const char * src , const char * dlime , char (* dest) [ 100 ] )
    • 【拓展】完成笔试题中遇到的字符串压缩的操作比如:
函数头:int Compress( const char * src  , char * dest );
输入: Heeello  apppppleeee
压缩后输出: 1H2e2l1o1 1a5p1l1e4e

三、作用域与存储期

基本概念

        C语言中,标识符(变量、函数..)都有一定的可见范围,这些可见范围保证了标识符只能在一个有限的区域内使用,这个可见范围,被称为作用域(scope)

        软件开发中,尽量缩小标识符的作用域是一项基本原则,一个标识符的作用域超过它实际所需要的范围时,就会对整个软件的命名空间造成污染,导致一些不必要的名字冲突和误解。

函数声明作用域

  • 概念:在函数的声明式中定义的变量,其可见范围仅限于该声明式
  • 示例:
void func(int fileSize, char *fileName);
void func(int , char *);  // 函数声明只需要说明参数的列表以及返回值类型即可,因此变量的名字是可以省略
  • 要点:
    • 变量 fileSize 和 fileName 只在函数声明式中可见。
    • 变量 fileSize 和 fileName 可以省略,但一般不这么做,它们的作用是对参数的注解。

局部作用域

  • 概念:在代码块中定义的变量,其可见范围从其定义处开始,到代码块结束为止
  • 示例:
int main()
{
    int a=1;
    int b=2;     // 变量 c 的作用域是第4行到第9行
    {
        int c=4;
        int d=5; // 变量 d 的作用域是第7行到第8行
        int a = 100;
    }
}
  • 要点:
    • 代码块指的是一对花括号 { } 括起来的区域。
    • 代码块可以嵌套包含,外层的标识符会被内嵌的同名标识符临时掩盖变得不可见。
    • 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量

全局作用域

  • 概念:在代码块外定义的变量,其可见范围可以跨越多个文件
  • 示例:
// 文件:a.c
int global = 888; // 变量 global 的作用域是第2行到本文件结束
int main()
{
    
}
void f()
{ 
}
// 文件:b.c
extern int global; // 声明在 a.c 中定义的全局变量,使其在 b.c 中也可见
void f1()
{
}
void f2()
{
}
  • 要点:
    • 代码块指的是一对花括号 { } 括起来的区域。
    • 代码块可以嵌套包含,外层的标识符会被内嵌的同名标识符临时掩盖变得不可见。
    • 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量。

作用域的临时掩盖

如果有多个不同的作用域相互嵌套,那么小范围的作用域会临时 “遮蔽” 大范围的作用域中的同名标识符,被 “遮蔽” 的标识符不会消失,只是临时失去可见性

  • 示例代码:
int a = 100;

// 函数代码块1
int main(void)
{
    printf("%d\n", a); // 输出100
    int a = 200;
    printf("%d\n", a); // 输出200
   
    // 代码块2 
    {
        printf("%d\n", a); // 输出200
        int a = 300;
        printf("%d\n", a); // 输出300
    }
    printf("%d\n", a); // 输出200
}

void f()
{
    printf("%d\n", a); // 输出100
}

static关键字

C语言的一大特色,是相同的关键字,在不同的场合下,具有不同的含义。static关键字在C语言中有两个不同的作用:

  1. 可见范围设定为标识符所在的文件(缩小可见范围)
    • 修饰全局变量:使得全局变量由原来的跨文件可见,变成仅限于本文件可见
    • 修饰普通函数:使得函数由原来的跨文件可见,变成仅限于本文件可见
  1. 将存储区域设定为数据段:
    • 修饰局部变量:使得局部变量由原来存储在栈内存,变成存储在数据段
  • 示例:
       int a; // 普通全局变量,跨文件可见
static int b; // 静态全局变量,仅限本文件可见

void f1()        // 普通函数,跨文件可见
{}

static void f2() // 静态函数,仅限本文件可见
{}

int main()
{
           int c; // 普通局部变量,存储于栈内存
    static int d; // 静态局部变量,存储于数据段
}

基本概念

C语言中,变量都是有一定的生存周期的,所谓生存周期指的是从分配释放时间间隔。为变量分配内存相当于变量的诞生,释放其内存相当于变量的死亡。从诞生到死亡就是一个变量的生命周期。

根据定义方式的不同,变量的生命周期有三种形式:

1. 自动存储期 --> 栈

2. 静态存储期 --> 数据段

3. 自定义存储期 --> 堆

自动存储期

内存中分配的变量,统统拥有自动存储期,因此也都被称为自动变量。这里自动的含义,指的是这些变量的内存管理不需要开发者操心,都是全自动的:在变量定义处自动分配,出了变量的作用域 { 变量所处的大括号 } 后自动释放

  • 以下三个概念是等价的:
    • 自动变量:从存储期的角度,描述变量的时间特性。
    • 临时变量:同上。
    • 局部变量:从作用域的角度,描述变量的空间特性

可以统一把它们称为变量,下面是示例代码:

int main()
{
    int a, b;     // 自动存储期
    static int c;  // 【数据段】 静态存储期
    
    f(a, b);
}

void f(int x, int y) //  x , y 自动存储期
{
}

静态存储期

        在数据段中分配的变量,统统拥有静态存储期,因此也都被称为静态变量。这里静态的含义,指的是这些变量的不会因为程序的运行而发生临时性的分配和释放,它们的生命周期是恒定的,跟整个程序一致。

  • 静态变量包含:
    • 全局变量:不管加不加 static,任何全局变量都是静态变量
    • static 型局部变量
  • 示例代码:
int g1;        // 静态存储期
static int g2; // 静态存储期

int main()
{
    int a, b;
    static int c; // 静态存储期
}
  • 注意1:
    • 若定义时未初始化,则系统会将所有的静态数据自动初始化为0 .bss
    • 静态数据初始化语句,只会执行一遍。
    • 静态数据从程序开始运行时便已存在,直到程序退出时才释放
  • 注意2:
    • static修饰局部变量:使之由内存临时数据,变成了静态数据,存储于数据段中
    • static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。

自定义存储期

        在中分配的变量,统统拥有自定义存储期,也就是说这些变量的分配和释放,都是由开发者自己决定的。由于堆内存拥有高度自治权,因此堆是程序开发中用得最多的一片区域

  • 相关API:
    • 申请堆内存:malloc() / calloc() / realloc () / reallocarray()
    • 清零堆内存:bzero()
    • 释放堆内存:free()
  • 示例:
int *p = malloc(sizeof(int)); // 申请1块大小为 sizeof(int) 的堆内存
bzero(p, sizeof(int));        // 将刚申请的堆内存清零

*p = 100; // 将整型数据 100 放入堆内存中
free(p);  // 释放堆内存

// 申请3块连续的大小为 sizeof(double) 的堆内存
double *k = calloc(3, sizeof(double));

k[0] = 0.618;
k[1] = 2.718;
k[2] = 3.142;
free(k);  // 释放堆内存
  • 注意:
    • malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零。
    • calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
    • free()只能释放内存,不能释放别的区段的内存。
  • 释放内存的含义:
    • 释放内存意味着将内存的使用权归还给系统
    • 释放内存并不会改变指针的指向。(出现了野指针)
    • 释放内存并不会对内存做任何修改,更不会将内存清零

总结

        本文细讲了打怪路上的函数BOSS和作用域沙城,各位只需认真学习,即可消灭攻破它们。祝各位都可爬上C语巅峰,斩尽拦路小妖。

        本文参考 粤嵌文哥 的部分课件,经过整理和修改后发布在C站。如有转载,请联系本人

相关推荐

  1. js作用函数以及解构赋值

    2023-12-07 17:44:03       21 阅读
  2. C语言作用存储简单介绍

    2023-12-07 17:44:03       57 阅读
  3. Python学习-函数

    2023-12-07 17:44:03       65 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2023-12-07 17:44:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2023-12-07 17:44:03       100 阅读
  3. 在Django里面运行非项目文件

    2023-12-07 17:44:03       82 阅读
  4. Python语言-面向对象

    2023-12-07 17:44:03       91 阅读

热门阅读

  1. fastapi实现websocket在线聊天

    2023-12-07 17:44:03       64 阅读
  2. Redis雪崩

    2023-12-07 17:44:03       58 阅读
  3. 【重点】【双指针】42. 接雨水

    2023-12-07 17:44:03       58 阅读