12-13 输入和输出形参
函数与其调用者之间通过两种机制进行通信:形参和返回值。当函数被调用时,调用者提供实参,函数通过其形参接收这些实参。这些实参可以按值、引用或地址传递。
通常情况下,我们会按值或常量引用传递形参。但有时我们可能需要采用其他方式。
输入形参
在大多数情况下,函数形参仅用于接收来自调用者的输入。仅用于接收来自调用者的输入的形参有时被称为输入形参in parameters。
#include <iostream>
void print(int x) // x is an in parameter
{
std::cout << x << '\n';
}
void print(const std::string& s) // s is an in parameter
{
std::cout << s << '\n';
}
int main()
{
print(5);
std::string s { "Hello, world!" };
print(s);
return 0;
}

通常情况下,输入形参是按值传递的,或者按常量引用传递的。
输出形参
通过非常量引用(或指向非常量对象的指针)传递函数实参,允许函数修改作为实参传递的对象的值。这为函数提供了一种在因某些原因无法使用返回值的情况下,将数据返回给调用者的方法。
仅用于向调用者返回信息的函数形参称为输出形参out parameter。
例如:
#include <cmath> // for std::sin() and std::cos()
#include <iostream>
// sinOut and cosOut are out parameters
void getSinCos(double degrees, double& sinOut, double& cosOut)
{
// sin() and cos() take radians, not degrees, so we need to convert
constexpr double pi { 3.14159265358979323846 }; // the value of pi
double radians = degrees * pi / 180.0;
sinOut = std::sin(radians);
cosOut = std::cos(radians);
}
int main()
{
double sin { 0.0 };
double cos { 0.0 };
double degrees{};
std::cout << "Enter the number of degrees: ";
std::cin >> degrees;
// getSinCos will return the sin and cos in variables sin and cos
getSinCos(degrees, sin, cos);
std::cout << "The sin is " << sin << '\n';
std::cout << "The cos is " << cos << '\n';
return 0;
}

该函数有一个形参degrees(其实参按值传递)作为输入,并“返回”两个形参(按引用)作为输出。
我们给这些输出形参加上了后缀“out”,以表明它们是输出形参。这有助于提醒调用者,传递给这些形参的初始值无关紧要,它们会被覆盖。按照惯例,输出形参通常是位于最右侧的形参。
让我们更详细地了解一下它的工作原理。首先,主函数创建了局部变量 sin和 cos。这些变量通过引用(而不是值)传递给函数。这意味着函数 getSinCos()可以访问 main()中实际存在的 sin 和 cos变量,而不仅仅是它们的副本。因此,函数会分别通过引用 sinOut和cosOut 为 sin和cos赋新值,从而覆盖 sin 和cos中的旧值。然后,函数会打印这些更新后的值。
如果sin 和 cos 是以值而不是引用的方式传递的,那么 getSinCos()会改变 sin 和cos副本,导致所有更改在函数结束时都被丢弃。但由于 sin 和cos是以引用的方式传递的,因此对sin 或cos(通过引用)所做的任何更改都会在函数结束后保留下来。因此,我们可以利用这种机制将值返回给调用者。
顺便提一下……
StackOverflow 上的这个答案很有意思,它解释了为什么不允许非常量左值引用绑定到右值/临时对象(因为隐式类型转换与输出形参out-parameters结合使用时会产生意外行为)。
输出形参的语法使用方式不自然
输出形参虽然功能齐全,但也存在一些缺点。
首先,调用者必须实例化(并初始化)对象,并将其作为实参传递,即使它并不打算使用这些对象。这些对象必须能够被赋值,这意味着它们不能被声明为 const。
其次,由于调用者必须传入对象,因此这些值不能用作临时值,也不能轻易地在单个表达式中使用。
以下示例展示了这两个缺点:
#include <iostream>
int getByValue()
{
return 5;
}
void getByReference(int& x)
{
x = 5;
}
int main()
{
// return by value
[[maybe_unused]] int x{ getByValue() }; // can use to initialize object
std::cout << getByValue() << '\n'; // can use temporary return value in expression
// return by out parameter
int y{}; // must first allocate an assignable object
getByReference(y); // then pass to function to assign the desired value
std::cout << y << '\n'; // and only then can we use that value
return 0;
}
如您所见,使用输出形参的语法有点不自然。
通过引用传递输出形参并不能清楚地表明实参将被修改。
当我们把函数的返回值赋给一个对象时,很明显,对象的值被修改了:
x = getByValue(); // obvious that x is being modified
这是好事,因为它清楚地表明我们应该预料到该值会x发生变化。
不过,让我们再看一下上面例子中的getSinCos()函数调用:
getSinCos(degrees, sin, cos);
从这个函数调用中无法明确看出 degrees是输入形参,但 sin 和 cos 是输出形参。如果调用者没有意识到 sin 和cos 会被修改,则很可能导致语义错误。
在某些情况下,使用按地址传递而不是按引用传递,可以要求调用者将对象的地址作为实参传递,从而使输出形参更加明确。
请看以下示例:
void foo1(int x); // pass by value
void foo2(int& x); // pass by reference
void foo3(int* x); // pass by address
int main()
{
int i{};
foo1(i); // can't modify i
foo2(i); // can modify i (not obvious)
foo3(&i); // can modify i
int *ptr { &i };
foo3(ptr); // can modify i (not obvious)
return 0;
}
请注意,在调用时foo3(&i),我们必须传入&i而不是i,这有助于更清楚地表明我们应该期望i被修改。
然而,这并非万无一失,因为它foo3(ptr)允许foo3()修改i,并且不要求调用者获取地址ptr。
调用者可能认为他们可以传入空指针nullptr作为有效实参,而实际上这是不允许的。现在函数需要进行空指针检查和处理,这增加了复杂性。这种额外的空指针处理需求通常比直接按引用传递实参更容易引发问题。
鉴于以上所有原因,除非没有其他好的选择,否则应避免使用输出形参。
最佳实践
避免使用输出形参(除非在极少数情况下没有更好的选择)。
对于非可选的输出形参,建议按引用传递。
输入/输出形参
在极少数情况下,函数会在覆盖输出形参的值之前实际使用其值。这种形参称为输入/输出形参in-out parameter。输入/输出形参的工作方式与输出形参完全相同,并面临同样的挑战。
何时通过非常量引用传递
如果为了避免复制参数而选择按引用传递,那么几乎总是应该按常量引用传递。
作者注
在以下示例中,我们将使用Foo来表示我们关心的某种类型。目前,您可以将 视为Foo您选择的类型的别名(例如std::string)。
然而,在以下两种主要情况下,通过非常量引用传递可能是更好的选择。
首先,当形参是输入输出形参时,请使用非常量引用传递。因为我们已经将所需的对象传入并传出,所以直接修改该对象通常会更直接、更高效。
void someFcn(Foo& inout)
{
// modify inout
}
int main()
{
Foo foo{};
someFcn(foo); // foo modified after this call, may not be obvious
return 0;
}
给函数起一个好名字会有帮助:
void modifyFoo(Foo& inout)
{
// modify inout
}
int main()
{
Foo foo{};
modifyFoo(foo); // foo modified after this call, slightly more obvious
return 0;
}
另一种方法是按值或常量引用传递对象(像往常一样),并按值返回一个新对象,然后调用者可以将其赋值回原始对象:
Foo someFcn(const Foo& in)
{
Foo foo { in }; // copy here
// modify foo
return foo;
}
int main()
{
Foo foo{};
foo = someFcn(foo); // makes it obvious foo is modified, but another copy made here
return 0;
}
这样做的好处是使用了更常规的返回语法,但需要进行 2 次额外的复制(有时编译器可以优化掉其中一个复制)。
其次,当函数原本会按值返回对象给调用者,但复制该对象开销极大时,应使用非常量引用传递。尤其是在性能关键代码段中多次调用该函数时,更应如此。
void generateExpensiveFoo(Foo& out)
{
// modify out
}
int main()
{
Foo foo{};
generateExpensiveFoo(foo); // foo modified after this call
return 0;
}
适合高级读者
上述情况最常见的例子是,当一个函数需要填充一个大型的 C 风格数组或std::array用数据填充数组时,而该数组的元素类型复制成本很高。我们将在后面的章节中讨论数组。
也就是说,复制对象的成本很少高到需要采用非常规方法来返回这些对象,因此这样做是值得的。
问:为什么不允许非常量左值引用绑定到右值/临时对象?
答:StackOverflow:Why is it illegal to take the address of an rvalue temporary?
Actually, in the original language design it was allowed to take the address of a temporary. As you have noticed correctly, there is no technical reason for not allowing this, and MSVC still allows it today through a non-standard language extension.
The reason why C++ made it illegal is that binding references to temporaries clashes with another C++ language feature that was inherited from C: Implicit type conversion. Consider:
// Source - https://stackoverflow.com/a/9779765
// Posted by ComicSansMS, modified by community. See post 'Timeline' for change history
// Retrieved 2025-12-18, License - CC BY-SA 3.0
void CalculateStuff(long& out_param) {
long result;
// [...] complicated calculations
out_param = result;
}
int stuff;
CalculateStuff(stuff); //< this won't compile in ISO C++
CalculateStuff() is supposed to return its result via the output parameter. But what really happens is this: The function accepts a long& but is given an argument of type int. Through C's implicit type conversion, that int is now implicitly converted to a variable of type long, creating an unnamed temporary in the process. So instead of the variable stuff, the function really operates on an unnamed temporary, and all side-effects applied by that function will be lost once that temporary is destroyed. The value of the variable stuff never changes.
References were introduced to C++ to allow operator overloading, because from the caller's point of view, they are syntactically identical to by-value calls (as opposed to pointer calls, which require an explicit & on the caller's side). Unfortunately it is exactly that syntactical equivalence that leads to troubles when combined with C's implicit type conversion.
Since Stroustrup wanted to keep both features (references and C-compatibility), he introduced the rule we all know today: Unnamed temporaries only bind to const references. With that additional rule, the above sample no longer compiles. Since the problem only occurs when the function applies side-effects to a reference parameter, it is still safe to bind unnamed temporaries to const references, which is therefore still allowed.
This whole story is also described in Chapter 3.7 of Design and Evolution of C++:
The reason to allow references to be initialized by non-lvalues was to allow the distinction between call-by-value and call-by-reference to be a detail specified by the called function and of no interest to the caller. For const references, this is possible; for non-const references it is not. For Release 2.0 the definition of C++ was changed to reflect this.
I also vaguely remember reading in a paper who first discovered this behavior, but I can't remember right now. Maybe someone can help me out?

浙公网安备 33010602011771号