我试图了解如何使用参考参数。我的文本中有几个例子,但是它们太复杂了,我无法理解为什么以及如何使用它们。
您要如何以及为什么要使用参考?如果您没有将参数设置为引用,而是将其&
关闭,会发生什么?
例如,这些功能之间有什么区别:
int doSomething(int& a, int& b);
int doSomething(int a, int b);
我知道使用引用变量是为了更改正式->引用,然后允许双向交换参数。但是,这是我的知识范围,更具体的例子会有很大帮助。
我试图了解如何使用参考参数。我的文本中有几个例子,但是它们太复杂了,我无法理解为什么以及如何使用它们。
您要如何以及为什么要使用参考?如果您没有将参数设置为引用,而是将其&
关闭,会发生什么?
例如,这些功能之间有什么区别:
int doSomething(int& a, int& b);
int doSomething(int a, int b);
我知道使用引用变量是为了更改正式->引用,然后允许双向交换参数。但是,这是我的知识范围,更具体的例子会有很大帮助。
将引用视为别名。当您在引用上调用某些东西时,您实际上是在引用所指的对象上调用它。
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);
i
将是x
. _ 因此设置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.
所以,如果你看过指针版本交换函数,我们传递我们想要交换的变量的地址,然后我们进行交换,取消引用以获取和设置值。
让我们举一个简单的例子,一个名为的函数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
GMan 的回答为您提供了参考资料的内幕。我只是想向您展示一个非常基本的函数,它必须使用 references: swap
,它交换两个变量。这是int
s (根据您的要求):
// 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”。指针上的其他操作是加法和减法,但这也是另一天的话题。
// 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);
引用用于传递变量的位置,因此它们不需要在堆栈上复制到新函数。
一对可以在线运行的简单示例。
第一个使用普通函数,第二个使用引用:
编辑 - 这是源代码,以防您不喜欢链接:
示例 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
}
我不知道这是否是最基本的,但这里......
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
内部进行修改,它可以在不搞砸调用者的情况下这样做。
打个比方怎么样:假设你的函数计算罐子里的豆子。它需要一罐豆子,并且您需要知道不能作为返回值的结果(出于多种原因)。您可以将 jar 和变量值发送给它,但您永远不会知道它是否或将值更改为什么。相反,您需要通过返回地址的信封向它发送该变量,以便它可以将值放入其中并知道它已将结果写入所述地址的值。
如果我错了,请纠正我,但引用只是一个取消引用的指针,还是?
与指针的区别在于,您不能轻易提交 NULL。