C程序设计语言--缓冲、常见的问题和全局变量

来源:互联网 发布:杜汶泽现状2017 知乎 编辑:程序博客网 时间:2024/06/06 04:29
 

C程序设计语言--缓冲、常见的问题

分类: C/C++ 《c程序设计语言》 69人阅读 评论(0) 收藏 举报
C程序设计语言c语言缓冲

C语言中你经常没意识到的问题

1、下面的程序并不见得会输出 hello-std-out,你知道为什么吗?

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <unistd.h>
int main() 
{
    while(1)
    {
        fprintf(stdout,"hello-std-out");
        fprintf(stderr,"hello-std-err");
        sleep(1);
    }
    return 0;
}

参考答案:stdout和stderr是不是同设备描述符。stdout是块设备,stderr则不是。对于块设备,只有当下面几种情况下才会被输入,1)遇到回车,2)缓冲区满,3)flush被调用。而stderr则不会。

2、下面的程序看起来是正常的,使用了一个逗号表达式来做初始化。可惜这段程序是有问题的。你知道为什么呢?

1
2
3
4
5
6
7
8
#include <stdio.h>
 
int main()
{
    int a = 1,2;
    printf("a : %d\n",a);
    return 0;
}

参考答案:这个程序会得到编译出错(语法出错),逗号表达式是没错,可是在初始化和变量声明时,逗号并不是逗号表达式的意义。这点要区分,要修改上面这个程序,你需要加上括号: int a = (1,2);

3、下面的程序会有什么样的输出呢?

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
    int i=43;
    printf("%d\n",printf("%d",printf("%d",i)));
    return 0;
}

参考答案:程序会输出4321,你知道为什么吗?要知道为什么,你需要知道printf的返回值是什么。printf返回值是输出的字符个数。

4、下面的程序会输出什么?

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() 
{
    float a = 12.5;
    printf("%d\n", a);
    printf("%d\n", (int)a);
    printf("%d\n", *(int *)&a);
    return 0; 
}

参考答案:
该项程序输出如下所示,
0
12
1095237632
原因是:浮点数是4个字节,12.5f 转成二进制是:01000001010010000000000000000000,十六进制是:0×41480000,十进制是:1095237632。所以,第二和第三个输出相信大家也知道是为什么了。而对于第一个,为什么会输出0,我们需要了解一下float和double的内存布局,如下:

  • float: 1位符号位(s)、8位指数(e),23位尾数(m,共32位)
  • double: 1位符号位(s)、11位指数(e),52位尾数(m,共64位)

然后,我们还需要了解一下printf由于类型不匹配,所以,会把float直接转成double,注意,12.5的float和double的内存二进制完全不一样。别忘了在x86芯片下使用是的反字节序,高位字节和低位字位要反过来。所以:

  • float版:0×41480000 (在内存中是:00 00 48 41)
  • double版:0×4029000000000000 (在内存中是:00 00 00 00 00 00 29 40)

而我们的%d要求是一个4字节的int,对于double的内存布局,我们可以看到前四个字节是00,所以输出自然是0了。

这个示例向我们说明printf并不是类型安全的,这就是为什么C++要引如cout的原因了。

5、下面,我们再来看一个交叉编译的事情,下面的两个文件可以编译通过吗?如果可以通过,结果是什么?

file1.c

1
int arr[80];

file2.c

1
2
3
4
5
6
7
extern int *arr;
int main() 
{     
    arr[1] = 100;
    printf("%d\n", arr[1]);
    return 0; 
}

参考答案:该程序可以编译通过,但运行时会出错。为什么呢?原因是,在另一个文件中用 extern int *arr来外部声明一个数组并不能得到实际的期望值,因为他们的类型并不匹配。所以导致指针实际并没有指向那个数组。注意:一个指向数组的指针,并不等于一个数组。修改:extern int arr[]。(参考:ISO C语言 6.5.4.2 节)

6、请说出下面的程序输出是多少?并解释为什么?(注意,该程序并不会输出 “b is 20″)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main() 
{     
    int a=1;     
    switch(a)     
    {  
        int b=20;         
        case 1:
            printf("b is %d\n",b);
            break;
        default:
            printf("b is %d\n",b);
            break;
    }
    return 0;
}

参考答案:该程序在编译时,可能会出现一条warning: unreachable code at beginning of switch statement。我们以为进入switch后,变量b会被初始化,其实并不然,因为switch-case语句会把变量b的初始化直接就跳过了。所以,程序会输出一个随机的内存值。

7、请问下面的程序会有什么潜在的危险?

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() 
{     
    char str[80];
    printf("Enter the string:");
    scanf("%s",str);
    printf("You entered:%s\n",str);
    return 0;
}

参考答案:本题很简单了。这个程序的潜在问题是,如果用户输入了超过80个长度的字符,那么就会有数组越界的问题了,你的程序很有可以及会crash了。

8、请问下面的程序输出什么?

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() 
{
    int i;
    i = 10;
    printf("i : %d\n",i);
    printf("sizeof(i++) is: %d\n",sizeof(i++));
    printf("i : %d\n",i);
    return 0;
}

参考答案:如果你觉得输出分别是,10,4,11,那么你就错了,错在了第三个,第一个是10没有什么问题,第二个是4,也没有什么问题,因为是32位机上一个int有4个字节。但是第三个为什么输出的不是11呢?居然还是10?原因是,sizeof不是一个函数,是一个操作符,其求i++的类型的size,这是一件可以在程序运行前(编译时)完全的事情,所以,sizeof(i++)直接就被4给取代了,在运行时也就不会有了i++这个表达式。

9、请问下面的程序的输出值是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
 
#define SIZEOF(arr) (sizeof(arr)/sizeof(arr[0]))
#define PrintInt(expr) printf("%s:%d\n",#expr,(expr))
 
int main()
{
    /* The powers of 10 */
    int pot[] = {
                    0001,
                    0010,
                    0100,
                    1000
                };
 
    int i;
    for(i=0;i<SIZEOF(pot);i++)
        PrintInt(pot[i]);
         
    return 0;
}

参考答案:好吧,如果你对于PrintInt这个宏有问题的话,你可以去看一看《语言的歧义》中的第四个示例。不过,本例的问题不在这里,本例的输出会是:1,8,64,1000,其实很简单了,以C/C++中,以0开头的数字都是八进制的。

10、请问下面的程序输出是什么?(绝对不是10)

#include #define PrintInt(expr) printf("%s : %dn",#expr,(expr))int main()  {    int y = 100;    int *p;    p = malloc(sizeof(int));    *p = 10;    y = y/*p; /*dividing y by *p */;    PrintInt(y);    return 0;}

参考答案:本题输出的是100。为什么呢?问题就出在 y = y/*p;上了,我们本来想的是 y / (*p) ,然而,我们没有加入空格和括号,结果y/*p中的 /*被解释成了注释的开始。于是,这也是整个恶梦的开始。

11、下面的输出是什么?

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() 
{
    int i = 6;
    if( ((++i < 7) && ( i++/6)) || (++i <= 9))
        ;
 
    printf("%d\n",i);
    return 0;
}

参考答案:本题并不简单的是考前缀++或反缀++,本题主要考的是&&和||的短路求值的问题。所为短路求值:对于(条件1 && 条件2),如果“条件1”是false,那“条件2”的表达式会被忽略了。对于(条件1 || 条件2),如果“条件1”为true,而“条件2”的表达式则被忽略了。所以,我相信你会知道本题的答案是什么了。

12、下面的C程序是合法的吗?如果是,那么输出是什么?

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() 
{
    int a=3, b = 5;
 
    printf(&a["Ya!Hello! how is this? %s\n"], &b["junk/super"]);
     
    printf(&a["WHAT%c%c%c  %c%c  %c !\n"], 1["this"],
        2["beauty"],0["tool"],0["is"],3["sensitive"],4["CCCCCC"]);
         
    return 0; 
}

参考答案:
本例是合法的,输出如下:

Hello! how is this? super
That is C !

本例主要展示了一种另类的用法。下面的两种用法是相同的:

“hello”[2]
2["hello"]

如果你知道:a[i] 其实就是 *(a+i)也就是 *(i+a),所以如果写成 i[a] 应该也不难理解了。

13、请问下面的程序输出什么?(假设:输入 Hello, World)

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() 
{
    char dummy[80];
    printf("Enter a string:\n");
    scanf("%[^r]",dummy);
    printf("%s\n",dummy);
    return 0;
}

参考答案:本例的输出是“Hello, Wo”,scanf中的”%[^r]“是从中作梗的东西。意思是遇到字符r就结束了。

14、下面的程序试图使用“位操作”来完成“乘5”的操作,不过这个程序中有个BUG,你知道是什么吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#define PrintInt(expr) printf("%s : %d\n",#expr,(expr))
int FiveTimes(int a) 
{
    int t;
    t = a<<2 + a;
    return t;
}
 
int main() 
{
    int a = 1, b = 2,c = 3;
    PrintInt(FiveTimes(a));
    PrintInt(FiveTimes(b));
    PrintInt(FiveTimes(c));
    return 0;
}

参考答案:本题的问题在于函数FiveTimes中的表达式“t = a<<2 + a;”,对于a<<2这个位操作,优先级要比加法要低,所以这个表达式就成了“t = a << (2+a)”,于是我们就得不到我们想要的值。该程序修正如下:

1
2
3
4
5
6
int FiveTimes(int a) 
{
    int t;
    t = (a<<2) + a;
    return t;
}

C程序设计语言--全局变量的认识

分类: C/C++ 《c程序设计语言》 69人阅读 评论(0) 收藏 举报
C程序设计语言c语言全局变量

目录(?)[+]

全局变量是C语言语法和语义中一个很重要的知识点,首先它的存在意义需要从三个不同角度去理解:对于程序员来说,它是一个记录内容的变量(variable);对于编译/链接器来说,它是一个需要解析的符号(symbol);对于计算机来说,它可能是具有地址的一块内存(memory)。其次是语法/语义:从作用域上看,带static关键字的全局变量范围只能限定在文件里,否则会外联到整个模块和项目中;从生存期来看,它是静态的,贯穿整个程序或模块运行期间(注意,正是跨单元访问和持续生存周期这两个特点使得全局变量往往成为一段受攻击代码的突破口,了解这一点十分重要);从空间分配上看,定义且初始化的全局变量在编译时在数据段(.data)分配空间,定义但未初始化的全局变量暂存(tentative definition)在.bss段,编译时自动清零,而仅仅是声明的全局变量只能算个符号,寄存在编译器的符号表内,不会分配空间,直到链接或者运行时再重定向到相应的地址上。

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

第一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* t.h */
#ifndef _H_
#define _H_
inta;
#endif
 
/* foo.c */
#include <stdio.h>
#include "t.h"
 
struct{
   chara;
   intb;
} b = { 2, 4 };
 
intmain();
 
voidfoo()
{
    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);
}
 
/* main.c */
#include <stdio.h>
#include "t.h"
 
intb;
intc;
 
intmain()
{
    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);
    return0;
}

Makefile如下:

1
2
3
4
5
6
7
8
test: main.o foo.o
    gcc -otest main.o foo.o
 
main.o: main.c
foo.o: foo.c
 
clean:
    rm*.o test

运行情况:

1
2
3
4
5
6
7
8
9
10
11
12
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被重复定义了,实际上可以看做代码段的地址。但编译器并未报错,只给出一条警告:

1
/usr/bin/ld: Warning: size of symbol'b' changed from 4in 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语言里的一个大坑,编译器对这种全局变量多重定义的“纵容”很可能会无端修改某个变量,导致程序不确定行为。如果你还没有意识到事态严重性,我再举个例子。

第二个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* foo.c */
#include <stdio.h>;
 
struct{
    inta;
    intb;
} b = { 2, 4 };
 
intmain();
 
voidfoo()
{
    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);
}
 
/* main.c */
#include <stdio.h>
 
intb;
intc;
 
intmain()
{
    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);
    }
    return0;
}

运行情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
    setb=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的改动。

1
2
3
4
5
6
7
8
9
test: main.o foo.o
    ar rcs libfoo.a foo.o
    gcc -static -otest main.o libfoo.a
 
main.o: main.c
foo.o: foo.c
 
clean:
    rm-f *.o test

运行情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
    setb=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在九泉之下邪恶的笑声——不,与其说是嘲笑,不如说是诅咒……

第四个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* foo.c */
#include <stdio.h>
 
conststruct {
    inta;
    intb;
} b = { 3, 3 };
 
intmain();
 
voidfoo()
{
    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);
}
 
/* t1.c */
#include <stdio.h>
 
intb = 1;
intc = 1;
 
intmain()
{
    intcount = 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);
    }
    return0;
}
 
/* t2.c */
#include <stdio.h>
 
intb;
intc;
 
intt2()
{
    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);
    return0;
}

Makefile脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
exportLD_LIBRARY_PATH:=.
 
all:test
    ./test
 
test: t1.o t2.o
    gcc -shared -fPIC -o libfoo.so foo.c
    gcc -otest t1.o t2.o -L. -lfoo
 
t1.o: t1.c
t2.o: t2.c
 
.PHONY:clean
clean:
    rm-f *.o *.so test*

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
./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是一门很恐怖的语言!

P.S.题外话写在最后。我无意挑起语言之争,只是就事论事地去“黑(hack)”一门语言而已,而且要黑就要黑得有理有力有层次,还要带点娱乐精神。其实黑一门语言并非什么尖端复杂的技术,个人觉得起码要做到两点:

  • 亲自动手写测试程序。动手写测试程序是开发人员必备的基础技能,只有现成的代码才能让人心服口服,那些只会停留在口头上的争论只能算作cheap hack。
  • 测试程序不能依赖于不成熟的代码。软件开发99%以上的bug都是基于不合格(substandard)开发人员导致,这并不能怪罪于语言以及编译器本身。使用诸如#define TRUE FALSE或者#define NULL 1之类的trick来黑C语言只能证明此人很有娱乐精神而不是真正的”hack value”,拿老北京梨园行当里的一句话——“那是下三滥的玩意儿”。

原创粉丝点击