指针
9. 指针
指针是 C 语言中一个强大而灵活的特性,它允许程序员直接操作内存地址,从而实现高效的数据处理和复杂的数据结构管理。掌握指针的概念、定义与使用方法,对于深入理解 C 语言及编写高效的 C 程序至关重要。
9.1 指针的概念
指针是一个变量,用于存储另一个变量的内存地址。通过指针,程序可以间接访问和修改存储在特定内存位置的数据。这种间接访问为动态内存管理、数据结构(如链表、树等)的实现提供了基础。
指针的基本概念
- 内存地址:每个变量在内存中都有一个唯一的地址,可以通过运算符
&获取。 - 指针类型:指针变量必须声明为特定的数据类型,以确保正确地解引用和操作数据。
- 解引用:通过指针访问其指向的变量的值,使用运算符
*。
指针的作用
- 动态内存管理:通过指针动态分配和释放内存。
- 高效数组和字符串操作:指针可以高效地遍历和操作数组和字符串。
- 实现复杂数据结构:如链表、树、图等。
- 函数参数传递:通过指针实现传址调用,提高函数的灵活性和效率。
示例
#include <stdio.h>
int main() {
int var = 20; // 声明一个整型变量
int *ptr; // 声明一个指向整型的指针变量
ptr = &var; // 将变量var的地址赋给指针ptr
printf("var的值: %d\n", var); // 输出 var的值
printf("ptr存储的地址: %p\n", ptr); // 输出 ptr存储的地址
printf("*ptr的值: %d\n", *ptr); // 输出指针ptr指向的值
return 0;
}
输出(具体地址可能因系统而异):
var的值: 20
ptr存储的地址: 0x7ffee4b2c89c
*ptr的值: 20
详细解释:
int *ptr;声明了一个指向整型的指针变量ptr。ptr = &var;将变量var的内存地址赋给指针ptr。*ptr表示指针ptr指向的变量的值,即var的值。
9.2 指针变量的定义与使用
指针变量是存储内存地址的变量。正确地定义和使用指针变量是理解和掌握指针的关键。
指针变量的定义
语法:
数据类型 *指针变量名;
- 数据类型:指针所指向的数据类型,如
int、float、char等。 \*符号:表示这是一个指针变量。- 指针变量名:指针的名称。
示例:
#include <stdio.h>
int main() {
int a = 10;
float b = 5.5;
char c = 'A';
int *ptrA; // 指向整型的指针
float *ptrB; // 指向浮点型的指针
char *ptrC; // 指向字符型的指针
ptrA = &a; // ptrA存储变量a的地址
ptrB = &b; // ptrB存储变量b的地址
ptrC = &c; // ptrC存储变量c的地址
// 输出指针的值(内存地址)
printf("ptrA 指向的地址: %p\n", ptrA);
printf("ptrB 指向的地址: %p\n", ptrB);
printf("ptrC 指向的地址: %p\n", ptrC);
// 通过指针访问变量的值
printf("*ptrA = %d\n", *ptrA);
printf("*ptrB = %.2f\n", *ptrB);
printf("*ptrC = %c\n", *ptrC);
return 0;
}
输出(具体地址可能因系统而异):
ptrA 指向的地址: 0x7ffee4b2c89c
ptrB 指向的地址: 0x7ffee4b2c8a0
ptrC 指向的地址: 0x7ffee4b2c8a1
*ptrA = 10
*ptrB = 5.50
*ptrC = A
指针的解引用
解引用是指通过指针访问其指向的变量的值,使用运算符*。
示例:
#include <stdio.h>
int main() {
int num = 25;
int *ptr = # // ptr指向num的地址
printf("num的值: %d\n", num); // 输出: 25
printf("*ptr的值: %d\n", *ptr); // 输出: 25
*ptr = 30; // 通过指针修改num的值
printf("修改后的num的值: %d\n", num); // 输出: 30
return 0;
}
输出:
num的值: 25
*ptr的值: 25
修改后的num的值: 30
详细解释:
*ptr = 30;通过指针ptr修改了变量num的值。- 解引用操作符
*用于访问指针所指向的内存位置。
指针的类型
指针类型决定了指针解引用后访问的数据类型。不同类型的指针占用的内存大小可能不同,但在大多数现代系统中,所有指针类型通常占用相同的内存空间(如 64 位系统中的 8 字节)。
示例:
#include <stdio.h>
int main() {
int *ptrInt;
double *ptrDouble;
char *ptrChar;
printf("int指针的大小: %zu 字节\n", sizeof(ptrInt)); // 输出: 8
printf("double指针的大小: %zu 字节\n", sizeof(ptrDouble)); // 输出: 8
printf("char指针的大小: %zu 字节\n", sizeof(ptrChar)); // 输出: 8
return 0;
}
输出(在 64 位系统中):
int指针的大小: 8 字节
double指针的大小: 8 字节
char指针的大小: 8 字节
指针的运算
指针支持有限的运算操作,包括加减整数和指针的比较。指针间的加减操作涉及到指针类型的大小。
示例:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // 指向数组的第一个元素
printf("ptr = %p\n", ptr);
printf("ptr + 1 = %p\n", ptr + 1); // 指向下一个整数
printf("*ptr = %d\n", *ptr);
printf("*(ptr + 1) = %d\n", *(ptr + 1));
return 0;
}
输出(具体地址可能因系统而异):
ptr = 0x7ffee4b2c890
ptr + 1 = 0x7ffee4b2c894
*ptr = 10
*(ptr + 1) = 20
详细解释:
ptr + 1指向数组的下一个元素,地址增加了sizeof(int)(通常为 4 字节)。- 指针运算考虑了数据类型的大小,确保指向正确的内存位置。
9.3 指针与数组的关系
指针和数组在 C 语言中有着密切的关系,理解它们之间的联系和区别对于高效地操作数据结构至关重要。
数组名与指针
在大多数情况下,数组名代表数组的首地址,可以被视为指向数组第一个元素的指针。
示例:
#include <stdio.h>
int main() {
int arr[3] = {100, 200, 300};
int *ptr = arr; // 等同于 int *ptr = &arr[0];
// 使用指针访问数组元素
printf("arr[0] = %d, *ptr = %d\n", arr[0], *ptr); // 输出: 100, 100
printf("arr[1] = %d, *(ptr + 1) = %d\n", arr[1], *(ptr + 1)); // 输出: 200, 200
printf("arr[2] = %d, *(ptr + 2) = %d\n", arr[2], *(ptr + 2)); // 输出: 300, 300
return 0;
}
输出:
arr[0] = 100, *ptr = 100
arr[1] = 200, *(ptr + 1) = 200
arr[2] = 300, *(ptr + 2) = 300
详细解释:
- 数组名
arr在表达式中通常被解释为指向数组第一个元素的指针。 ptr + 1指向数组的第二个元素,*(ptr + 1)即为arr[1]。
指针与数组的区别
尽管指针和数组在很多情况下表现相似,但它们在内存分配、可变性和运算方式上有显著区别。
| 特性 | 数组 | 指针 |
|---|---|---|
| 内存分配 | 编译时分配固定大小的内存空间 | 可以指向动态分配或不同的内存区域 |
| 大小 | sizeof运算符返回整个数组的大小 | sizeof运算符返回指针本身的大小 |
| 可变性 | 数组名不可修改,始终指向同一内存位置 | 指针可以修改指向不同的内存位置 |
| 运算方式 | 支持部分指针运算,如&arr[0] | 支持更多指针运算,如算术和逻辑运算 |
示例比较:
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
int *ptr = arr;
printf("sizeof(arr) = %zu 字节\n", sizeof(arr)); // 输出数组的总大小
printf("sizeof(ptr) = %zu 字节\n", sizeof(ptr)); // 输出指针的大小
// 尝试修改数组名(错误)
// arr = ptr; // 错误:数组名是不可修改的左值
// 修改指针指向
ptr = &arr[1];
printf("ptr现在指向的值: %d\n", *ptr); // 输出: 2
return 0;
}
输出(在 64 位系统中):
sizeof(arr) = 12 字节
sizeof(ptr) = 8 字节
ptr现在指向的值: 2
注意事项:
- 数组名不可赋值:数组名是常量指针,不能通过赋值改变其指向。
- 指针的灵活性:指针可以指向不同类型的内存区域,如动态分配的内存、其他变量等。
指针作为函数参数传递数组
将数组作为函数参数时,实际上是将数组名(指针)传递给函数,这使得函数能够访问和修改原数组的内容。
示例:
#include <stdio.h>
// 函数定义:打印数组元素
void printArray(int *arr, int size) {
for(int i = 0; i < size; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
}
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int size = sizeof(numbers) / sizeof(numbers[0]);
// 调用printArray函数
printArray(numbers, size);
return 0;
}
输出:
arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50
详细解释:
printArray函数接受一个指向整型的指针arr和一个整型参数size。- 在
main函数中,将数组numbers传递给printArray,实际上是传递了数组的首地址。
指针与数组的高级用法
- 指针数组:数组的每个元素都是一个指针。
- 多维数组与指针的关系:多维数组可以通过指针进行访问和操作。
示例:指针数组
#include <stdio.h>
int main() {
char *fruits[] = {"Apple", "Banana", "Cherry"};
int size = sizeof(fruits) / sizeof(fruits[0]);
for(int i = 0; i < size; i++) {
printf("fruits[%d] = %s\n", i, fruits[i]);
}
return 0;
}
输出:
fruits[0] = Apple
fruits[1] = Banana
fruits[2] = Cherry
详细解释:
char *fruits[]声明了一个指针数组,每个元素都是指向字符的指针(即字符串)。- 通过指针数组,可以方便地管理多个字符串。
9.4 函数指针与指针函数
指针在 C 语言中不仅可以指向数据,还可以指向函数。理解函数指针和指针函数的区别和用法,是实现回调函数、动态函数调用等高级编程技巧的基础。
函数指针
函数指针是一个指针变量,用于存储函数的地址。通过函数指针,可以间接调用函数,实现更灵活的函数调用机制。
函数指针的定义
语法:
返回类型 (*指针变量名)(参数类型1, 参数类型2, ...);
示例:
#include <stdio.h>
// 函数定义:求和
int add(int a, int b) {
return a + b;
}
// 函数定义:求差
int subtract(int a, int b) {
return a - b;
}
int main() {
// 定义函数指针,指向返回int,接受两个int参数的函数
int (*funcPtr)(int, int);
// 将add函数的地址赋给函数指针
funcPtr = add;
printf("add(10, 5) = %d\n", funcPtr(10, 5)); // 输出: 15
// 将subtract函数的地址赋给函数指针
funcPtr = subtract;
printf("subtract(10, 5) = %d\n", funcPtr(10, 5)); // 输出: 5
return 0;
}
输出:
add(10, 5) = 15
subtract(10, 5) = 5
详细解释:
int (*funcPtr)(int, int);声明了一个函数指针funcPtr,指向返回int,接受两个int参数的函数。- 通过将不同函数的地址赋给
funcPtr,可以实现动态函数调用。
函数指针的应用
- 回调函数:在某些库函数中,用户可以传递自定义函数作为回调,实现自定义操作。
- 动态函数调用:根据程序运行时的条件,动态选择调用不同的函数。
- 实现多态:通过函数指针,可以实现类似面向对象编程中的多态性。
示例:回调函数
#include <stdio.h>
// 函数定义:执行操作
void executeOperation(int a, int b, int (*operation)(int, int)) {
int result = operation(a, b);
printf("操作结果: %d\n", result);
}
// 函数定义:乘法
int multiply(int a, int b) {
return a * b;
}
int main() {
int x = 6, y = 7;
// 使用函数指针作为回调函数
executeOperation(x, y, multiply); // 输出: 操作结果: 42
executeOperation(x, y, add); // 输出: 操作结果: 13
return 0;
}
// 之前定义的add函数
int add(int a, int b) {
return a + b;
}
输出:
操作结果: 42
操作结果: 13
详细解释:
executeOperation函数接受两个整数和一个函数指针作为参数。- 通过传递不同的函数(如
multiply和add),可以动态执行不同的操作。
指针函数
指针函数是指返回指针的函数。它们在返回指向变量、数组或动态分配内存的指针时非常有用。
指针函数的定义
语法:
返回类型 *函数名(参数列表) {
// 函数体
}
示例:
#include <stdio.h>
// 函数定义:返回指向整型变量的指针
int* getPointer(int *ptr) {
return ptr;
}
int main() {
int var = 50;
int *ptrVar = &var;
// 调用指针函数
int *returnedPtr = getPointer(ptrVar);
printf("var = %d\n", *returnedPtr); // 输出: var = 50
return 0;
}
输出:
var = 50
详细解释:
int* getPointer(int *ptr)定义了一个返回指向整型的指针函数。- 函数返回传入的指针,使得调用者可以访问和修改原始变量。
指针函数的应用
- 访问动态分配的内存:通过指针函数返回动态分配的内存地址,便于管理内存。
- 操作复杂数据结构:返回指向结构体或数组的指针,便于访问和修改数据结构中的元素。
- 实现链式调用:通过返回指针,可以实现多个函数调用的链式操作。
示例:返回动态分配的数组
#include <stdio.h>
#include <stdlib.h>
// 函数定义:动态分配并返回整型数组的指针
int* createArray(int size) {
int *arr = (int*)malloc(size * sizeof(int));
if(arr == NULL) {
printf("内存分配失败\n");
exit(1);
}
// 初始化数组
for(int i = 0; i < size; i++) {
arr[i] = i * 10;
}
return arr;
}
int main() {
int size = 5;
int *myArray = createArray(size);
// 输出数组元素
for(int i = 0; i < size; i++) {
printf("myArray[%d] = %d\n", i, myArray[i]);
}
// 释放动态分配的内存
free(myArray);
return 0;
}
输出:
myArray[0] = 0
myArray[1] = 10
myArray[2] = 20
myArray[3] = 30
myArray[4] = 40
详细解释:
createArray函数动态分配一个整型数组,并返回其指针。- 在
main函数中,调用createArray获取数组指针,并使用该指针访问数组元素。
注意事项
- 区别指针函数与函数指针:指针函数是返回指针的函数,而函数指针是存储函数地址的指针变量。两者语法不同,易混淆。
- 避免悬挂指针:确保指针函数返回的指针指向有效的内存,避免返回指向局部变量的指针,因为局部变量在函数返回后会被销毁。
9.5 动态内存分配与指针
动态内存分配允许在程序运行时根据需要分配和释放内存空间。通过指针,程序可以高效地管理动态内存,适应不同的数据需求。C 语言提供了一系列标准库函数用于动态内存管理,主要包括malloc、calloc、realloc和free。
9.5.1 malloc
malloc(memory allocation)函数用于在堆内存中分配指定字节数的连续内存空间,返回指向该内存块的指针。分配的内存内容未初始化,可能包含随机值。
原型:
void* malloc(size_t size);
- 参数:要分配的内存字节数。
- 返回值:指向分配内存的指针,如果分配失败,返回
NULL。
示例与详细说明:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
int n, i;
printf("请输入要分配的整数个数: ");
scanf("%d", &n);
// 使用malloc分配内存
ptr = (int*)malloc(n * sizeof(int));
if(ptr == NULL) {
printf("内存分配失败\n");
return 1;
}
// 初始化数组
for(i = 0; i < n; i++) {
ptr[i] = i + 1;
}
// 输出数组元素
printf("分配的数组元素是: ");
for(i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
// 释放内存
free(ptr);
return 0;
}
示例输出:
请输入要分配的整数个数: 5
分配的数组元素是: 1 2 3 4 5
详细解释:
malloc(n * sizeof(int))分配了n个整型所需的内存空间。- 通过指针
ptr访问和初始化分配的内存。 - 使用
free(ptr)释放动态分配的内存,避免内存泄漏。
9.5.2 calloc
calloc(contiguous allocation)函数用于在堆内存中分配指定数量的元素,每个元素具有相同的大小,并将分配的内存初始化为零。
原型:
void* calloc(size_t num, size_t size);
-
参数
:
num:要分配的元素数量。size:每个元素的字节大小。
-
返回值:指向分配内存的指针,如果分配失败,返回
NULL。
示例与详细说明:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
int n, i;
printf("请输入要分配的整数个数: ");
scanf("%d", &n);
// 使用calloc分配内存,并初始化为0
ptr = (int*)calloc(n, sizeof(int));
if(ptr == NULL) {
printf("内存分配失败\n");
return 1;
}
// 输出数组元素
printf("分配并初始化的数组元素是: ");
for(i = 0; i < n; i++) {
printf("%d ", ptr[i]); // 所有元素初始化为0
}
printf("\n");
// 释放内存
free(ptr);
return 0;
}
示例输出:
请输入要分配的整数个数: 5
分配并初始化的数组元素是: 0 0 0 0 0
详细解释:
calloc(n, sizeof(int))分配了n个整型元素的内存,并将所有字节初始化为零。- 适用于需要初始化为零的内存分配场景。
9.5.3 realloc
realloc(reallocate)函数用于调整之前分配的内存块的大小,可以增加或减少内存的分配量。如果需要扩大内存,realloc可能会在内存中移动块的位置,以适应更大的空间。
原型:
void* realloc(void *ptr, size_t new_size);
-
参数
:
ptr:指向之前分配的内存块的指针(通过malloc、calloc或realloc获得)。new_size:新的内存块大小,以字节为单位。
-
返回值:指向重新分配的内存块的指针,如果分配失败,返回
NULL,原内存块保持不变。
示例与详细说明:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
int n, new_n, i;
printf("请输入初始要分配的整数个数: ");
scanf("%d", &n);
// 使用malloc分配内存
ptr = (int*)malloc(n * sizeof(int));
if(ptr == NULL) {
printf("内存分配失败\n");
return 1;
}
// 初始化数组
for(i = 0; i < n; i++) {
ptr[i] = i + 1;
}
// 输出初始数组
printf("初始数组元素: ");
for(i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
printf("请输入新的要分配的整数个数: ");
scanf("%d", &new_n);
// 使用realloc调整内存大小
ptr = (int*)realloc(ptr, new_n * sizeof(int));
if(ptr == NULL) {
printf("内存重新分配失败\n");
return 1;
}
// 初始化新增的元素
for(i = n; i < new_n; i++) {
ptr[i] = i + 1;
}
// 输出重新分配后的数组
printf("重新分配后的数组元素: ");
for(i = 0; i < new_n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
// 释放内存
free(ptr);
return 0;
}
示例输出:
请输入初始要分配的整数个数: 3
初始数组元素: 1 2 3
请输入新的要分配的整数个数: 5
重新分配后的数组元素: 1 2 3 4 5
详细解释:
realloc(ptr, new_n * sizeof(int))调整了内存块的大小,使其能够容纳new_n个整型元素。- 如果扩大了内存空间,新的元素需要手动初始化。
- 使用
realloc时,务必检查返回值是否为NULL,以避免内存泄漏。
9.5.4 free
**free**函数用于释放之前通过malloc、calloc或realloc分配的内存,防止内存泄漏。
原型:
void free(void *ptr);
- 参数:指向要释放的内存块的指针。
- 返回值:无。
示例与详细说明:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
int n, i;
printf("请输入要分配的整数个数: ");
scanf("%d", &n);
// 使用malloc分配内存
ptr = (int*)malloc(n * sizeof(int));
if(ptr == NULL) {
printf("内存分配失败\n");
return 1;
}
// 初始化数组
for(i = 0; i < n; i++) {
ptr[i] = i * 2;
}
// 输出数组元素
printf("数组元素: ");
for(i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
// 释放内存
free(ptr);
printf("内存已释放。\n");
// 访问已释放的内存(未定义行为)
// printf("访问ptr[0] = %d\n", ptr[0]); // 错误:ptr指向的内存已被释放
return 0;
}
示例输出:
请输入要分配的整数个数: 4
数组元素: 0 2 4 6
内存已释放。
详细解释:
free(ptr);释放了之前分配的内存块。- 释放后,指针
ptr仍然指向原内存地址,但该内存已被系统回收,不能再访问或修改。
动态内存分配的注意事项
- 检查分配是否成功:始终检查
malloc、calloc和realloc的返回值,确保内存分配成功。 - 避免内存泄漏:每次动态分配的内存都应在不再需要时通过
free释放。 - 避免重复释放:不要多次释放同一内存块,这会导致未定义行为。
- 悬挂指针:释放内存后,应将指针设置为
NULL,防止指针成为悬挂指针。
示例:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
if(ptr == NULL) {
printf("内存分配失败\n");
return 1;
}
*ptr = 100;
printf("ptr的值: %d\n", *ptr); // 输出: 100
free(ptr); // 释放内存
ptr = NULL; // 将指针设置为NULL
// 尝试访问ptr(安全)
if(ptr != NULL) {
printf("ptr的值: %d\n", *ptr);
} else {
printf("ptr 已被释放并设置为 NULL\n"); // 输出此行
}
return 0;
}
输出:
ptr的值: 100
ptr 已被释放并设置为 NULL
详细解释:
- 释放内存后,将指针
ptr设置为NULL,避免指针悬挂。 - 在访问指针之前,检查指针是否为
NULL,确保安全。
9.6 总结
指针是 C 语言中一个强大而灵活的工具,通过理解指针的基本概念、定义与使用方法,可以实现高效的内存管理和复杂的数据结构操作。本节详细介绍了指针的概念、指针变量的定义与使用、指针与数组的关系、函数指针与指针函数,以及动态内存分配与指针相关的操作函数。以下是本节的关键点:
- 指针的概念:理解指针用于存储变量的内存地址,及其在程序中的重要作用。
- 指针变量的定义与使用:掌握如何声明指针变量,如何通过指针访问和修改数据。
- 指针与数组的关系:了解数组名与指针的关系,以及如何通过指针操作数组元素。
- 函数指针与指针函数:区分函数指针和指针函数,掌握函数指针的应用场景。
- 动态内存分配与指针:掌握
malloc、calloc、realloc和free等动态内存管理函数,确保高效且安全地管理内存。