如何将对象传递给函数?

273
我对C++编程还不熟悉,但我有Java的经验。我需要指导如何在C++中传递对象给函数。
我需要传递指针、引用还是非指针和非引用的值?我记得在Java中没有这样的问题,因为我们只传递保存对象引用的变量。
如果您能解释在哪些情况下使用这些选项,那就太好了。

7
你正在学习C++的哪本书? - anon
18
那本书强烈不推荐。建议选择Stan Lippman的《C++ Primer》。 - Prasoon Saurav
23
好的,问题就在这里。Schildt的书基本上是垃圾 - 用Koenig和Moo的《Accelerated C++》。 - anon
10
我想知道为什么没有人提到Bjarne Stroustrup写的《C++程序设计语言》。Bjarne Stroustrup是C++的创始人,这是一本很好的学习C++的书。 - George
15
@George说:TC++PL不适合初学者,但被认为是C++的圣经。xD - Prasoon Saurav
显示剩余4条评论
8个回答

301

C++11的经验法则:

传递参数时,除非

  1. 你不需要对象的所有权且一个简单的别名就可以满足要求,这种情况下你应该使用按const引用传递
  2. 你必须改变对象,这种情况下,你应该使用按非const左值引用方式传递
  3. 你将派生类对象作为基类传递,这种情况下你需要使用引用进行传递。(使用前面的规则来确定是否按const引用方式传递。)

几乎从不建议通过指针进行传递。可选参数最好表示为std::optional(对于旧的std库,使用boost::optional),而别名可以通过引用很好地实现。

C ++ 11的移动语义使得通过值进行传递和返回即使对于复杂对象也更具吸引力。


C++03的经验法则:

传递参数时,请按const引用方式传递,除非

  1. 它们在函数内部被改变并且这样的更改应该在函数外部反映,这种情况下你应该使用按非const引用方式传递
  2. 函数应该在没有任何参数的情况下可调用,在这种情况下,你应该通过指针进行传递,以便用户可以传递NULL/0/nullptr;按照前一条规则确定是否应该通过指向const参数的指针进行传递
  3. 它们是内置类型,可以通过复制进行传递
  • 它们应当在函数内部进行更改,而这样的更改不会反映在外部,在这种情况下可以采用按拷贝传递参数(另一种方法是按照前面的规则传递参数,并在函数内部进行复制)
  • (这里,“按值传递”称为“按拷贝传递”,因为在C++03中按值传递始终会创建一个副本)


    还有更多需要注意的地方,但这些初学者的规则已经足够使用了。


    18
    我注意到有些人(比如Google)认为函数内部将要改变的对象应该通过指针而不是非const引用传递。这样做的原因是当将一个对象的地址传递给一个函数时,更容易看出该函数可能会改变它。例如:对于引用,调用是foo(bar);无论引用是否是const,而对于指针则是foo(&bar);更容易看出foo正在传递一个可变对象。 - RC.
    20
    @RC仍然无法告诉您指针是否为const。Google的指南在各种C++在线社区中受到了很多批评 - 我认为这是合理的。 - anon
    14
    在其他情境下Google可能领先,但在C++中,他们的样式指南并不是很好。 - David Rodríguez - dribeas
    4
    作为纯风格指南,Stroustrup 有一个为航空航天公司开发的指南(http://www2.research.att.com/~bs/JSF-AV-rules.pdf)。我浏览了谷歌指南,并出于一些原因不喜欢它。Sutter 和 Alexandrescu 的《C++ 编码规范》(http://www.amazon.com/Coding-Standards-Rules-Guidelines-Practices/dp/0321113586) 是一本很好的书,可以获得很多好的建议,但它并不是真正的风格指南。除了人类和常识外,我不知道是否有任何自动检查风格的工具。 - David Rodríguez - dribeas
    3
    但是,您会得到一个保证,即如果没有通过指针传递参数,则该参数不会被更改。在我的看法中,这非常有价值,否则在尝试跟踪函数中变量的变化时,您必须检查所有传递给该函数的函数的头文件,以确定是否更改了变量的值。而采用通过指针传递参数的方式,您只需要查看相应的部分即可。 - smehmood
    显示剩余22条评论

    119
    在C++和Java中,调用约定有一些不同。在C++中,严格来说只有两种约定:传值和传引用。一些文献包括第三种传指针的约定(实际上是传递指针类型的值)。除此之外,您可以将const属性添加到参数类型中,增强语义。
    传引用表示函数在概念上接收您的对象实例而不是其副本。引用在概念上是调用上下文中使用的对象的别名,并且不能为null。函数内执行的所有操作都应用于函数外的对象。这种约定在Java或C中不可用。
    编译器将在调用上下文中生成对象的副本并在函数内使用该副本。函数内执行的所有操作都会对副本而非外部元素进行处理。这是Java中原始类型的约定。
    其中的一个特殊版本是将指针(对象的地址)传递到函数中。函数接收指针,并且所有应用于指针本身的操作都应用于副本(指针),另一方面,应用于取消引用指针的操作将应用于该内存位置上的对象实例,因此函数可能具有副作用。使用指向对象的传值允许内部函数修改外部值,就像通过引用传递一样,并且还允许可选值(传递空指针)。
    这是C中用于修改外部变量的函数所使用的约定,并且还是Java引用类型使用的约定:引用被复制,但所引用的对象相同:对引用/指针的更改不可在函数外部看到,但所指内存的更改是可见的。
    在C++中,您可以在定义变量、指针和引用时分配常量性。您可以将变量声明为常量,可以将引用声明为常量实例,并且可以定义所有指向常量对象、常量指向可变对象和常量元素的指针。相反,在Java中,您只能定义一个常量性级别(final关键字):该变量的常量性级别(原始类型的实例,引用类型的引用),但是您不能定义对不可变元素的引用(除非类本身是不可变的)。
    这在C++调用约定中广泛使用。当对象很小时,可以通过值传递对象。编译器将生成副本,但该副本不是昂贵的操作。对于任何其他类型,如果函数不会更改对象,则可以传递类型的常量实例的引用(通常称为常量引用)。这不会复制对象,而是将其传递到函数中。但同时,编译器将保证在函数内部不会更改对象。
    这是一些基本规则:
    - 对于原始类型,应优先使用传值。 - 对于其他类型,应优先使用具有常量引用的传引用。 - 如果函数需要修改参数,请使用传引用。 - 如果参数是可选的,请使用传递指针(如果不希望修改可选值,则使用传递指向常量的指针)。
    这些规则还存在一些小偏差,其中第一个是处理对象的所有权。当使用new动态分配对象时,必须使用delete(或其[]版本)进行解除分配。负责销毁对象的对象或函数被认为是资源的所有者。当在代码片段中创建动态分配的对象,但所有权被转移给不同的元素时,通常使用传递指针的语义,或者如果可能,使用智能指针。
    重要的是坚持C++和Java引用之间的区别的重要性。在C++中,引用在
    // C++
    class Type; // defined somewhere before, with the appropriate operations
    void swap( Type & a, Type & b ) {
       Type tmp = a;
       a = b;
       b = tmp;
    }
    int main() {
       Type a, b;
       Type old_a = a, old_b = b;
       swap( a, b );
       assert( a == old_b );
       assert( b == old_a ); 
    }
    

    上述的swap函数通过引用修改其两个参数。在Java中最接近的代码:

    public class C {
       // ...
       public static void swap( C a, C b ) {
          C tmp = a;
          a = b;
          b = tmp;
       }
       public static void main( String args[] ) {
          C a = new C();
          C b = new C();
          C old_a = a;
          C old_b = b;
          swap( a, b ); 
          // a and b remain unchanged a==old_a, and b==old_b
       }
    }
    

    Java代码版本将在内部修改引用的副本,但不会在外部修改实际对象。 Java引用是没有指针算术运算的C指针,以按值传递到函数中。


    4
    我喜欢“经验法则”这一部分,特别是“对原始类型使用值传递”的建议。 - yadab
    依据我的看法,这是更好的回答问题的方式。 - unrealsoul007

    24

    需要考虑几种情况。

    参数修改(“out” 和 “in/out” 参数)

    void modifies(T &param);
    // vs
    void modifies(T *param);
    

    这个案例主要关于代码风格:你想让代码看起来像call(obj)还是call(&obj)?然而,在下面的可选情况和当你想要重载运算符时,有两个点需要注意不同之处。

    ...还有可选项

    void modifies(T *param=0);  // default value optional, too
    // vs
    void modifies();
    void modifies(T &param);
    

    参数未修改

    void uses(T const &param);
    // vs
    void uses(T param);
    

    这是有趣的情况。经验法则是“易于复制”的类型按值传递——这些通常是小型类型(但不总是),而其他类型则通过const引用传递。然而,如果您需要在函数内部进行复制,则应该按值传递(应该按值传递)。 (是的,这暴露了一些实现细节。C'est le C++。

    ...可选

    void uses(T const *param=0);  // default value optional, too
    // vs
    void uses();
    void uses(T const &param);  // or optional(T param)
    

    这里各种情况之间的差异最小,所以选择让你生活最轻松的方式。

    按值传递的常量是一种实现细节

    void f(T);
    void f(T const);
    

    这些声明实际上是完全相同的函数!当按值传递时,const只是一种实现细节。试一试:

    void f(int);
    void f(int const) { /* implements above function, not an overload */ }
    
    typedef void NC(int);       // typedefing function types
    typedef void C(int const);
    
    NC *nc = &f;  // nc is a function pointer
    C *c = nc;    // C and NC are identical types
    

    3
    +1 我不知道通过值传递时 const 是一种实现方法。 - balki

    24

    按值传递:

    void func (vector v)
    

    当函数需要与环境完全隔离时,即防止函数修改原始变量以及在执行函数时防止其他线程修改其值时,请按值传递变量。

    缺点是CPU周期和额外内存用于复制对象。

    通过const引用传递:

    void func (const vector& v);
    

    这个形式模拟了按值传递的行为,同时去除了复制开销。函数可以读取原始对象,但不能修改其值。

    缺点是线程安全性:另一个线程对原始对象所做的任何更改将在函数执行时显示出来。

    按非const引用传递:

    void func (vector& v)
    

    当函数需要将某些值写回变量并最终由调用者使用时,请使用此选项。

    与常量引用情况一样,这也不是线程安全的。

    通过const指针传递:

    void func (const vector* vp);
    

    与按const引用传递在功能上相同,除了语法不同外,调用函数可以传递NULL指针以指示它没有有效数据可传递。

    不支持多线程安全。

    按非const指针传递:

    void func (vector* vp);
    

    类似于非const引用。调用方通常会在函数不会回写值时将变量设置为NULL。这种约定在许多glibc API中都可以看到。例如:

    void func (string* str, /* ... */) {
        if (str != NULL) {
            *str = some_value; // assign to *str only if it's non-null
        }
    }
    

    就像所有的按引用/指针传递一样,不是线程安全的。


    0

    由于没有人提到,我来补充一下。当你在C++中将一个对象传递给函数时,如果你没有自己的复制构造函数,那么默认的复制构造函数会被调用,它会创建一个对象的克隆并将其传递给方法。因此,当你改变对象的值时,这将反映在对象的副本上而不是原始对象上,这就是C++中的问题。因此,如果你将所有类属性都设置为指针,那么复制构造函数将复制指针属性的地址,因此当对对象进行方法调用以操作存储在指针属性地址中的值时,更改也会反映在作为参数传递的原始对象中,这可以像Java一样工作,但不要忘记所有类属性必须是指针,同时你应该改变指针的值,这将在代码解释中更清晰。

    Class CPlusPlusJavaFunctionality {
        public:
           CPlusPlusJavaFunctionality(){
             attribute = new int;
             *attribute = value;
           }
    
           void setValue(int value){
               *attribute = value;
           }
    
           void getValue(){
              return *attribute;
           }
    
           ~ CPlusPlusJavaFuncitonality(){
              delete(attribute);
           }
    
        private:
           int *attribute;
    }
    
    void changeObjectAttribute(CPlusPlusJavaFunctionality obj, int value){
       int* prt = obj.attribute;
       *ptr = value;
    }
    
    int main(){
    
       CPlusPlusJavaFunctionality obj;
    
       obj.setValue(10);
    
       cout<< obj.getValue();  //output: 10
    
       changeObjectAttribute(obj, 15);
    
       cout<< obj.getValue();  //output: 15
    }
    

    但这不是一个好主意,因为你最终会写很多涉及指针的代码,这些指针容易出现内存泄漏问题,而且不要忘记调用析构函数。为了避免这种情况,C++提供了复制构造函数,当包含指针的对象被传递给函数参数时,将创建新的内存,以防止操作其他对象的数据。Java采用按值传递方式,值是引用,因此不需要复制构造函数。


    0

    在编写函数和选择参数类型时,我需要传递指针、引用还是非指针和非引用值?

    这是一个很重要的问题,它会影响到函数的调用方式,具体取决于一些因素。

    最简单的选择是按值传递对象。这基本上在函数中创建了对象的副本,这有许多优点。但有时复制是昂贵的,在这种情况下,通常最好使用常量引用const&。有时您需要通过函数更改对象。然后需要使用非常量引用&

    有关参数类型选择的指导,请参见C++核心准则的函数部分,从F.15开始。作为一般规则,请尽量避免使用裸指针*


    -1

    将对象作为参数传递给函数有三种方法:

    1. 通过引用传递
    2. 通过值传递
    3. 在参数中添加常量

    请查看以下示例:

    class Sample
    {
    public:
        int *ptr;
        int mVar;
    
        Sample(int i)
        {
            mVar = 4;
            ptr = new int(i);
        }
    
        ~Sample()
        {
            delete ptr;
        }
    
        void PrintVal()
        {
            cout << "The value of the pointer is " << *ptr << endl
                 << "The value of the variable is " << mVar;
       }
    };
    
    void SomeFunc(Sample x)
    {
    cout << "Say i am in someFunc " << endl;
    }
    
    
    int main()
    {
    
      Sample s1= 10;
      SomeFunc(s1);
      s1.PrintVal();
      char ch;
      cin >> ch;
    }
    

    输出:

    假设我在someFunc函数中
    指针的值为-17891602
    变量的值为4


    只有两种方法(你提到的前两种)。不知道你所说的“在参数中传递常量”的意思。 - M.M

    -2
    以下是在C++中传递参数到函数的方式。
    1. 按值传递。
    // passing parameters by value . . .
    
    void foo(int x) 
    {
        x = 6;  
    }
    

    2. 通过引用。

    // passing parameters by reference . . .
    
    void foo(const int &x) // x is a const reference
    {
        x = 6;  
    }
    
    // passing parameters by const reference . . .
    
    void foo(const int &x) // x is a const reference
    {
        x = 6;  // compile error: a const reference cannot have its value changed!
    }
    

    3. 通过对象。

    class abc
    {
        display()
        {
            cout<<"Class abc";
        }
    }
    
    
    // pass object by value
    void show(abc S)
    {
        cout<<S.display();
    }
    
    // pass object by reference
    void show(abc& S)
    {
        cout<<S.display();
    }
    

    2
    “传递对象”并不是一个概念。只有按值传递和按引用传递。你的“case 3”实际上展示了一种按值传递和一种按引用传递的情况。 - M.M

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