结构体
前面的数组是一组具有相同类型数据的集合,但是在实际编程过程中,我们往往还需要一组不同类型的数组,比如学生的信息登记表,姓名为字符串,学号为整数,性别为字符串等等;这种情况下,因为每个数据的类型不同,这时候显然不能用数组来保存;
因此在C语言中,可以通过创建结构体来保存一组不同类型的数据,也就是说结构体中的成员的数据类型可以不同,结构体的定义语法为:
struct 结构体名{
成员1;
成员2;
...
};
也就是说,结构体是一种集合,里面包含了多个成员,成员的数据类型可以不同,如下的例子:
struct stu{
char *name;
int num;
int age;
char group;
float score;
};
成员可以是任意类型的数据,当然也可以是结构体;前面用到的 int float 等是C本身提供的数据类型,我们称之为基本数据类型,而结构体可以包含多个基本类型数据,也可以包含其他结构体,我们称之为复杂数据类型;
注意:在定义结构体时,不能将结构体中的成员进行初始化!!
结构体变量
既然结构体是一种数据类型,那么就可以用定义的结构体类型来定义结构体变量,如下:
struct stu stu1, stu2;//注意最前面的 struct 不能省略
上面定义了两个结构体变量 stu1和stu2,类型为 struct stu;stu 就像是一个 “模板”,定义出来的变量都有相同的属性;也可以在定义结构体类型的同时定义结构体变量,如下:
struct stu{
char *name;
int num;
int age;
char group;
float score;
} stu1, stu2;
struct {//没有写stu,这样做的话并没有结构体名,导致后面无法再定义该结构体的结构体变量
char *name;
int num;
int age;
char group;
float score;
}stu1, stu2;
这两种方法都可以定义结构体变量,但是还是推荐先定义结构体,然后再定义结构体变量的方式,这种看起来比较简洁明了;
需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;在定义了结构体变量后,系统才会为之分配内存单元,理论上上面的结构体变量 stu1 占的字节个数为 4+4+4+1+8=21,但是实际上为24,如下:
这是因为计算机对内存的管理是以字为单位的,一个 word 为4个字节,所以对上面的 group 虽然理论上只占一个字节,但是这个 word 中的后三个字节并不会接着存放 score 成员,而是从下一个 word 开始存放,所以用 sizeof 运算符测量 stu1 的长度时,得到的是24;
结构体变量的初始化和引用
在定义结构体变量时,可以对它初始化,初始化可以对整个结构体一起进行,也可以对单个成员变量,如下:
如果在定义结构体变量时没有初始化,那么后面给结构体变量的成员赋值时只能单个赋值,而不能将结构体整体赋值,如下面的写法是错误的:
可以通过结构体变量来引用结构体中的成员,语法如下:
结构体变量名.成员名;
"." 是成员运算符,在所有运算符中优先级最高,如上面的第13行给成员 age 赋值为20;
注意:不能企图通过结构体变量名来一起输出结构体中的所有成员,只能对每个成员进行单独操作!!同时,不能再scanf 函数中使用结构体变量名一揽子输入全部成员的值,只能一个一个输入;看下面例子:
可以看到上面的scanf函数中,成员 num 和 score 都有取址符 &,而 name 没有,这是因为 name是数组名,本身就代表地址;
结构体数组
所谓结构体数组,就是指数组的每一个元素都是一个结构体;在实际应用中,结构体数组常用来表示一个拥有相同数据结构的群体,比如一个班的学生;定义结构体数组与定义数组的语法相同,看下面例子:
结构体指针
所谓结构体指针就是指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针;如果将结构体变量的起始地址存放在指针变量中,那么这个指针变量就指向该结构体变量;看下面例子:
//结构体
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;//需要加取址符
可以看到,在给结构体指针赋值时,需要在结构体变量名前加取址符 & ,这是结构体和数组不一样的地方;
数组名表示数组的首地址,而结构体变量名不是结构体的首地址,要想获取结构体变量的地址,必须在前面加 &;
如何通过结构体指针来访问成员?有两种方法:
(*pointer).member
或
pointer -> member
第一种写法中,. 的优先级高于 *,所以 (*pointer) 两边的括号不能省略;
第二种写法常用, ->称为箭头,指针可以通过箭头来直接对结构体成员操作;
上面的结构体指针指向了一个结构体变量,同时也可以用结构体指针指向一个结构体数组,如下:
上面的第21行中给结构体指针ps赋初值为 sts,也就是第一个元素的起始地址,然后在第25行的for循环中每次使得 ps 加1,此时指向了下一个元素的起始地址;
注意:
1)如果 ps 的初值为数组名,如上面的 sts,则 ps 指向 sts的第一个元素,其值为第一个元素的起始地址,ps++后就指向了下一个元素,这对任意指向数组的指针都是成立的;
2)上面的 ps 的类型为指向结构体类型 struct stu的指针变量,不能用它来指向结构体的某个成员,如下面的用法是不对的:
ps = sts[0].name;
如果要将某一个成员的地址赋给ps,可以使用强制类型转换,如下:
ps = (struct stu *)stu[0].name;
此时ps 的值为stu[0]元素的成员 name 的起始地址,可以使用 printf("%s\n", p)输出;
用结构体变量和结构体变量的指针作函数参数
将一个结构体的值传递给函数,有三个方法:
1)用结构体变量的成员作参数,如用 stu[1].name作函数实参,这与普通变量的传递是一致的,都是 值传递 方式,应当保持实参与形参的类型一致;
2)用结构体变量作实参,也是属于 值传递 方式,形参也必须是相同的结构体类型,这种方法在空间和时间上开销较大,很少使用;
3)用结构体指针作实参,将结构体变量的地址传递给形参,这种方法常用;
上面第26行函数的参数为结构体指针变量ps,然后在22行调用时传入的对应参数为结构体数组名,代表数组第一个元素的首地址,打印结果如下:
共用体
与结构体类似的是共用体(union)或叫联合,定义格式为:
union 共用体名{
成员列表
};
结构体与共用体的区别:
1)结构体的每个成员都会占用一段不同的内存,互相之间没有影响;而共用体的所有成员共同占用一段内存,修改一个成员会影响其余成员;、
2)结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存;共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉;
也就是说,共用体是用一段内存来存放不同类型的变量;例如把一个短整型变量、一个字符型变量、一个实数型变量存放在同一个人地址开始的内存单元中,如下图:
以上3个变量在内存中占用的字节数是不同的,但是起始地址都是从0x1000开始,也就是使用覆盖技术,后一个数据覆盖了前面的数据,这就是结构体类型;
共用体变量
共用体也是一种自定义类型,可以通过它来创建共用体变量,如下:
union data{
int n;
char ch;
double f;
};
union data a, b, c;//创建共用体变量 a b c
引用共用体变量的成员方法与结构体相同,都是使用 . ,格式如下:
结构体变量名.成员名
上面的共用体 data 中成员 f 的类型为 double,占用最多的字节数8,所以 data 类型的变量也占用8个字节;
在使用共用体时需要注意以下几点:
1)共用体使用同一段内存来存放不同类型的成员,但是在每个瞬间只能存放其中一个成员,而不是几个,如上面第13行的打印结构,多次给共用体的成员赋值后,得到的最终值是最后的赋值;
2)可以对共用体进行初始化,但是初始化列表中只能有一个常量,如下:
union data{
int n;
char ch;
double f;
};
union data a = {16};//正确
union data b = {16, 'a', 13.22};//错误
3)共用体变量的地址和它各个成员的地址都是同一个地址,如上面&a、&a.n、&a.ch、&a.f 都是同一个值
4)除了初始化外,不能对共用体变量赋值,只能对共用体成员赋值
枚举类型
如果一个变量只有几种可能的值,则可以定义为 枚举 类型;所谓枚举,就是把可能的值一一列举出来,变量的值仅限于列举出来的范围内;声明枚举变量的格式为:
enum 枚举名 {枚举元素列表};
eg:
enum Weekday {sun, mon, tue, wed, thu, fri, sat};//定义枚举类型
enum Weekday workday;//定义枚举变量
说明:
1)c编译时将枚举元素列表中的每一个枚举元素当做常量处理,在定义完枚举类型后就不能对枚举元素赋值了,下面的语句是错误的:
enum Weekday {sun, mon, tue, wed, thu, fri, sat};//定义枚举类型
sun = 1;//不能给枚举常量赋值
2)对于枚举元素,每一个元素都代表一个整数,它们的值按照顺序依次增加,如上面的 sun 默认值为0,mon 默认值为1...
3)可以在定义枚举类型时给其中的枚举元素赋值,但是要注意前面的元素的值必须要小于后面的元素的值,如下:
enum Weekday {sun=1, mon=3, tue, wed, thu, fri=9, sat};//定义枚举类型
//tue 默认为4,sat默认为10
用typedef 声明新类型
除了可以使用C提供的标准类型(如int char float)和程序编写者自己声明的结构体、共用体、枚举外,还可以使用 typedef 指定新的类型名来代替已有的类型名;有以下两种情况:
1)简单地用一个新的类型名来代替原有的类型名,如:
typedef int Integer;//指定用Integer来代替int
Integer a;//相当于 int a
2)命名一个简单的类型名来代替复杂的类型名,如:
typedef int Num[100];//声明Num为整型数组类型名
Num a;//相当于 int a[100]
简单来说,就是按照变量定义的方式,将变量名换为新类型名,并且在最前面加上 typedef ,这就声明了新类型名代表原来的类型;
习惯上,通常把typedef 定义的类型名的第一个字母用大写表示,以便区别
typedef 与 #define 的区别
ypedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西;如下:
#define INTERGE int
unsigned INTERGE n; //没问题
typedef int INTERGE;
unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned
在连续定义几个变量的时候,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 类型的指针;
再来看下面一个例子:
#inlcude <stdio.h>
#define base 0x0012ff60
#define flash ((TestType)* base) //这里定义了一个结构体指针flash,并将结构体指针flash的值赋值为0x0012ff60
typedef struct
{
int i;//假设int 类型占4个字节,那么 i 的地址为0x0012ff60,j的地址为0x0012ff64,依次类推
int j;
int k;
}TestType;
int main(void)
{
flash -> i = 0;
flash -> j = 1;
flash -> k = 2;
printf("%x \n", flash -> i);
return 0;
}