void foo(Object o)
声明一个函数 foo,它将从名为 'o' 的类 'Object' 的新实例开始执行。
这称为“按值传递”,但更准确地说是“复制”,因为 foo 收到的是它自己的、我们称之为 foo 的 Object 实例的个人副本。当“foo”结束时,它所知道、被喂养和完成学业的“Object o”将不复存在。
void foo(Object& o)
声明一个函数 foo,它将以对“对象”的现有实例的引用开始执行,该引用将被称为“o”。如果你戳或戳它,你将改变原来的。
这称为“按引用传递”。
void foo(Object* o)
声明一个函数 foo,它将从一个名为“o”的变量开始执行,该变量包含应该是“Object”实例的地址。如果您更改此变量,通过执行“o = nullptr”之类的操作,它只会影响 foo 内部的外观。但是,如果您将塞缪尔·杰克逊 (Samuel L Jackson) 发送到该地址,他可以进行持续超过 foo 生命周期的愤怒报复。
void foo(Object*& o)
声明一个函数 foo,它将从一个名为“o”的变量开始执行,该变量是对指向对象 o 实例的指针的引用——它就像一个别名,除了没有编译器优化,它实际上是由编译器使用一种指针。
让我们分别尝试这些。
#include <iostream>
#include <cstdint>
struct Object
{
int m_i;
void event(const char* what, const char* where)
{
std::cout <<
what<< " " << (void*)this <<
" value " << m_i <<
" via " << where <<
std::endl;
}
// Construct an object with a specific value.
Object(int i) : m_i(i)
{
event("Constructed", "Operator(int i)");
}
// This is called the copy constructor, create one object from another.
Object(const Object& rhs) : m_i(rhs.m_i)
{
event("Constructed", "Operator(const Object&)");
}
// This is how to handle Object o1, o2; o1 = o2;
Object& operator=(const Object& rhs)
{
m_i = rhs.m_i;
event("Assigned", "operator=");
return *this;
}
// Handle destruction of an instance.
~Object() { event("Destructed", "~Object"); }
};
void foo1(Object o)
{
std::cout << "Entered foo1, my o has value " << o.m_i << std::endl;
// poke our local o
o.m_i += 42;
std::cout << "I changed o.m_i, it is " << o.m_i << std::endl;
}
void foo2(Object* o)
{
std::cout << "Foo2 starts with a pointer, it's value is " << (uintptr_t)o << std::endl;
std::cout << "That's an address: " << (void*)o << std::endl;
std::cout << "m_i of o has the value " << o->m_i << std::endl;
o->m_i += 42;
std::cout << "I've changed it tho, now it's " << o->m_i << std::endl;
}
void foo3(Object& o)
{
std::cout << "foo3 begins with a reference called o, " << std::endl <<
"which is sort of like a pointer but the compiler does some magic " << std::endl <<
"and we can use it like a local concrete object. " <<
std::endl <<
"Right now o.m_i is " << o.m_i <<
std::endl;
o.m_i += 42;
std::cout << "Only now, it is " << o.m_i << std::endl;
}
void foo4(Object*& o)
{
std::cout << "foo4 begins with a reference to a pointer, " << std::endl <<
"the pointer has the value " << (uintptr_t)o << " which is " <<
(void*)o <<
std::endl <<
"But the pointer points to an Object with m_i of " << o->m_i << std::endl <<
"which we accessed with '->' because the reference is to a pointer, " <<
"not to an Object." <<
std::endl;
o->m_i += 42;
std::cout << "I poked o's m_i and now it is " << o->m_i << std::endl;
// Now for something really dastardly.
o = new Object(999);
std::cout << "I just changed the local o to point to a new object, " <<
(uintptr_t)o << " or " << (void*)o << " with m_i " << o->m_i <<
std::endl;
}
int main()
{
std::cout << "Creating our first objects." << std::endl;
Object o1(100), o2(200);
std::cout << "Calling foo1 with o1" << std::endl;
foo1(o1);
std::cout << "back in main, o1.m_i is " << o1.m_i << std::endl;
std::cout << "Calling foo2 with &o1" << std::endl;
foo2(&o1);
std::cout << "back in main, o1.m_i is " << o1.m_i << std::endl;
std::cout << "Calling foo3(o2), which looks like the way we called foo1." << std::endl;
foo3(o2);
std::cout << "back in main, o2.m_i is " << o2.m_i << std::endl;
std::cout << "Creating our pointer." << std::endl;
Object* optr;
std::cout << "Setting it to point to 'o2'" << std::endl;
optr = &o2;
std::cout << "optr now has the value " << (uintptr_t)optr <<
" which is the address " << (void*)optr <<
" which points to an Object with m_i = " << optr->m_i <<
std::endl;
foo4(optr);
std::cout << "back in main, o2 has the value " << o2.m_i << std::endl <<
"and now optr has the value " << (uintptr_t)optr << std::endl <<
"and optr->m_i is now " << optr->m_i <<
std::endl;
if (optr != &o2)
delete optr; // otherwise we'd technically be leaking memory.
return 0;
}
ideone.com 上的现场演示。
按值传递
这个术语在 C++ 开发的早期使人们感到困惑,因为用通俗的话来说,这听起来像是“Object& foo”会做的事情。
术语“按值传递”实际上源于语言必须执行的调用此类函数的操作,以按值方式将整个原始对象/结构复制到堆栈上,或者在复制 ctor 可用的情况下,将它们转发给按值构造的构造函数并重新创建原始值的副本。
值传递应该用于最简单的情况,在这些情况下,您不希望正在调用的函数对当前范围内的值产生副作用。
bool checkWidthdrawl(Dollars balance, Dollars amountToWithdraw)
{
// it's safe for me to change "balance" here because balance is mine
}
对比
bool checkWidthdrawl(Dollars& balance, Dollars amountToWithdraw)
{
balance -= amountToWithdraw;
if (balance < 0)
std::complaint << "My account seems to be missing $" << amountToWithdraw;
}
但是,通过引用传递可能会变得昂贵。
struct FourK { char a[1024], b[1024], c[1024], d[1024]; }
如果你整天按值传递它,你就有可能在某个时候炸毁你的堆栈,以及花费大量时间复制所有这些字节。
void foo(int i); // Unless you need to see the changes to i, this is perfectly fine.
void foo(FourK f); // Someone should hunt you down and yell "PEANUT" in your ear.
通过引用传递
引用实际上是指针系统的契约,它允许语言确保您真正谈论的是对象的具体实例,从而允许您引用函数外部值的预先存在的实例。
当然,有办法打破这一点,但语言非常非常努力地让它们难以做到。例如,尝试将其添加到上面的代码中:
Object& makeObjectNotWar(int i)
{
Object thisObjectGoesAway(i);
return thisObjectGoesAway /*right about now*/;
}
您还可以向调用者保证该函数不会对带有“const”修饰符的变量产生任何副作用。
void fooc(const Object& o)
{
o.m_i += 42; // Error
}
您甚至可以在函数中使用它作为对您自己(和编译器)的提示,您不想意外更改值,这是一个可以为编译器提供优化提示的情况:
std::vector<int> foo;
add1000valuesTo(foo);
const size_t fooSize = foo.size();
for (size_t i = 0; i < fooSize; ++i) {
// ... stuff you're sure won't decrease foo.size()
}
没有 const fooSize
for (size_t i = 0; i < foo.size(); ++i) {
编译器必须首先假设“foo.size()”可以在循环的任何给定迭代中更改。它可能会发现它没有,但是通过给它提示,你节省了一点编译时间,可能提高了你的性能,并使人们更容易准确地判断出你所期望的行为。缺点:如果你的循环确实改变了 foo 的大小,你会通过错误报告发现:(
关于传递引用的最后一件事是 C++ 引用不受保护或“引用计数”。该语言只承诺引用将在其范围内有效,只要您不做任何愚蠢的事情,比如调用删除对象的东西。
// Author intended this function to be called
// by the owner of a Dog.
void doneWithFoo(Dog& dog)
{
Dog* deadDog = &dog;
delete deadDog;
}
Rover& Babysitter::babysitDog(Dog& rover, int hours)
{
rover.feed(FeedType::Donut);
if (rover.pooped())
doneWithDog(rover);
// ...
return rover; // I have a bad feeling about this.
}
显然,您不会期望“babysitDog”会导致狗被丢弃。但是请记住,因为我们传入了一个引用,所以它也从调用者那里消失了,如果那是使用引用... rover 死了,Dave,死了。
与指针一样,如果您要将引用存储在您可以访问它们的范围之外,那么您将负责确保被引用的对象保持不变,或者在对象离开之前从容器中删除引用离开。