本文主要是C语言基础——自定义数据类型相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows windows11 Ubuntu Ubuntu16.04的64位版本 VMware® Workstation 16 Pro 16.2.3 build-19376536 SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 开发板 正点原子 i.MX6ULL Linux阿尔法开发板 uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) linux内核 linux-4.15(NXP官方提供) STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
点击查看相关文件下载
一、结构体 我们常见的可以存储很多数据的结构就是数组了,但是它有一个缺点,就是只能存放相同的数据类型。
但在实际的问题处理过程中,有许多信息是由多个不同类型的数据组合在一起进行描述的,这些不同类型的数据是互相联系而又组成了一个有机的整体。
例如对于学生信息登记,整体就是整个学校或者整个班级的学生,那每个学生又有姓名,性别,年龄,学号,成绩等不同的属性。姓名为字符串,性别为字符串,年龄为整数,学号为字符串,成绩却又是浮点数,因为数据类型不同,显然不能用一个数组来存放。
这个时候就需要用到一种新的构造类型数据——结构体。
1.定义结构体 1.1一般定义格式 定义一个结构体数据类型,我们需要用到 struct 关键字:
1 2 3 4 5 6 7 struct structure_name { data_type member_name1; data_type member_name2; ... data_type member_nameN; };
点击查看各部分说明
struct 结构体定义的关键字,表明这是一个结构体的定义 structure_name 结构体的名称,与普通变量名要求一样 data_type 结构体内成员的数据类型,可以是基本变量类型,指针类型,枚举类型,联合体类型或者结构体类型等。比如,char,int等 member_nameN 结构体内成员的名称(成员变量名),命名规则与变量相同
【注意】
(1)结构体定义结束的时候要有分号( ; )。
(2) { } 内的部分也称为成员列表,或者叫域表。
(3)结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化 。
(4)结构体类型中的成员名可以与程序中的变量名相同 ,二者并不代表同一对象,编译程序可以自动对它们进行区分。
(5)它属于 C 语言的一种数据类型 ,与整型、实型相当。因此,定义结构体时不分配空间,只有用正结构体定义变量时才分配空间 。
(6)结构体成员中不允许存在静态变量,即不可以出现 static 。
1.2定义实例 1 2 3 4 5 6 7 8 struct Student { char *name; char gender; int age; char id[11 ]; float score; };
Student 为结构体名,它包含了 5 个成员,分别是 name 、 gender 、 age 、 id 、 score 。
2.定义结构体变量 结构体是一种数据类型,在定义完结构体数据类型之后,我们就可以使用该结构体数据类型来定义变量,这样定义的变量就称之为结构体变量。结构体变量定义一般格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 struct Student { char *name; char gender; int age; char id[11 ]; float score; }; struct Student stu1 , stu2 ;
上边定义了两个变量 stu1 和 stu2 ,它们都是 Student 结构体类型,都由 5 个成员组成。
【注意】
(1)关键字 struct 不可以省略,若没有这个关键字,则系统不认为 Student 是结构体类型。
(2) Student 就像一个“模板 ”,定义出来的变量都具有相同的性质 。
(3)程序中使用结构体变量的时候,一般不能把它作为一个整体参与数据的处理,能够参与各种运算符和操作的是结构体变量的各个成员项的数据。
1 2 3 4 5 6 7 8 9 struct Student { char *name; char gender; int age; char id[11 ]; float score; } stu1, stu2;
与方式一类似,定义了两个变量 stu1 和 stu2 ,它们都是 Student 结构体类型,都由 5 个成员组成。
【说明】 如果只需要 stu1 、 stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名 。
1 2 3 4 5 6 7 8 9 struct /* 省略结构体名称 */{ char *name; char gender; int age; char id[11 ]; float score; } stu1, stu2;
3.使用结构体变量 定义完了结构体变量,接下来就是对结构体变量的使用了,包括了赋值和引用两种操作。
3.1引用和赋值的一般格式 结构体变量是一种变量,那么我们自然可以引用变量中的成员,也可以对结构体变量成员进行赋值,这些操作都是通过成员运算符 . 完成的。
1 2 3 4 结构体变量名.成员名1 结构体变量名.成员名2 ... 结构体变量名.成员名N
1 2 3 4 结构体变量名.成员名1 = value1; 结构体变量名.成员名2 = value2; ... 结构体变量名.成员名N = valueN;
【注意】
(1)不能将结构体变量当做一个整体进行引用,只能对结构体变量的各个成员分别引用。
(2)在引用和赋值的过程中,要注意成员的数据类型,按我自己的理解就是当我们把结构体成员取出来的时候,取出来的这个成员就可以按照普通的变量进行赋值和引用,如果是成员是数组,那么赋值和引用就和普通数组一样。
(3)关于结构体变量的引用就上边的一种格式了,但是对于结构体变量的赋值,却有多种不同的方式。
(4)结构体变量虽然不可以整体引用,但是相同类型的结构体变量之间允许相互赋值 ,而不同类型的结构体变量之间不允许相互赋值 ,即使两者包含有同样的成员。
点击查看实例
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 #include <stdio.h> #include <string.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; } stu1 = {"qidaink" , 'm' , 18 , "0000000001" , 95.8 }; int main (int argc, char *argv[]) { struct Student stu2 ; stu2 = stu1; printf ("stu1.name = %s\n" , stu1.name); printf ("stu1.gender = %c\n" , stu1.gender); printf ("stu1.age = %d\n" , stu1.age); printf ("stu1.id = %s\n" , stu1.id); printf ("stu1.score = %.2f\n\n" , stu1.score); printf ("stu2.name = %s\n" , stu2.name); printf ("stu2.gender = %c\n" , stu2.gender); printf ("stu2.age = %d\n" , stu2.age); printf ("stu2.id = %s\n" , stu2.id); printf ("stu2.score = %.2f\n" , stu2.score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 7 8 9 10 11 stu1.name = qidaink stu1.gender = m stu1.age = 18 stu1.id = 0000000001 stu1.score = 95.80 stu2.name = qidaink stu2.gender = m stu2.age = 18 stu2.id = 0000000001 stu2.score = 95.80
3.2结构体变量赋值 关于结构体变量的这几种赋值方式,我们都通过实例来进行说明。
3.2.1赋值方式一 我们可以先定义结构体变量,然后再对各个成员进行赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct STU { char *name; char gender; int age; char id[11 ]; float score; }; struct STU stu1 ;stu1.name = "qidaink" ; stu1.gender = 'm' ; stu1.age = 18 ; strcpy (stu1.id, "0000000001" );stu1.score = 95.8 ;
点击查看赋值实例源码
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 #include <stdio.h> #include <string.h> struct STU { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct STU stu1 ; stu1.name = "qidaink" ; stu1.gender = 'm' ; stu1.age = 18 ; strcpy (stu1.id, "0000000001" ); stu1.score = 95.8 ; printf ("stu1.name = %s\n" , stu1.name); printf ("stu1.gender = %c\n" , stu1.gender); printf ("stu1.age = %d\n" , stu1.age); printf ("stu1.id = %s\n" , stu1.id); printf ("stu1.score = %.2f\n" , stu1.score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 stu1.name = qidaink stu1.gender = m stu1.age = 18 stu1.id = 0000000001 stu1.score = 95.80
3.2.2赋值方式二 我们可以在定义结构体变量的同时进行结构体成员的赋值。
1 2 3 4 5 6 7 8 9 10 11 12 struct STU { char *name; char gender; int age; char id[11 ]; float score; }; struct STU stu1 = {"qidaink" , 'm' , 18 , "0000000001" , 95.8 };
点击查看赋值实例源码
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 #include <stdio.h> #include <string.h> struct STU { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct STU stu1 = {"qidaink" , 'm' , 18 , "0000000001" , 95.8 }; printf ("stu1.name = %s\n" , stu1.name); printf ("stu1.gender = %c\n" , stu1.gender); printf ("stu1.age = %d\n" , stu1.age); printf ("stu1.id = %s\n" , stu1.id); printf ("stu1.score = %.2f\n" , stu1.score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 stu1.name = qidaink stu1.gender = m stu1.age = 18 stu1.id = 0000000001 stu1.score = 95.80
3.2.3赋值方式三 我们可以在定义结构体变量的同时进行结构体成员的乱序赋值 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct STU { char *name; char gender; int age; char id[11 ]; float score; }; struct STU stu1 ={ .id = "0000000001" , .gender = 'm' , .name = "qidaink" , .age = 18 , .score = 95.8 };
点击查看赋值实例源码
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 #include <stdio.h> #include <string.h> struct STU { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct STU stu1 = { .id = "0000000001" , .gender = 'm' , .name = "qidaink" , .age = 18 , .score = 95.8 }; printf ("stu1.name = %s\n" , stu1.name); printf ("stu1.gender = %c\n" , stu1.gender); printf ("stu1.age = %d\n" , stu1.age); printf ("stu1.id = %s\n" , stu1.id); printf ("stu1.score = %.2f\n" , stu1.score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 stu1.name = qidaink stu1.gender = m stu1.age = 18 stu1.id = 0000000001 stu1.score = 95.80
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct STU { char *name; char gender; int age; char id[11 ]; float score; }; struct STU stu1 ={ id : "0000000001" , gender : 'm' , name : "qidaink" , age : 18 , score : 95.8 };
点击查看赋值实例源码
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 #include <stdio.h> #include <string.h> struct STU { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct STU stu1 = { id : "0000000001" , gender : 'm' , name : "qidaink" , age : 18 , score : 95.8 }; printf ("stu1.name = %s\n" , stu1.name); printf ("stu1.gender = %c\n" , stu1.gender); printf ("stu1.age = %d\n" , stu1.age); printf ("stu1.id = %s\n" , stu1.id); printf ("stu1.score = %.2f\n" , stu1.score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 stu1.name = qidaink stu1.gender = m stu1.age = 18 stu1.id = 0000000001 stu1.score = 95.80
3.2.4赋值方式四 我们可以在定义结构体定义结构体时直接定义结构体变量,并直接对结构体变量赋值。
1 2 3 4 5 6 7 8 9 struct STU { char *name; char gender; int age; char id[11 ]; float score; } stu1 = {"qidaink" , 'm' , 18 , "0000000001" , 95.8 };
点击查看赋值实例源码
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 #include <stdio.h> #include <string.h> struct STU { char *name; char gender; int age; char id[11 ]; float score; } stu1 = {"qidaink" , 'm' , 18 , "0000000001" , 95.8 }; int main (int argc, char *argv[]) { printf ("stu1.name = %s\n" , stu1.name); printf ("stu1.gender = %c\n" , stu1.gender); printf ("stu1.age = %d\n" , stu1.age); printf ("stu1.id = %s\n" , stu1.id); printf ("stu1.score = %.2f\n" , stu1.score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 stu1.name = qidaink stu1.gender = m stu1.age = 18 stu1.id = 0000000001 stu1.score = 95.80
3.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 #include <stdio.h> #include <string.h> struct STU { char *name; struct { int year; int month; int day; } birthday; }; int main (int argc, char *argv[]) { struct STU stu1 ; stu1.name = "qidaink" ; stu1.birthday.year = 2000 ; stu1.birthday.month = 7 ; stu1.birthday.day = 7 ; printf ("stu1.name = %s\n" , stu1.name); printf ("stu1.birthday.year = %d\n" , stu1.birthday.year); printf ("stu1.birthday.month = %d\n" , stu1.birthday.month); printf ("stu1.birthday.day = %d\n" , stu1.birthday.day); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 stu1.name = qidaink stu1.birthday.year = 2000 stu1.birthday.month = 7 stu1.birthday.day = 7
4.结构体的长度 4.1计算格式 当我们定义了一个结构体后,用它定义出来的结构体变量占多大空间呢?我们可以使用 sizeof 函数来求得所定义结构体的长度。一般格式如下:
1 2 3 4 sizeof ( struct structure_name) struct structure_name structure_variable ;sizeof (structure_variable)
4.2使用实例 点击查看实例
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 #include <stdio.h> struct STU { char *name; char gender; int age; char id[3 ]; float score; }; int main (int argc, char *argv[]) { struct STU stu1 = {"qidaink" , 'm' , 18 , "01" , 95.8 }; printf ("sizeof(stu1) = %ld\n" , sizeof (stu1)); printf ("sizeof( struct Student) = %ld\n" , sizeof (struct STU)); printf ("sizeof(stu1.name) = %ld\n" , sizeof (stu1.name)); printf ("sizeof(stu1.gender) = %ld\n" , sizeof (stu1.gender)); printf ("sizeof(stu1.age) = %ld\n" , sizeof (stu1.age)); printf ("sizeof(stu1.id) = %ld\n" , sizeof (stu1.id)); printf ("sizeof(stu1.score) = %.ld\n\n" , sizeof (stu1.score)); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 7 sizeof(stu1) = 24 sizeof( struct Student) = 24 sizeof(stu1.name) = 8 sizeof(stu1.gender) = 1 sizeof(stu1.age) = 4 sizeof(stu1.id) = 3 sizeof(stu1.score) = 4
【说明 】 64 位平台下的指针变量固定为 8 字节。
我们会发现,总的大小并非是各个成员所占空间大小的总和,那我们就接着往后看。
5.在内存中的存储 当结构体定义了一个结构体变量的时候,这个结构体变量在内存中是怎样的呢?
点击查看测试实例
【说明】结构体在定义的时候并不分配内存空间,所以无法对结构体名称进行取地址操作,但是当用它定义了结构体变量之后,就可以使用 & 符号来获取结构体变量在内存中的起始地址了。
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 #include <stdio.h> struct STU { char *name; char gender; int age; char id[3 ]; float score; }; int main (int argc, char *argv[]) { struct STU stu1 = {"qidaink" , 'm' , 18 , "01" , 95.8 }; printf ("sizeof( struct STU) = %ld\n" , sizeof (struct STU)); printf ("&stu1 = %p,sizeof(stu1) = %ld\n" , &stu1, sizeof (stu1)); printf ("&stu1.name = %p,sizeof(stu1.name) = %ld\n" , &stu1.name, sizeof (stu1.name)); printf ("&stu1.gender = %p,sizeof(stu1.gender) = %ld\n" , &stu1.gender, sizeof (stu1.gender)); printf ("&stu1.age = %p,sizeof(stu1.age) = %ld\n" , &stu1.age, sizeof (stu1.age)); printf ("&stu1.id = %p,sizeof(stu1.id) = %ld\n" , &stu1.id, sizeof (stu1.id)); printf ("&stu1.id[0] = %p,sizeof(stu1.id[0]) = %ld\n" , &stu1.id[0 ], sizeof (stu1.id[0 ])); printf ("&stu1.id[1] = %p,sizeof(stu1.id[1]) = %ld\n" , &stu1.id[1 ], sizeof (stu1.id[1 ])); printf ("&stu1.id[2] = %p,sizeof(stu1.id[2]) = %ld\n" , &stu1.id[2 ], sizeof (stu1.id[2 ])); printf ("&stu1.score = %p,sizeof(stu1.score) = %.ld\n\n" , &stu1.score, sizeof (stu1.score)); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 7 8 9 10 sizeof( struct Student) = 24 &stu1 = 0x7fff1635a610,sizeof(stu1) = 24 &stu1.name = 0x7fff1635a610,sizeof(stu1.name) = 8 &stu1.gender = 0x7fff1635a618,sizeof(stu1.gender) = 1 &stu1.age = 0x7fff1635a61c,sizeof(stu1.age) = 4 &stu1.id = 0x7fff1635a620,sizeof(stu1.id) = 3 &stu1.id[0] = 0x7fff1635a620,sizeof(stu1.id[0]) = 1 &stu1.id[1] = 0x7fff1635a621,sizeof(stu1.id[1]) = 1 &stu1.id[2] = 0x7fff1635a622,sizeof(stu1.id[2]) = 1 &stu1.score = 0x7fff1635a624,sizeof(stu1.score) = 4
【说明】 64 位平台下的指针变量固定为 8 字节。
从最终例子中打印的数据分析, gender 是一个 char 类型的数据,只占用了 1 个字节的数据,但是它后边空出来了 3 个字节数据。数组 id 也是,总长度是 3 字节,”空”出了 1 个字节空间。这样算下来,总长度就是 8+4+4+8=24 了,这也就对应上了。但是,这些”空”出来的空间是为什么呢?这就涉及到了字节对齐,这个请看下一节。
6.字节对齐 6.1字节对齐是什么? 上一节,出现了一些”空”的地址空间,所谓”空”其实也不是里面真的什么都没有,它就同定义了一个变量但没有初始化一样,里面是一个很小的、负的填充字。为了便于表达,我们就暂且称之为”空”好了。
字节对齐也可以叫内存对齐,计算机中内存大小的基本单位是字节( byte ),理论上 来讲,可以从任意地址访问 某种基本数据类型,但是实际上 ,计算机并非逐字节大小读写内存 ,而是以 2 , 4 ,或 8 的倍数的字节块来读写内存,如此一来就会对基本数据类型的合法地址作出一些限制,即它的地址必须是 2 , 4 或 8 的倍数。
这种对齐限制简化了形成处理器和存储器系统之间的接口的硬件设计。对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐 。例如在 32 位 CPU 下,假设一个整型变量的地址为 0x00000004 ,那它就是自然对齐的。再比如存放 int 型数据的地址一定要是 4 的倍数,存放 short 型数据的地址一定要是 2 的倍数。
6.2为什么要字节对齐? 根本在于为了让 CPU 更有效率地访问内存空间。例如,假设一个处理器总是从存储器中取出 8 个字节,则地址必须为 8 的倍数。如果我们能保证将所有的 double 类型数据的地址对齐成 8 的倍数,那么就可以用一个存储器操作来读或者写值了。如果对象被分放在两个 8 字节存储块中,横跨两个区域,我们就需要执行两次存储器访问操作。
点击查看实例
假设一个整型变量的地址不是自然对齐,比如为 0x00000002 ,则 CPU 如果取它的值的话需要访问两次内存,第一次取从 0x00000002-0x00000003 的一个 short 长度的数据,第二次取从 0x00000004-0x00000005 的一个 short 长度的数据,然后组合得到所要的数据;
如果变量在 0x00000003 地址上的话则要访问三次内存,第一次为 char 长度,第二次为 short 长度,第三次为 char 长度,然后组合得到整型数据。
如果变量在自然对齐位置上,则只要一次就可以取出数据。
6.3对齐原则? 点击查看 #pragma pack 说明
这是一个预处理指令。
1 2 #pragma pack(n) #pragma pack()
还有其他的用法,暂时用到的不过,后边有机会再专门总结一下吧。
点击查看实例 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 #include <stdio.h> #pragma pack(1) struct Student { char *name; char gender; int age; char id[3 ]; float score; }; #pragma pack() struct Test { char *name; char gender; int age; char id[3 ]; float score; }; int main (int argc, char *argv[]) { struct Student stu1 = {"qidaink" , 'm' , 18 , "01" , 95.8 }; printf ("sizeof(stu1) = %ld\n" , sizeof (stu1)); printf ("sizeof( struct Student) = %ld\n" , sizeof (struct Student)); printf ("sizeof(stu1.name) = %ld\n" , sizeof (stu1.name)); printf ("sizeof(stu1.gender) = %ld\n" , sizeof (stu1.gender)); printf ("sizeof(stu1.age) = %ld\n" , sizeof (stu1.age)); printf ("sizeof(stu1.id) = %ld\n" , sizeof (stu1.id)); printf ("sizeof(stu1.score) = %.ld\n\n" , sizeof (stu1.score)); struct Test stu2 = {"qidaink" , 'm' , 18 , "01" , 95.8 }; printf ("sizeof(stu2) = %ld\n" , sizeof (stu2)); printf ("sizeof( struct Student) = %ld\n" , sizeof (struct Student)); printf ("sizeof(stu2.name) = %ld\n" , sizeof (stu2.name)); printf ("sizeof(stu2.gender) = %ld\n" , sizeof (stu2.gender)); printf ("sizeof(stu2.age) = %ld\n" , sizeof (stu2.age)); printf ("sizeof(stu2.id) = %ld\n" , sizeof (stu2.id)); printf ("sizeof(stu2.score) = %.ld\n\n" , sizeof (stu2.score)); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译链接程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sizeof(stu1) = 20 sizeof( struct Student) = 20 sizeof(stu1.name) = 8 sizeof(stu1.gender) = 1 sizeof(stu1.age) = 4 sizeof(stu1.id) = 3 sizeof(stu1.score) = 4 sizeof(stu2) = 24 sizeof( struct Student) = 20 sizeof(stu2.name) = 8 sizeof(stu2.gender) = 1 sizeof(stu2.age) = 4 sizeof(stu2.id) = 3 sizeof(stu2.score) = 4
【说明】 64 位平台下的指针变量固定为 8 字节。
我们会发现,相同的结构体属性,但是对齐方式不同,占用的空间也就不一样了。
【原则1】 数据成员对齐规则:结构( struct )或联合( union )的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员的对齐按照 #pragma pack 指定的数值和这个数据成员自身长度中,比较小的那个进行。
【原则2】 结构或联合的整体对齐规则:在数据成员完成各自对齐之后,结构或联合本身也要进行对齐,对齐将按照 #pragma pack 指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
【原则3】 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
备注:数组成员按长度按数组类型长度计算,如char t[9],在第1步中数据自身长度按1算,累加结构体时长度为9;第2步中,找最大数据长度时,如果结构体T有复杂类型成员A,该A成员的长度为该复杂类型成员A的最大成员长度。
【注意】
(1)当 #pragma pack 的 n 值等于或超过所有数据成员长度的时候,这个 n 值的大小将不产生任何效果。
(2)对齐位数跟处理器位数和编译器都有关。 VS , VC 等编译器默认是 #pragma pack(8) ,所以测试我们的规则会正常;注意 gcc 默认是 #pragma pack(4) ,并且 gcc 只支持 1 , 2 , 4 对齐。套用三原则里计算的对齐值是不能大于 #pragma pack 指定的 n 值。
这样上边结构体的成员分布也就很清楚明白了。
二、结构体数组 具有相同结构体类型的结构体变量也可以组成数组,称它们为结构体数组 。结构体数组的每一个数组元素都是结构体类型的数据 ,它们都分别包括各个成员(分量)项。
1.定义结构体数组 定义结构体数组的方式与定义结构体变量的方式类似。
1 2 3 4 5 6 7 8 9 10 11 12 struct STU { char *name; char gender; int age; char id[11 ]; float score; }; struct STU stu [6];
定义了一个结构体数组 stu ,它有 6 个元素,每个元素都是 Student 结构体类型,每个元素都由 5 个成员组成。
1 2 3 4 5 6 7 8 9 struct STU { char *name; char gender; int age; char id[11 ]; float score; } stu1[6 ];
与方式一类似,定义了一个结构体数组 stu ,它有 6 个元素,每个元素都是 Student 结构体类型,每个元素都由 5 个成员组成。
【说明】 如果只需要 stu 一个结构体数组,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名 。
1 2 3 4 5 6 7 8 9 struct /* 省略结构体名称 */{ char *name; char gender; int age; char id[11 ]; float score; }stu[6 ];
2.结构体数组的使用 其实这与变量的赋值也是很类似的。
2.1使用格式
1 2 3 4 结构体数组名[index].成员名1 结构体数组名[index].成员名2 ... 结构体数组名[index].成员名N
1 2 3 4 结构体数组名[index].成员名1 = value1; 结构体数组名[index].成员名2 = value2; ... 结构体数组名[index].成员名N = valueN;
【注意】
(1)访问或者赋值的时候注意遵循数组访问格式,当访问到某个元素后,再对元素的成员进行访问即可。多维数组也是一样。
(2)可以将一个结构体数组元素值赋给同一结构体类型 的数组中的另一个元素,或者赋给同一类型 的结构体变量。
(3)我们不能把结构体数组元素作为一个整体 直接进行输入输出,另外与普通数组一样,需要使用循环遍历整个结构体数组。
2.2赋值与初始化 这里的话直接看实例把,其实没啥说的,就跟一般的数组一样的,注意结构体数据的引用方式即可。
点击查看实例
【说明】赋值方式二和赋值方式三也可以称之为结构体数组的初始化。
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 #include <stdio.h> #include <string.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct Student stu [3]; int i = 0 ; stu[1 ].name = "qidaink" ; stu[1 ].gender = 'm' ; stu[1 ].age = 18 ; strcpy (stu[1 ].id, "0000000001" ); stu[1 ].score = 95.8 ; for (i = 0 ; i < sizeof (stu)/sizeof (struct Student); i++) { printf ("i = %d, %s %c %d %s %.2f\n" , i, stu[i].name, stu[i].gender, stu[i].age, stu[i].id, stu[i].score); } return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 i = 0, (null) 0 0.00 i = 1, qidaink m 18 0000000001 95.80 i = 2, UH��AWAVAUATSH��(L��! 0 ��JTV 0.00
可以看到,出现了很多乱码,这就是因为数组没有初始化造成的。
1 2 3 4 5 6 7 8 9 struct 结构体名{ 数据类型 成员名1 ; 数据类型 成员名2 ; ... 数据类型 成员名N; }; struct 结构体名 数组名[元素个数] = { 初始数据表 }
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #include <string.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct Student stu [3] = {{"qidaink" , 'm' , 18 , "0000000001" , 95.8 }}; int i = 0 ; for (i = 0 ; i < sizeof (stu)/sizeof (struct Student); i++) { printf ("i = %d, %s %c %d %s %.2f\n" , i, stu[i].name, stu[i].gender, stu[i].age, stu[i].id, stu[i].score); } return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 i = 0, qidaink m 18 0000000001 95.80 i = 1, (null) 0 0.00 i = 2, (null) 0 0.00
1 2 3 4 5 6 7 8 struct 结构体名{ 数据类型 成员名1 ; 数据类型 成员名2 ; ... 数据类型 成员名N; }数组名[元素个数] = { 初始数据表 };
示例如下:
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 <string.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; }stu[3 ] = {{"qidaink" , 'm' , 18 , "0000000001" , 95.8 }}; int main (int argc, char *argv[]) { int i = 0 ; for (i = 0 ; i < sizeof (stu)/sizeof (struct Student); i++) { printf ("i = %d, %s %c %d %s %.2f\n" , i, stu[i].name, stu[i].gender, stu[i].age, stu[i].id, stu[i].score); } return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 i = 0, qidaink m 18 0000000001 95.80 i = 1, (null) 0 0.00 i = 2, (null) 0 0.00
三、结构体指针 我们可以设定一个指针变量用来指向一个结构体变量。此时该指针变量的值是结构体变量的起始地址,该指针称为结构体指针。
结构体指针与前面的各种指针变量在特性和方法上是相同的 。与前述相同,在程序中结构体指针也是通过访问目标运算 * 访问它的对象。
1.定义结构体指针 结构体指针的一般声明形式为:
其中的结构体名必须是已经定义过的结构体类型。
点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 struct Student { char *name; char gender; int age; char id[11 ]; float score; }; struct Student *pStu ;
pStu 是指向 struct Student 结构体类型的指针。结构体指针的说明规定了它的数据特性,并为结构体指针本身分配了一定的内存空间。但是指针的内容尚未确定,即它指向随机的对象 。
2.结构体指针的使用 2.1使用格式 一般使用格式有以下两种方式:
1 2 3 4 (*结构体指针名).成员名1 (*结构体指针名).成员名2 ... (*结构体指针名).成员名N
1 2 3 4 结构体指针名->成员名1 结构体指针名->成员名2 ... 结构体指针名->成员名N
【注意】确保是一个结构体指针的情况下才可以使用 -> 来获取成员
2.2赋值与初始化 与之前的指针变量格式一样。,赋值时要给的是一个地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct Student stu_v = {"fanhua" , 'g' , 18 , "0000000003" , 97.5 }; struct Student *pStu ; pStu = &stu_v; printf ("%s %c %d %s %.2f\n" , pStu->name, pStu->gender, pStu->age, pStu->id, pStu->score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 fanhua g 18 0000000003 97.50
这种赋值方式也被称为初始化。
1 2 3 4 5 6 7 8 9 struct 结构体名{ 数据类型 成员名1 ; 数据类型 成员名2 ; ... 数据类型 成员名N; }; struct 结构体名 *指针变量名 = address;
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct Student stu_v = {"fanhua" , 'g' , 18 , "0000000003" , 97.5 }; struct Student *pStu = &stu_v; printf ("%s %c %d %s %.2f\n" , pStu->name, pStu->gender, pStu->age, pStu->id, pStu->score); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 fanhua g 18 0000000003 97.50
2.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 #include <stdio.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct Student stu [3] = {{"fanhua" , 'g' , 18 , "0000000003" , 97.5 }, {"qidaink" , 'm' , 19 , "0000000001" , 95.67 }, {"a" , 'm' , 16 , "0000000002" , 87.5 }}; struct Student *pStu = stu; int len = sizeof (stu) / sizeof (struct Student); printf ("Name\tGender\tAge\t ID\t\tScore\t\n" ); for (pStu = stu; pStu < stu + len; pStu++) { printf ("%s\t %c\t%d\t%s\t%.2f\n" , pStu->name, pStu->gender, pStu->age, pStu->id, pStu->score); } return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 Name Gender Age ID Score fanhua g 18 0000000003 97.50 qidaink m 19 0000000001 95.67 a m 16 0000000002 87.50
【注意】当一个结构体指针指向数组后,它可以作为数组名使用,但是无法再像结构体指针那样可以使用 -> 来取相应元素的成员,但是可以使用 . 来获取相应元素的成员。
点击查看实例
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> struct Student { char *name; char gender; int age; char id[11 ]; float score; }; int main (int argc, char *argv[]) { struct Student stu [3] = {{"fanhua" , 'g' , 18 , "0000000003" , 97.5 }, {"qidaink" , 'm' , 19 , "0000000001" , 95.67 }, {"a" , 'm' , 16 , "0000000002" , 87.5 }}; struct Student *pStu = stu; printf ("%s\t %c\t%d\t%s\t%.2f\n" , pStu[0 ]->name, pStu[0 ]->gender, pStu[0 ]->age, pStu[0 ]->id, pStu[0 ]->score); return 0 ; }
一般来说应该会报这样的一个错误,而且是有一个报一个😂,能给报一堆:
1 2 3 test.c: In function ‘main’: test.c:20:47: error: invalid type argument of ‘->’ (have ‘struct Student’) printf("%s\t %c\t%d\t%s\t%.2f\n", pStu[0]->name, pStu[0]->gender, pStu[0]->age, pStu[0]->id, pStu[0]->score);
2.4结构体指针与函数 结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。
如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,速度将会有所提升。
点击查看实例
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 #include <stdio.h> struct Student { char *name; char gender; int age; char id[11 ]; float score; }; void average (struct Student *ps, int len) ;int main (int argc, char *argv[]) { struct Student stu [3] = {{"fanhua" , 'g' , 18 , "0000000003" , 97.5 }, {"qidaink" , 'm' , 19 , "0000000001" , 95.67 }, {"a" , 'm' , 16 , "0000000002" , 87.5 }}; struct Student *pStu = stu; int len = sizeof (stu) / sizeof (struct Student); printf ("Name\tGender\tAge\t ID\t\tScore\t\n" ); for (pStu = stu; pStu < stu + len; pStu++) { printf ("%s\t %c\t%d\t%s\t%.2f\n" , pStu->name, pStu->gender, pStu->age, pStu->id, pStu->score); } average(stu, len); return 0 ; } void average (struct Student *ps, int len) { int i = 0 ; float average, sum = 0 ; for (i = 0 ; i < len; i++) { sum += (ps + i) -> score; } average = sum / len; printf ("average = %.2f\n" , average); }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 Name Gender Age ID Score fanhua g 18 0000000003 97.50 qidaink m 19 0000000001 95.67 a m 16 0000000002 87.50 average = 93.56
四、枚举类型 在实际问题中,有些数据的取值往往是有限的,只能是非常少量的整数,并且最好为每个值都取一个名字,以方便在后续代码中使用,比如一个星期只有七天,一年只有十二个月,等。
针对上边那些特殊变量, C 语言为我们提供了枚举类型,在枚举的定义中,会将这些变量的值一一列举出来,并且枚举类型的变量的值就只限于列举出来的值的范围。
1.定义枚举类型 枚举类型的定义需要用到关键字 enum ,一般定义格式如下:
1 2 3 4 5 6 7 8 enum typeName { valueName1, valueName2, valueName3, ... valueNameN, };
typeName 为枚举类型的名称, valueNameN 为枚举类型的成员。
【注意】
(1)枚举类型中任意两个枚举成员不能具有相同的名称 。而且枚举列表中的 valueName1… 这些标识符的作用范围是全局的 (严格来说是 main() 函数内部),不能再定义与它们名字相同的变量 。
(2)枚举类型中间的枚举成员之间用逗号( , )隔开,定义结束时,分号( ; )必不可少。
(3)在枚举类型中,枚举成员的类型只能是整型 ,并且声明的第一个枚举成员默认值是 0 ,往后逐个加 1 (递增)。这样增加后的值必须是整型可表示的值得范围,否则会报错。
(4)我们也可以在定义枚举类型时,为枚举成员显示赋值,允许多个枚举成员具有相同的值。没有显示赋值的枚举成员的值总是前一个枚举成员的值加 1 。例如,
1 2 3 4 5 6 7 8 9 10 enum Weeks { Monday = 1 , Tuesday = 2 , Wednesday, Thursday, Friday, Saturday, Sunday };
(5)枚举与宏其实有些类似,宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
当我们的枚举类型定义完成后,枚举成员都是常量 ,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面 ,放到代码区 ,所以不能用 & 取得它们的地址。我们也无法修改枚举成员的值,除非直接修改相应枚举类型的定义。
2.定义枚举变量 与结构体一样,枚举也是一种构造的数据类型,所以可以像结构体一样定义枚举变量。枚举变量定义一般格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 enum Weeks { Monday = 1 , Tuesday = 2 , Wednesday, Thursday, Friday, Saturday, Sunday }; enum Weeks w1 , w2 ;
定义了两个枚举变量 w1 和 w2 。
【注意】关键字 Weeks 不可以省略,若没有这个关键字,则系统不认为 Weeks 是枚举类型。
1 2 3 4 5 6 7 8 9 10 11 enum Weeks { Monday = "1" , Tuesday = "2" , Wednesday, Thursday, Friday, Saturday, Sunday }w1, w2;
与方式一类似,定义了两个枚举变量 w1 和 w2 。
【说明】如果只需要 w1 、 w2 两个变量,后面不需要再使用枚举类型名称定义其他变量,那么在定义时也可以不给出枚举类型名称 。
1 2 3 4 5 6 7 8 9 10 11 enum { Monday = "1" , Tuesday = "2" , Wednesday, Thursday, Friday, Saturday, Sunday }w1, w2;
3.枚举变量的使用 3.1引用枚举成员 1 2 3 4 枚举类型名1 枚举类型名2 ... 枚举类型名N
3.2枚举变量赋值 枚举类型变量赋值方式有两种,接下来通过实例说明。
点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> enum Weeks { Monday = 1 , Tuesday = 2 , Wednesday, Thursday, Friday, Saturday, Sunday }; int main (int argc, char *argv[]) { enum Weeks w1 , w2 , w3 ; w1 = Monday; w2 = Sunday; w3 = 9 ; printf ("%d %d %d %d %d %d %d\n" , Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); printf ("%d %d %d \n" , w1, w2, w3); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
【说明】我们可以把定义枚举类类型时的枚举成员赋值给枚举变量,也可以直接把整数值赋给枚举变量。枚举变量其实也就是一种整型变量 。
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> enum Weeks { Monday = 1 , Tuesday = 2 , Wednesday, Thursday, Friday, Saturday, Sunday }; int main (int argc, char *argv[]) { enum Weeks w1 = Monday, w2 = Friday, w3 = 10 ; printf ("%d %d %d %d %d %d %d\n" , Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); printf ("%d %d %d \n" , w1, w2, w3); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
五、共用体 在 C 语言中,不同数据类型的数据可以使用共同的存储区域 ,这种数据构造类型称为共用体,简称共用 ,又称联合体 。共用体在定义、说明和使用形式上与结构体相似。两者本质上的不同仅在于使用内存的方式上 。
1.定义共用体 定义一个结构体数据类型,我们需要用到 union 关键字:
1 2 3 4 5 6 7 union union_name { data_type member_name1; data_type member_name2; ... data_type member_nameN; };
点击查看各部分说明
union 共用体的关键字,表明这是一个共用体的定义 union_name 共用体的名称,与普通变量名要求一样 data_type 共用体内成员的数据类型,可以是基本变量类型,指针类型,枚举类型,联合体类型或者结构体类型等。比如,char,int等 member_nameN 共用体内成员的名称(成员变量名),命名规则与变量相同
【注意】
(1)共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
(2)共用体占用的内存等于最长的成员占用的内存 。
(3)共用体使用了内存覆盖技术, 同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
2.共用体的长度 我们可以使用 sizeof 函数来求得所定义共用体的长度。共用体既然共用同一段内存区域,那么它的长度就应该等于共用体成员中最长的数据类型的长度 。
点击查看实例
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 #include <stdio.h> union data1 { char a; short b; int c; }; union data2 { char a; short b; }; int main (int argc, char *argv[]) { union data1 temp1 ; union data2 temp2 ; printf ("sizeof(temp1) = %ld\n" , sizeof (temp1)); printf ("sizeof(union data1) = %ld\n\n" , sizeof (union data1)); printf ("sizeof(temp2) = %ld\n" , sizeof (temp2)); printf ("sizeof(union data2) = %ld\n" , sizeof (union data2)); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 sizeof(temp1) = 4 sizeof(union data1) = 4 sizeof(temp2) = 2 sizeof(union data2) = 2
我们会发现(我用的 linux 是 64 位的),总的大小是等于最长的数据所占空间大小的。
3.在内存中的存储 既然共用体的成员共用一段内存空间,那打印地址的话,他们怎么分布呢?
点击查看测试实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> union data { char a; short b; int c; }; int main (int argc, char *argv[]) { union data temp ; printf ("&temp = %p\n" , &temp); printf ("&temp.a = %p\n" , &temp.a); printf ("&temp.b = %p\n" , &temp.b); printf ("&temp.c = %p\n" , &temp.c); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 sizeof(temp) = 4 sizeof(union data) = 4 &temp = 0x7ffc7c3d2b14 &temp.a = 0x7ffc7c3d2b14 &temp.b = 0x7ffc7c3d2b14 &temp.c = 0x7ffc7c3d2b14
我们会发现他们的地址都是同一个。
从例子中不难看出,这几个成员的地址都是一样的,他们确实共用同一段内存。
4.赋值? 看到这里,我会想,它跟结构体用法一样,但是却又共用同一段内存,那我还能给它赋值嘛?接下来我们就开看一看成员赋值时的变化情况。
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 #include <stdio.h> union data { char a; short b; int c; }; int main (int argc, char *argv[]) { union data temp ; printf ("sizeof(temp) = %ld\n" , sizeof (temp)); printf ("sizeof(union data) = %ld\n\n" , sizeof (union data)); printf ("&temp.a = %p,&temp.b = %p,&temp.b = %p\n" , &temp.a, &temp.b, &temp.c); printf ("temp.a\t\ttemp.b\t\ttemp.c\n" ); printf ("%c(%#x)\t%#hx\t\t%d(%#x)\n" , temp.a, temp.a, temp.b, temp.c, temp.c); temp.c = 0x40 ; printf ("%c(%#x)\t\t%#hx\t\t%d(%#x)\n" , temp.a, temp.a, temp.b, temp.c, temp.c); temp.a = '9' ; printf ("%c(%#x)\t\t%#hx\t\t%d(%#x)\n" , temp.a, temp.a, temp.b, temp.c, temp.c); temp.b = 0x2059 ; printf ("%c(%#x)\t\t%#hx\t\t%d(%#x)\n" , temp.a, temp.a, temp.b, temp.c, temp.c); temp.c = 0x3e25ad54 ; printf ("%c(%#x)\t\t%#hx\t\t%d(%#x)\n" , temp.a, temp.a, temp.b, temp.c, temp.c); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 7 8 9 10 sizeof(temp) = 4 sizeof(union data) = 4 &temp.a = 0x7ffc18626194,&temp.b = 0x7ffc18626194,&temp.b = 0x7ffc18626194 temp.a temp.b temp.c �(0xfffffffc) 0x7ffc 32764(0x7ffc) @(0x40) 0x40 64(0x40) 9(0x39) 0x39 57(0x39) Y(0x59) 0x2059 8281(0x2059) T(0x54) 0xad54 1042656596(0x3e25ad54)
为什么要先对整型成员赋值呢?这是因为整型成员赋值后可以将这一个字节都给初始化掉,未初始化的时候谁也不知道里边是什么,这也是为了保证结果的准确性。接下来我们来分析一下数据的变化情况。
【说明】红色字体为每次变化的位置。
0x7ffc18626194
0x7ffc18626195
0x7ffc18626196
0x7ffc18626197
0x40
0x00
0x00
0x00
temp.a = 0x40
temp.b = 0x40
temp.c = 0x40
0x7ffc18626194
0x7ffc18626195
0x7ffc18626196
0x7ffc18626197
0x39
0x00
0x00
0x00
temp.a = '9' (ASCII 为0x39)
temp.b = 0x39
temp.c = 0x39
0x7ffc18626194
0x7ffc18626195
0x7ffc18626196
0x7ffc18626197
0x59
0x20
0x00
0x00
temp.a = 0x59
temp.b = 0x2059
temp.c = 0x39
0x7ffc18626194
0x7ffc18626195
0x7ffc18626196
0x7ffc18626197
0x54
0xad
0x25
0x3e
temp.a = 0x54
temp.b = 0xad54
temp.c = 0x3e25ad54
成员 a 、 b 、 c 在内存中“对齐”到一头,对 a 赋值修改的是前一个字节,对 b 赋值修改的是前两个字节,对 c 赋值修改的是全部字节。也就是说, a 、 b 会影响到 c 的一部分数据,而 c 会影响到 a 、 b 的全部数据。
根据 temp.a 、 temp.b 和 temp.c 的分析,可以得出数据在内存中的存储形式。按照最后一次整型的赋值,我们再通过根据 temp.a 、 temp.b 的值,就可以分析出这四个字节的数据的存储是符合小端模式的,高字节存储在高地址,低字节存储在低位,同时这也是一种判定大小端 的方式(大小端可以看后边的关于大小端的笔记)。
六、位域 1.什么是位域 计算机的内存是以字节为单位,为变量分配内存,也是以字书为单位。但是,实际上,有些信息的存储,并不需要占用一个字节,只需要 1 个或几个二进制位就够了。
比如:人的性别,只有两种可能的取值男和女,我们就可以用 0 表示男, 1 表示女,这样使用 1 个二进制位就够了。多数情况,我们会选择 char 类型,占用 1 个字节,但是如果可以用一个位来表示,那有何必浪费 8 个位呢,毕竟在一些内存很小的 CPU 中,内存空间是非常珍贵的。
为了节省存储空间, C 语言中又提供了一种数据结构,称为位域 或位段 ,
所谓位域是把一个字节中的二进位划分为几个不同的区域 ,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作 。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
【位域的本质 】本质其实就是一种结构类型,只不过成员是按照二进制位进行分配的。
【注意】位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用 & 获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节( Byte )的编号,而不是位( Bit )的编号。
2.定义位域 作为一种数据类型,位域的定义与结构体很类似,都是使用关键字 struct ,一般格式如下:
1 2 3 4 5 6 7 struct bitField_name { data_type member_name1: len1; data_type member_name2: len2; ... data_type member_nameN: lenN; };
点击查看各部分说明
struct 位域定义的关键字,表明这是一个位域的定义 structure_name 位域解耦股的名称,与普通变量名要求一样 data_type 位域内成员的数据类型。 member_nameN 位域内成员的名称(成员变量名),命名规则与变量相同 lenN 位域内成员的长度(以bit为单位)
【说明】
(1)位域更像是定义在结构体内部的一种特殊的成员,在定义结构体的时候可以直接定义位域成员。例如,
1 2 3 4 5 struct data { unsigned m; unsigned n: 4 ; unsigned char ch: 6 ; };
: 后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节( Byte )的内存。成员 n 、 ch 被 : 后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4 、 6 位( Bit )的内存。
(2)位域的占用位数不能超过 8 个二进制位,同时位域也不允许跨字节。
(3)允许位域没有域名,只给出数据类型和位宽。没有域名的位域无名位域一般用来作填充或者调整成员位置。需要注意的是没有名称的域名是无法使用的。例如,
1 2 3 4 5 struct data { int m: 12 ; int : 20 ; int n: 4 ; };
如果没有位宽为 20 的无名成员, m 、 n 将会挨着存储, sizeof(struct data) 的结果为 4 ;有了这 20 位作为填充, m 、 n 将分开存储, sizeof(struct data) 的结果为 8 ,具体为什么,后边再分析。
(4) C 语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int 、 signed int 和 unsigned int ( int 默认就是 signed int );到了 C99 , _Bool 也被支持了。
3.定义位域变量 位域也是一种数据类型,所以自然也可以用于定义变量,位域变量的定义与结构体一样。
1 2 3 4 5 6 7 8 struct data { unsigned int a: 1 ; unsigned int b: 3 ; unsigned int c: 4 ; } struct data d1 , d2 ;
定义了两个位域变量 d1 和 d2 ,它们都是 data 位域类型,都由 3 个成员组成。
【注意】
(1)关键字 struct 不可以省略,若没有这个关键字,则系统不认为 data 是位域类型。
(2) data 就像一个“模板”,定义出来的变量都具有相同的性质 。
1 2 3 4 5 6 7 struct data { unsigned int a: 1 ; unsigned int b: 3 ; unsigned int c: 4 ; }d1, d2;
与方式一类似,定义了两个位域变量 d1 和 d2 ,它们都是 data 位域类型,都由 3 个成员组成。
【说明】如果只需要 d1 、 d2 两个变量,后面不需要再使用位域结构名称定义其他变量,那么在定义时也可以不给出位域结构名称 。
1 2 3 4 5 6 7 struct { unsigned int a: 1 ; unsigned int b: 3 ; unsigned int c: 4 ; }d1, d2;
4.位域变量的使用 位域成员的使用和结构体成员的使用方式相同,可以对位域变量成员进行引用和赋值,这些操作都是通过成员运算符 . 完成的。
1 2 3 4 位域变量名.位域名1 位域变量名.位域名2 ... 位域变量名.位域名N
1 2 3 4 位域变量名.位域名1 = value1; 位域变量名.位域名2 = value2; ... 位域变量名.位域名N = valueN;
【注意】赋值时不要超出位域定义时的位数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> struct data { unsigned int a: 1 ; unsigned int b: 3 ; unsigned int c: 4 ; }; int main (int argc, char *argv[]) { struct data d1 ; d1.a = 1 ; d1.b = 6 ; d1.c = 13 ; printf ("%d\t%d\t%d\n" , d1.a, d1.b, d1.c); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> struct data { unsigned int a: 1 ; unsigned int b: 3 ; unsigned int c: 4 ; }; int main (int argc, char *argv[]) { struct data d1 = {1 , 6 , 13 }; printf ("%d\t%d\t%d\n" , d1.a, d1.b, d1.c); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> struct data { unsigned int a: 1 ; unsigned int b: 3 ; unsigned int c: 4 ; }d1 = {1 , 6 , 13 }; int main (int argc, char *argv[]) { printf ("%d\t%d\t%d\n" , d1.a, d1.b, d1.c); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
5.位域的长度 5.1长度计算 当我们定义了一个位域后,用它定义出来的位域变量占多大空间呢?我们可以使用 sizeof 函数来求得所定义位域数据类型的长度。一般格式如下:
1 2 3 4 sizeof ( struct bitField_name) struct bitField_name bitField_variable ;sizeof (bitField_variable)
【注意】我们无法使用该函数求位域内成员的长度,即便位域成员长度超过一个字节,也无法使用,一般会报如下错误:
1 test.c:18:47: error: cannot take address of bit-field ‘a’
5.2使用实例 点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> struct data { unsigned int a: 1 ; unsigned int b: 3 ; unsigned int c: 4 ; }; int main (int argc, char *argv[]) { struct data d1 = {1 , 6 , 13 }; printf ("sizeof(unsigned int) = %ld\n" , sizeof (unsigned int )); printf ("sizeof(d1) = %ld\n" , sizeof (d1)); printf ("sizeof(struct data) = %ld\n" , sizeof (struct data)); printf ("%d\t%d\t%d\n" , d1.a, d1.b, d1.c); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 sizeof(unsigned int) = 4 sizeof(d1) = 4 sizeof(struct data) = 4 1 6 13
具体的那我们就接着往后看。存储方式,我们下节会通过实例来说明。
6.在内存中的存储 6.1非法操作? 当位域数据类型定义了一个位域变量的时候,这个位域以及其成员变量在内存中是怎样的呢?我么是否可以取出位域成员的地址来分析呢?
在 C 语言中,我们可以得到某个字节的内存地址,我们具备了操作任意内存字节的能力;但是在内存空间稀缺的年代,仅仅控制到字节级别还不足以满足 C 程序员的需求,为此 C 语言中又出现了 bit 级别内存的“有限操作能力” —— 位域。这里所谓的“有限”指的是机器的最小粒度寻址单位是字节,我们无法像获得某个字节地址那样得到某个 bit 的地址,因此我们仅能通过字节的运算来设置和获取某些 bit 的值。在C语言中,尝试获得一个bit field的地址是非法操作 。
6.2存储规则 在 C 语言标准中并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间 。
当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> struct data { unsigned int a: 6 ; unsigned int b: 12 ; unsigned int c: 4 ; }; int main (int argc, char *argv[]) { struct data d1 ; printf ("sizeof(unsigned int) = %ld\n" , sizeof (unsigned int )); printf ("sizeof(d1) = %ld\n" , sizeof (d1)); printf ("sizeof(struct data) = %ld\n" , sizeof (struct data)); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 sizeof(unsigned int) = 4 sizeof(d1) = 4 sizeof(struct data) = 4
a 、 b 、 c 的类型都是 unsigned int , sizeof 的结果为 4 个字节( Byte ),也就是 32 个位( Bit )。 a 、 b 、 c 的位宽之和为 6+12+4 = 22 ,小于 32 ,所以它们会挨着存储,中间没有缝隙。
sizeof(struct data) 的大小之所以为 4 ,而不是 3 ,是因为要将内存对齐到 4 个字节,以便提高存取效率。
如果将成员 a 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32,b 会从新的位置开始存储,相对 a 的偏移量是 sizeof(unsigned int),也即 4 个字节。
如果再将成员 c 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。
当相邻成员的类型不同时,不同的编译器有不同的实现方案, GCC 会压缩存储,而 VC/VS 不会。
点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> struct data { unsigned int a: 12 ; unsigned char b: 4 ; unsigned int c: 12 ; }; int main (int argc, char *argv[]) { struct data d1 ; printf ("sizeof(unsigned int) = %ld\n" , sizeof (unsigned int )); printf ("sizeof(unsigned char) = %ld\n" , sizeof (unsigned char )); printf ("sizeof(d1) = %ld\n" , sizeof (d1)); printf ("sizeof(struct data) = %ld\n" , sizeof (struct data)); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 sizeof(unsigned int) = 4 sizeof(unsigned char) = 1 sizeof(d1) = 4 sizeof(struct data) = 4
如果成员之间穿插着非位域成员,那么不会进行压缩。
点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> struct data { unsigned int a: 12 ; unsigned char b; unsigned int c: 12 ; }; int main (int argc, char *argv[]) { struct data d1 ; printf ("sizeof(unsigned int) = %ld\n" , sizeof (unsigned int )); printf ("sizeof(unsigned char) = %ld\n" , sizeof (unsigned char )); printf ("sizeof(d1) = %ld\n" , sizeof (d1)); printf ("sizeof(struct data) = %ld\n" , sizeof (struct data)); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 sizeof(unsigned int) = 4 sizeof(unsigned char) = 1 sizeof(d1) = 8 sizeof(struct data) = 8
【注意】小端模式下,位域成员定义时,位置靠下的为高位。
7.按位赋值 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 #include <stdio.h> union x_bit { char m; struct data { unsigned int a: 2 ; unsigned int b: 4 ; unsigned int c: 2 ; }d; }; int main (int argc, char *argv[]) { union x_bit x ; x.d.a = 2 ; x.d.b = 5 ; x.d.c = 1 ; printf ("x.d.a = %d(%#x)\n" , x.d.a, x.d.a); printf ("x.d.b = %d(%#x)\n" , x.d.b, x.d.b); printf ("x.d.c = %d(%#x)\n" , x.d.c, x.d.c); printf ("x.m = %d(%#x)\n" , x.m, x.m); return 0 ; }
在终端执行以下命令:
1 2 gcc test.c -Wall # 编译程序 ./a.out # 执行可执行文件
会看到有如下信息输出:
1 2 3 4 x.d.a = 2(0x2) x.d.b = 5(0x5) x.d.c = 1(0x1) x.m = 86(0x56)
上边分析共用体的时候,我们知道刚才使用的 CPU 是小端模式,低位存放在低的地址,所以组合方式就是 01 0101 10 合并为一个字节就是 0101 0110 ,也就是 0x56 换算为十进制就是 86 了。
七、 typedef 1.什么是 typedef typedef 为 C 语言的关键字,作用是为一种数据类型定义一个新名字,这里的数据类型包括内部数据类型( int , char 等)和自定义的数据类型( struct )。
就相当于自己的名字和小名,通过 typedef 为自己的名字定义一个小名,别人叫自己小名的时候也知道叫的是自己。
一般来说这个关键字经常在定义结构体类型时使用,毕竟结构体的名字还是有点长的。
2.怎么使用? 基本使用格式如下:
1 typedef 原来数据类型 用户自定义数据类型;
【注意】该关键字重新命名数据类型的时候,注意后边加上分号( ; )。
2.1常见类型起别名 1 2 3 4 typedef unsigned long uint32;uint32 a = 0 ; unsigned long a = 0 ;
2.2结构体类型起别名 一般来说,该关键字多会用在结构体中,例如,
1 2 3 4 5 typedef struct _node_ { int data; struct _node_ *next ; } listnode, *linklist;
这里定义了两个新的数据类型 listnode 和 linklist 。其中 listnode 等价于数据类型 struct node ,而 linklist 等价于 struct node * 。
2.3数组类型起别名 1 typedef char ARRAY_20[20 ];
它表示 ARRAY_20 是与类型 char [20] 等价。它是一个长度为 20 的数组类型。接着可以用 ARRAY_20 定义数组,例如
1 2 3 4 ARRAY_20 a1, a2; char a1[20 ], a2[20 ];
2.4数组指针类型起别名 1 typedef int (*PTR_TO_ARR) [4];
这里定义了一个新的数据类型 PTR_TO_ARR ,它等价于类型 int * [4] ,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针,例如
点击查看实例
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> typedef char (*PTR_TO_ARR) [30];char str[3 ][30 ] ={ "fanhua_0" , "fanhua_1" , "fanhua_2" , }; int main () { PTR_TO_ARR parr = str; int i; for (i=0 ; i<3 ; i++) { printf ("str[%d]: %s\n" , i, *(parr+i)); } return 0 ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示:
1 2 3 str[0]: fanhua_0 str[1]: fanhua_1 str[2]: fanhua_2
2.5函数指针起别名 1 2 typedef int (*PTR_TO_FUNC) (int , int ) ;PTR_TO_FUNC pfunc;
这里定义了一个新的类型 PTR_TO_FUNC ,也就是函数指针类型。
点击查看实例
test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> typedef int (*PTR_TO_FUNC) (int , int ) ;int max (int a, int b) { return a>b ? a : b; } int main () { PTR_TO_FUNC pfunc = max; printf ("max: %d\n" , (*pfunc)(5 , 6 )); return 0 ; }
在终端执行以下命令编译程序:
1 2 gcc test.c -Wall # 生成可执行文件 a.out ./a.out # 执行可执行程序
然后,终端会有以下信息显示: