在前面我们学习过char,short,int,long,float,double等,这些都属于内置类型,C语言本身就含有的数据类型。
而对于结构体,枚举,联合体等这种复杂的类型,我们称之为自定义类型。
结构体:
结构体的定义:结构是一些值的集合,这些值被称为成员变量,结构的每个成员可以是不同类型的变量。
结构体的定义:
struct tag //结构体标签 { member - list;//成员变量列表:可以是同种类型,也可以是不同种类型 }variable-list;//变量列表:注意后面的“;”,不要忘记了
结构体的声明:
#include<stdio.h> struct Stu { char name[20]; int age; char phone[20]; char sex; }Student1, Student2;//创建全局结构体变量 struct Stu Student3;//创建全局结构体变量 int main() { //创建局部结构体变量 struct Stu Student4; struct Stu Student5; struct Stu Student6; return 0; }
匿名结构体类型:
与上面所不同的是,匿名结构体是没有名字的,它的特点是只能使用一次,并且只能在创建的时候定义结构体变量,就是下述S的这种创建方式。
struct { char name[20]; int age; char phone[20]; char sex; }S;
那么省略了结构体标签,结构体指针是否能够指向两个成员变量相同的结构体呢?
举例:
#include<stdio.h> struct { int a; int b; }sa; struct { int a; int b; }*pasa; int main() { pasa = &sa; }
当你尝试运行该程序,你会发现,编译并没有通过,编译器指出,无法实现将指针变量pasa的指向改变。
原因是即使上述两个编译器的成员变量是完全相同的,但是编译器依然会把二者当成完全不同的两个类型,所以是无法通过编译的。
结构体的自引用:
在结构中包含一个类型为该结构本身的成员是否可以呢?
举例:
在数据结构中,我们接触过链表,如下所示:
想用结构体表示链表,是否可以用以下这种表示方法?
struct Node { int data; struct Node n; };
data包含存储的数据,struct Node n表示下一个节点,这样有点道理,但不多,原因是:struct Node n既包含了下个节点还包含了上个节点的数据等,这样反复包含,我们根本无法确定n的大小,那么sizeof(struct Node)的值是无法确定的,在此后,如果我们想使用该结构体,根本没有办法确定应该给它创建多大的空间。
因此,我们可以得出一个结论:结构体类型可以包含除了自己以外的结构体变量。
对于表示上述链表的正确代码应为:
#include<stdio.h> struct Node { int data;//4个字节 struct Node*next;//定义一个类型为struct Node的指针,字节大小为4/8 }; int main() { sizeof(struct Node);//字节大小为8-12 }
如下图所示:
对类型名重命名:
举例:
typedef struct Node { int data; struct Node*next; }Node1;//给struct Node起别名为Node1 //起别名后:原始名和别名都可以直接使用 int main() { struct Node s;//使用原始名创建结构体变量 Node1 s1;//使用别名创建结构体变量 }
对匿名结构体进行重命名:
当我们省略原始名之后,此时的结构体就变成了匿名结构体,那么,我们能否使用别名定义指针?
如下所示:
typedef struct { int data; Node1* next; }Node1; int main() { Node1 s1; }
答案是不可以的,程序由上自下进行,当程序运行到Node1* next;,结构体别名并未被定义,所以是不能使用这种方法进行的,因此对结构体进行重命名之后不要省略原始名。
结构体变量的初始化:
举例:
#include<stdio.h> struct Stu { int age; char name[20]; char phone[20]; }; int main() { struct Stu s={19,"张三","217361"}; printf("%d %s %s", s.age, s.name, s.phone); return 0; }
19 张三 217361
结构体嵌套初始化:
#include<stdio.h> struct Stu1 { char sex[10];//注意这里的sex:单个汉字,不能将类型直接定义为%c //原因:汉字由两个字符组成,%c根据ascll码值只能识别数字来打印,如果我们使用%c打印单独的汉字那么就会乱码 char address[20]; }; struct Stu { int age; char name[20]; char phone[20]; struct Stu1 s1;//结构体Stu1作为结构体Stu的成员变量 }; int main() { struct Stu s = { 19,"张三","217361",{"男","北京市海淀区"}}; //结构体嵌套初始化时需要注意:成员变量为结构体时,初始化的数据需要用括号括起来 printf("%d %s %s %s %s", s.age, s.name, s.phone,s.s1.sex,s.s1.address); return 0; }
输出如下所示:
结构体内存对齐:
举例:
#include<stdio.h> struct Stu1 { char c1; int a; char c2; }; struct Stu2 { char c1; char c2; int a; }; int main() { struct Stu1 s1 = { 0 }; printf("%d\n", sizeof(s1)); struct Stu2 s2 = { 0 }; printf("%d\n", sizeof(s2)); return 0; }
我们以Stu1为例进行分析:
结构体的对齐规则:
1:第一个成员在与结构体变量偏移量为零的地址处
分析如下:
2:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员大小的较小值。
vs中默认的值为8,而对于gcc编译器来说,是没有默认对齐数的,因此对齐数就是该成员大小
注:变量和变量之间的空间会被浪费
3:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有的最大对齐数(含嵌套结构体的对齐数)的整数倍。
举例:
struct Stu3 { double c1; char a; int c2; }; struct Stu4 { char c1; struct Stu3 s3; double d; };
分析如下:
为什么存在内存对齐?
1:平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2:性能原因:数据结构(尤其是栈)应尽可能地在自然边界上对齐,原因在于:为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次内存访问。
总结来说:结构体的内存对齐是拿空间换取时间的做法。
分析如下:
在设计结构体的时候,如果我们既要满足对齐还要满足节省空间,那么就需要让占用空间小的成员尽量集中在一起。
举例:
struct Stu3 { char c; int a; char c2; }; struct Stu4 { char c1; char c2; int a; }; //Stu4所占的空间少一些,因为浪费的空间少
修改默认对齐数:
方法:#pragma pack(4)//设置默认对齐数
#pragma pack()//取消设置的默认对齐数
修改之后,浪费的空间数就会减少。
举例:
#pragma pack(4)//设置默认对齐数为4 struct Stu3 { char c; double b; };
输出结果为
12
取消设置的默认对齐数
输出结果为
16
这样操作之后,空间就节省了4个字节,因此,结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
offsetof(structName,memberName):
用来计算成员变量的偏移量,它的头文件是<stddef.h>
举例:
#include<stdio.h> #include<stddef.h> struct Stu3 { char c; int i; double b; }; struct Stu4 { char c1; char c2; int a; }; int main() { printf("%d ", offsetof(struct Stu3, c)); printf("%d ", offsetof(struct Stu3, i)); printf("%d ", offsetof(struct Stu3, b)); }
输出如下所示:
0 4 8
结构体传参:
举例:
#include<stdio.h> struct Stu3 { char c; int i; double b; }; void Init(struct Stu3 tmp)//更改结构体Stu3的值 { tmp.c = 'b'; tmp.i = 0; tmp.b = 13.14; } int main() { struct Stu3 s3; s3.c = 'h'; s3.i = 3; s3.b = 23.45; Init(s3);//结构体名传递 printf("%c ", s3.c); printf("%d ", s3.i); printf("%f ", s3.b); return 0; }
输出如下所示:
为什么值没有发生改变呢?
和我们之前在指针学习哪里是一样的,这里也是引用传递,编译器在函数Init中更改的只是Stu3的复制品,因此出Init函数之后值还是原来的值,并没有发生真正意义上的改变。
正确的传参方式应该是地址传递:
代码可修改为:
#include<stdio.h> struct Stu3 { char c; int i; double b; }; void Init(struct Stu3* tmp) { tmp->c = 'b'; tmp->i = 0; tmp->b = 13.14; } void print(struct Stu3 tmp) { printf("%c ", tmp.c); printf("%d ", tmp.i); printf("%f ", tmp.b); } void print1(struct Stu3* tmp) { printf("%c ", tmp->c); printf("%d ", tmp->i); printf("%f ", tmp->b); } int main() { struct Stu3 s3; s3.c = 'h'; s3.i = 3; s3.b = 23.45; Init(&s3);//通过地址传递,进行结构体变量的修改 print(s3);//通过值传递,打印变量 print1(&s3);//通过地址传递,打印变量 return 0; }
此时的输出结果正是我们修改后的值:
但是在打印修改后的变量时,我们也选用了两种方法,值传递和地址传递,那么这两种方法,那种好一些呢?
答案是:地址传递
原因:函数进行传参时,参数是需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,会导致性能的下降。
因此,结构体传参的时候,要传结构体的地址。