11.结构体
typedef
typedef
可以让你为现有的类型创建一个别名。例如,如果你想简化指针类型的声明:
typedef unsigned long ulong;
这样,你可以使用 ulong
来代替 unsigned long
。
对于复杂的类型,如函数指针,typedef
也能大大简化声明:
假设我们有一个函数,它接受两个 int
参数并返回一个 int
:
int add(int a, int b) {return a + b;
}
我们可以使用 typedef
来定义一个指向这种类型函数的指针类型:
// 定义一个名为 FuncPtr 的类型,它是指向函数的指针
// 该函数接受两个 int 参数,返回一个 int
typedef int (*FuncPtr)(int, int);// 现在可以像使用普通类型一样声明函数指针变量
FuncPtr ptr = add; // ptr 指向 add 函数// 调用函数
int result = ptr(3, 4); // result = 7
这使得声明比较函数指针更加直观。
结构体 (struct
)
结构体是一种用户自定义的数据类型,它可以包含多个不同类型的变量。每个变量称为结构体的一个成员。
定义和声明
struct Person {char name[50];int age;float height;
};// 声明结构体变量
struct Person person1;
为了简化结构体变量的声明,可以使用 typedef
:
typedef struct {char name[50];int age;float height;
} Person;Person person1;
访问结构体成员
使用点运算符 .
来访问结构体成员:
strcpy(person1.name, "John");
person1.age = 30;
person1.height = 1.75f;
如果是指向结构体的指针,则使用箭头运算符 ->
:
Person *ptr = &person1;
ptr->age = 31; // 修改年龄
结构体成员的内存布局
字节对齐(Alignment):大多数处理器要求数据按特定的字节边界对齐,这意味着结构体中的每个成员都会被放置在一个地址上,这个地址是其自身大小或处理器架构规定的最小对齐值的倍数。例如,一个4字节的整数可能需要4字节对齐。
填充(Padding):为了满足上述对齐要求,在结构体成员之间或末尾可能会插入一些未使用的字节,这被称为填充。这些填充字节确保了后续成员能够正确地对齐。
总大小:结构体的总大小不仅取决于其成员的大小,还包括由于对齐要求而添加的任何填充字节。
示例
考虑以下结构体定义:
struct Example {char a; // 1 byteint b; // 4 bytes (假设为32位系统)short c; // 2 bytes
};printf("%d\n", sizeof(Example) ); // 输出12
- 内存布局:
a
放在偏移 0 处,占用 1 字节。b
需要 4 字节对齐。下一个 4 字节对齐的位置是偏移 4。因此,在a
(偏移0) 和b
(偏移4) 之间需要 3 字节填充。b
占用偏移 4, 5, 6, 7。c
需要 2 字节对齐。偏移 8 是 2 字节对齐的,所以c
可以放在偏移 8 处,占用 2 字节 (偏移 8, 9)。- 结构体总大小需要是其最大成员对齐要求(4 字节)的倍数。当前总大小是 10 字节。下一个 4 字节对齐的大小是 12。因此,在末尾添加 2 字节填充。
- 总大小: 1 (a) + 3 (填充) + 4 (b) + 2 (c) + 2 (末尾填充) = 12 字节
情况二:重新排序 (b, c, a)
struct Example {int b; // 4 bytesshort c; // 2 byteschar a; // 1 byte
};
- 内存布局:
b
放在偏移 0 处(4 字节对齐),占用 4 字节 (0-3)。c
需要 2 字节对齐。偏移 4 是 2 字节对齐的,所以c
放在偏移 4 处,占用 2 字节 (4-5)。a
只需要 1 字节对齐。偏移 6 是 1 字节对齐的,所以a
放在偏移 6 处,占用 1 字节 (6)。- 当前总大小是 7 字节。需要是最大成员对齐要求(4 字节)的倍数。下一个 4 字节对齐的大小是 8。因此,在末尾添加 1 字节填充。
- 总大小: 4 (b) + 2 (c) + 1 (a) + 1 (末尾填充) = 8 字节
结论: 通过将 int b
(4字节) 放在最前面,可以避免在 char a
和 int b
之间产生大的填充。无论 short c
和 char a
的顺序如何,只要 int b
在最前面,最终大小都可以优化到 8 字节。这比原始的 12 字节节省了 4 字节(33%)的空间。
最佳实践:在定义结构体时,尽量将成员按大小从大到小排序(int
, short
, char
),这样可以最大限度地减少填充字节,节省内存。
扩展
在标准 C 语言中,结构体(struct
)内部不能直接定义函数。
C 语言的结构体(struct
)是一种数据聚合类型,它的主要作用是将不同类型的数据(如 int
, char
, float
等)组合在一起,形成一个复合的数据结构。它本身不包含行为(即函数)。
在结构体中存储函数指针来模拟现在对象调用方法
在结构体中存储函数指针,将函数的地址赋给指针,从而实现类似“结构体有函数”的效果。这是一种在 C 语言中模拟“对象”和“方法”的常用技巧。
示例:
#include <stdio.h>// 定义一个结构体
struct Calculator {int value;// 在结构体中声明函数指针void (*add)(struct Calculator*, int);void (*print)(struct Calculator*);
};// 定义函数
void calc_add(struct Calculator* calc, int num) {calc->value += num;
}void calc_print(struct Calculator* calc) {printf("Current value: %d\n", calc->value);
}int main() {struct Calculator calc = {0, calc_add, calc_print};calc.add(&calc, 10);calc.print(&calc); // 输出: Current value: 10calc.add(&calc, 5);calc.print(&calc); // 输出: Current value: 15return 0;
}
在这个例子中:
struct Calculator
包含一个数据成员value
和两个函数指针成员add
和print
。- 我们将实际函数的地址赋给了这些指针。
- 通过结构体变量调用
calc.add(&calc, 10)
,看起来就像是在调用一个方法(虽然需要手动传入this
指针)。
为什么不能直接写函数?
C 语言的设计哲学是过程式和面向过程的。函数是独立于数据的。结构体纯粹用于组织数据。这与 C++ 等面向对象语言有本质区别。
注意:C++ 中的结构体
如果你使用的是 C++,那么情况完全不同!在 C++ 中:
struct
和class
非常相似。- 结构体(
struct
)内部可以定义函数(成员函数)、构造函数、析构函数等。 - C++ 的
struct
默认成员是public
的。
// 这是 C++ 代码!
struct Calculator {int value;// C++ 中允许在 struct 内定义函数void add(int num) {value += num;}void print() {printf("Current value: %d\n", value);}
};
语言 | 结构体能否包含函数 |
---|---|
C 语言 | ❌ 不行。但可以用函数指针成员来模拟。 |
C++ 语言 | ✅ 可以。struct 可以包含成员函数,用法几乎和 class 一样。 |
所以,在纯 C 语言环境下,记住:结构体里不能写函数,但可以写指向函数的指针。