我的编程空间,编程开发者的网络收藏夹
学习永远不晚

四个例子说明C语言 全局变量

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

四个例子说明C语言 全局变量

我们知道,全局变量是C语言语法和语义中一个很重要的知识点,首先它的存在意义需要从三个不同角度去理解:

  • 对于程序员来说,它是一个记录内容的变量(variable);
  • 对于编译/链接器来说,它是一个需要解析的符号(symbol);
  • 对于计算机来说,它可能是具有地址的一块内存(memory)。

其次是语法/语义:

  • 从作用域上看,带static关键字的全局变量范围只能限定在文件里,否则会外联到整个模块和项目中;
  • 从生存期来看,它是静态的,贯穿整个程序或模块运行期间(注意,正是跨单元访问和持续生存周期这两个特点使得全局变量往往成为一段受攻击代码的突破口,了解这一点十分重要);
  • 从空间分配上看,定义且初始化的全局变量在编译时在数据段(.data)分配空间,定义但未初始化的全局变量**暂存(tentative definition)**在.bss段,编译时自动清零,而仅仅是声明的全局变量只能算个符号,寄存在编译器的符号表内,不会分配空间,直到链接或者运行时再重定向到相应的地址上。

我们将向您展现一下,非static限定全局变量在编译/链接以及程序运行时会发生哪些有趣的事情,顺便可以对C编译器/链接器的解析原理管中窥豹。以下示例对ANSI C和GNU C标准都有效,笔者的编译环境是Ubuntu下的GCC-4.4.3。

第一个例子

#ifndef _H_
#define _H_
int a;
#endif

#include <stdio.h>
#include "t.h"
struct {
   char a;
   int b;
} b = { 2, 4 };
int main();
void foo()
{
    printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &a, &b, sizeof b, b.a, b.b, main);
}

#include <stdio.h>
#include "t.h"
int b;
int c;
int main()
{
    foo();
    printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n",
        &a, &b, &c, sizeof b, b, c);
  return 0;
}

Makefile如下:

test: main.o foo.o
  gcc -o test main.o foo.o
main.o: main.c
foo.o: foo.c
clean:
  rm *.o test

运行情况:

foo:  (&a)=0x0804a024
  (&b)=0x0804a014
  sizeof(b)=8
  b.a=2
  b.b=4
  main:0x080483e4
main:  (&a)=0x0804a024
  (&b)=0x0804a014
  (&c)=0x0804a028
  size(b)=4
  b=2
  c=0

这个项目里我们定义了四个全局变量,t.h头文件定义了一个整型a,main.c里定义了两个整型b和c并且未初始化,foo.c里定义了一个初始化了的结构体,还定义了一个main的函数指针变量。

由于C语言每个源文件单独编译,所以t.h分别包含了两次,所以int a就被定义了两次。两个源文件里变量b和函数指针变量main被重复定义了,实际上可以看做代码段的地址。但编译器并未报错,只给出一条警告:

/usr/bin/ld: Warning: size of symbol 'b' changed from 4 in main.o to 8 in foo.o

运行程序发现,main.c打印中b大小是4个字节,而foo.c是8个字节,因为sizeof关键字是编译时决议,而源文件中对b类型定义不一样。

但令人惊奇的是无论是在main.c还是foo.c中,a和b都是相同的地址,也就是说,a和b被定义了两次,b还是不同类型,但内存映像中只有一份拷贝。

我们还看到,main.c中b的值居然就是foo.c中结构体第一个成员变量b.a的值,这证实了前面的推断——**即便存在多次定义,内存中只有一份初始化的拷贝。**另外在这里c是置身事外的一个独立变量。

为何会这样呢?这涉及到C编译器对多重定义的全局符号的解析和链接。

在编译阶段,编译器将全局符号信息隐含地编码在可重定位目标文件的符号表里。这里有个**“强符号(strong)”和“弱符号(weak)”**的概念——前者指的是定义并且初始化了的变量,比如foo.c里的结构体b,后者指的是未定义或者定义但未初始化的变量,比如main.c里的整型b和c,还有两个源文件都包含头文件里的a。当符号被多重定义时,GNU链接器(ld)使用以下规则决议:

  • 不允许出现多个相同强符号。
  • 如果有一个强符号和多个弱符号,则选择强符号。
  • 如果有多个弱符号,那么先决议到size最大的那个,如果同样大小,则按照链接顺序选择第一个。

像上面这个例子中,全局变量a和b存在重复定义。如果我们将main.c中的b初始化赋值,那么就存在两个强符号而违反了规则一,编译器报错。

如果满足规则二,则仅仅提出警告,实际运行时决议的是foo.c中的强符号。而变量a都是弱符号,所以只选择一个(按照目标文件链接时的顺序)。

事实上,这种规则是C语言里的一个大坑,编译器对这种全局变量多重定义的“纵容”很可能会无端修改某个变量,导致程序不确定行为。如果你还没有意识到事态严重性,我再举个例子。

第二个例子


#include <stdio.h>;
struct {
    int a;
    int b;
} b = { 2, 4 };
int main();
void foo()
{
    printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
        \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &b, sizeof b, b.a, b.b, main);
}

#include <stdio.h>
int b;
int c;
int main()
{
    if (0 == fork()) {
        sleep(1);
        b = 1;
        printf("child:\tsleep(1)\n\t(&b):0x%08x\n
            \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tset b=%d\n\tc=%d\n",
            &b, &c, sizeof b, b, c);
        foo();
    } else {
        foo();
        printf("parent:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
            \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n\twait child...\n",
            &b, &c, sizeof b, b, c);
        wait(-1);
        printf("parent:\tchild over\n\t(&b)=0x%08x\n
            \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
            &b, &c, sizeof b, b, c);
    }
    return 0;
}

运行情况如下:

foo:  (&b)=0x0804a020
  sizeof(b)=8
  b.a=2
  b.b=4
  main:0x080484c8
parent:  (&b)=0x0804a020
  (&c)=0x0804a034
  sizeof(b)=4
  b=2
  c=0
  wait child...
child:  sleep(1)
  (&b):0x0804a020
  (&c)=0x0804a034
  sizeof(b)=4
  set b=1
  c=0
foo:  (&b)=0x0804a020
  sizeof(b)=8
  b.a=1
  b.b=4
  main:0x080484c8
parent:  child over
  (&b)=0x0804a020
  (&c)=0x0804a034
  sizeof(b)=4
  b=2
  c=0
 

(说明一点,运行情况是直接输出到stdout的打印,笔者曾经将./test输出重定向到log中,结果发现打印的执行序列不一致,所以采用默认输出。)

这是一个多进程环境,首先我们看到无论父进程还是子进程,main.c还是foo.c,全局变量b和c的地址仍然是一致的(当然只是个逻辑地址),而且对b的大小不同模块仍然有不同的决议。

这里值得注意的是,我们在子进程中对变量b进行赋值动作,从此子进程本身包括foo()调用中,整型b以及结构体成员b.a的值都是1,而父进程中整型b和结构体成员b.a的值仍是2,但它们显示的逻辑地址仍是一致的。

个人认为可以这样解释,fork创建新进程时,子进程获得了父进程上下文“镜像”(自然包括全局变量),虚拟地址相同但属于不同的进程空间,而且此时真正映射的物理地址中只有一份拷贝,所以b的值是相同的(都是2)。

随后子进程对b改写,触发了操作系统的**写时拷贝(copy on write)**机制,这时物理内存中才产生真正的两份拷贝,分别映射到不同进程空间的虚拟地址上,但虚拟地址的值本身仍然不变,这对于应用程序来说是透明的,具有隐瞒性。

还有一点值得注意,这个示例编译时没有出现第一个示例的警告,即对变量b的sizeof决议,笔者也不知道为什么,或许是GCC的一个bug?

第三个例子

这个例子代码同上一个一致,只不过我们将foo.c做成一个静态链接库libfoo.a进行链接,这里只给出Makefile的改动。

test: main.o foo.o
  ar rcs libfoo.a foo.o
  gcc -static -o test main.o libfoo.a
main.o: main.c
foo.o: foo.c
clean:
  rm -f *.o test

运行情况如下:

foo:  (&b)=0x080ca008
  sizeof(b)=8
  b.a=2
  b.b=4
  main:0x08048250
parent:  (&b)=0x080ca008
  (&c)=0x080cc084
  sizeof(b)=4
  b=2
  c=0
  wait child...
child:  sleep(1)
  (&b):0x080ca008
  (&c)=0x080cc084
  sizeof(b)=4
  set b=1
  c=0
foo:  (&b)=0x080ca008
  sizeof(b)=8
  b.a=1
  b.b=4
  main:0x08048250
parent:  child over
  (&b)=0x080ca008
  (&c)=0x080cc084
  sizeof(b)=4
  b=2
  c=0
 

从这个例子看不出有啥差别,只不过使用静态链接后,全局变量加载的地址有所改变,b和c的地址之间似乎相隔更远了些。不过这次编译器倒是给出了变量b的sizeof决议警告。

到此为止,有些人可能会对上面的例子嗤之以鼻,觉得这不过是列举了C语言的某些特性而已,算不上黑。

有些人认为既然如此,对于一切全局变量要么用static限死,要么定义同时初始化,杜绝弱符号,以便在编译时报错检测出来。只要小心地使用,C语言还是很完美的嘛~

对于抱这样想法的人,我只想说,请你在夜深人静的时候竖起耳朵仔细聆听,你很可能听到Dennis Richie在九泉之下邪恶的笑声——不,与其说是嘲笑,不如说是诅咒……

第四个例子


#include <stdio.h>
const struct {
    int a;
    int b;
} b = { 3, 3 };
int main();
void foo()
{
    b.a = 4;
    b.b = 4;
    printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
        \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &b, sizeof b, b.a, b.b, main);
}

#include <stdio.h>
int b = 1;
int c = 1;
int main()
{
    int count = 5;
    while (count-- > 0) {
        t2();
        foo();
        printf("t1:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
            \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
            &b, &c, sizeof b, b, c);
        sleep(1);
    }
    return 0;
}

#include <stdio.h>
int b;
int c;
int t2()
{
    printf("t2:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
        \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
        &b, &c, sizeof b, b, c);
    return 0;
}

Makefile脚本:

export LD_LIBRARY_PATH:=.
all: test
  ./test
test: t1.o t2.o
  gcc -shared -fPIC -o libfoo.so foo.c
  gcc -o test t1.o t2.o -L. -lfoo
t1.o: t1.c
t2.o: t2.c
.PHONY:clean
clean:
  rm -f *.o *.so test*

执行结果:

./test
t2:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=1
  c=1
foo:  (&b)=0x0804a01c
  sizeof(b)=8
  b.a=4
  b.b=4
  main:0x08048564
t1:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=4
  c=4
t2:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=4
  c=4
foo:  (&b)=0x0804a01c
  sizeof(b)=8
  b.a=4
  b.b=4
  main:0x08048564
t1:  (&b)=0x0804a01c
  (&c)=0x0804a020
  sizeof(b)=4
  b=4
  c=4
  ...
 

其实前面几个例子只是开胃小菜而已,真正的大坑终于出现了!而且这次编译器既没报错也没警告,但我们确实眼睁睁地看到作为main()中强符号的b被改写了,而且一旁的c也“躺枪”了。

眼尖的读者发现,这次foo.c是作为动态链接库运行时加载的,当t1第一次调用t2时,libfoo.so还未加载,一旦调用了foo函数,b立马中弹,而且c的地址居然还相邻着b,这使得c一同中弹了。

不过笔者有些无法解释这种行为的原因,有种说法是强符号的全局变量在数据段中是连续分布的(相应地弱符号暂存在.bss段或者符号表里),或许可以上报GNU的编译器开发小组。

另外笔者尝试过将t1.c中的b和c定义前面加上const限定词,编译器仍然默认通过,但程序在main()中第一次调用foo()时触发了Segment fault异常导致奔溃,在foo.c里使用指针改写它也一样。

推断这是GCC对const常量所在地址启用了类似操作系统写保护机制,但我无法确定早期版本的GCC是否会让这个const常量被改写而程序不会奔溃。

至于volatile关键词之于全局变量,自测似乎没有影响。

C语言在你心目中是否还是当初那个“纯洁”、“干净”、“行为一致”的姑娘呢?也许趁着你不注意的时候她会偷偷给你戴顶绿帽,这一切都是通过全局变量,特别在动态链接的环境下,就算全部定义成强符号仍然无法为编译器所察觉。

而一些IT界“恐怖分子”也经常**将恶意代码包装成全局变量注入到root权限下存在漏洞的操作序列中,**就像著名的栈溢出攻击那样。某一天当你傻傻地看着一个程序出现未定义的行为却无法定位原因的时候,请不要忘记Richie大爷那来自九泉之下最深沉的“问候”~

或许有些人会偷换概念,把这一切归咎于编译器和链接器身上,认为这同语言无关,但这里我要提醒,正是编译/链接器的行为支撑了整个语言的语法和语义。

我们可以反过来思考一下为何C的胞弟C++推出**“命名空间(namespace)”**的概念,或者你可以使用其它高级语言,对于重定义的全局变量是否能通过编译这一关。

到此这篇关于四个例子说明C语言 全局变量的文章就介绍到这了,更多相关C全局变量多多内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

四个例子说明C语言 全局变量

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C语言全局变量实例分析

本篇内容主要讲解“C语言全局变量实例分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C语言全局变量实例分析”吧!我们知道,全局变量是C语言语法和语义中一个很重要的知识点,首先它的存在意义需要从
2023-06-30

C语言全局变量和局部变量的示例代码

本文主要介绍了C语言全局变量和局部变量的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
2023-05-20

C语言中全局变量与局部变量的示例分析

小编给大家分享一下C语言中全局变量与局部变量的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!一:局部变量与全局变量1.1:局部变量局部变量:在函数内部定义
2023-06-25

c语言全局变量如何定义

在C语言中,全局变量的定义需要放在函数外部,即在所有函数的外部声明和定义。全局变量的作用域是整个程序,可以被程序中的任何函数访问和使用。全局变量的定义可以分为两个步骤:1. 在函数外部声明全局变量:在所有函数的外部使用关键字`extern`
2023-09-04

c语言中全局变量如何定义

在C语言中,全局变量可以在函数外部定义。它们的作用域从变量定义的位置开始,直到文件的末尾。全局变量在整个程序中都是可见的,可以被任何函数使用。全局变量的定义通常在所有函数之前,如下所示:```c#include // 全局变量的定义int
2023-09-29

C语言——全局变量和局部变量重名了会怎么样

前言 (1)今天在交流群里面看到这样一个问题: 为什么这个程序中下面我定义的void型函数smart在全局变量前声明了,但是在man函数中调用了smart函数,m的值打印出来还是0。 #includeint m;void smart(
2023-08-30

C语言中如何实现局部与全局变量

这篇“C语言中如何实现局部与全局变量”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“C语言中如何实现局部与全局变量”文章吧。C
2023-06-16

c语言静态变量和全局变量有什么区别

C语言中的静态变量和全局变量在作用域和生命周期上有所区别。1. 作用域:- 全局变量:全局变量定义在函数外部,可以被程序中的所有函数访问,即具有全局作用域。- 静态变量:静态变量定义在函数内部,只能在定义它的函数中使用,即具有局部作用域。2
2023-09-14

c语言中全局变量定义的方法是什么

在C语言中,全局变量可以在函数外部定义。以下是全局变量定义的方法:1. 在所有函数的外部定义变量,这样变量就可以在程序的任何地方访问。```cint globalVariable; // 全局变量定义```2. 在定义变量的同时进行初始化。
2023-09-29

C语言全局变量定义的注意事项有哪些

在C语言中,全局变量的定义需要注意以下几个事项:1. 全局变量的定义应该在所有函数的外部,即在函数体外部进行定义。2. 全局变量的定义应该在使用之前,否则可能出现未定义的错误。3. 全局变量的作用域为整个程序,可以在程序的任何地方被访问和修
2023-09-29

R语言将变量分组的3种方法实例(含cut函数说明)

在数据处理分析过程中,变量分组是经常遇到的,下面这篇文章主要给大家介绍了关于R语言将变量分组的3种方法,其中含cut函数说明的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
2022-11-13

C语言开发单片机为什么大多数都采用全局变量的形式

这是因为单片机的资源非常有限,而全局变量可以在程序的任何地方被访问,这样可以避免在函数之间传递参数的开销,从而提高程序的效率。此外,全局变量简直就是嵌入式系统的戈兰高地。冲突最激烈的双方是:一、做控制的工程师;二、做非嵌入式的软件工程师。
C语言开发单片机为什么大多数都采用全局变量的形式
2023-10-29

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录