C++中的引用传参和指针传参

核心概念对比

概念 C++ 前端 (JS/TS)
值传递 默认方式,完整拷贝 基本类型 (number, string等)
引用传递 需显式使用 & 或指针 对象、数组、函数等引用类型
指针传递 传递内存地址 类似传递对象的引用

1. 值传递(默认)- 完整拷贝

cpp 复制代码
#include <iostream>
using namespace std;

struct Point {
    int x, y;
    Point(int a, int b) : x(a), y(b) {
        cout << "构造: (" << x << "," << y << ")" << endl;
    }
    Point(const Point& p) : x(p.x), y(p.y) {
        cout << "拷贝构造: (" << x << "," << y << ")" << endl;
    }
    ~Point() {
        cout << "析构: (" << x << "," << y << ")" << endl;
    }
};

// ❌ 值传递:发生完整拷贝
void byValue(Point p) {
    p.x = 100;  // 修改的是副本,不影响原对象
    cout << "函数内: (" << p.x << "," << p.y << ")" << endl;
}

int main() {
    Point p1(1, 2);
    cout << "--- 调用函数 ---" << endl;
    byValue(p1);
    cout << "--- 函数返回 ---" << endl;
    cout << "原对象: (" << p1.x << "," << p1.y << ")" << endl;  // 仍是 (1,2)
    return 0;
}

输出:

makefile 复制代码
构造: (1,2)
--- 调用函数 ---
拷贝构造: (1,2)
函数内: (100,2)
析构: (100,2)
--- 函数返回 ---
原对象: (1,2)
析构: (1,2)

⚠️ 性能问题:大对象会发生昂贵的深拷贝


2. 引用传递(类似前端引用类型)

cpp 复制代码
// ✅ 引用传递:不拷贝,直接操作原对象(类似前端的对象引用)
void byReference(Point& p) {
    p.x = 100;  // 直接修改原对象!
    cout << "函数内: (" << p.x << "," << p.y << ")" << endl;
}

// ✅ const 引用:只读访问,不拷贝也不能修改
void byConstReference(const Point& p) {
    // p.x = 100;  // ❌ 编译错误:不能修改const引用
    cout << "只读访问: (" << p.x << "," << p.y << ")" << endl;
}

int main() {
    Point p1(1, 2);
    
    cout << "=== 引用传递 ===" << endl;
    byReference(p1);
    cout << "原对象已被修改: (" << p1.x << "," << p1.y << ")" << endl;  // (100,2)
    
    cout << "\n=== const引用 ===" << endl;
    byConstReference(p1);
    
    return 0;
}

与前端对比:

javascript 复制代码
// JavaScript - 对象天然是引用传递
function modify(obj) {
    obj.x = 100;  // 直接修改原对象
}
let p = {x: 1, y: 2};
modify(p);
console.log(p.x);  // 100 ✅

// C++ 必须显式加 & 才能实现同样效果

3. 指针传递(C风格,更灵活但危险)

cpp 复制代码
// 指针传递:传递内存地址
void byPointer(Point* p) {
    if (p != nullptr) {  // 必须检查空指针!
        p->x = 200;      // 使用 -> 访问成员
    }
}

// 指针的const版本
void byConstPointer(const Point* p) {
    // p->x = 200;  // ❌ 不能修改
    cout << p->x << endl;  // ✅ 可以读取
}

int main() {
    Point p1(1, 2);
    
    byPointer(&p1);  // 必须取地址
    cout << "修改后: (" << p1.x << "," << p1.y << ")" << endl;
    
    // 可以传 nullptr(这是与引用的重要区别)
    byPointer(nullptr);  // ✅ 合法
    
    return 0;
}

引用 vs 指针对比:

特性 引用 & 指针 *
语法 更简洁,像原对象 需解引用 ->*
空值 不能为 null 可以为 nullptr
重新绑定 不能改指向其他对象 可以指向不同对象
安全性 更高(必须初始化) 需检查空指针

4. 返回值优化(RVO)与移动语义

传统返回值(有拷贝)

cpp 复制代码
// ❌ C++11前:返回时会发生拷贝(或编译器RVO优化)
Point createPoint() {
    Point p(3, 4);
    return p;  // 理论上会拷贝,但编译器通常优化掉
}

// 接收时再次拷贝
Point p2 = createPoint();

现代C++:移动语义(C++11起)

cpp 复制代码
struct BigData {
    int* data;
    size_t size;
    
    // 移动构造函数(转移资源所有权,不拷贝数据)
    BigData(BigData&& other) noexcept 
        : data(other.data), size(other.size) {
        cout << "移动构造(偷资源)" << endl;
        other.data = nullptr;  // 置空源对象
        other.size = 0;
    }
    
    // 移动赋值
    BigData& operator=(BigData&& other) noexcept {
        cout << "移动赋值" << endl;
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
        }
        return *this;
    }
};

BigData createBigData() {
    BigData bd(1000000);  // 分配大量内存
    return bd;  // 触发移动语义,而非深拷贝!
}

int main() {
    BigData bd1 = createBigData();  // 移动构造,几乎零开销
    BigData bd2;
    bd2 = createBigData();          // 移动赋值
    return 0;
}

5. 最佳实践总结

cpp 复制代码
// ✅ 最佳实践速查表

// 1. 小对象(int, double, 小结构体):直接值传递
void f(int x, Point p);  

// 2. 大对象只读访问:const 引用(性能+安全)
void process(const BigData& data);

// 3. 需要修改原对象:非const引用
void modify(Point& p);

// 4. 参数可选(可能为空):指针
void maybeProcess(BigData* data);  // data可以是nullptr

// 5. 转移所有权(C++11):右值引用
void takeOwnership(BigData&& data);

// 6. 返回值:优先返回值(依赖RVO/移动),而非输出参数
BigData create();  // ✅ 现代C++推荐
void create(BigData& out);  // 旧式风格,避免

类比总结

cpp 复制代码
// C++ 显式控制传递方式
void f(Point p);      // 值传递(拷贝)    ← 像 JS 的 structuredClone
void f(Point& p);     // 引用传递(别名)   ← 像 JS 的对象引用
void f(const Point& p); // const引用       ← 像 JS 的只读引用
void f(Point* p);     // 指针传递         ← 像 JS 的弱引用/可空引用
void f(Point&& p);    // 右值引用(移动)   ← 像 JS 的 对象转移/解构赋值

关键区别 :C++ 默认是值语义 (拷贝),必须显式使用 & 才能获得引用语义 ;而前端(JS)对象默认就是引用语义


以下深入剖析 C++ 中引用传递和指针传递的本质差异。

核心对比表

维度 引用传递 T& 指针传递 T*
本质 别名(alias),必须绑定有效对象 内存地址,可独立存在
空值 ❌ 不能为 null ✅ 可以为 nullptr
重新绑定 ❌ 终身绑定,不能改指向 ✅ 可随时指向其他对象
语法 使用原对象语法 . 需解引用 ->*
初始化 必须初始化 可以延迟初始化
内存占用 通常优化为无开销 占用指针大小(4/8字节)

本质差异图解

scss 复制代码
引用 (&)                    指针 (*)
┌─────────┐                ┌─────────┐
│  引用变量  │ ──→ 对象      │ 指针变量  │ ──→ 对象
│  (别名)   │   (必须存在)   │ (地址)   │   或 ──→ nullptr
└─────────┘                └─────────┘
   语法糖,编译器处理           真正的内存实体

代码层面深度对比

1. 基础语法差异

cpp 复制代码
#include <iostream>
using namespace std;

struct Config {
    int port;
    string host;
};

// ========== 引用版本 ==========
void setupByRef(Config& cfg) {
    // 语法:直接使用对象
    cfg.port = 8080;
    cfg.host = "localhost";
    
    // ❌ 不能检查是否为空(假设一定存在)
    // if (&cfg == nullptr) {}  // 无意义,引用不能为null
}

// ========== 指针版本 ==========
void setupByPtr(Config* cfg) {
    // 语法:必须先检查空指针
    if (cfg == nullptr) {
        cerr << "错误:配置为空" << endl;
        return;
    }
    
    // 解引用方式1:->
    cfg->port = 8080;
    
    // 解引用方式2:*(获取引用后再用.)
    (*cfg).host = "localhost";
}

int main() {
    Config c{0, ""};
    
    // 引用调用:简单直接
    setupByRef(c);
    
    // 指针调用:必须取地址
    setupByPtr(&c);
    setupByPtr(nullptr);  // ✅ 可以传空,函数内部处理
    
    return 0;
}

2. 重新绑定能力(关键差异)

cpp 复制代码
void demonstrateRebinding() {
    int a = 10, b = 20;
    
    // ========== 引用:终身绑定 ==========
    int& ref = a;
    ref = b;      // ❌ 这不是让ref指向b!而是把b的值赋给a
                  // 结果:a = 20, ref仍是a的别名
    
    // 想改指向?不可能!
    // &ref = b;   // 编译错误!
    
    // ========== 指针:灵活重定向 ==========
    int* ptr = &a;
    cout << *ptr;  // 10
    
    ptr = &b;      // ✅ 随时改指向
    cout << *ptr;  // 20
    
    ptr = nullptr; // ✅ 可以指向空
}

3. 多级间接访问

cpp 复制代码
// 指针可以指向指针,引用不行
void pointerChain() {
    int x = 42;
    int* p = &x;
    int** pp = &p;      // ✅ 指针的指针
    int*** ppp = &pp;   // ✅ 可以无限套娃
    
    cout << ***ppp;     // 42
    
    // 引用只有一级(引用的引用被折叠)
    int& r = x;
    // int&& rr = r;    // 这是右值引用,不是引用的引用!
}

本质差异:编译器视角

cpp 复制代码
// 源代码
void byRef(int& r) { r = 10; }
void byPtr(int* p) { *p = 10; }

int main() {
    int x = 5;
    byRef(x);
    byPtr(&x);
}

编译后的伪代码:

asm 复制代码
; 引用版本(编译器优化后,与指针相同)
byRef:
    mov eax, [esp+4]    ; 取地址(与指针一样!)
    mov dword [eax], 10 ; 解引用赋值
    
; 指针版本
byPtr:
    mov eax, [esp+4]    ; 取地址
    test eax, eax       ; ✅ 检查空指针(编译器可能优化掉)
    je .null_handler
    mov dword [eax], 10
    
; 结论:引用是指针的语法糖,但编译器保证非空

本质真相 :引用在底层就是指针 ,但编译器添加了非空约束自动解引用的语法糖。


适用场景详解

✅ 引用传递适用场景

cpp 复制代码
// 场景1:参数必须存在(业务逻辑要求)
void connectDatabase(const ConnectionConfig& config) {
    // 配置必须提供,不存在"无配置"的情况
    // 无需检查空,简化代码
}

// 场景2:操作符重载(只能用引用)
class Vector {
public:
    // 必须返回引用才能链式赋值:v1 = v2 = v3
    Vector& operator=(const Vector& other) {
        // ...
        return *this;  // 解引用返回引用
    }
    
    // 下标运算符
    int& operator[](size_t index) {
        return data[index];  // 必须返回引用才能:v[0] = 10
    }
};

// 场景3:函数式编程风格(明确所有权)
void sort(vector<int>& data);           // 修改原数据
void print(const vector<int>& data);    // 只读访问

✅ 指针传递适用场景

cpp 复制代码
// 场景1:参数可选(可为空)
void logMessage(const string* prefix = nullptr) {
    if (prefix) {
        cout << "[" << *prefix << "] ";
    }
    cout << "日志内容" << endl;
}
// 调用:logMessage(); 或 logMessage(&debugTag);

// 场景2:动态内存管理(所有权转移)
void processLargeFile(unique_ptr<FileData>* filePtr) {
    // 可能转移所有权,或释放内存
    if (filePtr && *filePtr) {
        auto data = std::move(*filePtr);  // 接管所有权
        // filePtr 现在指向空unique_ptr
    }
}

// 场景3:C接口兼容/多态数组
void drawShapes(Shape** shapes, size_t count) {
    // 指针数组,支持运行时多态
    for (size_t i = 0; i < count; ++i) {
        shapes[i]->draw();  // 虚函数调用
    }
}

// 场景4:需要重新指向(迭代、遍历)
Node* findNode(Node* head, int value) {
    Node* current = head;      // 从head开始
    while (current) {
        if (current->val == value) return current;
        current = current->next;  // ✅ 指针可以遍历
    }
    return nullptr;
}
// 如果用引用:Node& current = head; current = ... 会修改原head!

现代C++的最佳实践

cpp 复制代码
// 🏆 参数传递决策树

// 1. 小对象(<= 2个指针大小):值传递
void f(int x, Point p);

// 2. 只读大对象:const 引用(首选)
void process(const BigData& data);

// 3. 必须修改原对象:非const 引用
void modify(Config& cfg);

// 4. 参数可选/可为空:指针
void render(const Camera* cam = nullptr);

// 5. 转移所有权(C++11):右值引用
void consume(vector<int>&& data);

// 6. 动态数组/多态:智能指针
void loadModel(shared_ptr<Mesh> mesh);      // 共享所有权
void takeData(unique_ptr<Buffer> buffer);   // 独占所有权

一句话总结

引用是"承诺存在的别名",指针是"可能为空的地址"。

用引用表达契约 (必须存在),用指针表达可选性(可能为空)。

相关推荐
寻寻觅觅☆2 小时前
东华OJ-基础题-120-顺序的分数(C++)
开发语言·c++·算法
云小逸2 小时前
【Vscode插件开发教程】VSCode插件开发入门指南:从C++开发者的视角
c++·ide·vscode
汉克老师2 小时前
GESP2024年12月认证C++二级( 第二部分判断题(1-10))
c++·循环结构·分支结构·gesp二级·gesp2级
Ronin3052 小时前
虚拟机数据管理模块
开发语言·c++·rabbitmq
一叶之秋14123 小时前
基石之力:掌握 C++ 继承的核心奥秘
开发语言·c++·算法
见牛羊3 小时前
CMakeLists 写法总结3.0
开发语言·c++
柒儿吖3 小时前
rudp Reliable UDP 库在 OpenHarmony 的 lycium 适配与 CRC32 测试
c++·c#·openharmony
拾光Ծ3 小时前
【优选算法】滑动窗口算法:专题一
c++·算法·滑动窗口·c++算法·滑动窗口算法·笔试面试
闻缺陷则喜何志丹3 小时前
【动态规划 AC自动机】P9188 [USACO23OPEN] Pareidolia S|普及+
c++·算法·动态规划·洛谷·ac自动机