结构体指针

前面说过多次,指针可以指向各种类型的可寻址对象,结构体也在其列,并且结构体指针有着重要意义.

由于结构体往往比较庞大,许多处理结构体的函数如果都直接传递结构体变量作为实参,那么参数值的复制会极大低降低运行效率,更别提如果返回值是一个修改后的结构体了.

我们仍然可以仅传递一个结构体变量的指针,让被调函数直接修改或访问主调函数内的结构体变量.

声明结构体指针

这样声明并初始化一个结构体指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
// 为了各个函数都能使用该结构体,将其定义在全局作用域
struct TEST {
int a;
int arr[10];
};

int main() {
// 这样声明一个结构体指针并初始化:
struct TEST t;
struct TEST *p = &t;
return 0;
}

使用结构体指针访问结构体

接下来,要使用该指针p来访问结构体,我们有2种方法:

  1. 使用(*p).a,(*p).arr这样的形式来访问指向的结构体的成员.

    由于运算符.的优先级高于运算符*,所以需要加括号()改变运算顺序.

  2. 使用指向结构体成员运算符->来直接访问指针指向的结构体的成员.

    p->a,p->arr这样的形式,它们和方法1完全相同,但是显然更加方便直观.

下面的例子写两个函数,用于给结构体赋值和打印结构体的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
// 为了各个函数都能使用该结构体,将其定义在全局作用域
struct TEST {
int a;
int arr[10];
};
void init(struct TEST *p) {
p->a = 0;
for (int i = 0; i < 10; i++) {
p->arr[i] = i;
}
}
void print(struct TEST *p) {
printf("a = %d\n", p->a);
for (int i = 0; i < 10; i++) {
printf("arr[%d] = %d\n", i, p->arr[i]);
}
}
int main() {
// 这样声明一个结构体指针并初始化:
struct TEST t, *p = &t; // 可以写在同一行,但是指针要在结构体变量之后声明
init(p); // 传入结构体指针,初始化结构体
print(p); // 打印结构体的成员
return 0;
}

当然,上面的例子是为了讲解结构体指针变量,这个程序其实在调用函数时直接使用init(&t)print(&t)即可.

初步实现引例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <string.h>

// 书籍结构体
struct Book {
char name[100]; // 书名
char author[100]; // 作者
int year; // 出版年份
int page; // 页数
int stock; // 库存
float price; // 价格
};
// 书店的信息
#define MAX_BOOKS 1000
struct BookStore {
char name[100]; // 书店名
char address[100]; // 地址
int phone; // 电话
struct Book books[MAX_BOOKS]; // 最多1000种书目
int bookCount; // 书籍数量
};

void init_bookstore(struct BookStore *store) {
// 实际上,这里都可以改成scanf()来输入数据,请读者自行尝试
strcpy(store->name, "xxx书店");
strcpy(store->address, "xx市xx区xxx街xxx号");
store->phone = 88888888;
store->bookCount = 0;
}

void add_book(struct BookStore *store, struct Book *book) {
if (store->bookCount < MAX_BOOKS) {
store->books[store->bookCount] = *book;
store->bookCount++;
}
}

int main() {
struct BookStore store;
init_bookstore(&store);
printf("书店名: %s\n", store.name);
printf("地址: %s\n", store.address);
printf("电话: %d\n", store.phone);

struct Book book1 = {"C语言教程", "WAHAHA", 2023, 500, 100, 0};
struct Book book2 = {"1+1的正确性证明","佚名", 2023, 500, 100, 100};
add_book(&store, &book1);
add_book(&store, &book2);

printf("书籍数量: %d\n", store.bookCount);
printf("书籍1: %s, 作者: %s, 出版年份: %d, 页数: %d, 库存: %d, 价格: %.2f\n", store.books[0].name, store.books[0].author, store.books[0].year, store.books[0].page, store.books[0].stock, store.books[0].price);
printf("书籍2: %s, 作者: %s, 出版年份: %d, 页数: %d, 库存: %d, 价格: %.2f\n", store.books[1].name, store.books[1].author, store.books[1].year, store.books[1].page, store.books[1].stock, store.books[1].price);

return 0;
}

运行结果:

image-20240209105727399

代码中使用了->这个运算符,它是用于结构体指针的.

例如有struct BookStore *store,那么store->bookCount等价于(*store).bookCount

指向自身类型的结构体指针成员

本节十分重要!!!关乎到后续数据结构的学习(C语言实现)!!!

前面说过,一个结构体内部不能拥有自身类型的成员,否则会无限递归下去,导致编译错误.

不过,一个结构体内部可以拥有指向自身类型的指针成员!原因很简单:一个指针变量的大小是确定的,无论指向的类型是什么,同一个环境下(例如现在全面普及的x86_64计算机)的大小都是固定的.

PS:复习一下,64位系统下的指针类型占用8字节,32位系统下的指针类型占用4字节,至于16位…呵呵,2字节.

既然指针的大小是固定的,那么这个结构体的大小自然可以确定,因此这个指针是可以正确确定的,自然整个结构体就可以确定.

我们可以试试查看一个结构体类型的大小:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
struct Node {
int data1,data2;
struct Node *next;
};
printf("sizeof(struct Node) = %d\n", sizeof(struct Node));
return 0;
}

运行结果:

image-20240209222539301

这里的大小:16字节=2个int变量(8字节)+1个指针(64位系统,8字节)

需要注意的是,这里故意使用了2个int变量,刚好字节对齐,关于字节对齐需要另外讲解.

看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
int main() {
// 一个结构体内部可以拥有指向自身类型的指针成员
struct Node {
int data;
struct Node *next; // 指向自身类型的指针成员
};
// 声明3个结构体变量
struct Node n1, n2, n3;
// 为data赋值
n1.data = 1;
n2.data = 2;
n3.data = 3;
// 为next指针赋值
n1.next = &n2;
n2.next = &n3;
n3.next = NULL;
// 打印链表
struct Node *p = &n1;
while (p != NULL) {
printf("%d->", p->data);
p = p->next;
}
printf("NULL\n");

return 0;
}

运行结果:

image-20240209222045099

可以看到,结构体中的next成员是指向自身类型的指针,这个例子中我们利用这个成员将各个变量"串联"在一起,实际上这就形成了一个非常简单的链表.

当然,这些结构体变量的next成员也完全可以被赋值为指向自己,不过显然并没有什么实际用途.

后续我们会大量地使用到这种写法!(除非你不学数据结构)

字节对齐

Cppreference中对于对齐的描述如下:

每个完整对象类型拥有一个称作对齐要求的属性,它是一个 size_t 类型的整数值,表示此类型对象可以分配的相继地址之间的字节数。合法的对齐值是二的非负数次幂。

结构体有着不同类型的成员,他们彼此之间的大小不尽相同,为了保持字节对齐,个别成员之间可能并不是紧密相邻,而是相隔着一些填充位.

有如下规则:

  1. 结构体的大小是有效对齐值的整数倍,如有需要会在最后一个成员后面填充若干字节.
  2. 结构体的每个成员相对结构体首地址的偏移量是有效对齐值的整数倍.

由于关于对齐的内容太过复杂,本教程主要面向初学者,并且本人能力有限,因此主要举几个例子即可,相信各位能够理解.


一般情况下,对齐值都取结构体内最大成员所占的字节数,然后各个成员向该值对齐.

结构体中各个成员按照声明顺序排列,按规则进行适当的填充对齐.

几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
int main() {
// 结构体字节对齐的几个例子
struct A {
char a;
char b;
}; // 均占1字节,对齐为1字节---共2字节
struct B {
int a;
char b;
}; // a占4字节,对齐为4字节, b占1字节,对齐为1字节---末尾填充3字节,共8字节
struct C {
int *a;
char b;
}; // a占8字节,对齐为8字节, b占1字节,对齐为1字节---末尾填充7字节,共16字节

printf("sizeof(A) = %d\n", sizeof(struct A));
printf("sizeof(B) = %d\n", sizeof(struct B));
printf("sizeof(C) = %d\n", sizeof(struct C));

return 0;
}

运行结果如下:

image-20240209233118305

实际上各结构体分布如下:

1
2
3
4
5
6
7
8
结构体A:
|char|char| 2字节
结构体B:
|--------int--------| 4字节
|char|----|----|----| 4字节
结构体C:
|---------------int*---------------| 8字节
|--short--|----|----|----|----|----| 8字节

其他大多数情况均类似这样.

有时候为了提高效率/空间利用率,需要对各个成员的顺序进行调整.但是如果这个结构体比较庞大的话,打乱顺序势必会导致可读性下降,有时候宁愿效率低点,也要保证可读性,二者之间要进行一个权衡.


本章内容不多,讲解了结构体与指针的一些内容,一定要彻底理解,后续实现各种数据结构有着重要应用!

——WAHAHA 2024.2.9(跨年23:50)

新年快乐~~~



上一篇:C语言教程-14_1-初识结构体

下一篇:C语言教程-14_3-使用位域进行位操作