Linux c语言之关键字复习

来源:互联网 发布:大二申请国外大学知乎 编辑:程序博客网 时间:2024/05/29 19:44

1.static  

全局静态变量:作用范围局限于它的源文件,即只有本文件内的代码才可以访问它,变量名在其他文件内不可见
 
局部静态变量:局限于特定函数,但出作用域并不释放,在函数体内的静态变量的值也能够维持
 
静态函数:作用范围仅限于它的源文件,即只有本文件内才能够调用,函数名在其他文件不可见
 
存放位置:程序开始时,存放在全局数据区,结束时释放空间,默认初始化值是0,使用时可改变其值;

2.const

 const是 constant的缩写,是恒定不变的意思,也翻译为常量和常数等。很不幸,正是因为这一点,很多人都认为被const修饰的值是常量。这是不精确的,精确来说应该是只读的变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容。或许当初这个关键字应该被替换为readonly。

const作用

2.1 修饰只读变量
 定义const只读变量,具有不可变性。例如:
const int max = 10;
int array[max];
 .这里请分别创建c.c文件和cpp.c文件并测试一下。你会发现在.c文件中,编译器会提示出错,而在cpp.c文件中则顺利运行。为什么呢?我们知道定义一个数组必须指定其元素的个数,这也从侧面证实在c语言中,const修饰的仍然是变量,只不过是只读变量属性罢了;而在c++里,扩展了const的含义 
注意:const修饰的只读变量必须在定义的同时初始化

2.2节省空间,避免不必要的内存分配,同时提高效率

    编译器通常不为普通const只读变量分配存储空间,而是将它们保存符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。例如:
    #define M 3        // 宏常量
    const int N = 5;   // 此时并未将N放入内存中
    ....
    int i = N;     // 此时为N分配内存,以后不再分配
    int j = M;    //  预编译期间进行宏替换,分配内存
    int k = N;   //  没有内存分配
    int a = M;   //  再进行宏替换,又一次分配内存 
 
    const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份备份(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个备份,#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。#define宏没有类型,而const修饰的只读变量具有特定的类型。

2.3修饰一般变量

 一般变量是指简单类型的只读变量。这种只读变量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。例如:
int const i = 2;

const int i = 2;

2.4修饰数组

定义或说明一个只读数组可采用如下格式:
int const a[5] = {1,2,3,4,5};

const int a[5] = {1,2,3,4,5}

2.5修饰指针

const int * p;             // p可变,p指向的对象不可变
 
int const * p;             // p可变,p指向的对象不可变
 
int * const p;             // p不可变,p指向的对象可变
 
const int * const p;    //指针p和p指向的对象都不可变
 
这里给出一个记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看const离哪个近,“近水楼台先得月”,离谁近就修饰谁
const (int) *p      //const 修饰*p,p是指针,*p是指针指向的对象,不可变。
(int) const * p;    //const 修饰*p,p是指针,*p是指针指向的对象,不可变。
( int) * const p;   //const 修饰p,p不可变,p指向的对象可变
const ( int)* const p  // 前一个const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变

2.6修饰函数参数

const修饰也可以修饰函数的参数,当不希望这个参数值在函数体内被意外改变时使用,例如:
   void Fun(const int *p);
告诉编译器*p在函数体中不能改变,从而防止了使用者的一些无意的或错误的修改。

2.7修饰函数返回值

const修饰符也可以修饰函数的返回值,返回值不可被改变。例如:

const int Fun(void)

3.extern

extern,外面的、外来的意思,那它有什么作用呢?举个例子:假设你在大街上看到一个黑皮肤、绿眼睛、红头发的美女(外星人?)或者帅哥,你的第一反应就是这人不是国产的。extern就相当于他们的这些区别于中国人的特性。extern可以置于变量或函数前,以表明变量或函数的定义在别的文件中,下面代码用到的这些变量或函数是外来的,不是本文件定义的,提示链接器遇到此变量和函数时在其他模块中解析/绑定此标识符。就好比在本文件中给这些外来的变量或函数“带了顶帽子”,告诉本文件中所有代码,这些家伙不是“土著”。试想extern修饰的变量或函数是定义还是声明?

 
 看例子:
A.c文件中定义:
int i = 10;
void fun()
{
    //code;
}
 
B.c文件中用extern修饰:
extern int i;              // 写成i = 10行吗?
extern void fun();
 
C.h文件中定义:
int j = 1;
int k = 2;
 
D.c文件中用extern修饰:
extern double j;          //这样行吗?为什么?
j = 3.0;                        //这样行吗?为什么?
 
 4.union

通过前面的讲解,我们知道结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在C语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union),它的定义格式为:
union 共用体名{
    成员列表
};
共用体有时也被称为联合或者联合体,这也是 Union 这个单词的本意。
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
 
共用体也是一种自定义类型,可以通过它来创建变量,例如:
union data{
    int n;
    char ch;
    double f;
};
union data a, b, c;
 
上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量:
union data{
    int n;
    char ch;
    double f;
} a, b, c;
 
如果不再定义新的变量,也可以将共用体的名字省略:
union {
    int n;
    char ch;
    double f;
} a, b, c;
 
共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存,请看下面的演示:
#include <stdio.h>
union data{
    int n;
    char ch;
    short m;
};
int main(){
    union data a;
    printf("%d, %d\n", sizeof(a), sizeof(union data) );
    a.n = 0x40;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.ch = '9';
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.m = 0x2059;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.n = 0x3E25AD54;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
   
    return 0;
}
运行结果:
4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54
这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。
 
要想理解上面的输出结果,弄清成员之间究竟是如何相互影响的,就得了解各个成员在内存中的分布。以上面的 data 为例,各个成员在内存中的分布如下: 
成员 n、ch、m 在内存中“对齐”到一头,对 ch 赋值修改的是前一个字节,对 m 赋值修改的是前两个字节,对 n 赋值修改的是全部字节。也就是说,ch、m 会影响到 n 的一部分数据,而 n 会影响到 ch、m 的全部数据。
 
上图是在绝大多数 PC 机上的内存分布情况,如果是 51 单片机,情况就会有所不同:


 
共用体的应用
共用体在一般的编程中应用较少,在单片机中应用较多。对于 PC 机,经常使用到的一个实例是: 现有一张关于学生信息和教师信息的表格。学生信息包括姓名、编号、性别、职业、分数,教师的信息包括姓名、编号、性别、职业、教学科目。请看下面的表格:


f 和 m 分别表示女性和男性,s 表示学生,t 表示教师。可以看出,学生和教师所包含的数据是不同的。现在要求把这些信息放在同一个表格中,并设计程序输入人员信息然后输出。
 
如果把每个人的信息都看作一个结构体变量的话,那么教师和学生的前 4 个成员变量是一样的,第 5 个成员变量可能是 score 或者 course。当第 4 个成员变量的值是 s 的时候,第 5 个成员变量就是 score;当第 4 个成员变量的值是 t 的时候,第 5 个成员变量就是 course。
 
经过上面的分析,我们可以设计一个包含共用体的结构体,请看下面的代码:
 #include <stdio.h>
#include <stdlib.h>
#define TOTAL 4  //人员总数
struct{
    char name[20];
    int num;
    char sex;
    char profession;
    union{
        float score;
        char course[20];
    } sc;
} bodys[TOTAL];
int main(){
    int i;
    //输入人员信息
    for(i=0; i<TOTAL; i++){
        printf("Input info: ");
        scanf("%s %d %c %c", bodys[i].name, &(bodys[i].num), &(bodys[i].sex), &(bodys[i].profession));
        if(bodys[i].profession == 's'){  //如果是学生
            scanf("%f", &bodys[i].sc.score);
        }else{  //如果是老师
            scanf("%s", bodys[i].sc.course);
        }
        fflush(stdin);
    }
    //输出人员信息
    printf("\nName\t\tNum\tSex\tProfession\tScore / Course\n");
    for(i=0; i<TOTAL; i++){
        if(bodys[i].profession == 's'){  //如果是学生
            printf("%s\t%d\t%c\t%c\t\t%f\n", bodys[i].name, bodys[i].num, bodys[i].sex, bodys[i].profession, bodys[i].sc.score);
        }else{  //如果是老师
            printf("%s\t%d\t%c\t%c\t\t%s\n", bodys[i].name, bodys[i].num, bodys[i].sex, bodys[i].profession, bodys[i].sc.course);
        }
    }
    return 0;
}
运行结果:
Input info: HanXiaoXiao 501 f s 89.5↙
Input info: YanWeiMin 1011 m t math↙
Input info: LiuZhenTao 109 f t English↙
Input info: ZhaoFeiYan 982 m s 95.0↙
 
Name            Num     Sex     Profession      Score / Course
HanXiaoXiao     501     f       s               89.500000
YanWeiMin       1011    m       t               math
LiuZhenTao      109     f       t               English
ZhaoFeiYan      982     m       s               95.000000
 

4.1大小端模式

如果我们将0x1234abcd 写入到以0x0000 开始的内存中,则Little endian 和Big endian 模式的存放结果如下:

union
{
    int i;
    char a[2];
}*p,u;
 
p = &u;
p->a[0] = 0x39;
p->a[1] = 0x38
 
p.i的值应该为多少?
这里需要考虑存储模式:大端模式和小端模式。
大端模式:字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。
小端模式:字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。
 
    union型数据所占的空间等于其最大的成员所占的空间,对union型成员的存取都从相对与该联合体基地址的偏移量为0处开始,也就是联合体的访问不论对哪个变量的存取都是从union的首地址位置开始。

函数判断大小端

 int checkSystem( )
{
union check
{
int i;
char ch;
} c;    
c.i = 1;
return (c.ch ==1);
}
 
 5.C语言位域

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。请看下面的例子:
struct bs{
    unsigned m;
    unsigned n: 4;
    unsigned char ch: 6;
}
:后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。成员 n、ch 被:后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、6 位(Bit)的内存。
n、ch 的取值范围非常有限,数据稍微大些就会发生溢出,请看下面的例子:
#include <stdio.h>
int main(){
    struct bs{
        unsigned m;
        unsigned n: 4;
        unsigned char ch: 6;
    } a = { 0xad, 0xE, '$'};
    //第一次输出
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
    //更改值后再次输出
    a.m = 0xb8901c;
    a.n = 0x2d;
    a.ch = 'z';
    printf("%#x, %#x, %c\n", a.m, a.n, a.ch);
    return 0;
}
运行结果:
0xad, 0xe, $
0xb8901c, 0xd, :
 
对于 n 和 ch,第一次输出的数据是完整的,第二次输出的数据是残缺的。
 
第一次输出时,n、ch 的值分别是 0xE、0x24('$' 对应的 ASCII 码为 0x24),换算成二进制是 1110、10 0100,都没有超出限定的位数,能够正常输出。
 
第二次输出时,n、ch 的值变为 0x2d、0x7a('z' 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101、111 1010,都超出了限定的位数。超出部分被直接截去,剩下 1101、11 1010,换算成十六进制为 0xd、0x3a(0x3a 对应的字符是 :)。
 
C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度。
 
例如上面的 bs,n 的类型是 unsigned int,长度为 4 个字节,共计 32 位,那么 n 后面的数字就不能超过 32;ch 的类型是 unsigned char,长度为 1 个字节,共计 8 位,那么 ch 后面的数字就不能超过 8。
 
我们可以这样认为,位域技术就是在成员变量所占用的内存中选出一部分位宽来存储数据。
 
C语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了。
但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。
 
位域的存储
 C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间。
 
位域的具体存储规则如下:
1) 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
 
以下面的位域 bs 为例:
#include <stdio.h>
int main(){
    struct bs{
        unsigned m: 6;
        unsigned n: 12;
        unsigned p: 4;
    };
    printf("%d\n", sizeof(struct bs));
    return 0;
}
4
m、n、p 的类型都是 unsigned int,sizeof 的结果为 4 个字节(Byte),也即 32 个位(Bit)。m、n、p 的位宽之和为 6+12+4 = 22,小于 32,所以它们会挨着存储,中间没有缝隙。
 sizeof(struct bs) 的大小之所以为 4,而不是 3,是因为要将内存对齐到 4 个字节,以便提高存取效率
如果将成员 m 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32,n 会从新的位置开始存储,相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节。
 
如果再将成员 p 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。
2) 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。
请看下面的位域 bs:
 #include <stdio.h>
int main(){
    struct bs{
        unsigned m: 12;
        unsigned char ch: 4;
        unsigned p: 4;
    };
    printf("%d\n", sizeof(struct bs));
    return 0;
}
在 GCC 下的运行结果为 4,三个成员挨着存储;在 VC/VS 下的运行结果为 12,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。
m 、ch、p 的长度分别是 4、1、4 个字节,共计占用 9 个字节内存,为什么在 VC/VS 下的输出结果却是 12 呢?
3) 如果成员之间穿插着非位域成员,那么不会进行压缩。例如对于下面的 bs:
struct bs{
    unsigned m: 12;
    unsigned ch;
    unsigned p: 4;
};
通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
 
无名位域
 位域成员可以没有名称,只给出数据类型和位宽,如下所示:
struct bs{
    int m: 12;
    int  : 20;  //该位域成员不能使用
    int n: 4;
};
无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。
 
上面的例子中,如果没有位宽为 20 的无名成员,m、n 将会挨着存储,sizeof(struct bs) 的结果为 4;有了这 20 位作为填充,m、n 将分开存储,sizeof(struct bs) 的结果为 8。
 

6.c语言位运算

所谓位运算,就是对一个比特(Bit)位进行操作。比特(Bit)是一个电子元器件,8个比特构成一个字节(Byte),它已经是粒度最小的可操作单元了。
 
C语言提供了六种位运算符:
&,|, ^,~,<<,>>

按位与运算(&)
 一个比特(Bit)位只有 0 和 1 两个取值,只有参与&运算的两个位都为 1 时,结果才为 1,否则为 0。例如1&1为 1,0&0为 0,1&0也为 0,这和逻辑运算符&&非常类似。
C语言中不能直接使用二进制,&两边的操作数可以是十进制、八进制、十六进制,它们在内存中最终都是以二进制形式存储,&就是对这些内存中的二进制位进行运算。其他的位运算符也是相同的道理。
例如,9 & 5可以转换成如下的运算:(32位逐个对应相与)

也就是说,按位与运算会对参与运算的两个数的所有二进制位进行&运算,9 & 5的结果为 1。
 
再强调一遍,&是根据内存中的二进制位进行运算的,而不是数据的二进制形式;其他位运算符也一样。以-9&5为例,-9 的在内存中的存储和 -9 的二进制形式截然不同 

-9在内存中的存储1111 1111  1111 1111  1111  1111  1111 0111

-9的二进制形式   -0000 0000 0000 0000 0000 0000 0000 1001 //多余的0可以去掉
按位与运算通常用来对某些位清 0,或者保留某些位。例如要把 n 的高 16 位清 0 ,保留低 16 位,可以进行n & 0XFFFF运算(0XFFFF 在内存中的存储形式为 0000 0000 -- 0000 0000 -- 1111 1111 -- 1111 1111)。
  
按位或运算(|)
参与|运算的两个二进制位有一个为 1 时,结果就为 1,两个都为 0 时结果才为 0。例如1|1为1,0|0为0,1|0为1,这和逻辑运算中的||非常类似。(以内存中存储形式进行或运算)
 
按位或运算可以用来将某些位置 1,或者保留某些位。例如要把 n 的高 16 位置 1,保留低 16 位,可以进行n | 0XFFFF0000运算(0XFFFF0000 在内存中的存储形式为 1111 1111 -- 1111 1111 -- 0000 0000 -- 0000 0000)。
【实例】对上面的分析进行校验。
 #include <stdio.h>
int main(){
    int n = 0X2D;
    printf("%d, %d, %X\n", 9 | 5, -9 | 5, n | 0XFFFF0000);
    return 0;
}
运行结果:
13, -9, FFFF002D
 
按位异或运算(^)//相同为0,不同为1
 参与^运算两个二进制位不同时,结果为 1,相同时结果为 0。例如0^1为1,0^0为0,1^1为0。

按位异或运算可以用来将某些二进制位反转。例如要把 n 的高 16 位反转,保留低 16 位,可以进行n ^ 0XFFFF0000运算(0XFFFF0000 在内存中的存储形式为 1111 1111 -- 1111 1111 -- 0000 0000 -- 0000 0000)。
 
取反运算(~)
 取反运算符~为单目运算符,右结合性,作用是对参与运算的二进制位取反。例如~1为0,~0为1,这和逻辑运算中的!非常类似。
 
左移运算(<<)
 左移运算符<<用来把操作数的各个二进制位全部左移若干位,高位丢弃,低位补0。(从最末尾的一位开始左移即最右边的开始左移)
 
例如,9<<3可以转换为如下的运算:
《 0000 0000 0000 0000 0000 0000 0000 1001

    0000 0000 0000 0000 0000 0000 0100  1000

所以9<<3的结果为 72。
 
又如,(-9)<<3可以转换为如下的运算:


所以(-9)<<3的结果为 -72
 
如果数据较小,被丢弃的高位不包含 1,那么左移 n 位相当于乘以 2 的 n 次方。
 
【实例】对上面的结果进行校验。
#include <stdio.h>
int main(){
    printf("%d, %d\n", 9<<3, (-9)<<3 );
    return 0;
}
运行结果:
72, -72
 
右移运算(>>)
 右移运算符>>用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1。如果数据的最高位是 0,那么就补 0;如果最高位是 1,那么就补 1。
 如果被丢弃的低位不包含 1,那么右移 n 位相当于除以 2 的 n 次方(但被移除的位中经常会包含 1)。
例如,9>>3可以转换为如下的运算:(从最左边开始右移)

>>0000 0000 0000 0000 0000 0000 0000 1001

    0000 0000 0000 0000 0000 0000 0000 0001



9.typedef--给类型起别名

起别名的目的不是为了提高程序运行效率,而是为了编码方便。例如有一个结构体的名字是 stu,要想定义一个结构体变量就得这样写:
struct stu stu1;
struct 看起来就是多余的,但不写又会报错。如果为 struct stu 起了一个别名 STU,书写起来就简单了:
STU stu1;
这种写法更加简练,意义也非常明确,不管是在标准头文件中还是以后的编程实践中,都会大量使用这种别名。
 
使用关键字 typedef 可以为类型起一个新的别名,语法格式为:
typedef  oldName  newName;
oldName 是类型原来的名字,newName 是类型新的名字。例如:
typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;
INTEGER a, b;等效于int a, b;
typedef 还可以给数组、指针、结构体等类型定义别名。先来看一个给数组类型定义别名的例子:
typedef char ARRAY20[20];
表示 ARRAY20 是类型char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:
ARRAY20 a1, a2, s1, s2;
它等价于:
char a1[20], a2[20], s1[20], s2[20];
又如,为结构体类型定义别名:
typedef struct stu{
    char name[20];
    int age;
    char sex;
} STU;
STU 是 struct stu 的别名,可以用 STU 定义结构体变量:
STU body1,body2;
它等价于:
struct stu body1, body2;
 
再如,为指针类型定义别名:
typedef int (*PTR_TO_ARR)[4];
表示 PTR_TO_ARR 是类型int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:
PTR_TO_ARR p1, p2;
按照类似的写法,还可以为函数指针类型定义别名:
typedef int (*PTR_TO_FUNC)(int, int);
PTR_TO_FUNC pfunc;
 需要强调的是,typedef 是赋予现有类型一个新的名字,而不是创建新的类型。为了“见名知意”,请尽量使用含义明确的标识符,并且尽量大写。
 
 

 
 
 
 
 


 

原创粉丝点击