C++ 指针

1. 什么是指针

指针是一个变量,它存储的是内存地址,而不是普通的数据值。通过地址,我们可以间接访问另一块内存中存储的数据。

类比

  • 普通变量:好比一间房子,里面直接住着人(数据)。
  • 指针变量:好比一张纸条,上面写着房子的地址(内存地址),通过地址可以找到那间房子。

为什么需要指针?

  • 实现函数内修改外部变量(引用传递)。
  • 动态分配内存(程序运行时申请内存)。
  • 高效处理数组和字符串。
  • 构建链表、树等复杂数据结构。

2. 指针的声明与初始化

声明语法

数据类型* 指针变量名;

数据类型 表示指针指向的变量的类型,* 表示这是一个指针。

int* p;        // 指向 int 的指针
double* q;     // 指向 double 的指针
char* r;       // 指向 char 的指针

注意int* pint *p 都是合法的,但建议将 * 靠近类型名(强调类型的一部分),或者靠近变量名(强调变量是指针)。个人风格保持一致即可。

初始化

指针必须指向一个有效的内存地址才能安全使用。

int a = 10;
int* p = &a;   // p 指向 a 的地址,& 是取地址运算符

int* q = nullptr;   // C++11 引入的空指针常量,推荐使用
// int* r = NULL;   // C 风格的空指针,不推荐在 C++ 中使用
  • &a:取变量 a 的内存地址。
  • nullptr:表示指针不指向任何有效对象。

未初始化的指针(野指针)是非常危险的,它们指向不确定的地址,解引用会导致未定义行为。


3. 解引用:访问指针指向的数据

使用 * 运算符(解引用运算符)来访问指针所指向的内存中的值。

#include <iostream>
int main() {
    int a = 42;
    int* p = &a;   // p 指向 a
    std::cout << *p << std::endl;  // 输出 42

    *p = 100;      // 通过指针修改 a 的值
    std::cout << a << std::endl;   // 输出 100
    return 0;
}

图解

a (地址 0x1000) -> [42]
p (地址 0x2000) -> [0x1000]
*p 就是找到地址 0x1000 中的值 42

4. 指针的运算

指针支持几种算术运算,但仅限于数组或连续内存区域中。

指针加减整数

当指针加 n 时,实际地址增加 n * sizeof(类型) 字节。

int arr[3] = {10, 20, 30};
int* p = arr;   // 指向 arr[0]
std::cout << *p << std::endl;  // 10
p++;                          // 现在指向 arr[1]
std::cout << *p << std::endl;  // 20
p += 1;                       // 指向 arr[2]
std::cout << *p << std::endl;  // 30

指针相减

两个同类型指针相减的结果是它们之间元素的个数(偏移量),不是字节数。

int* p1 = &arr[0];
int* p2 = &arr[2];
std::cout << p2 - p1 << std::endl;  // 输出 2

指针比较

可以使用关系运算符(==, !=, <, > 等)比较同一数组内的指针。

if (p1 < p2) { /* ... */ }

5. 指针与数组

数组名在很多情况下会隐式转换为指向其第一个元素的指针。

int arr[5] = {1,2,3,4,5};
int* p = arr;    // 等价于 int* p = &arr[0];

使用指针遍历数组

for (int* p = arr; p != arr + 5; ++p) {
    std::cout << *p << " ";
}

数组名与指针的区别

  • sizeof(arr) 返回整个数组的字节数,sizeof(p) 返回指针本身的字节数(通常 8 或 4)。
  • arr 是常量地址,不能修改(如 arr = someOtherPtr; 非法),而指针可以重新赋值。

指针数组与数组指针

  • 指针数组:数组的每个元素都是指针。
    int a=1, b=2, c=3;
    int* arrPtr[3] = {&a, &b, &c};
    
  • 数组指针:指向整个数组的指针。
    int arr[3] = {1,2,3};
    int (*pArr)[3] = &arr;  // pArr 指向具有 3 个 int 的数组
    

6. 指针与函数

6.1 指针作为函数参数

使用指针可以在函数内部修改外部变量的值(模拟引用传递)。

void swap(int* x, int* y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 5, b = 10;
    swap(&a, &b);
    // a=10, b=5
}

6.2 函数指针

函数指针指向函数代码的入口地址,用于回调、策略模式等。

#include <iostream>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int compute(int x, int y, int (*op)(int, int)) {
    return op(x, y);
}

int main() {
    std::cout << compute(3, 4, add) << std::endl;      // 7
    std::cout << compute(3, 4, multiply) << std::endl; // 12
}

6.3 指针作为返回值

函数可以返回指针,但不能返回局部变量的地址(局部变量在函数返回后销毁)。

int* badReturn() {
    int local = 10;
    return &local; // 危险!悬垂指针
}

int* goodReturn() {
    static int staticVar = 20;
    return &staticVar; // 合法,static 变量生命周期贯穿程序
}

int* dynamicReturn() {
    int* p = new int(30);
    return p; // 合法,但调用者需要负责 delete
}

7. 动态内存分配

C++ 中使用 newdelete 在堆(heap)上动态分配和释放内存。

分配单个对象

int* p = new int;      // 分配一个未初始化的 int
*p = 42;
delete p;              // 释放内存,避免内存泄漏

int* q = new int(100); // 分配并初始化为 100
delete q;

分配数组

int* arr = new int[10];   // 分配 10 个 int 的数组
for (int i = 0; i < 10; ++i) arr[i] = i;
delete[] arr;             // 注意使用 delete[]

注意事项

  • 每次 new 都要配对 delete,否则造成内存泄漏
  • nullptr 使用 delete 是安全的(什么都不做)。
  • 已释放的指针应置为 nullptr,避免悬垂指针。
delete p;
p = nullptr;

8. 指针与 const

const 可以修饰指针指向的内容,也可以修饰指针本身,或者两者都修饰。

8.1 指向常量的指针(底层 const)

const int* p;          // 或者 int const* p;
int a = 10, b = 20;
p = &a;                // 合法
// *p = 30;            // 错误:不能通过 p 修改指向的值
a = 30;                // 合法:a 本身不是常量
p = &b;                // 合法:p 可以指向其他变量

8.2 常量指针(顶层 const)

int* const p = &a;     // p 本身是常量,必须初始化
*p = 50;               // 合法:可以修改指向的值
// p = &b;             // 错误:p 不能指向其他变量

8.3 指向常量的常量指针

const int* const p = &a;
// 既不能修改 *p,也不能修改 p

应用场景

  • 函数参数中使用 const int* 表示函数不会修改传入的值。
  • 使用 int* const 表示指针地址固定(常用于嵌入式或硬件映射)。

9. 多级指针

指向指针的指针,用于间接多级访问。

int a = 10;
int* p = &a;    // 一级指针
int** pp = &p;  // 二级指针

std::cout << **pp << std::endl;  // 输出 10

常见用途

  • 函数中需要修改指针本身(如动态分配二维数组)。
  • 命令行参数 char** argv
void allocateMemory(int** ptr, int size) {
    *ptr = new int[size];
}

int main() {
    int* arr = nullptr;
    allocateMemory(&arr, 100);
    // 使用 arr...
    delete[] arr;
}

10. 智能指针简介(C++11起)

为避免手动 new/delete 带来的内存泄漏和悬垂指针,C++11 引入了智能指针(定义在 <memory> 中)。

std::unique_ptr

独占所有权的指针,不能拷贝,只能移动。

#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // 错误:不能拷贝
std::unique_ptr<int> p2 = std::move(p1); // 转移所有权
// p1 现在为空

std::shared_ptr

共享所有权,使用引用计数。当最后一个 shared_ptr 销毁时释放内存。

std::shared_ptr<int> sp1 = std::make_shared<int>(100);
std::shared_ptr<int> sp2 = sp1; // 引用计数变为 2
// 自动管理内存,无需 delete

std::weak_ptr

配合 shared_ptr 使用,解决循环引用问题。

建议:现代 C++ 中尽量避免裸指针管理动态内存,优先使用智能指针。


11. 常见陷阱与最佳实践

陷阱

  1. 未初始化指针(野指针):解引用未初始化的指针导致崩溃。
  2. 内存泄漏:忘记 deletedelete[]
  3. 悬垂指针:释放内存后继续使用原指针。
  4. 数组越界:指针运算超出合法范围。
  5. 返回局部变量地址

最佳实践

  • 总是初始化指针,没有合法地址就用 nullptr
  • 使用 delete 后立即将指针设为 nullptr
  • 优先使用引用代替指针,如果不需要为空或重定向。
  • 动态数组优先使用 std::vector 而非原始指针。
  • 单一所有权用 unique_ptr,共享所有权用 shared_ptr
  • 避免不必要的指针,能用对象就用对象。

12. 总结

概念 说明
指针的本质 存储内存地址的变量
& 运算符 取地址
* 运算符 解引用,访问指向的值
指针与数组 数组名退化为指向首元素的指针
new / delete 动态内存管理
const 指针 控制指针本身或指向内容的常量性
智能指针 自动管理内存,unique_ptr, shared_ptr

指针是 C++ 强大的工具,也是最容易出错的领域。掌握指针需要大量练习,理解内存模型是关键。现代 C++ 鼓励使用智能指针和容器来减少显式指针操作,但在底层编程、性能优化和与 C 代码交互时,指针依然不可或缺。

(页面内容为AI总结,如有疏漏请指正)

posted @ 2026-05-24 13:39  一亩食堂  阅读(26)  评论(0)    收藏  举报