第6章 数组

1. 数组的深入理解

1.1 数组的底层实现

数组是C语言中最基本的数据结构之一,其底层实现涉及内存布局、对齐要求和访问机制等核心概念。深入理解数组的底层实现对于编写高性能、安全的C代码至关重要。

1.1.1 数组的内存布局与对齐

数组在内存中以连续块形式存储,每个元素占用相同大小的内存空间,并遵循严格的对齐规则。

内存布局的底层细节

对于类型为 type 的数组 array_name[size],其内存布局具有以下特性:

  1. 连续存储:元素在内存中连续排列,无间隙
  2. 元素大小:每个元素占用 sizeof(type) 字节
  3. 地址计算array_name[i] 的地址为 base_address + i * sizeof(type)
  4. 内存对齐:数组起始地址必须满足 type 的对齐要求
  5. 边界检查:C语言不自动进行边界检查,需要程序员手动确保
内存对齐的技术深度

内存对齐是数组性能优化的关键因素:

  1. 对齐要求的计算

    • 基本类型的对齐要求通常等于其大小
    • 结构体的对齐要求等于其成员中最大的对齐要求
    • 可以使用 alignof 运算符获取类型的对齐要求
  2. 对齐对性能的影响

    • 对齐的内存访问可以充分利用CPU的内存访问带宽
    • 未对齐的访问可能导致CPU执行额外的内存读取操作
    • 某些架构(如SPARC)甚至不支持未对齐的内存访问
  3. 编译器对齐控制

    • 使用 __attribute__((aligned(n))) 强制指定对齐要求
    • 使用 __attribute__((packed)) 取消对齐优化(仅在特殊情况下使用)
  4. 多级缓存与对齐

    • 对齐的数组元素更容易落入同一个缓存行
    • 跨缓存行的元素访问会导致额外的缓存未命中
    • 合理的对齐可以提高缓存利用率
地址计算的底层优化

数组元素的地址计算是编译器优化的重要目标:

  1. 地址计算的汇编实现

    • 对于 array[i],编译器会生成 base_address + i * element_size 的计算
    • 现代编译器会使用移位操作优化乘法(如 i * 4 优化为 i << 2
    • 对于固定大小的数组,编译器会进行常量折叠优化
  2. 索引边界优化

    • 对于循环中的数组访问,编译器会尝试优化边界检查
    • 使用 size_t 类型作为索引可以避免符号扩展操作
    • 循环不变量优化可以将地址计算的部分移到循环外部
  3. SIMD指令与数组访问

    • 对齐的数组可以更有效地使用SIMD指令
    • 未对齐的数组可能需要额外的指令来处理边界情况
    • 编译器会根据数组对齐情况选择最优的SIMD指令集
内存对齐的重要性

内存对齐对数组性能有显著影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 对齐示例:不同数据类型的对齐要求
#include <stdalign.h>

// 检查对齐要求
printf("int 对齐要求: %zu 字节\n", alignof(int));
printf("double 对齐要求: %zu 字节\n", alignof(double));
printf("struct { char c; int i; } 对齐要求: %zu 字节\n",
alignof(struct { char c; int i; }));

// 对齐对数组访问性能的影响
// 对齐的数组访问
int aligned_array[10] __attribute__((aligned(16)));

// 未对齐的数组访问(模拟)
char buffer[sizeof(int) * 10 + 1];
int *unaligned_array = (int *)(buffer + 1); // 故意错位
多级缓存与数组访问

现代处理器的多级缓存架构对数组访问性能有深刻影响:

  1. 缓存行:通常为64字节,连续的数组元素可能落入同一缓存行
  2. 预取机制:处理器会自动预取连续的数组元素到缓存
  3. 空间局部性:数组的连续存储天然具有良好的空间局部性
  4. 缓存命中率:顺序访问数组时,缓存命中率接近100%

1.1.2 数组的声明与初始化进阶

一维数组的高级初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 完全初始化
int numbers[5] = {1, 2, 3, 4, 5};

// 部分初始化(剩余元素为0)
int numbers[5] = {1, 2};

// 自动计算大小
int numbers[] = {1, 2, 3, 4, 5};

// C99指定初始化器
int numbers[5] = {[0] = 1, [2] = 3, [4] = 5};

// C11泛型选择初始化
#define INIT_ARRAY(type, arr, ...) \n\
type arr[] = {__VA_ARGS__}

INIT_ARRAY(int, numbers, 1, 2, 3, 4, 5);
多维数组的内存布局

多维数组在内存中以行优先(row-major)顺序线性存储:

1
2
3
4
5
6
// 二维数组 int matrix[3][3] 的内存布局
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 0,0 | 0,1 | 0,2 | 1,0 | 1,1 | 1,2 | 2,0 | 2,1 | 2,2 |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 基地址 | 基地址+4 | 基地址+8 | 基地址+12| ... |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
复杂初始化示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 二维数组的嵌套初始化
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

// 二维数组的线性初始化
int matrix[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

// 不完全初始化(剩余元素为0)
int matrix[3][3] = {
{1, 2},
{4}
};

// C99指定初始化器
int matrix[3][3] = {
[0][0] = 1,
[1][1] = 5,
[2][2] = 9
};

1.1.3 数组的边界检查与安全性

C语言不自动进行数组边界检查,这是一把双刃剑:一方面提供了极高的性能,另一方面也带来了安全隐患。

边界检查的技术深度
  1. 边界检查的开销分析

    • 每次数组访问都进行边界检查会增加约10-20%的执行时间
    • 循环中的边界检查可以被编译器优化为单次检查
    • 可以使用条件编译在调试版本中启用边界检查,在发布版本中禁用
  2. 静态边界检查

    • 编译器可以在编译时检测到的边界检查错误
    • 静态分析工具(如Clang Static Analyzer、Coverity)可以检测潜在的边界问题
    • 类型安全的数组封装可以在编译时提供边界检查
  3. 动态边界检查

    • 运行时边界检查可以捕获所有越界访问
    • 内存安全工具(如AddressSanitizer、Valgrind)可以检测越界访问
    • 自定义的安全数组结构可以提供运行时边界检查
高级边界检查实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 编译时边界检查宏
#define STATIC_ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define ASSERT_ARRAY_BOUNDS(arr, idx) \
_Static_assert((void)sizeof(char[1 - 2*!!((idx) < 0 || (idx) >= STATIC_ARRAY_SIZE(arr))]), "数组索引越界")

// 运行时边界检查宏(带性能优化)
#define SAFE_ARRAY_ACCESS(arr, idx, default_val) \
(__builtin_expect(((idx) >= 0 && (idx) < STATIC_ARRAY_SIZE(arr)), 1) ? \
(arr)[idx] : (default_val))

// 动态数组的边界检查
#define DYNAMIC_ARRAY_ACCESS(arr, size, idx, default_val) \
(__builtin_expect(((idx) >= 0 && (idx) < (size)), 1) ? \
(arr)[idx] : (default_val))
安全数组的高级封装
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// 泛型安全数组结构
typedef struct {
void *data;
size_t element_size;
size_t size;
size_t capacity;
void (*destroy_element)(void*);
} GenericSafeArray;

// 初始化泛型安全数组
GenericSafeArray* gsa_create(size_t element_size, size_t initial_capacity,
void (*destroy_element)(void*)) {
GenericSafeArray* arr = malloc(sizeof(GenericSafeArray));
if (!arr) return NULL;

arr->data = malloc(initial_capacity * element_size);
if (!arr->data) {
free(arr);
return NULL;
}

arr->element_size = element_size;
arr->size = 0;
arr->capacity = initial_capacity;
arr->destroy_element = destroy_element;
return arr;
}

// 安全访问(返回元素指针)
void* gsa_get(const GenericSafeArray* arr, size_t index) {
if (!arr || index >= arr->size) {
return NULL;
}
return (char*)arr->data + index * arr->element_size;
}

// 安全修改
bool gsa_set(GenericSafeArray* arr, size_t index, const void* value) {
if (!arr || !value || index >= arr->size) {
return false;
}
memcpy((char*)arr->data + index * arr->element_size, value, arr->element_size);
return true;
}

// 安全添加元素(自动扩容)
bool gsa_push(GenericSafeArray* arr, const void* value) {
if (!arr || !value) {
return false;
}

// 检查容量
if (arr->size >= arr->capacity) {
size_t new_capacity = arr->capacity * 2;
if (new_capacity < 4) new_capacity = 4;

void* new_data = realloc(arr->data, new_capacity * arr->element_size);
if (!new_data) {
return false;
}

arr->data = new_data;
arr->capacity = new_capacity;
}

// 添加元素
memcpy((char*)arr->data + arr->size * arr->element_size, value, arr->element_size);
arr->size++;
return true;
}

// 安全删除元素
bool gsa_remove(GenericSafeArray* arr, size_t index) {
if (!arr || index >= arr->size) {
return false;
}

// 如果有元素销毁函数,调用它
if (arr->destroy_element) {
void* element = (char*)arr->data + index * arr->element_size;
arr->destroy_element(element);
}

// 移动剩余元素
size_t elements_to_move = arr->size - index - 1;
if (elements_to_move > 0) {
memmove((char*)arr->data + index * arr->element_size,
(char*)arr->data + (index + 1) * arr->element_size,
elements_to_move * arr->element_size);
}

arr->size--;
return true;
}

// 安全销毁
void gsa_destroy(GenericSafeArray* arr) {
if (!arr) return;

// 如果有元素销毁函数,销毁所有元素
if (arr->destroy_element) {
for (size_t i = 0; i < arr->size; i++) {
void* element = (char*)arr->data + i * arr->element_size;
arr->destroy_element(element);
}
}

free(arr->data);
free(arr);
}

// 类型安全的宏
#define DEFINE_SAFE_ARRAY_TYPE(type, prefix) \
typedef struct {
GenericSafeArray base;
} prefix##Array;
\
static inline prefix##Array* prefix##_array_create(size_t initial_capacity) {
return (prefix##Array*)gsa_create(sizeof(type), initial_capacity, NULL);
}\
\
static inline bool prefix##_array_push(prefix##Array* arr, type value) {
return gsa_push((GenericSafeArray*)arr, &value);
}\
\
static inline type prefix##_array_get(const prefix##Array* arr, size_t index) {
type* ptr = (type*)gsa_get((const GenericSafeArray*)arr, index);
return ptr ? *ptr : (type)0;
}\
\
static inline void prefix##_array_destroy(prefix##Array* arr) {
gsa_destroy((GenericSafeArray*)arr);
}

// 使用示例:定义int类型的安全数组
DEFINE_SAFE_ARRAY_TYPE(int, int);

// 使用示例
void safe_array_example() {
intArray* arr = int_array_create(4);
if (arr) {
// 添加元素
int_array_push(arr, 10);
int_array_push(arr, 20);
int_array_push(arr, 30);

// 访问元素
for (size_t i = 0; i < ((GenericSafeArray*)arr)->size; i++) {
printf("Element %zu: %d\n", i, int_array_get(arr, i));
}

// 销毁数组
int_array_destroy(arr);
}
}
边界检查的性能优化
  1. 分支预测优化

    • 使用 __builtin_expect 提示编译器预测分支方向
    • 将边界检查放在循环外部,只检查一次
    • 使用无分支的条件移动指令代替显式分支
  2. 内存布局优化

    • 将数组大小和容量信息放在数组结构的开头
    • 使用对齐的内存布局提高访问速度
    • 对于固定大小的数组,使用编译时常量
  3. 编译器优化提示

    • 使用 restrict 关键字告诉编译器指针不重叠
    • 使用 inline 关键字内联小型访问函数
    • 使用 register 关键字提示编译器将频繁使用的变量放入寄存器

1.1.4 数组的声明和初始化

一维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明一维数组
type array_name[size];

// 示例
int numbers[5]; // 声明一个包含 5 个整数的数组

// 初始化一维数组
int numbers[5] = {1, 2, 3, 4, 5}; // 完整初始化
int numbers[] = {1, 2, 3, 4, 5}; // 自动计算大小
int numbers[5] = {1, 2}; // 部分初始化,其余元素为 0
int numbers[5] = {0}; // 所有元素初始化为 0

// C99 及以上支持的指定初始化器
int numbers[5] = {[0] = 1, [2] = 3, [4] = 5}; // 只初始化指定位置的元素
二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明二维数组
type array_name[rows][columns];

// 示例
int matrix[3][3]; // 声明一个 3x3 的整数矩阵

// 初始化二维数组
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

int matrix[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 线性初始化
int matrix[][3] = {{1, 2, 3}, {4, 5, 6}}; // 自动计算行数

// C99 及以上支持的指定初始化器
int matrix[3][3] = {
[0][0] = 1,
[1][1] = 5,
[2][2] = 9
};
多维数组

多维数组的声明和初始化方式类似于二维数组,只是维度更多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明三维数组
int cube[2][3][4]; // 2 层,每层 3 行 4 列

// 初始化三维数组
int cube[2][3][4] = {
{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
},
{
{13, 14, 15, 16},
{17, 18, 19, 20},
{21, 22, 23, 24}
}
};

1.2 数组的访问

数组元素通过索引访问,索引从 0 开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int numbers[5] = {1, 2, 3, 4, 5};

// 访问单个元素
printf("第一个元素:%d\n", numbers[0]); // 输出 1
printf("最后一个元素:%d\n", numbers[4]); // 输出 5

// 修改元素
numbers[2] = 10; // 将第三个元素修改为 10

// 遍历数组
for (int i = 0; i < 5; i++)
{
printf("numbers[%d] = %d\n", i, numbers[i]);
}

// 遍历二维数组
int matrix[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("matrix[%d][%d] = %d\n", i, j, matrix[i][j]);
}
}

1.2.1 数组的边界检查

C 语言不进行数组边界检查,访问超出数组范围的元素会导致未定义行为。

1
2
int numbers[5] = {1, 2, 3, 4, 5};
numbers[10] = 100; // 越界访问,未定义行为

为了避免数组越界,可以采取以下措施:

  • 使用常量定义数组大小
  • 在循环中使用正确的边界条件
  • 使用静态分析工具检测潜在的越界访问

1.3 数组的特性

  • 数组大小固定 - 数组一旦声明,大小不能改变
  • 连续存储 - 数组元素在内存中连续存储
  • 零基索引 - 数组索引从 0 开始
  • 数组名是常量指针 - 数组名指向数组的第一个元素,是一个常量指针
  • 数组长度计算 - 使用 sizeof 运算符可以计算数组的总大小和单个元素的大小
1
2
3
4
int numbers[5];
printf("数组总大小:%zu 字节\n", sizeof(numbers)); // 输出 20(假设 int 为 4 字节)
printf("单个元素大小:%zu 字节\n", sizeof(numbers[0])); // 输出 4
printf("数组长度:%zu\n", sizeof(numbers) / sizeof(numbers[0])); // 输出 5

1.4 变长数组(VLA)

C99 引入了变长数组(Variable Length Array),允许使用变量作为数组大小。

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
#include <stdio.h>

void print_array(int n, int arr[n])
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

int main(void)
{
int size;
printf("请输入数组大小:");
scanf("%d", &size);

int numbers[size]; // 变长数组

for (int i = 0; i < size; i++)
{
numbers[i] = i + 1;
}

print_array(size, numbers);

return 0;
}

注意

  • 变长数组不能初始化
  • 变长数组的大小在运行时确定
  • 变长数组只在块作用域内有效
  • 不是所有编译器都支持变长数组(如 MSVC)

1.5 数组作为函数参数

当数组作为函数参数传递时,实际上传递的是数组第一个元素的地址。这一机制对函数设计和性能优化有着重要影响。

1.5.1 数组作为函数参数的底层机制

  1. 数组退化

    • 数组作为函数参数时会退化为指向其第一个元素的指针
    • 编译器会将 void func(int arr[]) 转换为 void func(int *arr)
    • 因此,函数无法通过 sizeof(arr) 获取数组的实际大小
  2. 参数传递的汇编实现

    • 数组参数在传递时,只需要将数组的基地址压栈
    • 相比传递整个数组,这大大减少了参数传递的开销
    • 对于大型数组,这种优化尤为重要
  3. 类型信息的保留

    • 虽然数组会退化为指针,但指针的类型信息会被保留
    • 这使得编译器能够正确处理指针算术和类型转换
    • 例如,int *float * 的指针算术步长不同

1.5.2 多维数组作为函数参数的高级处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 二维数组作为参数
void process_matrix(int matrix[][3], int rows) {
// 处理矩阵
}

// 等价于
void process_matrix(int (*matrix)[3], int rows) {
// 处理矩阵
}

// 三维数组作为参数
void process_3d_array(int arr[][2][3], int depth) {
// 处理三维数组
}

// 等价于
void process_3d_array(int (*arr)[2][3], int depth) {
// 处理三维数组
}

// 动态大小的多维数组(C99及以上)
void process_variable_matrix(int rows, int cols, int matrix[rows][cols]) {
// 处理可变大小的矩阵
}

1.5.3 数组参数的性能优化

  1. 传递数组长度

    • 始终将数组长度作为单独的参数传递
    • 使用 size_t 类型作为长度参数,提高可移植性
    • 避免使用全局变量传递数组长度
  2. 使用 restrict 关键字

    • restrict 关键字告诉编译器指针不与其他指针重叠
    • 这使得编译器能够进行更积极的优化
    • 例如:void process_array(int *restrict arr, size_t size)
  3. 内联函数优化

    • 对于小型数组处理函数,使用 inline 关键字
    • 内联可以消除函数调用开销,提高性能
    • 但要注意内联可能导致代码膨胀
  4. 编译时边界检查

    • 使用静态断言在编译时检查数组边界
    • 例如:_Static_assert(sizeof(arr) == expected_size, "数组大小不正确")

1.5.4 数组参数的安全性

  1. 边界检查

    • 在函数内部始终检查数组索引的边界
    • 使用 assert 或条件检查确保索引有效
    • 对于外部输入,进行严格的边界验证
  2. 指针有效性

    • 检查传入的数组指针是否为 NULL
    • 对于空指针,采取适当的错误处理策略
    • 例如:if (!arr) return error_code;
  3. 类型安全

    • 避免将不同类型的数组传递给同一个函数
    • 使用 void* 时,确保正确的类型转换
    • 考虑使用泛型编程技术提高类型安全性

1.5.5 高级数组参数传递技术

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
// 带长度信息的数组结构
typedef struct {
int *data;
size_t size;
} ArrayWithSize;

// 使用结构传递数组和长度
void process_array_safe(ArrayWithSize arr) {
for (size_t i = 0; i < arr.size; i++) {
// 安全处理 arr.data[i]
}
}

// 泛型数组处理函数
void generic_array_process(void *data, size_t element_size,
size_t count, void (*process)(void*)) {
char *ptr = (char*)data;
for (size_t i = 0; i < count; i++) {
process(ptr + i * element_size);
}
}

// 示例:处理整数数组
void process_int(void *data) {
int *value = (int*)data;
*value = *value * 2; // 简单处理:乘以2
}

// 使用示例
void example() {
int arr[] = {1, 2, 3, 4, 5};
generic_array_process(arr, sizeof(int), 5, process_int);
// 现在 arr 中的元素为 {2, 4, 6, 8, 10}
}

1.5.6 数组参数的现代C特性

  1. 可变长度数组(VLA)

    • C99引入的可变长度数组可以作为函数参数
    • 例如:void process_vla(int n, int arr[n])
    • 注意:并非所有编译器都支持VLA(如MSVC)
  2. 复合字面量

    • 复合字面量可以直接作为数组参数传递
    • 例如:process_array((int[]){1, 2, 3, 4, 5}, 5)
    • 这提供了一种方便的方式传递临时数组
  3. 数组指针的类型别名

    • 使用 typedef 创建数组指针的类型别名
    • 例如:typedef int (*IntArrayPtr)[10];
    • 这提高了代码的可读性和可维护性

2. 指针的深入理解

2.1 指针的基本概念

指针是一种存储内存地址的变量。指针变量本身也占用内存空间,其大小取决于系统架构(32位系统为4字节,64位系统为8字节)。深入理解指针的底层实现和内存模型对于编写高性能、安全的C代码至关重要。

2.1.1 指针的底层实现与内存模型

  1. 指针的存储

    • 指针在内存中存储的是另一个变量的内存地址
    • 指针的大小取决于系统架构:32位系统中为4字节,64位系统中为8字节
    • 指针的大小与所指向的数据类型无关
  2. 指针的类型

    • 指针的类型决定了指针算术的步长和解引用的行为
    • 例如,int * 类型的指针每次递增会移动4字节(在32位系统中)
    • void * 类型的指针不包含类型信息,需要显式转换后才能解引用
  3. 指针的表示

    • 指针值通常以十六进制形式表示内存地址
    • 空指针(NULL)在内存中表示为全0
    • 野指针是指向无效内存区域的指针,使用野指针会导致未定义行为

2.1.2 指针的内存布局

对于一个类型为 type * 的指针变量 p,其内存布局如下:

1
2
3
4
5
6
7
+---------------------+
| p |
+---------------------+
| 内存地址: &p |
+---------------------+
| 值: 指向的内存地址 |
+---------------------+

其中,&p 是指针变量本身的内存地址,而指针变量的值是它指向的内存地址。

2.1.3 指针的高级操作

  1. 多级指针

    • 指向指针的指针,用于处理二维数组、动态分配的多维数据等
    • 例如:int **pp 表示指向int指针的指针
    • 多级指针的使用需要谨慎,避免过度复杂
  2. 函数指针

    • 指向函数的指针,用于实现回调、策略模式等
    • 例如:void (*callback)(int) 表示指向接受int参数、返回void的函数的指针
    • 函数指针是C语言实现多态的重要手段
  3. 指针与数组的关系

    • 数组名可以视为指向数组第一个元素的常量指针
    • 指针可以像数组一样使用下标访问
    • 例如:p[0] 等价于 *pp[1] 等价于 *(p+1)
  4. 指针与结构体

    • 指针可以指向结构体,用于动态分配和操作结构体
    • 使用 -> 运算符访问结构体指针的成员
    • 例如:struct Person *p; p->name 等价于 (*p).name

2.1.4 指针的安全使用

  1. 空指针检查

    • 在解引用指针前始终检查是否为NULL
    • 例如:if (p != NULL) { *p = value; }
    • 可以使用 assert 宏在调试版本中进行检查
  2. 野指针的避免

    • 始终将指针初始化为NULL或有效的内存地址
    • 在释放指针后将其设置为NULL
    • 避免使用已经释放的指针
  3. 指针的边界检查

    • 在使用指针进行算术运算时,确保不会越界
    • 对于动态分配的内存,跟踪其大小并进行边界检查
  4. 指针的类型安全

    • 避免随意的指针类型转换
    • 使用 void * 作为通用指针类型,但在使用前进行正确的类型转换
    • 考虑使用 restrict 关键字提高指针的类型安全性和优化潜力

2.1.5 指针的性能优化

  1. 指针算术优化

    • 编译器会优化指针算术,例如将 p + 1 转换为适当的地址计算
    • 对于固定步长的指针算术,编译器会生成高效的汇编代码
  2. 指针别名优化

    • 使用 restrict 关键字告诉编译器指针不与其他指针重叠
    • 这使得编译器能够进行更积极的优化,如加载/存储优化
  3. 指针与缓存

    • 局部变量的指针通常指向栈内存,具有较高的缓存命中率
    • 全局变量的指针指向数据段,缓存命中率可能较低
    • 合理的指针使用可以提高缓存利用率
  4. 指针与分支预测

    • 指针的条件判断会影响分支预测
    • 避免在热点路径中使用难以预测的指针条件

2.1.6 指针的底层实现示例

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 指针的大小
printf("Size of int *: %zu bytes\n", sizeof(int *));
printf("Size of char *: %zu bytes\n", sizeof(char *));
printf("Size of double *: %zu bytes\n", sizeof(double *));

// 指针的地址计算
int arr[] = {1, 2, 3, 4, 5};
int *p = arr;
printf("Address of arr[0]: %p\n", (void *)p);
printf("Address of arr[1]: %p\n", (void *)(p + 1));
printf("Difference: %td bytes\n", (char *)(p + 1) - (char *)p);

// 多级指针
int x = 10;
int *px = &x;
int **ppx = &px;
printf("Value of x: %d\n", **ppx);

// 函数指针
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int (*operation)(int, int);
operation = add;
printf("5 + 3 = %d\n", operation(5, 3));
operation = subtract;
printf("5 - 3 = %d\n", operation(5, 3));

// 指针与结构体
struct Point {
int x;
int y;
};

struct Point p1 = {10, 20};
struct Point *pp1 = &p1;
printf("Point: (%d, %d)\n", pp1->x, pp1->y);

// 指针与数组的关系
int arr2[] = {1, 2, 3, 4, 5};
int *p2 = arr2;
printf("arr2[0] = %d\n", *p2);
printf("arr2[1] = %d\n", *(p2 + 1));
printf("arr2[2] = %d\n", p2[2]);

// 指针与字符串
char str[] = "Hello";
char *p3 = str;
while (*p3 != '\0') {
printf("%c", *p3);
p3++;
}
printf("\n");

// 动态内存分配与指针
int *dynamic_arr = malloc(5 * sizeof(int));
if (dynamic_arr != NULL) {
for (int i = 0; i < 5; i++) {
dynamic_arr[i] = i * 10;
}
for (int i = 0; i < 5; i++) {
printf("dynamic_arr[%d] = %d\n", i, dynamic_arr[i]);
}
free(dynamic_arr);
dynamic_arr = NULL; // 避免野指针
}

2.2 指针的声明和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明指针
type *pointer_name;

// 示例
int *p; // 声明一个指向整数的指针
float *fp; // 声明一个指向浮点数的指针
char *cp; // 声明一个指向字符的指针

// 初始化指针
int x = 10;
int *p = &x; // 初始化指针 p,使其指向变量 x

// 空指针
int *p = NULL; // 初始化为空指针
int *p = 0; // 初始化为空指针(0 等同于 NULL)

// C11 引入的空指针常量
int *p = nullptr; // 仅在 C++11 及以上或支持 C11 的编译器中可用

2.3 指针的解引用

使用 * 运算符可以访问指针指向的变量的值。

1
2
3
4
5
6
7
8
9
10
int x = 10;
int *p = &x;

printf("x 的值:%d\n", x); // 输出 10
printf("p 的值:%p\n", p); // 输出 x 的地址
printf("*p 的值:%d\n", *p); // 输出 10,解引用

// 修改指针指向的变量的值
*p = 20;
printf("x 的新值:%d\n", x); // 输出 20

2.4 指针的算术运算

指针支持以下算术运算:

  • 加法 - 指针 + 整数
  • 减法 - 指针 - 整数
  • 指针相减 - 两个指针相减
  • 比较 - 比较两个指针的大小

2.4.1 指针算术的本质

指针算术的结果取决于指针指向的类型。对于一个类型为 type * 的指针 pp + n 表示指向 p 指向的位置之后的第 ntype 类型的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int numbers[] = {1, 2, 3, 4, 5};
int *p = numbers; // 指向第一个元素

printf("*p = %d\n", *p); // 输出 1
printf("p 的地址:%p\n", (void *)p);

p++; // 指针向后移动一个元素(4字节)
printf("*p = %d\n", *p); // 输出 2
printf("p 的地址:%p\n", (void *)p);

p += 2; // 指针向后移动两个元素(8字节)
printf("*p = %d\n", *p); // 输出 4
printf("p 的地址:%p\n", (void *)p);

int *q = &numbers[4]; // 指向最后一个元素
printf("q - p = %ld\n", q - p); // 输出 1,两个指针之间的元素个数

// 比较指针
if (p < q)
{
printf("p 指向的元素在 q 指向的元素之前\n");
}

2.5 指针的类型转换

指针的类型转换需要谨慎使用,不正确的类型转换可能导致未定义行为。

1
2
3
4
5
6
7
8
9
10
11
12
int x = 10;
int *p = &x;

// 隐式类型转换
void *vp = p; // 任何指针都可以隐式转换为 void *

// 显式类型转换
int *p2 = (int *)vp; // 需要显式转换回原类型

// 不同类型之间的转换
char *cp = (char *)&x; // 将 int * 转换为 char *
printf("*cp = %d\n", *cp); // 输出 x 的第一个字节的值

2.6 空指针和野指针

2.6.1 空指针

空指针是指向空地址的指针,通常使用 NULL0 表示。

1
2
3
4
5
6
7
int *p = NULL;

// 检查空指针
if (p != NULL)
{
// 使用指针
}

2.6.2 野指针

野指针是指向无效内存地址的指针,通常由以下原因产生:

  • 未初始化的指针
  • 已释放内存的指针
  • 越界访问的指针
1
2
3
4
5
6
7
8
9
10
11
// 未初始化的指针
int *p; // 野指针

// 已释放内存的指针
int *p = (int *)malloc(sizeof(int));
free(p);
p = NULL; // 释放后应将指针置为空

// 越界访问的指针
int numbers[5];
int *p = &numbers[5]; // 指向数组末尾之后,野指针

2.7 指针数组

指针数组是一个存储指针的数组。

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
// 声明指针数组
type *array_name[size];

// 示例:字符串数组
char *names[] = {
"Alice",
"Bob",
"Charlie",
"David"
};

// 访问指针数组元素
for (int i = 0; i < 4; i++)
{
printf("%s\n", names[i]);
}

// 示例:整数指针数组
int a = 10, b = 20, c = 30;
int *ptr_array[] = {&a, &b, &c};

// 访问指针数组元素
for (int i = 0; i < 3; i++)
{
printf("%d\n", *ptr_array[i]);
}

2.8 指向指针的指针

指向指针的指针是一种存储指针地址的变量。

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
// 声明指向指针的指针
type **pointer_name;

// 示例
int x = 10;
int *p = &x;
int **pp = &p;

printf("x = %d\n", x); // 输出 10
printf("*p = %d\n", *p); // 输出 10
printf("**pp = %d\n", **pp); // 输出 10

// 修改值
**pp = 20;
printf("x = %d\n", x); // 输出 20

// 指向指针的指针的应用:二维动态数组
int **matrix;
int rows = 3, cols = 3;

// 分配内存
matrix = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++)
{
matrix[i] = (int *)malloc(cols * sizeof(int));
}

// 使用
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
matrix[i][j] = i * cols + j + 1;
}
}

// 释放内存
for (int i = 0; i < rows; i++)
{
free(matrix[i]);
}
free(matrix);

2.9 函数指针

函数指针是指向函数的指针,可以用于回调函数、函数表等场景。

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
// 声明函数指针
type (*pointer_name)(parameters);

// 示例
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

// 声明函数指针
int (*operation)(int, int);

// 初始化函数指针
operation = add;
printf("3 + 4 = %d\n", operation(3, 4)); // 输出 7

operation = subtract;
printf("3 - 4 = %d\n", operation(3, 4)); // 输出 -1

// 函数指针数组
int (*operations[])(int, int) = {add, subtract};
printf("3 + 4 = %d\n", operations[0](3, 4)); // 输出 7
printf("3 - 4 = %d\n", operations[1](3, 4)); // 输出 -1

3. 数组和指针的关系

3.1 数组名作为指针

数组名本质上是一个指向数组第一个元素的常量指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
int numbers[] = {1, 2, 3, 4, 5};
int *p = numbers; // 等同于 int *p = &numbers[0];

// 使用指针遍历数组
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i)); // 等同于 numbers[i]
}
printf("\n");

// 数组下标访问等同于指针解引用
printf("numbers[2] = %d\n", numbers[2]); // 输出 3
printf("*(numbers + 2) = %d\n", *(numbers + 2)); // 输出 3

3.2 数组下标和指针算术的等价性

对于数组 arr 和整数 iarr[i] 等价于 *(arr + i)

1
2
3
4
5
6
7
8
9
10
11
int numbers[] = {1, 2, 3, 4, 5};
int *p = numbers;

// 以下表达式等价
numbers[2] = 10;
*(numbers + 2) = 10;
p[2] = 10;
*(p + 2) = 10;

// 甚至可以写成这样(不推荐)
2[numbers] = 10; // 等价于 numbers[2],因为加法满足交换律

3.3 多维数组与指针

多维数组在内存中是线性存储的,其元素按行优先顺序排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 二维数组
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

// 内存布局:1, 2, 3, 4, 5, 6, 7, 8, 9

// 指向一维数组的指针
int (*p)[3] = matrix;

// 访问元素
printf("matrix[1][2] = %d\n", matrix[1][2]); // 输出 6
printf("*(*(p + 1) + 2) = %d\n", *(*(p + 1) + 2)); // 输出 6
printf("p[1][2] = %d\n", p[1][2]); // 输出 6

4. 动态内存管理

4.1 内存分配函数

C 语言提供了以下动态内存分配函数:

  • malloc - 分配指定大小的内存
  • calloc - 分配指定数量和大小的内存,并初始化为 0
  • realloc - 调整已分配内存的大小
  • free - 释放已分配的内存

这些函数都在 stdlib.h 头文件中声明。

4.2 内存分配示例

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
// 使用 malloc 分配内存
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL)
{
printf("内存分配失败\n");
return 1;
}

// 初始化数组
for (int i = 0; i < 5; i++)
{
arr[i] = i + 1;
}

// 打印数组
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
printf("\n");

// 使用 realloc 调整内存大小
int *new_arr = (int *)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL)
{
printf("内存分配失败\n");
free(arr);
return 1;
}
arr = new_arr;

// 初始化新分配的元素
for (int i = 5; i < 10; i++)
{
arr[i] = i + 1;
}

// 打印数组
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");

// 使用 free 释放内存
free(arr);

// 使用 calloc 分配内存
int *arr2 = (int *)calloc(5, sizeof(int));
if (arr2 == NULL)
{
printf("内存分配失败\n");
return 1;
}

// 打印数组(已初始化为 0)
for (int i = 0; i < 5; i++)
{
printf("%d ", arr2[i]);
}
printf("\n");

// 释放内存
free(arr2);

return 0;
}

4.3 内存管理的常见问题

4.3.1 内存泄漏

内存泄漏是指程序分配了内存但没有释放,导致内存使用量不断增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 内存泄漏示例
void leak_memory(void)
{
int *arr = (int *)malloc(100 * sizeof(int));
// 没有调用 free(arr);
}

int main(void)
{
for (int i = 0; i < 1000; i++)
{
leak_memory(); // 每次调用都会泄漏内存
}
return 0;
}

4.3.2 双重释放

双重释放是指对同一块内存多次调用 free,导致未定义行为。

1
2
3
int *p = (int *)malloc(sizeof(int));
free(p);
free(p); // 双重释放,未定义行为

4.3.3 悬垂指针

悬垂指针是指指向已释放内存的指针。

1
2
3
int *p = (int *)malloc(sizeof(int));
free(p);
*p = 10; // 悬垂指针,未定义行为

4.4 内存管理的最佳实践

  • 始终检查内存分配是否成功
  • 使用完内存后及时释放
  • 释放内存后将指针置为空
  • 避免双重释放
  • 使用适当的内存分配函数
  • 考虑使用内存池管理频繁分配的小内存

5. 高级应用

5.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
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h>
#include <stdlib.h>

// 动态数组结构
typedef struct {
int *data;
int size;
int capacity;
} DynamicArray;

// 初始化动态数组
DynamicArray *create_dynamic_array(int initial_capacity)
{
DynamicArray *array = (DynamicArray *)malloc(sizeof(DynamicArray));
if (array == NULL)
{
return NULL;
}

array->data = (int *)malloc(initial_capacity * sizeof(int));
if (array->data == NULL)
{
free(array);
return NULL;
}

array->size = 0;
array->capacity = initial_capacity;

return array;
}

// 添加元素
void add_element(DynamicArray *array, int element)
{
if (array->size >= array->capacity)
{
// 扩容
int new_capacity = array->capacity * 2;
int *new_data = (int *)realloc(array->data, new_capacity * sizeof(int));
if (new_data == NULL)
{
return;
}

array->data = new_data;
array->capacity = new_capacity;
}

array->data[array->size++] = element;
}

// 释放动态数组
void free_dynamic_array(DynamicArray *array)
{
if (array != NULL)
{
free(array->data);
free(array);
}
}

int main(void)
{
DynamicArray *array = create_dynamic_array(5);
if (array == NULL)
{
printf("内存分配失败\n");
return 1;
}

// 添加元素
for (int i = 0; i < 10; i++)
{
add_element(array, i + 1);
}

// 打印元素
for (int i = 0; i < array->size; i++)
{
printf("%d ", array->data[i]);
}
printf("\n");

// 释放内存
free_dynamic_array(array);

return 0;
}

5.2 指针数组的高级应用

指针数组可以用于实现字符串表、命令表等。

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>

void help_command(void)
{
printf("帮助命令\n");
}

void version_command(void)
{
printf("版本命令\n");
}

void exit_command(void)
{
printf("退出命令\n");
}

// 命令结构
typedef struct {
char *name;
void (*func)(void);
} Command;

// 命令表
Command commands[] = {
{"help", help_command},
{"version", version_command},
{"exit", exit_command},
{NULL, NULL} // 结束标记
};

// 执行命令
void execute_command(char *name)
{
for (int i = 0; commands[i].name != NULL; i++)
{
if (strcmp(name, commands[i].name) == 0)
{
commands[i].func();
return;
}
}
printf("未知命令\n");
}

int main(void)
{
execute_command("help");
execute_command("version");
execute_command("exit");
execute_command("unknown");

return 0;
}

5.3 函数指针的高级应用

函数指针可以用于实现回调函数、策略模式等。

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
56
57
58
59
60
61
62
63
64
// 回调函数示例
#include <stdio.h>

// 回调函数类型
typedef int (*CompareFunction)(int, int);

// 比较函数
int ascending(int a, int b)
{
return a - b;
}

int descending(int a, int b)
{
return b - a;
}

// 排序函数
void sort(int arr[], int size, CompareFunction compare)
{
for (int i = 0; i < size - 1; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
if (compare(arr[j], arr[j + 1]) > 0)
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

// 打印数组
void print_array(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

int main(void)
{
int numbers[] = {5, 2, 8, 1, 9};
int size = sizeof(numbers) / sizeof(numbers[0]);

printf("原始数组:");
print_array(numbers, size);

// 升序排序
sort(numbers, size, ascending);
printf("升序排序:");
print_array(numbers, size);

// 降序排序
sort(numbers, size, descending);
printf("降序排序:");
print_array(numbers, size);

return 0;
}

6. 性能优化

6.1 数组和指针操作的性能比较

在大多数情况下,数组下标访问和指针算术的性能差异很小,因为编译器会进行优化。然而,在某些特定场景下,指针算术可以提供更细粒度的控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数组下标访问
for (int i = 0; i < size; i++)
{
sum += arr[i];
}

// 指针算术访问
for (int *p = arr, *end = arr + size; p < end; p++)
{
sum += *p;
}

// 优化的指针访问:使用寄存器变量
register int *p asm("r12") = arr;
register int *end asm("r13") = arr + size;
register int s asm("r14") = 0;
for (; p < end; p++) {
s += *p;
}
sum = s;

6.2 缓存友好的数据结构和算法

缓存友好的代码可以显著提高程序性能,特别是在处理大型数组时。

6.2.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
26
27
28
29
// 缓存友好:按行访问
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
sum += matrix[i][j];
}
}

// 缓存不友好:按列访问
for (int j = 0; j < cols; j++)
{
for (int i = 0; i < rows; i++)
{
sum += matrix[i][j];
}
}

// 更优化的方式:分块访问(Cache Blocking)
#define BLOCK_SIZE 32
for (int i = 0; i < rows; i += BLOCK_SIZE) {
for (int j = 0; j < cols; j += BLOCK_SIZE) {
for (int ii = i; ii < i + BLOCK_SIZE && ii < rows; ii++) {
for (int jj = j; jj < j + BLOCK_SIZE && jj < cols; jj++) {
sum += matrix[ii][jj];
}
}
}
}

6.2.2 时间局部性优化

时间局部性是指程序倾向于多次访问同一内存位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 时间局部性好:重复使用变量
int value = arr[i];
for (int j = 0; j < 10; j++)
{
sum += value * j;
}

// 时间局部性差:重复访问数组元素
for (int j = 0; j < 10; j++)
{
sum += arr[i] * j;
}

// 循环展开:提高时间局部性和指令级并行
for (int i = 0; i < size - 3; i += 4) {
sum += arr[i] + arr[i+1] + arr[i+2] + arr[i+3];
}
// 处理剩余元素
for (; i < size; i++) {
sum += arr[i];
}

6.3 SIMD指令优化

SIMD(Single Instruction Multiple Data)指令可以同时处理多个数据元素,显著提高数组操作性能。

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
// 使用AVX2指令优化数组求和
#include <immintrin.h>

float sum_array_avx2(const float *arr, size_t size) {
__m256 sum_vec = _mm256_setzero_ps();
size_t i;

// 处理8个元素为一组
for (i = 0; i + 7 < size; i += 8) {
__m256 vec = _mm256_loadu_ps(arr + i);
sum_vec = _mm256_add_ps(sum_vec, vec);
}

// 水平求和
float sum = 0.0f;
float temp[8];
_mm256_storeu_ps(temp, sum_vec);
for (int j = 0; j < 8; j++) {
sum += temp[j];
}

// 处理剩余元素
for (; i < size; i++) {
sum += arr[i];
}

return sum;
}

// 使用SSE指令优化整数数组求和
int sum_array_sse(const int *arr, size_t size) {
__m128i sum_vec = _mm_setzero_si128();
size_t i;

// 处理4个元素为一组
for (i = 0; i + 3 < size; i += 4) {
__m128i vec = _mm_loadu_si128((__m128i*)(arr + i));
sum_vec = _mm_add_epi32(sum_vec, vec);
}

// 水平求和
int sum = 0;
int temp[4];
_mm_storeu_si128((__m128i*)temp, sum_vec);
for (int j = 0; j < 4; j++) {
sum += temp[j];
}

// 处理剩余元素
for (; i < size; i++) {
sum += arr[i];
}

return sum;
}

6.4 内存池设计与数组管理

内存池可以显著提高频繁分配和释放小型数组的性能。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// 固定大小内存池实现
typedef struct {
void *pool; // 内存池起始地址
void *free_list; // 空闲块链表
size_t block_size; // 每个块的大小
size_t pool_size; // 内存池总大小
size_t block_count; // 总块数
} MemoryPool;

// 初始化内存池
bool pool_init(MemoryPool *pool, size_t block_size, size_t block_count) {
// 确保块大小至少能容纳一个指针
if (block_size < sizeof(void*)) {
block_size = sizeof(void*);
}

// 分配内存池
pool->pool = malloc(block_size * block_count);
if (!pool->pool) {
return false;
}

pool->block_size = block_size;
pool->pool_size = block_size * block_count;
pool->block_count = block_count;
pool->free_list = NULL;

// 初始化空闲块链表
char *current = (char*)pool->pool;
for (size_t i = 0; i < block_count; i++) {
void *block = current;
current += block_size;

// 将当前块链接到空闲链表
*(void**)block = pool->free_list;
pool->free_list = block;
}

return true;
}

// 从内存池分配内存
void* pool_alloc(MemoryPool *pool) {
if (!pool->free_list) {
return NULL; // 内存池耗尽
}

// 从空闲链表头部取出一个块
void *block = pool->free_list;
pool->free_list = *(void**)block;

return block;
}

// 释放内存到内存池
void pool_free(MemoryPool *pool, void *ptr) {
if (!ptr) {
return;
}

// 确保指针在内存池范围内
if (ptr < pool->pool || ptr >= (char*)pool->pool + pool->pool_size) {
return;
}

// 将块放回空闲链表头部
*(void**)ptr = pool->free_list;
pool->free_list = ptr;
}

// 销毁内存池
void pool_destroy(MemoryPool *pool) {
free(pool->pool);
pool->pool = NULL;
pool->free_list = NULL;
}

// 使用内存池管理小型数组
typedef struct {
int *data;
size_t size;
} SmallArray;

// 从内存池分配小型数组
SmallArray* alloc_small_array(MemoryPool *pool, size_t size) {
SmallArray *array = (SmallArray*)pool_alloc(pool);
if (!array) {
return NULL;
}

array->size = size;
array->data = (int*)pool_alloc(pool);
if (!array->data) {
pool_free(pool, array);
return NULL;
}

return array;
}

// 释放小型数组
void free_small_array(MemoryPool *pool, SmallArray *array) {
if (!array) {
return;
}

if (array->data) {
pool_free(pool, array->data);
}
pool_free(pool, array);
}

6.5 内存访问模式的高级优化

  • 减少内存访问次数:将频繁访问的数据存储在寄存器或局部变量中
  • 使用连续内存:使用数组而非链表,提高空间局部性
  • 避免内存碎片:合理规划内存分配,避免频繁分配和释放小内存
  • 内存对齐:合理安排数据结构,提高内存访问效率
  • 预取技术:使用软件预取指令提前加载数据到缓存
  • 分支预测优化:减少数组操作中的分支预测失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用软件预取优化大型数组操作
void process_large_array(const int *arr, size_t size, int *result) {
for (size_t i = 0; i < size; i++) {
// 预取下一组数据
if (i + 64 < size) {
__builtin_prefetch(&arr[i + 64], 0, 0);
}

// 处理当前元素
result[i] = arr[i] * 2 + 1;
}
}

// 分支预测优化:避免条件分支
void process_array_branchless(int *arr, size_t size) {
for (size_t i = 0; i < size; i++) {
// 无分支实现:使用位操作替代条件判断
int value = arr[i];
int is_negative = value < 0;
arr[i] = (value ^ -is_negative) + is_negative; // 取绝对值
}
}

7. 常见错误和调试

7.1 数组越界

数组越界是最常见的错误之一,可能导致程序崩溃或安全漏洞。

1
2
3
4
5
6
// 错误:数组越界
int numbers[5];
for (int i = 0; i <= 5; i++) // 循环条件错误,应使用 i < 5
{
numbers[i] = i;
}

7.2 野指针和悬垂指针

野指针和悬垂指针可能导致程序崩溃或未定义行为。

1
2
3
4
5
6
7
8
// 错误:未初始化的指针
int *p;
*p = 10; // 野指针

// 错误:悬垂指针
int *p = (int *)malloc(sizeof(int));
free(p);
*p = 10; // 悬垂指针

7.3 内存泄漏

内存泄漏会导致程序内存使用量不断增加,最终可能导致程序崩溃。

1
2
3
4
5
6
// 错误:内存泄漏
void function(void)
{
int *p = (int *)malloc(100 * sizeof(int));
// 没有调用 free(p);
}

7.4 调试技巧

  • 使用断言:在关键位置添加断言,检查指针是否为空、数组下标是否越界等
  • 使用内存调试工具:如 Valgrind、AddressSanitizer 等工具检测内存错误
  • 打印调试信息:在关键位置打印指针值、数组下标等信息
  • 代码审查:仔细检查内存分配和释放的配对情况

8. 数组的高级应用

8.1 高性能数组算法

8.1.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
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
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <stddef.h>
#include <string.h>

// 通用二分查找实现
void *binary_search_generic(const void *key, const void *base, size_t nmemb,
size_t size, int (*compar)(const void *, const void *)) {
const char *ptr = (const char *)base;
size_t left = 0;
size_t right = nmemb - 1;

while (left <= right) {
size_t mid = left + (right - left) / 2;
const void *mid_ptr = ptr + mid * size;

int cmp = compar(key, mid_ptr);
if (cmp == 0) {
return (void *)mid_ptr;
} else if (cmp < 0) {
right = mid - 1;
} else {
left = mid + 1;
}
}

return NULL;
}

// 整数比较函数
int compare_int(const void *a, const void *b) {
return *(const int *)a - *(const int *)b;
}

// 字符串比较函数
int compare_str(const void *a, const void *b) {
return strcmp(*(const char **)a, *(const char **)b);
}

// 二分查找的分支预测优化版本
int binary_search_optimized(const int *arr, size_t size, int target) {
int left = 0;
int right = size - 1;

// 快速路径:检查边界
if (size == 0) return -1;
if (target < arr[0]) return -1;
if (target > arr[size-1]) return -1;

while (left <= right) {
int mid = left + (right - left) / 2;
int mid_val = arr[mid];

// 无分支预测优化
if (mid_val == target) {
return mid;
}

// 使用条件移动而非分支
int left_cond = (target > mid_val);
left = left_cond ? (mid + 1) : left;
right = left_cond ? right : (mid - 1);
}

return -1;
}

8.1.2 数组排序的高级实现

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 交换函数
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

// 插入排序
void insertion_sort(int *arr, size_t left, size_t right) {
for (size_t i = left + 1; i <= right; i++) {
int key = arr[i];
size_t j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}

// 堆排序
void heapify(int *arr, size_t n, size_t i) {
size_t largest = i;
size_t left = 2 * i + 1;
size_t right = 2 * i + 2;

if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(&arr[i], &arr[largest]);
heapify(arr, n, largest);
}
}

void heap_sort(int *arr, size_t left, size_t right) {
size_t n = right - left + 1;
for (size_t i = n / 2 - 1; i >= 0; i--) {
heapify(arr + left, n, i);
}
for (size_t i = n - 1; i > 0; i--) {
swap(&arr[left], &arr[left + i]);
heapify(arr + left, i, 0);
}
}

// 快速排序分区
size_t partition(int *arr, size_t left, size_t right) {
int pivot = arr[right];
size_t i = left - 1;
for (size_t j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[right]);
return i + 1;
}

// introsort 实现(结合快速排序、堆排序和插入排序)
void introsort_helper(int *arr, size_t left, size_t right, size_t max_depth);

void introsort(int *arr, size_t size) {
// 计算最大递归深度
size_t max_depth = 2 * (size_t)__builtin_log2(size);

introsort_helper(arr, 0, size - 1, max_depth);
}

void introsort_helper(int *arr, size_t left, size_t right, size_t max_depth) {
if (right - left <= 16) {
// 小数组使用插入排序
insertion_sort(arr, left, right);
} else if (max_depth == 0) {
// 递归深度达到上限,使用堆排序
heap_sort(arr, left, right);
} else {
// 使用快速排序
size_t pivot = partition(arr, left, right);
if (pivot > left) {
introsort_helper(arr, left, pivot - 1, max_depth - 1);
}
introsort_helper(arr, pivot + 1, right, max_depth - 1);
}
}

8.2 并行数组处理

8.2.1 OpenMP 并行数组操作

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
#include <omp.h>

// 并行数组求和
long long parallel_sum(const int *arr, size_t size) {
long long sum = 0;

#pragma omp parallel
{
long long local_sum = 0;

#pragma omp for
for (size_t i = 0; i < size; i++) {
local_sum += arr[i];
}

#pragma omp atomic
sum += local_sum;
}

return sum;
}

// 并行矩阵乘法
void parallel_matrix_multiply(const double *A, const double *B, double *C,
size_t n, size_t m, size_t p) {
#pragma omp parallel for collapse(2)
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < p; j++) {
double sum = 0.0;
for (size_t k = 0; k < m; k++) {
sum += A[i * m + k] * B[k * p + j];
}
C[i * p + j] = sum;
}
}
}

8.3 数组的内存优化技术

8.3.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
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
56
57
58
// 位图压缩(适用于布尔数组)
typedef struct {
uint64_t *bits;
size_t size;
size_t capacity;
} BitArray;

// 初始化位图
BitArray *bit_array_create(size_t capacity) {
BitArray *ba = malloc(sizeof(BitArray));
if (!ba) return NULL;

ba->size = 0;
ba->capacity = capacity;
ba->bits = calloc((capacity + 63) / 64, sizeof(uint64_t));
if (!ba->bits) {
free(ba);
return NULL;
}

return ba;
}

// 设置位
void bit_array_set(BitArray *ba, size_t index, bool value) {
if (index >= ba->capacity) return;

size_t word = index / 64;
size_t bit = index % 64;

if (value) {
ba->bits[word] |= (1ULL << bit);
} else {
ba->bits[word] &= ~(1ULL << bit);
}

if (index >= ba->size) {
ba->size = index + 1;
}
}

// 获取位
bool bit_array_get(const BitArray *ba, size_t index) {
if (index >= ba->capacity) return false;

size_t word = index / 64;
size_t bit = index % 64;

return (ba->bits[word] & (1ULL << bit)) != 0;
}

// 销毁位图
void bit_array_destroy(BitArray *ba) {
if (ba) {
free(ba->bits);
free(ba);
}
}

8.4 示例:高性能矩阵运算库

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// 矩阵结构体
typedef struct {
size_t rows;
size_t cols;
double *data;
bool owner;
} Matrix;

// 创建矩阵
Matrix matrix_create(size_t rows, size_t cols) {
Matrix mat;
mat.rows = rows;
mat.cols = cols;
mat.data = malloc(rows * cols * sizeof(double));
mat.owner = true;
return mat;
}

// 销毁矩阵
void matrix_destroy(Matrix *mat) {
if (mat && mat->owner && mat->data) {
free(mat->data);
mat->data = NULL;
}
}

// 矩阵乘法(使用分块优化)
Matrix matrix_multiply(const Matrix *a, const Matrix *b) {
assert(a->cols == b->rows);

Matrix result = matrix_create(a->rows, b->cols);
if (!result.data) {
return result;
}

// 初始化结果矩阵为0
for (size_t i = 0; i < result.rows * result.cols; i++) {
result.data[i] = 0.0;
}

// 分块大小
const size_t BLOCK_SIZE = 32;

// 分块矩阵乘法
for (size_t i = 0; i < a->rows; i += BLOCK_SIZE) {
for (size_t j = 0; j < b->cols; j += BLOCK_SIZE) {
for (size_t k = 0; k < a->cols; k += BLOCK_SIZE) {
// 计算块边界
size_t i_end = i + BLOCK_SIZE < a->rows ? i + BLOCK_SIZE : a->rows;
size_t j_end = j + BLOCK_SIZE < b->cols ? j + BLOCK_SIZE : b->cols;
size_t k_end = k + BLOCK_SIZE < a->cols ? k + BLOCK_SIZE : a->cols;

// 执行块乘法
for (size_t ii = i; ii < i_end; ii++) {
for (size_t kk = k; kk < k_end; kk++) {
double ak = a->data[ii * a->cols + kk];
for (size_t jj = j; jj < j_end; jj++) {
result.data[ii * result.cols + jj] +=
ak * b->data[kk * b->cols + jj];
}
}
}
}
}
}

return result;
}

// LU 分解辅助函数
int lu_decomposition(double *A, size_t n, int *pivot) {
for (size_t i = 0; i < n; i++) {
pivot[i] = i;
}

for (size_t i = 0; i < n; i++) {
double max_val = 0.0;
size_t max_idx = i;

// 寻找主元
for (size_t j = i; j < n; j++) {
double val = fabs(A[j * n + i]);
if (val > max_val) {
max_val = val;
max_idx = j;
}
}

// 如果主元为0,矩阵奇异
if (max_val == 0.0) {
return -1;
}

// 交换行
if (max_idx != i) {
for (size_t j = 0; j < n; j++) {
double temp = A[i * n + j];
A[i * n + j] = A[max_idx * n + j];
A[max_idx * n + j] = temp;
}
int temp = pivot[i];
pivot[i] = pivot[max_idx];
pivot[max_idx] = temp;
}

// 分解
for (size_t j = i + 1; j < n; j++) {
A[j * n + i] /= A[i * n + i];
for (size_t k = i + 1; k < n; k++) {
A[j * n + k] -= A[j * n + i] * A[i * n + k];
}
}
}

return 0;
}

// LU 求解
void lu_solve(const double *A, const int *pivot, double *b, size_t n) {
// 前向替换
for (size_t i = 0; i < n; i++) {
double temp = b[pivot[i]];
b[pivot[i]] = b[i];
b[i] = temp;

for (size_t j = 0; j < i; j++) {
b[i] -= A[i * n + j] * b[j];
}
}

// 后向替换
for (size_t i = n - 1; i < n; i--) {
for (size_t j = i + 1; j < n; j++) {
b[i] -= A[i * n + j] * b[j];
}
b[i] /= A[i * n + i];
}
}

9. 小结

本章深入介绍了 C 语言中数组和指针的概念、使用方法和高级应用。数组和指针是 C 语言的核心概念,掌握它们对于理解 C 语言的工作原理和编写高效的 C 程序至关重要。

9.1 关键知识点

  • 数组:连续存储的相同类型元素的集合,大小固定,零基索引
  • 指针:存储内存地址的变量,支持算术运算和解引用
  • 数组和指针的关系:数组名是指向数组第一个元素的常量指针,数组下标访问等同于指针算术
  • 动态内存管理:使用 malloc、calloc、realloc 和 free 进行内存的分配和释放
  • 高级应用:动态数组、指针数组、函数指针、回调函数等
  • 性能优化:缓存友好的数据结构和算法,内存访问模式的优化
  • 常见错误:数组越界、野指针、悬垂指针、内存泄漏等

9.2 学习建议

  • 多写代码:通过实际编程练习掌握数组和指针的使用
  • 深入理解:理解数组和指针的内存布局和工作原理
  • 注意安全:始终检查指针是否为空,避免数组越界,正确管理内存
  • 学习调试:掌握使用调试工具检测和修复内存错误的技巧
  • 阅读优秀代码:学习开源项目中数组和指针的使用技巧

数组和指针是 C 语言的强大特性,也是 C 语言的难点所在。通过本章的学习,希望读者能够深入理解数组和指针的概念,掌握它们的使用方法,编写更加高效、安全的 C 程序。

在后续章节中,我们将学习字符串处理、结构体和共用体等复合数据类型,这些内容都与数组和指针密切相关。