深入理解C语言结构体

目录

引言

[一. 结构体的基本概念](#一. 结构体的基本概念)

1.结构体的声明

[2. 结构体变量的创建和初始化](#2. 结构体变量的创建和初始化)

[3. 结构体成员访问操作符](#3. 结构体成员访问操作符)

4.结构体的特殊声明

[1. 匿名结构体](#1. 匿名结构体)

[2. 嵌套结构体](#2. 嵌套结构体)

3.结构体自引用

[4. typedef 声明](#4. typedef 声明)

二、结构体内存对⻬

1.对⻬规则

2.为什么存在内存对⻬?

3.修改默认对齐数

三、结构体传参

1.按值传递和按指针传递对比

四、结构体实现位段

1.位段的定义

2.位段的内存分配

3.注意事项

总结


引言

在C语言中,结构体(struct)是一种强大的数据组织工具,它允许你将不同类型的数据组合成一个单一的实体。无论是在处理复杂数据、设计数据模型还是进行内存优化,结构体都能帮助你更好地管理和组织数据。在本文中,我们将深入探讨C语言中的结构体。

一. 结构体的基本概念

什么是结构体?

结构体是一种用户自定义的数据类型,它允许我们将逻辑上相关的数据组合在一起。每个数据项称为结构体的成员。结构体的成员可以是基本数据类型(如int、float、char等),也可以是其他复合数据类型(如数组、指针、甚至其他结构体)。

1.结构体的声明

在C语言中,结构体的声明用于定义新的数据类型,这种数据类型由多个不同的数据成员组成。声明结构体的基本语法如下:

cpp 复制代码
struct 结构体名称 {
    数据类型 成员1;
    数据类型 成员2;
    // 更多成员
};

示例:

cpp 复制代码
#include <stdio.h>

// 声明一个结构体类型Student
struct Student {
   char name[20];//名字
   int age;//年龄
   char sex[5];//性别
   char id[20];//学号
};

在上面的代码中,Student是一个命名结构体,可以用这个类型名称创建多个结构体变量,而point是一个匿名结构体,没有显式的名称,以此无法无法使用这个结构体来创建其他的变量。

2. 结构体变量的创建和初始化

声明结构体类型后,你可以创建结构体变量并对其进行初始化。结构体变量可以是结构体类型的实例,你可以在声明时进行初始化,也可以在运行时赋值。

cpp 复制代码
#include <stdio.h>
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	//按照结构体成员的顺序初始化
	struct Stu s = { "张三", 20, "男", "20230818001" };
	printf("name: %s\n", s.name);
	printf("age : %d\n", s.age);
	printf("sex : %s\n", s.sex);
	printf("id : %s\n", s.id);

	//按照指定的顺序初始化
	struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };
	printf("name: %s\n", s2.name);
	printf("age : %d\n", s2.age);
	printf("sex : %s\n", s2.sex);
	printf("id : %s\n", s2.id);
	return 0; 
} 

运行结果:

3. 结构体成员访问操作符

C语言提供了两种操作符来访问结构体的成员:
点操作符(. ):用于通过结构体变量访问成员。
箭头操作符(->):用于通过结构体指针访问成员。

示例:

cpp 复制代码
#include <stdio.h>
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	struct Stu s = { "张三", 20, "男", "20230818001" };
	struct Stu* ptr = &s;
	printf("name: %s\n", ptr->name);
	printf("age : %d\n", ptr->age);
	printf("sex : %s\n", ptr->sex);
	printf("id : %s\n", ptr->id);
	return 0; 
} 

运行结果:

4.结构体的特殊声明

1. 匿名结构体

当你定义一个匿名结构体时,你只能在定义它的同时创建一个变量。这个结构体没有名字,因此无法在其他地方使用这个结构体来创建新的变量。

cpp 复制代码
struct {
    int x;
    int y;
} point;

这里point是一个结构体变量,而结构体本身没有名字。

2. 嵌套结构体

嵌套结构体就是在结构体内部定义另一个结构体。结构体可以嵌套其他结构体,包括匿名结构体。

cpp 复制代码
struct Date {
    int day;
    int month;
    int year;
};

struct Person {
    char name[50];
    struct Date birthday; // 嵌套结构体
    float height;
};

在这个例子中,Person 结构体包含了 Date 结构体作为其一个成员。

3.结构体自引用

结构体自引用是指结构体中的一个或多个成员是指向相同结构体类型的指针。

cpp 复制代码
struct Node {
    int value;
    struct Node* next; // 自引用:指向相同结构体类型的指针
};

在这个例子中,Node 结构体包含一个名为 next 的指针,它指向另一个 Node 结构体实例。

4. typedef 声明

使用 typedef 关键字可以为结构体定义一个新的类型名,使结构体声明更加简洁。

cpp 复制代码
typedef struct {
    char* name;
    int age;
} Person;
Person p1,p2;//创建两个结构体变量

在这个例子中,Person成为了struct { char* name; int age; }这个结构体类型的别名,可以用Person来创建多个结构体变量,如Person p1,p2;。

二、结构体内存对⻬

什么是内存对齐?

内存对齐是指将数据存储在内存中的特定地址上,使得数据的起始地址满足某种对齐要求。对齐的要求通常与数据类型的大小有关。例如,4字节的整数通常要求存储在4的倍数的地址上。

1.对⻬规则

⾸先得掌握结构体的对⻬规则:

  1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
    对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值
  • VS 中默认的值为 8
  • Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
  1. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
    整数倍。
  2. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
    体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

示例:

cpp 复制代码
#include <stdio.h>

struct S1
{
    char c1;//占1字节
    int i;//占4字节
    char c2;//占1字节
};

int main()
{
    printf("%d\n", sizeof(struct S1));//结果是12
    return 0;
}

内存分布:

2.为什么存在内存对⻬?

  1. 平台原因 (移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
    总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
    那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到 :
    让占⽤空间⼩的成员尽量集中在⼀起
cpp 复制代码
#include <stdio.h>
struct S1
{
	char c1;//占1字节
	int i;//占4字节
	char c2;//占1字节
};
struct S2//s2中占用空间小的成员集中在了一起
{
	char c1;//占1字节
	char c2;//占1字节
	int i;//占4字节
};
int main()
{
	printf("Size of S1:%d\n", sizeof(struct S1));
	printf("Size of S2:%d\n", sizeof(struct S2));
}

S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别:

3.修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

cpp 复制代码
#include <stdio.h>

#pragma pack(1) // 设置对齐数为1字节
struct MyStruct {
	char a; // 占1字节
	int b; // 占4字节
	double c;// 占8字节
};
#pragma pack()// 恢复默认对齐方式

int main() {
	printf("Size of MyStruct: %zu\n", sizeof(struct MyStruct));
	return 0;
}

运行结果:

#pragma pack(1)的效果仅限于它和随后的#pragma pack()之间的代码。一旦执行到#pragma pack(),对齐数将恢复到编译器的默认设置,但这不会改变MyStruct的定义,因为MyStruct是在#pragma pack(1)的作用下定义的。

所以,MyStruct的大小计算如下:

char a; 占用1字节
int b; 由于对齐数为1,所以紧接着char a后面,占用4字节
double c; 由于对齐数为1,所以紧接着int b后面,占用8字节
因此,MyStruct的总大小是1 + 4 + 8 = 13字节。这里没有额外的填充字节,因为对齐数被设置为1,这意味着结构体中的成员是紧挨着存放的,没有额外的填充字节。

三、结构体传参

1.按值传递和按指针传递对比

cpp 复制代码
#include<stdio.h>
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;
}

上⾯的 print1 和 print2 函数哪个好些?

答案是:⾸选print2函数。

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

结论: 结构体传参的时候,要传结构体的地址。

四、结构体实现位段

1.位段的定义

位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以
    选择其他类型。
  2. 位段的成员名后边有⼀个冒号和⼀个数字。
    位段在结构体中的定义方式如下:
cpp 复制代码
struct bit_field_struct {
    type member_name : width;
};

type 是位段的数据类型,通常是 unsigned int 或 int。

member_name 是位段的名称。

width 是位段的宽度,表示该位段所占的位数。

2.位段的内存分配

cpp 复制代码
#include<stdio.h>
struct Flags {
    unsigned int flag1 : 1; // 占用1位
    unsigned int flag2 : 1; // 占用1位
    unsigned int flag3 : 1; // 占用1位
    unsigned int flag4 : 1; // 占用1位
    unsigned int : 0;       // 用于对齐
    unsigned int value : 4; // 占用4位
};

int main() {
    struct Flags f;
    f.flag1 = 1; // 设置flag1
    f.flag2 = 0; // 清除flag2
    f.flag3 = 1; // 设置flag3
    f.value = 10; // 设置value为10(二进制1010)
    printf("Flag1: %d\n", f.flag1);
    printf("Flag2: %d\n", f.flag2);
    printf("Flag3: %d\n", f.flag3);
    printf("Value: %d\n", f.value);
    printf("Size of Flags:%d", sizeof(struct Flags));
    return 0;
}

运行结果:

在这个例子中,我们定义了一个名为Flags的结构体,它包含四个标志位和一个4位的值。每个标志位占用1位,而value占用4位。结构体总大小为8位。

3.注意事项

位段类型: 位段的类型必须是int、unsigned int或signed int。
位段宽度: 位段的宽度必须是一个非负整数常量表达式。
位段对齐: 位段成员可能会跨越其类型的自然边界,这取决于具体的编译器实现。
未命名的位段 :可以使用未命名的位段(如上面例子中的unsigned int : 0;)来强制下一个位段从下一个存储单元开始,这有助于对齐。
访问位段: 可以使用结构体变量名和点操作符来访问位段成员,就像访问普通结构体成员一样。
位段的大小: 结构体中位段的总大小可能比所有位段宽度之和要大,因为编译器可能为了对齐而添加填充位。
位段是一种节省内存的有效方式,特别是在嵌入式系统或需要大量布尔标志的情况下。然而,由于它们的实现细节和可移植性问题,使用位段时应谨慎。

总结

通过对C语言结构体的详细探讨,我们了解了结构体的声明、创建和初始化、成员访问、匿名结构体的使用、结构体自引用、内存对齐、结构体传参以及结构体实现位段。这些知识可以帮助你在C语言编程中更高效地组织和管理数据,编写出更清晰、更高效的代码。掌握这些概念对于任何C语言开发者都是必不可少的。如果你有任何问题或进一步的讨论,请在评论区留言,我们一起探讨!

相关推荐
爱吃西瓜的小菜鸡3 小时前
【C语言】判断回文
c语言·学习·算法
FeboReigns5 小时前
C++简明教程(文章要求学过一点C语言)(1)
c语言·开发语言·c++
FeboReigns5 小时前
C++简明教程(文章要求学过一点C语言)(2)
c语言·开发语言·c++
_小柏_6 小时前
C/C++基础知识复习(43)
c语言·开发语言·c++
yoyobravery6 小时前
c语言大一期末复习
c语言·开发语言·算法
落羽的落羽10 小时前
【落羽的落羽 C语言篇】自定义类型——结构体
c语言
Kisorge11 小时前
【C语言】代码BUG排查方式
c语言·开发语言·bug
yoyo勰11 小时前
sqlite3
c语言·sqlite
就爱学编程12 小时前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(上)
c语言·数据结构
意疏12 小时前
【C 语言指针篇】指针的灵动舞步与内存的神秘疆域:于 C 编程世界中领略指针艺术的奇幻华章
c语言·开发语言·指针