第07章 指针
7.1 指针基础
7.1.1 什么是指针
指针: 存储内存地址的变量。
int a = 10; // 普通变量,存储值
int *p = &a; // 指针变量,存储地址
内存示意图:
变量a: 地址0x1000 → 值10
指针p: 地址0x2000 → 值0x1000 (a的地址)
7.1.2 指针的定义
语法:
数据类型 *指针名;
示例:
int *p1; // 整型指针
float *p2; // 浮点型指针
char *p3; // 字符指针
double *p4; // 双精度指针
⚠️ 注意: *属于变量名,不是类型的一部分
int* p1, p2; // p1是指针,p2是int(容易误解!)
int *p1, *p2; // 推荐:p1和p2都是指针
7.1.3 指针的基本操作
取地址运算符 &:
int a = 10;
int *p = &a; // p指向a的地址
**解引用运算符 ***:
int a = 10;
int *p = &a;
printf("%d\n", *p); // 通过指针访问a的值: 10
*p = 20; // 通过指针修改a的值
printf("%d\n", a); // a变成了20
完整示例:
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
printf("a的值: %d\n", a); // 10
printf("a的地址: %p\n", (void*)&a); // 0x7fff...
printf("p的值(a的地址): %p\n", (void*)p); // 0x7fff...
printf("*p的值: %d\n", *p); // 10
*p = 20; // 通过指针修改a
printf("修改后a的值: %d\n", a); // 20
return 0;
}
7.1.4 指针的大小
指针的大小取决于系统架构:
#include <stdio.h>
int main(void)
{
printf("指针大小:\n");
printf("int*: %lu 字节\n", sizeof(int*));
printf("char*: %lu 字节\n", sizeof(char*));
printf("double*: %lu 字节\n", sizeof(double*));
return 0;
}
输出(64位系统):
int*: 8 字节
char*: 8 字节
double*: 8 字节
⚠️ 重点: 所有类型的指针大小相同,取决于系统(32位→4字节,64位→8字节)
7.1.5 空指针
NULL指针: 不指向任何有效内存地址的指针。
#include <stdio.h>
int main(void)
{
int *p = NULL; // 初始化为空指针
if (p == NULL)
{
printf("p是空指针\n");
}
// *p = 10; // 错误!空指针不能解引用,会崩溃
return 0;
}
⚠️ 重要: 使用指针前务必检查是否为NULL!
7.1.6 野指针
野指针: 未初始化或指向无效内存的指针。
int *p; // 野指针!未初始化,指向随机地址
*p = 10; // 危险!可能崩溃
int *q = NULL; // 正确:初始化为NULL
避免野指针:
- 声明时初始化
- 释放后置为NULL
- 使用前检查是否为NULL
int *p = NULL; // 初始化
if (p != NULL) // 使用前检查
{
*p = 10;
}
7.2 指针与数组
7.2.1 数组名与指针
数组名是指向数组首元素的常量指针:
#include <stdio.h>
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 int *p = &arr[0]
printf("arr: %p\n", (void*)arr);
printf("&arr[0]: %p\n", (void*)&arr[0]);
printf("p: %p\n", (void*)p);
return 0;
}
7.2.2 指针访问数组
方式1: 数组下标
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", arr[2]); // 3
方式2: 指针解引用
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", *p); // 1 (第一个元素)
printf("%d\n", *(p+1)); // 2 (第二个元素)
printf("%d\n", *(p+2)); // 3 (第三个元素)
方式3: 指针下标
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", p[0]); // 1
printf("%d\n", p[1]); // 2
printf("%d\n", p[2]); // 3
7.2.3 指针算术运算
指针加法:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
p + 0 // 指向arr[0]
p + 1 // 指向arr[1]
p + 2 // 指向arr[2]
⚠️ 重要: 指针+1移动的字节数 = sizeof(指针类型)
#include <stdio.h>
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("p: %p\n", (void*)p);
printf("p+1: %p\n", (void*)(p+1)); // 地址增加4字节(int大小)
printf("p+2: %p\n", (void*)(p+2)); // 地址增加8字节
return 0;
}
指针减法:
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[4];
int *p2 = &arr[1];
printf("%ld\n", p1 - p2); // 3 (相差3个int元素)
指针比较:
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[1];
int *p2 = &arr[3];
if (p1 < p2)
{
printf("p1在p2前面\n");
}
7.2.4 指针遍历数组
方式1: 指针递增
#include <stdio.h>
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++)
{
printf("%d ", *p);
p++; // 指针移动到下一个元素
}
printf("\n");
return 0;
}
方式2: 指针偏移
#include <stdio.h>
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
return 0;
}
7.2.5 数组名与指针的区别
| 特性 | 数组名 | 指针变量 |
|---|---|---|
| 本质 | 常量 | 变量 |
| 可否赋值 | 否 | 是 |
| sizeof | 整个数组大小 | 指针大小 |
| &运算 | 整个数组地址 | 指针变量地址 |
示例:
#include <stdio.h>
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("sizeof(arr): %lu\n", sizeof(arr)); // 20 (5*4)
printf("sizeof(p): %lu\n", sizeof(p)); // 8 (指针大小)
// arr = arr + 1; // 错误!数组名不能赋值
p = p + 1; // 正确!指针可以修改
return 0;
}
7.3 指针与字符串
7.3.1 字符串的两种表示
方式1: 字符数组
char str[] = "Hello"; // 可修改
str[0] = 'h'; // 正确
方式2: 字符指针
char *str = "Hello"; // 指向字符串常量,不可修改
// str[0] = 'h'; // 错误!字符串常量不可修改
⚠️ 区别:
char str1[] = "Hello"; // 栈上分配,可修改
char *str2 = "Hello"; // 常量区,不可修改
7.3.2 字符指针操作字符串
#include <stdio.h>
int main(void)
{
char *str = "Hello World";
char *p = str;
// 遍历字符串
while (*p != '\0')
{
printf("%c", *p);
p++;
}
printf("\n");
return 0;
}
7.3.3 字符串数组
#include <stdio.h>
int main(void)
{
char *fruits[] = {
"Apple",
"Banana",
"Orange",
"Grape"
};
int n = sizeof(fruits) / sizeof(fruits[0]);
for (int i = 0; i < n; i++)
{
printf("%s\n", fruits[i]);
}
return 0;
}
7.3.4 字符串处理示例
示例: 实现strlen
#include <stdio.h>
size_t myStrlen(const char *str)
{
const char *p = str;
while (*p != '\0')
{
p++;
}
return p - str;
}
int main(void)
{
char *str = "Hello";
printf("长度: %lu\n", myStrlen(str));
return 0;
}
示例: 实现strcpy
#include <stdio.h>
char* myStrcpy(char *dest, const char *src)
{
char *ret = dest;
while ((*dest++ = *src++) != '\0')
{
// 循环体为空
}
return ret;
}
int main(void)
{
char src[] = "Hello";
char dest[20];
myStrcpy(dest, src);
printf("dest: %s\n", dest);
return 0;
}
7.4 数组指针与指针数组
7.4.1 指针数组
定义: 数组的每个元素都是指针
语法:
数据类型 *数组名[元素个数];
示例:
#include <stdio.h>
int main(void)
{
int a = 10, b = 20, c = 30;
int *arr[3] = {&a, &b, &c}; // 指针数组
for (int i = 0; i < 3; i++)
{
printf("arr[%d] = %d\n", i, *arr[i]);
}
return 0;
}
输出:
arr[0] = 10
arr[1] = 20
arr[2] = 30
7.4.2 数组指针
定义: 指向数组的指针
语法:
数据类型 (*指针名)[元素个数];
示例:
#include <stdio.h>
int main(void)
{
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // 数组指针,指向整个数组
for (int i = 0; i < 5; i++)
{
printf("%d ", (*p)[i]);
}
printf("\n");
return 0;
}
7.4.3 区分指针数组与数组指针
int *arr[5]; // 指针数组:5个int*元素的数组
int (*p)[5]; // 数组指针:指向包含5个int的数组
// 记忆方法:先看[]还是*
// arr[5] → 数组
// (*p) → 指针
优先级: [] > *
int *arr[5]; // arr先与[]结合 → 数组
// 再与*结合 → 元素是指针
int (*p)[5]; // ()改变优先级
// p先与*结合 → 指针
// 再与[]结合 → 指向数组
7.4.4 数组指针的应用
应用: 二维数组
#include <stdio.h>
int main(void)
{
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int (*p)[4] = matrix; // 指向每一行(每行4个int)
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%3d ", p[i][j]);
// 等价于: (*(p+i))[j]
// 等价于: *(*(p+i)+j)
}
printf("\n");
}
return 0;
}
7.5 const与指针
7.5.1 四种组合
1. 指向常量的指针:
const int *p; // 指针指向的内容不可修改
int const *p; // 同上
int a = 10;
const int *p = &a;
// *p = 20; // 错误!不能通过p修改a
a = 20; // 正确!可以直接修改a
p = &b; // 正确!指针本身可以修改
2. 常量指针:
int * const p = &a; // 指针本身不可修改,内容可以修改
*p = 20; // 正确!可以修改内容
// p = &b; // 错误!指针不能修改
3. 指向常量的常量指针:
const int * const p = &a; // 指针和内容都不可修改
// *p = 20; // 错误!
// p = &b; // 错误!
4. 普通指针:
int *p = &a; // 指针和内容都可以修改
*p = 20; // 正确!
p = &b; // 正确!
7.5.2 记忆技巧
规则: const修饰的是其右边的内容
const int *p; // const修饰int → *p不可修改
int * const p; // const修饰p → p不可修改
const int * const p; // 两者都不可修改
7.5.3 const指针的应用
保护函数参数:
#include <stdio.h>
#include <string.h>
// 不会修改str的内容
size_t myStrlen(const char *str)
{
const char *p = str;
while (*p != '\0')
{
p++;
}
return p - str;
}
int main(void)
{
char str[] = "Hello";
printf("长度: %lu\n", myStrlen(str));
return 0;
}
7.6 多级指针
7.6.1 二级指针
定义: 指向指针的指针
int a = 10;
int *p = &a; // 一级指针
int **pp = &p; // 二级指针
内存示意图:
a: 地址0x1000 → 值10
p: 地址0x2000 → 值0x1000 (a的地址)
pp: 地址0x3000 → 值0x2000 (p的地址)
访问:
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
int **pp = &p;
printf("a = %d\n", a); // 10
printf("*p = %d\n", *p); // 10
printf("**pp = %d\n", **pp); // 10
**pp = 20; // 通过二级指针修改a
printf("a = %d\n", a); // 20
return 0;
}
7.6.2 二级指针的应用
应用1: 指针数组
#include <stdio.h>
int main(void)
{
char *fruits[] = {"Apple", "Banana", "Orange"};
char **p = fruits; // 二级指针指向指针数组
for (int i = 0; i < 3; i++)
{
printf("%s\n", p[i]);
// 等价于: *(p + i)
}
return 0;
}
应用2: main函数参数
#include <stdio.h>
int main(int argc, char **argv) // 或 char *argv[]
{
printf("参数个数: %d\n", argc);
for (int i = 0; i < argc; i++)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
执行:
$ ./test hello world
参数个数: 3
argv[0] = ./test
argv[1] = hello
argv[2] = world
7.7 函数指针
7.7.1 函数指针的定义
语法:
返回类型 (*指针名)(参数类型列表);
示例:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int (*func)(int, int) = add; // 函数指针
int result = func(3, 5); // 通过函数指针调用
printf("3 + 5 = %d\n", result);
return 0;
}
7.7.2 函数指针数组
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int main(void)
{
int (*funcs[])(int, int) = {add, sub, mul, divide};
char *ops[] = {"+", "-", "*", "/"};
int a = 10, b = 5;
for (int i = 0; i < 4; i++)
{
printf("%d %s %d = %d\n", a, ops[i], b, funcs[i](a, b));
}
return 0;
}
输出:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
7.7.3 回调函数
#include <stdio.h>
void forEach(int arr[], int n, void (*callback)(int))
{
for (int i = 0; i < n; i++)
{
callback(arr[i]);
}
}
void printElement(int x)
{
printf("%d ", x);
}
void printSquare(int x)
{
printf("%d ", x * x);
}
int main(void)
{
int arr[] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("元素: ");
forEach(arr, n, printElement);
printf("\n");
printf("平方: ");
forEach(arr, n, printSquare);
printf("\n");
return 0;
}
7.8 实践练习
练习1: 指针交换两个变量
#include <stdio.h>
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main(void)
{
int x = 10, y = 20;
printf("交换前: x=%d, y=%d\n", x, y);
swap(&x, &y);
printf("交换后: x=%d, y=%d\n", x, y);
return 0;
}
练习2: 使用指针反转字符串
#include <stdio.h>
#include <string.h>
void reverseString(char *str)
{
char *left = str;
char *right = str + strlen(str) - 1;
while (left < right)
{
char temp = *left;
*left = *right;
*right = temp;
left++;
right--;
}
}
int main(void)
{
char str[] = "Hello";
printf("原字符串: %s\n", str);
reverseString(str);
printf("反转后: %s\n", str);
return 0;
}
练习3: 使用函数指针实现计算器
#include <stdio.h>
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }
double calculate(double a, double b, double (*op)(double, double))
{
return op(a, b);
}
int main(void)
{
double a = 10.0, b = 5.0;
printf("%.2f + %.2f = %.2f\n", a, b, calculate(a, b, add));
printf("%.2f - %.2f = %.2f\n", a, b, calculate(a, b, sub));
printf("%.2f * %.2f = %.2f\n", a, b, calculate(a, b, mul));
printf("%.2f / %.2f = %.2f\n", a, b, calculate(a, b, div));
return 0;
}
7.9 常见错误
错误1: 野指针
int *p; // 未初始化
*p = 10; // 崩溃!
// 正确
int *p = NULL;
if (p != NULL) {
*p = 10;
}
错误2: 空指针解引用
int *p = NULL;
*p = 10; // 崩溃!
错误3: 指针悬挂
int *p;
{
int a = 10;
p = &a;
} // a的生命周期结束
*p = 20; // 危险!p指向无效内存
错误4: 返回局部变量地址
int* func()
{
int a = 10;
return &a; // 错误!返回局部变量地址
}
错误5: 忘记分配内存
char *str;
strcpy(str, "Hello"); // 错误!str未分配内存
// 正确
char str[20];
strcpy(str, "Hello");
7.10 小结
本章学习了:
指针的概念与基本操作(&、*) 指针与数组的关系 指针算术运算 指针与字符串 数组指针与指针数组 const与指针的四种组合 多级指针(二级指针) 函数指针与回调函数
重点提示:
- 指针使用前必须初始化
- 使用前检查是否为NULL
- 避免野指针和悬挂指针
- 理解指针与数组的关系
- 掌握const指针的各种形式
- 指针是C语言的精华,也是难点,需要大量练习
记忆口诀:
- 指针数组:
int *arr[5]→ 数组,元素是指针 - 数组指针:
int (*p)[5]→ 指针,指向数组 - 函数指针:
int (*func)(int, int)→ 指针,指向函数