Day 5

Day 5

数组

线性表和数组

为了帮助那些未曾深入研究《数据结构》的同学,我们首先要明确数据结构中的两个核心概念:

  1. 逻辑结构:描述数据元素之间的逻辑关系。常见的如:
    1. 线性表:其中元素之间存在一对一的关系。
    2. 树型结构:一个层次结构的节点集合,其中n个节点(n≥0)有明确的上下级(父子)关系。
  2. 物理结构:描述数据元素如何在存储介质上物理存放。例如:
    1. 数组:线性表的一种常见物理实现,要求元素在内存中连续存储。
    2. 链表:线性表另一种常见物理实现,元素在内存中可以不连续,但每个元素指向下一个,形成链式结构。

数组是一种在内存中需要一片连续空间,连续存储数据的数据结构。

随机访问

了解数组的随机访问特性首先需要明白**“随机访问”**这一概念:

随机访问:指的是能够直接、立即访问序列中的任何一个元素,而不需要先访问其他元素。换句话说,访问第n个元素不依赖于序列中的其他元素。

非随机访问:相反,它意味着为了访问第n个元素,必须首先遍历前面的n-1个元素。

很明显,随机访问的效率要比非随机访问高很多,从时间复杂度(时间复杂度是指执行算法所需要的计算工作量)上来说:

  1. 随机访问是常数级别的访问效率,即O(1)
  2. 非随机访问的访问效率是O(n)

数组的核心优势就是其随机访问能力,它允许我们通过索引直接、迅速地访问数组中的任意元素。

数组优缺点

数组的优点:

  1. 随机访问:由于连续内存分配,数组可以在O(1)时间复杂度内通过索引直接访问任意元素。这是数组最大的优势。
  2. 内存使用:连续的内存结构意味着较少的内存碎片。
  3. 简洁性:在C语言中,数组的使用和操作都相对直接和简单。

数组的缺点:

  1. **固定大小:**在C语言中,普通数组的大小在声明时确定,之后不能更改。这是数组最大的限制,它使用一般数组在处理动态数据时很不灵活。
  2. 插入/删除困难:向数组的中间位置插入或删除元素需要移动其他元素,效率比较低。
  3. 类型限制:C语言的数组只能存储同一数据类型的元素。

注:C/C++都可以通过动态的内存分配,在程序运行时创建一个动态长度的数组,这在一定程度上弥补了普通数组灵活性差的缺点。


数组是如何实现随机访问的呢?(重点)

首先,数组实现随机访问的关键在于其数据结构的连续内存分配以及固定的元素大小

  1. 连续内存分配:当声明一个数组时,例如**“int arr[10];”**,运行时操作系统为该数组分配10个连续存储的int空间。因此,你有一块连续的内存,每个int元素都紧密地排列在一起。
  2. 固定元素大小:数组中的每个元素都具有相同的大小。例如,对于一个int类型的数组,(一般)每个元素都占用4字节的空间。

上述两点意味着数组中元素的地址,均匀可以连续计算,是实现随机访问必不可少的前提。

了解上述前提后,我们还需要知道两个概念:基地址与偏移量,我们要想随机访问数组的某个元素,就需要知道目标元素的地址。

[目标元素的地址] = 数组基地址 + 偏移量,其中:

  1. 数组的基地址,即首元素的地址,也是数组变量的地址。实际上,数组名在内存中就代表该数组的基地址。
  2. 偏移量指的是目标元素地址和首元素地址的字节差值。恰好这个字节差值就等于:[目标元素的下标i] * [sizeof_element] (思考一下为什么?)

于是我们就得到一个计算目标元素地址的,寻址公式

eaddress(arr[i]) = base_address(arr) + i * sizeof_element

假如我们有一个长度为5的int数组,其基地址是0x0053fbd4,要访问第五个元素,即arr[4],那么根据寻址公式就可以计算出地址:

address_arr[4] = 0x0053fbd4 + (4 * 4) = 0x0053fbe4

所以,每次你用**"arr[4]"这样的语法访问数组时,程序会直接访问0x0053fbe4**这个地址来获取或设置元素值,不再需要查找或遍历,效率非常高,这就是随机访问的魅力。


数组长度声明

在C语言当中,声明一个普通数组时,数组的长度/大小必须是一个编译时常量。这意味着数组的长度必须是一个在编译时期确定大小的常量,这一般是:

  1. 整数字面值常量(以及它的表达式)
  2. 宏常量

一般在标准C代码中,为了更好的可读性和扩展性,我们更推荐使用宏常量作为数组长度的表示。例如:

#define ARR_LEN 10
int arr[ARR_LEN];

而那些需要在程序运行时期确定大小的变量,const修饰的只读变量(不是编译时常量),都是不能作为数组长度的

int len = 10;
const int len2 = 20;
int arr[len];   // ERROR
int arr2[len2];   // ERROR

除此之外,C语言也不允许数组的长度是0,以下声明是错误的:

数组元素的循环遍历

注:

  1. **sizeof(arr)**计算整个数组占用的内存大小。
  2. **sizeof(arr[0])**计算数组中单个元素的内存占用大小。

为了进一步简化和增强代码的可读性,我们可以定义一个函数宏来计算数组长度:

#define ARR_LEN(arr) (sizeof(arr) / sizeof(arr[0]))
...
for (i = 0; i < ARR_LEN(arr); i++){
  printf("%d", arr[i]);
}

**在实际开发中,建议使用这种方法来获取数组的长度,增强了代码的可读性和可维护性。**这是一个常见的惯用法。
在这里插入图片描述

二维数组循环遍历

行优先遍历的方式是更推荐,因为它效率更高

列优先遍历方式也可以,但不常用

注意代码当中的宏定义,当然你可以使用宏函数来计算数组长度,但对于二维数组这会稍嫌麻烦一些,如下:

#define ARRAY_LENGTH(array) (sizeof(array) / sizeof((array)[0]))
...

int ROWS = ARRAY_LENGTH(arr);
int COLS = ARRAY_LENGTH(arr[0]);

for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        printf("%d ", arr[i][j]);
    }
    printf("\n");
}

循环过程中,要注意边界值,谨防不合法的索引出现。

const关键字

在C语言中,const是一个特定的关键字。简单来说,你可以认为它就是把一个变量变成常量,也就是const常量。

基础的,你可以在变量声明时使用 const 关键字,这样就定义了一个常量:

const int a = 10;
a = 20;  // 编译错误

const关键字还常用于修饰数组,这就是常量数组。

深入理解const关键字

一个const关键字声明的变量(const常量),实际上仍然不能作为数组声明时的长度:

const int len = 20;
int arr[len];   // ERROR

这是因为const声明的常量,并不是一个总是能够在编译时期确定取值的常量。在很多时候,C语言允许const常量在运行时确定取值。例如:

int func(){
return 200;
}

const int a = 100;  // 编译时期确定
const int b = func();   // 运行时确定

但不管是何时确定,一个const常量在确定取值后就不可通过变量名修改取值了。因为const常量的这种特点,很多C程序员会把它称之为"只读变量",这确实很合理合适。

实际上,从更深层的角度理解const,const关键字只是C语言编译时期语法层面的限制罢了:你无法通过变量名去修改一个const常量的取值,否则就会编译失败,仅此而已。

所以const实际上也是一个比较"坑爹"的语法,程序员其实有很多办法在运行时期修改const常量的取值,思考一下有哪些?(考虑指针)

    /* 思考:
	* 如何在运行时修改const常量的取值呢?
	* 只要通过地址操作,都是可以改变的
	* 比如指针,比如scanf
	* 但实际情况下,不要这么做
	*/
	// int arr[a];
	const int b = 10;
	// b = 20;
	scanf("%d", &b);

	int* p = &b;
	*p = 30;

	return 0;
}

常量数组

const除了用来修饰基本数据类型变量,也可以和数组结合,创建出一个常量数组。这意味着一旦该数组被初始化,其元素就不能再被修改了:

const int array[5] = {1, 2, 3, 4, 5};
array[0] = 10;  // 编译错误

常量数组在编程中尤为有价值,以下是一些使用常量数组的典型场景:

  1. **固定数据集:**当你有一组不会改变的数据,使用常量数组是理想的选择。例如,存储一年12个月份英文单词的数组。
  2. **防止误操作:**在某些情况下,数据的意外修改可能会导致严重的后果。使用常量数组可以确保这些数据在程序执行期间保持不变,从而避免潜在的问题。

函数

函数定义的细节问题

返回值类型:

  1. C语言不允许函数返回数组类型。
  2. 在C90标准中,如果省略了函数的返回值类型,编译器默认其为int类型。但从C99标准开始,这种做法已不再被支持。因此现代C编程中建议总是明确指定返回值类型。

形参列表:

  1. 形式参数(形参)组成的列表。形参的语法形式为:“数据类型 形参名”,多个形参间用";"分隔。
  2. 形参列表可以是空的,表示函数不需要任何外部输入。在C语言中,推荐使用void取代空形参,以明确地表示函数不接受任何参数。
  3. 形参列表中的数据类型,决定了调用函数时所需的实际参数的类型。例如,int a表明需要一个整数型参数。
  4. 形参列表中的形参名,决定了参数传入函数后,如何去使用此参数。
  5. 形参列表中的数据类型、形参个数、顺序都会影响此函数的调用,而形参名则不会影响函数调用。

函数调用

在C语言中,函数调用语句可以看作一条表达式语句,它同样有主要作用和副作用:

  1. **主要作用。**函数调用的主要作用一般就是返回值。虽然你可以选择不处理这个返回值,但最佳实践是接收、处理或使用此返回值。如果函数的返回类型为void,则这样的函数调用没有主要作用,因为它不返回任何值。
  2. **副作用。**函数体当中进行的任何操作都可以看成是函数调用的副作用。比如键盘录入、屏幕打印等IO操作、修改全局变量等外部变量的赋值操作。函数调用并不一定就有副作用,比如上面的代码示例,就没有任何副作用。

总之一个函数调用具有两大潜在效应:主要作用和副作用:

  1. 为了充分利用函数的主要作用,调用者应当主动接收并处理函数的返回值。
  2. 若一个函数调用后仅以分号“;”结尾,并未进一步处理其返回值,这意味着调用者关注该函数的副作用。
  3. void函数没有主要作用,调用它需要关注函数调用的副作用。

函数的定义位置影响函数调用

首先我们要明确C程序编译的特点:C编译器是从上到下逐行编译代码的。

当编译器遇到函数调用语句时,它会查找该函数的声明或定义。如果在调用处之前未找到函数的声明或定义,编译器将报错,因为它不知道这个函数的细节。

总之,应对针对C语言的这一特性,建议采用以下策略:

  1. 始终将函数定义放在其调用之前。
  2. 在函数调用前使用函数的声明语法,事先声明此函数。

函数的声明

注意:

  1. **函数的声明和定义是完全不同的概念,大家要注意区分。**函数声明告诉编译器有这样一个函数,但不提供具体实现;函数定义则提供实现。规范的C程序代码,应该在调用函数前,声明或定义该函数。
  2. 函数在定义时,形参列表必须带上形参名, 但函数在声明时可以不带形参名(但加上形参名也有利于理解函数)。
  3. 从语法上讲,一个函数可以在多处声明,但只能定义一次。

函数设计的原则

我们在设计一个程序中的函数时,最重要的有两个原则:

  1. “单一性原则”。一个函数的功能,应该越单一越好。
  2. “性能优化原则”。一个函数的性能,在可能的情况下越高效越好。

通俗地说,一个函数应该只做自己的一件事情,并且尽力做得好。

遵循单一原则,使得函数代码可读性更强,易于维护。而且单一功能的函数,也更容易被复用。

而性能优化就直接关乎用户体验和程序的运行成本,也是非常重要的。

常见变量分类

学习到这里,也是时候学习总结一下C语言中几种常见的变量类型了。

根据变量的定义位置、存储位置、存储期限(生命周期)、作用域等的不同,C语言中的变量主要有以下几种类型:

  1. 局部变量 (Local Variable)
  2. 全局变量 (Global Variable)
  3. 静态局部变量 (Static Local Variable)

在这些变量当中,最常见常用的当属局部变量,但剩下几种变量类型也需要我们掌握。

局部变量

在C语言中,局部变量就是在函数当中定义的变量,它最主要的特点就是只在声明它的"{}"内部有效。

存储位置以及生命周期(重要)

操作系统为每个执行的C程序(进程)分配虚拟内存空间,局部变量存储在一片被称之为"栈(stack)"的内存区域。

栈的工作原理是后进先出,这意味着最后放入栈的项是第一个被移除的。

在C程序的运行过程中,每当一个函数被调用,系统会为其创建一个**“栈帧”来存储该函数的执行上下文,并将这个栈帧压入栈底部,这个过程称为函数进栈(push)**。一个函数被调用,就是函数栈帧进栈的的过程。

栈帧中会存储此函数的局部变量(包括形式参数)。

当函数开始执行,对应的栈帧被压入栈顶时,局部变量得以初始化并生效。随着函数执行完毕,栈帧从栈顶中弹出,此时,函数内的局部变量也随之被销毁,这个过程称为函数出栈(pop)

所以局部变量的生命周期与包含它们的函数的生命周期是一致的:

  1. 当函数被调用时,其局部变量被创建;
  2. 当函数返回时,这些变量被销毁。

在C语言当中,这种**“依托于变量存储空间单元存在而存在"的变量生命周期形式被称为"自动存储期限”**。局部变量的自动存储期限,依赖于函数调用栈。

如下列代码案例:

#include <stdio.h>

void test(void);
void test2(void);

int main(void) {
  printf("main before\n");
  int a = 10;
  test();
  printf("mian after\n");
  return 0;
}

void test(void) {
  printf("test before\n");
  int a = 20;
  test2();
  printf("test after\n");
}

void test2(void) {
  printf("test2 before\n");
  int a = 30;
  printf("test2 after\n");
}

函数调用的过程是:

  1. main函数先调用,栈帧先进栈
  2. main函数中调用test函数,test函数栈帧进栈
  3. test函数中调用test2函数,test2函数栈帧进栈

也就是说,**函数调用的顺序(也就是函数栈帧进栈的顺序)**是:

main —> test —> test2

但函数调用结束的顺序却恰好相反,依赖于栈的先进后出的特点,**函数调用结束的顺序(也就是函数栈帧出栈的顺序)**是:

test2—> test —> main

越晚调用的函数,越早调用结束。最先调用的函数,最晚结束。事实上,main函数最先调用,它是程序的入口,main函数执行完毕意味着程序就结束,所以它也确实最晚结束。

以上过程的示意图如下(栈区从高地址向低地址生长):

在这里插入图片描述

三个函数栈帧都有一个局部变量a,但很明显它们之间是相互独立的。理解这一点,对于理解局部变量非常有帮助。

在这里插入图片描述

局部变量作用域

在C语言中,局部变量的作用域

  1. 开始:局部变量的作用域从其声明的位置开始。
  2. 结束:局部变量的作用域到该变量所在的块或函数结束为止。通俗的说,就是到定义此局部变量的大括号结束。比如:
    1. 如果一个局部变量在函数体中被声明,那么它的作用域从声明的位置开始,直到函数结束为止。
    2. 如果局部变量在一个块(如if语句、for循环、while循环、switch语句等)内部被声明,它的作用域只在该块及嵌套在此块内的其他块中。

这里有一个小的细节,C语言的代码块"{}“具有遮蔽外层”{}"作用域的功能。比如:

void foo(void){
  int a = 10;
  if(1){
    // int a = 20;    // 这行代码在C语言中是允许的,if的{}具有遮蔽外出foo函数体{}作用域的功能
    printf("%d", a);  // 如果不放开上面的注释a = 10,放开后a = 20,且两个a不是一回事
  }
  // foo中定义的a在这里依然有效,但如果是if中的a这里即失效
}

这也是C语言语法比较奇特的地方,{}复合语句具有自身新的、独立的作用域,又和外层共享作用域。

请看以下代码示例:

#include <stdio.h>

void foo() {
    int a = 10;  // a的作用域从这里开始

    if (a == 10) {
        // int a = 20;  // 再次声明a允许的,此时if的{}会屏蔽外层的a
        int b = 20;  // b的作用域从这里开始
    
        // 在这里,外层的a和b都可以被访问。但如果内层自己声明了a,那么将访问自身的a
        printf("%d, %d\n", a, b);
    
    }  // b的作用域在这里结束
    
    int b = 20;   // 再次声明b也是可以的,因为if中的b已经结束作用域了
    
    // 在这里只有a可以被访问
    printf("%d\n", a);

}  // a的作用域在这里结束

int main() {
    foo();
    // 在这里,既不能访问a也不能访问b
    return 0;
}

总之,局部变量的作用域就限定在声明它的"{}"内部,具体而言:

  1. 从变量的声明点开始,直到所在代码块"{}"结束,该局部变量都是可访问的。
  2. 在此作用域内,该局部变量是唯一的,不可被重复定义。但如果作用域内又定义了一个新的复合语句{},则会出现作用域屏蔽的情况。

总结

在 C 语言中,局部变量是在函数内部定义的变量,它只在该函数的作用域内可见和可用。使用局部变量时,总结如下:

  1. 在C语言中,局部变量就是声明定义在函数体内部的变量。
  2. 局部变量不会自动初始化。如果一个局部变量仅有声明,那么它们的初始值是未定义的,通常称为**“垃圾值,随机值”**。因此,局部变量在使用之前必须手动初始化它。
  3. 局部变量的生命周期是自动存储期限,意味着它们仅在声明它们的函数调用期间存活,函数返回后它们就随栈帧销毁。
  4. 局部变量的作用域就是仅限于"{}“内部,实际使用时要注意”{}"的起止。
  5. "{}“嵌套后,内层”{}"会共享外层大括号中的变量。但也可以屏蔽外层大括号作用域,以定义自身独立局部变量。

全局变量

在C语言中,全局变量也是一种特别常见的变量类型,有时它也被称为外部变量。

这是因为它们在函数之外被定义,并且可以在整个文件内,甚至其他文件中(通过外部链接)被访问和使用。

定义位置

只要在一个文件内,所有函数的外部,直接声明定义的变量都是全局变量。

存储位置以及生命周期

全局变量被存储在虚拟内存空间当中,一片被称之为**“数据段”**的内存区域当中。

不同于局部变量随着函数的调用和返回被创建和销毁,全局变量的生命周期从程序开始执行时开始,持续到程序完全结束。

简而言之,全局变量与程序的生命周期同步:它们在程序开始时被创建并初始化,并在程序结束时被销毁。

在C语言中,这种持续存在于程序整个执行周期的生命周期特性被称为**“静态存储期限”**。

初始化

当C程序开始执行时,会在程序加载时为数据段中的变量进行初始化,包括全局变量。此时:

  1. 如果全局变量已由程序员明确初始化赋值,该初始值会直接在程序启动时分配给它。
  2. 若程序员未进行显式初始化赋值,系统则会为其设置默认值(也就是0值),如:整型和浮点型变量默认值为0,指针变量默认值为NULL。

注意:

  1. 除非不确定此全局变量的初始值,否则建议对其进行手动初始化。
  2. 全局变量的初始化有且仅有程序启动时的一次。
作用域

**全局变量的作用域从"声明位置"开始,并延申至整个程序。**具体来说:

  1. 在定义全局变量的文件内,全局变量可以在其声明之后的任何位置被访问和修改。
  2. 要想在其他源文件中使用该全局变量,可以通过extern关键字来引用它。

例如,在一个main.c文件中定义全局变量:

#include <stdio.h>

int global_num = 10;  // 从此处开始,整个文件都可以访问和修改global_num

int main(void) {
  printf("在main.c文件中,打印全局变量的值为%d\n", global_num);
  return 0;
}

如果想在demo.c文件中使用此全局变量,使用extern关键字,演示代码如下:

#include <stdio.h>
// 使用extern声明来表明global_num是在其他文件中定义的
// 从这里开始该文件中也可以使用变量global_num了
extern int global_num;  

void print_global(void) {
  printf("在demo.c文件中,打印全局变量的值为%d\n", global_num);
}

为了在main.c文件中调用print_global函数,需要借助头文件进行函数声明,最终代码如下:

// main.c
#include <stdio.h>
#include "demo.h"

int global_num = 10;

int main(void) {
  printf("在main.c文件中,打印全局变量的值为%d\n", global_num);
  print_global();

  printf("在main.c中将全局变量的取值改为100\n");
  global_num = 100;
  print_global();
  return 0;
}

// demo.c
#include <stdio.h>
#include "demo.h"

extern int global_num;  

void print_global(void) {
  printf("在demo.c文件中,打印全局变量的值为%d\n", global_num);
}

// demo.h
void print_global(void); // 声明函数,供其他文件使用

运行此程序,结果是:

在main.c文件中,打印全局变量的值为10

在demo.c文件中,打印全局变量的值为10

在main.c中将全局变量的取值改为100

在demo.c文件中,打印全局变量的值为100

总结:

  1. global_num变量是在main.c文件中定义的,它在本文件中声明位置以下的部分,都可以随意使用。
  2. 在demo.c文件中,使用extern关键字只是告诉编译器global_num变量在其余文件中定义,它并不会创建了一个新的变量。
  3. 关于跨文件调用函数,一般步骤如下:
    1. 在一个头文件中声明你想跨文件调用的函数。
    2. 在一个源文件中包含头文件,然后定义这个函数。(包含自定义头文件,使用**#include “xx.h”**)
    3. 在另一个源文件中,包含头文件并直接调用该函数。

填坑:只属于变量声明但不属于变量定义的语句

我们再来回顾一下C语言中关于变量声明和定义的概念:

  1. 变量的声明:声明是给编译器看的,告诉编译器变量的类型和名字等信息,但变量的具体内存分配发生在运行时期。
  2. 变量的定义是声明一个变量,并且为运行时期为变量分配内存空间的组合动作。

变量的定义总是一个声明,但某些变量的声明并不是定义,也就是说某些变量的声明不会在运行时期分配内存空间。

在以往,我们见到的所有声明语句都是变量的定义,也就是在运行时期会为变量分配内存。那么在这里,我们就见到了仅声明,不分配内存空间的变量声明语句:

extern int global_num;  

这条语句就仅是一条声明语句,不是一条变量的定义语句。

global_numbianl变量的内存分配并不是在这一行代码进行的,而是在真正定义全局变量global_num的位置进行的。

在本案例中,main.c文件中的:

int global_num = 10;

这是一条变量定义的语句,会在程序运行时期分配内存。

static修饰全局变量

static修饰的全局变量的主要效果是将其作用域限制在声明它的文件中,这意味着该变量只能在它所在的源文件中被访问和修改。其它的特性,如生命周期、初始化方式和存储位置,与普通的全局变量是相同的。

比如上面的global_num全局变量,一旦使用static关键字修饰,再次运行程序,就会产生链接错误。因为此时的全局变量已经不能被链接到外部使用了。

静态全局变量

静态局部变量,简单来说,就是static修饰的局部变量。但它与一般的局部变量在很多地方都是完全不同的,下面我们来详细学习一下静态局部变量。

定义位置

静态局部变量就是在原本局部变量定义的基础上,使用static关键字声明得到的变量。比如:

void foo() {
  static int static_var = 0; // 定义了一个静态局部变量
}

存储位置及生命周期

静态局部变量的存储位置和生命周期和全局变量是一致的:

  1. 都是存储在数据段区域当中。
  2. 生命周期都是从程序启动到程序结束,都是"静态存储期限"。

这就意味着,静态局部变量与一般的局部变量不同:

静态局部变量不会随着函数的返回而销毁,它会始终保留到程序执行完毕。

初始化(重点)

不要将静态局部变量理解成"活得更长"的局部变量,局部变量和静态局部变量在初始化方面有非常大的区别。

静态局部变量的初始化特性

  1. 默认初始化:如果不显式地为静态局部变量提供一个初始值,系统会默认将其初始化为0值。
  2. 初始化只有一次:静态局部变量只会在其所在函数,第一次被调用时初始化一次。此后,不论调用几次该函数,都不会再次初始化了。

下面我们举一个代码案例,来讲解这两个特点。

#include <stdio.h>

void call_counter() {
    static int count = 0; // 静态局部变量,仅在首次调用时初始化为0,再次调用不会再次初始化
    count++; // 每次调用函数时,增加count计数器
    printf("此函数已经被调用了%d次!\n", count);
}

int main(void) {
    for (int i = 0; i < 5; i++) {
        call_counter();
    }
    return 0;
}

程序的输出结果是:

此函数已经被调用了1次!

此函数已经被调用了2次!

此函数已经被调用了3次!

此函数已经被调用了4次!

此函数已经被调用了5次!

作用域

静态局部变量的作用域仅限于其所在的函数。这意味着:

尽管静态局部变量的生命周期与程序的整个执行期间一致,但它只能在定义它的函数内部被访问。

静态局部变量的使用场景

静态局部变量,可以在同一个函数的多次调用之间进行数据保存和修改,这非常有用。也就是说,如果一个数据需要被一个函数的多次调用进行操作,那么就非常适合使用静态局部变量。

一个非常经典的例子就是"生成独立序号":

定义一个函数,每次调用该函数都生成一个独立的序号。

参考代码如下:

int get_id() {
  static int current_id = 1;  // 初始ID为1
  return current_id++;        // 返回当前ID,然后进行累加
}

int main(void) {
  printf("New ID: %d\n", get_id());  // 输出: New ID: 1
  printf("New ID: %d\n", get_id());  // 输出: New ID: 2
  printf("New ID: %d\n", get_id());  // 输出: New ID: 3
  return 0;
}

这个代码虽然很简单,但在许多实际应用中都会用到类似的机制来生成唯一独立序号。

总结

用一张表格来总结我们上面讲的变量所有知识点,请大家务必理解、牢记。

特性局部变量全局变量静态局部变量static修饰的全局变量
定义位置函数内部函数外部函数内部函数外部
存储位置数据段数据段数据段
存储期限(生命周期)从函数调用开始到函数退出为止(自动存储期限)程序启动到程序结束(静态存储期限)程序启动到程序结束(静态存储期限)程序启动到程序结束(静态存储期限)
作用域仅限于所在函数整个程序的所有文件,但在其它文件使用需要用extern声明仅限于所在函数仅限于定义它的文件
初始化时机每次函数调用时都初始化,不同函数调用初始化不同程序启动时初始化,只初始化一次。函数第一次调用时进行初始化,只初始化一次程序启动时初始化,只初始化一次。
默认初始化不手动初始化,可能得到一个随机值即便不手动初始化,也会默认初始化为0值即便不手动初始化,也会默认初始化为0值即便不手动初始化,也会默认初始化为0值
                             | 数据段                                |

| 存储期限(生命周期) | 从函数调用开始到函数退出为止(自动存储期限) | 程序启动到程序结束(静态存储期限) | 程序启动到程序结束(静态存储期限) | 程序启动到程序结束(静态存储期限) |
| 作用域 | 仅限于所在函数 | 整个程序的所有文件,但在其它文件使用需要用extern声明 | 仅限于所在函数 | 仅限于定义它的文件 |
| 初始化时机 | 每次函数调用时都初始化,不同函数调用初始化不同 | 程序启动时初始化,只初始化一次。 | 函数第一次调用时进行初始化,只初始化一次 | 程序启动时初始化,只初始化一次。 |
| 默认初始化 | 不手动初始化,可能得到一个随机值 | 即便不手动初始化,也会默认初始化为0值 | 即便不手动初始化,也会默认初始化为0值 | 即便不手动初始化,也会默认初始化为0值 |
| 是否可以被其他文件访问 | 否 | 在其它文件中使用extern关键字链接访问 | 否 | 否 |

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如是我闻艺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值