征服C语言指针:从基础语法到高级用法 – wiki基地

征服 C 语言指针:从基础语法到高级用法

C 语言的指针是其强大和灵活的基石,但同时也是许多初学者感到困惑的难点。掌握指针,意味着你将能更有效地管理内存、优化程序性能,并深入理解计算机底层运作机制。本文将带你从指针的基础语法出发,逐步探索其在 C 语言中的高级应用。

第一部分:指针基础——理解“地址”与“间接”

1.1 指针的本质:存储内存地址的变量

在 C 语言中,每个变量都存储在内存的某个位置,这个位置有一个唯一的编号,称为内存地址。指针,顾名其义,就是“指向”内存地址的变量。它不存储数据本身,而是存储数据所在的内存地址。

  • 声明指针变量
    使用 * 运算符来声明一个指针。例如:
    c
    int *ptr; // 声明一个指向整型变量的指针
    char *name; // 声明一个指向字符型变量的指针

    这里的 * 表示 ptr 是一个指针,它将指向一个 int 类型的数据。

  • 获取变量地址
    使用 & 运算符(取地址运算符)来获取一个变量的内存地址。
    c
    int num = 10;
    ptr = # // 将变量 num 的地址赋给指针 ptr

  • 通过指针访问数据
    使用 * 运算符(解引用运算符或间接寻址运算符)来访问指针所指向的内存地址中的数据。
    c
    printf("num 的值:%d\n", num); // 直接访问 num
    printf("num 的地址:%p\n", &num); // 打印 num 的地址
    printf("ptr 存储的地址:%p\n", ptr); // 打印 ptr 存储的地址(即 num 的地址)
    printf("通过 ptr 访问 num 的值:%d\n", *ptr); // 通过指针 ptr 访问 num 的值,输出 10

    %p 是用于打印内存地址的格式说明符。

1.2 指针的类型与大小

指针的类型(如 int *, char *)表明了它所指向的数据类型。这很重要,因为它告诉编译器在解引用时应该读取多少字节的数据。例如,int * 知道它应该读取一个 int 类型的大小(通常是 4 字节),而 char * 则读取一个 char 类型的大小(1 字节)。

尽管指向不同数据类型的指针有不同的类型,但在同一个系统架构下,所有指针变量本身的大小是相同的(通常是 4 字节或 8 字节,取决于系统是 32 位还是 64 位)。指针变量存储的都是内存地址,而内存地址的长度是固定的。

第二部分:指针与数组——密不可分的关系

2.1 数组名即为首元素地址

在 C 语言中,数组名在大多数情况下可以看作是指向其首元素的常量指针。
“`c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 数组名 arr 被视为 &arr[0]

printf(“arr[0] 的地址:%p\n”, &arr[0]);
printf(“arr 的值(首元素地址):%p\n”, arr);
printf(“p 存储的地址:%p\n”, p);

printf(“通过指针访问 arr[0]:%d\n”, p); // 访问第一个元素
printf(“通过指针访问 arr[1]:%d\n”,
(p + 1)); // 访问第二个元素
“`

2.2 指针算术

指针可以进行算术运算,但这些运算是基于其所指向数据类型的大小。
指针加减整数p + n 表示向前移动 n 个其指向类型大小的字节。
例如,如果 pint *p + 1 实际上是指向内存中下一个 int 存储位置的地址,而不是仅仅地址值加 1。
指针相减:两个指向同一数组的指针相减,结果是它们之间元素的个数。

2.3 使用指针遍历数组

“`c
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

for (int i = 0; i < 5; i++) {
printf(“%d “, (p + i)); // (p + i) 等价于 arr[i]
}
printf(“\n”);

// 另一种遍历方式
while (p < &arr[5]) { // 循环直到 p 指向数组末尾之后的一个位置
printf(“%d “, *p);
p++; // 指针移动到下一个元素
}
printf(“\n”);
“`

第三部分:指针与函数——灵活的参数传递与返回值

3.1 函数参数传递——值传递与地址传递

  • 值传递:函数接收参数的副本,对副本的修改不会影响原始变量。
    c
    void increment(int x) {
    x++; // 改变的是 x 的副本
    }
    // 调用:int a = 5; increment(a); // a 仍然是 5
  • 地址传递(通过指针):函数接收变量的地址,可以通过解引用指针来修改原始变量的值。
    c
    void increment_by_ptr(int *ptr_x) {
    (*ptr_x)++; // 解引用并修改原始变量
    }
    // 调用:int a = 5; increment_by_ptr(&a); // a 变为 6

    这是 C 语言实现函数内部修改外部变量的唯一方式(除了全局变量)。

3.2 返回指针的函数

函数也可以返回指针,通常用于返回动态分配的内存地址或字符串。
“`c
int create_array(int size) {
int
arr = (int *)malloc(size * sizeof(int));
if (arr == NULL) {
// 错误处理
return NULL;
}
for (int i = 0; i < size; i++) {
arr[i] = i * 10;
}
return arr; // 返回动态分配内存的地址
}

// 调用:
// int my_arr = create_array(3);
// // 使用 my_arr
// free(my_arr); // 记得释放内存
“`
注意:* 绝对不要返回局部变量的地址,因为局部变量在函数返回后会被销毁,其地址将变为无效的野指针。

第四部分:二级指针(指向指针的指针)

二级指针是指向另一个指针的指针。
“`c
int num = 100;
int ptr1 = # // ptr1 指向 num
int
*ptr2 = &ptr1; // ptr2 指向 ptr1

printf(“num 的值:%d\n”, num);
printf(“通过 ptr1 访问 num 的值:%d\n”, ptr1);
printf(“通过 ptr2 访问 num 的值:%d\n”,
ptr2); // 两次解引用
二级指针常用于:
1. **修改指针变量本身**:当你想在函数内部改变一个外部指针变量所指向的地址时,就需要传递该指针的地址(即二级指针)。
c
void allocate_memory(int
p, int size) {
p = (int )malloc(size * sizeof(int)); // 修改外部指针 p
// … 填充数据 …
}
// 调用:
// int
my_ptr = NULL;
// allocate_memory(&my_ptr, 5); // 传递 my_ptr 的地址
// // 使用 my_ptr
// free(my_ptr);
``
2. **处理指针数组**:指向字符串数组 (
char argv[]) 的char *` 参数是常见用法。

第五部分:函数指针——将函数作为参数传递

函数指针是指向函数的指针,它可以存储函数的地址,并通过该指针调用函数。
“`c
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a – b; }

int main() {
int (*op_func)(int, int); // 声明一个函数指针,可以指向接收两个 int 返回 int 的函数

op_func = add;
printf("Add: %d\n", op_func(5, 3)); // 通过函数指针调用 add

op_func = subtract;
printf("Subtract: %d\n", op_func(5, 3)); // 通过函数指针调用 subtract

return 0;

}
``
函数指针的常见用途:
1. **回调函数**:将函数作为参数传递给另一个函数,在特定事件发生时调用。例如,
qsort` 函数就接受一个比较函数的指针。
2. 实现状态机或命令模式:根据不同的条件调用不同的函数。

第六部分:动态内存分配——malloc, calloc, realloc, free

指针在动态内存管理中扮演核心角色。当程序运行时需要可变大小的内存时,可以使用标准库函数进行动态分配。

  • malloc(size_t size):分配 size 字节的内存,并返回指向这块内存起始位置的 void * 指针。如果分配失败,返回 NULL
  • calloc(size_t num, size_t size):分配 num * size 字节的内存,并初始化所有字节为零。
  • realloc(void *ptr, size_t new_size):重新调整已分配内存的大小。
  • free(void *ptr):释放由 malloc, calloc, realloc 分配的内存。必须及时释放动态分配的内存,否则会导致内存泄漏。

“`c
int *dynamic_arr;
int n = 5;

// 分配 5 个整型大小的内存
dynamic_arr = (int *)malloc(n * sizeof(int));
if (dynamic_arr == NULL) {
printf(“内存分配失败!\n”);
return 1;
}

// 使用动态分配的内存
for (int i = 0; i < n; i++) {
dynamic_arr[i] = (i + 1) * 10;
printf(“%d “, dynamic_arr[i]);
}
printf(“\n”);

// 重新分配为 10 个整型大小
dynamic_arr = (int *)realloc(dynamic_arr, 10 * sizeof(int));
if (dynamic_arr == NULL) {
printf(“内存重新分配失败!\n”);
return 1;
}

// 释放内存
free(dynamic_arr);
dynamic_arr = NULL; // 最佳实践:释放后将指针置为 NULL,避免野指针
“`

第七部分:指针的陷阱与注意事项

  1. 野指针:指向无效内存地址的指针。
    • 未初始化:int *p; 此时 p 是野指针。
    • 释放后未置空:free(p); p = NULL; 是好习惯。
    • 返回局部变量地址。
  2. 空指针:值为 NULL 的指针,不指向任何有效内存。解引用空指针会导致程序崩溃(段错误)。
    • 始终检查 malloc 返回值是否为 NULL
    • 解引用前检查指针是否为 NULL
  3. 内存泄漏:动态分配的内存未被释放,导致程序可用的内存越来越少。
    • 每次 malloc/calloc/realloc 后都要确保有对应的 free
  4. 越界访问:指针操作超出了其有效内存区域,可能导致数据损坏或程序崩溃。
    • 数组越界是常见原因。
    • 指针算术要小心。
  5. 常量指针与指向常量的指针
    • const int *p;p 指向的值是常量,不能通过 p 修改 *p,但 p 本身可以指向其他地址。
    • int *const p;p 是常量,必须初始化,且不能改变 p 所指向的地址,但可以通过 p 修改 *p
    • const int *const p;p*p 都是常量,都不能改变。

总结

C 语言指针是理解内存管理和底层编程的关键。从基础的声明、取地址、解引用,到与数组、函数、动态内存分配的结合,再到函数指针和二级指针等高级用法,指针贯穿于 C 语言的方方面面。虽然它带来强大的能力,但也伴随着野指针、内存泄漏和越界访问等风险。通过细致的学习和大量的实践,你将能够驾驭 C 语言指针,编写出高效、灵活且健壮的程序。征服指针,就是征服 C 语言。

滚动至顶部