指针是C++语言中最强大也最容易出错的特性之一。理解指针的本质需要从计算机内存的基本工作原理开始。在计算机系统中,内存被组织为一系列连续的存储单元,每个单元都有唯一的地址标识。指针变量就是专门用来存储这些内存地址的变量。
当我们声明一个指针变量时,例如int* p1,系统会在栈上分配一块内存来存储这个指针。在64位系统中,指针变量本身固定占用8字节(32位系统为4字节),这与它指向的数据类型无关。指针变量存储的值是一个内存地址,这个地址指向堆或栈中的实际数据。
cpp复制int* p1 = new int; // p1存储在栈上,指向堆中新分配的int类型内存
这里有几个关键点需要注意:
new操作符在堆上分配内存并返回其地址解引用操作(*操作符)是使用指针访问其所指内存的关键:
cpp复制*p1 = 101; // 将101写入p1指向的内存位置
这个操作实际上包含两个步骤:
重要提示:解引用未初始化的指针或已释放的指针会导致未定义行为,可能引发程序崩溃。
new和delete是C++中动态内存管理的核心操作符。当执行new int时,会发生以下操作:
对应的delete操作则:
cpp复制int* p = new int(42); // 分配并初始化
// 使用p...
delete p; // 释放内存
p = nullptr; // 必须手动置空
内存泄漏是动态内存管理中最常见的问题之一。以下是一个典型的内存泄漏场景:
cpp复制void leakyFunction() {
int* p = new int[100];
// 忘记delete[] p;
} // p离开作用域,但分配的内存永远无法回收
防止内存泄漏的最佳实践包括:
野指针问题同样危险:
cpp复制int* p = new int;
delete p;
*p = 42; // 危险!p现在是野指针
解决方案是:
指针支持有限的算术运算,这些运算与指针指向的数据类型密切相关:
cpp复制int arr[5] = {1,2,3,4,5};
int* p = arr;
p++; // 移动sizeof(int)字节,指向arr[1]
指针运算的特点:
p++移动的距离取决于指向类型的大小C++的指针系统是强类型的,不同类型的指针不能直接互相赋值:
cpp复制int i = 10;
double* dp = &i; // 错误:类型不匹配
void指针可以存储任意类型的地址,但使用前必须显式转换:
cpp复制void* vp = &i;
int* ip = static_cast<int*>(vp);
注意:滥用void指针会破坏类型安全,应谨慎使用。
在大多数情况下,数组名会退化为指向数组首元素的指针:
cpp复制int arr[3] = {1,2,3};
int* p = arr; // 等价于 &arr[0]
但数组名与指针仍有重要区别:
多维数组的指针操作更为复杂:
cpp复制int matrix[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3] = matrix; // 指向包含3个int的数组的指针
访问元素的方式:
matrix[i][j]*(*(matrix + i) + j)*(matrix[i] + j)函数指针允许将函数作为参数传递:
cpp复制bool compare(int a, int b) { return a < b; }
void sort(int* arr, int size, bool (*comp)(int,int)) {
// 使用comp比较元素
}
sort(array, 10, compare);
现代C++更推荐使用std::function和lambda表达式。
多级指针常用于需要修改指针本身的场景:
cpp复制void allocate(int** pp) {
*pp = new int(42);
}
int* p = nullptr;
allocate(&p);
理解多级指针的关键:
int*是指向int的指针int**是指向"指向int的指针"的指针现代C++推荐使用智能指针管理资源:
cpp复制#include <memory>
std::unique_ptr<int> up(new int(10)); // 独占所有权
std::shared_ptr<int> sp = std::make_shared<int>(20); // 共享所有权
智能指针的优势:
在使用指针时,建议遵循以下检查清单:
理解对象存储位置对指针使用至关重要:
cpp复制// 栈对象 - 自动管理生命周期
int stackVar = 10;
int* pToStack = &stackVar;
// 堆对象 - 手动管理生命周期
int* heapVar = new int(20);
关键区别:
类成员指针需要特别注意所有权问题:
cpp复制class MyClass {
int* data;
public:
MyClass(int size) : data(new int[size]) {}
~MyClass() { delete[] data; }
// 需要实现拷贝构造函数和赋值操作符!
};
遵循三法则(C++11后是五法则):
指针和引用在底层实现上通常相同,但语义不同:
cpp复制void byPointer(int* p) { *p += 1; }
void byReference(int& r) { r += 1; }
性能考虑:
指针使用方式会影响程序性能:
cpp复制// 不好的缓存使用 - 随机访问
for (int i = 0; i < N; ++i) {
process(array[randomIndex[i]]);
}
// 好的缓存使用 - 顺序访问
for (int i = 0; i < N; ++i) {
process(array[i]);
}
优化建议:
现代调试器提供了强大的指针检查工具:
在代码中添加指针日志:
cpp复制#define LOG_PTR(p) \
std::cout << #p << " = " << (p) \
<< ", *" << #p << " = " << (p ? *(p) : 0) \
<< std::endl
int* ptr = new int(42);
LOG_PTR(ptr);
delete ptr;
LOG_PTR(ptr);
ptr = nullptr;
LOG_PTR(ptr);
现代C++提供了更安全的替代方案:
cpp复制// 原始指针
void oldWay(int* p) {
if (p) { /*...*/ }
}
// 现代方式
void modernWay(std::unique_ptr<int>& p) {
// 不需要空检查,所有权明确
}
void bestWay(int& r) {
// 引用保证非空
}
标准库容器通常比原始指针更安全:
cpp复制// 原始数组
int* arr = new int[100];
// 使用...
delete[] arr;
// std::vector
std::vector<int> vec(100);
// 自动管理内存
选择建议: