一、写在前面
上一篇我们把环境搭好了,这一篇开始,花两三篇的时间把C语言里和数据结构最相关的知识点过一遍。
如果你已经对指针、结构体很熟了,这篇可以快速浏览,当个复习。但如果这些概念还模糊,建议耐心看完,因为后面的链表、树全都要靠它们。
二、指针:C语言的灵魂
2.1 什么是指针
指针就是一个变量,它存的是地址,不是具体的数值。
c
int a = 10;
int *p = &a; // p存的是a的地址
画个图理解一下:
text
变量a: [10] 地址假设是 0x1000
指针p: [0x1000] 地址假设是 0x2000
p里面存的是0x1000,通过*p就能找到a,拿到10。
2.2 指针的运算
指针的加减运算,不是简单的地址加1,而是加上它所指向类型的大小。
c
int arr[] = {10, 20, 30, 40};
int *p = arr; // p指向arr[0]
printf("%p\n", p); // 假设输出 0x1000
printf("%p\n", p+1); // 输出 0x1004,因为int占4字节
关键点:
-
p+1跳过了4个字节,指向了arr[1] -
p++也是同样的效果,指针向后移动一个元素
这个特性特别重要,后面遍历数组、操作链表节点全靠它。
2.3 指针和数组的关系
数组名在很多情况下可以当作指针用,但它们不完全一样。
c
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", arr[2]); // 输出3
printf("%d\n", p[2]); // 也是3,p可以像数组一样用
printf("%d\n", *(arr+2)); // 也是3,arr可以像指针一样用
关键区别:
-
arr是常量,不能改变指向,比如arr++会报错 -
p是变量,可以随意改,比如p++是合法的
访问数组元素的本质:arr[i] 其实等价于 *(arr + i)。
三、结构体:自定义数据类型
3.1 基本用法
结构体可以把多个不同类型的数据打包在一起。
c
struct Student {
char name[20];
int age;
float score;
};
// 使用
struct Student s1 = {"张三", 20, 85.5};
printf("%s的分数是%.1f\n", s1.name, s1.score);
3.2 结构体指针
操作结构体时,经常用指针,因为传指针比传整个结构体效率高。
c
struct Student s1 = {"李四", 21, 90.0};
struct Student *p = &s1;
// 两种访问方式
printf("%s\n", (*p).name); // 写法麻烦,不常用
printf("%s\n", p->name); // 箭头操作符,推荐
记住 :p->name 等价于 (*p).name。
3.3 内存对齐(重点)
这是一个容易踩坑的点。结构体的大小不是成员大小的简单相加。
c
struct Example1 {
char c; // 1字节
int i; // 4字节
};
printf("%d\n", sizeof(struct Example1)); // 输出8,不是5
为什么是8?因为编译器会做内存对齐:
-
char c占1字节,但为了int i对齐到4字节边界,c后面会空出3个字节(填充) -
最终占1+3+4 = 8字节
c
struct Example2 {
int i; // 4字节
char c; // 1字节
};
printf("%d\n", sizeof(struct Example2)); // 输出8,还是8?
这个也是8,但原因不同:int占4,char占1,整体大小需要是最大成员(4)的倍数,所以补3个字节,还是8。
c
struct Example3 {
char c1; // 1
char c2; // 1
int i; // 4
};
printf("%d\n", sizeof(struct Example3)); // 输出8(1+1+2填充+4)
对齐规则:
-
每个成员相对于结构体起始地址的偏移量,必须是该成员大小的整数倍
-
结构体的总大小,必须是最大成员大小的整数倍
写数据结构时要注意:如果结构体内有指针,指针在64位系统占8字节,对齐会影响整个结构体的大小。
四、typedef:给类型起别名
typedef能让代码更简洁,尤其是后面写链表,不用每次都写struct Node。
c
// 不用typedef
struct Node {
int data;
struct Node *next;
};
struct Node n1; // 每次都要写struct
// 用typedef
typedef struct Node {
int data;
struct Node *next;
} Node;
Node n1; // 简洁多了
还有一种写法更常见:
c
typedef struct Node {
int data;
struct Node *next;
} Node, *PNode; // PNode是指向Node的指针类型
Node n1;
PNode p = &n1; // p是Node*类型
五、一个综合例子
把前面学的串起来,定义一个学生结构体,用指针操作:
c
#include <stdio.h>
#include <string.h>
typedef struct Student {
char name[20];
int age;
float score;
} Student;
void printStudent(Student *p) {
printf("姓名:%s,年龄:%d,分数:%.1f\n",
p->name, p->age, p->score);
}
int main() {
Student s1;
strcpy(s1.name, "王小明");
s1.age = 19;
s1.score = 88.5;
Student *p = &s1;
printStudent(p);
// 演示指针运算(数组)
Student class[3] = {
{"张三", 20, 85},
{"李四", 21, 90},
{"王五", 19, 87}
};
Student *q = class;
printf("第一个学生:%s\n", q->name);
printf("第二个学生:%s\n", (q+1)->name); // 指针移动
return 0;
}
输出:
text
姓名:王小明,年龄:19,分数:88.5
第一个学生:张三
第二个学生:李四
六、小结
这一篇讲了三个核心概念:
| 概念 | 要点 |
|---|---|
| 指针 | 存地址,加减运算跳过的字节数取决于指向的类型 |
| 数组 | 数组名可当指针用,但是常量不能改指向 |
| 结构体 | 自定义类型,大小要考虑内存对齐 |
| typedef | 简化类型名,让代码更简洁 |
这些是写数据结构的基石,下一篇我们会讲动态内存分配(malloc和free),学会之后就可以开始手写链表了。
七、思考题
- 下面的结构体占多少字节?(假设是32位系统,指针4字节)
c
typedef struct {
char a;
int *p;
char b;
} Test;
- 为什么结构体要有内存对齐?不直接按顺序存?