五分钟掌握C++参数传递精髓,从此告别内存泄漏

开发 后端
本指南将为你揭开C++参数传递的神秘面纱,带你领略其中的优雅与智慧。让我们一起踏上这段奇妙的代码之旅吧!

嘿,C++程序员们! 

你是否曾经对着代码发呆,思考这些问题:

  • 这个参数该传值还是传引用? 
  • 为什么我的程序这么慢,是参数传递的问题吗? 
  • 移动语义到底是什么黑魔法? 
  • 返回多个值时该用tuple还是struct? 

别担心!本指南将为你揭开C++参数传递的神秘面纱,带你领略其中的优雅与智慧。让我们一起踏上这段奇妙的代码之旅吧! 

本指南将帮助你:

  • 理解何时使用值传递vs引用传递
  • 掌握const引用的最佳实践
  • 学会使用移动语义优化性能
  • 正确处理输出参数和返回值

准备好了吗?让我们开始探索C++参数传递的艺术吧! 

输入参数的传递方式 - 该值传值还是传引用? 

这是一个经典的C++困扰 - 参数到底该怎么传?来看看这条黄金法则:

  • 如果参数很"便宜"(比如int、指针这种小家伙)就传值 
  • 如果参数"贵"(比如string这种大块头)就传const引用 

为什么呢?因为:

  • 传值的好处是简单直接,不用担心指针解引用的开销
  • 传const引用的好处是避免拷贝大对象的开销

来看个例子:

// 处理用户信息
void processUserInfo(const string& name);     // 👍 name可能很长,用const引用
void processUserInfo(string name);            // 😱 每次都要拷贝name

// 处理坐标
void movePoint(int x, int y);                // 👍 坐标是简单的int,直接传值
void movePoint(const int& x, const int& y);  // 🤦 引用反而增加了开销

// 处理配置
void updateConfig(const vector<int>& config); // 👍 vector可能很大,用const引用
void updateConfig(vector<int> config);        // 😱 拷贝整个vector开销太大

// 处理ID
void processId(int id);                      // 👍 ID就是个数字,传值
void processId(const int& id);               // 🤦 完全没必要用引用

当然规则总有例外,比如你想要移动语义的时候,可以考虑用右值引用(&&)。但是不要过度优化,简单明了才是王道! 

对于需要修改的参数,使用非const引用传递

为什么要这样做呢? 

想象一下,你把一个参数传给函数,但不知道它会不会被修改 - 这就像把钥匙借给别人,却不知道他会不会偷偷配一把新的! 

使用非const引用就像给参数贴上一个大大的标签:"警告 这个参数会被修改哦!" - 代码意图一目了然。

来看个有趣的例子:

// 糟糕:result像个谜一样,不知道是输入还是输出
void calculate_sum(int values[], int count, int* result); 

// 完美:sum像个告示牌,一看就知道会被修改
void calculate_sum(const int values[], int count, int& sum);

// 糟糕:str像个间谍,不知道会不会被暗中修改
void parse_name(char* str);

// 完美:str像个实习生,明确表示要被指导和修改
void parse_name(string& str);

有趣的例外 - 别被表象骗了! 

有些类型看起来人畜无害,实际上却能神不知鬼不觉地修改原对象:

class Widget {
    vector<int> data;
public:
    void add(int x) { data.push_back(x); }
};

void process(shared_ptr<Widget> w) 
{
    w->add(42);  // 看似人畜无害的按值传递,实际是个"内鬼"!
}

void update_iterator(vector<int>::iterator it)
{
    *it = 100;  // 迭代器虽然是按值传递,但有"穿墙"特技!
}

需要当心的陷阱

引用参数就像一把双刃剑 - 既能输入也能输出,用不好就容易伤到自己:

class Person {
    string name_;
    int age_;
public:
    // 糟糕:这简直是"夺舍"!完全替换了原对象
    void update(Person& p) { 
        *this = p;  // 危险!像换了个人似的
    }
    
    // 完美:像个温柔的美容师,只改变你想改的部分
    void set_name(const string& new_name) { name_ = new_name; }
    void set_age(int new_age) { 
        if (new_age < 0) throw invalid_argument("年龄不能为负,你想穿越吗?");
        age_ = new_age; 
    }
};

如何避免掉进这些坑? 

幸运的是,编译器是我们的好朋友,会帮我们把关:

  • 如果一个非const引用参数没被写入,编译器会提醒:"喂,你说要改它的,怎么放鸽子?" 
  • 如果一个非const引用参数被move了,编译器也会警告:"这样不太好吧,有点过分了..." 

所以记住这条黄金法则:当你想在函数里修改参数时,用非const引用准没错! 让代码既安全又清晰,何乐而不为呢? 

小贴士

想一想:如果你看到一个函数用了非const引用参数,你是不是立刻就能明白它要做什么?这就是好代码的魅力所在! 

"移动"这出戏要这么演 - 参数传递的艺术

嘿,各位C++演员们! 今天我们来聊聊如何优雅地"移动"对象这出戏该怎么演

为什么要这么演? 

  • 想象一下,你要把一个大型字符串从一个函数传递到另一个函数:
  • 复制一遍(拷贝传递) ❌ - 需要分配新内存并复制所有字符

转移所有权(移动传递) ✅ - 直接"偷走"原字符串的内存,超快!

这就是为什么我们要用移动语义 - 它让数据传递更高效。

来看个

string make_greeting(string&& name)  // name说:"请尽管移动我!"
{
    string result = "Hello, ";
    result += std::move(name);      // 直接把name的内容"偷"过来
    return result;                  // result也会被移动返回,超高效!
}

// 使用示例
string name = "Alice";
string greeting = make_greeting(std::move(name)); 
// 现在name变空了,greeting里面有完整的问候语

但是要注意!

移动之后,原对象就会变成"空壳子",不能再使用它的值:

string str = "Hello";
process_string(std::move(str));  // str的内容被移走了
cout << str;          

检查清单

  • 看到T&&参数,记得用std::move把它移走
  • 被移动后的对象不要再使用它的值
  • 如果对象后面还要用,就不要移动它

记住:移动是为了性能,但要负责任地使用。移动后的对象就像倒空的杯子,不要期待里面还有水! 

怎么样,现在对移动语义是不是更清楚了呢? 让我们一起写出更高效的代码吧! 

返回值 vs 输出参数之战

想象一下,你是一个快递员,要把包裹送到客户手中。你有两个选择:

  • 直接把包裹递给客户(返回值)
  • 让客户先给你一个空箱子,然后你把东西放进去(输出参数)

显然第一种方式更直观对吧?客户也不用准备空箱子,多方便~

// 😊 干净利落的返回值方式
vector<Pizza*> find_pizzas(const vector<Pizza>&, Topping t); 

// 😫 麻烦的输出参数方式
void find_pizzas(const vector<Pizza>&, vector<Pizza*>& out, Topping t);

什么时候该用输出参数? 

当然也有一些特殊情况需要用输出参数:

  • 如果你要搬运一台特别重的钢琴(昂贵的移动操作),最好让客户先准备好位置:
// 钢琴太重了,还是用引用吧
void move_piano(Piano& destination); 
  • 如果你要在循环里反复使用同一个容器:
string message;
for(int i = 0; i < 100; i++) {
  // 重复使用message,避免创建新的
  append_to_message(message, i); 
}

返回值优化的小魔法

现代C++编译器会帮你优化返回值,所以不用太担心性能:

Matrix operator+(const Matrix& a, const Matrix& b) {
    Matrix result;
    // 编译器:放心,我会帮你优化掉不必要的拷贝~
    return result; 
}

所以除非真的有特殊需求,就用简单直观的返回值方式吧!毕竟代码写出来是给人看的,要让同事看得开心才对

记住:返回值就像递快递,输出参数就像装修房子 - 能直接递过去为什么要让人准备空房间呢? 

返回多个值时,用结构体来装! 

在C++中,一个函数只能返回一个值。但有时我们确实需要返回多个值,该怎么办呢? 

有些人可能会这样写:

// 糟糕的写法 🤦
void calculate_stats(const vector<int>& data, 
                    double& mean,    // 输出参数:平均值
                    double& std_dev) // 输出参数:标准差
{
    // 计算平均值
    mean = /* ... */;
    // 计算标准差
    std_dev = /* ... */;
}

// 使用时:
double avg, dev;
calculate_stats(numbers, avg, dev); // 不清楚哪个是输入哪个是输出

这种通过引用参数来"偷偷"返回多个值的方式,不仅可读性差,而且容易让人困惑。来看看更好的方式:

// 优雅的写法 ✨
struct Stats {
    double mean;    // 平均值
    double std_dev; // 标准差
};

Stats calculate_stats(const vector<int>& data)
{
    double mean = /* ... */;
    double std_dev = /* ... */;
    return {mean, std_dev}; // 清晰地返回所有结果
}

// 使用时:
auto stats = calculate_stats(numbers); // 一目了然!
cout << "平均值:" << stats.mean << "\n";

C++17的结构化绑定让使用更加优雅:

auto [mean, std_dev] = calculate_stats(numbers);
cout << "平均值:" << mean << "\n";

特殊情况:输入输出参数

有时候使用引用参数也是合理的,比如处理文件输入输出:

// 这样写是合适的 👍
bool read_record(istream& in,      // 输入输出流
                Record& record)    // 输出:读取的记录
{
    // 读取记录...
    return true; // 返回是否成功
}

// 使用示例:
Record r;
while (read_record(file, r)) {
    process(r);
}

这种情况下使用引用参数是合理的,因为:

  • istream本身就是设计为通过引用传递和修改的
  • Record可能很大,通过引用避免了复制开销

返回复杂数据的最佳实践

对于复杂的返回值,最好定义专门的类型:

// 表示地理位置
struct Location {
    double latitude;   // 纬度
    double longitude;  // 经度
    double altitude;   // 海拔(米)
    
    string toString() const {
        return fmt::format("({:.2f}, {:.2f}, {:.2f}m)", 
                          latitude, longitude, altitude);
    }
};

// 使用示例
Location get_current_location() {
    // ... 获取GPS数据 ...
    return {37.7749, -122.4194, 0}; // 旧金山
}

不要使用pair/tuple,除非真的是临时的、简单的值对:

// 不好的写法 👎
tuple<double,double,double> get_position() {
    return {37.7749, -122.4194, 0};
}

// 使用时不知道各个值的含义
auto [x, y, z] = get_position(); 

用tuple返回多个值就像在API中返回查询结果:"这个查询返回了-1、false和'error occurred'"。让人一头雾水! 

换成结构体就清晰多了:

struct QueryResult {
    int status_code;     // HTTP状态码
    bool has_data;       // 是否找到数据
    string error_msg;    // 错误信息
};

这样一看,每个返回值的含义清清楚楚!不然调用者还得翻文档:"等等,第一个是状态码还是错误码?" 

记住:代码是写给人看的,要让API调用者一眼就能看懂返回值的含义。除非你觉得让别人猜谜很有趣... 

性能优化小技巧

对于大对象,使用移动语义避免复制:

struct HugeResult {
    vector<double> data;  // 可能很大的数据
    string metadata;
};

HugeResult process_data(const string& input) 
{
    HugeResult result;
    result.data = /* ... 大量计算 ... */;
    result.metadata = /* ... */;
    return result;  // 编译器会自动使用移动语义
}

最后的建议

  • 优先使用结构体返回多个值
  • 结构体字段要有清晰的名字和注释
  • 只在处理IO时使用引用参数
  • 考虑使用optional<T>表示可能失败的操作

遵循这些原则,你的代码会更加清晰、易维护! 

指针和引用的爱恨情仇 - 到底该用哪个?

两个"主角"的性格特点:

(1) 指针君(T*):

  • 性格随性,可以是 nullptr (就是说人家可以单身)
  • 经常说"我现在没对象"

(2) 引用妹(T&):

  • 必须死心塌地跟一个对象绑定(非常专一)
  • 从出生就必须有对象,单身都不行!

如何选择约会对象?

当你在写代码时,遇到这样的场景:

// 相亲角色: 顾客类
class Customer { /*...*/ };

// 场景1: 处理VIP折扣
class Order {
    // 错误写法: 用引用妹,显得太强势了!
    void process_discount(Customer& vip) { /*...*/ }
    
    // 正确写法: 用指针君,比较随意自然
    void process_discount(Customer* maybe_vip) {
        if(maybe_vip) {  // 有VIP就打折
            give_discount();
        } else {         // 没VIP就原价
            normal_price(); 
        }
    }
};

爱情忠告

  • 如果这段关系"可能不存在" → 选指针君(T*)
  • 如果是"一定要在一起" → 选引用妹(T&)

特别提醒

虽然可以强行给引用妹安排一个无效对象(Customer* p = nullptr; Customer& r = *p;),但这样做会把关系搞得一团糟(未定义行为)。要尊重引用妹的专一本性!

想要安全感满满?

如果你想要指针君的潇洒,又想要引用妹的可靠,可以试试这个:

void process_order(not_null<Customer*> customer) {
    // 这位指针君保证有对象,超级可靠!
    customer->validate();
}

记住:选择权在你,但要尊重对方的性格特点。该放飞的时候就用指针,该专一的时候就用引用~

C++参数传递总结

  • 参数传递的选择: 传参就像坐车 - 小朋友(int、指针)直接抱着走就行,大胖子(string、vector)最好拉着手(用引用)省力气。要是打算帮他减肥(修改值),就别戴手套(const)了,直接拉手更方便!😄
  • 移动语义的使用: 移动就是个"搬家公司",不用复制家具直接整车搬走。但搬完后原来的房子就空了,可别指望还能在老房子找到沙发啊!所以除非真要"搬家",不然就别瞎折腾了。🚛
  • 返回值的处理: 返回东西就该像快递小哥 - 直接送到家多痛快!非要客户准备个箱子(输出参数)多麻烦。要是要寄好几样东西,就打包成礼盒(结构体)吧,总比塞个麻袋(tuple)里让人猜里面是啥强! 📦
  • 指针与引用的选择: 指针就像随性的单身汉 - 今天有对象明天没对象都行。引用可是专一的好姑娘 - 认定一个就得一直跟着。所以看你要找个什么样的"对象"啦! 💑

记住:代码写得越简单,同事越喜欢你! 😉

责任编辑:赵宁宁 来源: everystep
相关推荐

2021-06-07 09:51:22

原型模式序列化

2009-11-17 14:50:50

Oracle调优

2024-12-25 12:00:00

C++解包代码

2009-11-05 10:55:22

Visual Stud

2021-01-11 09:33:37

Maven数目项目

2017-01-10 09:07:53

tcpdumpGET请求

2020-03-03 19:59:38

主板无线网卡

2021-01-13 09:23:23

优先队列React二叉堆

2021-06-06 13:08:22

C#特性Attribute

2018-01-08 16:19:04

微信程序轮播图

2009-11-16 10:53:30

Oracle Hint

2024-12-11 07:00:00

面向对象代码

2022-08-04 13:27:35

Pythonopenpyxl

2021-10-20 06:58:10

工具低代码无代码

2017-04-25 12:07:51

AndroidWebViewjs

2024-03-21 09:51:22

Python爬虫浏览网站

2024-06-07 08:19:05

2020-06-16 08:47:53

磁盘

2021-04-27 10:16:51

优化机器学习人工智能

2020-12-17 10:00:16

Python协程线程
点赞
收藏

51CTO技术栈公众号