C++ 中的指针是其最核心和强大的特性之一,但也是初学者容易混淆的地方。它允许你直接操作内存地址,实现高效的算法和数据结构。
1. 指针的核心概念
指针 (Pointer) 本质上是一个变量,但它存储的不是普通的值(如整数、字符),而是另一个变量的内存地址。
核心操作符:
-
&
(取地址符 / Address-of Operator): 获取一个变量的内存地址。 -
*
(解引用符 / Dereference Operator): 访问指针所指向地址上的值。
2. 指针的声明、初始化与使用
2.1 声明指针
声明一个指针时,需要指定它将要指向的数据类型。
语法:
type *pointer_name;
int *p_int; // 声明一个指向 int 类型的指针
double *p_double; // 声明一个指向 double 类型的指针
std::string *p_str; // 声明一个指向 string 对象的指针
注意: *
的位置可以紧跟类型 int* p;
,也可以紧跟变量名 int *p;
,或者在中间 int * p;
。它们的含义完全相同,但 int* p1, p2;
这种写法容易误导,它表示 p1
是一个 int
指针,而 p2
只是一个普通的 int
变量。因此,推荐将 *
与变量名写在一起。
2.2 初始化指针
指针在使用前必须被初始化,即让它指向一个有效的内存地址或 nullptr
。未初始化的指针(野指针)非常危险。
#include <iostream>
#include <string>
int main() {
int var = 42;
std::string greeting = "Hello";
// 1. 声明的同时初始化
int *p_var = &var; // p_var 存储了 var 的地址
// 2. 先声明后初始化
std::string *p_greeting;
p_greeting = &greeting;
// 访问地址
std::cout << "Address of var: " << p_var << std::endl;
std::cout << "Address of greeting: " << p_greeting << std::endl;
}
2.3 解引用:访问值
使用解引用符 *
可以获取或修改指针指向地址上的数据。
#include <iostream>
int main() {
int score = 90;
int *p_score = &score;
// 读取值
std::cout << "Value of score (via variable): " << score << std::endl;
std::cout << "Value of score (via pointer): " << *p_score << std::endl;
// 修改值
*p_score = 95; // 通过指针修改了 score 的值
std::cout << "New value of score: " << score << std::endl; // 输出 95
}
2.4 空指针 (Null Pointer)
空指针是一个不指向任何有效内存地址的特殊指针。在 C++11 及之后,强烈推荐使用 nullptr
来表示空指针。
int *p_safe = nullptr; // 推荐的现代 C++ 写法
// int *p_legacy = NULL; // C 风格的写法,在 C++ 中应避免
if (p_safe == nullptr) {
std::cout << "This is a null pointer." << std::endl;
}
3. 指针与数组
指针和数组在 C++ 中关系紧密。数组名本身就可以被看作一个指向数组第一个元素的常量指针。
3.1 数组名即指针
#include <iostream>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int *p_numbers = numbers; // 数组名直接赋值给指针
std::cout << "First element: " << *p_numbers << std::endl; // 输出 10
std::cout << "Address of first element: " << p_numbers << std::endl;
std::cout << "Address of the array: " << &numbers[0] << std::endl;
}
3.2 指针算术
可以对指针进行加减运算,使其在内存中前后移动。指针的每一步移动,跨越的字节数等于其所指向数据类型的大小。
#include <iostream>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int *ptr = numbers; // ptr 指向 numbers[0]
// 移动指针
ptr++; // ptr 现在指向 numbers[1]
std::cout << "Second element: " << *ptr << std::endl; // 输出 20
// 使用指针算术访问数组元素
std::cout << "Third element: " << *(ptr + 1) << std::endl; // 输出 30
std::cout << "First element again: " << *(ptr - 1) << std::endl; // 输出 10
// 遍历数组
std::cout << "Traversing array with a pointer:" << std::endl;
for (int *p = numbers; p < numbers + 5; ++p) {
std::cout << *p << " ";
}
std::cout << std::endl;
}
注意: *(ptr + i)
在功能上等价于 ptr[i]
。
4. const
与指针
const
关键字可以用来修饰指针,产生不同的效果。
-
指向常量的指针 (Pointer to
const
)-
语法:
const type *ptr;
或type const *ptr;
-
含义: 不能通过这个指针来修改它所指向的值,但指针本身可以指向其他地址。
int val = 10; const int *p_const_val = &val; // *p_const_val = 20; // 错误!不能通过指针修改值 val = 20; // 正确,直接修改变量是可以的 int another_val = 30; p_const_val = &another_val; // 正确,指针可以指向别处
-
-
常量指针 (
const
Pointer)-
语法:
type * const ptr;
-
含义: 指针本身是一个常量,它必须在声明时初始化,之后不能再指向其他地址,但可以通过它修改所指向的值。
int val = 10; int * const const_p = &val; *const_p = 20; // 正确,可以修改指向的值 int another_val = 30; // const_p = &another_val; // 错误!指针不能指向别处
-
-
指向常量的常量指针 (
const
Pointer toconst
)-
语法:
const type * const ptr;
-
含义: 指针本身和它所指向的值都不能被修改。
int val = 10; const int * const const_p_const_val = &val; // *const_p_const_val = 20; // 错误! // const_p_const_val = &another_val; // 错误!
-
5. 指针与函数
5.1 指针作为函数参数(传址调用)
将指针传递给函数,可以在函数内部修改实参的值。
#include <iostream>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
swap(&x, &y);
std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
}
5.2 从函数返回指针
函数可以返回一个指针。但绝对不要返回指向函数内部局部变量的指针,因为函数执行完毕后,局部变量的内存会被释放,返回的指针将成为悬挂指针 (dangling pointer)。
int* create_on_heap() {
// 在堆上分配内存,这是安全的
int *p = new int(100);
return p;
}
int* create_on_stack() {
int local_var = 50;
// 危险!返回局部变量的地址
return &local_var; // 函数结束后 local_var 被销毁
}
int main() {
int *p1 = create_on_heap();
std::cout << *p1 << std::endl; // 正确,输出 100
delete p1; // 记得释放内存
int *p2 = create_on_stack();
// std::cout << *p2 << std::endl; // 未定义行为!非常危险!
}
5.3 函数指针
函数指针存储的是一个函数的地址,允许你像调用普通函数一样通过指针来调用函数。
语法:
return_type (*pointer_name)(parameter_types);
#include <iostream>
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int main() {
// 声明并初始化一个函数指针
int (*p_func)(int, int);
p_func = add; // 指向 add 函数
int result1 = p_func(5, 3); // 通过指针调用函数
std::cout << "Addition: " << result1 << std::endl;
p_func = subtract; // 指向 subtract 函数
int result2 = p_func(5, 3);
std::cout << "Subtraction: " << result2 << std::endl;
}
6. 动态内存分配
程序运行时,可以在堆 (Heap) 上动态地请求内存。这必须通过指针来完成。
-
new
:分配内存。 -
delete
:释放内存。
6.1 单个对象
int *p_int = new int(25); // 分配一个 int,并初始化为 25
double *p_double = new double; // 分配一个 double,未初始化
// ... 使用指针 ...
// 必须手动释放,否则会造成内存泄漏
delete p_int;
delete p_double;
// 释放后,最好将指针设为 nullptr,防止悬挂指针
p_int = nullptr;
p_double = nullptr;
6.2 动态数组
int size = 10;
int *p_array = new int[size]; // 分配一个包含 10 个 int 的数组
for (int i = 0; i < size; ++i) {
p_array[i] = i * 2;
}
// ... 使用数组 ...
// 释放动态数组必须使用 delete[]
delete[] p_array;
p_array = nullptr;
重要: new
和 delete
,new[]
和 delete[]
必须配对使用,否则会导致未定义行为。
7. 指针与类/结构体
7.1 指向对象的指针
#include <iostream>
#include <string>
struct Student {
std::string name;
int id;
};
int main() {
Student s1 = {"Alice", 101};
Student *p_student = &s1;
// 访问成员的两种方式
// 1. 先解引用,再用点号 .
std::cout << "Name: " << (*p_student).name << std::endl;
// 2. 使用箭头操作符 -> (更常用,更简洁)
std::cout << "ID: " << p_student->id << std::endl;
}
7.2 this
指针
在类的非静态成员函数中,this
是一个隐藏的指针,它指向调用该成员函数的对象实例。
class MyClass {
public:
void setID(int id) {
this->id = id; // 使用 this-> 来明确表示是成员变量
}
int getID() {
return id;
}
private:
int id;
};
int main() {
MyClass obj;
obj.setID(123);
std::cout << obj.getID() << std::endl;
}
8. 现代 C++:智能指针 (Smart Pointers)
为了解决手动管理内存(new
/delete
)带来的内存泄漏和悬挂指针等问题,现代 C++ 引入了智能指针。它们是行为类似于指针的类对象,利用 RAII (Resource Acquisition Is Initialization) 技术,在对象生命周期结束时自动释放所管理的内存。
需要包含头文件 <memory>
。
-
std::unique_ptr
(独占指针)-
特性: 独占所指向的对象,保证同一时间只有一个
unique_ptr
指向一个对象。它无法被复制,但可以被移动。 -
用途: 当你需要一个指针明确地“拥有”一块内存,并确保它在指针离开作用域时被释放。
#include <memory> std::unique_ptr<int> p1(new int(100)); // std::unique_ptr<int> p2 = p1; // 错误!不能复制 std::unique_ptr<int> p3 = std::move(p1); // 正确,所有权转移给 p3 // 此时 p1 变为空指针 // 当 p3 离开作用域时,它指向的内存会被自动 delete
-
-
std::shared_ptr
(共享指针)-
特性: 允许多个
shared_ptr
指向同一个对象。内部使用引用计数,当最后一个指向对象的shared_ptr
被销毁时,内存才会被释放。 -
用途: 当多个地方需要共享对同一块内存的管理权时。
#include <memory> #include <iostream> std::shared_ptr<int> sp1 = std::make_shared<int>(200); std::cout << "Count: " << sp1.use_count() << std::endl; // 输出 1 std::shared_ptr<int> sp2 = sp1; // 复制,引用计数增加 std::cout << "Count: " << sp1.use_count() << std::endl; // 输出 2 // 当 sp1 和 sp2 都离开作用域时,内存才会被释放
-
-
std::weak_ptr
(弱指针)-
特性: 一种不控制对象生命周期的智能指针,它指向一个由
shared_ptr
管理的对象。它不增加引用计数。 -
用途: 主要用于解决
shared_ptr
可能导致的循环引用问题。
-
建议: 在现代 C++ 编程中,应优先使用智能指针,仅在必要时(如与C库交互)才使用原始指针。这能极大地提高代码的安全性和健壮性。