首页 > 其他分享 >深入理解C语言中的结构体

深入理解C语言中的结构体

时间:2024-08-02 09:53:12浏览次数:10  
标签:char struct int C语言 位段 理解 深入 printf 结构

目录

引言

一. 结构体的基本概念

1.结构体的声明

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

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

4.结构体的特殊声明

1. 匿名结构体

2. 嵌套结构体

3.结构体自引用

4. typedef 声明

二、结构体内存对⻬

1.对⻬规则

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

3.修改默认对齐数

三、结构体传参

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

四、结构体实现位段  

1.位段的定义

 2.位段的内存分配

3.注意事项

总结


引言

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

一. 结构体的基本概念

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

1.结构体的声明

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

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

示例:

#include <stdio.h>

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

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

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

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

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

示例:

#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. 匿名结构体

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

struct {
    int x;
    int y;
} point;

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

2. 嵌套结构体

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

struct Date {
    int day;
    int month;
    int year;
};

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

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

3.结构体自引用

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

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

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

4. typedef 声明

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

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 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩ 3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。 4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

示例:

#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字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。 那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到 : 让占⽤空间⼩的成员尽量集中在⼀起
#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 这个预处理指令,可以改变编译器的默认对⻬数。

#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.按值传递和按指针传递对比

#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. 位段的成员名后边有⼀个冒号和⼀个数字。

位段在结构体中的定义方式如下:

struct bit_field_struct {
    type member_name : width;
};

type 是位段的数据类型,通常是 unsigned int 或 int。
member_name 是位段的名称。
width 是位段的宽度,表示该位段所占的位数。

 2.位段的内存分配

#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语言开发者都是必不可少的。如果你有任何问题或进一步的讨论,请在评论区留言,我们一起探讨!

标签:char,struct,int,C语言,位段,理解,深入,printf,结构
From: https://blog.csdn.net/2302_81410974/article/details/140843263

相关文章

  • 深入理解PHP数组反转的算法
    本文由ChatMoney团队出品在PHP开发中,数组反转是一个常见的操作,它涉及到将数组的键值对或者键的顺序进行倒序排列。本文将深入探讨PHP数组反转的算法,并提供相应的代码示例。一、PHP数组反转基础在PHP中,数组反转通常涉及到两个函数:array_reverse()和array_flip()。......
  • 对于PHP数组反转的算法的深度理解
    本文由ChatMoney团队出品在PHP开发中,数组反转是一个常见的操作,它涉及到将数组的键值对或者键的顺序进行倒序排列。本文将深入探讨PHP数组反转的算法,并提供相应的代码示例。一、PHP数组反转基础在PHP中,数组反转通常涉及到两个函数:array_reverse()和array_flip()。......
  • C语言中的条件判断与分支选择:深入解析if, else, else if与switch
    引言在C语言编程中,条件判断与分支选择是构建复杂逻辑、实现不同场景下代码执行路径分流的基石。if、else、elseif以及switch语句作为C语言提供的强大工具,让我们能够根据条件表达式的真假值,灵活地控制程序的执行流程。本文将带您深入探索这些判断语句的语法细节、使用场景及最佳......
  • 《深入浅出WPF》学习笔记三.x命名空间以及常见属性
    《深入浅出WPF》学习笔记三.x命名空间以及常见属性X命名空间的由来和作用xaml:是eXtensibleApplicationMarkupLanguage的英文缩写(可扩展应用程序标记语言);声明       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"使用x:Class="WpfApp10.Main......
  • 深入理解MyCAT分库分表机制:架构师的秘密武器
    一、MyCAT分库和分表的概念1.分库(DatabaseSharding)分库是将一个大数据库拆分成多个小数据库,以减小单个数据库的压力并提高系统的扩展性。每个子数据库可以分布在不同的服务器上,从而分散负载并提高性能。示例:假设我们有一个用户信息数据库users_db,其中包含了大量的用......
  • 类和对象的深入了解6
    1.初始化列表初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。classDate{public:Date(intyear,intmonth,intday):_year(year),_month(month),_day(day){}private:int_year;......
  • 类和对象的深入了解7
    1.static成员1.1概念声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。1.2特性1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。2.静态......
  • C语言运算符深度解析--超详细
    引言在C语言的浩瀚宇宙中,运算符如同点亮星辰的魔法棒,它们不仅连接着数据的海洋,更驱动着程序的逻辑流转。从基础的算术运算到复杂的位操作,每一个运算符都承载着特定的功能,是构建程序逻辑的基石。掌握C语言的运算符,就如同手握开启编程世界大门的钥匙,让你能够自如地编写出高效、精准......
  • C++和C语言if else
    一、if。if(/*表达式*/)//如果表达式成立,执行大括号里的代码。如果表达式不成立,执行大括号下面的代码。{//代码}二、else。 else//else一般和if配对,如果if表达式不成立,执行else。如果if表达式成立,不执行else{//代码} 所以执行如上代码输出的是"1是单数"。......
  • Lab0 C Programming Lab(CMU)(CSAPP深入理解计算机系统)
    该文章是我在别处写的小随笔,现在转过来实验下载地址15-213/14-513/15-513:IntrotoComputerSystems,Spring2022大致要求1.Linux命令行基础2.C语言基础3.数据结构基础(链表基本操作)4.基本英语阅读能力大致操作下载.tar文件,解压后对着README操作即可;简单来说,允许直......