c语言结构体进阶学习——程序小白的c语言学习

前几天复习了下c语言结构体的知识,以此文章作为复习资料,愿与诸君共勉。

目录

一:什么是结构体

1. 结构体

1.1 结构体的需求

1.2 结构体的声明

2.结构成员访问操作符

2.1 结构体成员的直接访问

2.2 结构体成员的间接访问

2.3 综合运用 = v = 

3.结构体变量初始化

二: 结构体的进阶运用

1.结构体的特殊声明

2.结构的⾃引⽤

2.1 结构体在链表中使用

2.2 使用typedef时结构体的自引用

3. 结构体内存对⻬

3.1 引入

3.2 对⻬规则

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

5.修改默认对⻬数

6.结构体传参

7.结构体实现位段

7.1 什么是位段

7.2 位段的内存分配

7.3 位段的跨平台问题

7.4 位段的应⽤

7.5 位段使⽤的注意事项


一:什么是结构体

1. 结构体

1.1 结构体的需求

  c语言给了我们许多种内置的类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。

描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;

描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类 型,让程序员可以⾃⼰创造适合的类型。

结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚⾄是其他结构体。

1.2 结构体的声明

原型代码如下

struct tag
 {
 member-list;
 }variable-list;

首先要先声明是结构体 struct

Tag 是你创建的类型的总名字就像 int,float 等

memberlist 写入你想要的各种类型,可以是别的结构体,可以是c语言给的类型,后面还有位段的概念,等到后面我再予以解释

variable-list 顾名思义,是你用你自己创建类型所创建的类型的名字,可以省略,这里方便你直接给名字,关于这一项,后面一些特殊结构体有运用

描述⼀个学⽣:

struct Stu
 {
 char name[20];//名字
 
int age;//年龄
 
char sex[5];//性别
 
char id[20];//学号
 
}; //分号不能丢
 

再给一些嵌套初始化的例子

 struct Node
 {
 int data;
 struct Point p;
 struct Node* next;  
}n1 = {10, {4,5}, NULL};            //结构体嵌套初始化
 
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
 

这里注意最后一行,嵌套结构体的初始化方法


2.结构成员访问操作符

2.1 结构体成员的直接访问

        要想访问结构体,给结构体中的变量进行赋值,有特殊的写法

        结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数

 #include <stdio.h>
 struct Point
 {
     int x;
     int y;
 }p = {1,2};

 int main()
 {
     printf("x: %d y: %d\n", p.x, p.y);
     return 0;
 }

如图,最终代码分别显示了结构体的两个变量,而不是直接全部表示出来

使⽤⽅式结构体变量.成员名 (注意  .  )

2.2 结构体成员的间接访问

有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针

 #include <stdio.h>
 struct Point
 {
     int x;
     int y;
 };
int main()
 {
     struct Point p = {3, 4};
     struct Point *ptr = &p;
     ptr->x = 10;
     ptr->y = 20;
     printf("x = %d y = %d\n", ptr->x, ptr->y);
     return 0;
 }

遇到结构体的地址时,想要直接从其地址访问数据,就需要  “  -> "  这一操作符

比如图中的 ptr -> x 就相当于 

ptr -> x = 10  ==  (*ptr).x = 10

使⽤⽅式:结构体指针->成员名

2.3 综合运用 = v = 

#include <stdio.h>
#include <string.h>
struct Stu
 {
     char name[15];//名字
     int age;     //年龄  
};
void print_stu(struct Stu s)
 {
     printf("%s %d\n", s.name, s.age);
 }
void set_stu(struct Stu* ps)
 {
     strcpy(ps->name, "李四");
     ps->age = 28;
 }
int main()
 {
     struct Stu s = { "张三", 20 };
     print_stu(s);
     set_stu(&s);
     print_stu(s);
     return 0;
 }

3.结构体变量初始化

结构体的初始化有两种顺序,

1.按照结构体成员的顺序初始化

2.按照指定的顺序初始化

下面将用代码说明

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;
 }

二: 结构体的进阶运用

1.结构体的特殊声明

前面我们说到结构体中的tag,自己设置的名字,实际上有时候可以不写

在声明结构的时候,可以不完全的声明。

 //匿名结构体类型
 
struct
 {
     int a;
     char b; 
    float c;
 }x;

 struct
 {
     int a;
     char b;
     float c;
 }a[20], *p;

上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。

那它与一般正常的结构体有什么不同呢?

那么问题来了,因为这两个结构体中的变量类型都一样,它们可以说是一个结构体

那么如果我将上面的结构体 “ x ” 的地址放到 下面的结构体中呢?

//在上⾯代码的基础上,下⾯的代码合法吗?
 
p = &x;

警告:

编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。

匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。

2.结构的⾃引⽤

讲到自引用,在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?

下面我将用链表来作例子,

在此之前,由于我还没有学数据结构,因而理解十分浅显

先大概说一下数据结构的分类

2.1 结构体在链表中使用

假如我们想要存储1,2,3,4,5这几个数据,同时希望能够通过这几个数据中的一个数据找到另外几个数据时,用的就是链表

那么这就意味着,我们要能在一个结构体中找到另外一个结构体

那么直接在创建结构体的时候放入另外一个结构体的内容以此来作为循迹的方式可以吗?

比如下面的代码

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

仔细分析,其实是不⾏的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤ ⼩就会⽆穷的⼤,是不合理的

因而就需要一个大小有限的方式,c语言中指针的运用很为重要,那试试看指针

正确的⾃引⽤⽅式:

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

2.2 使用typedef时结构体的自引用

在使用结构体中,夹杂了typedef对匿名结构体类型重命名,也容易引⼊问题,比如下面的代码

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

错在typedef 是在创建完结构体类型后才能对这个结构体重命名,

但是代码中的  Node*  意味着已经你正在创建的结构体已经创建完成,这是矛盾的,因而错误。

解决⽅案如下:定义结构体不要使⽤匿名结构体了

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

3. 结构体内存对⻬

3.1 引入

我们已经掌握了结构体的基本使⽤了。

现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。

这也是⼀个特别热⻔的考点: 结构体内存对⻬

对于结构体内存的对齐,我们要使用一个宏  

offsetof

它可以计算出结构体的成员相较于结构体起始位置的偏移量

需要头文件#include <stddef.h>

大概使用方法

 offsetof(struct S2, i)

下面让我们看一下这两段代码在内存中所占大小

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


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

按照我们前面的知识来说,这两个结构体类型的大小应当都是相同的 6 个字节

然而代码出来却是大相径庭的结果

这是为什么呢?

使用offsetof宏

我们可以得知它们相对于起始位置的偏移量竟然是不同的,这是怎么回事呢?

实际上,结构体中的元素都有其对应的对齐规则

3.2 对⻬规则

1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

对齐数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值

V S 中默认的值为 8

Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍

4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

下面是一些练习以及我的一些笔记的解答

首先第一条规则  : 第一个成员总是在0偏移处位置,很好理解

第二条规则:意味着你的类型会影响你所偏移的位置,比如int 类型,它有4个字节,需要在偏移量为4的倍的位置。但如果你的大小比编译器的默认对齐数大的话,用的是编译器的默认对齐数,(当然默认对齐数后期可以改)这就是影响结构体大小的原因之一。

第三条规则: 意味着你结构体的最终大小与成员中的最大对齐数有关,是它的倍数。

第四条规则:与第三条类似,把嵌套结构体中的结构体看作一种类型去看它大小去算对齐数就行

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

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

struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

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

⼤ 部分的参考资料都是这样说的:

1. 平台原因(移植原因)

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

2. 性能原因

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

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

那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:

让 占⽤空间⼩的成员尽量集中在⼀起

比如下面这样,空间的运用上有了变化

struct S1
{
 char c1;
 int i;
 char c2;
};
 struct S2
{
 char c1;
 char c2;
 int i;
};

5.修改默认对⻬数

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

int main()
{
	struct s1;
	struct s2;
	printf("%d\n", sizeof(struct S4));

	return 0;
}


#pragma pack(1)//设置默认对⻬数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认

int main()
{
	//输出的结果是什么?


	printf("%zd\n", sizeof(struct S));
	return 0;

结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。

当然设置默认对齐数时一般都是2的次方

6.结构体传参

写函数的过程中,有时需要用到结构体传参,传参有两种

1.传值传参

2.传址传参

那么那种对于结构体传参来说更好呢?先说答案,传址传参

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); //(*ps).data[i]
}

原因: 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

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


7.结构体实现位段

7.1 什么是位段

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

1. 位段的成员必须是 int 、 unsigned int signed int ,在C99中位段成员的类型也可以 选择其他类型。

2. 位段的成员名后边有⼀个冒号和⼀个数字。

即多了个冒号和数字,下面将从我的笔记出发,说说它们的作用

正如上面所写,位段能够限制类型使用的bit位数,使用得当,可以减少性能开销。

7.2 位段的内存分配

1. 位段的成员可以是 int   unsigned  signed int 或或者是 char 等类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。

位段的开辟是一段一段开辟的,用到哪开到哪,但是有些问题

7.3 位段的跨平台问题

1. int 位段被当成有符号数还是⽆符号数是不确定的。

2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义

4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。

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

7.4 位段的应⽤

下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络 的畅通是有帮助的。

7.5 位段使⽤的注意事项

位段中有可能出现一个字节中有多个变量,这个时候无法直接赋值,只能用另外一种手段

至此,完,感谢观看

<think>好的,用户想了解小白如何学习C语言。首先,我需要考虑用户可能的背景。小白可能没有编程经验,所以要从最基础的概念讲起。接下来,我应该分步骤来规划学习路径,确保逻辑清晰,容易跟随。 第一步,基础知识很重要。得先介绍C语言的概貌,比如历史、特点,为什么学习它。然后安装开发环境,比如推荐VS Code或者Dev-C++,这对新手来说比较友好。还要解释编译过程,预处理、编译、汇编、链接,这些概念虽然有点难,但理解后对后续有帮助。 然后是语法基础。变量、数据类型、运算符这些是必须的。控制结构像条件判断和循环,得举例说明,比如用if-else和for循环的例子。函数和数组部分也要详细,尤其是函数参数传递和数组操作,常见错误可能需要提醒。 接下来是进阶内容,指针和内存管理。这里容易出错,得用简单明了的例子,比如指针的基本使用和动态内存分配。结构体和文件操作也是重点,特别是文件读写,可以给出实际例子帮助理解。 实践项目部分,要推荐一些适合新手的项目,比如计算器或学生管理系统,这样能巩固知识。调试技巧也很重要,教他们使用调试工具,比如GDB,或者IDE内置的调试器。 学习资源方面,推荐经典的书籍如《C Primer Plus》和在线平台,比如菜鸟教程和慕课网,这些资源比较适合初学者。社区如Stack Overflow和优快云能帮助解决问题。 常见问题需要提前预警,比如指针错误和内存泄漏,避免他们走弯路。学习建议部分要强调多敲代码,理解原理,分阶段学习,避免急躁。 最后,总结学习路径,鼓励坚持。要确保语言通俗易懂,避免专业术语过多,用比喻或实例帮助理解。检查是否有遗漏的步骤,比如开发环境配置的具体步骤是否需要详细说明?可能需要简单提及,但可以让他们参考其他教程。另外,调试部分可能需要更详细的步骤,但考虑到是总体介绍,可能点到为止即可。整体结构是否合理,是否由浅入深,逻辑连贯?是的,先基础再进阶,最后实践和资源,符合学习流程。最后检查LaTeX格式是否正确,比如代码示例用反引号,数学公式用$符号,但用户的问题中没有涉及数学公式,所以可能不需要。总之,确保回答全面、有条理,适合小白的理解水平。</think>学习C语言需要系统化的路径规划和持续实践。以下是为零基础学习者整理的详细学习路线,结合原理理解和实操训练,帮你高效掌握C语言编程: --- ### 一、基础知识准备 1. **了解C语言特点** - 诞生于1972年,是面向过程的编程语言 - 具有高效性(直接操作内存)、可移植性(跨平台编译)、底层控制能力 - 广泛应用于操作系统开发(如Linux内核)、嵌入式系统、驱动程序等领域 2. **开发环境搭建** ```bash # Windows推荐方案 1. 安装Visual Studio Community(带MSVC编译器) 2. 或安装MinGW + VS Code(轻量级组合) # MacOS推荐方案 1. 安装Xcode Command Line Tools 2. 使用Clang编译器 + VS Code ``` 3. **第一个C程序** ```c #include <stdio.h> int main() { printf("Hello, World!\n"); return 0; } ``` - 编译命令:`gcc hello.c -o hello` - 运行命令:`./hello` --- ### 二、核心语法学习路线 #### 阶段1:基础语法(建议2周) 1. **变量与数据类型** - 基本类型:`int`, `float`, `double`, `char` - 类型转换:显式转换 `(int)3.14`,隐式转换规则 - 常量和宏定义:`#define PI 3.14159` 2. **运算符与表达式** - 算术运算:注意 `/` 的整数除法特性 - 关系运算:`==` 与 `=` 的区别 - 逻辑运算:短路求值特性 3. **流程控制** ```c // if-else嵌套示例 if(score >= 90) { printf("A"); } else if(score >= 60) { printf("B"); } else { printf("C"); } // 经典for循环 for(int i=0; i<10; i++) { printf("%d ", i*i); } ``` #### 阶段2:函数与复合类型(建议3周) 1. **函数编写** - 函数原型声明 - 参数传递方式:值传递 vs. 地址传递 - 递归函数实现(斐波那契数列案例) 2. **数组与字符串** ```c // 数组翻转算法 void reverseArray(int arr[], int size) { for(int i=0; i<size/2; i++) { int temp = arr[i]; arr[i] = arr[size-1-i]; arr[size-1-i] = temp; } } ``` 3. **结构体与联合体** ```c typedef struct { char name[20]; int age; float score; } Student; Student stu = {"张三", 18, 95.5}; ``` #### 阶段3:进阶核心(建议4周) 1. **指针精讲** - 指针运算:`*(ptr+2)` 与 `ptr[2]` 的等价性 - 多级指针应用场景 - 函数指针:`int (*funcPtr)(int, int) = add;` 2. **内存管理** ```c // 动态数组实现 int *arr = (int*)malloc(10*sizeof(int)); if(arr == NULL) { // 错误处理 } // 使用后必须释放 free(arr); ``` 3. **文件操作** ```c FILE *fp = fopen("data.txt", "r"); if(fp) { char buffer[100]; while(fgets(buffer, 100, fp)) { printf("%s", buffer); } fclose(fp); } ``` --- ### 三、项目实践路线 1. **基础项目(选择3-4个)** - 科学计算器(实现加减乘除、指数运算) - 通讯录管理系统(文件存储版) - 简单文本加密工具(凯撒密码算法) 2. **进阶项目(选择1-2个)** - 迷你数据库系统(基于文件存储) - 命令行版贪吃蛇游戏(使用ncurses库) - 简易HTTP服务器(Socket编程基础) --- ### 四、调试与优化技巧 1. **GDB调试器基础** ```bash gcc -g program.c -o program gdb program (gdb) break main (gdb) run (gdb) print variable ``` 2. **常见错误处理** - 段错误(Segmentation Fault):检查指针越界访问 - 内存泄漏:使用Valgrind检测 ```bash valgrind --leak-check=full ./program ``` --- ### 五、学习资源推荐 1. **经典教材** - 《C Primer Plus》(第6版):适合系统学习 - 《C和指针》:深入理解指针精髓 - 《C陷阱与缺陷》:避开常见误区 2. **在线平台** - 菜鸟教程(C语言版块):适合快速查阅 - 浙江大学慕课《程序设计入门——C语言》 - LeetCode初级算法题(选择C语言解题) 3. **开发工具** - IDE:CLion(专业级)、Code::Blocks(轻量级) - 代码格式化工具:Clang-Format - 静态检查工具:Cppcheck --- ### 六、学习建议 1. **代码规范** - 坚持使用有意义的变量命名(如`studentCount`而非`a`) - 添加必要注释(特别是算法实现部分) - 保持合理的缩进(推荐4空格缩进) 2. **学习技巧** - 每天坚持代码量:建议初期每天50-100行有效代码 - 制作知识卡片:记录重点语法点 - 参与开源项目:从阅读简单C项目开始(如TinyHTTPd) 3. **避坑指南** - 避免在`scanf`中忘记`&`符号 - 字符串操作注意预留`\0`结束符 - 数组越界访问是常见错误源 --- 坚持每天2小时的有效学习,配合实践项目,通常3-4个月可以掌握C语言核心开发能力。遇到问题多查文档(参考C99/C11标准),善用调试工具,保持持续实践,就能逐步建立扎实的编程基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值