MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 指针用法详解

2022-12-296.9k 阅读

指针的基本概念

在C++ 中,指针是一种特殊的变量类型,它存储的是内存地址。理解指针,首先要明白内存的组织方式。计算机的内存就像是一排连续编号的小格子,每个格子可以存放数据。这些编号就是内存地址。

例如,当我们声明一个变量 int num = 10; 时,编译器会在内存中为 num 分配一块空间来存储整数 10。这块空间有一个对应的地址。指针变量就是用来存储这个地址的。

声明指针变量的语法为:数据类型 *指针变量名;。例如:

int *ptr;

这里声明了一个名为 ptr 的指针变量,它可以指向 int 类型的数据。需要注意的是,* 在这里用于声明指针变量,表明 ptr 是一个指针,而不是乘法运算符。

初始化指针

指针变量声明后,它的值是未定义的,也就是它并没有指向任何有效的内存地址。在使用指针之前,必须对其进行初始化。

  1. 使用变量地址初始化指针 可以使用取地址运算符 & 获取变量的地址,并将其赋给指针。例如:
int num = 10;
int *ptr = #

在上述代码中,&num 获取了 num 变量的地址,并将其赋值给 ptr,此时 ptr 就指向了 num

  1. 使用 nullptr 初始化指针 在C++ 11 及以后,可以使用 nullptr 来初始化指针,表明指针不指向任何有效对象。例如:
int *ptr = nullptr;

在C++ 11 之前,通常使用 NULL 来表示空指针,但 NULL 本质上是一个整数(通常为0),在某些情况下可能会导致类型混淆。而 nullptr 是一个真正的空指针类型,能避免这类问题。

解引用指针

解引用指针是指通过指针访问它所指向的内存位置的值。使用解引用运算符 * 来实现。例如:

int num = 10;
int *ptr = #
std::cout << *ptr << std::endl; // 输出 10

这里 *ptr 表示 ptr 所指向的内存位置的值,也就是 num 的值。

通过解引用指针,还可以修改所指向变量的值。例如:

int num = 10;
int *ptr = &num;
*ptr = 20;
std::cout << num << std::endl; // 输出 20

在上述代码中,通过 *ptr = 20; 改变了 num 的值,因为 ptr 指向 num,解引用 ptr 就相当于直接操作 num

指针与数组

在C++ 中,指针和数组有着紧密的联系。数组名本身就可以看作是一个指针常量,它指向数组的第一个元素。例如:

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;

这里 arr 指向数组的第一个元素 arr[0]ptr 也指向 arr[0]

通过指针访问数组元素:

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
for (int i = 0; i < 5; i++) {
    std::cout << *(ptr + i) << " ";
}
std::cout << std::endl;

上述代码中,*(ptr + i) 等同于 arr[i],通过指针偏移的方式访问数组的各个元素。

另外,也可以通过指针来动态分配数组内存。使用 new 运算符:

int n = 5;
int *arr = new int[n];
for (int i = 0; i < n; i++) {
    arr[i] = i + 1;
}
for (int i = 0; i < n; i++) {
    std::cout << arr[i] << " ";
}
std::cout << std::endl;
delete[] arr;

在这段代码中,new int[n] 动态分配了一个包含 nint 类型元素的数组,并返回指向这个数组首元素的指针 arr。使用完后,要使用 delete[] 来释放内存,以避免内存泄漏。

指针与函数

  1. 指针作为函数参数 指针可以作为函数参数传递,这样可以在函数内部修改调用函数中变量的值。例如:
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() {
    int num1 = 5, num2 = 10;
    swap(&num1, &num2);
    std::cout << "num1: " << num1 << ", num2: " << num2 << std::endl;
    return 0;
}

swap 函数中,通过指针参数 ab 直接操作了 num1num2 的值,实现了交换功能。如果不使用指针,函数参数传递的是值的副本,无法直接修改外部变量的值。

  1. 函数返回指针 函数也可以返回指针。例如:
int *createArray(int size) {
    int *arr = new int[size];
    for (int i = 0; i < size; i++) {
        arr[i] = i + 1;
    }
    return arr;
}
int main() {
    int n = 5;
    int *arr = createArray(n);
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    delete[] arr;
    return 0;
}

createArray 函数中,动态分配了一个数组并返回指向该数组的指针。在 main 函数中接收这个指针并使用,最后要记得释放内存。

需要注意的是,返回局部变量的指针是错误的做法,因为局部变量在函数结束时会被销毁,指针指向的内存就变成了无效内存。例如:

int *wrongFunction() {
    int num = 10;
    return &num;
}

wrongFunction 中返回了局部变量 num 的指针,当函数结束,num 被销毁,这个指针就变成了野指针,使用它会导致未定义行为。

多级指针

多级指针是指针的指针。例如,二级指针是指向指针的指针。声明二级指针的语法为:数据类型 **指针变量名;。例如:

int num = 10;
int *ptr1 = &num;
int **ptr2 = &ptr1;

这里 ptr1 是一级指针,指向 numptr2 是二级指针,指向 ptr1

解引用二级指针需要两次解引用操作。例如:

int num = 10;
int *ptr1 = &num;
int **ptr2 = &ptr1;
std::cout << **ptr2 << std::endl; // 输出 10

这里 **ptr2 先解引用 ptr2 得到 ptr1,再解引用 ptr1 得到 num 的值。

多级指针在一些复杂的数据结构如链表的链表等场景中有应用。例如,在双向链表的插入操作中,如果要更新头指针(头指针可能会改变),就可能会用到二级指针。

指针与结构体

  1. 结构体指针 可以定义指向结构体的指针。例如:
struct Student {
    std::string name;
    int age;
};
int main() {
    Student stu = {"Alice", 20};
    Student *stuPtr = &stu;
    std::cout << "Name: " << stuPtr->name << ", Age: " << stuPtr->age << std::endl;
    return 0;
}

这里定义了一个 Student 结构体,然后创建了一个结构体变量 stu 和一个指向 stu 的指针 stuPtr。通过 stuPtr->namestuPtr->age 来访问结构体成员。-> 运算符是用于通过指针访问结构体成员的。

  1. 动态分配结构体内存 可以使用 new 运算符动态分配结构体内存,并通过指针来操作。例如:
struct Point {
    int x;
    int y;
};
int main() {
    Point *pointPtr = new Point();
    pointPtr->x = 10;
    pointPtr->y = 20;
    std::cout << "x: " << pointPtr->x << ", y: " << pointPtr->y << std::endl;
    delete pointPtr;
    return 0;
}

在上述代码中,new Point() 动态分配了一个 Point 结构体对象,并返回指向它的指针 pointPtr。使用完后,通过 delete 释放内存。

指针运算

  1. 指针的算术运算 指针可以进行一些算术运算,主要包括加法和减法。
    • 指针加法:当指针加上一个整数 n 时,指针会移动 n 个其所指向数据类型大小的字节数。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
ptr = ptr + 2; // ptr 现在指向 arr[2]
std::cout << *ptr << std::endl; // 输出 3
  • 指针减法:两个指针相减可以得到它们之间相隔的元素个数(前提是它们指向同一块连续内存区域,如数组)。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[3];
int diff = ptr2 - ptr1;
std::cout << "Difference: " << diff << std::endl; // 输出 3
  1. 指针的比较运算 指针可以进行比较运算,如 ==!=<> 等。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[2];
if (ptr1 < ptr2) {
    std::cout << "ptr1 is before ptr2" << std::endl;
}

指针比较通常用于判断指针在内存中的相对位置,在数组场景中比较常见。

指针的类型转换

  1. C风格的指针类型转换 C风格的类型转换可以用于指针类型转换。例如:
int num = 10;
int *intPtr = &num;
char *charPtr = (char *)intPtr;

这里将 intPtr 转换为 charPtr,这种转换需要谨慎使用,因为 intchar 的大小不同,可能会导致数据截断或错误访问内存。

  1. C++风格的指针类型转换
    • static_cast:用于在相关类型之间进行转换,例如将 void* 转换为其他类型指针。例如:
void *voidPtr;
int num = 10;
voidPtr = &num;
int *intPtr = static_cast<int*>(voidPtr);
  • reinterpret_cast:用于不相关类型之间的转换,通常会改变指针的位模式。例如:
int num = 10;
int *intPtr = &num;
char *charPtr = reinterpret_cast<char*>(intPtr);

与C风格转换类似,reinterpret_cast 也需要非常小心,因为它可能会导致未定义行为。

  • const_cast:用于去除指针的 const 特性。例如:
const int num = 10;
const int *constPtr = &num;
int *nonConstPtr = const_cast<int*>(constPtr);

这种转换在某些情况下可能有用,但要注意如果通过 nonConstPtr 修改了原本 const 对象的值,会导致未定义行为。

指针与内存管理

  1. 动态内存分配与释放
    • newdelete 运算符new 用于动态分配内存,delete 用于释放内存。例如:
int *numPtr = new int;
*numPtr = 10;
std::cout << *numPtr << std::endl;
delete numPtr;

对于数组,使用 new[]delete[]。例如:

int *arrPtr = new int[5];
for (int i = 0; i < 5; i++) {
    arrPtr[i] = i + 1;
}
for (int i = 0; i < 5; i++) {
    std::cout << arrPtr[i] << " ";
}
std::cout << std::endl;
delete[] arrPtr;

如果不释放动态分配的内存,会导致内存泄漏,即程序占用的内存不断增加,最终可能导致系统资源耗尽。

  1. 智能指针 为了简化内存管理并避免内存泄漏,C++ 引入了智能指针。
    • std::unique_ptrstd::unique_ptr 是一种独占所有权的智能指针。当 std::unique_ptr 被销毁时,它所指向的对象也会被自动销毁。例如:
#include <memory>
std::unique_ptr<int> uniquePtr(new int(10));
std::cout << *uniquePtr << std::endl;

这里 uniquePtr 独占了 new int(10) 分配的内存,当 uniquePtr 超出作用域时,内存会自动释放。

  • std::shared_ptrstd::shared_ptr 允许多个指针共享同一个对象。它使用引用计数来跟踪有多少个 std::shared_ptr 指向同一个对象。当引用计数为0时,对象被自动销毁。例如:
#include <memory>
std::shared_ptr<int> sharedPtr1(new int(10));
std::shared_ptr<int> sharedPtr2 = sharedPtr1;
std::cout << "Reference count: " << sharedPtr1.use_count() << std::endl;

在上述代码中,sharedPtr1sharedPtr2 共享同一个 int 对象,use_count() 可以获取当前的引用计数。

  • std::weak_ptrstd::weak_ptr 是一种弱引用,它不增加对象的引用计数。主要用于解决 std::shared_ptr 中的循环引用问题。例如:
#include <memory>
std::shared_ptr<int> sharedPtr(new int(10));
std::weak_ptr<int> weakPtr = sharedPtr;
if (auto locked = weakPtr.lock()) {
    std::cout << *locked << std::endl;
}

这里 weakPtr 指向 sharedPtr 所指向的对象,但不增加引用计数。lock() 方法可以尝试获取一个 std::shared_ptr,如果对象已经被销毁,lock() 会返回一个空的 std::shared_ptr

通过合理使用智能指针,可以大大减少手动内存管理带来的错误,提高程序的稳定性和可维护性。

指针的常见错误与陷阱

  1. 野指针 野指针是指向已释放或未初始化内存的指针。例如:
int *ptr;
std::cout << *ptr << std::endl; // 未初始化指针,导致未定义行为
int *numPtr = new int(10);
delete numPtr;
std::cout << *numPtr << std::endl; // numPtr 变成野指针,导致未定义行为

避免野指针的方法是在声明指针时立即初始化,并且在释放内存后将指针设为 nullptr

  1. 悬空指针 悬空指针与野指针类似,通常是由于对象被释放但指针未更新导致的。例如:
int *numPtr = new int(10);
int *anotherPtr = numPtr;
delete numPtr;
// anotherPtr 现在是悬空指针

为了避免悬空指针,在释放对象后,应该将所有指向该对象的指针都更新为 nullptr 或使其指向有效的对象。

  1. 内存泄漏 如前面提到的,忘记释放动态分配的内存会导致内存泄漏。例如:
while (true) {
    int *numPtr = new int(10);
    // 这里没有释放 numPtr,随着循环进行会导致内存泄漏
}

使用智能指针可以有效避免这类内存泄漏问题。

  1. 指针类型不匹配 进行指针类型转换时,如果类型不匹配,可能会导致错误。例如:
int num = 10;
int *intPtr = &num;
double *doublePtr = (double *)intPtr;
std::cout << *doublePtr << std::endl; // 类型不匹配,导致未定义行为

在进行指针类型转换时,要确保转换是合理且安全的。

指针在实际项目中的应用

  1. 数据结构实现
    • 链表:链表是一种常用的数据结构,指针在链表的实现中起着关键作用。例如,单链表的节点结构通常定义如下:
struct ListNode {
    int data;
    ListNode *next;
    ListNode(int val) : data(val), next(nullptr) {}
};

这里 next 指针指向下一个节点,通过指针连接各个节点形成链表。插入、删除等操作都依赖指针来修改节点之间的连接关系。

  • :在树结构如二叉树中,指针用于表示节点之间的父子关系。例如,二叉树节点结构:
struct TreeNode {
    int data;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int val) : data(val), left(nullptr), right(nullptr) {}
};

通过 leftright 指针构建二叉树的层次结构,进行遍历(如前序、中序、后序遍历)等操作也依赖指针移动。

  1. 图形处理 在图形处理中,指针可用于表示图形对象及其关系。例如,在一个简单的二维图形库中,可能会有一个 Shape 基类,以及 CircleRectangle 等派生类。可以使用指针数组或链表来管理多个图形对象。
class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() {}
};
class Circle : public Shape {
public:
    void draw() override {
        // 绘制圆的代码
    }
};
class Rectangle : public Shape {
public:
    void draw() override {
        // 绘制矩形的代码
    }
};
int main() {
    Shape *shapes[2];
    shapes[0] = new Circle();
    shapes[1] = new Rectangle();
    for (int i = 0; i < 2; i++) {
        shapes[i]->draw();
        delete shapes[i];
    }
    return 0;
}

这里通过指针数组 shapes 管理不同类型的图形对象,利用多态性调用不同图形的 draw 方法。

  1. 内存池实现 在一些高性能应用中,为了减少频繁的内存分配和释放开销,会实现内存池。内存池使用指针来管理预先分配的内存块。例如,一个简单的内存池实现:
class MemoryPool {
private:
    char *pool;
    int poolSize;
    char *nextFree;
public:
    MemoryPool(int size) : poolSize(size) {
        pool = new char[poolSize];
        nextFree = pool;
    }
    void* allocate(int size) {
        if (nextFree + size <= pool + poolSize) {
            void *result = nextFree;
            nextFree += size;
            return result;
        }
        return nullptr;
    }
    ~MemoryPool() {
        delete[] pool;
    }
};

在这个内存池中,pool 指针指向预先分配的内存块,nextFree 指针记录下一个可用的内存位置。通过指针操作实现内存的分配和管理。

通过上述对指针各个方面的详细讲解,希望能帮助读者深入理解C++ 指针的用法及其在不同场景下的应用,在实际编程中能够正确、高效地使用指针。