【C语言篇】函数

友情链接:C/C++系列系统学习目录

知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
 
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上


文章目录


🚀一、为什么要使用函数

  1. 首先,使用函数可以省去编写重复代码的苦差。如果程序要多次完成某项任务,那么只需编写一个合适的函数,就可以在需要时使用这个函数,或者在不同的程序中使用该函数,就像许多程序中使用 putchar()一样。
  2. 其次,即使程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而提高了程序代码的可读性,更方便后期修 改、完善。
  3. 避免“重复制造轮子”,提高开发效率
  4. 便于维护

模块化程序设计:使用函数封装代码的思路

函数就是功能。每一个函数用来实现一个特定的功能。函数的名字应反映其代表的功能。

🚀二、函数的定义

C语言要求,在程序中用到的所有函数,必须“先定义,后使用”。例如想用max函数去求两个数中的大者,必须事先按规范对它进行定义,指定它的名字﹑函数返回值类型、函数实现的功能以及参数的个数与类型,将这些信息通知编译系统。这样,在程序执行max时,编译系统就会按照定义时所指定的功能执行。如果事先不定义,编译系统怎么能知道max是什么、要实现什么功能呢!

许多程序员喜欢把函数看作是根据传入信息(输入)及其生成的值或响应的动作(输出)来定义的“黑盒”。如果不是自己编写函数,根本不用关心黑盒的内部行为。例如,使用printf()时,只需知道给该函数传入格式字符串或一些参数以及 printf()生成的输出,无需了解 printf()的内部代码。以这种方式看待函数有助于把注意力集中在程序的整体设计,而不是函数的实现细节上。因此,在动手编写代码之前,仔细考虑一下函数应该完成什么任务, 以及函数和程序整体的关系。

定义函数应包括以下几个内容:

  1. 指定函数的名字,以便以后按名调用。
  2. 指定函数的类型,即函数返回值的类型。
  3. 指定函数的参数的名字和类型,以便在调用函数时向它们传递数据。对无参函数不需要这项。
  4. 指定函数应当完成什么操作,也就是函数是做什么的,即函数的功能。这是最重要的,是在函数体中解决的

对于C编译系统提供的库函数,是由编译系统事先定义好的,库文件中包括了对各函数的定义。程序设计者不必自己定义,只须用#include指令把有关的头文件包含到本文件模块中即可。在有关的头文件中包括了对函数的声明。例如,在程序中若用到数学函数(如sqrt,fabs,sin,cos等),就必须在本文件模块的开头写上:

include <math.h>

函数的设计方法:

  1. 先确定函数的功能
  2. 确定函数的参数,是否需要参数,参数的个数,参数的类型
  3. 确定函数的返回值是否需要返回值,返回值的类型
  4. 确定函数名,函数名, 一定要顾名思义.
  5. 函数名的命名方法, 和变量名相同
  6. 函数的实现

⛳(一)定义无参函数

类型名 函数名()
{
	函数体
}

//例如

void test(void)
{
	函数体
}

第一个void表示函数返回值为空,函数名后面括号内的void表示“空”,即函数没有参数。

⛳(二)定义有参函数

类型名 函数名(形式参数列表)
{
	函数体
}

//例如:
int max(int x,int y)
{
    int z;
    z = x>y? x:y;
    return z;
}

该行告知编译器max()使用两个参数x和y,x是int类型, y也是int类型。这两个变量被称为形式参数,简称形参。return(z)的作用(可以带括号也可以不带)是指定将z的值作为函数值(称函数返回值)带回到主调函数。

拓展:

1.ANSI C要求在每个变量前都声明其类型。也就是说,不能像普通变量声明那样使用同一类型的变量列表:

void dibs(int x, y, z) /* 无效的函数头 */
void dubs(int x, int y, int z) /* 有效的函数头 */

2.ANSI C也接受ANSI C之前的形式,但是将其视为废弃不用的形式:

void show_n_char(ch, num) char ch; int num; {

}

//如果变量是同一类型,这种形式可以用逗号分隔变量名列表,如下所示:
void dibs(x, y, z) int x, y, z; /* 有效 */{

}

这里,圆括号中只有参数名列表,而参数的类型在后面声明。注意,普通的局部变量在左花括号之后声明,而上面的变量在函数左花括号之前声明。

⛳(三)定义空函数

类型名 函数名()
{
}

//例如:
void example()
{    
}

为什么要特地强调定义一个空函数呢:

在程序设计中往往根据需要确定若干个模块,分别由一些函数来实现。而在第1阶段只设计最基本的模块,其他一些次要功能或锦上添花的功能则在以后需要时陆续补上。在编写程序的开始阶段,可以在将来准备扩充功能的地方写上一个空函数(函数名取将来采用的实际函数名(如用merge( ) , matproduct() , concatenate()和 shell()等,分别代表合并、矩阵相乘、字符串连接和希尔法排序等),只是这些函数暂时还未编写好,先用空函数占一个位置,等以后扩充程序功能时用一个编好的函数代替它。这样做,程序的结构清楚,可读性好,以后扩充新功能方便,对程序结构影响不大。空函数在程序设计中常常是有用的。

🚀三、函数的调用和声明

⛳(一)函数的调用

定义函数的目的是为了调用此函数,函数调用表明在此处执行函数,以得到预期的结果。

函数调用的三种方式:

  1. 函数调用语句:把函数调用单独作为一个语句,如:printf();

  2. 函数表达式:函数调用出现在另一个表达式中,如:c = 2 * max(a,b);

  3. 函数参数:函数调用作为另一个函数调用时的参数,如:m = max(a,max(b,c));

⛳(二)函数的声明

函数的声明:

函数的声明和函数定义中的第1行(函数首部)基本上是相同的,只差一个分号(函数声明比函数定义中的首行多一个分号)。因此写函数声明时,可以简单地照写已定义的函数的首行,再加一个分号,就成了函数的“声明”。函数的首行(即函数首部)称为函数原型(function prototype),所以全称叫函数原型的声明,例如:

void starbar(void);   /* 函数原型 */

void starbar(void) /* 定义函数 */
{
    int count;
    for (count = 1; count <= WIDTH; count++)
    putchar('*');
    putchar('\n');
}

函数原型(function prototype)告诉编译器这是一个函数;函数调用(function call)表明在此处执行函数; 函数定义(function definition)明确地指定了函数要做什么。

一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。 这些信息称为该函数的签名(signature),对于以下starbar()函数而言,其签名是该函数没有返回值,没有参数。

拓展:

1.声明函数用逗号分隔的列表指明参数的数量和类型。在函数声明中的形参名可以省略,而只写形参的类型

2.在原型中使用变量名并没有实际创建变量

3.在ANSI C标准之前,声明函数的方案有缺陷,只需要声明函数的类型,不用声明任何参数。这种方法如果调用函数时使用的参数个数不对或类型不匹配,编译器根本不会察觉出来。

在一个函数中(主调函数)调用另一个函数(即被调用函数)需要具备如下条件:

  1. 被调函数是已经定义的函数(库函数或用户自己定义的函数)
  2. 如果使用库函数,应该在本文件开头用#include指令将调用有关库函数时所需用到的信息“包含”到本文件中来。例如,前几章中已经用过的指令:include <stdio.h>
  3. 如果使用用户自己定义的函数,而该函数的位置在调用它的函数(即主调函数)的后面(在同一个文件中),应该在主调函数中对被调用的函数作声明(declaration)。声明的作用是把函数名,函数参数的个数和参数类型等信息通知编译系统﹐以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法。

在这里插入图片描述

🚀四、函数参数

⛳(一)形式参数和实际参数

定义函数时函数名后面括号中的变量名称为“形式参数”(简称“形参”)或“虚拟参数”。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”(简称“实参”)。实际参数可以是常量,变量或表达式。

🎈1.实参和形参间的数据传递

调用函数时,形参被赋值为对应的实参, 实参本身不会受到函数的影响!

在这里插入图片描述

🎈2.函数调用的过程
  1. 在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。在发生函数调用时,函数max的形参才被临时分配内存单元

  2. 将实参的值传递给对应形参。

  3. 在执行max函数期间,由于形参已经有值,就可以利用形参进行有关的运算

  4. 通过return语句将函数值带回到主调函数。返回值的类型与函数类型一致。

  5. 如果函数不需要返回值,则不需要return语句。这时函数的类型应定义为void类型。

  6. 调用结束,形参单元被释放。注意:实参单元仍保留并维持原值,没有改变。如果在执行一个被调用函数时,形参的值发生改变,不会改变主调函数的实参的值。

    注意:实参向形参的数据传递是“值传递”,单向传递,只能由实参传给形参,而不能由形参传给实参。实参和形参在内存中占有不同的存储单元,实参无法得到形参的值。

⛳(二)使用数组作为函数参数

调用有参函数时,需要提供实参。例如sin(x), sqrt(2.0) , max(a,b)等。实参可以是常量、变量或表达式。数组元素的作用与变量相当,一般来说﹐凡是变量可以出现的地方﹐都可以用数组元素代替。

  • 因此,数组元素也可以用作函数实参,其用法与变量相同,向形参传递数组元素的值。
  • 此外,数组名也可以作实参和形参,传递的是数组第一个元素的地址。
🎈1.数组元素作实参

数组元素可以用作函数实参,但是不能用作形参。因为形参是在函数被调用时临时分配存储单元的,不可能为一个数组元素单独分配存储单元(数组是一个整体,在内存中占连续的一段存储单元)。在用数组元素作函数实参时,把实参的值传给形参,是“值传递”方式。数据传递的方向是从实参传到形参,单向传递。

🎈2.一维数组名作函数参数

除了可以用数组元素作为函数参数外,还可以用数组名作函数参数(包括实参和形参)。

注意:用数组元素作实参时,向形参变量传递的是数组元素的值,而用数组名作函数实参时,向形参(数组名或指针变量)传递的是数组首元素的地址。

float average(float array[10]);						//函数声明

float average( float array[1o])						//定义average函数				
{
    int i;
    float aver,sum=array[o];
    for(i=1;i≤10;i十+)
    	sum=sum+array[i];							//累加学生成绩
    aver=sum/10;
    return(aver);
}
  1. 注意主调函数(这里是main函数)与被调用函数(average)的位置,如果主调函数在被调用函数定义前,要记得声明函数
  2. 实参数组与形参数组类型应一致(今都为float型),如不一致,结果将出错。
  3. 在定义average函数时,声明形参数组的大小为10,但在实际上,指定其大小是不起任何作用的,因为C语言编译系统并不检查形参数组大小,只是将实参数组的首元素的地址传给形参数组名。临时分配的形参数组空间,形参数组首元素(array[O])和实参数组首元素(score[o])具有同一地址,它们共占同一存储单元, score[n]和 array[n]指的是同一单元。score[n]和array[n]具有相同的值。
  4. 形参数组可以不指定大小,在定义数组时在数组名后面跟一个空的方括号
🎈3.多维数组名作函数参数

多维数组元素可以作函数参数,这点与前述的情况类似。

可以用多维数组名作为函数的实参和形参,在被调用函数中对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小说明。例如:

int array[3][10];
//或:
int array[][10];

二者都合法而且等价。但是不能把第 ⒉维以及其他高维的大小说明省略。

🚀六、函数的栈空间

当调用一个函数时,就会在栈空间,为这个函数,分配一块内存区域, 这块内存区域,专门给这个函数使用。 这块内存区域,就叫做“栈帧”。

在这里插入图片描述

🚀七、函数的嵌套和内联、递归函数

⛳(一)函数的嵌套

C语言的函数定义是互相平行,独立的,也就是说,在定义函数时,一个函数内不能再定义另一个函数,即不能嵌套定义,但可以嵌套调用函数,即在调用一个函数的过程中,又调用另一个函数,

在这里插入图片描述

⛳(二)内联函数

内联函数的用法:

inline int add(int a, int b)
{ 
	return a + b; 
}

普通函数的缺点: 每调用一次函数,就会为这个函数分配一个“栈”, 在计算机底层做很多准备工作(保护原来的执行环境,切换到新的执行环境) 有一定的“时间开销”,解决方案: 使用内联函数

当编译器在编译时, 如果遇到内联函数, 就会直接将整个函数体的代码插入”调用处”, 就相当于内联函数的函数体, 在调用处被重写了一次。 以避免函数调用的开销, 获得更快的时间

内联函数的缺点: 使调用内联函数的程序,变得“臃肿”,消耗主调函数的“栈”空间。

标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。因此,最简单的方法是使用函数说明符 inline 和存储类别说明符 static。

并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。

内联函数的使用场合:

  1. 内联函数中的代码应该只是很简单、执行很快的几条语句。
  2. 这个函数的使用频度非常高,比如在一个循环中被千万次地使用。

C99新增inline关键字时,它是唯一的函数说明符(关键字extern和 static 是存储类别说明符,可应用于数据对象和函数)

C11新增了第2个函数说明符_Noreturn:

表明调用完成后函数不返回主调函数。exit()函数是 _Noreturn 函数的一个示例,一旦调用exit(),它不会再返回主调函数。注 意,这与void返回类型不同。void类型的函数在执行完毕后返回主调函数, 只是它不提供返回值。

⛳(三)递归函数

在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。C语言的特点之一就在于允许函数的递归调用。

  1. 设计递归函数的要点:把问题拆解成问题本身, 但是拆解后的问题的”规模”更小, 或者难度更低.
  2. 再定义递归函数时,一定要确定一个“结束条件”!!!
  3. 递归函数的优点:递归为某些编程问题提供了最简单的解决方案
  4. 递归函数的缺点: 性能很低!!! ,会快速消耗计算机资源,实际开发中, 极少使用

在这里插入图片描述

尾递归:

最简单的递归形式是把递归调用置于函数的末尾,即正好在 return 语句之前。这种形式的递归被称为尾递归(tail recursion),因为递归调用在函数的末尾。尾递归是最简单的递归形式,因为它相当于循环。

经典实例:斐波那契数列、汉诺塔问题

/*
斐波那契数列
1, 1, 2, 3, 5, 8, 13, 21, .... 计算第 n 个数是多少?

//分析:
f(n)
当 n >2 时,f(n) = f(n-1) + f(n-2)
当 n=1 或 n=2 时, f(n)就是 1
f(8) = f(7) + f(6)


//实现:
int fib(int n) {
    int s;
    
    if (n == 1|| n == 2) {
    	return 1;
    }
    
    s = fib(n-1) + fib(n-2);
    
    return s;
}
*/

🚀八、变量

在一个函数中定义的变量,在其他函数中能否被引用?在不同位置定义的变量,在什么范围内有效?这就是变量的作用域问题。每一个变量都有一个作用域问题,即它们在什么范围内有效。本节专门讨论这个重要问题。

⛳(一)C语言内存分区

🎈1.C语言五大内存分区

在这里插入图片描述

内存存放顺序 (由上到下) : 栈区 -> 堆区 -> 全局区 -> 常量区 -> 代码区;

(1)栈区:

  • 分配, 释放方式 : 由编译器自动分配和释放;
  • 存放内容 : 局部变量, 参数;
  • 特点 : 具有后进先出特性, 适合用于保存恢复现场;

(2)堆区:

  • 分配, 释放方式 : 由程序员手动分配(malloc)和释放(free), 注意需要手动释放否则会造成内存泄漏。如果程序员没有手动释放,那么程序结束时可能由OS回收。
  • 存放内容 : 存放程序运行中动态分配内存的数据;
  • 特点 : 大小不固定, 可能会动态的放大或缩小;

(3)全局/静态存储区:

  • 分配, 释放方式 : 编译器分配内存, 程序退出时系统自动释放内存;

  • 存放内容 : 全局变量, 静态变量;

  • 特点 : 全局变量和静态变量存储在一个区域(包括静态全局变量与静态局部变量), 初始化的两种变量和未初始化的储在不同区域, 但是两个区域是相邻的:

    .bss段
    未初始化的全局变量和未初始化的静态变量存放在.bss段。
    初始化为0的全局变量和初始化为0的静态变量存放在.bss段。
    .bss段不占用可执行文件空间,其内容由操作系统初始化。

    .data段:
    已初始化的全局变量存放在.data段。
    已初始化的静态变量存放在.data段。
    .data段占用可执行文件空间,其内容有程序初始化。

(4)常量区:

  • 分配, 释放方式 : 常量在统一运行被创建,退出程序由系统自动释放;
  • 存放内容 : 常量; (比如char *s = “hello”,此处的hello就存储在常量区),常量区的内存是只读的

(5)程序代码区:

  • 分配, 释放方式 : 编译器分配内存, 程序退出时系统自动释放内存;
  • 存放内容 : 存放函数体的二进制代码。内存由系统管理

🎈2.程序的3个基本段:Text段,Date段,Bss段

在这里插入图片描述

一个程序的3个基本段:text段,dtae段,bss段,text段在内存中被映射为只读,但date段与bss段是可写的。

(1)Text段:

代码段,就是放程序代码的,编译时确定,只读

(2)Data段:

存放在编译阶段(而非运行时)就能确定的数据,可读可写。也就是通常所说的静态存储区,赋了初值的全局变量和赋初值的静态变量存放在这个区域,常量也存在这个区域

(3)Bss段:

已经定义但没赋初值的全局变量和静态变量存放在这个区域。

🎈3.可执行程序内存空间与逻辑地址空间的映射与划分

在这里插入图片描述

⛳(二)局部变量和全局变量

🎈1.局部变量

定义变量可能有3种情况:

  1. 在函数的开头定义;
  2. 在函数内的复合语句内定义
  3. 在函数的外部定义。

在一个函数内部定义的变量只在本函数范围内有效,也就是说只有在本函数内才能引用它们,在此函数以外是不能使用这些变量的。在复合语句内定义的变量只在本复合语句范围内有效,只有在本复合语句内才能引用它们。在该复合语句以外是不能使用这些变量的,以上这些称为“局部变量”。

主函数中定义的变量(如 m, n)也只在主函数中有效﹐并不因为在主函数中定义而在整个文件或程序中有效。主函数也不能使用其他函数中定义的变量。

不同函数中可以使用同名的变量,它们代表不同的对象,互不干扰。

形式参数也是局部变量。

在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也称为“分程序”或“程序块”。

🎈2.全局变量

前已介绍,程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量称为外部变量,外部变量是全局变量(也称全程变量)。全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。

但是,建议不在必要时不要使用全局变量,原因如下:

  1. 全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。

  2. 它使函数的通用性降低了,因为如果在函数中引用了全局变量,那么执行情况会受到有关的外部变量的影响,如果将一个函数移到另一个文件中,还要考虑把有关的外部变量及其值一起移过去。但是若该外部变量与其他文件的变量同名时,就会出现问题。这就降低了程序的可靠性和通用性。

    在程序设计中,在划分模块时要求模块的“内聚性”强、与其他模块的“耦合性”弱。即模块的功能要单一(不要把许多互不相干的功能放到一个模块中),与其他模块的相互影响要尽量少,而用全局变量是不符合这个原则的。一般要求把C程序中的函数做成一个相对的封闭体,除了可以通过“实参—形参”的渠道与外界发生联系外,没有其他渠道。这样的程序移植性好,可读性强。

  3. 使用全局变量过多﹐会降低程序的清晰性,人们往往难以清楚地判断出每个瞬时各个外部变量的值。由于在各个函数执行时都可能改变外部变量的值,程序容易出错。因此,要限制使用全局变量。

  4. 当全局变量与局部变量同名时,在相同的作用域部分,局部变量的作用域会覆盖全局变量的作用域

⛳(三)变量的存储类别

不同的存储类别具有不同的存储期、作用域和链接。先来学习作用域、链接和存储期的含义,再介绍具体的存储类别。

🎈1.作用域、链接和存储区

(1)作用域

作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。

  • 块作用域:定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。另外,虽然函数的形式参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。所以到目前为止,我们使用的局部变量(包括函数的形式参数)都具有块作用域。

    拓展:

    1.以前,具有块作用域的变量都必须声明在块的开头。C99 标准放宽了这一限制,允许在块中的任意位置声明变量。

    2.C99把块的概念扩展到包括for循环、while循环、 do while循环和if语句所控制的代码,即使这些代码没有用花括号括起来, 也算是块的一部分。以第一条语句(分号结束)作为块结束,一般就只有一条语句

  • 函数作用域:仅用于goto语句的标签,这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。

  • 函数原型作用域:用于函数原型中的形参名,函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话) 通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹 配。只有在变长数组中,形参名才有用:

    void use_a_VLA(int n, int m, ar[n][m]);
    
  • 文件作用域:变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。文件作用域变量也称为全局变量(global variable)。

(2)链接

C 变量有 3 种链接属性:外部链接、内部链接或无链接。

  • ①外部链接变量:外部链接变量可以在多文件程序中使用

  • ②内部链接变量:内部链接变量只能在一个翻译单元中使用。

    C预处理实际上是用 包含的头文件内容替换#include指令。所以,编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元

  • ③无链接变量:具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接

    描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。

(3)存储期

作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。

  • ①静态存储期:在程序的执行期间一直存在

  • ②线程存储期:用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。

  • ③自动存储期:块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。

    然而,块作用域变量也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键字static

    注意,对于文件作用域变量,关键字 static表明 了其链接属性,而非存储期。以 static声明的文件作用域变量具有内部链 接。但是无论是内部链接还是外部链接,所有的文件作用域变量都具有静态 存储期。 线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。

  • ④动态分配存储期:使用malloc()和free()函数动态的分配内存的变量

(4)C 使用作用域、链接和存储期为变量定义了多种存储方案:

存储类别说明符:

C 语言有6个关键字作为存储类别说明符:auto、register、static、extern、 _Thread_local和typedef。typedef关键字与任何内存存储无关,把它归于此类有一些语法上的原因。

在绝大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的 一部分。唯一例外的是_Thread_local,它可以和static或extern一起使用。

在这里插入图片描述

在这里插入图片描述

🎈2.动态存储方式和静态存储方式

从变量的作用域(即从空间)的角度来观察,变量可以分为全局变量和局部变量。

还可以从另一个角度﹐即从变量值存在的时间(即存储期)来观察。有的变量在程序运行的整个过程都是存在的,而有的变量则是在调用其所在的函数时才临时分配存储单元,而在函数调用结束后该存储单元就马上释放了,变量不存在了。也就是说,变量的存储有两种不同的方式:静态存储方式和动态存储方式。

静态存储方式:指在程序运行期间由系统分配固定的存储空间的方式,

动态存储方式:是在程序运行期间根据需要进行动态的分配存储空间的方式。

内存中供用户使用的存储空间:

在这里插入图片描述

动态存储区:

  • 函数形式参数。在调用函数时给形参分配存储空间。
  • 函数中定义的没有用关键字static声明的变量,即自动变量(详见后面的介绍)。
  • 函数调用时的现场保护和返回地址等

对以上这些数据,在函数调用开始时分配动态存储空间,函数结束时释放这些空间。在程序执行过程中,这种分配和释放是动态的,如果在一个程序中两次调用同一函数,而在此函数中定义了局部变量,在两次调用时分配给这些局部变量的存储空间的地址可能是不相同的。

数据分别存放在静态存储区和动态存储区中。全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。

🎈3.局部变量的存储类别

在C语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类别。对数据类型,读者已经熟知(如整型、浮点型等)。存储类别指的是数据在内存中存储的方式(如静态存储和动态存储)。

在定义和声明变量和函数时,一般应同时指定其数据类型和存储类别,也可以采用默认方式指定(即如果用户不指定﹐系统会隐含地指定为某一种存储类别)。

C的存储类别包括4种:自动的( auto)、静态的( statis)、寄存器的(register)、外部的( extern)。根据变量的存储类别,可以知道变量的作用域和生存期。下面分别作介绍。

(1)自动变量(auto变量)

函数中的局部变量,如果不专门声明为static(静态)存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的局部变量(包括在复合语句中定义的局部变量),都属于此类。在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字auto作存储类别的声明

实际上,关键字auto可以省略,不写auto则隐含指定为“自动存储类别”,它属于动态存储方式。程序中大多数变量属于自动变量。前面几章中介绍的例子,在函数中定义的变量都没有声明为auto,其实都隐含指定为自动变量。

块中声明的变量仅限于该块及其包含的块使用。

内层块中声明的变量与外层块中的变量同名,内层块会隐藏外层块的定义。

(2)静态局部变量(static局部变量)

有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用该函数时,该变量已有值(就是上一次函数调用结束时的值)。这时就应该指定该局部变量为“静态局部变量”,用关键字static进行声明。

  1. 静态局部变量属于静态存储类别,在静态存储区内分配存储单元。
  2. 对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。
  3. 如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符’o’(对字符变量)。而对自动变量来说,它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,
  4. 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。因为它是局部变量,只能被本函数引用,而不能被其他函数引用。
(3)寄存器变量(register变量)

一般情况下,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的。当程序中用到哪一个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中。经过运算器进行运算,如果需要存数﹐再从运算器将数据送到内存存放,

在这里插入图片描述

如果有一些变量使用频繁(例如,在一个函数中执行10 000次循环,每次循环中都要引用某局部变量),则为存取变量的值要花费不少时间。为提高执行效率,允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄―图7.18存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种变量叫做寄存器变量,用关键字register作声明。

注意:3种局部变量的存储位置是不同的:自动变量存储在动态存储区﹔静态局部变量存储在静态存储区﹔寄存器存储在CPU中的寄存器中。

拓展:

由于现在的计算机的速度愈来愈快,性能愈来愈高,优化的编译系统能够识别使用频繁的变量,从而自动地将这些变量放在寄存器中,而不需要程序设计者指定。因此,现在实际上用register声明变量的必要性不大。

🎈4.全局变量的存储类别

全局变量都是存放在静态存储区中的。因此它们的生存期是固定的,存在于程序的整个运行过程。但是,对全局变量来说,还有一个问题尚待解决,就是它的作用域究竞从什么位置起,到什么位置止。作用域是包括整个文件范围还是文件中的一部分范围?是在一个文件中有效还是在程序的所有文件中都有效?这就需要指定不同的存储类别。

一般来说,外部变量是在函数的外部定义的全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾。在此作用域内,全局变量可以为程序中各个函数所引用。但有时程序设计人员希望能扩展外部变量的作用域。有以下几种情况。

(1)extern声明
①在一个文件内拓展外部变量的作用域

如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。如果由于某种考虑,在定义点之前的函数需要引用该外部变量,则应该在引用之前用关键字extern对该变量作“外部变量声明”,表示把该外部变量的作用域扩展到此位置。有了此声明,就可以从“声明”处起,合法地使用该外部变量。

注意:提倡将外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个extern声明。

用extern声明外部变量时,类型名可以写也可以省写。

②将外部变量的作用域扩展到其他文件

一个C程序可以由一个或多个源程序文件组成。如果程序只由一个源文件组成,使用外部变量的方法前面已经介绍。如果程序由多个源程序文件组成,那么在一个文件中想引用另一个文件中已定义的外部变量,有什么办法呢?

如果一个程序包含两个文件,在两个文件中都要用到同一个外部变量Num,不能分别在两个文件中各自定义一个外部变量Num,否则在进行程序的连接时会出现“重复定义”的错误。正确的做法是:在任一个文件中定义外部变量Num,而在另一文件中用extern对Num作“外部变量声明”,即“extern Num;”。在编译和连接时,系统会由此知道Num有“外部链接”,可以从别处找到已定义的外部变量Num,并将在另一文件中定义的外部变量Num的作用域扩展到本文件,在本文件中可以合法地引用外部变量Num。

有的读者可能会问:extern既可以用来扩展外部变量在本文件中的作用域,又可以使外部变量的作用域从一个文件扩展到程序中的其他文件,那么系统怎么区别处理呢?实际上,在编译时遇到extern时,先在本文件中找外部变量的定义﹐如果找到,就在本文件中扩展作用域;如果找不到,就在连接时从其他文件中找外部变量的定义。如果从其他文件中找到了,就将作用域扩展到本文件;如果再找不到,就按出错处理。

(2)static静态外部变量

有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个static声明。

这种加上static声明、只能用于本文件的外部变量称为静态外部变量。

说明:不要误认为对外部变量加static声明后才采取静态存储方式(存放在静态存储区中),而不加 static的是采取动态存储(存放在动态存储区)。声明局部变量的存储类型和声明全局变量的存储类型的含义是不同的。对于局部变量来说,声明存储类型的作用是指定变量存储的区域(静态存储区或动态存储区)以及由此产生的生存期的问题,而对于全局变量来说,由于都是在编译时分配内存的,都存放在静态存储区﹐声明存储类型的作用是变量作用域的扩展问题。

用static声明一个变量的作用是:

  1. 对局部变量用static声明,把它分配在静态存储区﹐该变量在整个程序执行期间不释放﹐其所分配的空间始终存在。
  2. 对全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。

注意:用auto,register和 static声明变量时,是在定义变量的基础上加上这些关键字,而不能单独使用。下面的用法不对:

int a;
//先定义整型变量a
static a;
//企图再将变量a声明为静态变量

编译时会被认为“重新定义”。

⛳(四)类型限定符

我们通常用类型和存储类别来描述一个变量。

C99为类型限定符增加了一个新属性:它们现在是幂等的 (idempotent)!这个属性听起来很强大,其实意思是可以在一条声明中多 次使用同一个限定符,多余的限定符将被忽略

1.const类型限定符

以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。

2.volatile类型限定符

volatile 限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。

可以同时用const和volatile限定一个值。例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用 volatile。只能在声明中同时使用这两个限定符,它们的顺序不重要,

3.restrict限定符

restrict 关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。不能通过该指针外所有其他直接或间接的方式修改该对象的内容

int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par = ar;

这里,指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始 方式,也不是唯一方式。所以不用把它设置为restrict。

4._Atomic类型限定符(C11)

并发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的) 管理方法。

注意:要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。例如:

int hogs;// 普通声明
hogs = 12; // 普通赋值
//可以替换成:
_Atomic int hogs; // hogs 是一个原子类型的变量
atomic_store(&hogs, 12); // stdatomic.h中的宏

拓展:

C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数的初始方括号中。例如:

void ofmouth(int * const a1, int * restrict a2, int n); // 以前的风格

void ofmouth(int a1[const], int a2[restrict], int n); // C99允许

根据新标准,在声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符。static的情况不同,因为新标准为static引入了一种与以前用法不相关的新用法。

🚀九、内部函数和外部函数

函数也有存储类别,可以是外部函数(默认)或静态函数。C99 新增了 第 3 种类别——内联函数,

前面提到有全局变量和局部变量,在函数中对应也有内部函数和外部函数的。有的函数可以被本文件中的其他函数调用,也可以被其他文件中的函数调用,而有的函数只能被本文件中的其他函数调用.不能被其他文件中的函数调用.

函数本质上是全局的,因为定义一个函数的目的就是要被另外的函数调用。如果不加声明的话,一个文件中的函数既可以被本文件中其他函数调用,也可以被其他文件中的函数调用。但是,也可以指定某些函数不能被其他文件调用。根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。

⛳(一)内部函数

如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加static,即:

static 类型名 函数名(形参列表)

内部函数又称静态函数,因为它是用static声明的。使用内部函数,可以使函数的作用域只局限于所在文件。这样,在不同的文件中即使有同名的内部函数,也互不干扰,不必担心所用函数是否会与其他文件模块中的函数同名。

⛳(二)外部函数

如果在定义函数时,在函数首部的最左端加关键字extern,则此函数是外部函数,可供其他文件调用。

extern 类型名 函数名(形参列表)

C语言规定,如果在定义函数时省略extern,则默认为外部函数。前面所用的函数都是外部函数。


行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C语言中,创建线程需要使用线程函数库(pthread)。通过调用pthread_create函数来创建一个子线程,需要指定一个处理函数,该函数会在子线程中执行。创建线程的示例代码如下: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> // 子线程的处理函数 void* working(void* arg) { printf("我是子线程,线程ID:%ld\n", pthread_self()); for(int i=0; i<9; i++) { printf("child==i:=%d\n", i); } return NULL; } int main() { // 创建一个子线程 pthread_t tid; pthread_create(&tid, NULL, working, NULL); printf("子线程创建成功,线程ID:%ld\n", tid); // 子线程不会执行下边的代码,主线程执行 printf("我是主线程,线程ID:%ld\n", pthread_self()); for(int i=0; i<3; i++) { printf("i=%d\n", i); } return 0; } ``` 在上述代码中,我们通过调用`pthread_create`函数来创建了一个子线程,并在子线程中执行了`working`函数。主线程和子线程是同时执行的,它们的执行顺序可能是不确定的。需要注意的是,主线程会在子线程之前执行完毕。 除了创建线程,线程函数库还提供了其他一些常用的函数,比如线程分离和线程取消等功能。如果需要将子线程和主线程分离,可以使用`pthread_detach`函数。示例代码如下: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> // 子线程的处理函数 void* working(void* arg) { printf("我是子线程,线程ID:%ld\n", pthread_self()); for(int i=0; i<9; i++) { printf("child==i:=%d\n", i); } return NULL; } int main() { // 创建一个子线程 pthread_t tid; pthread_create(&tid, NULL, working, NULL); printf("子线程创建成功,线程ID:%ld\n", tid); // 主线程执行 printf("我是主线程,线程ID:%ld\n", pthread_self()); for(int i=0; i<3; i++) { printf("i=%d\n", i); } // 设置子线程和主线程分离 pthread_detach(tid); // 让主线程自己退出即可 pthread_exit(NULL); return 0; } ``` 在上述代码中,我们通过调用`pthread_detach`函数将子线程和主线程分离。这样主线程执行完毕后会自动退出,不会等待子线程的结束。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C语言线程库的使用,这值得收藏!](https://blog.csdn.net/LxXlc468hW35lZn5/article/details/125650245)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

陈七.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值