56

我试图了解如何使用参考参数。我的文本中有几个例子,但是它们太复杂了,我无法理解为什么以及如何使用它们。

您要如何以及为什么要使用参考?如果您没有将参数设置为引用,而是将其&关闭,会发生什么?

例如,这些功能之间有什么区别:

int doSomething(int& a, int& b);
int doSomething(int a, int b);

我知道使用引用变量是为了更改正式->引用,然后允许双向交换参数。但是,这是我的知识范围,更具体的例子会有很大帮助。

4

8 回答 8

126

将引用视为别名。当您在引用上调用某些东西时,您实际上是在引用所指的对象上调用它。

int i;
int& j = i; // j is an alias to i

j = 5; // same as i = 5

在功能方面,请考虑:

void foo(int i)
{
    i = 5;
}

上面,是一个值,传递的参数是按值int i传递的。这意味着如果我们说:

int x = 2;
foo(x);

ix. _ 因此设置i为 5 对 没有影响,因为它是被更改x的副本。x但是,如果我们做i一个参考:

void foo(int& i) // i is an alias for a variable
{
    i = 5;
}

然后说foo(x)不再复制x; i x。所以如果我们说foo(x),函数内部i = 5;是完全一样的x = 5;,并且x变化。

希望这能澄清一点。


为什么这很重要?编程时,您永远不想复制和粘贴代码。您想要创建一个执行一项任务并且做得很好的函数。每当需要执行该任务时,您都可以使用该功能。

假设我们要交换两个变量。看起来像这样:

int x, y;

// swap:
int temp = x; // store the value of x
x = y;        // make x equal to y
y = temp;     // make y equal to the old value of x

好,太棒了。我们想让它成为一个函数,因为:swap(x, y);更容易阅读。所以,让我们试试这个:

void swap(int x, int y)
{
    int temp = x;
    x = y;
    y = temp;
}

这行不通!问题是这是交换两个变量的副本。那是:

int a, b;
swap(a, b); // hm, x and y are copies of a and b...a and b remain unchanged

在 C 中,如果引用不存在,解决方案是传递这些变量的地址;也就是说,使用指针*:

void swap(int* x, int* y)
{
    int temp = *x;
    *x = *y;
    *y = temp;
}

int a, b;
swap(&a, &b);

这很好用。但是,它使用起来有点笨拙,实际上有点不安全。swap(nullptr, nullptr),交换两个空并取消引用空指针...未定义的行为!可通过一些检查修复:

void swap(int* x, int* y)
{
    if (x == nullptr || y == nullptr)
        return; // one is null; this is a meaningless operation

    int temp = *x;
    *x = *y;
    *y = temp;
}

但是看起来我们的代码变得多么笨拙。C++ 引入了解决这个问题的参考。如果我们可以给一个变量起别名,我们就会得到我们正在寻找的代码:

void swap(int& x, int& y)
{
    int temp = x;
    x = y;
    y = temp;
}

int a, b;
swap(a, b); // inside, x and y are really a and b

既易于使用,又安全。(我们不能不小心传入一个空值,没有空值引用。)这是可行的,因为函数内部发生的交换实际上发生在函数外部别名的变量上。

(注意,永远不要编写swap函数。:) 一个已经存在于 header<algorithm>中,并且它被模板化以适用于任何类型。)


另一个用途是删除调用函数时发生的副本。考虑我们有一个非常大的数据类型。复制这个对象需要很多时间,我们想避免这种情况:

struct big_data
{ char data[9999999]; }; // big!

void do_something(big_data data);

big_data d;
do_something(d); // ouch, making a copy of all that data :<

但是,我们真正需要的只是变量的别名,所以让我们指出这一点。(同样,回到 C 语言中,我们将传递大数据类型的地址,解决了复制问题,但引入了笨拙。):

void do_something(big_data& data);

big_data d;
do_something(d); // no copies at all! data aliases d within the function

这就是为什么你会听到它说你应该一直通过引用传递东西,除非它们是原始类型。(因为在内部传递别名可能是通过指针完成的,就像在 C 中一样。对于小对象来说,制作副本然后担心指针会更快。)

请记住,您应该是 const 正确的。这意味着如果您的函数不修改参数,请将其标记为const. 如果do_something上面只看但没有改变data,我们会将其标记为const

void do_something(const big_data& data); // alias a big_data, and don't change it

我们避免复制说“嘿,我们不会修改它”。这还有其他副作用(例如临时变量),但您现在不必担心。

相反,我们的swap函数不能是const,因为我们确实在修改别名。

希望这能澄清更多。


*粗略的指针教程:

指针是保存另一个变量地址的变量。例如:

int i; // normal int

int* p; // points to an integer (is not an integer!)
p = &i; // &i means "address of i". p is pointing to i

*p = 2; // *p means "dereference p". that is, this goes to the int
        // pointed to by p (i), and sets it to 2.

所以,如果你看过指针版本交换函数,我们传递我们想要交换的变量的地址,然后我们进行交换,取消引用以获取和设置值。

于 2010-04-02T03:45:58.147 回答
4

让我们举一个简单的例子,一个名为的函数increment增加它的参数。考虑:

void increment(int input) {
 input++;
}

这将不起作用,因为更改发生在传递给实际参数的函数的参数副本上。所以

int i = 1;
std::cout<<i<<" ";
increment(i);
std::cout<<i<<" ";

1 1作为输出产生。

为了使函数对传递的实际参数起作用,我们将其传递reference给函数:

void increment(int &input) { // note the & 
 input++;
}

input对函数内部所做的更改实际上是对实际参数进行的。这将产生预期的输出1 2

于 2010-04-02T03:46:37.487 回答
4

GMan 的回答为您提供了参考资料的内幕。我只是想向您展示一个非常基本的函数,它必须使用 references: swap,它交换两个变量。这是ints (根据您的要求):

// changes to a & b hold when the function exits
void swap(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
}

// changes to a & b are local to swap_noref and will go away when the function exits
void swap_noref(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
}

// changes swap_ptr makes to the variables pointed to by pa & pb
// are visible outside swap_ptr, but changes to pa and pb won't be visible
void swap_ptr(int *pa, int *pb) {
    int tmp = *pa;
    *pa = *pb;
    *pb = tmp;
}

int main() {
    int x = 17;
    int y = 42;
    // next line will print "x: 17; y: 42"
    std::cout << "x: " << x << "; y: " << y << std::endl

    // swap can alter x & y
    swap(x,y);
    // next line will print "x: 42; y: 17"
    std::cout << "x: " << x << "; y: " << y << std::endl

    // swap_noref can't alter x or y
    swap_noref(x,y);
    // next line will print "x: 42; y: 17"
    std::cout << "x: " << x << "; y: " << y << std::endl

    // swap_ptr can alter x & y
    swap_ptr(&x,&y);
    // next line will print "x: 17; y: 42"
    std::cout << "x: " << x << "; y: " << y << std::endl
}

s有一个更聪明的交换实现,int不需要临时的。但是,在这里我更在意清晰而不是聪明。

如果没有引用(或指针),swap_noref就无法更改传递给它的变量,这意味着它根本无法工作。swap_ptr可以改变变量,但它使用的是混乱的指针(当引用不能完全削减它时,指针可以完成这项工作)。swap是最简单的整体。

关于指针

指针可以让你做一些与引用相同的事情。然而,指针将更多的责任交给了程序员来管理它们和它们指向的内存(一个叫做“内存管理”的话题——但现在不要担心)。因此,参考文献应该是您现在的首选工具。

将变量视为绑定到存储值的框的名称。常量是直接绑定到值的名称。两者都将名称映射到值,但不能更改常量的值。虽然保存在盒子中的值可以改变,但 name 到盒子的绑定不能,这就是为什么不能改变引用来引用不同的变量的原因。

变量的两个基本操作是获取当前值(只需使用变量名即可完成)和分配新值(赋值运算符,'=')。值存储在内存中(保存值的盒子只是内存的一个连续区域)。例如,

int a = 17;

结果类似于(注意:在下文中,“foo @ 0xDEADBEEF”代表存储在地址“0xDEADBEEF”处的名称为“foo”的变量。内存地址已组成):

             ____
a @ 0x1000: | 17 |
             ----

存储在内存中的所有内容都有一个起始地址,因此还有一个操作:获取值的地址(“&”是地址运算符)。指针是存储地址的变量。

int *pa = &a;

结果是:

              ______                     ____
pa @ 0x10A0: |0x1000| ------> @ 0x1000: | 17 |
              ------                     ----

请注意,指针只是存储一个内存地址,因此它无法访问它所指向的名称。事实上,指针可以指向没有名字的东西,但这是另一天的话题。

对指针有一些操作。您可以取消引用指针(“*”运算符),它为您提供指针指向的数据。取消引用与获取地址相反:*&a与 相同的框a&*pa与 相同的值pa,与*pa相同的框a。特别是,pa在示例中保存 0x1000;* pa表示“内存中位置 pa 的 int”,或“内存中位置 0x1000 的 int”。“a”也是“内存位置 0x1000 的 int”。指针上的其他操作是加法和减法,但这也是另一天的话题。

于 2010-04-02T04:13:39.030 回答
1
// Passes in mutable references of a and b.
int doSomething(int& a, int& b) {
  a = 5;
  cout << "1: " << a << b;  // prints 1: 5,6
}

a = 0;
b = 6;
doSomething(a, b);
cout << "2: " << a << ", " << b;  // prints 2: 5,6

或者,

// Passes in copied values of a and b.
int doSomething(int a, int b) {
  a = 5;
  cout << "1: " << a << b;  // prints 1: 5,6
}

a = 0;
b = 6;
doSomething(a, b);
cout << "2: " << a << ", " << b;  // prints 2: 0,6

或 const 版本:

// Passes in const references a and b.
int doSomething(const int &a, const int &b) {
  a = 5;  // COMPILE ERROR, cannot assign to const reference.
  cout << "1: " << b;  // prints 1: 6
}

a = 0;
b = 6;
doSomething(a, b);

引用用于传递变量的位置,因此它们不需要在堆栈上复制到新函数。

于 2010-04-02T03:50:47.403 回答
1

一对可以在线运行的简单示例。

第一个使用普通函数,第二个使用引用:


编辑 - 这是源代码,以防您不喜欢链接:

示例 1

using namespace std;

void foo(int y){
    y=2;
}

int main(){
    int x=1;
    foo(x);
    cout<<x;//outputs 1
}


示例 2

using namespace std;

void foo(int & y){
    y=2;
}

int main(){
    int x=1;
    foo(x);
    cout<<x;//outputs 2
}
于 2010-04-02T04:00:31.927 回答
0

我不知道这是否是最基本的,但这里......

typedef int Element;
typedef std::list<Element> ElementList;

// Defined elsewhere.
bool CanReadElement(void);
Element ReadSingleElement(void); 

int ReadElementsIntoList(int count, ElementList& elems)
{
    int elemsRead = 0;
    while(elemsRead < count && CanReadElement())
        elems.push_back(ReadSingleElement());
    return count;
}

在这里,我们使用引用将元素列表传递到ReadElementsIntoList(). 这样,该函数将元素直接加载到列表中。如果我们不使用引用,那么elems将是传入列表的副本,其中将添加元素,但elems在函数返回时将被丢弃。

这是双向的。在 的情况下count,我们让它成为引用,因为我们不想修改传入的计数,而是返回读取的元素数。这允许调用代码将实际读取的元素数量与请求的数量进行比较;如果它们不匹配,则CanReadElement()必须已返回false,并且立即尝试阅读更多内容可能会失败。如果它们匹配,那么可能count少于可用元素的数量,并且进一步阅读将是合适的。最后,如果ReadElementsIntoList()需要在count内部进行修改,它可以在不搞砸调用者的情况下这样做。

于 2010-04-02T03:52:38.680 回答
0

打个比方怎么样:假设你的函数计算罐子里的豆子。它需要一罐豆子,并且您需要知道不能作为返回值的结果(出于多种原因)。您可以将 jar 和变量值发送给它,但您永远不会知道它是否或将值更改为什么。相反,您需要通过返回地址的信封向它发送该变量,以便它可以将值放入其中并知道它已将结果写入所述地址的值。

于 2015-09-06T01:15:35.647 回答
0

如果我错了,请纠正我,但引用只是一个取消引用的指针,还是?

与指针的区别在于,您不能轻易提交 NULL。

于 2017-10-05T20:02:53.827 回答