Item 20:传递常量引用比传值更好

探讨C++中函数参数传递方式的选择,分析传值与传引用的成本与风险,特别是对于类实例的传递,强调使用const引用以提高效率并避免截断问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Item 20: Prefer pass-by-reference-to-const to pass-by-value

C++函数的参数和返回值默认采用传值的方式,这一特性是继承自C语言的。如果不特殊指定, 函数参数将会初始化为实参的拷贝,调用者得到的也是返回值的一个副本。 这些拷贝是通过调用对象的拷贝构造函数完成的,正是这一方法的调用使得拷贝的代价可能会很高。

通常来讲,传递常量引用比传值更好,同时避免了截断问题。但是内置类型和STL容器、迭代器,还是传值更加合适。

来个例子

一个典型的类的层级可能是这样的:

class Person {
    string name, address;
};
class Student: public Person {
    string schoolName, schoolAddress;
};

假如有这样一处函数调用:

bool validateStudent(Student s);           // function taking a Student by value

Student plato;                             // Plato studied under Socrates
bool platoIsOK = validateStudent(plato);   // call the function

在调用validateStudent()时进行了6个函数调用:

  1. Person的拷贝构造函数,为什么Student的拷贝构造一定要调用Person的拷贝构造请参见:Item 12:完整地拷贝对象
  2. Student的拷贝构造函数
  3. name, address, schoolName, schoolAddress的拷贝构造函数

解决办法便是传递常量引用:

bool validateStudent(const Student& s);

首先以引用的方式传递,不会构造新的对象,避免了上述例子中6个构造函数的调用。 同时const也是必须的:传值的方式保证了该函数调用不会改变原来的Student, 而传递引用后为了达到同样的效果,需要使用const声明来声明这一点,让编译器去进行检查!

截断问题

将传值改为传引用还可以有效地避免截断问题:由于类型限制,子类对象被传递时只有父类部分被传入函数。

比如一个Window父类派生了子类WindowWithScrollBars:

class Window {
public:
  ...
  std::string name() const;           // return name of window
  virtual void display() const;       // draw window and contents
};

class WindowWithScrollBars: public Window {
public:
  ...
  virtual void display() const;
};

有一个访问Window接口的函数,通过传值的方式来获取Window的实例:

// incorrect! parameter may be sliced!
void printNameAndDisplay(Window w){     
  std::cout << w.name();
  w.display();
}

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

当调用printNameAndDisplay时参数类型从WindowWithScrollBars被隐式转换为Window。 该转换过程通过调用Window的拷贝构造函数来进行。 导致的结果便是函数中的w事实上是一个Window对象, 并不会调用多态子类WindowWithScrollBars的display()。

// fine, parameter won't be sliced
void printNameAndDisplay(const Window& w){ 
  std::cout << w.name();
  w.display();
}

这就很好嘛,如果你曾深入过编译器你会发现引用是通过指针来实现的。

特殊情况

一般情况下相比于传递值,传递常量引用是更好的选择。但也有例外情况,比如内置类型和STL迭代器和函数对象。

内置类型传值更好是因为它们小,而一个引用通常需要32位或者64位的空间。可能你会认为小的对象也应当首选传值, 但对象小并不意味着拷贝构造的代价不高!比如STL容器通常很小,只包含一些动态内存的指针。然而它的拷贝构造函数中, 必然会分配并拷贝那些动态内存的部分。

即使拷贝构造函数代价很小,传值的方式仍然有性能问题。有些编译器会区别对待内置类型和用户定义类型, 即使它们有相同的底层表示。比如有些编译器虽然会把double放入寄存器,但是拒绝将只含一个double的对象放入寄存器。

一个只含double的对象大小为8,它和一个double具有相同的大小和底层表示。关于对象大小的计算,请参考:Item 7:将多态基类的析构函数声明为虚函数

从面向对象设计方面来讲,即使对象现在很小,但它作为用户定义类型是有可能变大的(如果你更改了内部实现)。 从长远来讲的性能考虑,也应当采取传引用的方式来设计使用它的函数。

STL迭代器和函数对象也应当被传值,这是因为它们在STL中确实是被这样设计的,同时它们的拷贝构造函数代价并不高。

转载地址:http://harttle.land/2015/08/13/effective-cpp-20.html
感谢作者 Harttle

<think>我们正在讨论C++中std::pair的方法。根据用户提供的引用材料,引用[1]提到了std::pair<intconst,int>,引用[2]提到了C++中的右绑定规则,引用[3]涉及std::map的原理(其中使用了std::pair),引用[4]是一个错误示例,但其中包含std::basic_string的使用。这些引用虽然不是直接说明方法,但我们可以结合C++知识来回答。在C++中,std::pair是一个模板类,用于将两个组合成一个对象。传递std::pair对象有几种方式:1.按传递(passbyvalue):直接传递整个pair对象的副本。2.按引用传递(passbyreference):传递pair对象的引用,避免复制。3.按常量引用传递(passbyconstreference):当不想改变pair对象时,可以使用常量引用传递。4.移动语义(movesemantics):对于临时对象或可以移动的对象,通过std::move实现高效传递。具体说明如下:###1.按传递当函数需要修改pair且不影响原始对象,或者pair很小且复制成本低时,可以使用按传递。```cppvoidmodify_pair(std::pair<int,double>p){p.first=42;//修改的是副本}```###2.按引用传递当需要修改调用者传递的pair对象时,使用引用传递。```cppvoidmodify_pair_ref(std::pair<int,double>&p){p.first=42;//修改原始对象}```###3.按常量引用传递当不需要修改pair对象,且希望避免复制时(尤其是pair包含大型对象时),使用常量引用传递。```cppvoidprint_pair(conststd::pair<int,double>&p){std::cout<< p.first<< ","<< p.second;}```###4.移动语义(C++11及以后)对于临时对象或者我们明确要转移所有权的对象,可以使用移动语义。```cppvoidprocess_pair(std::pair<int,std::string>&&p){//移动构造函数被调用,转移资源}//调用process_pair(std::make_pair(1,"temporary"));//临时对象可以直接绑定到右引用//或者如果有一个左,但不再需要它,可以移动std::pair<int,std::string>my_pair={1,"hello"};process_pair(std::move(my_pair));```此外,在C++11以后,我们可以利用移动语义来优化按传递:如果传递的是一个临时对象(右),则函数参数会通过移动构造来初始化,而不是拷贝构造。因此,有时可以这样写:```cppvoiduse_pair(std::pair<int,std::string>p){//如果传递的是右,则移动构造;如果是左,则拷贝构造}//如果我们想避免拷贝,可以传递引用,并配合std::move使用use_pair(std::make_pair(1,"world"));//移动构造std::pair<int,std::string>pair2={2,"foo"};use_pair(pair2);//拷贝构造use_pair(std::move(pair2));//移动构造```###注意事项-对于包含大型对象的pair(如字符串、容器等),应避免按传递,因为复制代价较高。使用常量引用或移动语义更高效。-C++17及之后,结构化绑定(structuredbindings)可以方便地解构pair,但这不影响传递方式。-对于std::map中的元素(其元素是std::pair<constKey,Value>),由于键是const的,所以通过引用传递时要注意键的不可修改性。根据引用[1]和引用[3]中提到的std::map原理,其中存储的pair的键是const的,例如:```cppstd::map<int,int>my_map;autoit=my_map.begin();//it->first是constint,不能修改```在这种情况下,如果我们要传递这个pair(如迭代器指向的pair),我们通常使用常量引用传递,因为我们不应该修改键(key):```cppvoidprint_map_pair(conststd::pair<constint,int>&p){//p.first是const,不能修改}```###总结-小对象:按传递引用传递均可。-大对象或避免拷贝:按常量引用传递(不需要修改时)或按引用传递(需要修改时)。-转移所有权:使用移动语义(右引用传递或按传递配合std::move)。###相关问题根据我们讨论的内容,可以生成以下相关问题:1.在C++中,如何高效地传递包含字符串的std::pair对象?2.std::pair在map容器中作为元素使用时传递有哪些限制?3.在C++11之后,移动语义如何优化std::pair的效率?最后,注意在回答中适当加入引用标注(例如,提到map中pair的键为const时,引用[1]和[3]提到了pair在map中的使用)。</think> ### C++ 中 std::pair 的方法详解 在 C++ 中,`std::pair` 的传递方式遵循常规的 C++ 对象传递规则。以下是四种常用方式及其原理分析: #### 1. **按传递** (Pass by Value) ```cpp void processPair(std::pair<int, double> p) { // p 是原始对象的独立副本 p.first = 42; // 修改不影响原始对象 } ``` - **特点**: - 创建对象的完整副本 - 调用 `std::pair` 的**拷贝构造函数** - 修改不影响原始对象[^1] - **适用场景**:小型 `pair` 对象(基本类型组合) #### 2. **按引用传递** (Pass by Reference) ```cpp void modifyPair(std::pair<int, double>& refPair) { refPair.second = 3.14; // 直接修改原始对象 } ``` - **特点**: - 传递内存地址引用 - 零拷贝开销 - 所有修改直接影响原始对象[^3] - **适用场景**:需要修改原始 `pair` 或包含大型对象的 `pair` #### 3. **按常量引用传递** (Pass by Const Reference) ```cpp void readPair(const std::pair<int, double>& crefPair) { // crefPair.first++; // 错误!常量引用禁止修改 std::cout << crefPair.second; // 只读操作安全 } ``` - **特点**: - 无拷贝开销 - 防止意外修改 - 可将右 `pair` 绑定到常量引用[^2] - **适用场景**:只读访问大型对象或临时对象 #### 4. **移动语义传递** (C++11 以上) ```cpp void consumePair(std::pair<int, std::string>&& movePair) { auto data = std::move(movePair.second); // 移动资源所有权 } // 调用 consumePair(std::make_pair(1, "large_data")); // 传递 ``` - **特点**: - 使用 `std::move` 转移资源所有权 - 避免深拷贝开销 - 需要 C++11 及以上支持 - **适用场景**:包含可移动资源的大对象(如字符串、容器) ### ✨ 最佳实践建议 1. **基本数据类型组合** 按传递(如 `std::pair<int, float>`) 2. **包含类对象时** ```cpp // 推荐使用常量引用 void handleObjPair(const std::pair<std::string, MyClass>& p); ``` 3. **STL容器中的pair处理**(如map元素) ```cpp std::map<int, Data> myMap; for (auto& [key, value] : myMap) { // C++17结构化绑定 // 按引用访问避免拷贝 } ``` 4. **函数返回优化** ```cpp // 编译器通常使用RVO(返回优化)避免拷贝 std::pair<int, std::string> createPair() { return {42, "answer"}; // 无需std::move } ``` > ⚠️ **关键注意事项** > - `map` 中的键对为 `std::pair<const Key, Value>`,键(key)不可修改[^1][^3] > -传递含`const`成员的pair会触发拷贝构造(如`pair<const int, int>`) > - C++11后对右pair优先使用移动语义提升性能[^2] ### 📚 性能对比表 | 传递方式 | 拷贝开销 | 可修改性 | 适用场景 | |------------------|----------|----------|-----------------------| | 按传递 | 高 | 副本修改 | 小型pair | | 按引用传递 | 无 | 直接修改 | 需要修改原对象 | | 按常量引用传递 | 无 | 不可修改 | 只读访问 | | 移动语义(C++11+) | 无 | 转移所有权| 含可移动资源的大对象 | 在 `std::map` 等关联容器中,元素访问返回的是 `pair<const Key, Value>`,键(key)的常量性必须严格保持,这是容器内部结构的重要约束[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值