C语言 结构体
结构体
问题的引入
- 多个相同数据类型的数据可以用数组表示,那么,如果多个不同数据类型的数据如何用一个集合表示呢??
- 前面我们所介绍的普通数据类型实际上远远未能满足我们对实际应用中的要求,比如说一个学生,可能包含的属性有年龄、姓名、分数等等,不可能用一个基本数据类型(
int
、float
、char
)就能打发他们,因为学生有很多的属性才能构成一个完整的数据--学生,所以说,我们需要自己定义一个复合数据类型,满足现实应用中的要求。
结构体: 自定义的一种复合数据类型
struct student {
char *name;
int age;
int id;
float score;
}; // 注意这里的分号一定不能丢失
上面我们定义一个新的数据类型: struct student
这个类型里面包含了 name age score id等成员
- 使用struct student模块的结构体定义两个结构体变量:如
struct student tom;
struct student jerry;
- 注意: 区别
创建一个结构体数据类型
与定义一个结构体变量
- 变量名可以省略,但是分号
;
一定不能漏掉
typedef struct {
char *name;
int age;
int id;
float score;
} Student; // 分号不能漏掉
typedef 是一个关键字,作用为已经存在的数据类型定义一个新的名字(别名).这个别名在后面的代码中代替原来的名字,从而使代码更简洁
typedef 使用场景:结构体
,联合体
,枚举
优点:
- 提高代码的可读性
- 简化代码编写
- 便于代码的维护和修改
结构体的初始化
结构体跟普通变量一样,涉及定义、初始化、赋值、取址、传值等等操作,这些操作绝大部分都跟普通变量别无二致,只有少数操作有些特殊性。这其实也是结构体这种组合类型的设计初衷,就是让开发者用起来比较顺手,不跟普通变量产生太多差异。
- 结构体的定义和初始化。
- 由于结构体内部拥有多个不同类型的成员,因此初始化采用与数组类似的列表方式。
- 结构体的初始化有两种方式:①普通初始化;②指定成员初始化。
- 为了能适应结构体类型的升级迭代,一般建议采用指定成员初始化。
(1)按定义时的顺序依次初始化,用逗号隔开
struct date
{
int year;
int month;
int day;
};
struct student
{
char name[32];
int age;
struct date birthday;
};
struct student stu1={"zhangsan",18,{2008,9,10}};
(2)不按定义时的顺序,指定成员初始化
struct student stu2={
.age=18,
.name="lisi",
.birthday={
.day = 10,
.month = 9,
.year = 2008
}
};
- 指定成员初始化的好处:
- 成员初始化的次序可以改变。
- 可以初始化一部分成员。
- 结构体新增了成员之后初始化语句仍然可用。
结构体成员引用
语法: 使用 .
语法(成员引用符)访问结构体变量成员
结构体变量名.成员名
struct student stu1;
stu1.age;
// 成员变量与普通变量的用法及含义是一样。
&(stu1.age)
结构体指针
结构体推荐配合 typedef
关键字一起使用简化结构体类型的声明与相应指针类型的声明
例子:
- 不使用
typedef
的结构体指针
truct student
{
char name[20];
int age;
float score;
};
void test(struct student *s)
{
(*s).score = 18;
}
int main(int argc, char const *argv[])
{
struct student tom = {
.name = "tom",
.age = 18,
.score = 60};
printf("%f \n", tom.score);
test(&tom);
printf("%f \n", tom.score);
return 0;
}
- 使用
typedef
结构体指针
#include <stdio.h>
typedef struct
{
char name[20];
int age;
float score;
} Student;
void test(Student *s)
{
// (*s).score = 18;
s->score = 18;
}
int main(int argc, char const *argv[])
{
Student tom = {
.name = "tom",
.age = 18,
.score = 60};
printf("%f \n", tom.score);
// 结构体可以看成一种自己的基础数据类型,所以需要使用 & 来获取地址。
test(&tom);
printf("%f \n", tom.score);
return 0;
}
上面的代码中我们看到使用指针访问结构体成员时因为优先级关系,导致每次都需要 ()
将指针包含语法比较繁琐,c语言提供了结构体指针符号优化改写法 (语法糖)
// 在一些高级的编辑工具中我们需要使用 . 符号就行,如果是指针话编辑工具会自动转换 -> 语法的
s->score = 18;
// (*s).score = 18; 等价于
注意 typedef在结构体声明另一种写法
#include <stdio.h>
typedef struct
{
int id;
char name[10];
} Student, *Studentp; // Studentp 表示 Student 结构体的指针数据类型
int main(int argc, char const *argv[])
{
/* code */
Student tom = {
.id = 1,
.name = "tom"
};
Student *p = &tom;
Studentp p1 = &tom; // 与上面的代码等价
printf("%s \n", p->name);
printf("%s \n", p1->name);
return 0;
}
结构体数组
理解的前提:
- malloc 返回的是指针
- 变量先和 * 结合是一个数组指针(指向数组的指针) , 变量先和 [] 结合是一个指针数组 (数组中保存的指针元素)
() > [] > * > +
- 结构式一种自定义的普通数据类型,( 需要使用 & 来获取地址值。 )
使用 malloc 创建结构体数组方式:
结构体:
typedef struct user{
long id;
char username[20];
char password[20];
}User;
-
malloc 创建结构体数组,数组元素类型是结构体类型,malloc 返回数组指针
// 使用 malloc 创建一个结构体数组 , 数组中直接存储结构体数据类型 , // 优点:访问元素的属性速度相对较快 // 缺点:要求连续空间比较大 ( 结构体越大 或者元素越多就越明显) User (*arr)[4] = malloc(sizeof(User) * 4); (*arr)[0].age = 19; printf("%d \n", (*arr)[0].age); free(arr); arr = NULL;
- 优点:数组中保存的就是元素本身,所以可以通过 . 直接返回元素的属性值
- 缺点:如果结构本身占用的字节比较多,而且数组还要保存多个元素,这时就要求有一片较大的连续的内存空间才能完成结构体数组的创建。
- 返回值:数组指针
-
malloc 创建结构体数组 , 数组元素是结构体的指针类型, malloc 返回数组指针。
// 使用malloc 创建一个 User 结构体 User *p = malloc(sizeof(User)); strcpy(p->username, "zhangsan"); strcpy(p->password, "12234"); p->age = 19; // 使用 malloc 创建一个结构体数组,并且数组的元素是结构体的指针 // 优点:需要的连续内存空间相对较小(一个指针 8 个字节就可以了) // 缺点:1. 由于数组中的存储的结构体的指针,所以在使用之前必须进行初始化, 2. 访问属性的速度比较慢 User *(*arr2)[4] = malloc(sizeof(User *) * 4); printf("%p \n", arr2); printf("%p \n", *arr2); (*arr2)[0] = p; // 必须先初始化 (*arr2)[0]->age = 100; printf("%d \n", (*arr2)[0]->age); free(arr2); arr2 = NULL;
- 优点:占用连续的内存空间比较小, 数组中每一个元素的都是一个指针(一个指针占用 8 个字节 )
- 缺点:由于数组中元素是一个一个指针,如果要访问属性需要先解引用后才能访问属性值。( 有语法糖可以简化 指针-> 属性 )
- 返回值:数组指针
-
malloc 创建结构体数组,数组的每个元素是元素本身,返回数组中首个元素的地址
// 利用 malloc 函数来创建结构体数组, 数组元素是 User,并且 malloc 返回真的指针指向的数组的首地址 User *arr3 = malloc(sizeof(User) * 4); arr3[0].age = 199; printf("%d \n", arr3[0].age); free(arr3); arr3 = NULL;
- 优点:数组中保存的就是元素本身,所以可以通过 . 直接返回元素的属性值
- 缺点:如果结构本身占用的字节比较多,而且数组还要保存多个元素,这时就要求有一片较大的连续的内存空间才能完成结构体数组的创建。
- 返回值:数组中首个元素的地址 。 ( 可以少解一次引用 ,可以直接当成一维数组使用 )
-
malloc 创建结构体数组 , 数组元素是指向元素的指针 , 返回数组的首个元素的地址
// 使用malloc 创建一个 User 结构体 User *p = malloc(sizeof(User)); strcpy(p->username, "zhangsan"); strcpy(p->password, "12234"); p->age = 19; // 利用 malloc 函数来创建结构体数组, 数组元素是 User* 指针 // ,并且 malloc 返回的首个元素的地址 // User* 是元素的数据类型 , *arr 是指针变量 User **arr4 = malloc(sizeof(User *) * 4); arr4[0] = p; arr4[0]->age = 10086; printf("%d \n", arr4[0]->age); free(arr4); arr4 = NULL;
- 优点:占用连续的内存空间比较小, 数组中每一个元素的都是一个指针(一个指针占用 8 个字节 )
- 缺点:由于数组中元素是一个一个指针,如果要访问属性需要先解引用后才能访问属性值。( 有语法糖可以简化 指针-> 属性 )
- 返回值:数组首个元素的地址 , ( 可以少解一次引用 ,可以直接当成一维数组使用 )
内存对齐
为何需要地址对齐
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地
- 址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理
- 器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
- 总体来说:结构体的内存对齐是拿空间来换取时间的做法。目的是提高CPU访问内存数据的效率.
内存对齐:
1. 不管是结构体,还是普通的变量都存在内存对齐。
规则:
- 只要放在自己的类型的整数倍的内存地址上。
理解:
- 内存地址 / 占用字节 = 结果可以被整除
举例:
- int 存在的位置:内存地址一定可以被 4 整除。
- long long 存放的位置:内存地址一定可以被 8 整除。
- double 存放的位置:内存地址一定可以被 8 整除
结构体特有的: 结构体的内存对齐加一个条新的规则:结构体的大小,是最大类型的整数倍 (用来确定后一个部位的情况)
注意点:对齐的时候会补充空白字符,但是不会改变原来字节的大小,比如 char 在补位之后,本身还是一个字节
总结:
- 任何数据类型的数据都放在自己的整数倍的地址上
- 结构体的大小一定是最大数据类型的整数倍。
省内存技巧:定义结构体的时候,数据类型大小按照从小到大写。
共用体
几个不同的变量共用同一段内存的结构,在C语言中,被称为"共用体"类型结构
定义共用体类型
union 共用体名
{
成员类型1 成员名1;
成员类型2 成员名2;
...
};
比如:
union A{
char c[9];
int n;
double d;
};
特点
a.联合体中的所有成员共享一段内存的
b.联合体中的最大个数据成员的大小就是联合体的大小
大小端模式
-
概念
一个多字节存储单元的低地址存储数据的低有效位还是高有效位 -
为何会有大小端模式
以前不同的芯片公司在处理把寄存器的数据存放到内存中时,采用的方式不统一,才产生了大小端两种模式。小端模式:低地址存放数据的低有效位
大端模式:低地址存放数据的高有效位
测试 当前主机 是 小端序 还是 大端序
#include<stdio.h>
union data{
int a;
char b;
};
int main()
{
union data myData;
myData.a = 0x12345678;
printf("%x\n",myData.b); //78 说明当前主机是小端序
return 0;
}
实际应用场景
网络中两个不同端序的主机之间进行通信时,需要进行字节序转换
结构体与共用体的区别
- 结构体变量所占内存长度是各成员占的内存长度之和,每个成员分别占有其自己的内存单元。共用体变量所占的内存长度等于最长的成员的长度。共用体的内存开销要小一点。
- 在共用体所用的内存中已经写入了数据,当使用其它元素时上次使用的内容将被覆盖. 也就是说他使几个不同类型的变量共占一段内存(相互覆盖),每次只有一个能使用。结构体则不然, 每个成员都会有存储空间的,可以一起用.内部变量间是相互独立的
枚举
概念:
枚举类型用于声明一组命令的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。
格式:
enum 枚举的名字{
常量名字1,
常量名字2,
常量名字3,
};
//设计一个枚举类型
enum CAR_MOVE{
CAR_MOVE_UP = 1, //如果没有进行赋值默认从0开始 此时 CAR_MOVE_UP 就是一个常量标识符 1
CAR_MOVE_DOWN,
CAR_MOVE_LEFT,
CAR_MOVE_RIGHT
};
注意:如果里面的变量没有赋值,则默认从0开始,然后往后面递增
访问
外界访问枚举类型的数据成员可以直接访问
//定义一个枚举类型的变量
enum color mycolor;
//赋值的时候直接访问
mycolor = RED;
作用
不管是使用宏来定义常量,还是使用枚举来定义常量,目的只有一个:增强程序的可读性。
#include<stdio.h>
//设计一个枚举类型
enum CAR_MOVE{
//枚举常量
CAR_MOVE_UP , //如果没有进行赋值默认从0开始 此时 CAR_MOVE_UP 就是一个常量标识符 1
CAR_MOVE_DOWN,
CAR_MOVE_LEFT, //2
CAR_MOVE_RIGHT
};
int main()
{
//int mode = -1;
//定义一个枚举变量
enum CAR_MOVE mode = CAR_MOVE_LEFT; //整型变量
printf("%d size:%ld\n",CAR_MOVE_LEFT,sizeof(CAR_MOVE_LEFT));
printf("mode size:%ld\n",sizeof(mode));
switch(mode)
{
case CAR_MOVE_UP://小车向上移动
break;
case CAR_MOVE_DOWN://小车向下移动
break;
case CAR_MOVE_LEFT://小车向左移动
printf("CAR_MOVE_LEFT\n");
break;
case CAR_MOVE_RIGHT://小车向左移动
break;
}
return 0;
}