9.9 - 按地址传递
在前面的课程中我们介绍了函数的两种传参方式:按值传递 (2.4 - 函数形参和实参) 和按引用传递 (9.5 -- Pass by lvalue reference)。
下面这段代码基于 std::string
对象分别演示了上面两种传参方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
When we pass argument str
by value, the function parameter val
receives a copy of the argument. Because the parameter is a copy of the argument, any changes to the val
are made to the copy, not the original argument.
When we pass argument str
by reference, the reference parameter ref
is bound to the actual argument. This avoids making a copy of the argument. Because our reference parameter is const, we are not allowed to change ref
. But if ref
were non-const, any changes we made to ref
would change str
.
In both cases, the caller is providing the actual object (str
) to be passed as an argument to the function call.
Pass by address
C++ provides a third way to pass values to a function, called pass by address. With pass by address, instead of providing an object as an argument, the caller provides an object’s address (via a pointer). This pointer (holding the address of the object) is copied into a pointer parameter of the called function (which now also holds the address of the object). The function can then dereference that pointer to access the object whose address was passed.
Here’s a version of the above program that adds a pass by address variant:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
COPY
Note how similar all three of these versions are. Let’s explore the pass by address version in more detail.
First, because we want our printByAddress()
function to use pass by address, we’ve made our function parameter a pointer named ptr
. Since printByAddress()
will use ptr
in a read-only manner, ptr
is a pointer to a const value.
1 2 3 4 |
|
COPY
Inside the printByAddress()
function, we dereference ptr
parameter to access the value of the object being pointed to.
Second, when the function is called, we can’t just pass in the str
object -- we need to pass in the address of str
. The easiest way to do that is to use the address-of operator (&) to get a pointer holding the address of str
:
1 |
|
COPY
When this call is executed, &str
will create a pointer holding the address of str
. This address is then copied into function parameter ptr
as part of the function call. Because ptr
now holds the address of str
, when the function dereferences ptr
, it will get the value of str
, which the function prints to the console.
That’s it.
Although we use the address-of operator in the above example to get the address of str
, if we already had a pointer variable holding the address of str
, we could use that instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
按地址传递不会创建被指的对象的拷贝
Consider the following statements:
1 2 |
|
As we noted in9.5 - 传递左值引用, 9.5 -- Pass by lvalue reference, copying a std::string
is expensive, so that’s something we want to avoid. When we pass a std::string
by address, we’re not copying the actual std::string
object -- we’re just copying the pointer (holding the address of the object) from the caller to the called function. Since an address is typically only 4 or 8 bytes, a pointer is only 4 or 8 bytes, so copying a pointer is always fast.
Thus, just like pass by reference, pass by address is fast, and avoids making a copy of the argument object.
Pass by address allows the function to modify the argument’s value
When we pass an object by address, the function receives the address of the passed object, which it can access via dereferencing. Because this is the address of the actual argument object being passed (not a copy of the object), if the function parameter is a pointer to non-const, the function can modify the argument via the pointer parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
COPY
This prints:
1 2 |
|
As you can see, the argument is modified and this modification persists even after changeValue()
has finished running.
If a function is not supposed to modify the object being passed in, the function parameter can be made a pointer to const:
1 2 3 4 |
|
COPY
指针判空
Now consider this fairly innocent looking program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
COPY
When this program is run, it will print the value 5
and then most likely crash.
In the call to print(myptr)
, myptr
is a null pointer, so function parameter ptr
will also be a null pointer. When this null pointer is dereferenced in the body of the function, undefined behavior results.
When passing a parameter by address, care should be taken to ensure the pointer is not a null pointer before you dereference the value. One way to do that is to use a conditional statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
COPY
In the above program, we’re testing ptr
to ensure it is not null before we dereference it. While this is fine for such a simple function, in more complicated functions this can result in redundant logic (testing if ptr is not null multiple times) or nesting of the primary logic of the function (if contained in a block).
In most cases, it is more effective to do the opposite: test whether the function parameter is null as a precondition (7.17 -- Assert and static_assert) and handle the negative case immediately:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
COPY
If a null pointer should never be passed to the function, an assert
(which we covered in lesson 7.17 -- Assert and static_assert) can be used instead (or also) (as asserts are intended to document things that should never happen):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
COPY
推荐按(const)引用传递
Note that function print()
in the example above doesn’t handle null values very well -- it effectively just aborts the function. Given this, why allow a user to pass in a null value at all? Pass by reference has the same benefits as pass by address without the risk of inadvertently dereferencing a null pointer.
Pass by const reference has a few other advantages over pass by address.
First, because an object being passed by address must have an address, only lvalues can be passed by address (as rvalues don’t have addresses). Pass by const reference is more flexible, as it can accept lvalues and rvalues:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
COPY
Second, the syntax for pass by reference is natural, as we can just pass in literals or objects. With pass by address, our code ends up littered with ampersands (&) and asterisks (*).
In modern C++, most things that can be done with pass by address are better accomplished through other methods. Follow this common maxim: “Pass by reference when you can, pass by address when you must”.
最佳实践
Prefer pass by reference to pass by address unless you have a specific reason to use pass by address.