这篇文章将总结c语言中一系列关于结构体的知识点。
如果您对这篇文章内容感到满意,可以点进我的主页查看更多相关内容。
我的主页:OMGmyhair的CSDN博客主页
目录
一、创建结构体变量
当我们想创建结构体变量时,这里有三种方式。
(1)第一种
放在结构体括号后面。
struct Stu {
int age;
char name[20];
}s1,s2;
(2)第二种
放在结构体和main函数外面。
struct Teacher {
int age;
char name[20];
char dep[20];
};
struct Teacher t1;
int main()
{
t1.age = 30;
printf("%d", t1.age);
return 0;
};
(3)第三种
main函数里面。
struct Parent {
int age;
char sex[20];
char name[20];
};
int main()
{
struct Parent p1 = {30,"mom","angel"};
printf("%s", p1.name);
return 0;
};
每一个结构体都是一个新的作用域,不同结构体的成员变量名字可以相同,这并不冲突,可以看下面的例子:
二、对结构体变量的赋值
(1)3种初始化方法
我们可以在初始化的时候赋值,也可以在初始化后进行赋值,还可以用scanf进行赋值:
注意:在对s2.name[20]进行赋值的时候要注意,以下都是错误的
s2.name = "李四"; s2.name[20] = "李四";
1.在第一种情况中name数组名就是数组的首地址,不能对地址进行赋值。(此时编译器报错,左边必须是可修改)
2.在第二种情况中,是在给name[0]进行赋值
所以我们在这里用到strncpy来进行对字符串的赋值,这个函数头文件为string.h
此处我们还能将结构体中一个数组赋值给另外一个数组(前提是这两个成员是一个结构体的):
(2)赋值顺序:
对结构体成员变量的赋值不一定要和在结构体中成员变量初始化的顺序一致,具体例子如下:
struct Family {
int age;
char sex[20];
char name[20];
};
int main()
{
struct Family f1 = {30,"mom","angel"};
struct Family f2 = { .sex="grandmother",.age=56,.name="big angel"};
printf("%s\n", f2.name);
return 0;
};
注意:在f3父亲这一个成员赋值的地方,32前面没有.号指示器,编译器会认为它是初始化结构中位于name和sex后的成员。初始化其中没有涉及到的成员都设为0。
我在f2奶奶的名字这块也没有加.号指示器,奶奶的名字是符合初始化顺序的,那么结果如何呢?
struct Family {
int age;
char sex[20];
char name[20];
};
int main()
{
struct Family f1 = {30,"mom","angel"};
struct Family f2 = { .sex="grandmother",.age=56,"big angel"};
struct Family f3 = { .name = "moutain",.sex="father",32};
printf("%d %s %s\n", f2.age, f2.name, f2.sex);
printf("%d %s %s\n", f3.age, f3.name, f3.sex);
return 0;
};
此处运行我们可以看到尽管赋值了奶奶的名字以及父亲的年龄,编译器还是给它们定义为0。
三、结构类型
I、typedef
每次初始化成员变量,我们都需要写一遍struct 类名,这有点麻烦,我们可以用上typedef,示例如下。这里的Fam和struct Family效果相同:
typedef struct Family {
int age;
char sex[20];
char name[20];
}Fam;
int main()
{
struct Family f1;
Fam f2;
f1.age = 21;
f2.age = 20;
return 0;
};
II.匿名结构体
这里的匿名结构体顾名思义就是没有类名,匿名结构体有一个缺点,即不能进行自引用,关于结构体自引用见后面文章:
四、结构体作为参数和返回值
typedef struct People {
int age;
char name[20];
}Per;
void print_people(Per p)
{
printf("%d %s\n", p.age, p.name);
}
Per build_person(int age, char* name)
{
Per p0;
p0.age = age;
strcpy(p0.name, name);
return p0;
}
int main()
{
Per p1 = { 20,"战士" };
print_people(p1);
print_people(build_person(10, "平民"));
print_people((Per) { 34, "护士" });
Per p2 = (Per){ 43,"指挥家" };
print_people(p2);
return 0;
}
在上面这个例子当中,print_people将结构变量作为参数,build_person将结构变量作为返回值。需要注意的是,函数在创建形参的时候,形参是实参的一份临时拷贝,所以形参会复制一份实参。但有的时候,结构体变量作为实参,占有的内存空间很大,拷贝会又需要一份很大的内存空间。所以为了避免此类问题,我们可以用指针的方法。
typedef struct People {
int age;
char name[20];
}Per;
void print_people(Per *p)
{
printf("%d %s\n", p->age, p->name);
}
//Per* build_person(int age, char* name)
//{
// Per* pf0;
// pf0->age = age;
// strcpy(pf0->name, name);
// return pf0;
//}
int main()
{
Per p1 = { 20,"战士" };
Per* pf1 = &p1;
print_people(pf1);
//print_people(build_person(10, "平民"));
return 0;
}
上面的代码需要注意的是,创建结构体变量的函数,不能像打印函数一样,直接改为指针,pf0是临时变量,出了函数体就自动销毁了,我们将pf0所指的地址返回来,在print_people(build_person(10, "平民"));这条语句中通过调用函数对这个地址进行使用,此时就成了非法访问,编译器会报错。
五、结构体自引用
自引用通常见于链表中,有关链表内容可以点进我的主页进行查看。
typedef struct Node {
int data;
struct Node* next;
}Node;
int main()
{
Node n1;
return 0;
}
六、结构体内存对齐
你是否思考过,在结构体中的数据,它们在内存是紧挨在一起的吗?它们是如何存放的呢?一个结构体变量内存大小是多少个字节呢?
首先我们来看一个例子:
typedef struct Math {
int i;
char j;
int k;
}M;
int main()
{
M m1, m2;
printf("%zd", sizeof(m1));
return 0;
}
我们来看结果:
为什么是12呢?接下来我们就来到了结构体的对齐规则:
结构体是从偏移量为0的地址处开始存放,首先我们进行存放结构体的第一个成员,int i。
int为4个字节,从偏移量为0处开始存放:
存放好了int之后,我们再来存放char成员:
接下来我们再进行存放第二个int成员:
此时需要注意的是,这个int并不是紧挨着char进行存放,而是来到偏移量为8处进行存放:
最终,这个结构体变量的大小为12个字节。
为什么?
这里就要引入一个概念,对齐数。
对齐规则和对齐数:
1.在对齐中,结构体的第一个成员对齐到起始偏移量为0的地址处
2.其它成员变量要对齐到它的对齐数的整数倍的地址处
(1)该成员的对齐数是编译器默认值与该成员变量大小的较小值
(2)vs中默认值为8
(3)Linux中gcc无默认值,对齐数是成员自身的大小
3.结构体总大小为每个成员变量的对齐数中最大的那一个的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处。结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
我们按照上面的对齐规则再来好好看看这个例子(在vs中实现):
第一步:将第一个成员对齐到起始偏移量为0的地址处,占4个字节:
第二步:将char放过去,char的大小为1字节,编译器默认8字节,1字节更小,按照1字节的整数倍数进行对齐,4是1的倍数,对齐4进行存放:
第三步:存放int,int的大小为4个字节,小于默认字节数8,则int按照4的整数倍数的地址进行存放,此时偏移量5不是4的整数倍数,所以浪费3个字节,来到偏移地址为8处进行存放:
又因为,结构体总大小为每个成员变量的对齐数中最大的那一个的整数倍,此时3个成员变量的最大对齐数为4,12是4的整数倍,故结构体的总大小为12。
七、修改默认对齐数
#pragma pack(1)
#pragma pack()
#pragma pack(1)设置默认对齐数为1
#pragma pack()取消设置的对齐数,还原为默认
接下来我们实例使用一下:
#pragma pack(2)
struct Test
{
int i;
char j;
double k;
};
#pragma pack()
int main()
{
struct Test t1, t2;
printf("%zd", sizeof(t1));
return 0;
}
此处我们将默认对齐数修改为2
第一步:首先将结构体第一个成员对齐到起始偏移量为0的地址处,第一个成员为int,4个字节
第二步:对齐第二个成员char,跟默认对齐数2相比,char1个字节更小,所以对齐数为1,按照偏移地址为1的整数倍进行存放,4是1的倍数,从4开始存放。
第三步:存放第三个成员double,double为8个字节,跟默认对齐数2相比,默认对齐数更小,所以2为对齐数,即在偏移地址为2的整数倍开始存放,即在6的位置开始存放。
最后结构体的总大小为成员中最大对齐数的整数倍,最大对齐数为2,14是2的整数倍,故结构体的总大小为14。
八、位段
我们已知int为4个字节,32个比特位,但是有的时候,我们不需要用到int的4个字节,我们只需要其中的1个字节,这时候就可以用到位段。
此处的a只占3个比特位,b只占4个比特位,c只占5个比特位,d只占4个比特位。
第一步存放a,首先开辟处一个字节的空间
注意:此时是从左向右使用空间还是从右向左使用空间,不同编译器情况不同(这里我们从右向左)
第二步:存放b
第三步:因为第一个字节不够存放c,所以再开辟一个字节用于存放c
注意:当剩余的空间不够存放下一个成员时,是浪费还是继续使用,不同编译器有不同的做法
第四步:因为第二个字节不够存放d,所以再开辟一个字节用于存放d
此时我们只使用了3个字节,编写程序证明一下:
注意:由于,位段中很多问题都没有达到一致,还有关于位段有无符号?以及早期16位机器中int为2个字节大小。所以位段的使用不跨平台(这一点可移植程序需要注意)
注意事项:由于位段中部分成员的起始地址不是字节,所以不能对位段成员用&
如果这篇文章有帮助到你,请留下您珍贵的点赞、收藏+评论,这对于我将是莫大的鼓励!学海无涯,共勉!