持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情
前言
本文就来分享一波作者对C中存储类别的学习心得与见解。
笔者水平有限,难免存在纰漏,欢迎指正交流。
存储类别
存储期
作用域和链接描述了标识符的可见性,而存储期则描述了通过这些标识符访问的对象的生命周期。
C对象有四种存储期:
静态存储期
线程存储期
自动存储期
动态分配存储期
静态存储期
在程序的执行期间一直存在。所有文件作用域变量都具有静态存储期,关键字static只表明了文件作用域变量的链接属性而非存储期。
而局部变量被static修饰后生命周期发生变化而作用域不变,具有静态存储期。
线程存储期
具有线程存储期的对象,从被声明到线程结束一直存在。线程存储期用于并发程序设计,程序执行可被分为多个线程。
自动存储期
块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出块时,释放之前分配的内存。比如:一个函数调用结束后,其变量占用的内存可用于存储下一个被调用函数的变量。
(与函数栈帧有关,感兴趣的可以移步至作者写的这篇博客: [深入浅出C语言]深入函数栈帧 - 掘金 (juejin.cn)
变长数组稍有不同,它的存储期从声明处到块的末尾,而不是从块的开始到块的末尾。
总结一下:
全局变量是定义在函数外面,具有文件作用域和静态存储期的变量。
局部变量是定义在块内的,具有局部作用域(块作用域、函数作用域或函数原型作用域)和自动存储期的变量。
然而,局部变量也能具有静态存储期,只需要在定义时前面加上一个static关键字即可。
比如:
int main()
{
static int ct = 10;
//...
return 0;
}
变量ct存储在静态内存中,在程序执行期间一直存在,但是其作用域未发生改变,仍然是main()函数块中。不过值得注意的是,可以给其他函数提供地址以间接访问该对象。
不同存储类别变量的总结表格:
自动变量
对应表格的第一行,默认情况下,声明在块内或函数头中的任何变量都属于自动存储类别。实际上,局部变量,自动变量,临时变量,都是一回事,我们统称局部变量。
自动变量的初始化
自动变量并不会自动初始化,必须显式初始化,比如
int main()
{
int repid;
int tents = 10;
}
变量repid的内容可能是随机值,具体是什么值与编译器和函数栈帧的初始化有关,反正不手动初始化的话放的就是一个垃圾值。
小插曲——auto关键字
关键字auto是存储类别说明符之一,用来修饰自动变量。
如何使用:一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略,所以现在在C语言里基本上用不到该关键字。
自动变量的特点
块作用域和无链接属性意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,使用函数传参或函数返回值这类间接方式也可以实现变量值在块之间的传递)。不同函数中可以使用同名变量,因为一般而言这些变量存储位置不同,不是一回事儿,而且作用域都限制在彼此的函数内,互相之间不会冲突。
举个例子,就好比咱们班里有个叫张三的,隔壁班也有一个同名的,可他们是同一个人吗?压根就不是。但是他们在各自的班级里正常上课互不干扰,老师同学们也都能分得清他们。
变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,而程序在退出该块时变量消失,原来该变量所占用的内存位置现在可以被其他变量使用。
关于块嵌套问题
块中的声明的变量仅限于该块及其包含的块使用。
那要是内层块中声明的变量与外层块中的变量同名会怎样?
实际上,内层块会隐藏外层块的定义,直到离开内层块之后才可见。
例子:
int main()
{
int x = 30;//原始的x
printf("x在块外面值:%d,地址:%p\n", x, &x);
{
int x = 77;//新的x,隐藏了原始的x
printf("x在块里面值:%d,地址:%p\n", x, &x);
}
printf("x在块外面值:%d,地址:%p\n", x, &x);//原始的x
while(x++ < 33)//原始的x
{
int x= 100;//新的x,隐藏了原始的x
x++;
printf("x在块里面值:%d,地址:%p\n", x, &x);
}
printf("x在块外面值:%d,地址:%p\n", x, &x);//原始的x
return 0;
}
程序的输出:
分析:
根据显示的地址可知,在块中创建了新变量并隐藏了原始的x。在块外面前两个printf的打印结果说明原始的x既没有消失也没有改变。
比较麻烦的是while循环,其中的测试条件用的是原始的x,因为块内的x需要进入块中才会创建,而测试条件的判断是循环的第一步,是在进入块前执行的。每轮循环结束新创建的x就会销毁,待到下一次进入循环体再创建新的x。
需要注意的是,该循环必须得在测试条件中递增x,因为如果在循环体中递增x,那么递增的就是循环体中新创建的x而非原始的x。
寄存器变量
硬件简单介绍
其实,CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从内存读取到CPU内,那么也就需要CPU具有一定的数据临时存储能力。注意:CPU并不是当前要计算了,才把特定数据读到CPU里面,那样太慢了。
所以现代CPU内,都集成了一组叫做寄存器的硬件,用来做临时数据的保存。
存储分级金字塔:
距离CPU越近的存储硬件,效率越高,速度越快,单价成本也更高。
距离CPU越远的存储硬件,效率越低,速度越慢,单价成本也更低。
同时,对于任何一种硬件而言,它都充当上游硬件的缓存,这有利于CPU访问数据时能够以最小的成本来达到最高的效率。
寄存器存在的意义:在硬件层面上,提高计算机的运算效率。因为不需要从内存里读取数据而直接可以在离的近的寄存器里读取数据。
register修饰变量
变量通常储存在计算机内存中,而寄存器变量则储存在CPU的寄存器中。
寄存器变量使用存储类别关键字register声明,比如:
int main()
{
register int quick;
return 0;
}
register 修饰变量
作用:尽量将所修饰变量放入CPU寄存器中(不一定能实现),从而达到提高效率的目的。
那么什么样的变量,可以采用register呢?
- 局部变量
(全局变量会导致CPU寄存器被长时间占用)
- 不会被写入的
(意思就是不会再被赋值的,写入就需要写回内存,后续还要读取检测的话,速度就慢了,register的意义又何在?)
- 高频被读取的
(提高效率的原因所在,相较于内存,存到寄存器能使访问处理数据的速度更快)
不过要注意,如果要使用,请不要大量使用,因为寄存器数量有限。
特点
由于是将变量放到了寄存器而非内存中去,所以CPU访问和处理寄存器变量的速度更快。
寄存器变量有没有地址呢?
答案是没有,因为地址是内存的概念,而寄存器和内存完全是两码事。
绝大多数方面,寄存器变量和自动变量都一样,它们都是块作用域、无链接属性和自动存储期的。
声明寄存器变量与直接命令相比更像是一种请求,编译器必须根据寄存器或可用的最快内存的数量来衡量你的请求,或者直接忽略你的请求,不一定会如你所愿的。这种情况下,寄存器变量就与自动变量几乎没有区别,只是仍旧不能对该变量使用地址运算符。
可声明为register的数据类型有限,例如,处理器中的寄存器可能没有足够大的空间来储存double类型的值。
关键字register其实不用管,因为现在的编译器,已经很智能了,能够进行比人更好的代码优化。只是早期编译器需要人为指定register,来进行手动优化,现在不需要了。
静态变量
对于静态变量,你可能对它存在一个误解:是不是意味着该变量不可变呀。No!
实际上,静态的意思是 中原地不动(静态数据区),而并不是说它的值不变。
块作用域的静态变量(无链接)
这个其实就是前面提到过的static修饰的局部变量,它和自动变量唯一的区别就是存储期不同——被改变成了静态存储期,也就是说出了它的定义所在的块后变量仍然存在。
不能在函数的形参中使用static:
int wont(static int flu);//不允许
例子:
void fun1()
{
int i = 0;
i++;
printf("no static: i=%d\n", i);
}
void fun2()
{
static int i = 0;
i++;
printf("has static: i=%d\n", i);
}
int main()
{
for (int i = 0; i < 10; i++)
fun1();
for (int i = 0; i < 10; i++)
fun2();
return 0;
}
为什么会这样?
首先看fun1(),里面的i变量是局部变量,在函数时开辟空间并初始化,而在函数调用结束后释放空间而销毁,每次调用时i都是重新创建并赋值的,所以循环调用打印出来的结果相同。
而在fun2()中的i变量就不一样了,从结果看出,很明显上一次调用结束后i的值依旧保存着,接着可以在下一次调用中继续使用,你可能会觉得不是有static int i = 0;嘛,每次进来不都赋值为0了吗?实际上你需要区分一下初始化和赋值,那是初始化,而初始化只会初始化一次!
static修饰局部变量,变量的生命周期变成静态存储期。(作用域不变)
为什么局部变量具有自动存储期而static修饰局部变量具有全局性呢?
这跟数据在C程序地址空间(不是内存)内的存储有关:
局部变量存储在栈区,而static修饰的局部变量存储位置相较于原局部变量发生了改变,存储在静态数据区了,因此生命周期发生了改变。
我们由此也可以看出,生命周期和变量存储位置有较为紧密的关系。
拓展思考:
这一类变量可以在其他函数中直接使用吗?
我们看看下面这段代码,编译器直接提醒标识符a未定义,
你可能会说,因为a变量作用域只在test函数中,在main函数中使用的话就对a不可见,是这么回事。
还记得我们前面说“作用域是可访问标识符的区域”吗?访问标识符和访问变量是不一样的,有人就会说了,标识符不就代表变量了吗,这怎么就不一样了呢?我们来看个简单的例子:
int a = 0;
int* pa = &a;
a = 10;
*pa = 20;
这段代码中a = 10就是通过访问标识符来访问变量,那*pa = 20呢,是不是也是在访问变量?但是它有访问标识符a吗?没有,而是通过解引用pa来访问变量的,是间接访问,前面通过标识符来访问变量是直接访问。
回到第一个例子,在main函数中遇到标识符a时,编译器会去找a的定义,这时候,由于static a = 1是定义在另一个函数test中的,该变量具有的是块作用域,也就是跨函数就不可见了(要注意这里是指标识符不可见了而非变量不可见),所以编译器这么找一圈最终认为你这个标识符a压根就没定义。
那么是不是说块作用域静态变量就不可以在别的函数中使用了?
走大门(直接访问)行不通,那翻窗户(间接访问)总行了吧。所以我们可以通过返回指针或者参数传递指针的方式来实现跨函数访问。
int* test1()
{
static a = 1;
return &a;
}
void test2(int* pb)
{
++(*pb);
}
int main()
{
int* pa = test1();
printf("%d\n", *pa);
static b = 2;
test2(&b);
printf("%d\n", b);
return 0;
}
我们由上述分析可知,实际上作用域指的是变量的标识符的可见范围而非变量的可使用范围,因为变量除了可以直接被访问还可以间接被访问,这么一看变量的可使用范围实际上应当和生命周期有关。
外部链接的静态变量
外部链接的静态变量具有文件作用域、外部链接属性以及静态存储期,也被称为外部变量。
一般把变量的定义放在所有函数外面便创建了外部变量。
初始化外部变量
外部变量可以被显式初始化,但如果未手动初始化的话它们会被自动初始化为0。这一原则同样适用于外部定义的数组元素。
只能使用常量表达式初始化文件作用域变量:
int x = 10;//可以,10是字面常量
int y = 3 + 20;//可以,用于初始化的是常量表达式
size_t z = sizeof(int);//可以,用于初始化的是常量表达式
int x2 = 2 * x;//不行,x是变量
只要不是变长数组,sizeof表达式可被视为常量表达式
使用外部变量
在同一源文件下使用外部变量一般不需要特别声明,因为该变量本身具有文件作用域,全文件范围内可见嘛,不过要是块中定义了同名变量的话,当程序运行进入块中时,会暂时隐藏外部变量,采用“就近原则”使用块中的同名变量,直到程序运行出了该块为止。
这时候如果就想要在块中使用外部变量的话,就需要使用存储类别关键字extern修饰一下,以此声明该变量,这样就表示你在这使用的是外部变量而非块中同名变量。
如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须使用extern在该文件中声明该变量。使用extern声明并不会引起存储空间的分配,而是指示编译器该变量的定义不在这里,用它来引用外部定义。
内部链接的静态变量
该类型变量具有静态存储期、文件作用域和内部链接。在所有函数外用static修饰而定义的变量就是内部链接的静态变量。
int traveler = 1;//外部链接
static int stayhome = 1;//内部链接
int main()
{
extern int traveler;//使用了定义在别处的traveler
extern int stayhome;//使用了定义在别处的stayhome
//...
return 0;
}
存储类别小结
自动变量具有块作用域、无链接、自动存储期的属性。它们是局部变量,属于其定义所在块私有。寄存器变量的属性和自动变量相同,但是编译器会使用更快的内存或寄存器来存储它们,同时不能获取寄存器变量的地址。
具有静态存储期的变量可以具有外部链接、内部链接或无链接属性。在同一个文件中,在所有函数的外部声明的变量是外部变量,具有文件作用域、外部链接和静态存储期属性。如果在这种声明前面加上关键字static修饰的话,链接属性由外部链接转变为内部链接。如果是在函数中使用static修饰一个变量,则该变量具有块作用域、无链接和静态存储期属性。
具有自动存储期的变量,程序在进入该变量的定义所在块时才为其分配内存,在退出该块时释放之前分配的内存。如果未初始化,自动变量的值是随机值。程序在编译时为具有静态存储期的变量分配内存并在程序的运行过程中一直保留这块内存,静态变量若未初始化则会被设置为0。
函数的存储类别
函数也有存储类别,分为外部函数、静态函数以及C99新增的内联函数(本文不讲)。
外部函数可以在其他文件使用,而静态函数只能用于其定义所在的文件。
默认情况下,函数为外部函数,在其他文件中用extern声明后就可以被使用,并且函数声明即使没有extern也会默认为extern修饰的,除非遇到static修饰。
静态函数就是受到static修饰的函数,相当于屏蔽了函数的外部链接属性,受限于其定义所在文件。
static的总结
-
static修饰局部变量,作用域不变,而由于存储位置由栈区改为静态区,生命周期就由自动存储期变为静态存储期。
-
static修饰全局变量,作用域和生命周期不变,而外部链接属性被屏蔽,只具有内部链接属性。
-
static修饰函数,外部链接属性被屏蔽,只具有内部链接属性。
static的实际意义
可以为项目维护提供安全保证,因为它可以屏蔽外部链接属性,让其他文件不能直接调用。
如何体现呢?我们用static修饰函数不直接让外部调用功能函数,而是让另一个函数调用它,让外部调用这一个函数来间接调用那一个函数。
举个非常简单的例子:我想要从外部调用函数来进行四则运算,不让直接调用,而是通过另一个函数cul来封装,那我就只能通过调用cul来实现计算,也就只能使用提供的接口,具体是如何实现每一类计算功能的我就没法知道了。
static int add(int x, int y)
{
return x + y;
}
static int sub(int x, int y)
{
return x - y;
}
static int mul(int x, int y)
{
return x * y;
}
static int div(int x, int y)
{
return x / y;
}
int cul(int x, int y, int type)
{
switch (type)
{
case 1:
return add(x, y);
case 2:
return sub(x, y);
case 3:
return mul(x, y);
case 4:
return div(x, y);
default:
return -1;
}
}
这样一来,若是有些逻辑不想被使用者知道,可以用static来“隐藏”,封装起来,只提供接口让人使用。
多文件
当程序由多个翻译单元组成时,体现出区别内部链接和外部链接的重要性。
复杂的C程序通常由多个单独的源文件组成,文件之间可能要共享外部变量或函数,倘若有一百个源文件要使用某一个外部源文件的变量和函数,要是依次在一个一个文件中声明的话不得麻烦死,也就是说单纯地使用源文件组织项目结构时,项目越大越复杂则维护成本会变得越来越高。
所以C语言允许通过在一个文件中定义,在其他文件中进行声明来引用外部变量或函数以实现共享,这个文件就是头文件。注意要使用关键字extern声明。
extern 在多文件下的理解与使用
.h文件:我们称之为头文件,一般包含函数声明,变量声明,宏定义,typedef重命名,struct结构声明,其他头文件等内容,也就是把可能会多次用到的东西全部放在一块,在组织项目结构的时候,能减少大型项目地维护成本问题。
.h文件基本都是要被多个源文件包含的,头文件又有可能被重复包含
.c文件: 我们称之为源文件,一般包含函数实现,变量定义等
编译器发出警告针对的是编译期间的问题,在main.c中使用test.c的函数show()实际上是在链接之后才实现的,也就是说,在链接之前,这两个源文件各自不知道对方的内容,所以main.c中无法找到show()函数,而它又不知道show()在test.c中,就认为是未定义的函数,发出警告。要解决这个问题就要用extern声明show函数。
并且g_val变量已经在test.c中定义了,变量只能定义一次,所以在main.c中只能声明而不能赋值(赋值就相当于再次定义初始化了)。声明并没有开辟空间,所以变量声明时,不能设置初始值。
要在别的源文件中使用其他源文件中的变量和函数,把变量在头文件中声明,方便各个源文件使用,不过要注意在定义时用static修饰一下,不然会报错。
extern int g_val;//变量声明必须带上extern
因为int g_val很有可能会被编译器认为是变量定义,只是没有初始化而已,为了避免这种摸棱两可的二义性,必须得带上extern。
extern void show();//函数声明建议带上extern
实际上void show();还是函数声明,函数是不是定义取决于后面有没有函数体,所以其实不加上也还是声明,只不过为了清晰明了,最好就把extern加上。
以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~