在 C 语言中,char、int、float……等属于系统内置的基本数据类型,往往只能解决简单的问题。当遇到比较复杂的问题时,只使用基本数据类型是难以满足实际开发需求的。因此C 语言允许用户根据实际项目需求,自定义一些数据类型,并且用它们来定义变量。
结构体是一种组合数据类型,由用户自己定义。结构体类型中的元素既可以是基本数据类型,也可以结构体类型。
定义结构体类型的一般格式为:
struct 结构体名
{
成员列表
};
成员列表由多个成员组成,每个成员都必须作类型声明,成员声明格式为:
数据类型 成员名;
struct Employee
{
char name[8];
int age;
int id;
int salary;
};//一定要有分号
struct Employee
{
char name[8];
int age;
int id;
int salary;
};
struct Employee emp;//全局的结构体变量
struct Employee
{
char name[8];
int age;
int id;
int salary;
}emp, emp1;//emp,emp1就是全局变量
struct
{
char name[8];
int age;
int id;
int salary;
}emp;
这种方式由于没有指定结构体名,不能再使用该结构体类型去定义其他变量
两个完全相同的匿名结构体,编译器也会把他们当做两个完全不同的类型
结构体的自引用
结构体中是不能直接包含一个该结构体本身的成员变量的,否则sizeof(struct Node)就无法计算其值,会类似递归一直循环计算
struct Node
{
int value;
struct Node next;//错误写法
};
此时就需要用到结构体类型的指针
指向结构体变量的指针就是结构体指针,指针变量中保存一个结构体变量的地址, 则这个指针变量指向该结构体变量,需要注意的是指针变量的类型必须和结构体变量的类型相同。
定义结构体指针变量的一般形式为: struct 结构体名* 指针变量名
struct Node
{
int value;
struct Node* next;
};
在 C 语言中,结构体变量初始化,本质上是对结构体变量中的成员进行初始化,使用花括号{ }在初始化列表中对结构体变量中各个成员进行初始化。
结构体中的成员可以是标量,数组,指针,甚至是其他结构体。
编译器会将“test”、20、1、10000按照顺序依次赋值给结构体变量emp中的成员name、 age、id、salary。
struct Employee
{
char name[8];
int age;
int id;
int salary;
};
struct Employee emp = {“test”,20,1,10000};
除了采用初始化列表,还可以使用赋值运算符对成员进行初始化
#include
#include
struct Employee
{
char name[8];
int age;
int id;
int salary;
};
int main(void)
{
struct Employee emp;
strcpy(emp.name,"test");
emp.age=20;
emp.id=1;
emp.salary=10000;
getchar();
return 0;
}
使用成员列表的方式初始化时,编译器会自动将字符串“test”复制到字符数组 name 中。而使用成员赋值方式初始化时,需要调用 strcpy 函数,将字符串“test”复制到字符数组 name 中。
在 C 语言中,通过结构体指针 p 也可以引用结构体中的属性(成员变量),有以下两种方式:
(1) (*p).成员变量名; *p表示的是结构体变量,可以通过变量直接引用其属性。注意,“.”运算符优先级是最高的,(*p_emp)两侧的括号不能省略。
(2) p->成员变量名; p表示的是指向结构体变量的指针,“->”称为指向运算符。
#include
struct Employee
{
char name[8];
int age;
int id;
int salary;
};
int main(void)
{
struct Employee emp={"test",20,1,10000};
struct Employee *p_emp = &emp;
printf("%s\n", p_emp->name);
printf("%d\n", (*p_emp).age);
printf("%d\n", emp.id);
printf("%d\n", emp.salary);
getchar();
return 0;
}
typedef 给类型起别名
在 C 语言中,除了使用 C 语言提供的标准类型名:char、int、double……以及自定义的结构体类型。还可以使用 typedef 关键字指定一个新的类型名来代替已有的类型名,相当于给已有类型起别名。
typedef 的一般使用形式为: typedef 原类型名 新类型名
#include
struct Employee
{
char name[8];
int age;
int id;
int salary;
};
typedef int integer;//给基本数据类型起别名
typedef struct Employee test;//给结构体类型起别名
int main(void)
{
integer a=10;
test t = {"test",20,1,10000};//如果不起别名,定义结构体类型的变量时,需加struct
printf("%d\n",a);
printf("%s",t.name);
getchar();
return 0;
}
起别名的另一种方式
typedef struct Stu
{
int age;
}Stu;
结构体复制
在 C 语言中,允许相同类型的结构体变量之间相互赋值。
#include
struct Employee
{
char name[8];
int age;
int id;
int salary;
};
typedef struct Employee test;
int main(void)
{
test t1 = {"test",20,1,10000};
test t2 = t1;//将结构体变量t1的各个成员的值原样复制一份到变量t2的各个成员中,t1变量中数据的修改不会影响到t2变量
getchar();
return 0;
}
如果不想复制一份数据,可以使用指针,这样p_t2就也指向t1变量对应的数据
test *p_t2 = &t1;
结构体传参
#include
struct S
{
int i;
char c;
};
//如果直接把结构体传入test函数,test函数中使用的是传入的结构体的一份拷贝
//改变其内容,并不会影响传入的结构体
void test(struct S s)
{
s.i = 5;
s.c = 'a';
}
int main(){
struct S s = {0};
test(s);
printf("%d\n", s.i);//还是输出0
printf("%d\n", s.c);//还是输出0
getchar();
return 0;
}
如果想要实现改变传入的结构体的数据,则可以传其地址或者说指针
#include
struct S
{
int i;
char c;
};
void test(struct S* s)
{
s->i = 5;//结构体指针就可以使用这种->的方式,获取结构体内部的属性
s->c = 'a';
}
int main(){
struct S s = {0};
test(&s);
printf("%d\n", s.i);//输出5
printf("%c\n", s.c);//输出a
getchar();
return 0;
}
结构体的对齐规则
1.第一个成员在与结构体变量偏移量为0的地址处
2.从结构体变量偏移量为0的地址处开始算,其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数:编译器默认的一个对齐数与该成员数据的大小相比的较小值
VS中默认的对齐数为8(gcc编译器没有默认对齐数)
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数,取其中的最大值)的整数倍
4.如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
#include
//假设s1的内存地址为0x00000010
struct S1
{
char c1;//c1在结构体变量偏移量为0的位置,所以c1的内存地址为0x00000010,char类型的数据占了一个字节
char c2;//c2的对齐数为1,所以c2的内存地址为0x00000011
//value占4个字节,4与8比较,4较小,所以value的对齐数为4,所以要从s1偏移量为0的地址开始算对齐数整数倍的地址,给value
//所以value的地址为0x00000014,0x00000012和0x00000013地址都被跳过,最后value再占4个字节,s1的所占内存总大小为8个字节
int value;
};
//假设s2的内存地址为0x00000010
struct S2
{
char c1;//c1在结构体变量偏移量为0的位置,所以c1的内存地址为0x00000010,char类型的数据占了一个字节
//value占4个字节,4与8比较,4较小,所以value的对齐数为4,所以要从s2偏移量为0的地址开始算对齐数整数倍的地址,给value
//所以value的地址为0x00000014,0x00000011、0x00000012、0x00000013地址都被跳过
int value;
//c2的对齐数为1,c2的内存地址为0x00000018
char c2;
//但是在计算结构体所占内存的总大小时,要为最大对齐数的整数倍数,此时结构体中所有数据占了9个字节,最大对齐数为4
//所以s2的总大小为12,0x00000019、0x00000020、0x00000021的空间都被浪费
};
int main(void)
{
struct S1 s1 = {0};
struct S2 s2 = {0};
printf("%d\n", sizeof(s1));//输出8
printf("%d\n", sizeof(s2));//输出12
getchar();
return 0;
}
结构体嵌套的情况
#include
//计算s111的大小为16,s111的最大对齐数为8
struct S1
{
double d;
char c;
int i;
};
//假设s222的内存地址为0x00000010
struct S2
{
//c的内存地址为0x00000010
char c;
//嵌套的结构体对齐到自己的最大对齐数的整数倍处,s1的最大对齐数为8,所以s1的内存地址为0x00000018,0x00000011~0x00000017都被浪费了,又因为s1的大小为16,所以s1占用16个字节的空间
struct S1 s1;
//d的内存地址为0x00000034,d占用8个字节,所以最后s222占用的内存空间为32
double d;
};
int main(void)
{
struct S1 s111 = {0};
struct S2 s222 = {0};
printf("%d\n", sizeof(s1));//输出16
printf("%d\n", sizeof(s2));//输出32
getchar();
return 0;
}
可以使用offsetof宏,来计算结构体中某变量相对于首地址的偏移
函数原型:size_t offsetof(structName, memberName);
#include
#include
struct S1
{
char c;
int i;
double d;
};
int main(void)
{
printf("%d\n", offsetof(struct S1, c));//输出0
printf("%d\n", offsetof(struct S1, i));//输出4
printf("%d\n", offsetof(struct S1, d));//输出8
getchar();
return 0;
}
为什么存在内存对齐?
1.平台原因。不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常
2.性能原因。数据结构(尤其是栈)应该尽可能的在自然边界上对齐。为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存仅需要一次访问
所以内存对齐,是一种空间换时间的做法
可以更改默认对齐数,使用预处理指令#pragma
#include
#pragma pack(4)//设置默认对齐数为4
struct S1
{
char c;
double d;
};
# pragma pack()//取消设置的默认对齐数,还原为默认值
int main(void)
{
struct S1 s1 = {0};
printf("%d\n", sizeof(s1));//输出12
getchar();
return 0;
}
位段的声明和结构体是类似的,但是有两个不同
1.位段的成员必须是int、signed int、unsigned int或者short、char(整型类型)
2.位段的成员名后面有一个冒号和一个数字,如果是int类型的成员,数字不能大于32,如果是char类型的成员,数字不能大于8
位段的是按照需要开辟内存使用空间的,如果位段的成员是int类型,则以4个字节为单位开辟空间,如果位段的成员是char类型,则以1个字节为单位开辟空间
#include
//位段,指的是二进制位
struct S
{
int a : 2;//a变量需要2个比特位
int b : 5;//b变量需要5个比特位
int c : 10;//c变量需要10个比特位
int d : 30;//d变量需要30个比特位
};
int main(){
struct S s = {0};
//在开辟内存时,位段S中数据类型为int,所以先分配4个字节,a占2个比特位,b占5个比特位,c占10个比特位,这个字节中还剩余15个比特位,不够分配给d,所以会再开辟4个字节的空间用于存放d,这15个比特位的未使用空间会被浪费掉。所以最后s占了8个字节的空间
printf("%d\n", sizeof(s));//输出8
getchar();
return 0;
}
位段赋值时
#include
struct S
{
char a : 3;//一个字节中,从右往左占3个比特位
char b : 4;//一个字节中,从右往左占4个比特位
char c : 5;//一个字节中,从右往左占5个比特位
char d : 4;//一个字节中,从右往左占4个比特位
//所以所占内存为 0bbbbaaa 000ccccc 0000dddd
};
int main(){
struct S s = {0};
printf("%d\n", sizeof(s));//输出3,这个位段占3个字节
s.a = 10;//10的二进制是1010,但是a只占3个比特位,所以只有010会放入a的内存空间中
s.b = 20;//20的二进制是10100,但是b只占4个比特位,所以只有0100会放入b的内存空间中
s.c = 3;//3的二进制是11,但是c占5个比特位,所以会把00011会放入c的内存空间中
s.d = 4;//4的二进制是100,但是d占4个比特位,所以只有0100会放入d的内存空间中
//所以在赋值完之后,位段从0bbbbaaa 000ccccc 0000dddd变为00100010 00000011 00000100,转化为16进制22 03 04
return 0;
}
位段涉及很多不确定因素,是不跨平台的
枚举的定义
#include
enum Color
{
//枚举的可能取值-是常量
RED,//默认值为0,可以在定义的时候修改,但是只能在定义的时候修改。比如RED = 5;如果RED为5,那么GREEN的值为6,BLUE的值为7。比如RED = 'a';那么GREEN的值为b,BLUE的值为c
GREEN,
BLUE
};
int main()
{
enum Color c = BLUE;
//输出0 1 2,是这三个枚举常量的默认值
printf("%d %d %d", RED, GREEN, BLUE);//为什么可以直接取到这三个枚举值?
return 0;
}
枚举中每个常量是什么类型的数据,是由编译器指定的,一般是整型,可以用sizeof()计算其大小,然后看是什么类型的数据
#define也可以定义常量
#include
#define RED 0;
int main()
{
int red = RED;
printf("%d", red);//输出0
return 0;
}
枚举相比于#define的好处
1.增加代码的可读性和可维护性
2.相比于#define,枚举有类型检查,更加严谨。#define只是声明了一个标识符,但是枚举中的变量,类型为此枚举类型
联合也叫共用体或者联合体
联合是一种特殊的自定义类型,在联合中的成员变量,共用同一块内存空间。
#include
union MyUnion
{
char c;
int i;
};
int main()
{
union MyUnion un = {0};
printf("%d\n", sizeof(un));//输出4
//un的地址、un.c的地址、un.i的地址都是一样的
printf("%p\n", &un);
printf("%p\n", &(un.c));
printf("%p\n", &(un.i));
return 0;
}
联合变量的大小,至少是最大成员的大小
当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍
//最大成员大小为arr的大小5,但是最大对齐数为4,所以MyUnion联合体的大小为8个字节
union MyUnion
{
int i;//最大对齐数为4
char arr[5];//最大对齐数为1,是按照数组中元素的数据类型来决定最大对齐数的
};