提问人:Rakesh K 提问时间:1/26/2010 最后编辑:Jan SchultkeRakesh K 更新时间:9/27/2023 访问量:146967
我应该如何将对象传递给函数?
How should I pass objects to functions?
问:
我是 C++ 编程的新手,但我有 Java 经验。我需要有关如何在 C++ 中将对象传递给函数的指导。
是否需要传递指针、引用或非指针和非引用值?我记得在 Java 中没有这样的问题,因为我们只传递了包含对对象的引用的变量。
如果您还能解释在哪里使用这些选项,那就太好了。
答:
C++11的经验法则:
按值传递,除非
- 你不需要对象的所有权,一个简单的别名就可以了,在这种情况下,你通过
const
引用传递, - 您必须更改对象,在这种情况下,请使用非常
量
左值引用的传递, - 将派生类的对象作为基类传递,在这种情况下,需要通过引用传递。(使用前面的规则来确定是否通过引用传递。
const
几乎从不建议通过指针。可选参数最好表示为 a(对于较旧的 std 库),并且通过引用可以很好地完成别名。std::optional
boost::optional
C++11 的移动语义使得按值传递和返回更具吸引力,即使对于复杂的对象也是如此。
C++03的经验法则:
通过 const
引用传递参数,除非
- 它们将在函数内部进行更改,并且此类更改应反映在外部,在这种情况下,您将通过非常
量
引用进行传递 - 该函数应该在没有任何参数的情况下是可调用的,在这种情况下,您可以通过指针传递,以便用户可以传递 // 代替;应用前面的规则来确定是否应传递指向
const
参数的指针NULL
0
nullptr
- 它们是内置类型,可以通过复制传递
- 它们将在函数内部更改,并且此类更改不应反映在外部,在这种情况下,您可以通过复制传递(另一种方法是根据以前的规则传递并在函数内部复制)
(在这里,“按值传递”称为“按复制传递”,因为在 C++03 中,按值传递总是创建一个副本)
还有更多,但这几条初学者的规则会让你走得很远。
评论
C++ 和 Java 中的调用约定存在一些差异。在 C++ 中,从技术上讲,只有两种约定:按值传递和按引用传递,一些文献包括第三种按指针传递约定(实际上是指针类型的按值传递)。最重要的是,您可以为参数类型添加恒定性,从而增强语义。
通过引用传递
通过引用传递意味着该函数在概念上将接收对象实例,而不是它的副本。从概念上讲,引用是调用上下文中使用的对象的别名,不能为 null。在函数内部执行的所有操作都适用于函数外部的对象。此约定在 Java 或 C 中不可用。
按值传递(和按指针传递)
编译器将在调用上下文中生成对象的副本,并在函数中使用该副本。在函数内部执行的所有操作都是对副本执行的,而不是对外部元素执行的。这是 Java 中基元类型的约定。
它的一个特殊版本是将指针(对象的地址)传递到函数中。该函数接收指针,并且应用于指针本身的任何和所有操作都将应用于副本(指针),另一方面,应用于取消引用的指针的操作将应用于该内存位置的对象实例,因此该函数可能会产生副作用。使用指向对象的指针的按值传递的效果将允许内部函数修改外部值,就像按引用传递一样,并且还允许可选值(传递空指针)。
这是 C 语言中函数需要修改外部变量时使用的约定,也是 Java 中用于引用类型的约定:引用被复制,但引用的对象是相同的:对引用/指针的更改在函数外部不可见,但对指向内存的更改是可见的。
将常量添加到等式中
在 C++ 中,在定义不同级别的变量、指针和引用时,可以为对象分配常量。可以将变量声明为常量,可以声明对常量实例的引用,并且可以定义指向常量对象的所有指针、指向可变对象的常量指针和指向常量元素的常量指针。相反,在 Java 中,您只能定义一个级别的常量(最终关键字):变量的常量(原始类型的实例,引用类型的引用),但您不能定义对不可变元素的引用(除非类本身是不可变的)。
这在 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 指针,它们按值传递到函数中。
评论
有几种情况需要考虑。
参数修改(“out”和“in/out”参数)
void modifies(T ¶m);
// vs
void modifies(T *param);
这种情况主要与风格有关:您希望代码看起来像 call(obj) 还是 call(&obj)?但是,有两点的区别很重要:下面的可选大小写,以及您希望在重载运算符时使用引用。
...和可选
void modifies(T *param=0); // default value optional, too
// vs
void modifies();
void modifies(T ¶m);
参数未修改
void uses(T const ¶m);
// vs
void uses(T param);
这是一个有趣的案例。经验法则是“复制成本低廉”类型是按值传递的——这些通常是小类型(但并非总是如此)——而其他类型则由 const ref 传递。但是,如果您无论如何都需要在函数中复制,则应按值传递。(是的,这暴露了一些实现细节。C'est le C++.)
...和可选
void uses(T const *param=0); // default value optional, too
// vs
void uses();
void uses(T const ¶m); // or optional(T param)
所有情况之间的差异最小,因此请选择最轻松的一种。
Const by value 是实现细节
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
评论
const
有三种方法可以将对象作为参数传递给函数:
- 通过引用传递
- 按值传递
- 在参数中添加常量
请看以下示例:
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
评论
按值传递:
void func (vector v)
当函数需要与环境完全隔离时,按值传递变量,即防止函数修改原始变量,以及防止其他线程在执行函数时修改其值。
缺点是复制对象所花费的 CPU 周期和额外内存。
通过常量引用传递:
void func (const vector& v);
此表单模拟按值传递行为,同时消除复制开销。该函数获得对原始对象的读取访问权限,但无法修改其值。
缺点是线程安全性:另一个线程对原始对象所做的任何更改都会在函数仍在执行时显示在函数内部。
通过非常量引用传递:
void func (vector& v)
当函数必须将某个值写回变量时,请使用此选项,该值最终将由调用方使用。
就像 const 参考案例一样,这不是线程安全的。
通过 const 指针传递:
void func (const vector* vp);
除了语法不同之外,在功能上与通过 const-reference 传递相同,再加上调用函数可以传递 NULL 指针以指示它没有要传递的有效数据。
不是线程安全的。
通过非常量指针传递:
void func (vector* vp);
类似于非常量引用。当函数不应写回值时,调用方通常将变量设置为 NULL。此约定在许多 glibc API 中都可以看到。例:
void func (string* str, /* ... */) {
if (str != NULL) {
*str = some_value; // assign to *str only if it's non-null
}
}
就像所有通过引用/指针传递一样,不是线程安全的。
由于没有人提到我正在添加它,当您在 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 确实通过值传递并且值是引用,因此它不需要复制构造函数。
以下是在 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();
}
评论
是否需要传递指针、引用或非指针和非引用值?
在编写函数和选择它所采用的参数类型时,这是一个很重要的问题。该选择将影响函数的调用方式,这取决于一些因素。
最简单的选择是按值传递对象。这基本上在函数中创建对象的副本,这具有许多优点。但有时复制成本很高,在这种情况下,通常最好使用恒定的引用。有时你需要通过函数来改变你的对象。然后需要一个非常量引用 。const&
&
有关参数类型选择的指南,请参阅 C++ 核心指南的“函数”部分,从 F.15 开始。作为一般规则,尽量避免使用原始指针。*
评论