概览

  • 指针
  • 迭代器与普通指针有什么区别
  • C++的智能指针及其原理
  • 悬挂指针和野指针有什么区别?
  • 指针常量和常量指针的区别
  • 指针和引用有什么区别呢?
  • 如何避免悬挂指针?

指针的基础概念

指针是存储内存地址的变量。它指向一个特定类型的对象或变量,并且通过解引用操作(*)可以访问该地址上的值。

示例

int a = 10;
int* p = &a;  // p是指向a的指针
std::cout << *p;  // 解引用p,输出a的值:10

指针的用途很多,例如动态内存分配、函数参数传递、数组操作等。

指针常量与常量指针

  • 指针常量(constant pointer):指针本身是常量,指向的地址不能改变,但指向的内容可以改变。
  • 常量指针(pointer to constant):指针指向的对象是常量,不能通过指针修改该对象的值,但可以改变指针指向的地址。
int a = 5;
int b = 10;

// 指针常量:指向的地址不能改变
int* const ptr1 = &a;
*ptr1 = 20;  // 可以修改a的值
// ptr1 = &b;  // 错误,ptr1不能改变指向

// 常量指针:指向的值不能改变
const int* ptr2 = &a;
ptr2 = &b;  // 可以改变ptr2的指向
// *ptr2 = 30;  // 错误,不能修改指向对象的值

指针和引用的区别

  • 指针可以为空,也可以重新指向其他对象;指针的大小是固定的。
  • 引用必须在定义时初始化,且不能重新绑定到其他对象。引用实际上是对象的别名,不占用额外内存。

主要区别:指针更灵活,允许指向不同的对象;引用更简单,用于定义后不会变化的别名。

int a = 5;
int* ptr = &a;  // 指针
int& ref = a;   // 引用

ref = 10;  // 通过引用修改a的值
ptr = nullptr;  // 指针可以为空
// ref = &b;  // 引用不能重新绑定

nullptr 的含义是什么?

nullptr 是C++11引入的,用来统一表示“空指针”概念。它替代了之前使用的 NULL 宏定义。

  • 类型安全: 与旧的 NULL 不同,nullptr 有明确的类型,是 std::nullptr_t 类型的常量值,它可以自动转换为任何指针类型,但它不是整数(这避免了 NULL 被解释为整数的歧义)。nullptr 只能用于指针类型,不能用于非指针类型的场景。
  • 表示空指针: nullptr 代表一个明确的“空指针”,即它不指向任何有效的内存地址。

指针的基本注意事项

  • 解引用空指针:在解引用指针之前,确保它指向有效的内存,否则会导致运行时错误(如段错误)。
  • 初始化指针:指针必须初始化(如nullptr或指向有效内存),否则可能成为悬挂指针或野指针。
  • 动态内存管理:在使用new分配的内存时,必须记得用delete释放,防止内存泄漏。

悬挂指针与野指针

  • 悬挂指针(dangling pointer):指向已经被释放或销毁的内存地址。
  • 野指针(wild pointer):未初始化的指针,指向随机的内存位置。
int* ptr = new int(5);
delete ptr;  // 释放内存
// 此时ptr是悬挂指针,因为它指向的内存已经释放
ptr = nullptr;  // 通过将指针置为nullptr避免悬挂指针

如何避免悬挂指针?

  • 使用智能指针:智能指针自动管理内存,避免悬挂指针。
  • 设置指针为nullptr:在释放内存后,将指针置为nullptr,避免误用。
  • 尽量减少使用原始指针:优先使用智能指针和引用,减少直接使用原始指针的场景。
  • 注意作用域:确保指针的作用域与其指向的对象保持一致,避免指向已经销毁的对象。
int* ptr = new int(10);
delete ptr;
ptr = nullptr;  // 避免悬挂指针

智能指针及其原理

C++11引入了智能指针,它们自动管理内存,避免手动delete。主要有三种类型的智能指针:

  • std::unique_ptr:独占所有权,指针不能共享,生命周期结束时自动释放内存。
  • std::shared_ptr:允许多个智能指针共享同一个对象,使用引用计数来管理内存。引用计数为0时,自动释放内存。
  • std::weak_ptr:与shared_ptr配合使用,不影响引用计数,防止循环引用。
#include <memory>

std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::cout << *ptr1 << std::endl;  // 输出10

std::shared_ptr<int> ptr2 = std::make_shared<int>(20);
std::shared_ptr<int> ptr3 = ptr2;  // 引用计数增加
std::cout << *ptr3 << std::endl;  // 输出20

原理:智能指针通过构造函数和析构函数控制指针的生命周期。当智能指针超出作用域或引用计数为0时,自动释放所管理的内存,避免内存泄漏。

unique_ptr

  • 独占所有权:某个对象只允许有一个智能指针来管理它,即对象的所有权是唯一的。当该指针被销毁时,所管理的对象会自动被释放。
  • 适合使用场景:动态分配的资源不需要共享,或希望确保资源的唯一性,例如RAII模式下的资源管理。

注意事项

  • 不能复制:std::unique_ptr的所有权是独占的,不能进行复制操作,但可以通过移动语义转移所有权。
  • 适合动态分配对象时,避免忘记释放资源的情况。
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
    void display() { std::cout << "MyClass::display\n"; }
};

int main() {
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>(); // 创建一个unique_ptr

    ptr1->display();  // 通过ptr1调用对象的方法

    // 不能复制unique_ptr,但可以移动
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 移动所有权

    if (ptr1 == nullptr) {
        std::cout << "ptr1 is now nullptr\n";  // 移动后,ptr1为空
    }

    ptr2->display();  // ptr2接管了对象的所有权

    // ptr2被销毁时,自动释放MyClass对象
    return 0;
}

输出

MyClass constructor
MyClass::display
ptr1 is now nullptr
MyClass::display
MyClass destructor

shared_ptr

  • 共享所有权:允许多个智能指针共享一个对象的所有权。当最后一个指针销毁时,所管理的对象才会被释放。
  • 适合使用场景:多个对象或函数需要共享同一资源,比如多个模块之间共享的缓存或数据。

注意事项

  • 引用计数:std::shared_ptr会维护一个引用计数,当有新的shared_ptr指向对象时,计数增加;当一个shared_ptr销毁时,计数减少。只有当计数为0时,才释放对象。
  • 循环引用问题:当两个shared_ptr对象互相引用时,会导致引用计数无法归零,造成内存泄漏。需要搭配std::weak_ptr来避免循环引用。
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
    void display() { std::cout << "MyClass::display\n"; }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();  // 创建shared_ptr
    {
        std::shared_ptr<MyClass> ptr2 = ptr1;  // 共享所有权
        std::cout << "ptr1 use count: " << ptr1.use_count() << '\n'; // 查看引用计数
        std::cout << "ptr2 use count: " << ptr2.use_count() << '\n';

        ptr2->display();  // 通过ptr2调用对象方法
    }  // ptr2出了作用域,引用计数减1

    std::cout << "ptr1 use count after ptr2 goes out of scope: " << ptr1.use_count() << '\n';
    
    ptr1->display();  // ptr1仍然可以使用

    // ptr1被销毁时,自动释放MyClass对象
    return 0;
}

输出

MyClass constructor
ptr1 use count: 2
ptr2 use count: 2
MyClass::display
ptr1 use count after ptr2 goes out of scope: 1
MyClass::display
MyClass destructor

weak_ptr

std::shared_ptr使用引用计数机制管理资源,但如果两个shared_ptr对象互相持有对方的引用,会形成循环引用,导致引用计数无法归零,资源永远无法释放。

解决方案

使用std::weak_ptr来打破循环引用。std::weak_ptr不参与引用计数,不会阻止所管理对象的销毁。它可以从shared_ptr构造,但不能直接使用,需要通过lock()方法临时提升为shared_ptr。

#include <iostream>
#include <memory>

class Node;

class Node {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // 使用weak_ptr打破循环引用
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();

    node1->next = node2;  // node1指向node2
    node2->prev = node1;  // node2通过weak_ptr指向node1

    return 0;  // node1和node2出了作用域,自动销毁
}

输出

Node destroyed
Node destroyed