目录
C 语言结构体(Struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、 char、 float等基本类型组成的。
一、什么是结构体?
1、结构体的简述
在 C 语言中,可以使用结构体(Struct) 来存放一组不同类型的数据。结构体的定义形式为:
struct 结构体名
{
结构体所包含的变量或数组
};
结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员。
struct stu
{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
2、结构体变量
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu stu1, stu2;
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字 struct 不能少。
另一种方式:
struct stu
{
char *name; 姓名
int num; 学号
int age; 年龄
char group; 所在学习小组
float score; 成绩
} stu1, stu2;
将变量放在结构体定义的最后即可。
如果只需要 stu1、 stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
struct{ 没有写 stu
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
3、成员的获取和赋值
结构体使用点号 . 获取单个成员。获取结构体成员的一般格式为:结构体变量名.成员名;
通过这种方式可以获取成员的值,也可以给成员赋值:
#include <stdio.h>
int main()
{
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;
//给结构体成员赋值
stu1.name = "Tom";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f! \n", stu1.name, stu1.num, stu1.age,stu1.group, stu1.score);
return 0;
}
除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 }
不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。
需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。
二、结构体数组
所谓结构体数组,是指数组中的每个元素都是一个结构体。在实际应用中, C 语言结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个车间的职工等。
在 C 语言中,定义结构体数组和定义结构体变量的方式类似,请看下面的例子:
struct stu{
char *name; 姓名
int num; 学号
int age; 年龄
char group; 所在小组
float score; 成绩
}class[5];
结构体数组在定义的同时也可以初始化,例如:
struct stu{
char *name; 姓名
int num; 学号
int age; 年龄
char group; 所在小组
float score; 成绩
}class[5] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};
当对数组中全部元素赋值时,也可不给出数组长度,例如:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}class[] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};
结构体数组的使用也很简单:
#include <stdio.h>
struct Books
{
char *title;
char *author;
char *subject;
int book_id;
} book[2]= {
{"C 语言", "RUNOOB", "编程语言", 123456},
{"C 言", "BBBBBB", "语言编程", 456123}
};
int main(void)
{
book[1].author = "sumjess";
book[1].book_id=666;
book[0].subject="指针";
book[0].title="C语言";
printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book[0].title, book[1].author, book[0].subject, book[1].book_id);
return 0;
}
三、结构体指针(指向结构体的指针)
1、结构体指针的基本用法
当一个指针变量指向结构体时,我们就称它为结构体指针。 C 语言结构体指针的定义形式一般为:
struct 结构体名 *变量名;
//结构体
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;
也可以在定义结构体的同时定义结构体指针:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;
注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&,所以给 pstu 赋值只能写作:
struct stu *pstu = &stu1;
而不能写作:
struct stu *pstu = stu1;
结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、 float、 char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。下面的写法是错误的,不可能去取一个结构体名的地址,也不能将它赋值给其他变量:
- struct stu *pstu = &stu;
- struct stu *pstu = stu;
2、获取结构体成员
通过结构体指针可以获取结构体成员,一般形式为:
(*pointer).memberName
或
pointer->memberName
->可以通过结构体指针直接取得结构体成员;这也是->在 C 语言中的唯一用途。
#include <stdio.h>
int main()
{
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;//*pstu = &stu1可以换成 *pstu,不写 = &stu1
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f! \n", (*pstu).name, (*pstu).num,(*pstu).age, (*pstu).group, (*pstu).score);
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f! \n", pstu->name, pstu->num, pstu->age,pstu->group, pstu->score);
return 0;
}
运行结果:
Tom 的学号是 12,年龄是 18,在 A 组,今年的成绩是 136.5!
Tom 的学号是 12,年龄是 18,在 A 组,今年的成绩是 136.5!
3、结构体指针作为函数参数
结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。
#include <stdio.h>
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}stus[] = {
{"Li ping", 5, 18, 'C', 145.0},
{"Zhang ping", 4, 19, 'A', 130.5},
{"He fang", 1, 18, 'A', 148.5},
{"Cheng ling", 2, 17, 'F', 139.0},
{"Wang ming", 3, 17, 'B', 144.5}
};
void average(struct stu *ps, int len);
int main()
{
int len = sizeof(stus) / sizeof(struct stu);
average(stus, len);
return 0;
}
void average(struct stu *ps, int len)
{
int i, num_140 = 0;
float average, sum = 0;
for(i=0; i<len; i++)
{
sum += (ps + i) -> score; //ps为数组的地址
if((ps + i)->score < 140) num_140++;
}
printf("sum=%.2f\naverage=%.2f\nnum_140=%d\n", sum, sum/5, num_140);
}
四、C 语言枚举类型(enum 关键字)
C 语言提供了一种枚举(Enum)类型,能够列出所有可能的取值,并给它们取一个名字。
枚举类型的定义形式为:
enum typeName{ valueName1, valueName2, valueName3, ...... };
例如,列出一个星期有几天:
enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };
可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说, week 中的 Mon、 Tues ...... Sun 对应的值分别为 0、 1 ...... 6。
我们也可以给每个名字都指定一个值:
enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };
更为简单的方法是只给第一个名字指定值:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
枚举是一种类型,通过它可以定义枚举变量:
enum week a, b, c;
也可以在定义枚举类型的同时定义变量:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;
有了枚举变量,就可以把列表中的值赋给它:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;
或者:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;
【示例】判断用户输入的是星期几。
#include <stdio.h>
int main()
{
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
scanf("%d", &day);
switch(day)
{
case Mon: puts("Monday"); break;
case Tues: puts("Tuesday"); break;
case Wed: puts("Wednesday"); break;
case Thurs: puts("Thursday"); break;
case Fri: puts("Friday"); break;
case Sat: puts("Saturday"); break;
case Sun: puts("Sunday"); break;
default: puts("Error!");
}
return 0;
}
需要注意的两点是:
- 枚举列表中的 Mon、 Tues、 Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。
- Mon、 Tues、 Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
五、C 语言共用体(union 关键字)
通过前面的讲解,我们知道结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在 C 语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union) ,它的定义格式为:
union 共用体名{
成员列表
};
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
1、共用体也是一种自定义类型,可以通过它来创建变量
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
这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。
2、共用体的应用
共用体在一般的编程中应用较少,在单片机中应用较多。
六、C 语言位域(位段)
1、位域的基本用法
有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑, C 语言又提供了一种叫做位域的数据结构。
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。 请看下面的例子:
struct bs
{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
};
: 后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。成员 n、 ch 被 : 后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、 6 位(Bit)
#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 的值分别是 0xE、 0x24('$' 对应的 ASCII 码为 0x24),换算成二进制是1110、 10 0100,都没有超出限定的位数,能够正常输出。
第二次输出时, n、 ch 的值变为 0x2d、 0x7a('z' 对应的 ASCII 码为 0x7a),换算成二进制分别是10 1101、 1111010,都超出了限定的位数。超出部分被直接截去,剩下 1101、 11 1010,换算成十六进制为0xd、 0x3a(0x3a 对应的字符是 :)
C 语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度, : 后面的数字不能超过这个长度。
C 语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、 signed int 和unsigned int(int 默认就是 signed int);到了 C99, _Bool 也被支持了。
2、位域的存储
位域的具体存储规则如下:
- 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
#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,所以它们会挨着存储,中间没有缝隙。 如果将成员 m 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32, n 会从新的位置开始存储,相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节。 如果再将成员 p 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。
-
当相邻成员的类型不同时,不同的编译器有不同的实现方案, GCC 会压缩存储,而 VC/VS 不会。·
#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,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。
- 如果成员之间穿插着非位域成员,那么不会进行压缩。
struct bs{ unsigned m: 12; unsigned ch; unsigned p: 4; }; 在各个编译器下 sizeof 的结果都是 12。
通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的, C 语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
3、无名位域
位域成员可以没有名称,只给出数据类型和位宽,如下所示:
struct bs{
int m: 12;
int : 20; //该位域成员不能使用
int n: 4;
};
无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。上面的例子中,如果没有位宽为 20 的无名成员, m、 n 将会挨着存储, sizeof(struct bs) 的结果为 4;有了这 20位作为填充, m、 n 将分开存储, sizeof(struct bs) 的结果为 8。
七、C 语言 typedef 的用法
C 语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。
起别名的目的不是为了提高程序运行效率,而是为了编码方便。例如有一个结构体的名字是 stu,要想定义一个结
构体变量就得这样写:
struct stu stu1;
struct 看起来就是多余的,但不写又会报错。如果为 struct stu 起了一个别名 STU,书写起来就简单了:
STU stu1;
这种写法更加简练,意义也非常明确,不管是在标准头文件中还是以后的编程实践中,都会大量使用这种别名。
使用关键字 typedef 可以为类型起一个新的别名。 typedef 的用法一般为:
typedef oldName newName;
1、typedef给int、char等类型定义别名
typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;
INTEGER a, b;等效于 int a, b;
2、typedef给数组类型定义别名
typedef char ARRAY20[20];
表示 ARRAY20 是类型 char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:
ARRAY20 a1, a2, s1, s2;
它等价于:
char a1[20], a2[20], s1[20], s2[20];
注意,数组也是有类型的。例如 char a1[20];定义了一个数组 a1,它的类型就是 char [20]。
3、为结构体类型定义别名
typedef struct stu
{
char name[20];
int age;
char sex;
} STU;
STU 是 struct stu 的别名,可以用 STU 定义结构体变量:
STU body1,body2;
它等价于:
struct stu body1, body2;
4、为指针类型定义别名
typedef int (*PTR_TO_ARR)[4];
表示 PTR_TO_ARR 是类型 int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:
PTR_TO_ARR p1, p2;
5、函数指针类型定义别名
typedef int (*PTR_TO_FUNC)(int, int);
PTR_TO_FUNC pfunc;
6、指针示例
#include <stdio.h>
typedef char (*PTR_TO_ARR)[30];
typedef int (*PTR_TO_FUNC)(int, int);
int max(int a, int b)
{
return a>b ? a : b;
}
char str[3][30] = {
"http://c.biancheng.net",
"C语言中文网",
"C-Language"
};
int main()
{
PTR_TO_ARR parr = str;
PTR_TO_FUNC pfunc = max;
int i;
printf("max: %d\n", (*pfunc)(10, 20));
for(i=0; i<3; i++){
printf("str[%d]: %s\n", i, *(parr+i));
}
return 0;
}
运行结果:
max: 20
str[0]: http://c.biancheng.net
str[1]: C 语言中文网
str[2]: C-Language
需要强调的是, typedef 是赋予现有类型一个新的名字,而不是创建新的类型。为了“见名知意”,请尽量使用含义明确的标识符,并且尽量大写。
7、typedef 和 #define 的区别
1) 可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。如下所示:
#define INTERGE int
unsigned INTERGE n; 没问题
typedef int INTERGE;
unsigned INTERGE n; 错误,不能在 INTERGE 前面添加 unsigned
2) 在连续定义几个变量的时候, typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
#define PTR_INT int *
PTR_INT p1, p2;
经过宏替换以后,第二行变为:
int *p1, p2;
这使得 p1、 p2 成为不同的类型: p1 是指向 int 类型的指针, p2 是 int 类型。
相反,在下面的代码中:
typedef int * PTR_INT
PTR_INT p1, p2;
p1、 p2 类型相同,它们都是指向 int 类型的指针。