前言:C语言虽然已经提供了很多内置类型:比如:int、char、short、long、float、double等等,但是这些类型并不能满足一些特殊场景的需求,比如,我想描述一个人的特征,需要名字、年龄、性别、身高、体重等;描述一部手机,需要型号、内存、颜色、价格、配置、品牌等;描述一瓶水,需要品牌、价格、容量等。为此C语言专门提供结构体这种自定义的数据,可以让程序员自己创造所需的数据类型。
1.结构体
结构是一些值的集合,这些值称为成员变量。结构中的每个成员可以是不同的数据类型,如:数组、指针、甚至是其他的结构体。
1.1 结构体声明
struct test
{
member_list;
}variable-list;
描述一个人:
struct Stduent
{
char name[20];//姓名
int age;//年龄
char sex[4];//性别
int height;//身高
int weight;//体重
};//注意带上分号
1.2 结构体变量如何定义和初始化
写法有三种:
//代码1:变量的定义
struct Test
{
char a;
char b;
}t1;//声明类型的同时定义变量t1
struct Test t2;//定义结构体变量t2
//代码2:对变量初始化
struct Test t3 = { 'h','o' };
struct Phone //类型声明
{
char brand[20];//品牌
int capacity;//内存容量
};
struct Phone p1 = { "华为",256};//常见初始化
struct Phone p2 = { .capacity = 256,.brand = "小米" };//指定顺序的初始化
//代码3
struct Node
{
int data;
struct Test t;
struct Node* next;//结构体体嵌套初始化
}n = { 10,{'h','y'},NULL};
struct Node nn = { 100,{'z','g'},NULL };//结构体嵌套初始化
2.结构成员访问操作符
2.1 (.)- 结构体成员的直接访问
对结构体成员直接访问使用点(.)操作符。点操作符接受两个操作数。例如:
#include <stdio.h>
struct Water
{
char brand[20];//品牌
int capacity;//容量
int price;//价格
};
int main()
{
struct Water w = { "娃哈哈",596,1.5 };
printf("%s %dml %d元\n", w.brand, w.capacity, w.price);
return 0;
}
【运行结果】
使用方式:结构体变量.成员名
2.2 结构体成员的间接访问
有时候我们会使用指向结构体指针进行操作。这时就要使用这种箭头(->)操作符。如:
#include <stdio.h>
struct Test
{
int a;
int b;
};
int main()
{
struct Test t = { 2,6 };
struct Test* pt = &t;
pt->a = 3;
pt->b = 9;
printf("a = %d, b = %d\n", pt->a, pt->b);
return 0;
}
【运行结果】
使用方式:结构体指针->成员名
综合使用如下:
#include <stdio.h>
#include <string.h>
struct Person
{
char name[20];//姓名
int age;//年龄
};
void PrintPs(struct Person p)
{
printf("%s %d\n", p.name, p.age);
}
void set_ps(struct Person* ps)
{
strcpy(ps->name, "王五");
ps->age = 24;
}
int main()
{
struct Person p = { "赵六",18 };
PrintPs(p);
set_ps(&p);
PrintPs(p);
return 0;
}
【运行结果】
2.3 结构体的特殊声明
在声明结构体的时,可以不完全声明。
如:
//匿名结构体类型
struct
{
int n;
char c;
float f;
};
struct
{
int n;
char c;
float f;
}n[10],*s;
可以看到上面两个结构体在在声明的时候,并没有给它类型(省略了结构体标签(tag)).
留意:如果在此基础上,这样写代码是非法的!!
s = &m;//非法
注意:
编译器会把将上述两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体,如果没有对结构体类型重命名的化,基本上只能使用一次。
2.4 结构体的自引用
结构体中可以包含一个类型为该结构本身的成员吗?
例如,定义一个链表的节点:
struct Node
{
int date;
struct Node n;
};
上面代码是正确吗?若是,那么 sizeof(struct Node)会是多少呢?
可以分析得出,这种写法是不行的,原因是一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大 -- 不合理!
正确的自引用写法,如下:
struct Node
{
int date;
struct Node* next;
};
当使用的typedef匿名结构体类型重命名和结构体自引用遇上时,也容易出现一些问题。比如:
typedef struct
{
int data;
Node* next;
}Node;
注意:这种写法是不可行的,原因是Node是对前面的匿名结构体类型重命名产生的。所以在匿名结构体中提前使用Node类型来创建成员变量,是不可以的。
匿名的结构体类型是不能实现这种结构体自引用的效果的!!!
如何解决这个问题呢? --- 定义结构体时不要使用匿名结构体定义。
typedef struct Node
{
int data;
struct Node* next;
}Node;
3.结构体内存对齐
我们前面已经学习了结构体的基本使用。现在学习一个新的知识点 -- 结构体内存对齐
学习计算结构体的大小。
3.1 对齐规则
掌握结构体的对齐规则:
1.结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处;
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
- VS 中默认的对齐数是 8 ;
-Linux 中 gcc没有默认对齐数,对齐数就是成员自身的大小。
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员的最大对齐数的整数倍地址处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
分析下面结构体的大小:
小练1:
#include <stdio.h>
struct T1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct T1));
return 0;
}
详解:
【运行结果】:
小练2:
#include <stdio.h>
struct T2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct T2));
return 0;
}
详解:
【运行结果】
小练3:
#include <stdio.h>
struct T3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct T3));
return 0;
}
详解:
【运行结果】
小练4:
#include <stdio.h>
struct T3
{
double d;
char c;
int i;
};
//练习4 - 结构体嵌套问题
struct T4
{
char c1;
struct T3 t3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct T4));
return 0;
}
详解:
【运行结果】
3.2 内存对齐存在的原因
主要原因 - 性能原因:
数据结构(特别是栈)应该尽量的在自然边界对齐。原因是为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存只需一次访问,便可读取全部。假设一个处理器总是从内存中访问8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐到8的整数倍上,那么就可以用一个内存操作来读或值,否则,我们可能需要执行两次内存访问,因为一个对象,可能被存放在两个8字节内存块中。
总结:结构体的内存对齐是拿空间来换取时间的做法。
如果在设计结构体的时,既要内存对齐,又想要节省空间就要:让占用空间小的成员尽量放在一起。
如上述出现过的代码:
struct T1 //占用12个字节
{
char c1;
int i;
char c2;
};
struct T2 //占用8个字节
{
char c1;
char c2;
int i;
};
可以看到T1 与 T2成员是一样的,但是所占用的内存大小发生了改变。
3.3 如何修改默认对齐数
#pargma 这个预处理指令,它可以改变编译器的默认对齐数。
#include <stdio.h>
#pragma pack(1) //设置默认对齐数是1
struct T
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对齐,还原为默认对齐数
int main()
{
printf("%zd\n", sizeof(struct T));
return 0;
}
详解:
【运行结果】
结论: 结构体在对齐方式不合适的时候,可以字节修改默认的对齐数。
3.4 结构体传参
#include <stdio.h>
struct T
{
int arr[10];
char c;
float f;
};
//结构体传参
void Print1(struct T pt)
{
int i = 0;
for (i = 0; i < 6; i++)
{
printf("%d ", pt.arr[i]);
}
printf("%c ", pt.c);
printf("%lf\n", pt.f);
}
//结构体地址传参
void Print2(struct T* pt)
{
int i = 0;
for (i = 0; i < 6; i++)
{
printf("%d ", pt->arr[i]);
}
printf("%c ", pt->c);
printf("%lf\n", pt->f);
}
int main()
{
struct T t = { {1,2,3,4,5,6},'h',2.98 };
Print1(t);//传结构体方式
Print2(&t);//传地址方式
return 0;
}
【运行结果】
上述两种传参方式更好? --- 答案是结构体传地址方式。
原因在于:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象时,结构体过大,参数 压栈的系统开销会比较大,这样会导致程序的性能下降。如果指传递地址过去,地址用指针接收,指针的大小在64位平台下只占8个字节(32位平台下,占4个字节),开销不算太大。
结论:结构体传参时,首选结构体的地址。
4.结构体实现位段
结构体是位段存在的基础。
4.1 位段是什么
位段的作用和结构体的对齐方式作用是类似的。并且位段的声明和结构也是类似的。有两个不同点:
1.位段的成员必须是int、unsigned int 或 signed int,在C99中位段成员类型可以选择其他类型。
2.位段的成员名后面跟着一个冒号和一个数字。
比如:
#include <stdio.h>
//1.申请到的一块内存中,从左向右使用,还是从右向左使用,是不确定
//2.剩余的空间,不足下一个成员使用的时候,是被浪费掉,还是继续使用是未知的
struct T
{
int x : 2;//后面的数值是以bit(位)位单位的
int y : 6;
int z : 10;
int w : 30;
};
int main()
{
printf("%zd\n", sizeof(struct T));
return 0;
}
【运行结果】
在这里T是一个位段类型。
位段T的内存大小是如何计算的?
4.2 位段的内存分配
1.位段的成员可以是int 、unsigned int 、signed int或者是char等类型。
2.位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式分配的。
3.位段涉及很多不确定因素,并且位段是不支持跨平台的,注重可移植的程序,应避免使用位段。
分析:
#include <stdio.h>
struct T
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct T t = { 0 };
t.a = 10;
t.b = 12;
t.c = 3;
t.d = 4;
return 0;
}
分析:
4.3 位段跨平台问题
1.int 位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数目是不确定的。(16位机器最大16位,32位机器最大32位,在16机器下写26会出问题)。
3.位段中的成员在内存中从左向右分配,还是从右向左分配,尚未有标准。
4.当一个结构体包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余位时,剩余位是被利用起来,还是丢弃,这还不确定。
总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但有跨平台问题存在。
4.4 位段使用时需要注意的点
位段是几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的,内存中每个字节分配一个地址,一个字节内部的bit是没有地址的。
所以我们不能对位段的成员进行取地址(&),这样就不能使用scanf直接给位段的成员输入值,只可以放在一个变量中,然后赋值给位段的成员。
如:
#include <stdio.h>
struct T
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct T t = { 0 };
scanf("%d", &t._a);//错误写法
//正确写法
int a = 0;
scnaf("%d", &a);
t._a = a;
return 0;
}
看到这里,结构体的相关知识已经学习完毕,恭喜你又掌握了一个知识点。