C语言易混淆关键词详解-const, static, extern, typedef, 声明

简介:

Const

Const关键词并不能把一个变量变成一个常量, 在符号前加上const表示这个符号不能被赋值, 即他的值对这个符号来说是只读的, 但并不代表这个值不能用其他方法去改变. 通过下面的例子就能比较好理解,

int i = 5;
const int *a = &i;
*a = 8;   //报错, 只读不能赋值
i = 10;   //OK

Const最有用处的地方是用它来限定函数的形参, 来表明该函数不能修改实参指针所指向的数据. 同上面的理解, 并不表示这个数据是常量, 在函数外是可以修改的. 如void func(const char *) 

Const出现的位置也比较让人困惑, c太灵活的坏处

char *p              = "hello";          // 非const指针, 非const数据
const char *p        = "hello";          //非const指针 const数据
char * const p       = "hello";          // const指针,非const数据
const char * const p = "hello";          // const指针,const数据

你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量

Static 

C语言 中,

static 局部变量 , 生存期为这个源程序, 不过作用域仍难是局部

int fun()

{

     static int a = 1;

     a++;

     print('%d',a);

}

只有第一次调用该函数时a会被初始化为1, 后面每次调用a都会增加1, 所以只要程序不结束这个static a是一直存在的
但他是局部变量, 所以在fun函数之外无法访问, 虽然static a 一直存在


static 全局变量

全局变量本身就是静态存储方式, 再加上static, 是改变他的作用域, 即只能本当前文件访问. 而非static 全局变量的作用域为整个源程序

所以对局部变量, static改变的是他的生存期, 而对于全局变量, static改变的是他的作用域
对于c这样用相同关键词, 却用做完全不同的用处, 真是无法理解, 简直是在忽悠用户


static 函数

在c语言中, 函数的默认作用域是全局可见的, 即整个源程序, 你也可以给函数加上个冗余的extern, 来表示其作用域

如果在函数前加上static, 表示将其作用域缩小至本文件, 同于静态全局变量的用法.
此处普遍认为是c语言的设计失误, 不应该默认将作用域设为全局, 容易造成命名空间冲突.


C++ 中

C++中除了C中的用法, 还多了static成员变量, 和static成员函数的用法

表示属于一个类而不是属于此类的任何特定对象的变量和函数. 这是与普通成员函数的最大区别, 也是其应用所在, 比如在对某一个类的对象进行计数时, 计数生成多少个类的实例, 就可以用到静态数据成员.

在这里面, static既不是限定作用域的, 也不是扩展生存期的作用, 而是指示变量/函数在此类中的唯一性. 这也是”属于一个类而不是属于此类的任何特定对象的变量和函数”的含义.

所以对静态成员的引用不需要用对象名, 可以直接使用类名,
静态成员函数仅能访问静态的数据成员,不能访问非静态的数据成员,也不能访问非静态的成员函数,这是由于静态的成员函数没有this指针


Extern

参考自(http://blog.csdn.net/keensword/archive/2005/06/23/401114.aspx)

如果想明白为什么需要extern, 需要从编译和链接讨论起,

现代编译器一般采用按文件编译的方式,因此在编译时,各个文件中定义的全局变量是互相透明的,也就是说,在编译时,全局变量的可见域限制在文件内部。但是到了链接阶段,要将各个文件的内容“合为一体”,因此,如果某些文件中定义的全局变量名相同的话,会报错. 因此,各个文件中定义的全局变量名不可相同。

//A.cpp
int i;

void main()
{

}

//B.cpp
int i;

所以上面两个文件编译是没有问题的, 但是到了链接就会报重名错误

如果此时A.cpp里面要用到, B.cpp中定义的i, 应该怎么办?

那么既然上面说了重复定义出错, 那就把A.cpp中的"int i;"定义直接去掉是否可以

看起来好像可以的, 因为全局变量的作用域是整个源程序, 这边也许是很多人会产生疑问的地方, 既然全局变量和全局函数的作用域是整个源程序的, 为什么在其他的文件里面使用一定要先声明(这样的声明往往在.h文件中, 并在使用处include该.h, 记住include就是copy, 之所以要使用.h, 而不是直接写在.c中, 只是为了保证易维护性, 最终编译器会自动将.h copy到每个.c中)


答案就在编译阶段, 过程是先编译后链接, 而在编译时只能知道本文件的内容, 编译器并不能预见到你这个变量或函数在其他文件里面被定义过. 只有到了链接阶段编译器产能看到其他的文件.

所以如果不事先声明, 那么在编译阶段一定会报错找不到该变量或函数.

extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”


多说一句, 在声明变量是必须要加extern, 而在声明函数时却不需要, 为什么

上面说了, 声明只是简单的告诉编译器, 这个东西在其他地方定义过了, 你不用管了, 所以编译器不会为声明分配空间, 或做其他操作, 这和定义是有本质区别的, 必须要正确区分

对于变量必须用extern才能区分定义和声明, 因为这是变量定义和声明的唯一区别

而函数不需要extern也能区分处定义和声明, 有实现就是定义, 没有就是声明, 所以不需要再加extern

这就是c的简洁之处, 不需要的就别写


再多说一句, 在c中, 全局变量和函数都是默认对外可见的, 如果想变成仅当前文件可见, 必须加上static.

对于函数, 默认和加上extern是等价的, 都是表示对外可见

但是对于变量, 确不一样, 加上extern就变成声明了, 所以不能给定义加上extern

所以对于extern有如下说法,

用于变量,声明该变量在其它地方定义;
用于函数定义, 表示全局可见(属于冗余的)

总觉得c语言的设计者是在玩程序员, 不把你绕进去, 他不爽. 你不能把每个程序员都想的和你智商一样高啊...


extern“c”

extern "c" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。

extern的意思通过上面的解释, 应该是很明白了, 那么C什么意思

这个就要谈起C和C++的混合编译, 重要的是C和C++编译器对变量名的改写方式是不同的

对于C编译器, 往往是在变量名和函数名之前统一加上了一个下划线

int func(int t)    >>>   PUBLIC    _func


而对于C++编译器, 则要复杂的多, 因为C++中有函数重载等, 允许相同的函数名有不同的参数, 和不同的作用域, 所以使用name mangling来唯一标识每个函数, 比如上面的函数,  被编译成了func@@YAHH@Z

//A.CPP 
void func();

void main()
{
         func();
}

//B.C 
void func()
{

}

所以象上面C和C++文件的混合编译, 就会报错, A.obj : error LNK2001: unresolved external symbol "void __cdecl func(void)" (?func@@YAXXZ)

这个错以前经常看到的说...

为什么会报这个错了, 因为你在A.cpp编译是声明了func, 所以编译通过, 然后在链接的时候, 编译器就会去找这个func函数, 因为这个是C++编译器, 所以他就是去找func@@YAHH@Z, 结果没找到, 编译器发现被骗了...于是不干了

为啥找不到了, 因为你下面的B.C是用C编译器编译的, 所以生成的函数名是_func, 而不是 func@@YAHH@Z, 所以发生这个情况

你把A.cpp中的声明改成这样就可以了, 明确告诉C++编译器, 这个函数的名字不要乱改, 还是用c的方式, 这样就能找到了
//A.CPP
extern "C"
{
    void func();
}

void main()
{
    func();
}
 

补充一下, 这个问题对于全局变量一样存在, 在C++中调用C中的全局变量一样要加 extern "C", 来限制name mangling.


Struct Union Enum

struct

在C中结构的定义是这样的

struct optional_tag {
    type_1 identifier_1;
    type_2 identifier_2;
    ...
    type_N identifier_N;
} optional_variable_definitions;


So with the declarations
struct date_tag { short dd,mm,yy; } my_birthday, xmas;
struct date_tag easter, groundhog_day;

Variables my_birthday, xmas, easter, and groundhog_day all have the identical type.


在结构中允许出现位段, 无名段, 填充段

struct pid_tag {
unsigned int inactive :1;
unsigned int :1;                 /* 1 bit of padding */
unsigned int refcount :6;
unsigned int :0;                 /* pad to next word boundary*/
short pid_id;
struct pid_tag *link;
};

This is commonly used for "programming right down to the silicon," and you'll see it in systems programs. It can also be used for storing aBoolean flag in a bit rather than a char . A bit field must have a type of int, unsigned int, or signed int (or a qualified version of one of these).


下面给两个struct的比较有意思的用法,

数组copy

int a[100], b[100];

如果这时想将b数组这个copy到a数组, 或把数组作为参数或返回值(虽然这样不常用, 一般用指针)

比较简单的办法, 是把数组分装到struct中

struct s_tag { int a[100]; };
struct s_tag orange, lime, lemon;

//先初始化lemon

orange = lemon; /* assigns entire struct */


结构体常用来实现链表, tree之类的数据结构

struct node_tag { int datum;
                             struct node_tag *next;
};
struct node_tag a,b;
a.next = &b;          /* example link-up */
a.next->next=NULL;


字节对齐

说到结构, 顺便谈一下字节对齐

计算机从存储器上读取数据的时候是以机器字为单位的, 机器字的大小取决于计算机本身的处理位数, 最常见的32位机, 机器字就是32位, 即4字节. 这个是合理的, 因为cpu的处理单位是一次32位, 所以必然一次也读32位, 多读了也处理不了.

既然一次读4字节, 所以从存储器上读取数据的时候, 只会从能被4整除的字节地址开始读, 即只能从机器字起始位置开始读

这样有个问题, 一般读取一个int, 只需要一个读周期, 因为int就是4字节, 刚好可以一个读周期被读到, 但是问题在于不能保证int存储的首地址是4的倍数, 也就是说这个int的存储跨越了2个机器字, 这样通过一个读周期就无法读出这个int了, 必须要两个读周期读出2个机器字, 然后拼出这个int来.

那么这样明显是低效的, 浪费了很多读指令周期, 那么要解决这个问题, 简单的方法就是尽量让数据存放在更少的机器字中, 即4字节对齐

先给出常用类型的字节大小

char                      在字节边界上对齐             N=1
short (16-bit)            在双字节边界上对齐           N=2
int (32-bit)              在4字节边界上对齐            N=4
long (32-bit)             在4字节边界上对齐            N=4
float                     在4字节边界上对齐            N=4
double                    在8字节边界上对齐            N=8


下面的例子就给出了4字节对齐

struct {char a; int b;} T1;

sizeof(T1) == 8; N = 4 中间填充了3字节

对于上面的例子, 一般的人可能认为size应该为5, 可是其实使用了8

那是因为编译器考虑4字节对齐, 在char后自动填充了3个字节


由于编译器的不同,对于四字节对齐的定义就不同,有的编译器会自动补齐成四字节,有的不会。这样会造成交叉编译时不兼容。因此在设计数据结构时,应该尽量设计成4字节的倍数。

struct {char a; char b;} T;

sizeof(T) == 2

上面这样的写法不太好, 应该习惯把他补齐

改为, struct {char a; char b;short pad ;} T;


注意调整结构体内的数据顺序可以有效的节省存储空间
struct {char a ; int i; char b;} t1;
struct {char a; char b; int i;} t2;
sizeof(t1)==12  
sizeof(t2)==8


Union

Union和struct的定义一样的, 只是把struct换成Union

但不同的是对于union来说, 所有的成员都是从偏移地址0开始存储, 即是重合的, 同一时间只能有一个成员真正存在, 而union的size就是成员中最大size.

Union一般用来节省空间, 结构中可能有些成员是不会同时出现的, 就把他封装在一个union中, 以节省空间

Union其他用途比如可以把同一个数据做不同解释,

union bits32_tag {
    int whole; /* one 32-bit value*/
    struct {char c0,c1,c2,c3;} byte; /* four 8-bit bytes*/
} value;

你即可以用value.whole取整个32bit int, 也可以用value.byte.c0取前8bit char


Enum

Enum的作用就是把一串名字和一串整型联系在一起, 可以说在c中Enum完全可以被#define取代, 比较鸡肋的

enum sizes { small=7, medium, large=10, humungous };

如果不赋值, 会从0开始递增, 或从赋的值开始递增

Enum一个优点, 便于debug

There is one advantage to enums: unlike #defined names which are typically discarded during compilation, enum names usually persist through to the debugger, and can be used while debugging your code.

 

C语言声明的解析

这个纯粹出于理论研究, 实际开发中, 如果写出这样需要看半天才明白的声明, 真是......

想正确解析c语言的声明先记住如下的优先级

优先级规则如下:
A 声明从它的名字 开始读取,然后按照优先级顺序依次读取;
B 优先级从高到低依次是:
        B.1 声明中被括号括起来 的那部分
        B.2 后缀 操作符;
             括号()表示这是一个函数,而
             方括号[] 表示这是一个数组。
        B.3 前缀 操作符; 星号 * 表示“指向...的指针”。
C 如果const 和 ( 或 ) volatile 关键字的后面紧跟类型说明符 (如 int, long等),那么它作用于类型说明符。在其他情况下,const 和 ( 或 ) volatile 关键字作用于它左边紧邻的指针星号。


然后来个例子, char * const * ( *next )( );

分析过程:
A、 首先,看变量名"next", 并注意到它直接被括号所括住;
B.1、所以先把括号里的东西作为一个整体,得出“next 是一个指向 ...的指针"。
B、 然后考虑括号外面的东西,在星号前缀和括号后缀之间做出选择。
B.2、规则告诉我们优先级较高的是右边的函数括号,所以得出”next是一个函数指针,指向一个返回...的函数”。
B.3、然后,处理前缀“*”,得出指针所指的内容。
C、 最后,把"char * const" 解释为指向字符的常量指针。
把上述分析结果加以概括,这个声明表示“next是一个指针,它指向一个函数,该函数返回另一个指针,该指针指向一个类型为char 的常量指针”,大功告成。


下面给出几个比较难看懂的声明

函数的返回值是一个函数指针:              int (* fun())();
函数的返回值是一个指向int数组的指针 : int (* foo())[]
数组的元素为函数指针:                        int (*foo[])()
数组的元素为数组, 多维数组:                int foo[][]

拿 int (* foo())[]详细分析一下

首先从左边找到第一个变量名foo, 明确foo是个函数, 而不是个指针 , 这点很重要, 因为()的优先级高于*

确定foo是函数后, 前面的*就表示foo的返回值为指针

括号外[]表示返回值是数组的指针

最后int表示 返回值是一个指向int数组的指针

多说一下, 如果int (*(* foo)())[], foo就是个指针, 因为加上了()


typedef

typedef为类型引入新的名字, 而不是为变量分配空间, 某些方面typedef类似于宏文本替换, 不过他们是有些不同的

在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。

和#define的区别

1.typedef是一种彻底的"封装"类型, 一旦声明不能再增加.

#define peach int
unsigned peach i; /* works fine */
typedef int banana;
unsigned banana i; /* Bzzzt! illegal */

对于#define只是单纯的文本替换, 所以在前面加上unsigned是可以的

而typedef就不可以, 编译器会认为你的语法不合法, 就象你写 float int i; 一样


2. typedef能保证声明中所有的变量类型一致

这点上比#define要强

#define int_ptr int *
int_ptr chalk, cheese;

typedef char * char_ptr;
char_ptr Bentley, Rolls_Royce;

这儿宏就是文本替换, 结果就是int * chalk, cheese; cheese被声明为int, 而非int *

而typedef就可以保证Bentley, Rolls_Royce都是char *

typedef的用法

不要为了省去写struct, 而对结构使用typedef, struct会给阅读code的人一些提示, 不应该省掉它

注意下面两句, 很容易混淆...nnd...C对于程序员就是噩梦

typedef struct fruit {int weight, price_per_lb } frt; //将struct fruit命名为frt
struct fruit {int weight, price_per_lb } apple;//定义struct fruit, 并创建struct fruit类型的变量 apple

这就是上面说的情况, 用typedef只是在创建变量时, 省去不用写struct

struct fruit lemon;

frt lemon;

虽然很多地方都看到这样使用typedef, 但这种用法不推荐 

typedef应该用在:

参考http://www.cnblogs.com/csyisong/archive/2009/01/09/1372363.html

1. 数组, 结构, 指针以及函数的组合类型

为复杂的声明定义一个新的简单的别名。方法是:在原来的声明里逐步用别名替换一部分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化版。举例:

原声明:void (*b[10]) (void (*)());
 
变量名为b,先替换右边部分括号里的,pFunParam为别名一:
typedef void (*pFunParam)();
再替换左边的变量b,pFunx为别名二:
typedef void (*pFunx)(pFunParam);
原声明的最简化版:
pFunx b[10];
 
原声明:doube(*)() (*e)[9];
变量名为e,先替换左边部分,pFuny为别名一:
typedef double(*pFuny)();
再替换右边的变量e,pFunParamy为别名二
typedef pFuny (*pFunParamy)[9];
原声明的最简化版:
pFunParamy e;


2. 可移植类型 .

比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedef long double REAL;
在不支持 long double 的平台二上,改为:
typedef double REAL;
在连 double 都不支持的平台三上,改为:
typedef float REAL;
也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。
标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健


3. 为后面的强制类型转化 提供一个简单的名字

typedef int (*ptr_to_int_fun)(void);
char * p;
(ptr_to_int_fun) p;

这边我们拿void (*b[10]) (void (*)());来详细分析一下

首先确认只是个数组b, size为10, 数组的元素是函数指针(无返回值, 参数为无返回值的函数指针)

其实你理解这个声明的意思, 就很容易简化了

所以首先为无返回值的函数指针定义一个别名叫pFunParam

typedef void (*pFunParam)();

当时我看到这个定义, 很疑惑, 一般看到的typedef都是类似, typedef double REAL; 

typedef后面两个参数, 把double称为REAL

但是这边就一个函数指针, 啥意思

其实你可以看成typedef void (*)() pFunParam;

但是你直接这样写, 会报错

我猜想对于这样的复杂组合类型, 只能写成这样的形式, 忍不住又要控诉c语言......太晦涩了

好继续, 再为函数指针(无返回值, 参数为无返回值的函数指针)定义一个别名pFunx

typedef void (*pFunx)(pFunParam);

好了, 你现在就把pFunx当做是int一样去定义数组

pFunx b[10];


本文章摘自博客园,原文发布日期:2011-07-05

目录
相关文章
|
4月前
|
存储 安全 编译器
『C语言进阶』const详解
『C语言进阶』const详解
|
28天前
|
程序员 C语言
在C语言中,typedef是一种用来创建新的数据类型名的关键字
在C语言中,typedef是一种用来创建新的数据类型名的关键字
9 0
|
5月前
|
程序员 C语言
【C语言】如何写出好(易于调试)的代码——assert和const的使用
【C语言】如何写出好(易于调试)的代码——assert和const的使用
22 0
|
1月前
|
存储 C语言
【C语言】const修饰指针的不同作用
【C语言】const修饰指针的不同作用
20 0
|
5月前
|
存储 安全 编译器
13 C++ - const关键字(比较C语言)
13 C++ - const关键字(比较C语言)
42 0
|
1月前
|
C语言
C语言中关键字static的三种用法
C语言中关键字static的三种用法
|
3月前
|
C语言
【C 语言经典100例】C 练习实例41 - static
【C 语言经典100例】C 练习实例41 - static
22 0
|
3月前
|
C语言 C++
C语言中结构体用到的typedef
C语言中结构体用到的typedef
23 0
|
4月前
|
C语言
c语言中的static静态(1)static修饰局部变量
c语言中的static静态(1)static修饰局部变量
26 1
|
5月前
|
C语言
关键字static在C语言中的三种用法
关键字static在C语言中的三种用法
37 0