结构体、共用体和位运算

来源:互联网 发布:js search方法查找中文 编辑:程序博客网 时间:2024/04/30 11:23

1.C语言结构体的定义和使用

在实际问题中,一组数据往往具有不同的数据类型;例如在学生信息登记表中,姓名为字符型,学号为整型或字符型,年龄为整型,性别为字符型,成绩为整型或实型。因为数据类型不同,显然不能用一个数组来存放。

在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。定义结构体的一般形式为:

struct 结构体名{
    成员列表
};
每个成员都是结构体的组成部分,有名字,也有数据类型,形式为:
类型说明符 成员名;
例如用结构体来表示学生信息:
  1. struct stu{
  2. char *name; //姓名
  3. int num; //学号
  4. char sex; //性别
  5. float score; //成绩
  6. };
结构体是一种构造数据类型(也称复杂数据类型),由若干不同类型的变量(数据)组成。每个变量都是结构体的成员(member),每个成员可以是基本数据类型,也可以是构造类型。
相应的,int、char、float 这些数据类型被称为基本数据类型。
上面的代码定义了一个结构体,名字为stu。stu由4个成员组成,分别为 name、num、sex、score。

注意大括号后面的分号;不能少。

结构体变量

结构体也是一种数据类型,可以用来说明变量。例如:
struct stu stu1, stu2;
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 4 个成员组成。注意关键字struct不能少。

stu 就像一个“模板”,定义出来的变量都具有相同的性质。也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。

结构体的各个成员在内存中是连续存储的。stu1、stu2 的内存如下图所示:
你也可以在定义结构体的同时定义结构体变量:
  1. struct stu{
  2. char *name; //姓名
  3. int num; //学号
  4. char sex; //性别
  5. float score; //成绩
  6. } stu1, stu2;
将变量放在结构体定义的最后即可。

如果只需要 stu1、stu2 两个变量,也可以不写结构体名字:
  1. struct{ //没有写 stu
  2. char *name; //姓名
  3. int num; //学号
  4. char sex; //性别
  5. float score; //成绩
  6. } stu1, stu2;
这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。

也可以用宏定义使一个符号常量来表示一个结构类型,例如:
  1. #define STU struct stu
  2. STU{
  3. char *name; //姓名
  4. int num; //学号
  5. char sex; //性别
  6. float score; //成绩
  7. };
  8. STU stu1, stu2;
后面我们会学习Windows编程,大部分结构体都是这样定义的。

成员的获取和赋值

在C语言中,使用点号.来获取结构体中的一个成员,一般格式为:
结构变量名.成员名
例如:
  1. stu1.name; //第一个学生的姓名
  2. stu2.score; //第二个学生的成绩
获取成员后,就可以对该成员进行赋值了,例如:
  1. stu1.name = "Tom";
  2. stu2.score = 90.5;
除了可以对成员进行逐一赋值,也可以像数组一样整体赋值,例如:
  1. struct stu{
  2. char *name; //姓名
  3. int num; //学号
  4. char sex; //性别
  5. float score; //成绩
  6. } stu1, stu2 = { "Tom", 10, 'M', 90 };
不过整体赋值仅限于上面这样的情况,也就是定义结构体的同时声明变量。下面的写法是错误的:
stu2 = { "Tom", 10, 'M', 90 };

一个完整的示例:
  1. #include <stdio.h>
  2. #define STU struct stu
  3. int main(){
  4. STU{
  5. char *name; //姓名
  6. int num; //学号
  7. char sex; //性别
  8. float score; //成绩
  9. };
  10. STU stu1;
  11. stu1.name = "James Bond";
  12. stu1.num = 1;
  13. stu1.sex = 'M'; //用M表示男性,W表示女性
  14. stu1.score = 99;
  15. printf("Hello everyone! My name is %s, a naughty boy, but with good scores(%.2f) and top No.(%d)!", stu1.name, stu1.score, stu1.num);
  16. return 0;
  17. }
运行结果:
Hello everyone! My name is James Bond, a naughty boy, but with good scores(99.00) and top No.(1)!


2.C语言结构体数组

结构体数组的每一个元素都是结构体。在实际应用中,经常用结构体数组来表示一个拥有相同数据结构的群体,比如一个班的学生,一个车间的职工等。

定义结构体数组和定义结构体变量的方式类似,例如:

  1. struct stu{
  2. char *name;
  3. int num;
  4. char sex;
  5. float score;
  6. }class[5];
表示该班级有5个学生。

结构体数组可以初始化赋值,例如:
  1. struct stu{
  2. char *name;
  3. int num;
  4. char sex;
  5. float score;
  6. }class[5] = {
  7. {"Li ping", 5, 'M', 45},
  8. {"Zhang ping", 4, 'M', 62.5},
  9. {"He fang", 1, 'F', 92.5},
  10. {"Cheng ling", 2, 'F', 87},
  11. {"Wang ming", 3, 'M', 58}
  12. };
当对数组中全部元素赋值时,也可不给出数组长度。

结构体数组的使用也很简单,例如,获取“Wang ming”的成绩:
class[4].score;
【示例】计算学生的平均成绩和不及格的人数。
  1. #include <stdio.h>
  2. struct stu{
  3. char *name;
  4. int num;
  5. char sex;
  6. float score;
  7. }class[5] = {
  8. {"Li ping", 5, 'M', 45},
  9. {"Zhang ping", 4, 'M', 62.5},
  10. {"He fang", 1, 'F', 92.5},
  11. {"Cheng ling", 2, 'F', 87},
  12. {"Wang ming", 3, 'M', 58}
  13. };
  14. int main(){
  15. int i, fail_num=0;
  16. float average, sum=0;
  17. for(i=0; i<5; i++){
  18. sum += class[i].score;
  19. if(class[i].score < 60) fail_num++;
  20. }
  21. printf("sum=%.2f\naverage=%.2f\nfail_num=%d\n", sum, sum/5, fail_num);
  22. return 0;
  23. }
运行结果:
sum=345.00
average=69.00
fail_num=2


3.C语言结构体和指针

指针也可以指向一个结构体变量。定义的一般形式为:

struct 结构体名 *变量名;
前面已经定义了一个结构体 stu:
  1. struct stu{
  2. char *name;
  3. int num;
  4. char sex;
  5. float score;
  6. };
下面定义一个指向 stu 的指针变量 pstu:
struct stu *pstu;
当然在定义结构体的同时也可以定义 pstu:
  1. struct stu{
  2. char *name;
  3. int num;
  4. char sex;
  5. float score;
  6. } *pstu, stu1, stu2;
pstu 可以指向结构体变量:
pstu = &stu1;pstu = &stu2;
应该把结构体变量的首地址赋给 pstu,而不能把结构体名赋给 pstu,而且结构体变量前面要加取地址符&。下面的写法都是错误的:
pstu = &stu;pstu = stu1;
结构体名和结构体变量是两个不同的概念,不能混淆。结构体名只能表示一个结构形式,是一种数据类型,编译器并不对它分配内存空间,就像 int、float 这些关键字本身不分配内存一样。只有当一个变量被定义为这种数据类型时,才对该变量分配内存空间。所以上面 &stu 这种写法是错误的,不可能去取一个结构体名的首地址。

另外,不要认为结构体和数组类似,数组名表示数组的首地址,而结构体变量不表示首地址,它表示的是结构体本身这块内存空间的整体。就像int a;,a 不是首地址,而是表示它本身。

有了结构体指针,就可以获取结构体成员了,一般形式为:
(*结构指针变量).成员名
或者:
结构指针变量->成员名
例如:
(*pstu).num
或者:
pstu->num
注意(*pstu)两侧的括号不可少,因为成员符.的优先级高于*。如去掉括号写作*pstu.num,那么等效于*(pstu.num),这样意义就完全不对了。

【示例】结构体指针变量的使用。
  1. #include <stdio.h>
  2. struct stu{
  3. char *name;
  4. int num;
  5. char sex;
  6. float score;
  7. } *pstu, stu1 = {"He fang", 1, 'F', 92.5};
  8. int main(){
  9. pstu = &stu1;
  10. printf("Number=%d, Name=%s\n",stu1.num, stu1.name);
  11. printf("Sex=%c, Score=%f\n\n",stu1.sex, stu1.score);
  12. printf("Number=%d, Name=%s\n",(*pstu).num, (*pstu).name);
  13. printf("Sex=%c, Score=%f\n\n",(*pstu).sex, (*pstu).score);
  14. printf("Number=%d, Name=%s\n",pstu->num, pstu->name);
  15. printf("Sex=%c, Score=%f\n",pstu->sex, pstu->score);
  16. return 0;
  17. }
运行结果:
Number=1, Name=He fang
Sex=F, Score=92.500000

Number=1, Name=He fang
Sex=F, Score=92.500000

Number=1, Name=He fang
Sex=F, Score=92.500000


4.C语言结构体数组指针以及函数

结构体数组指针

指针变量可以指向一个结构体数组,这时指针变量的值是整个数组的首地址。

设 ps 为指向结构体数组的指针变量,则 ps 也指向该结构体数组的第0个元素,ps+1指向第一个元素,ps+i则指向第i元素,这与普通数组的情况是一样的。

【示例】用指针变量输出结构体数组。
  1. #include <stdio.h>
  2. struct stu{
  3. int num;
  4. char *name;
  5. char sex;
  6. float score;
  7. } *ps, boy[5]={
  8. {101, "Zhou ping", 'M', 45},
  9. {102, "Zhang ping", 'M', 62.5},
  10. {103, "Liou fang", 'F', 92.5},
  11. {104, "Cheng ling", 'F', 87},
  12. {105, "Wang ming", 'M', 58}
  13. };
  14. int main(){
  15. printf("No\tName\t\tSex\tScore\t\n");
  16. for(ps=boy; ps<boy+5; ps++)
  17. printf("%d\t%s\t%c\t%f\t\n", ps->num, ps->name, ps->sex, ps->score);
  18. return 0;
  19. }
运行结果:
No      Name            Sex     Score101     Zhou ping       M       45.000000102     Zhang ping      M       62.500000103     Liou fang       F       92.500000104     Cheng ling      F       87.000000105     Wang ming       M       58.000000

应该注意的是,一个结构体指针变量虽然可以用来访问结构体变量或结构体数组元素的成员,但是,不能使它指向一个成员。也就是说不允许取一个成员的地址来赋予它。所以,下面的赋值是错误的:
ps=&boy[1].sex;
而只能是:
ps=boy;  // 赋予数组首地址
或者是:
ps=&boy[0];  //赋予0号元素首地址

结构体指针变量作函数参数

结构体变量代表的是结构体本身这个整体,而不是首地址,作为函数参数时传递的整个结构体,也就是所有成员。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,严重降低程序的效率。所以最好的办法就是使用指针,也就是用指针变量作为函数参数。这时由实参传向形参的只是地址,非常快速。

【示例】计算一组学生的平均成绩和不及格人数。
  1. #include <stdio.h>
  2. #define STU struct stu
  3. STU{
  4. int num;
  5. char *name;
  6. char sex;
  7. float score;
  8. }boy[5]={
  9. {101,"Li ping",'M',45},
  10. {102,"Zhang ping",'M',62.5},
  11. {103,"He fang",'F',92.5},
  12. {104,"Cheng ling",'F',87},
  13. {105,"Wang ming",'M',58}
  14. };
  15. void average(STU *ps);
  16. int main(){
  17. STU *ps = boy;
  18. average(ps);
  19. return 0;
  20. }
  21. void average(struct stu *ps){
  22. int flunk=0, i;
  23. float sum=0;
  24. for(i=0; i<5; i++,ps++){
  25. sum += ps->score;
  26. if(ps->score < 60) flunk += 1;
  27. }
  28. printf("sum=%.2f, average=%.2f, flunk=%d\n", sum, sum/5, flunk);
  29. }
运行结果:
sum=345.00, average=69.00, flunk=2


5.C语言枚举类型

实际问题中,有些变量的取值被限制在一定范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等。

C语言提供了一种枚举(Enum)类型,可以列出所有可能的取值。定义形式为:

enum 变量名{ 枚举值列表 };
这些值也称为枚举元素。注意最后的分号;不能少。

例如,列出一个星期有几天:
enum week{sun, mon, tue, wed, thu, fri, sat};
和结构体一样,枚举变量可以先定义后说明,也可以在定义的同时说明,例如:
enum week a, b, c;
或者:
enum week{sun, mon, tue, wed, thu, fri, sat} a, b, c;
枚举值为常量,不是变量,不能赋值。枚举值默认从0开始,逐个加1。也就是说,上面的代码定义了7个常量,分别为 sun、mon…sat,它们的值分别为 0、1…6。

【示例】枚举的使用。
  1. #include <stdio.h>
  2. int main(){
  3. enum week{
  4. sun, mon, tue, wed, thu, fri, sat
  5. } a, b, c;
  6. a = sun;
  7. b = mon;
  8. c = tue;
  9. printf("%d, %d, %d\n",a,b,c);
  10. return 0;
  11. }
运行结果:
0, 1, 2

因为枚举值是常量,不能赋值,所以下面的写法是错误的:
sun = 5;mon = 2;
只能把枚举值赋予枚举变量,例如:
a = sun;b = sat;
同时,不建议把数值直接赋给枚举变量,例如:
a = 1;b = 6;
如果一定要使用数值,必须使用强制类型转换:
a = (enum week)1;b = (enum week)6;
因为已经使用了 sun、mon…sat 几个标识符,所以不能再使用它们来定义变量等,例如:
int sun = 3;char mon;
都是错误的。


6.C语言共同体(共用体)

共用体(共同体)的定义和结构体类似,不过结构体的各个成员都会分配相应的内存空间,而共用体的所有成员共享一段内存,它们的起始地址一样,并且同一时刻只能使用其中的一个成员变量。

共用体在实际开发中应用较少,你可以暂时跳过,需要时再来温习。
共用体定义的一般格式为:
union 共用体名{
    成员列表
};
例如:
  1. //先定义共用体,再定义变量
  2. union data{
  3. int i;
  4. char ch;
  5. };
  6. data a, b, c;
或者:
  1. //定义共用体的同时定义变量
  2. union data{
  3. int i;
  4. char ch;
  5. } a, b, c;
共用体所占用的内存空间大小等于最长的成员所占用的字节数。共用体使用了覆盖技术,几个成员变量相互覆盖,从而使几个不同变量共占同一段内存。这也就意味着,同一时刻只能存放一个成员变量的值,只能有一个成员存在,不可能像结构体一样同时存放。如果对新的成员变量赋值,就会把原来成员变量的值覆盖掉。

共用体 data 中,成员 i 所占用的空间最大,为 4 个字节,所以 data 类型的变量(也就是a、b、c)也占用4个字节的内存。请看下面的例子:
  1. #include <stdio.h>
  2. union{
  3. int i;
  4. char c;
  5. }a;
  6. int main(){
  7. printf("Size of a: %d\n", sizeof(a));
  8. a.c='A'; //此时共用体变量4个字节的取值情况为0x00000041
  9. printf("a.i = %d\n",a.i);
  10. a.i=0x42; //0x42为字母B的ASCII码
  11. printf("a.c = %c\n",a.c);
  12. return 0;
  13. }
运行结果:
Size of a: 4
a.i = 65
a.c = B

两个不能:不能使用共用体变量,只能引用共用体变量中的成员。不能在定义共用体变量时进行初始化。


7.C语言类型定义符typedef

C语言不仅提供了丰富的数据类型,还允许用户定义自己的数据类型。

定义数据类型使用 typedef 关键字,一般形式为:

typedef 原类型名 新类型名;
例如:
  1. typedef int INTEGER;
  2. INTEGER a, b;
  3. a = 1;
  4. b = 2;
INTEGER a, b;等效于int a, b;

用 typedef 定义数组、指针、结构体等类型会带来很大的方便,不仅使程序书写简单而且意义更加明确。例如:
typedef char NAME[20];
表示NAME是字符数组类型,长度为20。然后可用NAME 说明变量,如:
NAME a1, a2, s1, s2;
完全等效于:
char a1[20], a2[20], s1[20], s2[20];
又如:
  1. typedef struct stu{
  2. char name[20];
  3. int age;
  4. char sex;
  5. } STU;
定义STU表示stu的结构体类型,然后可用STU来说明结构体变量:
STU body1,body2;

两点说明:

1) 自定义数据类型必须以现有的数据类型为基础,可以认为是现有类型的“别名”,并没有产生真正意义上的数据类型。为了“见名知意”,自定义数据类型一般大写,尽量使用含义明确的标识符,例如:
typedef   (int*)   PINT;
2) 有时也可用宏定义来代替 typedef 的功能,但是宏定义是在预处理阶段完成的,而 typedef 是在编译时完成的。


8.C语言位运算

所谓位运算,就是对一个比特(Bit)位进行操作。在《二进制思想以及数据的存储》一节中讲到,比特(Bit)是一个电子元器件,8个比特构成一个字节(Byte),它已经是粒度最小的可操作单元了。

C语言提供了六种位运算符:
运算符&|^~<<>>说明按位与按位或按位异或取反左移右移

按位与运算

一个比特(Bit)位只有0和1两个取值,只有参与&运算的两个位都为1时,结果才为1,否则为0。例如1&1为1,0&0为0,1&0为0。

数值在内存中以二进制的形式存在,9&5可写算式如下:
       00001001    (9的二进制)
    &00000101    (5的二进制)
       00000001    (1的二进制)
所以9&5=1
严格来说,数值在内存中以补码形式存在,整数的补码与它的二进制形式相同,负数则不一样,不了解的读者可自行脑补。
按位与运算符&会对参与运算的两个数的所有二进制位进行&运算。

按位与运算通常用来对某些位清0或保留某些位。例如把 c 的高16位清 0 ,保留低16位,可作a&65535运算(65536占用4个字节,二进制数为00000000000000001111111111111111)。

【示例】位运算举例。
  1. #include <stdio.h>
  2. int main(){
  3. unsigned a=9; //二进制数 00001001
  4. unsigned b=5; //二进制数 00000101
  5. unsigned c=0XDE09A32B; //十进制数 3725173547
  6. unsigned d=0X0000FFFF; //十进制数 65535
  7. printf("a=%u, b=%u, a&b=%u\n", a, b, a&b);
  8. printf("c=%u, d=%u, c&d(%%d)=%u, c&d(%%X)=%X\n", c, d, c&d, c&d);
  9. return 0;
  10. }
运行结果:
a=9, b=5, a&b=1
c=3725173547, d=65535, c&d(%d)=41771, c&d(%X)=A32B

按位或运算

参与或运算|的两个二进制位有一个为1时,结果就为1,两个都为0时结果才为0。例如1|1为1,0|0为0,1|0为1。

9|5可写算式如下:
     00001001    (9的二进制)
    |00000101    (5的二进制)
     00001101    (13的二进制)
所以9|5=13

按位或运算可以用来将某些二进制位置1,而保留某些位。

【示例】或运算举例。
  1. #include <stdio.h>
  2. int main(){
  3. unsigned a=9; //二进制数 00001001
  4. unsigned b=5; //二进制数 00000101
  5. unsigned c=0XDE09A30B; //十进制数 3725173547
  6. unsigned d=0XFFFF0000; //十进制数 65535
  7. printf("a=%u, b=%u, a|b=%u\n", a, b, a|b);
  8. printf("c=%u, d=%u, c|d(%%d)=%u, c|d(%%X)=%X\n", c, d, c|d, c|d);
  9. return 0;
  10. }
运行结果:
a=9, b=5, a|b=13
c=3725173515, d=4294901760, c|d(%d)=4294943499, c|d(%X)=FFFFA30B

按位异或运算

参与异或运算^的两个二进制位不同时,结果为1,相同时结果为0。也就是说,0^1为1,0^0为0,1^1为0。

9^5可写成算式如下:
      00001001    (9的二进制)
    ^00000101    (5的二进制) 
      00001100    (12的二进制)
所以9^5=12

按位异或运算可以用来反转某些二进制位。

【示例】异或运算举例。
  1. #include <stdio.h>
  2. int main(){
  3. unsigned a=9; //二进制数 00001001
  4. unsigned b=5; //二进制数 00000101
  5. unsigned c=0X00FFFF00; //十进制数 3725173547
  6. unsigned d=0XFFFF0000; //十进制数 65535
  7. printf("a=%u, b=%u, a^b=%u\n", a, b, a^b);
  8. printf("c=%u, d=%u, c^d(%%d)=%u, c^d(%%X)=%X\n", c, d, c^d, c^d);
  9. return 0;
  10. }
运行结果:
a=9, b=5, a^b=12
c=16776960, d=4294901760, c^d(%d)=4278255360, c^d(%X)=FF00FF00

取反运算

取反运算符~为单目运算符,右结合性,作用是对参与运算的数的各二进位按位取反。例如 ~1为0,~0为1。

~9的运算为:
    ~0000000000001001
      1111111111110110
所以~9=65526

左移运算

左移运算符<<用来把操作数的各二进位全部左移若干位,高位丢弃,低位补0。例如:
a=9;a<<3;
<<左边是要移位的操作数,右边是要移动的位数。

上面的代码表示把a的各二进位向左移动3位。a=00001001(9的二进制),左移3位后为01001000(十进制72)。

右移运算

右移运算符>>用来把操作数的各二进位全部右移若干位,低位丢弃,高位补0(或1)。例如:
a=9;a>>3;
表示把a的各二进位向右移动3位。a=00001001(9的二进制),右移3位后为00000001(十进制1)。

需要注意的是,对于有符号数,在右移时,符号位将随同移动。当为正数时,最高位补0,而为负数时,符号位为1,最高位是补0或是补1 取决于编译器的规定。

【示例】位操作综合示例。
  1. #include <stdio.h>
  2. int main(){
  3. unsigned c=0X00FFFF00; //十进制数 3725173547
  4. unsigned d=0XFFFF0000; //十进制数 65535
  5. printf("c=%X, d=%X, c^d(%%X)=%X, c|d(%%X)=%X, c>>4=%X, c<<8=%X\n", c, d, c^d, c|d, c>>4, c<<8);
  6. return 0;
  7. }
运行结果:
c=FFFF00, d=FFFF0000, c^d(%X)=FF00FF00, c|d(%X)=FFFFFF00, c>>4=FFFF0, c<<8=FFFF0000


9.C语言位域(位段)

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如开关只有通电和断电两种状态,用0和1表示足以,也就是用一个二进位。所以C语言又提供了一种数据结构,称为位域位段

位域在应用开发中较少使用,你可以暂时跳过,遇到相关问题再回来温习。
所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个区域有一个域名,允许在程序中按域名进行操作。

位域的定义和位域变量的说明

位域的定义与结构类似,形式为:
struct 位域结构名{
    位域列表
};
其中位域列表的形式为:
类型说明符 位域名: 位域长度;
例如:
  1. struct bs{
  2. int a:8;
  3. int b:2;
  4. int c:6;
  5. };
可以先定义位域再定义位域变量,也可以同时定义。例如:
struct bs data;
或者:
  1. struct bs{
  2. int a:8;
  3. int b:2;
  4. int c:6;
  5. } data;
说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。

对位域的几点说明

1) 一个位域必须存储在同一个字节中,不能跨两个字节。如果一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
  1. struct bs{
  2. unsigned a:4;
  3. unsigned :0; //空域
  4. unsigned b:4; //从下一单元开始存放
  5. unsigned c:4
  6. }
在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。

2) 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。

3) 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
  1. struct k{
  2. int a:1;
  3. int :2; //该2位不能使用
  4. int b:3;
  5. int c:2;
  6. };
从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。

位域的使用和结构成员的使用相同,其一般形式为:
位域变量名·位域名;
位域允许用各种格式输出。

【示例】位域的使用。
  1. #include <stdio.h>
  2. int main(){
  3. struct{
  4. unsigned a:1;
  5. unsigned b:3;
  6. unsigned c:4;
  7. } bit, *pbit;
  8. bit.a=1;
  9. bit.b=7;
  10. bit.c=15;
  11. printf("%d, %d, %d\n", bit.a, bit.b, bit.c);
  12. pbit=&bit;
  13. pbit->a=0;
  14. pbit->b&=3;
  15. pbit->c|=1;
  16. printf("%d, %d, %d\n", pbit->a, pbit->b, pbit->c);
  17. return 0;
  18. }
运行结果:
1, 7, 15
0, 3, 15

程序第15行使用了复合的位运算符&=,该行相当于:
pbit->b=pbit->b&3;
位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算符|=,相当于:
pbit->c=pbit->c|1;
其结果为15。


10.用C语言对数据或文件内容进行加密

数据加密解密是一个常用的功能,如果你不希望让别人看到文件中的内容,可以通过密钥(也称”密码“)将文件的内容加密。比如文本文件(.txt),加密前的内容是能够读懂的,加密后的内容是”乱码“,都是一些奇怪的字符,根本无法阅读。

数据加密解密的原理也很简单,就是使用异或运算。请先看下面的代码:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main(){
  4. char plaintext = 'a'; // 明文
  5. char secretkey = '!'; // 密钥
  6. char ciphertext = plaintext ^ secretkey; // 密文
  7. char decodetext = ciphertext ^ secretkey; // 解密后的字符
  8. char buffer[9];
  9. printf(" char ASCII\n");
  10. // itoa()用来将数字转换为字符串,可以设定转换时的进制(基数)
  11. // 这里将字符对应的ascii码转换为二进制
  12. printf(" plaintext %c %7s\n", plaintext, itoa(plaintext, buffer, 2));
  13. printf(" secretkey %c %7s\n", secretkey, itoa(secretkey, buffer, 2));
  14. printf("ciphertext %c %7s\n", ciphertext, itoa(ciphertext, buffer, 2));
  15. printf("decodetext %c %7s\n", decodetext, itoa(decodetext, buffer, 2));
  16. return 0;
  17. }
运行结果:
            char    ASCII plaintext   a     1100001 secretkey   !      100001ciphertext   @     1000000decodetext   a     1100001
看到了吗,plaintext 与 decodetext相同,也就是说,两次异或运算后还是原来的结果。

这就是加密的关键技术:
  • 通过一次异或运算,生成密文,密文没有可读性,与原文风马牛不相及,这就是加密;
  • 密文再经过一次异或运算,就会还原成原文,这就是解密的过程;
  • 加密和解密需要相同的密钥,如果密钥不对,是无法成功解密的。

上面的加密算法称为对称加密算法,加密和解密使用同一个密钥。

如果加密和解密的密钥不同,则称为非对称加密算法。在非对称算法中,加密的密钥称为公钥,解密的密钥称为私钥,只知道公钥是无法解密的,还必须知道私钥。

注意:程序中的 itoa() 位于 stdlib.h 头文件,它并不是一个标准的C函数,只有Windows下有,更多信息请猛击:C语言itoa()函数和atoi()函数详解


11.C语言贪吃蛇游戏源码下载、源码解析和设计思路

在《C语言贪吃蛇游戏演示和说明》一节中,我们对贪吃蛇游戏的玩法进行了介绍和演示,这节就来分析一下它的源码。

贪吃蛇源代码下载地址:http://pan.baidu.com/s/1bnwJB8V    提取密码:81qm

各位读者不妨先将源码下载下来浏览一遍,记住关键的几个函数,整理一下不了解的知识点,做到心中有数。

需要说明的是:贪吃蛇背景地图、食物、贪吃蛇本身都是由特殊字符组成(由 printf() 输出),并不是绘制出来的图形。C语言标准库没有绘图函数,如果绘图的话,就需要使用第三方库,增加了大家的学习成本,所以我们采用了“投机取巧”的办法,用特殊字符来模拟不同的图形。

一. 关键知识点

下面请各位读者先学习一下该游戏中涉及到的几个关键知识点,有了这些必备条件,我们才好讲解贪吃蛇的设计思路。

1) 改变输出文本的颜色

贪吃蛇游戏的背景地图是是绿色的,边框是钻红色的,食物是红色的,贪吃蛇本身是黄色的,这就涉及到如何改变文本的输出颜色,请大家猛击《彩色版的C语言》了解详情。

2) 在任意位置输出文本

在一般的程序中,字符都是依次输出的,例如当前控制台上显示的是“123456”,如果我们希望输出“abcd”,那么“abcd”就位于“123456”之后。在一般的程序中这是没有问题的,但是对于贪吃蛇游戏,我们需要自己来控制字符的输出位置,例如:
  • 输出背景地图后,我们需要在背景地图中间输出贪吃蛇和食物;
  • 要统计贪吃蛇吃掉的食物的数量,就必须不断改变同一位置的数字。

这是如何做到的呢?请大家猛击《C语言在任意位置输出》了解详情。

3) 键盘监听

在贪吃蛇移动过程中,必须能够及时捕获用户按下的方向键,并改变移动方向,这是如何做到的呢?请大家猛击《C语言非阻塞式键盘监听》了解详情。

4) 获取随机数

贪吃蛇的食物会随机出现在背景地图上的任意位置,没有任何规律,这就要求程序生成一对随机数值,来控食物所在的行和列。那么,随机数值是如何产生的呢?请大家猛击《C语言获取随机数》了解详情。

二. 输出贪吃蛇背景地图

贪吃蛇背景地图的最终效果如下图所示:


钻红色空心方框表示边框,绿色实心方框表示贪吃蛇的活动区域。实现代码如下:
  1. #include <stdio.h>
  2. #include <conio.h>
  3. #include <windows.h>
  4. int main(){
  5. int width = 30, height = width; //宽度和高度
  6. int x, y; //x、y分别表示当前行和列
  7. HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
  8. //设置窗口大小
  9. system("mode con: cols=64 lines=32");
  10. //打印背景,按行输出
  11. for(x=0; x<width; x++){
  12. for(y=0; y<height; y++){
  13. if(y==0 || y==width-1 || x==0 || x==height-1){ //输出边框
  14. SetConsoleTextAttribute(hConsole, 4 );
  15. printf("□");
  16. }else{ //贪吃蛇活动区域
  17. SetConsoleTextAttribute(hConsole, 2 );
  18. printf("■");
  19. }
  20. }
  21. printf("\n");
  22. }
  23. //暂停
  24. getch();
  25. return 0;
  26. }
程序的关键是两层嵌套的循环。x=0 时,内层循环执行30次,输出第0行;x=1 时,内层循环又执行30次,输出1行。以此类推,直到 x=30,外层循环不再执行(内存循环当然也就没机会执行),输出结束。
注意,□和■虽然都是单个字符,但它们不在ASCII码范围内,是宽字符,占用两个字节,用 putchar 等输出ASCII码(一个字节)的函数输出时可能会出现问题,所以作为字符串输出。

三. 让贪吃蛇移动起来

接下来,我们来让一条长度为 n 的贪吃蛇移动起来,而且可以用WASD四个键控制移动方向,如下图所示:


其实,移动贪吃蛇并不需要移动所有节点,只需要添加蛇头、删除蛇尾,就会有动态效果,这样会大大提高程序的效率。

我们可以定义一个结构体来表示贪吃蛇一个节点在控制台上的位置(也即所在行和列):
  1. struct POS{
  2. int x; //所在行
  3. int y; //所在列
  4. }

然后再定义一个比贪吃蛇长的数组来保存贪吃蛇的所有节点:
struct POS snakes[n+m];
并设置两个变量 headerIndex、tailIndex,分别用来表示蛇头、蛇尾在数组中的下标坐标,这样每次添加蛇头、删除蛇尾时只需要改变两个变量的值就可以。如下图所示:


headerIndex 和 tailIndex 都向前移动,也就是每次减1。如果 headerIndex=0,也就是指向数组的头部,那么下次移动时 headerIndex = arrayLength - 1,也就是指向数组的尾部,就这样一圈一圈地循环,tailIndex 也是如此。这相当于把数组首尾相连成一个圆圈,贪吃蛇在这个圆圈中不停地转圈。

由于这部分的演示代码较长,请大家到百度网盘下载:http://pan.baidu.com/s/1bouZGoZ    提取密码:4g74

对代码的说明

1) 贪吃蛇的最大长度为绿色方框的个数,所以我们将容纳贪吃蛇的数组 snakes 的长度定义为(HEIGHT-2) * (WIDTH-2)

2) □、■、★ 占用两个字符的宽度,所以在 setPosition() 中该变光标位置时,光标的X坐标应该是:
coord.X = 2*y;

四. 随机生成食物

食物的生成是贪吃蛇游戏的难点,因为食物只能在绿色背景(■)部分生成,它不能占用钻红色边框(□)贪吃蛇本身(★)的位置。

最容易想到的思路是:随机生成一个坐标,然后检测该坐标是不是绿色背景,如果是,那么成功生成,如果不是,继续生成随机数,继续检测。幸运的话,可以一次生成;不幸的话,可能要循环好几次甚至上百次才能生成,这样带来的后果就是程序卡死一段时间,贪吃蛇不能移动。

这种方案的优点就是思路简单,容易实现,缺点就是贪吃蛇移动不流畅,经常会卡顿。

改进的方案

最好的方案是生成的随机数一定会在绿色背景的范围内,这样一次就能成功生成食物。该如何实现呢?

这里我们提供了一种看起来不容易理解却行之有效的方案。

我们不妨将贪吃蛇的活动范围称为“贪吃蛇地图”,而加上边框就称为“全局地图”。首先定义一个二维的结构体数组,用来保存所有的点(也即全局地图):
  1. struct{
  2. char type;
  3. int index;
  4. }globalMap[MAXWIDTH][MAXHEIGHT];
MAXWIDTH 为宽度,也即列数;MAXHEIGHT 为高度,也即行数。成员 type 表示点的类型,它可以是食物、绿色背景、边框和贪吃蛇节点。
直观上讲,应该将 type 定义为int类型,不过int占用四个字节,而节点类型的取值范围非常有限,一个字节就足够了,所以为了节省内存才定义为char类型。
然后再定义一个一维的结构体数组,用来保存贪吃蛇的有效活动范围:
  1. struct{
  2. int x;
  3. int y;
  4. } snakeMap[ (MAXWIDTH-2)*(MAXHEIGHT-2) ];
x、y 表示行和列,也就是 globalMap 数组的两个下标。globalMap 数组中的 index 成员就是 snakeMap 数组的下标。

globalMap 表示了所有节点的信息,而 snakeMap 只表示了贪吃蛇的活动区域。通过 snakeMap 可以定位 globalMap 中的元素,反过来通过 globalMap 也可以找到 snakeMap 中的元素。它们之间的对应关系请看下图:

图1:globalMap 和 snakeMap 的初始对应关系

贪吃蛇向左移动时,headerIndex 指向 404,tailIndex指向 406。

在 snakeMap 数组中,贪吃蛇占用一部分元素,剩下的元素都是绿色的背景,可以随机选取这些元素中的一个作为食物,然后通过 x、y 确定食物的坐标。而这个坐标,一定在绿色背景范围内。
需要注意的是,在贪吃蛇移动过程中需要维护 globalMap 和 snakeMap 的对应关系。
这种方案的另外一个优点就是,贪吃蛇移动时很容易知道下一个节点的类型,不用遍历数组就可以知道是否与自身相撞。

1 0