12-10 按地址传递
在之前的课程中,我们已经介绍了两种不同的向函数传递参数的方法:按值传递(2.4 -- 函数参数和实参简介)和按引用传递(12.5 -- 按左值引用传递)。
以下是一个示例程序,展示了std::string如何按值和按引用传递对象:
#include <iostream>
#include <string>
void printByValue(std::string val) // The function parameter is a copy of str
{
std::cout << val << '\n'; // print the value via the copy
}
void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
std::cout << ref << '\n'; // print the value via the reference
}
int main()
{
std::string str{ "Hello, world!" };
printByValue(str); // pass str by value, makes a copy of str
printByReference(str); // pass str by reference, does not make a copy of str
return 0;
}
当我们str按值传递参数时,函数参数val接收到的是原始参数的副本。由于参数是原始参数的副本,因此对原始参数的任何更改val都会作用于副本,而不是原始参数。
当我们按str引用传递参数时,引用参数ref会绑定到实际的参数。这样就避免了复制参数。因为我们的引用参数是常量,所以我们不能修改它ref。但如果ref它是非常量,我们对它所做的任何更改ref都会影响到它本身str。
在这两种情况下,调用者都会提供实际对象(str)作为参数传递给函数调用。
按地址
C++ 提供了第三种向函数传递值的方式,称为按地址传递。使用按地址传递时,调用者不是提供对象本身作为参数,而是提供对象的地址(通过指针)。这个指针(保存着对象的地址)会被复制到被调用函数的指针参数中(此时该指针参数也保存着对象的地址)。然后,函数可以通过解引用该指针来访问被传递地址的对象。
#include <iostream>
#include <string>
void printByValue(std::string val) // The function parameter is a copy of str
{
std::cout << val << '\n'; // print the value via the copy
}
void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
std::cout << ref << '\n'; // print the value via the reference
}
void printByAddress(const std::string* ptr) // The function parameter is a pointer that holds the address of str
{
std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}
int main()
{
std::string str{ "Hello, world!" };
printByValue(str); // pass str by value, makes a copy of str
printByReference(str); // pass str by reference, does not make a copy of str
printByAddress(&str); // pass str by address, does not make a copy of str
return 0;
}
请注意这三个版本有多么相似。让我们更详细地探讨一下按地址传递的版本。
首先,因为我们希望printByAddress()函数按地址传递参数,所以我们将函数参数(形参)设为了一个名为 ptr 的指针。由于printByAddress() 将以只读方式使用,因此它(ptr)是一个指向常量值的指针。
以下是上述程序的一个版本,它增加了按地址传递的功能:
void printByAddress(const std::string* ptr)
{
std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}
在函数printByAddress()内部,我们通过解引用ptr参数来访问被指向对象的值。
其次,当调用该函数时,我们不能直接传入对象str本身——我们需要传入对象str的地址。最简单的方法是使用地址运算符 (&) 获取一个指向对象str地址的指针:
printByAddress(&str); // use address-of operator (&) to get pointer holding address of str
当执行此调用时,&str会创建一个指针来保存str地址。然后,该地址会被复制到函数参数ptr中,作为函数调用的一部分。因为ptr现在持有地址str,所以当函数解引用时ptr,它将获得的str值,函数会将该值打印到控制台。
就是这样。
虽然我们在上面的例子中使用了取地址运算符来获取变量str的地址,但如果我们已经有一个指针变量保存了变量str的地址,我们也可以直接使用该指针变量:
int main()
{
std::string str{ "Hello, world!" };
printByValue(str); // pass str by value, makes a copy of str
printByReference(str); // pass str by reference, does not make a copy of str
printByAddress(&str); // pass str by address, does not make a copy of str
std::string* ptr { &str }; // define a pointer variable holding the address of str
printByAddress(ptr); // pass str by address, does not make a copy of str
return 0;
}
std::string str{ "Hello, world!" };
printByAddress(&str); // use address-of operator (&) to get pointer holding address of str
命名法(
Nomenclature)
当我们使用operator&作为参数传递变量的地址时,我们称该变量是通过地址传递的。
当我们有一个指针变量保存着对象的地址,并且我们将该指针作为参数传递给相同类型的参数时,我们称对象是按地址传递的,而指针是按值传递的。
按地址传递不会复制被指向的对象。
请考虑以下陈述:
std::string str{ "Hello, world!" };
printByAddress(&str); // use address-of operator (&) to get pointer holding address of str
正如我们在12.5 节“按左值引用传递”中提到的,复制对象std::string开销很大,因此我们应该避免这样做。当我们按std::string地址传递对象时,我们并没有复制std::string对象本身,而只是将指向对象的指针(保存着对象的地址)从调用方复制到被调用函数。由于地址通常只有 4 或 8 个字节,指针也只有 4 或 8 个字节,因此复制指针总是很快的。
因此,就像按引用传递一样,按地址传递速度很快,并且避免了复制参数对象。
按地址传递参数允许函数修改参数值。
当我们通过地址传递对象时,函数会接收到被传递对象的地址,并可以通过解引用来访问该地址。由于这是实际传递的参数对象的地址(而不是对象的副本),因此如果函数参数是指向非常量值的指针,则函数可以通过指针参数修改参数:
#include <iostream>
void changeValue(int* ptr) // note: ptr is a pointer to non-const in this example
{
*ptr = 6; // change the value to 6
}
int main()
{
int x{ 5 };
std::cout << "x = " << x << '\n';
changeValue(&x); // we're passing the address of x to the function
std::cout << "x = " << x << '\n';
return 0;
}
打印出来的内容:

如您所见,参数已被修改,并且即使在changeValue()程序运行结束后,这种修改仍然存在。
如果一个函数不应该修改传入的对象,那么该函数的参数应该是一个指向常量的指针:
void changeValue(const int* ptr) // note: ptr is now a pointer to a const
{
*ptr = 6; // error: can not change const value
}
出于相同的许多原因,我们通常不使用常规const(非指针、非引用)函数参数(详见5.1 节——常量变量(命名常量) ),我们通常也不使用const指针函数参数。让我们做出两个断言:
- 用于将指针函数参数设为常量指针的关键字const几乎没有价值(因为它对调用者没有影响,主要作用是作为指针不会改变的文档)。
- const用于区分指向常量对象的指针和指向非常量对象的指针的关键字非常重要,因为调用者需要知道函数是否可以改变参数的值。
如果我们只使用非常量指针函数参数,那么所有 const 的使用都是有意义的。一旦我们开始使用const 来表示常量指针函数参数,就很难判断某个 const 的使用是否有意义。更重要的是,这也使得我们更难注意到指向非常量参数的指针。例如:
void foo(const char* source, char* dest, int count); // Using non-const pointers, all consts are significant.
void foo(const char* const source, char* const dest, int count); // Using const pointers, `dest` being a pointer-to-non-const may go unnoticed amongst the sea of spurious consts.
在前一种情况下,很容易看出 source是指向常量的指针,而 dest是指向非常量的指针。在后一种情况下,就很难看出 dest 是指向非常量的常量指针,其指向的对象可以被函数修改!
最佳实践
除非函数需要修改传入的对象,否则应优先使用指向 const 的函数参数,而不是指向非常量函数参数的指针。
除非有特殊原因,否则不要将函数参数设为 const 指针。
空值检查
现在请看这个看似无害的程序:
#include <iostream>
void print(int* ptr)
{
std::cout << *ptr << '\n';
}
int main()
{
int x{ 5 };
print(&x);
int* myPtr {};
print(myPtr);
return 0;
}

当这个程序运行时,它会打印出这个值5,然后很可能会崩溃。
在调用 print(myPtr) 函数时,myPtr 是一个空指针,因此函数参数 ptr 也将是空指针。当在函数体中解引用这个空指针时,会导致未定义行为。
#include <iostream>
void print(int* ptr)
{
}
int main()
{
int x{ 5 };
print(&x);
print(nullptr);
return 0;
}

注解:
AddressSanitizer:地址消毒器(内存错误检测工具)
DEADLYSIGNAL:致命信号
SEGV:段错误(Segmentation Violation)
on unknown address:在未知地址
pc:程序计数器(Program Counter)
bp:基指针(Base Pointer)
sp:栈指针(Stack Pointer)
T0:线程0(Thread 0)
The signal is caused by a READ memory access:该信号由读内存访问引起
Hint:提示
this fault was caused by a dereference of a high value address:此错误由解引用一个高值地址引起
Disassemble the provided pc to learn which register was used:反汇编提供的程序计数器以了解使用了哪个寄存器
nested bug in the same thread, aborting:同一线程中的嵌套错误,正在中止
[Finished in 0 seconds with code 1]:[在0秒内完成,退出代码为1]
通过地址传递参数时,在解引用该值之前,务必确保指针不是空指针。一种方法是使用条件语句:
#include <iostream>
void print(int* ptr)
{
if (ptr) // if ptr is not a null pointer
{
std::cout << *ptr << '\n';
}
}
int main()
{
int x{ 5 };
print(&x);
print(nullptr);
return 0;
}

在上面的程序中,我们在解引用之前会先检查它ptr是否为空。对于如此简单的函数来说,这样做没有问题,但在更复杂的函数中,这可能会导致逻辑冗余(多次检查 ptr 是否为空)或函数主要逻辑的嵌套(如果包含在代码块中)。
在大多数情况下,反其道而行之更为有效:先测试函数参数是否为空作为前提条件(9.6 -- Assert 和 static_assert),然后立即处理否定情况:
#include <iostream>
void print(int* ptr)
{
if (!ptr) // if ptr is a null pointer, early return back to the caller
return;
// if we reached this point, we can assume ptr is valid
// so no more testing or nesting required
std::cout << *ptr << '\n';
}
int main()
{
int x{ 5 };
print(&x);
print(nullptr);
return 0;
}

如果不应该将空指针传递给函数,则可以使用断言assert(我们在9.6 课中介绍过——断言和 static_assert)(因为断言旨在记录不应该发生的事情):
#include <iostream>
#include <cassert>
void print(const int* ptr) // now a pointer to a const int
{
assert(ptr); // fail the program in debug mode if a null pointer is passed (since this should never happen)
// (optionally) handle this as an error case in production mode so we don't crash if it does happen
if (!ptr)
return;
std::cout << *ptr << '\n';
}
int main()
{
int x{ 5 };
print(&x);
print(nullptr);
return 0;
}

优先按(常量)引用传递
请注意,上面示例中的函数print()对空值处理得并不好——它实际上会直接中止函数执行。既然如此,为什么还要允许用户传入空值呢?按引用传递与按地址传递具有相同的优点,而且避免了意外解引用空指针的风险。
与按地址传递相比,按常量引用传递还有一些其他优势。
首先,由于通过地址传递的对象必须具有地址,因此只有左值才能通过地址传递(因为右值没有地址)。通过常量引用传递则更加灵活,因为它既可以接受左值也可以接受右值:
#include <iostream>
void printByValue(int val) // The function parameter is a copy of the argument
{
std::cout << val << '\n'; // print the value via the copy
}
void printByReference(const int& ref) // The function parameter is a reference that binds to the argument
{
std::cout << ref << '\n'; // print the value via the reference
}
void printByAddress(const int* ptr) // The function parameter is a pointer that holds the address of the argument
{
std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}
int main()
{
printByValue(5); // valid (but makes a copy)
printByReference(5); // valid (because the parameter is a const reference)
printByAddress(&5); // error: can't take address of r-value
return 0;
}

其次,按引用传递的语法很自然,因为我们可以直接传递字面量或对象。而按地址传递的话,我们的代码最终会充斥着 & 符号和 * 符号。
在现代 C++ 中,大多数可以用地址传递完成的任务,用其他方法会更好。遵循这条常见的原则:“能按引用传递时就按引用传递,必须按地址传递时才按地址传递”。
最佳实践
除非有特殊原因需要使用地址传递方式,否则请优先使用引用传递方式,而不是地址传递方式。

浙公网安备 33010602011771号