c语言中为我们提供了几种基本数据类型,但是在实际编程中,有时候要表达复杂数据关系时,单单使用基本数据类型不能很好的表示,为了解决这种问题,可以自己构建一个结构体数据类型,定义的结构体数据类型与基本数据类型使用方式一致,这样就可以很好的满足对数据类型扩充的需求。
要表示一个学生信息,一般包括姓名、学号、年龄等。如果不使用结构体,要表示这些信息我们要定义下面这些字段:
char name[8] = "kobe";
int number = 24;
int age = 9;
这种方式虽然可以表示学生的信息,但是考虑到使用数据的地方,这种方式显然并不友好。比如现在有一个方法要打印学生信息,如果用这种定义方式,在方法定义时需要定义三个参数:
/**
* 打印学生信息
*/
void print_student(char name[], int number, int age) {
printf("Student : %s, %d, %d\n", name, number, age);
}
但是使用结构体,上面的定义和使用方式会变成下面这种形式:
#include <stdio.h>
#include <string.h>
/**
* 学生结构体
*/
typedef struct student {
unsigned short age; // 年龄
int number; // 学号
char name[20]; // 姓名
} student;
/**
* 打印学生信息
*/
void print_student(struct student stu) {
printf("Student : %s, %d, %d\n", stu.name, stu.number, stu.age);
}
int main() {
struct student stu;
strcpy(stu.name, "rose");
stu.number = 1;
stu.age = 12;
print_student(stu);
return 0;
}
通过使用结构体,表达的含义会变得简单并且更有逻辑性,而且在c语言中,结构体完全可以按照基本数据类型那样定义和使用。数组、指针这些使用场景完全适用。
1、结构体定义和初始化
定义结构体通过struct关键字,定义的格式为:
struct 结构体名 {
字段信息1;
字段信息2;
...
字段信息n;
};
比如定义一个学生信息的结构体:
struct student {
unsigned short age; // 年龄
int number; // 学号
char name[20]; // 姓名
};
在使用结构体时,同时需要带上 struct 结构体名 这两部分,为了简化使用,可以为结构体定义别名,定义格式调整为:
typedef struct 结构体名 {
字段信息1;
字段信息2;
...
字段信息n;
} 结构体别名;
如:
typedef struct student {
unsigned short age; // 年龄
int number; // 学号
char name[20]; // 姓名
} student;
在使用结构体类型时,就可以省略 struct 关键字,只通过结构体别名定义数据类型,就像使用基本数据类型一样:
// 不省略 struct 关键字,通过结构体名定义变量
struct student stu;
// 省略 struct 关键字,通过结构体别名定义变量
student stu;
结构体的初始化有几种方式:
1、先定义结构体变量,然后对结构体里面的字段逐个进行初始化赋值:
// 分字段初始化
struct student stu;
strcpy(stu.name, "Rose");
stu.number = 1;
stu.age = 12;
print_student(stu);
2、定义结构体变量时按照结构体中定义字段的顺序进行初始化:
// 顺序初始化
struct student stu = {11, 2, "Jordan" };
print_student(stu);
3、定义结构体变量时指定属性名进行初始化,属性名称不一定是按照在结构体中定义的顺序赋值:
// 指定属性名乱序初始化
struct student stu = { .name = "Kobe", .number = 2, .age = 12 };
print_student(stu);
2、结构体和数组、指针
定义的结构体跟c语言中提供的基本数据类型使用方式一致,它同样支持数组和指针这种使用方式:
// 结构体数组
student arr[] = {
{13, 5, "Johnson" },
{14, 6, "Bird" }
};
int len = sizeof(arr) / sizeof(student);
for(int i = 0; i < len; i++) {
print_student(arr[i]);
}
指针也一样,只是取数据方式支持两种(.和->):
// 结构体指针
student* p = malloc(sizeof(student));
strcpy(p->name, "ONeal");
p->number = 7;
p->age = 12;
print_student(*p);
// 单独访问某个属性
printf("visit field : %d, %d\n", p->age, (*p).age);
完整示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
* 学生结构体
*/
typedef struct student {
unsigned short age; // 年龄
int number; // 学号
char name[20]; // 姓名
} student;
/**
* 打印学生信息
*/
void print_student(struct student stu) {
printf("Student : %s, %d, %d\n", stu.name, stu.number, stu.age);
}
int main() {
// 分字段初始化
struct student stu;
strcpy(stu.name, "Rose");
stu.number = 1;
stu.age = 12;
print_student(stu);
// 顺序初始化
struct student stu1 = {11, 2, "Jordan" };
print_student(stu1);
// 指定属性名乱序初始化
struct student stu2 = { .name = "Kobe", .number = 3, .age = 12 };
print_student(stu2);
student stu3 = {14, 4, "James" };
print_student(stu3);
// 结构体数组
student arr[] = {
{13, 5, "Johnson" },
{14, 6, "Bird" }
};
int len = sizeof(arr) / sizeof(student);
for(int i = 0; i < len; i++) {
print_student(arr[i]);
}
// 结构体指针
student* p = malloc(sizeof(student));
strcpy(p->name, "ONeal");
p->number = 7;
p->age = 12;
print_student(*p);
// 单独访问某个属性
printf("visit field : %d, %d\n", p->age, (*p).age);
free(p);
return 0;
}
3、结构体内存对齐
在c语言中,所有数据类型都遵循内存对齐的原则,也就是:不同类型的数据只能存储在相对于起始地址偏移为该类型整数倍的内存地址上。表述起来很拗口,简单来说,就是存储数据的地址能被该类型所占内存大小整除。
比如下面这几个数据类型的内存地址输出如下:
#include <stdio.h>
int main() {
// int占4字节
int a = 10;
int b = 20;
printf("%p\n", &a);
printf("%p\n", &b);
// long long占8字节
long long fa = 10L, fb = 20L;
printf("%p\n", &fa);
printf("%p\n", &fb);
return 0;
}
其中的某一次输出:
00000072b55ff81c
00000072b55ff818
00000072b55ff810
00000072b55ff808
大家可以通过计算器做一下运算,这些内存地址肯定能整除变量的占用空间大小。
结构体的内存对齐原则如下:
1、结构体第一个成员对齐到结构体变量起始位置偏移量为0的地址处
2、其他成员变量需要对齐到变量所占内存大小的整数倍位置
3、结构体总大小应为所有变量类型中最大数据类型的整数倍
4、在结构体的对应位置填充字节让整个结构体满足上面的规则
下面通过代码验证上面的规则:
#include <stdio.h>
struct memory_demo {
char c1;
char c2;
int i;
double d;
long long l;
};
int main() {
struct memory_demo md;
printf("-------- struct size --------\n");
printf("%llu\n", sizeof(struct memory_demo));
printf("%llu\n", sizeof(md));
printf("-------- struct address --------\n");
printf("%p\n", &md);
printf("%p\n", &(md.c1));
printf("%p\n", &(md.c2));
printf("%p\n", &(md.i));
printf("%p\n", &(md.d));
printf("%p\n", &(md.l));
return 0;
}
-------- struct size --------
24
24
-------- struct address --------
000000774f9ff800
000000774f9ff800
000000774f9ff801
000000774f9ff804
000000774f9ff808
000000774f9ff810
分析一下上面的内存使用:前两个属性是char类型,占用1个字节内存;第三个属性是int类型占用4个字节内存,所以它相对起始位置偏移4个字节;第四个属性是double类型占用8个字节内存,所以它应该相对起始位置偏移8的整数倍字节;第五个属性是long long类型也是占用8字节,所以它应该在起始位置偏移16个字节的位置。
调整结构体内字段的顺序,在观察一下内存占用:
#include <stdio.h>
struct memory_demo {
char c1;
char c2;
double d;
int i;
long long l;
};
int main() {
struct memory_demo md;
printf("-------- struct size --------\n");
printf("%llu\n", sizeof(struct memory_demo));
printf("%llu\n", sizeof(md));
printf("-------- struct address --------\n");
printf("%p\n", &md);
printf("%p\n", &(md.c1));
printf("%p\n", &(md.c2));
printf("%p\n", &(md.d));
printf("%p\n", &(md.i));
printf("%p\n", &(md.l));
return 0;
}
-------- struct size --------
32
32
-------- struct address --------
000000e8349ffd40
000000e8349ffd40
000000e8349ffd41
000000e8349ffd48
000000e8349ffd50
000000e8349ffd58
分析一下上面的内存使用:前两个属性是char类型,占用1个字节内存;第三个属性是double类型占用8个字节内存,所以它相对起始位置偏移8个字节;第四个属性是int类型占用4个字节内存,所以它应该相对起始位置偏移4的整数倍字节,第三个属性在起始位置偏移8字节,同时占用8字节,所以第四个属性相对起始位置偏移16个字节;第五个属性是long long类型也是占用8字节,所以它应该在起始位置偏移24个字节的位置。
再次调整结构体属性字段顺序:
#include <stdio.h>
struct memory_demo {
double d;
long long l;
int i;
char c1;
char c2;
};
int main() {
struct memory_demo md;
printf("-------- struct size --------\n");
printf("%llu\n", sizeof(struct memory_demo));
printf("%llu\n", sizeof(md));
printf("-------- struct address --------\n");
printf("%p\n", &md);
printf("%p\n", &(md.d));
printf("%p\n", &(md.l));
printf("%p\n", &(md.i));
printf("%p\n", &(md.c1));
printf("%p\n", &(md.c2));
return 0;
}
-------- struct size --------
24
24
-------- struct address --------
000000477f1ff660
000000477f1ff660
000000477f1ff668
000000477f1ff670
000000477f1ff674
000000477f1ff675
分析一下上面的内存使用:第一个属性是double类型,占用8个字节内存;第二个属性是long long类型占用8个字节内存,所以它相对起始位置偏移8个字节;第三个属性是int类型占用4个字节内存,所以它应该相对起始位置偏移16个字节;最后两个属性是char类型占用1字节,所以它们应该在起始位置偏移20、21个字节的位置。由于整个结构体最大数据类型占用8字节,在结构体最后补2个字节,确保整个结构体是8的整数倍。
通过上面的几个示例分析,大家对结构体的内存结构会有很好的理解,在以后设计结构体内属性顺序时,尽量编排占用内存小的属性靠前,这样会让整个结构体占用内存更小。
4、联合体
联合体与结构体比较类似,只不过在联合体中多个属性字段共用一段内存空间,联合体占用内存大小为最大数据类型的大小。如果同时给多个属性字段赋值,那么后面的值会覆盖前面的值,所以,在联合体中只能使用其中的一个数据类型。
联合体占用内存大小:
#include <stdio.h>
union memory_demo {
char ch;
int i;
double d;
};
int main() {
printf("%d\n", sizeof(union memory_demo));
return 0;
}
联合体内所有属性在内存中占用的地址相同:
#include <stdio.h>
union memory_demo {
char c;
int i;
double d;
};
int main() {
union memory_demo md;
printf("-------- union size --------\n");
printf("%llu\n", sizeof(union memory_demo));
printf("%llu\n", sizeof(md));
printf("-------- union address --------\n");
printf("%p\n", &md);
printf("%p\n", &(md.c));
printf("%p\n", &(md.i));
printf("%p\n", &(md.d));
return 0;
}
由于所有属性的内存地址相同,同时给多个属性赋值时,会导致数据错乱:
#include <stdio.h>
union memory_demo {
char c;
int i;
double d;
};
int main() {
printf("-------- union value --------\n");
union memory_demo md;
md.c = 'A';
md.i = 22;
md.d = 33.3;
printf("%c\n", md.c);
printf("%d\n", md.i);
printf("%lf\n", md.d);
return 0;
}