| 概念 | C++ | 前端 (JS/TS) |
|---|---|---|
| 值传递 | 默认方式,完整拷贝 | 基本类型 (number, string等) |
| 引用传递 | 需显式使用 & 或指针 | 对象、数组、函数等引用类型 |
| 指针传递 | 传递内存地址 | 类似传递对象的引用 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#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; } |
输出:
构造: (1,2)
--- 调用函数 ---
拷贝构造: (1,2)
函数内: (100,2)
析构: (100,2)
--- 函数返回 ---
原对象: (1,2)
析构: (1,2)
?? 性能问题:大对象会发生昂贵的深拷贝
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// ? 引用传递:不拷贝,直接操作原对象(类似前端的对象引用) 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; } |
与前端对比:
|
1 2 3 4 5 6 7 8 9 |
// JavaScript - 对象天然是引用传递 function modify(obj) { obj.x = 100; // 直接修改原对象 } let p = {x: 1, y: 2}; modify(p); console.log(p.x); // 100 ?
// C++ 必须显式加 & 才能实现同样效果 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 指针传递:传递内存地址 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 |
| 重新绑定 | 不能改指向其他对象 | 可以指向不同对象 |
| 安全性 | 更高(必须初始化) | 需检查空指针 |
|
1 2 3 4 5 6 7 8 |
// ? C++11前:返回时会发生拷贝(或编译器RVO优化) Point createPoint() { Point p(3, 4); return p; // 理论上会拷贝,但编译器通常优化掉 }
// 接收时再次拷贝 Point p2 = createPoint(); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// ? 最佳实践速查表
// 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); // 旧式风格,避免 |
|
1 2 3 4 5 6 |
// 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字节) |
|
1 2 3 4 5 6 |
引用 (&) 指针 (*) ┌─────────┐ ┌─────────┐ │ 引用变量 │ ──→ 对象 │ 指针变量 │ ──→ 对象 │ (别名) │ (必须存在) │ (地址) │ 或 ──→ nullptr └─────────┘ └─────────┘ 语法糖,编译器处理 真正的内存实体 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#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; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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; // ? 可以指向空 } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 指针可以指向指针,引用不行 void pointerChain() { int x = 42; int* p = &x; int** pp = &p; // ? 指针的指针 int*** ppp = &pp; // ? 可以无限套娃
cout << ***ppp; // 42
// 引用只有一级(引用的引用被折叠) int& r = x; // int&& rr = r; // 这是右值引用,不是引用的引用! } |
|
1 2 3 4 5 6 7 8 9 |
// 源代码 void byRef(int& r) { r = 10; } void byPtr(int* p) { *p = 10; }
int main() { int x = 5; byRef(x); byPtr(&x); } |
编译后的伪代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
; 引用版本(编译器优化后,与指针相同) byRef: mov eax, [esp+4] ; 取地址(与指针一样!) mov dword [eax], 10 ; 解引用赋值
; 指针版本 byPtr: mov eax, [esp+4] ; 取地址 test eax, eax ; ? 检查空指针(编译器可能优化掉) je .null_handler mov dword [eax], 10
; 结论:引用是指针的语法糖,但编译器保证非空 |
本质真相:引用在底层就是指针,但编译器添加了非空约束和自动解引用的语法糖。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 场景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); // 只读访问 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// 场景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! |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// ???? 参数传递决策树
// 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); // 独占所有权 |
引用是"承诺存在的别名",指针是"可能为空的地址"。
用引用表达契约(必须存在),用指针表达可选性(可能为空)。