自定义类型
结构体
结构体的定义:
结构是一些值的集合,这些值被称为成员变量。每个成员都可以是不同的类型。
结构体成员变量的类型可以是标量、数组、指针甚至是其他结构体。
结构体变量是其他结构体叫做结构体嵌套
结构体的声明
首先使用结构体就必须先声明一个结构体类型,声明结构体类型就必须使用struct关键字。
struct tag是结构体类型,tag是结构体标签,用来区分不同类型的结构体类型。
member -list是成员列表,成员列表中可以有多个成员变量,每个成员变量的类型可以不同。
variavle-list是变量列表,这里可写可不写。写了代表创建了一个struct tag类型的结构体变量;不写就仅仅代表你创建了一个结构体类型,要创建结构体类型变量就要在main函数内部进行创建,创建方法和创建普通变量相同:结构体类型 变量名。
#include<stdio.h>
struct tag
{
member -list;
}variable-list;
int main()
{
struct tag variable-list2;
return 0;
}
注意:
1.variable-list后一定要记得带 ;
2.此时我们的结构体声明是在main函数外的,所以结构体的成员变量都是全局变量。
3.结构体的成员变量是可以为结构体类型的。
结构体变量的创建和初始化
当我们在声明完结构体类型后,要进行创建结构体变量,有两种创建形式:
1.在变量列表处进行创建,这里创建的结构体变量是全局变量
2.在main内部像创建普通变量创建结构体变量:结构体类型+变量名,这里的结构体变量是局部变量
但我们在声明完结构体类型后,想要初始化结构体成员变量,要使用{}来按成员变量顺序进行初始化。
如果要初始化或者打印成员变量其中的某一个或者不想按照成员变量顺序来初始化和打印,使用.操作符来进行操作。
#include<stdio.h>
typedef struct stu 0
{
int a;
char s;
}stu;
struct s
{
char e;
stu s5;
int arr[10];
};
int main()
{
stu s1 = { 3,'s' };
stu s2 = { .a = 9 };
struct s ss1= { 'w',{2,'b'},{1,2,3,4,5,6} };
printf("%d %c\n", s1, s1);
printf("%d\n", s1.a);
printf("%d\n", s2);
printf("%c %d %c ", ss1.e, ss1.s5.a, ss1.s5.s);
for (int i = 0; i < 10; i++)
{
printf("%d ", ss1.arr[i]);
}
return 0;
}
我们在对结构体类型初始化时,一定要用struct+结构体标签+变量名的形式进行初始化吗?其实用typedef(重定义)更加方便
#include<stdio.h>
typedef struct stu
{
int a;
char s;
}stu;
int main()
{
stu s1 = { 3,'s' };
stu s2 = { .a = 9 };
printf("%d %c\n", s1, s1);
printf("%d\n", s1.a);
printf("%d\n", s2);
return 0;
}
注意:
这里的stu可不代表变量列表,就不是创建结构体变量了哦
stu现在代表的typedef对struct stu结构体的类型重命名,stu是结构体类型。
特殊的声明
在声明结构体类型时不完全声明,就是不写结构体标签
创建的struct结构体类型是匿名结构体类型
此时创建结构体类型变量只能在变量列表处创建。
#include<stdio.h>
struct
{
char name[20];
char sex[5];
int age;
}s1;
int main()
{
p = &s1;
return 0;
}
看下面代码:
当我们创建两个结构体类型的成员变量完全相同时,编译器为什么认为他们时两个不同的结构体类型?
#include<stdio.h>
struct
{
char name[20];
char sex[5];
int age;
}s1;
struct
{
char name[20];
char sex[5];
int age;
}*p;
int main()
{
p = &s1;
return 0;
}
struct关键字 +结构体标签才可以声明(创建)一个结构体类型,而在这里我们只使用了struct关键字就声明(创建)了一个结构体类型。
虽然语法是支持这种写法的,但我们还是要注意这种匿名结构体声明只可以使用一次。
存在即合理,匿名结构体的存在是让我们只想使用一次自己声明的结构体类型。
那typedef是否可以对匿名结构体进行重命名呢?
#include<stdio.h>
typedef struct
{
int a;
char s;
}stu;
int main()
{
stu s1 = { 4,'a' };
printf("%d %c\n", s1.a, s1.s);
return 0;
}
经过尝试,我们发现typedef是可以对匿名结构体进行重命名的。
typedef对结构体类型重命名,那么我们在当前结构体中包含重命名后的结构体是否可以成功呢?
#include<stdio.h>
typedef struct stu
{
int a;
char s;
stu* next;
}stu;
int main()
{
return 0;
}
我们需要明白从struct开始到}结束才算是成功声明了结构体类型
那么我们在没有成功声明结构体类型时进行使用成功声明结构体类型后重命名后的结构体类型,难道还能成功吗?
结构体的自引用
数据在内存中存放的组织结构被称为数据结构,数据结构分为多种:
线性数据结构、树形数据结构、图等。线性顺序结构有分为顺序表、链表。
顺序表:在内存中是连续存放的,数组在内存中就是按照顺序表的形式存放的。
链表:内存中存放的位置未知,但是有一条从前向后的链子串起来,只要找到1,就可以找到2…
那么我们是不是也可以模仿链表在结构体中调用自己,达到链表的效果。
在结构体中调用自己,可以理解为俄罗斯套娃,我们在当前数据中调用下一个数据,相当于实现链表,这种想法是否可行呢?
#include<stdio.h>
struct stu
{
char a;
struct stu next;
};
int main()
{
return 0;
}
现在确实像俄罗斯套娃一样,有当前数据找到了下一个数据,但是这个数据的大小是多少呢
#include<stdio.h>
struct stu
{
char a;
struct stu next;
};
int main()
{
printf("%d", sizeof(struct stu));
return 0;
}
哎呀,vs告诉我们使用了未定义的struct stu,这是因为当前数据指向下一个数据,没有结束条件,相当于一条无线长的链表,这这么可能计算出结构体的大小呢?那么我们可以将数据2的地址赋给数据1的节点,数据3的地址赋给数据2的节点,以此类推,直到最后一个数据中的节点存放NULL(因为链表是单向的,只有直到找到1的地址,并且拿出节点中指向2的地址才能找到2,不能通过找到3来进行逆推找到2)来进行解决呢?红色框框代表节点
#include<stdio.h>
struct stu
{
char a;
struct stu* next;
};
int main()
{
printf("%d", sizeof(struct stu));
return 0;
}
哎呀,此时我们发现确实是计算除了结构体类型的所占空间的大小,可是我们创建的成员变量不是char和指针吗?char大小是1个字节,一个指针的大小是4\8,结果应该是5\9呀,为啥结果是8呢?这到底是我们出错还是计算机出错了呢?
想要直到这个答案,那么就必须要知道结构体内存对齐规则
结构体内存对齐
结构体内存对齐规则:
1.第一个成员在与结构体变量偏转为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数:编译器默认的一个对齐数与该成员变量大小的较小值
- vs中默认的对齐数是8
- linux中没有默认的对齐数,对齐数是该成员变量的大小
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套的结构体的对齐数)的整数倍。
例题1:
#include<stdio.h>
struct stu
{
char a;
struct stu* next;
};
int main()
{
printf("%d", sizeof(struct stu));
return 0;
}
所以上面这个代码中结构体的大小为8个字节就很好解释了:
1.第一个成员变量类型为char,char类型数据大小为1个字节,对齐数是1,存放到偏转量为0的地址处,占1个字节空间的大小,也就是红色框框。
2.第二个成员变量类型为struct stu,存放的是指向下一个节点的地址,地址的大小为4\8个字节,博主当前使用的vs环境是x86,所以地址大小是4个字节,所以对齐数是4和8的较小值4,只能在存放到对齐数4的整数倍的位置,也就是偏转量为4处,占4个字节的空间大小,也就是粉色框框。
3.既然我们的第二个成员变量存放到4的地址处,那么1-3处的地址也就会被对齐,也就是黄色框框。
4.结构体总大小是最大的成员变量的对齐数的整数倍,也就是4的整数倍,当前占1+3+4个字节,是4的整数倍,所以结构体的大小是8个字节。
我们可以利用offsetof函数计算结构体成员变量相较于结构体起始位置的偏移量,使用offsetof函数记得要包含stddef.h头文件哦,那offsetof函数来验证一下我们画的图和计算的偏移量是否正确
#include<stdio.h>
#include<stddef.h>
struct stu
{
char a;
struct stu* next;
};
int main()
{
printf("%d\n", sizeof(struct stu));
printf("%d\n", offsetof(struct stu, a));
printf("%d\n", offsetof(struct stu,next));
return 0;
}
例题2:
我们只是将stu中成员变量的顺序进行了调整,那stu和stu2的大小会不同吗?
#include<stdio.h>
struct stu
{
int a;
char s;
char b;
};
struct stu2
{
char s;
int a;
char b;
};
int main()
{
printf("%d\n", sizeof(struct stu));
printf("%d\n", sizeof(struct stu2));
return 0;
}
stu大小的解释:
1.stu第一个成员变量类型是int,int的大小是4个字节,4和8进行比较,较小值是4,对齐数是4,存放到偏转量未0处,占4个字节的空间,也就是粉色框框。
2.stu第二个成员变量类型是char,char的大小是1个字节,1和8进行比较,较小值是1,对齐数是1,存放到1的整数倍的地址处,也就是4的地址处,占一个字节的大小,也就是一个红色框框。
3.stu第二个成员变量类型是char,char的大小是1个字节,1和8进行比较,较小值是1,对齐数是1,存放到1的整数倍的地址处,也就是5的地址处,char占一个字节的大小,也就是第二个红色框框。
4.结构体总大小是最大的成员变量的对齐数的整数倍,也就是4的整数倍,当前占4+1+1个字节,不是4的整数倍,向后对齐,直到总大小是最大成员变量的偏转量8(对齐的也就是黄色框框)。
stu2大小的解释:
1.stu第一个成员变量类型是char,char的大小是1个字节,1和8进行比较,较小值是1,对齐数是1,存放到偏转量为0处,占1个字节大小,也就是红色框框。
2.stu第二个成员变量类型是int,int的大小是4个字节,4和8进行比较,较小值是4,对齐数是4,1的,存放到4的整数倍的地址处,也就是4,占4个字节大小,也就是第一个红色框框。
3.stu第二个成员变量类型是char,char的大小是1个字节,1和8进行比较,较小值是1,对齐数是1,存放到1的整数倍的地址处,也就是8,char占一个字节的大小,也就是第二个红色框框。
4.结构体总大小是最大的成员变量的对齐数的整数倍,也就是4的整数倍,当前占1+3+4+1个字节,不是4的整数倍,向后对齐,直到总大小是最大成员变量的偏转量的整数倍,也就是12(对齐的也就是黄色框框)。
同样,我们也拿offsetof函数验证我们这个题画的图和计算的值是否正确:
#include<stdio.h>
#include<stddef.h>
struct stu
{
int a;
char s;
char b;
};
struct stu2
{
char s;
int a;
char b;
};
int main()
{
printf("%d\n", sizeof(struct stu));
printf("%d\n", offsetof(struct stu, a));
printf("%d\n", offsetof(struct stu, s));
printf("%d\n", offsetof(struct stu, b));
printf("%d\n", sizeof(struct stu2));
printf("%d\n", offsetof(struct stu2, s));
printf("%d\n", offsetof(struct stu2, a));
printf("%d\n", offsetof(struct stu2, b));
return 0;
}
例题3:
#include<stdio.h>
int main()
{
struct stu
{
int a;
double b;
};
struct stu2
{
int a;
struct stu s1;
double b;
};
printf("%d\n",sizeof(struct stu));
printf("%d\n",sizeof(struct stu2));
return 0;
}
1.stu第一个结构体成员变量类型是int,int的大小是4个字节,4和8比较,较小值是4,对齐数是4,存放到结构体偏转量为0处,占4个字节的空间,也就是粉色框框。
2.stu第二个结构体成员变量类型是double,double的大小是8个字节,8和8进行对比,较小值是8,存放到8的整数倍位置处,也就是偏转量为8处,占8个字节的空间,也就是绿色框框。
3.结构体总大小是最大的成员变量的对齐数的整数倍,也就是8的整数倍,当前占4+4+8个字节,是8的整数倍,总结构体大小就是12(对齐的也就是黄色框框)。
1.stu2第一个成员变量类型是int,int的大小是4个字节,4和8比较,较小值是4,对齐数是4,存放到结构体偏转量为0处,占4个字节的空间,也就是粉色框框。
2.stu2第二个成员变量类型是struct stu,struct stu的大小是16个字节,16和8比较,较小数是8,对齐数是8,但是定义中规定了嵌套的结构体要对齐到自己的成员变量的最大对齐数的整数倍处,嵌套的struct stu成员变量的最大对齐数是8,存放到stu2结构体偏转量为8位置处,占16个字节的空间,也就是紫色框框
3.stu2第三个成员变量类型是double,double的大小是8个字节,8和8比较,较小值是8,对齐数是8,存放到8的整数倍的偏转量位置处,也就是偏转量为24处,占8个字节空间。
3.结构体总大小是最大的成员变量的对齐数的整数倍,也就是8的整数倍,当前占4+4+16+8个字节,是8的整数倍,所以struct stu2的总大小是32。(对齐的空间是黄色框框)
我们还是拿offsetof函数来验证我们画得图和计算的值是否正确:
#include<stdio.h>
#include<stddef.h>
int main()
{
struct stu
{
int a;
double b;
};
struct stu2
{
int a;
struct stu s1;
double b;
};
printf("%d\n",sizeof(struct stu));
printf("%d\n", offsetof(struct stu,a));
printf("%d\n", offsetof(struct stu, b));
printf("%d\n",sizeof(struct stu2));
printf("%d\n", offsetof(struct stu2, a));
printf("%d\n", offsetof(struct stu2, s1));
printf("%d\n", offsetof(struct stu2, b));
return 0;
}
例题4:
那如果结构体成员变量有数组有应该怎么去计算呢?
#include<stdio.h>
int main()
{
struct stu
{
int a;
char b;
int arr[5];
};
printf("%d\n", sizeof(struct stu));
return 0;
}
1.stu第一个结构体成员变量类型是int,int的大小是4个字节,4和8比较,较小值是4,对齐数是4,存放到结构体偏转量为0处,占4个字节的空间,也就是粉色框框。
2.stu第二个结构体成员变量类型是char,char的大小是8个字节,1和8进行对比,较小值是8,对齐数是1,存放到1的整数倍位置处,也就是偏转量为4处,占1个字节的空间,也就是红色框框。
3.stu结构体的第三个成员变量是int[5],int[5]是个数组,数组有5个int类型的元素组成,int类型的大小是4,4和8比较,较小值是4,对齐数是4,存放到偏转量为4的整数倍位置8处,占5个int的大小也就是20个字节的空间,也就是
3.结构体总大小是最大的成员变量的对齐数的整数倍,也就是4的整数倍,当前占4+1+3+20个字节,是4的整数倍,总结构体大小就是28(对齐的也就是黄色框框)。
同样,拿offsetof函数进行验证:
#include<stdio.h>
#include<stddef.h>
int main()
{
struct stu
{
int a;
char b;
int arr[5];
};
printf("%d\n", sizeof(struct stu));
printf("%d\n", offsetof(struct stu, a));
printf("%d\n", offsetof(struct stu, b));
printf("%d\n", offsetof(struct stu, arr));
return 0;
}
我们通过上面的四道例题可以发现,每个题中或多或少都会有对齐的空间(黄色框框),那这些对齐的空间存在意义是什么呢??
结构体内存对齐存在的原因:
1.平台移植性原因
- 不是所有硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
- 数据结构(尤其是栈区)应该尽可能的在自然边界上对齐
- 原因在于为了访问未对齐的内存,处理器要进行两次内存访问,而对于对齐的内存只需要进行一次访问。
性能原因:32位机器有32根地址线,一次可以访问32个bit位(4个字节)的空间;64位机器有64根地址线,一次可以访问64个bit位(8个字节)的空间。如果有我们将内存对齐到我们所处机器一次可以访问的最大空间,我们使用内存对齐可以说是拿空间换时间,提高了效率。
- 结构体类型struct stu的成员变量有两个,char A,int B。当我们要访问整个结构体时,对齐和未对齐访问次数的是相同的,但是我指向访问成员变量B呢?未对齐的要先访问4个字节,拿到B的前三个字节的数据,在访问4个字节,拿到最后一个字节的数据;二对齐的只用访问一次就可以拿到成员变量B的全部字节的数据。
- 每访问一次的时间是相同的,未对齐的要比对齐的时间花的多,这不就是拿空间换取了时间吗?而栈区是我们数据处理最快的地方,是用来存储临时变量、函数参数等的空间
这时候问题又来了,如果成员变量过多时,会浪费大量的空间去进行对齐,我们应该这么做呢?
#include<stdio.h>
struct stu
{
char a;
int b;
char c;
};
struct stu2
{
char a;
char c;
int b;
};
int main()
{
printf("%d\n", sizeof(struct stu));
printf("%d\n", sizeof(struct stu2));
return 0;
}
这是stu的内存空间分布图:
stu2的内存分布空间图:
这时候我们只需要将类型相同的成员变量集中在一块,浪费的空间就会显著减少。
修改默认结构体对齐数
在vs中,默认的结构体对齐数是8,我们可以利用#pragma预处理来进行修改,达到我们想要的默认对齐数
#pragma pack(1)//设置默认对齐数为1
#include<stdio.h>
struct S1
{
char a;
int b;
char c;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
#pragma pack()//取消设置的默认对齐数,还原为默认
如果我们不通过#pragma修改默认对齐数,此时结构体类型大小是12,而通过修改默认对齐数得到结构体类型大小是6,也就说明可以修改结构体内存空间的分布,来接修改结构体类型的大小。
结构体传参
我们都知道c语言中传参有两种方式:传值、传址。
那么结构体是否也可以通过这两种传参方法进行传参呢?
#include<stdio.h>
typedef struct S1
{
char a;
int b;
char c;
}s1;
void print(s1 ss1)
{
printf("%c %d %c\n", ss1.a, ss1.b, ss1.c);
}
void print2(s1* ss1)
{
printf("%c %d %c\n", ss1->a, ss1->b, ss1->c);
}
int main()
{
s1 ss1 = { .a = 'b',.b = 10,.c = 'd' };
print(ss1);
print2(&ss1);
return 0;
}
答案是两种传参都是可行的,但是哪一种效率更高呢?
肯定是传址的形式效率更高,我们都知道传值是相当于创建了该变量的一份临时拷贝,而临时拷贝的创建是需要我们在栈区开辟一块空间去接收拷贝的数据,而这时就需要压栈了,如果结构体类型过大,传参所需要的空间就越大,压栈的开销也就越大,性能也会随之下降;而传址就不需要开辟一块足够放下整个结构体类型的空间去接受,只需要创建一块4/8个字节的空间去接收地址的数据,性能能够下降多少呢?所以我们在进行结构体传参时尽量使用传址调用。
位段
位段的声明和结构体的声明时类似的,但是有两个不同:
位段的成员必须是int 、unsigned int 、signed int等整型家族成员。
位段的成员后多一个冒号(:)和数字。
例如
{
int a : 1;
int b : 10;
int c : 20;
int d : 30;
};
此时,我们可以说a就是一个位段,那问题来了,位段的大小是多少呢?
这需要我们明白位段的内存分配
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
#include<stdio.h>
struct a
{
int a : 1;
int b : 10;
int c : 20;
int d : 30;
};
int main()
{
printf("%d", sizeof(struct a));
return 0;
}
8个字节???struct a中有4个int类型成员变量,不应该是16个字节吗?为什么是这样呢?
原来,在成员变量名:后的数字代表该成员变量所占内存的二进制位,1+10+20+30=61个bit位,超过7个字节不到8个字节,而当前成员变量都是int,每次开辟4个字节空间,开辟一个int空间不够,在开辟一次就够了,所以是8个字节
我们了解了位段的内存分配规则,那位段是如何开辟内存的呢?
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
在我们看过上图的如何开辟空间,我们就可以发现我们想象中的开辟的空间和编译器实际开辟的空间有很大的区别,这是为什么呢?
位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
所以上面的题就很容易解释了:
因为此时第一个变量是char,先开辟一块1个字节的空间当我们成员变量a只占3个bit位,而存在3个bit位空间的是0101,会保留后三位,101,第一个字节中的空间剩下了5个bit位空间,足够放下成员变量b1100,此时第一个字节的空间中只剩下了1个bit位空间,不足以放下成员变量c的二进制序列,舍弃掉这1bit位的空间,在向后开辟一块一字节的空间,从右开始存放c的5bit位空间00011,剩下三bit位空间不足以放下成员变量d的二进制序列,再向后开辟一字节的空间,从右开始存放d的二进制序列0100。此时的二进制序列是01100010 00000011 00000100转换为16进制就是0x62 02 04 。(cc代表随机值)
所以我们使用位段是可以达到节省空间的作用,但我们要注意该位段中的某个成员变量是否可以放下你想的值。
枚举
枚举:可以理解位列举
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
枚举的定义
使用enum关键字进行定义枚举
enum Day//星期
{
Mon=1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的enum Day,enum Sex,enum Color是枚举类型,{}内的内容是枚举类型的可能取值,也叫枚举常量。
这些可能取值都是有值的,默认从0开始,依次递增1,当然也可以进行赋值。
enum Sex//性别
{
MALE=1,
FEMALE=2,
SECRET=3
};
枚举的使用
#include<stdio.h>
enum Day//星期
{
Mon=1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
enum Sex today=Sat;
printf("今天是星期%d",today);
return 0;
}
联合体(共用体)
联合体:是一种特殊的自定义类型。联合体中可以包含一系列成员,这些成员的特征是使用同一块空间,所以联合体也叫共用体。
联合体的定义
联合体使用union关键字进行声明。
union aa//枚举类型的声明
{
char a;
int b;
};
union aa a1;
联合体的特点
联合的成员是共用一块空间的,这个联合体变量的大小最少是最大成员变量(因为联合至少要有能力去存放最大的成员变量)。
希望以上对您有所帮助!当然,如果文章出现错误,欢迎您在评论区或私信我指出哦~