什么是结构体呢?
我们都知道数组,我们可以在数组中存放相同数据类型的数据,比如说整型数组里存放的都是整形,字符数组里存放的都是字符,但是,当我们想要存放不同数据类型的时候,比如说一个学生的姓名,年龄,性别,电话号码,我们该怎么做呢?这时候就体现到自定义数据类型:结构体的作用了。我们可以在结构体中存放我们想要存放的数据类型,这就极大的方便了我们的日常使用。
那么我们先来通过一个简单代码,来看看结构体是怎样定义和使用的吧。
#include<stdio.h> struct Stu { char name[20]; int age; char sex[10]; char number[11]; }S; int main() { S = { "张三",18,"female","12345678901" }; return 0; }
struct是关键字,而struct tag则是结构体类型 ,member-list是成员变量,variable-list是一个结构体变量,它属于全局变量,当我们初始化的时候我们可以像上面那样按顺序初始化,也可以像这样不需要严格的按顺序来初始化。
#include<stdio.h> struct Stu { char name[20]; int age; char sex[10]; char number[11]; }; int main() { //通过.来访问结构体成员变量 struct Stu S = { .age = 18,.sex = "female",.name = "张三",.number = "12345678901" }; return 0; }
并且这里得注意,当我们在给结构体成员变量赋值的时候,前面必须得加上结构体数据类型,否则会报错。就像这样:
#include<stdio.h> struct Stu { char name[20]; int age; char sex[10]; char number[11]; } S; int main() { S = { .age = 18,.sex = "female",.name = "张三",.number = "12345678901" }; return 0; }
在声明结构体类型的时候,还有一种特殊的声明:不完全声明,也叫匿名结构体
struct { char name[20]; int age; char sex[10]; char number[11]; } S;
这个结构体因为省略了tag,所以我们不知道他的数据类型,所以如果我们要在使用的时候,就需要在定义的时候就初始化,否则我们就找不到这个结构体了。
#include<stdio.h> struct { int a; int b; }x; struct { int c; int d; } *p; int main() { p = &x; }
这个代码就是典型的匿名结构体不知道结构体类型的错误。
结构体的自引用
当我们知道了结构体之后,那么结构体有什么作用呢?结构体通常被使用在链表当中,链表中分为数据域跟指针域,数据域中用来数据,指针域中用来存放地址。所以这就需要我们使用结构体来存放不同类型的变量。但是我们这样写可以吗?
struct ListNode { int data; struct ListNode next; };
这样很显然是不能达到我们的目的的,因为这里struct ListNode并没有定义结束,我们在这里使用的时候可能会报错。正确的定义方法应该是这样的:
struct ListNode { int data; struct ListNode* next; };
我们定义一个结构体指针,这个结构体指针用来存放另一个结构体的地址。
并且我们知道,在函数传参的时候,我们可以传值,也可以传址。那么当我们的参数是结构体的时候我们是传值好一些还是传地址好一些呢?
#include<stdio.h> struct S { int data[1000]; int num; }; void Print1(struct S s) { printf("%d\n", s.num); } void Print2(struct S* s) { printf("%d\n", s->num); } int main() { struct S s = { {1,2,3,4,5},10 }; Print1(s); Print2(&s); return 0; }
是Print1好一些还是Print2好一些呢?我们都知道当我们传入的是值时,其实传入的是一份参数的临时拷贝,拷贝就当然需要额外的消耗内存了,如果结构体的占用内存小一点还好,如果内存很大,那么代码的速度就会减慢,而我们传址,传入的是地址,用指针变量来接收,指针变量最多也就是8个字节,这样就极大的节省了空间,但是又有人会问了,如果传入的结构体的指针,我们在函数中可能会修改结构体里面的内容,这样不就显得不安全了吗?没错,这个顾虑是对的,但是我们可以通过适当的修改来解决这个忧患,那就是加上const修饰。
void Print2(const struct S* s) { printf("%d\n", s->num); }
说到结构体占用的内存,我们如果想知道结构体的大小该怎么办呢
计算结构体的大小(对齐数)
#include<stdio.h> struct S1 { char c1; int i; char c2; }; int main() { printf("%d\n",sizeof(struct S1)); return 0; }
结果会是什么呢?6?我们来看看。
12,为什么这个结构体的大小是12呢?
这里我们就需要知道结构体的对齐数了,在结构体中每个地址都有一个跟0地址处的偏移量,每个数据存储的时候都必须存放在该数据类型跟编译器的默认对齐数相比,较小的数字的整数倍,我们平时使用的vs默认的对齐数是8个字节,而gcc则没有默认对齐数。不仅如此,结构体的第一个数据必须得从0偏移处开始存放,在计算出所有的占用内存后,内存的大小还必须是每个成员变量的整数倍数。知道了这些后我们来看看上面这个12是怎么来的吧。
那么再来一个题,小试一手吧。
#include<stdio.h> struct S1 { char c1; int i; char c2; }; struct S2 { int a; char b; struct S1; }; int main() { printf("%d\n", sizeof(struct S2)); return 0; }
这个原理跟上面那个一样的,只是一个结构体中多了一个结构体,我们可以把里面的结构体拆开来看。
我们再来了解下面一个知识:位段。
位段
什么是位段
1.位段的成员得是整形家族
2.位段的成员名后边有一个冒号和一个数字。
例如:
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
A是一个位段类型,那么A的大小是多少呢?
#include<stdio.h> struct A { char _a : 2; char _b : 5; char _c : 6; char _d : 5; }; int main() { printf("%d\n", sizeof(struct A)); return 0; }
我们来看看位段在内存中是怎样分配内存的吧。
1.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
2. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
char类型,先分配8个比特位,并且从右向左使用,如果剩下的不够使用,就另外再开辟。
一共开辟的是3个字节。
枚举
枚举顾名思义就是列举,可以把可能的值都列举出来。
enum Day//星期 { Mon, Tues, Wed, Thur, Fri, Sat, Sun };
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
enum Color//颜色 { RED=1, GREEN=2, BLUE=4 }; enum Color clr = GREEN;
总结
这些就是我学到的关于自定义数据类类型的知识,欢迎大家来点评,记得点赞哦!