c语言关键字总结

由于面试中经常被问到各种c语言的关键字,本着利己又利人的原则,在这里做以总结。
C语言中的关键字因为被各种重载,有时候显得比较诡异,好多关键字我用的也不太多,平时编码没有太注意,面试的时候有些细节就不太敢肯定了,因此我决定好好学习一下这些关键字,对那些说不清的细节我决定自己写几个简单的例子验证和分析一下。

static

其实static是很常用的关键字了,因为我自己是搞操作系统的,对这个关键字还比较熟悉,简单说一下吧。
static可以修饰变量和函数,这里体现了c语言对关键字的重载,可以看到,它修饰变量和函数时的作用可以说不太相干。当static修饰变量时,它表示这个变量是静态变量,所谓静态变量,它会和全局变量(在函数体之外声明的变量)一样存储到全局数据区(如果已经初始化会存到数据段,如果没有初始化会存到bss段)而不会像局部变量一样存储在堆栈,我们知道,局部变量会存在堆栈里,当函数调用结束之后就会被释放,因此如下所示的函数返回被认为是错误的。

int* fun(){
    int *p;
    int res;
    p=&res;
    res=1;
    return p;
}

很明显,当这个函数调用返回的时候,res这个变量已经释放了,那么此时返回一个指向res的指针肯定是错误的,甚至会产生不可预期的后果。这时你可以在int res前面加一个static就可以了。
接下来讨论static修饰函数的情况。正常情况下,每个函数默认是被extern修饰的,这就意味着每个函数都是外部函数,可以被外部引用,也就是被另一个文件中的函数调用(只要包含对应的头文件)。但是一个函数如果被static修饰,那么它将变成内部函数,只能被本文件的其他函数调用,不能被外部引用。《c++ expert programming》中对此进行论述,认为这种设计是c语言的一个缺陷或者是一个不合理的地方,因为所有的函数在default的情况下都是外部函数,那么重名的风险就会很大,作者认为应该显式的声明函数为外部函数才比较合理,我个人觉得两边都有道理....因为相互之间的调用也实在非常频繁,如果从概率意义上讲超过百分之50的函数都需要声明extern那么我认为声明为extern就有问题,还有一个原因是,有些函数可能最初是static的,但是有一天你需要把它变成extern,这时你可能需要修改源码甚至你压根儿没有源码,那这种修改就非常麻烦了。

volatile

第一次接触volatile实际是在内联汇编里,因为很多OS源码里面有很多内联汇编,而内联汇编又经常被volatile修饰。当时的一个印象是,只要被volatile修饰,编译器就不会对它进行优化了,所谓编译器的优化可以如下所示:

static int a;
void fun(){
    a=1;
    if(a==1)
        printf("hello world\n");
}

上面是一个很简单的小程序,但是用来说明这个问题足够了。需要说明的是,不同编译器面对相同问题的做法可能不一样,我们这里一般考虑的都是gcc。在上述代码中,在判断a和1是否相等之前做了一个将a赋值为1的操作,那么编译器可能进行这样的优化,它不会再进行判断而直接跳转到printf去执行。这在多线程编程的时候就有问题了,另一个线程可能在这期间更改了a的值。使用volatile关键字会强制编译器每次都去内存地址中取对应的值,而不会在寄存器中取值或者简单的进行优化而跳过比较。正确的做法是将static int a改为volatile static int a;

const

const如果你在网络上搜会发现有大量的人写了大量的内容,其实我觉得他们考虑的过于复杂和细化了,对于这样一个概念我认为最重要的是要理解它在内存中怎么存的,如何保证它的不可修改性以及编译器在这个过程中做了什么,沿着这样的思路我们进行分析。
当然,首先我们要明确为什么要用const,这里主要就是代码健壮性和可读性了,我想当我说出这两个词的时候有经验的读者一定已经完全明白了。接下来我们分析它的原理,其实关于这个const,之前我一直把它理解成read-only,说白了还是变量,只是编译器强制它为只读变量,它和局部变量一样存在堆栈里,而且它的不可更改性是在编译阶段限定的,于是乎我写了一个小程序,试图骗过编译器。

void fun(){
    const int a=10;
    int *p;
    p=&a;
    *p=5;
    printf("*p=%d  a=%d\n",*p,a);
}

输出的结果是

*p=5  a=5

可见,任你花言巧语,其实它还是内存中一个数据而已,照改不误,它只是存在堆栈中的一个普通数据,而不是像常量一样存在只读数据区。const的特性都是编译器做的,编译完之后就没有这回事儿了。
const的另一个问题是const修饰谁的问题,其实就是这两种情况的辨析。

const int *p;//p指向的内容不可变
int const *p;//p本身不可变,此时的p可以类比于数组名了

好了,关于const我只想说这么多,但是我觉得已经够用了,至于它的万千变种读者可以自行理解下,我认为都是上述核心原理的扩展。

attribute

__attribute机制是GNU C的一大特色,__attribute__可以设置函数属性、变量属性和类型属性。关于__attribute__如何书写,下面举个我在实际代码中遇到的例子。

struct e820map {
    int nr_map;
    struct {
        uint64_t addr;
        uint64_t size;
        uint32_t type;
    } __attribute__((packed)) map[E820MAX];
};

__attribute__括号里的参数有6个,即:aligned,packed,transparent_union,unused,deprecated和may_alias。这里先介绍两个简单的,aligned和packed,先看一个例子。

struct S{
    short b[3];
}__attribute__((aligned(8)));

这会强制编译器将struct S在分配空间时采用8字节对齐的方式,所以sizeof(struct S)等于8。如果aligned后面不跟数字会怎么样呢?看下面的例子。

struct S{
    short b[3];
}__attribute((aligned));

struct L{
    short b[9];
}__attribute((aligned));


struct XL{
    short b[17];
}__attribute((aligned));

把它们的大小都输出一遍,sizeof(struct S)=16,sizeof(struct L)=32,sizeof(struct XL)=48。我估计应该是这样的,默认参数最小是16,然后根据你的大小,取16的最近倍数作为对齐大小,比如struct L,大小是9*2=18,所以取16的2倍,也就是32,同理XL取48。
接下来我们看packed,先看一个例子。

struct packed_S{
    char c;
    int i;
}__attribute__((packed));

sizeof(packed_S)是5,如果去掉__attribute__((packed))那么就是8。这里的packed是缩小内存使用的关键字。

VA_ARGS

首先看这段代码。

#define __KERNEL_EXECVE(name, path, ...) ({                         \
const char *argv[] = {path, ##__VA_ARGS__, NULL};       \
                     cprintf("kernel_execve: pid = %d, name = \"%s\".\n",    \
                             current->pid, name);                            \
                     kernel_execve(name, argv);                              \
})

简单解释一下,这是ucore执行exec()这个系统调用的一个宏,...和##__VA_ARGS__是对应的,目的是传递给应用程序启动参数个数和参数列表,也就是我们的argc和argv。
GNU C允许你可以定义可变参数宏(variadic macros),这样你就可以使用拥有可以变化的参数表的宏。先看下面的例子。

#define debug(...) printf(__VA_ARGS__)

缺省号代表一个可以变化的参数表。使用保留名 VA_ARGS 把参数传递给宏。当宏的调用展开时,实际的参数就传递给 printf()了。例如:

Debug("Y = %d\n", y);

将被翻译成:

printf("Y = %d\n", y);

这就是保留字#__VA_ARGS__的作用,它的出现实现了可变参数宏。

(未完,有空写)

posted @ 2017-05-30 20:04  miao_zheng  阅读(494)  评论(0编辑  收藏  举报