指针是C++中最强大也最危险的工具之一。它直接操作内存地址的特性,让C++程序员能够实现高效的内存管理,但同时也带来了内存泄漏、野指针等常见问题。理解指针的本质,是掌握C++内存管理的第一步。
指针变量存储的是内存地址,而不是实际的值。当我们声明一个指针时,需要指定它指向的数据类型。例如,int* ptr声明了一个指向整型的指针。这里的星号(*)表示这是一个指针变量。指针的大小在32位系统上是4字节,在64位系统上是8字节,这与系统架构有关,而与指向的数据类型无关。
动态内存分配是指在程序运行时(而不是编译时)请求和释放内存。在C++中,我们使用new和delete运算符来管理动态内存。这与C语言的malloc()和free()函数不同,new和delete是运算符,它们不仅分配/释放内存,还会调用构造函数/析构函数。
重要提示:每个
new操作都应该有对应的delete操作,否则会导致内存泄漏。这是C++内存管理中最基本也最重要的规则。
指针的声明语法是在类型后面加上星号(*)。例如:
cpp复制int* intPtr; // 指向int的指针
double* dblPtr; // 指向double的指针
char* charPtr; // 指向char的指针
指针在使用前应该被初始化。未初始化的指针称为"野指针",指向不确定的内存位置,使用这样的指针会导致未定义行为。初始化指针有三种常见方式:
cpp复制int* p1 = nullptr; // 初始化为空指针
int x = 10;
int* p2 = &x; // 指向变量x的地址
int* p3 = new int(20); // 指向动态分配的int,初始值为20
解引用指针使用星号(*)运算符,它允许我们访问指针指向的值:
cpp复制int value = 42;
int* ptr = &value;
cout << *ptr; // 输出42,通过解引用访问指针指向的值
*ptr = 100; // 通过指针修改value的值
cout << value; // 现在输出100
对于指向结构体或类的指针,访问成员可以使用箭头运算符(->):
cpp复制struct Person {
string name;
int age;
};
Person p;
Person* ptr = &p;
ptr->name = "Alice"; // 等价于 (*ptr).name = "Alice";
ptr->age = 25;
指针支持有限的算术运算:加、减、比较等。这些运算基于指针指向的类型大小:
cpp复制int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // 指向数组第一个元素
cout << *ptr; // 输出10
ptr++; // 移动到下一个int位置
cout << *ptr; // 输出20
cout << *(ptr+2); // 输出40 (ptr+2指向第三个元素)
指针运算常用于数组遍历,但要注意不要越界访问。指针比较通常用于检查指针是否到达某个位置:
cpp复制int* end = arr + 5; // 指向数组末尾之后的位置
for(int* p = arr; p != end; p++) {
cout << *p << " ";
}
new运算符在堆上分配内存并返回指向该内存的指针。基本语法:
cpp复制// 分配单个int
int* p = new int;
*p = 10;
// 分配并初始化
int* p2 = new int(20);
// 分配数组
int* arr = new int[10];
for(int i = 0; i < 10; i++) {
arr[i] = i * 10;
}
// 释放内存
delete p;
delete p2;
delete[] arr; // 注意数组的特殊语法
重要提示:
new和delete必须配对使用,new[]和delete[]必须配对使用。混用会导致未定义行为。
动态分配的数组比静态数组更灵活,大小可以在运行时决定:
cpp复制int size;
cout << "Enter array size: ";
cin >> size;
int* dynamicArray = new int[size];
// 使用数组
for(int i = 0; i < size; i++) {
dynamicArray[i] = i * 2;
}
// 释放内存
delete[] dynamicArray;
动态数组的一个常见问题是忘记释放内存或错误的释放方式。使用delete而不是delete[]来释放动态数组是常见错误,这可能导致内存泄漏或程序崩溃。
对于类对象,new会调用构造函数,delete会调用析构函数:
cpp复制class MyClass {
public:
MyClass() { cout << "Constructor called\n"; }
~MyClass() { cout << "Destructor called\n"; }
};
MyClass* obj = new MyClass; // 调用构造函数
delete obj; // 调用析构函数
如果忘记delete动态分配的对象,不仅会泄漏内存,对象的析构函数也不会被调用,可能导致资源泄漏(如文件未关闭、锁未释放等)。
内存泄漏是指程序未能释放不再使用的内存。长期运行的程序中,内存泄漏会逐渐消耗所有可用内存,导致程序或系统崩溃。
常见的内存泄漏场景:
解决方案:
cpp复制// 内存泄漏示例
void leakyFunction() {
int* p = new int(100);
// 使用p...
// 忘记delete p;
}
// 解决方案1:使用智能指针
#include <memory>
void safeFunction() {
std::unique_ptr<int> p(new int(100));
// 自动管理内存,无需手动delete
}
// 解决方案2:异常安全的传统方法
void anotherSafeFunction() {
int* p = nullptr;
try {
p = new int(100);
// 使用p...
delete p;
} catch(...) {
delete p; // 确保异常时也能释放内存
throw;
}
}
野指针是指指向无效内存地址的指针。使用野指针会导致未定义行为,通常是程序崩溃。
常见的野指针场景:
解决方案:
cpp复制// 野指针示例
int* wildPointer; // 未初始化
*wildPointer = 10; // 危险!
int* p = new int(20);
delete p;
*p = 30; // p现在是野指针
// 解决方案
int* safePointer = nullptr; // 初始化为nullptr
if(safePointer) { // 检查是否为null
*safePointer = 10;
}
int* p2 = new int(40);
delete p2;
p2 = nullptr; // 置为nullptr
悬空指针是指向曾经有效但现在已经释放的内存的指针。它与野指针类似,但特指那些曾经有效过的指针。
常见场景:
解决方案:
cpp复制// 悬空指针示例
int* p1 = new int(50);
int* p2 = p1; // p2和p1指向同一内存
delete p1;
*p2 = 60; // p2现在是悬空指针
// 解决方案1:使用shared_ptr
#include <memory>
std::shared_ptr<int> sp1(new int(70));
std::shared_ptr<int> sp2 = sp1; // 共享所有权
// 当最后一个shared_ptr离开作用域时自动删除
// 解决方案2:明确所有权
int* owner = new int(80);
// 其他指针只能借用,不负责释放
int* borrower = owner;
// ...
delete owner; // 只有owner负责释放
borrower = nullptr; // 明确表示不再使用
C++11引入了智能指针来自动管理动态内存,大大减少了内存泄漏和指针误用的风险。主要的智能指针有三种:
unique_ptr表示独占所有权,同一时间只能有一个unique_ptr指向特定对象。当unique_ptr被销毁时,它指向的对象也会被自动删除。
cpp复制#include <memory>
void uniquePtrDemo() {
std::unique_ptr<int> up1(new int(100));
// up1拥有这个int的所有权
// std::unique_ptr<int> up2 = up1; // 错误,不能复制
std::unique_ptr<int> up3 = std::move(up1); // 转移所有权
// 现在up3拥有所有权,up1为空
if(up1) {
cout << *up1; // 不会执行,up1为空
}
if(up3) {
cout << *up3; // 输出100
}
// up3离开作用域,自动删除int
}
unique_ptr是轻量级的,几乎不带来额外开销,是大多数情况下的首选智能指针。
shared_ptr实现共享所有权,多个shared_ptr可以指向同一对象,内部使用引用计数跟踪所有者数量。当最后一个shared_ptr被销毁时,对象才会被删除。
cpp复制#include <memory>
void sharedPtrDemo() {
std::shared_ptr<int> sp1(new int(200));
// 引用计数=1
{
std::shared_ptr<int> sp2 = sp1;
// 引用计数=2
cout << *sp2; // 输出200
}
// sp2离开作用域,引用计数=1
std::shared_ptr<int> sp3 = sp1;
// 引用计数=2
sp1.reset(); // sp1放弃所有权,引用计数=1
// sp3仍然保持对象存活
// sp3离开作用域,引用计数=0,删除int
}
shared_ptr适用于需要共享所有权的场景,但要注意循环引用问题。
weak_ptr是对shared_ptr管理的对象的弱引用,它不增加引用计数。用于解决shared_ptr的循环引用问题。
cpp复制#include <memory>
void weakPtrDemo() {
std::shared_ptr<int> sp(new int(300));
std::weak_ptr<int> wp = sp;
if(auto locked = wp.lock()) { // 尝试提升为shared_ptr
cout << *locked; // 输出300
// locked是一个新的shared_ptr,引用计数=2
}
// locked离开作用域,引用计数=1
sp.reset(); // 引用计数=0,删除int
if(wp.expired()) {
cout << "Object has been deleted";
}
}
weak_ptr常用于观察者模式、缓存等场景,避免不必要的对象生命周期延长。
指针是实现运行时多态的关键。通过基类指针可以调用派生类的虚函数:
cpp复制class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() {} // 虚析构函数很重要!
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing a circle\n";
}
};
class Square : public Shape {
public:
void draw() const override {
cout << "Drawing a square\n";
}
};
void polymorphismDemo() {
Shape* shapes[] = {new Circle(), new Square()};
for(Shape* s : shapes) {
s->draw(); // 调用正确的派生类函数
}
// 记得删除
for(Shape* s : shapes) {
delete s;
}
}
重要提示:基类必须有虚析构函数,否则通过基类指针删除派生类对象会导致未定义行为(通常只会调用基类的析构函数,而不会调用派生类的析构函数)。
对于性能关键的场景,可能需要自定义内存管理。这通常通过重载new和delete运算符实现:
cpp复制class MemoryIntensive {
public:
void* operator new(size_t size) {
cout << "Custom new for size " << size << "\n";
return malloc(size);
}
void operator delete(void* p) {
cout << "Custom delete\n";
free(p);
}
void* operator new[](size_t size) {
cout << "Custom new[] for size " << size << "\n";
return malloc(size);
}
void operator delete[](void* p) {
cout << "Custom delete[]\n";
free(p);
}
};
void customMemoryDemo() {
MemoryIntensive* mi = new MemoryIntensive;
delete mi;
MemoryIntensive* array = new MemoryIntensive[5];
delete[] array;
}
自定义内存管理可以用于实现内存池、调试内存分配等高级功能。
指针允许直接操作内存,这在某些系统编程场景中是必要的:
cpp复制void lowLevelOps() {
// 类型转换指针
int value = 0x12345678;
char* bytes = reinterpret_cast<char*>(&value);
// 查看内存中的字节顺序
for(int i = 0; i < sizeof(int); i++) {
cout << hex << (int)bytes[i] << " ";
}
cout << "\n";
// 使用指针进行位操作
unsigned int flags = 0;
unsigned int* flagPtr = &flags;
*flagPtr |= 0x1; // 设置第一位
*flagPtr |= 0x4; // 设置第三位
cout << "Flags: " << *flagPtr << "\n";
}
这类低级操作通常用于硬件交互、协议实现等场景,但会降低代码的可移植性,应谨慎使用。
在现代C++中,应优先使用智能指针而非原始指针来管理资源:
cpp复制void modernMemoryDemo() {
// 独占所有权
auto up = std::make_unique<int>(42); // C++14引入的make_unique
// 共享所有权
auto sp = std::make_shared<double>(3.14); // make_shared更高效
// 观察而不拥有
std::weak_ptr<double> wp = sp;
// 数组支持
auto arr = std::make_unique<int[]>(10);
arr[0] = 1;
}
make_unique和make_shared比直接使用new更安全高效,应优先使用。
在应用代码中,应尽量避免直接使用new和delete,而是使用:
vector、string)cpp复制// 不好的做法
int* createArray(int size) {
return new int[size];
}
// 好的做法
std::vector<int> createVector(int size) {
return std::vector<int>(size);
}
// 或者返回unique_ptr
std::unique_ptr<int[]> createSmartArray(int size) {
return std::make_unique<int[]>(size);
}
指针使用不当容易破坏异常安全。智能指针和RAII技术可以自动保证异常安全:
cpp复制// 不安全的代码
void unsafe() {
int* p = new int(100);
someFunctionThatMightThrow(); // 如果抛出异常,内存泄漏
delete p;
}
// 安全的代码
void safe() {
auto p = std::make_unique<int>(100);
someFunctionThatMightThrow(); // 即使抛出异常,内存也会被释放
}
尽管智能指针是现代C++的首选,但在某些情况下原始指针仍有其用途:
在这些情况下使用原始指针时,应明确记录指针的所有权语义,避免混淆。