对C语言中的static关键字的深入理解

在一次面试的时候面试官问我static全局变量与全局变量的区别,之前虽然用过但是并没仔细去搞懂他,这次来细心的学习一下。

基本概念

使用static有三种情况:

  • 函数内部static变量
  • 函数外部static变量
  • static函数

函数内部的static变量,关键在于生命周期持久,他的值不会随着函数调用的结束而消失,下一次调用时,static变量的值,还保留着上次调用后的内容。

函数外部的static变量,以及static函数,关键在于私有性,它们只属于当前文件,其它文件看不到他们。例如:

/* test_static1.c */
#include <stdio.h> void foo() {
} static void bar() {
} int i = ;
static int j = ; int main(void){
printf ("%d \n", i);
printf ("%d \n", j);
return ;
}
/* test_static2.c */
void foo() {
} static void bar() {
} int i = ;
static int j = ;

将两个文件一起编译

gcc test_static1.c test_static2.c -o test_static

编译器会提示:

/tmp/ccuerF9V.o: In function `foo':
test_static2.c:(.text+0x0): multiple definition of `foo'
/tmp/cc9qncdw.o:test_static1.c:(.text+0x0): first defined here
/tmp/ccuerF9V.o:(.data+0x0): multiple definition of `i'
/tmp/cc9qncdw.o:(.data+0x0): first defined here
collect2: ld returned exit status

把与非static变量i相的语句注释掉就不会有此提示i重复定义了,原因就在于使用static声明后,变量私有化了,不同文件中的同名变量不会相互chong_tu。

static 函数也与此类似,将函数声明为static,说明我们只在当前文件中使用这个函数,其它文件看不到,即使重名,也不会相互chong_tu。

深入理解

作为一名程序员我们就不应该仅仅满足于了解现象,还要了解现象的背后有什么

为什么函数内部的static变量和普通函数变量生命周期不一样

我们的程序,从源代码经过编译,链接生成了可执行文件,可执行文件被加载到存储器中,然后执行。以Linux程序为例,每个Linux程序都有一个运行时存储器映像。可以理解为程序运行时,存储器中的数据分布。

图1 Linux运行时存储器映像

当程序运行时,操作系统会创建用户栈(User stack),一个函数被调用时,它的参数,局部变量,返回地址等等,都会被压入栈中,当函数执行结束后,这些数据就会被其它函数使用,所以函数调用结束后,局部变量的值不会被保持。我们将此区域放大,可以看到用户栈中都有哪些内容。

图2 栈帧结构

而static变量与普通局部变量不同,它不是保留在栈中。注意图一中,有一块区域,"Loaded from executable file",其中有一块 .data, .bss区,static变量会被存储在这里,所以函数调用结束后,static变量的值仍然会得到保留。而 .data, .bss区,executable file,与程序的编译,链接,相关。

首先,多个源代码会分别被编译成可重定位目标程序,然后链接器会最终生成可执行目标程序。可重定位目标程序的结构如图3所示,可以看出,此时,.data, .bss区,已经出现。

图3 可重定位目标程序

.data 区存储已经初始化的全局C变量,.bss 区存储没有初始化的全局C变量,所以这两个区域又被称为全局区。而编译器会为每个static变量在.data或者.bss中分配空间。

可执行目标程序的结构如图4所示

图4 可执行目标程序

将图4与图1比较,就会发现,可执行目标程序的一部分被加载到存储器中,这就是"Loaded from executable file"的来源。

另外,从图一中,也可以看出,使用malloc分配的内存空间,与函数局部变量,static变量的不同。

为什么函数外部的static变量及static函数只对文件内部可见

要解释这个问题,我们首先要理解问题本身。这个问题的本质其实是,当我们遇到一个变量或者函数时,我们去哪里寻找它,static变量/函数与普通变量/函数的寻找方式有什么不同。

我们回到刚才的例子,这一次,仔细地观察编译链接时的提示信息:

/* test_static1.c */
#include <stdio.h> void foo() {
} static void bar() {
} int i = ;
static int j = ; int main(void){
printf ("%d \n", i);
printf ("%d \n", j);
return ;
}
/* test_static2.c */
void foo() {
} static void bar() {
} int i = ;
static int j = ;

将两个文件一起编译

gcc test_static1.c test_static2.c -o test_static

编译器会提示:

/tmp/ccuerF9V.o: In function `foo':
test_static2.c:(.text+0x0): multiple definition of `foo'
/tmp/cc9qncdw.o:test_static1.c:(.text+0x0): first defined here
/tmp/ccuerF9V.o:(.data+0x0): multiple definition of `i'
/tmp/cc9qncdw.o:(.data+0x0): first defined here
collect2: ld returned exit status

  你会发现,虽然我们只用了一条命令对两个文件进行编译链接,但是,实际上,两个源文件是被分别编译成/tmp/ccuerF9V.o及/tmp/cc9qncdw.o,并且,错误并不是出现在编译时,而是出现在链接时,链接器ld返回了1。链接是把两个可重新定位的目标程序,组合在一起,组合的时候,我们发现了变量i及函数foo的定义出现chong_tu。而声明为static的变量j及函数bar并没有提示chong_tu。

  这说明,在ld进行链接时,需要进行某种检查,去发现chong_tu。ld的输入是每个源文件生成的可重定位目标文件,那么这些目标文件里一定会有一些信息,告诉ld它们有什么变量,然后ld才能检查是不是有chong_tu。

  说起可重定位目标文件,我们一直都没有解释为什么要重定位。其实这很好理解,一个源文件编译后,如果生成的目标文件中,各个地址就是最终运行时的地址,那么这些地址很可能会和其它文件中的地址chong_tu。因为编译一个文件时,我们不会知道有其它文件的存在,所以编译时无法确定最终的地址。因此,编译单个文件时,生成的目标文件中的地址都是从0开始,链接时,链接器会将不同目标文件中的地址重新定位,最终生成可执行文件。注意这里的chong_tu和前面说的chong_tu不是一回事,这里的chong_tu是不同的可重定位目标文件中相同地址的chong_tu,前面一段讲的是同名变量之间的chong_tu。

此时,我们不得不回到可重定位目标文件的格式。

图3 可重定位目标程序

注意 .symtab节,这个节存储符号表,假设当前可重定位目标模块为m, 符号表会告诉我们m中定义和引用的符号信息,主要分为:

  • m定义,并可以被其它模块引用的全局符号:m中的非static函数,非static全局变量。
  • 由其它模块定义,并被m引用的全局符号:m中使用extern声明的变量
  • 只被m引用的本地符号:m中的static函数,static全局变量。

现在编译一下,然后用GNU READELF工具看一下符号表。

    $ gcc -c test_static1.c -o test_static1.o
$ readelf -s test_static1.o
Symbol table '.symtab' contains  entries:
Num: Value Size Type Bind Vis Ndx Name
: NOTYPE LOCAL DEFAULT UND
: FILE LOCAL DEFAULT ABS test_static1.c
: SECTION LOCAL DEFAULT
: SECTION LOCAL DEFAULT
: SECTION LOCAL DEFAULT
: FUNC LOCAL DEFAULT bar
: OBJECT LOCAL DEFAULT j
: SECTION LOCAL DEFAULT
: SECTION LOCAL DEFAULT
: SECTION LOCAL DEFAULT
: SECTION LOCAL DEFAULT
: FUNC GLOBAL DEFAULT foo
: OBJECT GLOBAL DEFAULT i
: 0000000a FUNC GLOBAL DEFAULT main
: NOTYPE GLOBAL DEFAULT UND printf

表的数据结构不解释,有兴趣,看扩展阅读部分。

现在,假如你是链接器ld,我给你2个可重定位目标程序,你从中得到两个符号表,这时候,你就可以检查出两个符号表是否存在chong_tu了。

由于全局符号可能会定义相同的名字,链接器会有一套规则,来确定选择哪个符号。符号分为强符号与弱符号。

  • 强符号:函数和已经初始化的全局变量是强符号
  • 弱符号:未初始化的全局变量是弱符号

处理相同名字的全局符号的规则是:

  1. 不允许有多个强符号
  2. 如果有一个强符号,多个弱符号,那么选择强符号
  3. 如果有多个弱符号,那么从中任意选择一个

总结:

1.static全局变量与全局变量

  static 全局变量:只对本文件生效,可以使用,本工程其他文件不可见,不能使用。存放在全局数据区。

  全局变量:全局变量只要加上extern,则对本工程全部文件有效。

2.static局部变量与局部变量

  static局部变量:存放在全局数据区,只对本函数有效。

  局部变量:存放在局部数据区,只对本函数有效。

3.static函数与函数

  static函数:定义的函数只对本文件可见,对于本工程其他文件不可见,不可使用。

  函数:对于本工程都可以进行调用,只声明了此文件即可。

共同点:static声明一次,如果不改变那么static的值一直是初始化的值,如果在初始化的时候没有进行赋值,则系统默认赋0。这一次调用的值是上一次修改的值。

对C语言中static的理解的更多相关文章

  1. C语言中static的作用及C语言中使用静态函数有何好处

    转自:http://www.jb51.net/article/74830.htm 在C语言中,static的作用有三条:一是隐藏功能,二是保持持久性功能,三是默认初始化为0. 在C语言中,static ...

  2. C语言中static的使用方法【转】

    本文转自:http://blog.csdn.net/renren900207/article/details/21609649 全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量 ...

  3. c语言中static 函数和普通函数的区别

    C程序一直由下列部分组成: 1)正文段——CPU执行的机器指令部分:一个程序只有一个副本:只读,防止程序由于意外事故而修改自身指令: 2)初始化数据段(数据段)——在程序中所有赋了初值的全局变量,存放 ...

  4. C语言中static用法介绍

    C语言中static用法介绍     对于新手来说,很多东西的用法还不是很清楚,我们今天一起来看看C语言中static用法介绍     1.声明了static的变量称为静态变量,根据作用域的不同又分为 ...

  5. C语言中static关键字的作用

    static的作用(精辟分析) 在C语言中,static的字面意思很容易把我们导入歧途,其实它的作用有三条. (1)先来介绍它的第一条也是最重要的一条:隐藏. 当我们同时编译多个文件时,所有未加sta ...

  6. C语言中static作用

    在C语言中,static的字面意思很容易把我们导入歧途,其实它的作用有三条. (1)先来介绍它的第一条也是最重要的一条:隐藏. 当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有 ...

  7. C语言中static关键字的用法

    C记得还是大一时学的,现在觉得好久没用了,又捧起来看看.今天刚看到有关static关键字,仔细地看了一遍<C和指针>这本书中的解释,现在觉得清楚多了. 首先,我们将static关键字,修饰 ...

  8. 在不同语言中static的用法

    static (计算机高级语言) 编辑 像在VB,C#,C,C++,Java,PHP中我们可以看到static作为关键字和函数出现,在其他的高级计算机语言如FORTRAN.ALGOL.COBOL.BA ...

  9. C语言中static修饰符的意义

    在C语言中,static通常有2种含义:1)定义变量的生命周期:2)定义变量或者函数的作用域. 变量的生命周期是指,相对于程序运行的进程生命周期,变量存在的时间段.变量的生命周期由变量的存储类型(位置 ...

随机推荐

  1. Cow Exhibition (背包中的负数问题)

    个人心得:背包,动态规划真的是有点模糊不清,太过于抽象,为什么有些是从后面递推, 有些状态就是从前面往后面,真叫人头大. 这一题因为涉及到负数,所以网上大神们就把开始位置从10000开始,这样子就转变 ...

  2. stack容器

    一.stack特性 stack是一种先进后出(first in last out,FILO)的数据结构,它只有一个出口,stack只允许在栈顶新增元素,移除元素,获得顶端元素,但是除了顶端之外,其他地 ...

  3. deque容器

    一.deque容器基本概念 deque是“double-ended queue”的缩写,和vector一样,deque也支持随机存取.vector是单向开口的连续性空间,deque则是一种双向开口的连 ...

  4. openvswitch以及docker网络

    修改docker0的IP,教程写的是/etc/default/docker文件,但是那是过时的配置,真正的配置是在/etc/docker/daemon.json,格式是json的: { "r ...

  5. bootstrap简单的签收页面

    http://aqvatarius.com/themes/atlant/html/ui-icons.html <%@ Page Language="C#" AutoEvent ...

  6. L3-001. 凑零钱(dfs或者01背包)

    L3-001. 凑零钱 时间限制 200 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 陈越 韩梅梅喜欢满宇宙到处逛街.现在她逛到了一家火星店里,发现 ...

  7. PostgreSQL 监控磁盘使用

    监控磁盘使用 1. 判断磁盘用量 每个表都有一个主要的堆磁盘文件,大多数数据都存储在其中.如果一个表有着可能会很宽(尺寸大)的列, 则另外还有一个TOAST文件与这个表相关联, 它用于存储因为太宽而不 ...

  8. jQuery UI vs Kendo UI & jQuery Mobile vs Kendo UI Mobile

    jQuery UI vs Kendo UI http://jqueryuivskendoui.com/#introduction jQuery Mobile vs Kendo UI Mobile ht ...

  9. 第十六章 Velocity工作原理解析(待续)

    Velocity总体架构 JJTree渲染过程解析 事件处理机制 常用优化技巧 与JSP比较 设计模式解析之合成模式 设计模式解析之解释器模式

  10. 第三章 Java内存模型(上)

    本章大致分为4部分: Java内存模型的基础:主要介绍内存模型相关的基本概念 Java内存模型中的顺序一致性:主要介绍重排序和顺序一致性内存模型 同步原语:主要介绍3个同步原语(synchroized ...