MST

星途 面试题库

面试题:C++ 中指针与引用在函数参数传递中的性能分析

在 C++ 编程中,当函数的参数分别为指针和引用时,从内存分配、栈空间使用、数据拷贝次数等方面分析它们的性能差异,并举例说明在不同场景下如何选择更优的参数传递方式。
16.6万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

性能差异分析

  1. 内存分配
    • 指针:指针本身是一个变量,存储的是所指向对象的地址,在函数调用时,会为指针变量在栈上分配内存空间,其大小取决于系统架构(32 位系统通常为 4 字节,64 位系统通常为 8 字节)。
    • 引用:引用本质上是一个别名,在函数调用时,并不会为引用额外分配内存空间,它与被引用的对象共享同一块内存。
  2. 栈空间使用
    • 指针:由于指针本身需要在栈上分配内存来存储地址,所以会占用一定的栈空间。
    • 引用:因为不额外分配内存,所以相比指针,在栈空间使用上更节省。
  3. 数据拷贝次数
    • 指针:传递指针时,只是将指针变量的值(即地址)拷贝一份到函数栈中,无论所指向的数据量多大,拷贝的只是一个固定大小的地址值,数据本身不会被拷贝。
    • 引用:传递引用时,同样不会拷贝数据本身,而是直接使用原对象的别名,所以在数据拷贝次数上,指针和引用是相同的,都只进行了地址(或别名)的传递,而非数据内容的拷贝。

不同场景下的选择

  1. 当需要表示“无对象”情况时
    • 指针:指针可以为 nullptr,表示不指向任何对象。例如,在链表节点的删除操作中,删除节点后,指向该节点的指针可以设为 nullptr
    struct ListNode {
        int val;
        ListNode* next;
        ListNode(int x) : val(x), next(nullptr) {}
    };
    void deleteNode(ListNode*& head, int val) {
        if (head == nullptr) return;
        if (head->val == val) {
            ListNode* temp = head;
            head = head->next;
            delete temp;
            return;
        }
        ListNode* cur = head;
        while (cur->next!= nullptr && cur->next->val!= val) {
            cur = cur->next;
        }
        if (cur->next!= nullptr) {
            ListNode* temp = cur->next;
            cur->next = cur->next->next;
            delete temp;
        }
    }
    
    • 引用:引用必须初始化,不能表示“无对象”的情况。所以在这种可能需要表示“无对象”的场景下,应选择指针。
  2. 当需要改变指向对象时
    • 指针:指针可以重新赋值,指向不同的对象。例如,在动态内存管理中,有时需要改变指针的指向。
    int* ptr = new int(5);
    int* newPtr = new int(10);
    ptr = newPtr;
    delete newPtr;
    
    • 引用:引用一旦初始化,就不能再引用其他对象。所以如果需要在函数内部改变所指向的对象,应选择指针。
  3. 当需要确保对象存在且不想额外处理空指针情况时
    • 引用:由于引用必须初始化且不能为 nullptr,所以在调用函数时,能确保引用所关联的对象一定存在,代码更加简洁和安全。例如,在一些内部逻辑中,已知对象必然存在的场景。
    void printValue(const int& ref) {
        std::cout << ref << std::endl;
    }
    int main() {
        int num = 10;
        printValue(num);
        return 0;
    }
    
    • 指针:使用指针时,每次使用前都需要检查是否为 nullptr,否则可能导致程序崩溃。在这种情况下,选择引用更优。
  4. 当传递大对象时
    • 指针和引用:因为指针和引用都不会拷贝大对象本身,只是传递地址或别名,所以在性能上都优于值传递。在这种场景下,两者都可以选择,但如果确定对象一定存在,引用的语法更简洁。例如:
    class BigObject {
    public:
        int data[10000];
        BigObject() {
            for (int i = 0; i < 10000; i++) {
                data[i] = i;
            }
        }
    };
    void processObject(const BigObject& obj) {
        // 处理对象逻辑
    }
    void processObjectPtr(const BigObject* ptr) {
        if (ptr!= nullptr) {
            // 处理对象逻辑
        }
    }
    
    这里使用引用 processObject 函数调用更简洁,且不需要额外检查 nullptr,如果对象可能为空,processObjectPtr 使用指针更合适。