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

C++指针的典型应用场景

2023-06-097.4k 阅读

动态内存分配与管理

在 C++ 编程中,动态内存分配是指针的一个重要应用场景。在许多情况下,我们无法在编译时确定需要多少内存,比如需要根据用户输入来决定数组的大小。这时,就需要在程序运行时动态分配内存。

堆内存分配与释放

使用 new 运算符可以在堆上分配内存,new 会返回一个指向所分配内存起始地址的指针。例如:

int* ptr = new int;
*ptr = 10;

在这段代码中,new int 在堆上分配了一个 int 类型大小的内存空间,并返回一个指向该空间的指针 ptr。然后通过指针 ptr 对这块内存进行赋值。

当我们不再需要这块动态分配的内存时,需要使用 delete 运算符来释放它,以避免内存泄漏:

delete ptr;

如果分配的是数组,则需要使用 delete[]

int* arr = new int[5];
for (int i = 0; i < 5; i++) {
    arr[i] = i;
}
delete[] arr;

这里 new int[5] 分配了一个包含 5 个 int 类型元素的数组,最后使用 delete[] 释放整个数组的内存。

实现动态数据结构

指针在构建动态数据结构(如链表、树等)中起着关键作用。以链表为例,链表的每个节点都是动态分配的,节点之间通过指针相连。

下面是一个简单的单向链表的实现:

struct Node {
    int data;
    Node* next;
};

Node* createNode(int value) {
    Node* newNode = new Node;
    newNode->data = value;
    newNode->next = nullptr;
    return newNode;
}

void insertNode(Node** head, int value) {
    Node* newNode = createNode(value);
    if (*head == nullptr) {
        *head = newNode;
    } else {
        Node* current = *head;
        while (current->next != nullptr) {
            current = current->next;
        }
        current->next = newNode;
    }
}

void deleteList(Node** head) {
    Node* current = *head;
    Node* next;
    while (current != nullptr) {
        next = current->next;
        delete current;
        current = next;
    }
    *head = nullptr;
}

在上述代码中,Node 结构体包含一个 int 类型的数据成员 data 和一个指向下一个节点的指针 nextcreateNode 函数用于创建新节点,insertNode 函数用于向链表末尾插入新节点,deleteList 函数用于释放整个链表的内存。

这里需要注意的是,在 insertNode 函数中,参数 head 是一个指向指针的指针。这是因为我们需要修改 head 指针本身(例如当链表为空时,head 需要指向新创建的节点)。如果只传递 head 指针,函数内部对 head 的修改不会影响到函数外部的 head 变量。

函数参数传递与返回值

指针在函数参数传递和返回值方面也有重要应用,这可以实现一些特殊的功能,并提高程序的效率。

传递指针参数

通过传递指针作为函数参数,可以在函数内部修改调用函数中变量的值。例如:

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int num1 = 5;
    int num2 = 10;
    swap(&num1, &num2);
    return 0;
}

swap 函数中,参数 ab 是指向 int 类型变量的指针。通过指针,函数可以直接访问和修改调用函数中的 num1num2 的值。如果不使用指针,仅仅传递变量的值,函数内部对参数的修改不会影响到外部变量。

返回指针

函数也可以返回指针。例如,下面的函数返回一个动态分配数组的指针:

int* createArray(int size) {
    int* arr = new int[size];
    for (int i = 0; i < size; i++) {
        arr[i] = i;
    }
    return arr;
}

int main() {
    int* myArray = createArray(5);
    // 使用 myArray
    delete[] myArray;
    return 0;
}

createArray 函数中,动态分配了一个大小为 size 的数组,并返回指向该数组的指针。调用函数 main 中接收这个指针,并可以使用这个数组。需要注意的是,调用者有责任在使用完毕后释放这个动态分配的数组,以避免内存泄漏。

多态与虚函数表

在面向对象编程中,C++ 的多态性是一个重要特性,而指针在实现多态中扮演着关键角色。

虚函数与动态绑定

当一个类包含虚函数时,通过基类指针或引用调用虚函数,会根据指针或引用实际指向的对象类型来决定调用哪个函数版本,这就是动态绑定。

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape" << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

void drawShape(Shape* shape) {
    shape->draw();
}

int main() {
    Circle circle;
    Rectangle rectangle;
    drawShape(&circle);
    drawShape(&rectangle);
    return 0;
}

在上述代码中,Shape 类是基类,包含虚函数 drawCircleRectangle 类继承自 Shape 类,并分别重写了 draw 函数。drawShape 函数接受一个 Shape* 类型的指针作为参数,并调用 draw 函数。在 main 函数中,分别传递 CircleRectangle 对象的指针给 drawShape 函数,实际调用的是对应对象的 draw 函数版本,这就是多态性的体现。

虚函数表指针

在底层实现中,每个包含虚函数的类都有一个虚函数表(vtable)。对象内部会有一个指向虚函数表的指针(vptr)。当通过指针或引用调用虚函数时,程序会根据对象的 vptr 找到对应的虚函数表,然后在虚函数表中查找并调用正确的函数版本。

例如,当创建一个 Circle 对象时,它的 vptr 会指向 Circle 类的虚函数表,该虚函数表中存放着 Circle 类重写的 draw 函数的地址。当通过 Shape* 指针调用 draw 函数时,程序会通过 vptr 找到 Circle 类的虚函数表,进而调用 Circle 类的 draw 函数。

操作数组与字符串

指针在操作数组和字符串方面提供了高效且灵活的方式。

数组指针与指针数组

数组指针是指向数组的指针,而指针数组是数组的每个元素都是指针。

数组指针

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

在上述代码中,int (*ptr)[5] 定义了一个数组指针 ptr,它指向一个包含 5 个 int 类型元素的数组。&arr 获取数组 arr 的地址并赋值给 ptr。通过 (*ptr)[i] 可以访问数组中的元素。

指针数组

int main() {
    int num1 = 10;
    int num2 = 20;
    int* ptrArr[2] = {&num1, &num2};
    for (int i = 0; i < 2; i++) {
        std::cout << *ptrArr[i] << " ";
    }
    return 0;
}

这里 ptrArr 是一个指针数组,每个元素都是一个 int* 类型的指针,分别指向 num1num2。通过 *ptrArr[i] 可以访问指针所指向的变量的值。

字符串操作

在 C++ 中,C 风格字符串是以 '\0' 结尾的字符数组,指针在操作 C 风格字符串时非常有用。

#include <cstring>
#include <iostream>

int main() {
    char str1[] = "Hello";
    char str2[6];
    char* ptr1 = str1;
    char* ptr2 = str2;
    while (*ptr1 != '\0') {
        *ptr2 = *ptr1;
        ptr1++;
        ptr2++;
    }
    *ptr2 = '\0';
    std::cout << "Copied string: " << str2 << std::endl;
    return 0;
}

在这段代码中,通过指针 ptr1ptr2 实现了字符串的复制。ptr1 指向源字符串 str1ptr2 指向目标字符串 str2。通过循环逐个字符复制,直到遇到源字符串的结束符 '\0',最后在目标字符串末尾添加 '\0'

同时,C++ 标准库中的 string 类也在内部使用指针来管理字符串的存储。string 类封装了许多字符串操作,使得字符串处理更加方便和安全,但了解底层指针的操作有助于理解 string 类的实现原理。

内存映射与文件 I/O

在一些系统编程场景中,指针可以用于内存映射和文件 I/O 操作,以提高数据访问效率。

内存映射文件

内存映射文件是将文件的内容直接映射到进程的地址空间,这样可以像访问内存一样访问文件内容,而不需要频繁的磁盘 I/O 操作。

在 POSIX 系统中,可以使用 mmap 函数来实现内存映射:

#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>

int main() {
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }
    void* ptr = mmap(nullptr, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }
    char* data = static_cast<char*>(ptr);
    std::cout << "File content: " << data << std::endl;
    // 修改文件内容
    std::strcpy(data, "New content");
    if (munmap(ptr, sb.st_size) == -1) {
        perror("munmap");
        close(fd);
        return 1;
    }
    close(fd);
    return 0;
}

在上述代码中,首先通过 open 函数打开文件,然后使用 fstat 获取文件的状态信息。接着使用 mmap 函数将文件映射到内存,返回一个指向映射内存起始地址的指针 ptr。通过这个指针可以像访问普通内存一样访问文件内容,修改后通过 munmap 函数解除映射。

文件 I/O 中的指针应用

在标准 C++ 的文件 I/O 操作中,虽然 ifstreamofstream 等类封装了文件操作,但在一些底层实现或特殊需求场景下,指针也会起到作用。

例如,在处理二进制文件时,有时需要直接操作文件中的数据块,这时可以使用指针来定位和读取/写入数据。

#include <iostream>
#include <fstream>

struct Data {
    int value;
    char text[10];
};

int main() {
    Data data = {42, "Hello"};
    std::ofstream outFile("data.bin", std::ios::binary);
    if (outFile.is_open()) {
        outFile.write(reinterpret_cast<char*>(&data), sizeof(Data));
        outFile.close();
    } else {
        std::cerr << "Unable to open file for writing" << std::endl;
    }
    Data readData;
    std::ifstream inFile("data.bin", std::ios::binary);
    if (inFile.is_open()) {
        inFile.read(reinterpret_cast<char*>(&readData), sizeof(Data));
        inFile.close();
        std::cout << "Read value: " << readData.value << ", text: " << readData.text << std::endl;
    } else {
        std::cerr << "Unable to open file for reading" << std::endl;
    }
    return 0;
}

在这段代码中,reinterpret_cast<char*>(&data)Data 结构体的地址转换为 char* 类型的指针,以便 writeread 函数能够正确地操作二进制数据。通过这种方式,可以直接将结构体数据写入文件并从文件中读取,而不需要逐个处理结构体的成员。

与 C 语言的兼容性与交互

由于 C++ 对 C 语言的兼容性,指针在与 C 语言代码交互以及处理 C 风格的库时有着重要作用。

调用 C 函数

许多系统库和底层库仍然是用 C 语言编写的。在 C++ 程序中调用这些 C 函数时,常常需要处理指针。

例如,假设存在一个 C 函数 c_function 定义在 c_library.c 文件中:

#include <stdio.h>
void c_function(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;
    }
}

在 C++ 中调用这个函数:

extern "C" {
    void c_function(int* arr, int size);
}

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

这里通过 extern "C" 告诉编译器,c_function 是一个 C 语言函数,遵循 C 语言的函数命名和调用约定。然后在 C++ 代码中可以像调用普通函数一样调用它,传递数组指针和数组大小。

处理 C 风格库的数据结构

许多 C 风格的库使用指针来构建数据结构,例如 OpenGL 图形库。在 C++ 中使用这些库时,需要正确处理指针类型。

例如,在 OpenGL 中创建顶点数组对象(VAO)和顶点缓冲对象(VBO):

#include <GL/glut.h>
#include <iostream>

const GLfloat vertices[] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f, 0.5f, 0.0f
};

int main(int argc, char** argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
    glutInitWindowSize(800, 600);
    glutCreateWindow("OpenGL with C++");
    GLuint VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);
    GLuint VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // 设置顶点属性指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    // 渲染循环等代码
    glutMainLoop();
    return 0;
}

在上述代码中,glBufferData 函数接受一个指向顶点数据的指针 verticesglVertexAttribPointer 函数中的 (GLvoid*)0 也是一个指针,表示数据的偏移量。这些指针操作都是与 OpenGL 库交互的关键部分,正确处理指针对于在 C++ 中使用 OpenGL 进行图形编程至关重要。

指针在 C++ 编程中有着广泛且重要的应用场景,从基本的动态内存管理到复杂的面向对象多态实现,从数组和字符串操作到系统级的内存映射与文件 I/O,以及与 C 语言的兼容性交互等方面,指针都发挥着不可或缺的作用。深入理解指针的应用,能够帮助开发者编写出高效、灵活且功能强大的 C++ 程序。