#C语言——学习攻略:自定义类型路线--结构体--结构体类型,结构体变量的创建和初始化,结构体内存对齐,结构体传参,结构体实现位段

发布于:2025-08-14 ⋅ 阅读:(20) ⋅ 点赞:(0)

🌟菜鸟主页:@晨非辰的主页

👀学习专栏:《C语言学习》

💪学习阶段:C语言方向初学者

⏳名言欣赏:“人理解迭代,神理解递归。”


目录

1.  结构体类型

1.1  旧知识回顾

1.1.1  结构体声明

1.1.2  结构体的创建和初始化

1.2  结构体的特殊声明

1.3  结构体的自引用

2.  结构体内存对齐(热门考点)

2.1  对齐规则

习题1

习题2

习题3

习题4--嵌套结构体大小

2.2  为什么存在内存对齐

2.3  修改默认对齐数

3.  结构体传参

4.  结构体实现位段

4.1  什么是位段

4.2  位段的内存分配

4.3  位段的跨平台问题

4.4  位段的应用

4.5  位段使用注意事项


1.  结构体类型

1.1  旧知识回顾

-- 在前面操作符部分有过初步的了解: 结构体是一些值的集合,这些值称为成员变量。结构体的每个成员都可以是不同类型的变量;比如:数组、指针、其他结构题、体等等。

        --博客跳转链接:结构成员访问操作符

1.1.1  结构体声明

struct tag        struct表明是结构体
{
	member-list;    一个或多个成员变量
}variable - list    变量列表, 可有可无,在声明变量类型是可同时定义的变量,且为全局变量
  

--比如,当我们想描述一个学生时,就要包括;姓名、成绩、年龄、学号等等,这时候单一的内置类型就显得力不从心; 哎~~,结构体就上场了:

struct Stu
{
	char name[20];  名字拼音
	int age;        年龄
	char id[20];    学号
	float score;    成绩
	//……
};    分号绝对不能丢

1.1.2  结构体的创建和初始化

struct Stu
{
	char name[20];
	int age;
	char sex[5];
	char id[20];
};

int main()
{
	//初始化--按成员顺序
	struct Stu s1 = { "liming", 18, "man", "2023319829" };
	//进行访问-
	printf("name:%s\n", s1.name);
	printf("age:%d\n", s1.age);
	printf("sex:%s\n", s1.sex);
	printf("id:%s\n", s1.id);

	printf("\n");
	//初始化--按指定顺序
	struct Stu s2 = { .age = 20, .sex = "man", .name = "zhangsan", .id = "2023393839" };
	
	printf("name:%s\n", s1.name);
	printf("age:%d\n", s1.age);
	printf("sex:%s\n", s1.sex);
	printf("id:%s\n", s1.id);
	return 0;
}

1.2  结构体的特殊声明

--在声明时,结构体也存在着不完全声明:

        --匿名结构体:

struct
{
	int a;
	char b;
	float c;
}x;//全局变量

struct
{
	int a;
	char b;
	float c;
}* p;

--可以发现,上面的结构体省略了标签-tag;

--那如果在上面代码的基础,那下面的合理吗?

p = &x;

警告:

--虽然上面两个结构体成员相同,但是编译器会将上面两个声明当作不同的类型(类似两个同名但不同地址的房屋),会导致类型不兼容错误;

--匿名结构体(无标签)只能通过原始定义使用(同时声明结构体、变量),无法在其他地方引用相同的类型匿名结构体无法复用-改进:

        --使用结构体标签、用 typedef 创建类型别名;

1.3  结构体的自引用

--在对结构体进行定义后,那是否可以将结构体本身当作结构体的一个成员呢?

        --比如定义一个链表的结点:

struct Node
{
	int data;
	struct Node next;
};

--这样其实是错误的!!

        --因为当结构体里面在包含一个同类型的结构体,会导致结构体内存无限大,显然是错误的。

--但是可以这样操作(可以包含指向同类型的指针):

struct Node
{
	int data;
	struct Node* next;
};

--在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,容易引入问题,看下面的代码:

typedef struct
{
   int data;
   Node* next;
}Node;

        --这样也是错误的!!因为Node是重命名来的,在结构体内部提前使用重命名的结果是不可行的。所以最好不要使用匿名结构体!!


2.  结构体内存对齐(热门考点)

--了解了基础知识后,下面来谈一谈它的内存如何计算??

2.1  对齐规则

结构体对齐规则:

  • 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处;
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;

        -- 对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。

                -- VS 中默认的值为 8 、 Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小;

  • 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍;
  • 如果嵌套了结构体,嵌套者对齐到自己成员最大对齐数的整数倍,总的结构体大小由第3条进行判断(包括嵌套者的成员);

--这样干巴得理解还是有点模糊,别急,下面几道例题来救一下!

习题1


struct s1
{
	char c1;
	int i;
	char c2;
};

int main()
{
	printf("%zd\n", sizeof(struct s1));	12
	return 0;
}

图解演示——

习题2

struct S2
{
	char c1;
	char c2;
	int i;
};
 
int main()
{
	printf("%zu\n", sizeof(struct S2));//8
}

图解演示——

习题3

struct S3
{
	double d;
	char c;
	int i;
};
 
 
int main()
{
	printf("%zu\n", sizeof(struct S3));//16
}

图解演示——

习题4--嵌套结构体大小

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
 
int main()
{
	printf("%zu\n", sizeof(struct S4));  32
}

图解演示——

2.2  为什么存在内存对齐

  • 平台原因 (移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;

  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中;

--总体来说:结构体的内存对齐是拿空间来换取时间的做法。

--那该如何做到是设计结构体是,满足对齐和节省空间呢,对此,我们可以让占用空间小的成员尽量集中在一起。(减少空间浪费)

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
 
int main()
{
	printf("%zu\n", sizeof(struct S1));  12
	printf("%zu\n", sizeof(struct S2));  8
}

--看上面的代码,成员一样,但是排列顺序不同,明显看到S2更小一点。

2.3  修改默认对齐数

--#pragma 预处理指令,可以改变默认对齐数,但是一般设置为2的次方数。

#pragma pack(2)//设置默认对⻬数为2
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认,下次再到别的结构体中就是默认的了
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S));//8
	return 0;
}

3.  结构体传参

struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };

//结构体直接传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}

//结构体通过地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}

int main()
{
	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

--很显然,通过地址来传参数最好的:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销;
  • 如果传递⼀个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降;

4.  结构体实现位段

4.1  什么是位段

--位段的声明和结构十分类似,但有者两个不同:

  • 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型;
  • 位段的成员名后边有⼀个冒号和一个数字;

        --比如:

struct A
{
     int _a:  2 ;
     int _b:  5 ;
     int _c:  10 ;
     int _d:  30 ;
};

补充——

--一般习惯在位段成员加上'-' ;

--冒号后面的数字表示:这个成员要占用的比特位的数量;

--A就是⼀个位段类型。 那位段A所占内存的大小是多少呢?   

4.2  位段的内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型‘
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的;
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。;
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;
	//空间是如何开辟的?
}

4.3  位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的;
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32),写成27,在16位机器会出问题;
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义;
  4. 当⼀个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的;

总结——

--跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在;

4.4  位段的应用

--图片为网络协议中,IP数据报的格式,可以看到其中大多属性只需要几个bit位就能描述,这里使用位段能够实现想要的结果,也节省了空间;这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。

4.5  位段使用注意事项

--位段的几个成员共有同⼀个字节,导致有些成员的起始位置不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值只能是先输入放在⼀个变量中,然后赋值给位段的成员。

struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

int main()
{
    struct A sa = { 0 };
    scanf("%d", &sa._b); // 这是错误的
    // 正确的示范
    int b = 0;
    scanf("%d", &b);
    sa._b = b;
    return 0;
}

旧知识回顾——

#C语言——学习攻略:数据在内存中的存储--整数在内存中的存储,大小端字节序和字节序判断,浮点数在内存中的存储

#C语言——学习攻略:探索内存函数--memcpy、memmove的使用和模拟实现,memset、memcmp函数的使用

结语:本篇文章到此结束,呈现了自定义--结构体的内容,内涵丰富,大家要多次回顾,如果这篇文章对你的学习有帮助的话,欢迎一起讨论学习,你这么帅、这么美给个三连吧~~~