C++中传递非原始类型的值?

6
我对C++11有一个基本的误解。我的教授告诉我,除了引用或指针,无法传递非原始类型的参数给函数。但是,以下代码可以正常工作。
#include <iostream>
using namespace std;

class MyClass
{
public: 
    int field1;
};

void print_string(string s) { 
    cout << s << endl; 
}

void print_myclass(MyClass c) { 
    cout << c.field1 << endl; 
}

int main(int argc, char *argv[]) 
{
    string mystr("this is my string"); 
    print_string(mystr); // works
    MyClass m; 
    m.field1=9; 
    print_myclass(m); 
    return 0;
}

运行该程序会产生以下输出结果。
this is my string
9

RUN SUCCESSFUL (total time: 67ms)

我在Win7上使用MinGW/g++。

为什么会这样工作?我以为非原始类型不能按值传递?!


8
如果你的教授说了那样的话,他是错误的。更可能的是,他说这并不明智。 - user207421
3
如果他作为一个笼统的陈述表示不建议这样做,那么他也是错误的;-)尽管这是“过时或没有经过深思熟虑的表现建议”类型的错误,而不是“不了解语言基础”类型的错误,但略微好一些。 - user395760
好的。谢谢!我想这样做并不可取,因为它会导致每个参数对象的另一个副本? - Jared Lindsey
@JaredLindsey,是的,它会调用类的复制构造函数,除非它已被明确删除或设置为私有。然后你的代码就无法编译。如果复制构造函数已被重写,则传递的参数必须符合你明确定义的复制构造函数的定义。 - Jonathan Henson
1
编程教授不一定是优秀的程序员。请注意! - Neil Kirk
3个回答

10

非原始类型当然可以以值的方式传递。(这在C++标准的5.2.2 [expr.call]章节中有所涉及。)

但是,有几个原因通常不鼓励这样做,特别是在C++03代码中。

首先,对于大型对象来说,效率比传递引用低(与传递引用相比,数据会传递到堆栈上)。引用会占用堆栈上的一个字,因此通过堆栈传递任何大于一个字的对象都必然会更慢。

其次,按值传递会调用复制构造函数(或者,如@templatetypedef指出的,在C++11中可能会调用移动构造函数)。这个额外的处理可能会产生一定的开销。

第三,你可能打算修改传递进来的对象,但是通过传递副本(按值),你在函数内部进行的任何更改都不会影响原始对象。因此,正确地获得语义非常重要(即是否要修改原始对象)。因此,在某些情况下,这是一个潜在的错误。

最后,如果有一个糟糕编写的类没有复制构造函数或赋值运算符,编译器将为您自动生成一个默认的复制构造函数。这将执行浅层复制,可能会导致诸如内存泄漏之类的问题。这是非常重要实现这些特殊方法的另一个很好的原因。完整的细节在本文中:

一般来说,在C++03代码中,如果不打算修改对象,则通常会通过const&引用传递,如果需要修改对象,则使用普通的&引用传递。如果参数是可选的,则使用指针。

这些问题中也可以找到一些好的答案和讨论,特别是关于移动语义的讨论:

  • C++:按值传递对象的原因
  • 移动语义是什么?
  • 传引用和传值有什么区别?
  • 对于 C++11 的完整答案更加复杂:

    可能最好的总结如何处理这些问题:


    通常非常低效?撇开那些容易复制且只有一个或几个单词的大量类别不谈,还有这个:http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ - user395760
    1
    如果复制导致泄漏/错误,因为默认值是错误的,那么这是类的问题,应该在那里修复,要么通过禁止复制(即使在C++11之前也很容易,如果你不介意无用的错误消息),要么遵守三/五/零规则。 - user395760
    因此,“通常”限定词。在25个字或更少的情况下,很难传达所有这些微妙之处。当然,完整的答案更加细致入微。但对于教授新手来说,这是一个相当不错的经验法则。 - gavinb
    RE性能:不要给新手提供关于他们暂时不需要了解的问题的不准确信息,这些问题他们以后完全可以学习。除了这个哲学上的反对意见,“通常”和“绝大多数”都是不正确的:有很多对象永远不会满足这个条件,对于几乎所有其他对象,它只在有限的情况下成立,因此如果你决定包括任何关于这个主题的评论,那么“有时候”或“在特定情况下”会更加合适。 - user395760
    Indeed - 更新了答案以澄清泄漏是由于编写不良的类而需要复制构造函数和赋值运算符。 - gavinb
    仍然不需要由于浅拷贝而出现内存泄漏。只需使用现有的 RAII 类替代指针即可。 - chris

    3

    你的教授完全错了,也许他在想JAVA或C#?在C++中所有东西都是按值传递的。要通过引用传递某些东西,你需要使用&修饰符。


    在Java中,一切都是按值传递。只是引用类型基本上是指针,所以看起来像是按引用传递。 - user904963
    这是一个破坏了任何有意义的区分使用的定义。如果你将其视为一个sizeof(void *)整数,指针是按值传递的。语义上是最重要的部分。在Java中,除非你使用一些原始类型,否则一切都被传递为指针,而Java将其抽象为引用,尽管它们也不是严格定义中的“引用”,因为引用不能为null。C#在这方面有更复杂的调用约定,但JAVA调用约定是基于指向JObject的指针,除非它是一个原始类型。 - undefined

    1
    在C++中,非原始类型确实可以通过值传递。如果您尝试这样做,C++将使用一个称为“复制构造函数”(或在某些情况下在C++11中为“移动构造函数”)的特殊函数来将参数初始化为参数的副本。编写复制构造函数和赋值运算符被认为是C++中棘手的部分(错误很容易产生,正确也很难),因此教授可能会试图阻止您这样做。未能编写复制构造函数或不正确地编写它很容易导致程序崩溃,并且是新C++程序员困惑的常见来源。
    我建议您搜索“C++ Rule of 3”或“copy constructor assignment operator”以了解如何编写智能复制对象的函数。需要一点时间才能掌握如何做到这一点,但一旦理解了概念,就不太难了。
    希望这有所帮助!

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接