引言
指针是 C/C++ 语言的灵魂,也是初学者公认的难点。它的本质是内存地址,通过指针可以直接操作内存,实现高效的数据访问、动态内存管理和泛型编程。
但指针的灵活性也带来了复杂性 ------ 野指针、内存泄漏、类型不匹配等问题常常让开发者头疼。
本文将从基础到进阶,系统梳理 C/C++ 中所有核心指针类型,结合代码示例、内存模型和实际应用场景,帮你彻底搞懂指针的分类、用法和避坑指南。
无论你是初学者还是需要查漏补缺的开发者,这篇文章都能成为你的指针学习手册。
目录
[一、C/C++ 通用指针类型(基础核心)](#一、C/C++ 通用指针类型(基础核心))
[1.1 普通指针(基础指针)](#1.1 普通指针(基础指针))
[1.2 const 修饰的指针(常量指针 vs 指针常量)](#1.2 const 修饰的指针(常量指针 vs 指针常量))
[1.3 数组指针 vs 指针数组(最易混淆的组合)](#1.3 数组指针 vs 指针数组(最易混淆的组合))
[1.4 函数指针(回调函数的核心)](#1.4 函数指针(回调函数的核心))
[1.5 多级指针(指向指针的指针)](#1.5 多级指针(指向指针的指针))
[1.6 void 指针(万能指针)](#1.6 void 指针(万能指针))
[1.7 空指针与野指针(避坑重点)](#1.7 空指针与野指针(避坑重点))
[二、C++ 专属指针类型(进阶特性)](#二、C++ 专属指针类型(进阶特性))
[2.1 智能指针(内存安全的救星)](#2.1 智能指针(内存安全的救星))
[2.2 成员指针(指向类成员的指针)](#2.2 成员指针(指向类成员的指针))
[2.3 std::optional(伪指针的替代方案)](#2.3 std::optional(伪指针的替代方案))
一、C/C++ 通用指针类型(基础核心)
这类指针是 C 和 C++ 的共同基础,掌握它们是后续学习的关键。核心原则:指针的类型决定了内存的解释方式和操作范围(如int*解引用占 4 字节,char*占 1 字节)。
1.1 普通指针(基础指针)
最常用的指针类型,指向单个变量、数组元素或动态内存。
- 关键特性:必须与指向目标的类型严格匹配(否则需强制转换,风险较高)。
- 定义格式:类型* 指针名;(*与变量名绑定,推荐int* p而非int *p,更清晰)。
- 代码示例:
cpp
#include <stdio.h>
int main() {
int a = 10;
int* p = &a; // 指向int变量(&a获取变量地址)
printf("a的值:%d\n", *p); // 解引用访问值,输出10
// 注意:C中允许char*指向字符串常量,C++11后需const char*(否则编译警告)
const char* str = "hello"; // 修正:添加const适配C++标准
printf("字符串:%s\n", str); // 输出hello
int arr[5] = {1,2,3,4,5};
int* parr = arr; // 数组名本质是首元素地址,等价于&arr[0]
printf("数组第3个元素:%d\n", *(parr+2)); // 指针偏移访问,输出3
return 0;
}
- 应用场景:基础数据访问、动态内存分配(malloc/new)、数组遍历。
1.2 const 修饰的指针(常量指针 vs 指针常量)
const的位置决定了限制对象 ------ 是 "指针指向的内容" 不可改,还是 "指针本身" 不可改。
|---------|-----------------------|----------------|--------------------------------------------------|
| 类型 | 定义格式 | 核心限制 | 代码示例 |
| 常量指针 | const 类型* 指针名 | 指向的内容不可改,指针可改 | const int* p = &a; *p=20; // 错误;p=&b; // 合法 |
| 指针常量 | 类型* const 指针名 | 指针本身不可改,内容可改 | int* const p = &a; p=&b; // 错误;*p=20; // 合法 |
| 常量指针常量 | const 类型* const 指针名 | 两者都不可改 | const int* const p = &a; // 完全只读 |
- 记忆口诀:const靠近谁,就限制谁 ------ 靠近类型限制内容,靠近指针名限制指针。
- 应用场景:函数参数中保护传入数据(如const char* str避免字符串被修改)、固定指针指向(如硬件地址)。
1.3 数组指针 vs 指针数组(最易混淆的组合)
两者名称相似但本质完全不同 ------数组指针是指针,指针数组是数组。
(1)指针数组:存储指针的数组
- 本质:数组,每个元素都是指针。
- 定义格式:类型* 数组名[数组大小];(数组优先级高于*,无需括号)。
- 内存模型:数组占用连续空间,每个元素存储一个地址(如 32 位系统占 4 字节)。
- 代码示例:
cpp
#include <stdio.h>
int main() {
const char* strArr[3] = {"apple", "banana", "cherry"}; // 修正:添加const适配C++
int a=1, b=2, c=3;
int* pArr[3] = {&a, &b, &c}; // 每个元素是int*指针
printf("字符串2:%s\n", strArr[1]); // 输出banana
printf("变量b的值:%d\n", *pArr[1]); // 输出2
return 0;
}
- 应用场景:存储多个字符串(避免字符数组浪费空间)、管理多个独立变量的地址。
(2)数组指针:指向整个数组的指针
- 本质:指针,指向 "完整数组" 而非单个元素。
- 定义格式:类型 (*指针名)[数组大小];(括号必须加,提升*优先级)。
- 内存模型:指针存储数组的起始地址,解引用时获取整个数组(偏移时跳过整个数组长度)。
- 代码示例(遍历二维数组):
cpp
#include <stdio.h>
int main() {
int arr[2][3] = {{1,2,3}, {4,5,6}};
int (*p)[3] = arr; // 指向int[3]类型数组(二维数组本质是"数组的数组")
// 访问元素:(*p)[j] 是第一行第j列,*(p+1)[j]是第二行第j列
for(int i=0; i<2; i++) {
for(int j=0; j<3; j++) {
printf("%d ", *(*(p+i)+j)); // 输出1 2 3 4 5 6
}
printf("\n");
}
return 0;
}
- 应用场景:遍历二维数组、传递多维数组到函数(避免数组退化为指针)。
核心区别
|-----------------|-------|---------------|---------------|
| 表达式 | 本质 | 占用空间(32 位) | 偏移量(p+1) |
| int* p[3] | 指针数组 | 12 字节(3 个指针) | 4 字节(下一个指针) |
| int (*p)[3] | 数组指针 | 4 字节(一个指针) | 12 字节(下一个数组) |
1.4 函数指针(回调函数的核心)
指向函数的指针,存储函数的入口地址,核心用途是 "动态切换函数逻辑"。
- 关键特性:指针类型必须与函数的返回值类型和参数列表完全匹配(函数名本质是函数地址)。
- 定义格式:返回值类型 (*指针名)(参数类型1, 参数类型2, ...);。
- 代码示例(回调函数场景):
cpp
#include <stdio.h>
// 普通函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
// 函数指针作为参数(回调函数)
void calculate(int a, int b, int (*func)(int, int)) {
printf("结果:%d\n", func(a, b));
}
int main() {
int (*funcPtr)(int, int) = add; // 指向add函数
printf("3+5=%d\n", funcPtr(3,5)); // 输出8
funcPtr = sub; // 切换指向sub函数
printf("3-5=%d\n", funcPtr(3,5)); // 输出-2
calculate(4,6, add); // 回调add,输出10
calculate(4,6, sub); // 回调sub,输出-2
return 0;
}
- 典型应用:qsort排序函数的比较器、状态机设计、框架中的钩子函数。
1.5 多级指针(指向指针的指针)
指针的指向目标是另一个指针,用于 "间接修改指针变量的指向"(如函数中修改外部指针)。
- 定义格式:类型** 指针名;(*数量 = 指针级数,三级及以上极少用)。
- 内存模型:一级指针存储变量地址,二级指针存储一级指针的地址,依此类推。
- 代码示例(函数中修改外部指针):
cpp
#include <stdio.h>
#include <stdlib.h>
// 用二级指针修改外部指针的指向
void allocateMemory(int** pp) {
// 注意:C++中malloc返回值必须强制转换为目标类型,C中可省略
*pp = (int*)malloc(sizeof(int)); // 强制转换适配C++标准
**pp = 100; // 给分配的内存赋值
}
int main() {
int* p = NULL;
allocateMemory(&p); // 传入p的地址(二级指针)
printf("p指向的值:%d\n", *p); // 输出100
free(p); // 释放内存
p = NULL; // 避免野指针
return 0;
}
- 应用场景:函数中动态分配内存并返回、管理指针数组(如char** argv命令行参数)。
1.6 void 指针(万能指针)
指向 "无类型" 数据的指针,可兼容任意类型的指针,但不能直接解引用。
- 关键特性:
- C 中可接收任意类型指针(无需强制转换),C++ 中需显式转换;
- 解引用前必须强制转换为具体类型(否则编译器无法确定内存大小);
- 不能直接参与算术运算(需转换后才能偏移)。
- 代码示例(内存拷贝函数):
cpp
#include <stdio.h>
// 通用内存拷贝(模拟memcpy)
void myMemcpy(void* dest, const void* src, int size) {
char* destPtr = (char*)dest; // 转换为char*,按字节拷贝
char* srcPtr = (char*)src;
for(int i=0; i<size; i++) {
destPtr[i] = srcPtr[i];
}
}
int main() {
int a = 10;
int b;
void* voidPtr = &a; // C中合法,C++中需(void*)&a强制转换
b = *(int*)voidPtr; // 强制转换为int*后解引用
printf("b=%d\n", b); // 输出10
myMemcpy(&b, &a, sizeof(int)); // 通用拷贝,支持任意类型
return 0;
}
- 应用场景:泛型编程(C 语言无模板,用 void 指针模拟)、内存操作函数(memcpy/memset)、函数参数兼容多类型。
1.7 空指针与野指针(避坑重点)
(1)空指针:明确的无效地址
指向 "无效内存" 的指针,表示指针未指向任何有效数据。
- 定义方式:C++11 推荐nullptr(类型安全),C 语言用NULL(本质是(void*)0,C11 也支持nullptr)。
- 代码示例:
cpp
int* p = nullptr; // C++11+推荐,C11+兼容
int* q = NULL; // C/C++兼容(C++中本质是0)
if (p == nullptr) { // 合法判断(避免解引用空指针)
printf("p是空指针\n");
}
- 注意:空指针不可解引用(会触发段错误),常用于指针初始化和有效性判断。
(2)野指针:最危险的指针
未初始化、指向已释放内存或非法地址的指针(状态不确定)。
- 危害:解引用会导致程序崩溃、内存污染(未定义行为)。
- 避免方法:
- 指针声明时立即初始化(或赋值为nullptr);
- 动态内存释放(free/delete)后,将指针置为nullptr;
- 不访问已超出作用域的局部变量地址;
- 函数不返回栈内存的地址。
二、C++ 专属指针类型(进阶特性)
C++ 在 C 的基础上扩展了面向对象和泛型特性,新增的指针类型主要解决 "内存安全" 和 "类成员访问" 问题。
2.1 智能指针(内存安全的救星)
C++11 引入的模板类指针,核心作用是 "自动管理动态内存"------ 无需手动delete,超出作用域时自动释放内存,从根源上避免内存泄漏。
C++ 标准库提供 3 种核心智能指针(定义在<memory>头文件):
(1)std::unique_ptr:独占所有权
- 核心特性:同一时间只能有一个unique_ptr指向某块内存,不可拷贝,只能移动(通过std::move)。
- 优势:轻量高效(无额外开销),适合管理独占资源。
- 代码示例:
cpp
#include <memory>
#include <iostream>
using namespace std;
int main() {
// 管理单个对象(推荐用make_unique创建,C++14+支持)
unique_ptr<int> ptr1 = make_unique<int>(10);
cout << *ptr1 << endl; // 输出10
// 转移所有权(ptr1失效)
unique_ptr<int> ptr2 = move(ptr1);
if (!ptr1) {
cout << "ptr1已失去所有权" << endl;
}
// 管理动态数组(自动调用delete[])
unique_ptr<int[]> arrPtr = make_unique<int[]>(3);
arrPtr[0] = 1;
arrPtr[1] = 2;
arrPtr[2] = 3;
cout << arrPtr[1] << endl; // 输出2
return 0; // 自动释放内存,无需手动delete
}
(2)std::shared_ptr:共享所有权
- 核心特性:通过 "引用计数" 跟踪指针数量,最后一个shared_ptr销毁时释放内存。
- 关键说明:引用计数的增减操作是线程安全的,但解引用指针修改对象数据需额外同步。
- 优势:支持多模块共享资源,适合容器存储动态对象、多线程共享数据(需同步对象访问)。
- 代码示例:
cpp
#include <memory>
#include <iostream>
using namespace std;
int main() {
// 推荐用make_shared创建(更高效,一次内存分配)
shared_ptr<int> ptr1 = make_shared<int>(20);
cout << "引用计数:" << ptr1.use_count() << endl; // 输出1
// 共享所有权(引用计数+1)
shared_ptr<int> ptr2 = ptr1;
cout << "引用计数:" << ptr1.use_count() << endl; // 输出2
// 重置指针(引用计数-1)
ptr2.reset();
cout << "引用计数:" << ptr1.use_count() << endl; // 输出1
return 0; // ptr1销毁,引用计数为0,释放内存
}
(3)std::weak_ptr:解决循环引用
- 核心问题:shared_ptr的循环引用会导致内存泄漏(如 A 指向 B,B 指向 A,引用计数永远不为 0)。
- 核心特性:弱引用(不增加引用计数),可通过lock()获取shared_ptr(需先判断是否过期)。
- 代码示例(解决循环引用):
cpp
#include <memory>
#include <iostream>
using namespace std;
class B; // 前置声明
class A {
public:
weak_ptr<B> bPtr; // 用weak_ptr避免循环引用
~A() { cout << "A被销毁" << endl; }
};
class B {
public:
weak_ptr<A> aPtr; // 用weak_ptr避免循环引用
~B() { cout << "B被销毁" << endl; }
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->bPtr = b;
b->aPtr = a;
// 离开作用域时,a和b的引用计数都为1?不!weak_ptr不增加计数,所以计数为0,正常销毁
return 0;
}
智能指针使用禁忌
- 不能指向栈内存(如unique_ptr<int> p(&a),会导致双重释放);
- 避免shared_ptr循环引用(用weak_ptr解决);
- 不使用原始指针初始化多个shared_ptr(会导致双重释放);
- shared_ptr不直接支持数组(需自定义删除器,C++20 可使用std::make_shared_for_overwrite)。
2.2 成员指针(指向类成员的指针)
指向类的成员变量或成员函数的指针,用于 "间接访问类成员"(无需通过具体对象,可动态切换成员)。
(1)成员变量指针
- 定义格式:类型 类名::*指针名 = &类名::成员变量;
- 访问方式:对象用.*,对象指针用->*。
- 代码示例:
cpp
#include <iostream>
using namespace std;
class Person {
public:
string name;
int age;
};
int main() {
// 指向Person类的age成员变量
int Person::*agePtr = &Person::age;
string Person::*namePtr = &Person::name;
Person p;
p.*agePtr = 25; // 对象访问
p.*namePtr = "Alice";
Person* pPtr = &p;
cout << pPtr->*namePtr << " " << pPtr->*agePtr << endl; // 输出Alice 25
return 0;
}
(2)成员函数指针
- 定义格式:返回值类型 (类名::*指针名)(参数类型) = &类名::成员函数;
- 注意:非静态成员函数隐含this指针,必须绑定对象才能调用。
- 代码示例:
cpp
#include <iostream>
using namespace std;
class Calculator {
public:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
};
int main() {
// 指向Calculator类的add成员函数
int (Calculator::*funcPtr)(int, int) = &Calculator::add;
Calculator calc;
cout << (calc.*funcPtr)(3,5) << endl; // 对象调用,输出8
Calculator* calcPtr = &calc;
funcPtr = &Calculator::sub;
cout << (calcPtr->*funcPtr)(3,5) << endl; // 对象指针调用,输出-2
return 0;
}
核心区别:成员函数指针 vs 普通函数指针
- 普通函数指针:指向全局函数 / 静态成员函数(无this指针);
- 成员函数指针:指向非静态成员函数(隐含this指针,必须绑定对象)。
2.3 std::optional(伪指针的替代方案)
C++17 引入,虽不是严格意义上的指针,但可看作 "可能存在的对象" 封装 ------ 替代 "空指针 + 值" 的场景,避免野指针问题。
- 核心特性:可存储一个值或 "无值"(std::nullopt),类型安全。
- 代码示例:
cpp
#include <optional>
#include <iostream>
using namespace std;
// 返回可选值(成功返回int,失败返回无值)
optional<int> divide(int a, int b) {
if (b == 0) {
return nullopt; // 无值
}
return a / b;
}
int main() {
auto result1 = divide(10, 2);
if (result1.has_value()) {
cout << "10/2=" << result1.value() << endl; // 输出5
}
auto result2 = divide(10, 0);
if (!result2.has_value()) {
cout << "除数为0,无法计算" << endl;
}
return 0;
}
- 应用场景:函数返回值可能无效(替代NULL或错误码)、可选参数传递。
三、总结:指针学习路径与面试考点
- 核心原则
指针的本质是内存地址,指针的类型决定了:
- 如何解释内存中的数据(如int* vs char*);
- 内存操作的范围(如指针偏移时的步长)。
- 学习路径
基础指针(普通指针、const指针)→ 复杂指针(数组指针、指针数组、函数指针)→ 多级指针、void指针 → C++智能指针、成员指针
- 初学者:先掌握基础指针和避坑技巧(如野指针、类型匹配);
- 进阶者:重点学习智能指针(内存安全)、函数指针(回调函数)、成员指针(面向对象)。
-
面试高频考点
-
const int* p、int* const p、const int* const p的区别;
- 数组指针与指针数组的区别(内存模型 + 代码示例);
- 函数指针的定义与回调函数应用(如qsort);
- 智能指针的实现原理(引用计数)、循环引用问题及解决方案;
- 野指针的成因与避免方法;
-
void 指针的特性与 C/C++ 中的使用差异。
-
实战建议
- 编写代码时,遵循 "指针初始化→使用→释放→置空" 的流程;
- 优先使用智能指针(C++)替代原始指针,减少内存错误;
- 复杂指针类型(如函数指针、成员指针)结合代码示例记忆,避免死记硬背;
- 跨 C/C++ 开发时,重点关注字符串常量、void 指针、malloc 转换的语言差异。
指针是 C/C++ 的核心难点,也是拉开差距的关键。如果你在学习过程中遇到了具体问题(如指针调试、内存泄漏排查),欢迎在评论区留言分享你的困惑或经验,我们一起讨论交流!
如果觉得本文对你有帮助,别忘了点赞、收藏、转发给身边的开发者~