12-11 按地址传递(第二部分)
本课是12.10 课的延续——按地址传递。
按地址传递“可选”参数
按地址传递参数的一个常见用途是允许函数接受一个“可选”参数。用例子来说明比用文字描述更容易:
#include <iostream>
void printIDNumber(const int *id=nullptr)
{
if (id)
std::cout << "Your ID number is " << *id << ".\n";
else
std::cout << "Your ID number is not known.\n";
}
int main()
{
printIDNumber(); // we don't know the user's ID yet
int userid { 34 };
printIDNumber(&userid); // we know the user's ID now
return 0;
}
此示例输出:

在这个程序中,该printIDNumber()函数有一个参数,该参数通过地址传递,默认值为空nullptr。在程序内部main(),我们调用该函数两次。第一次调用时,我们不知道用户的 ID,因此printIDNumber()不带参数调用。id参数默认值为空nullptr,函数输出Your ID number is not known.。第二次调用时,我们现在有了有效的 ID,因此调用printIDNumber(&userid)。id参数接收地址userid,因此函数输出Your ID number is 34.。
然而,在很多情况下,函数重载是实现相同结果的更好替代方案:
#include <iostream>
void printIDNumber()
{
std::cout << "Your ID is not known\n";
}
void printIDNumber(int id)
{
std::cout << "Your ID is " << id << "\n";
}
int main()
{
printIDNumber(); // we don't know the user's ID yet
int userid { 34 };
printIDNumber(userid); // we know the user is 34
printIDNumber(62); // now also works with rvalue arguments
return 0;
}
这样做有很多优点:我们不再需要担心空指针解引用,而且我们可以传入字面量或其他右值作为参数。

改变指针参数指向的内容
当我们向函数传递一个地址时,该地址会从参数复制到指针参数中(这没问题,因为复制地址很快)。现在考虑以下程序:
#include <iostream>
// [[maybe_unused]] gets rid of compiler warnings about ptr2 being set but not used
void nullify([[maybe_unused]] int* ptr2)
{
ptr2 = nullptr; // Make the function parameter a null pointer
}
int main()
{
int x{ 5 };
int* ptr{ &x }; // ptr points to x
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
nullify(ptr);
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
return 0;
}
该程序会输出:

如您所见,更改指针参数所持有的地址不会影响实参所持有的地址(ptr仍然指向x)。当函数nullify()被调用时,ptr2会收到传入地址的副本(在本例中,是实参所持有的地址ptr,即x的地址)。当函数更改指向的ptr2内容时,这只会影响实参所持有的副本ptr2。
那么,如果我们想允许一个函数改变指针参数所指向的内容呢?
按地址传递……按引用传递?
没错,确实可以。就像我们可以按引用传递普通变量一样,我们也可以按引用传递指针。以下是与上面相同的程序,只是将指针ptr2改为指向地址的引用refptr:
#include <iostream>
void nullify(int*& refptr) // refptr is now a reference to a pointer
{
refptr = nullptr; // Make the function parameter a null pointer
}
int main()
{
int x{ 5 };
int* ptr{ &x }; // ptr points to x
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
nullify(ptr);
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
return 0;
}
该程序会输出:

因为refptr现在是对指针的引用,当ptr作为参数传递时,refptr它绑定到ptr。这意味着对的任何更改refptr都会应用到ptr。
顺便提一下……
由于指针引用并不常见,很容易混淆语法(是int*&还是int&*?)。好消息是,如果你用反了,编译器会报错,因为你不能让指针指向引用(指针必须保存对象的地址,而引用不是对象)。然后你就可以把它们反过来用了。
为什么不再首选使用 0 or NULL(可选)
在本小节中,我们将解释为什么不再推荐使用0或NULL。
该字面量0既可以解释为整数字面量,也可以解释为空指针字面量。在某些情况下,我们想要表达的是哪一种可能存在歧义——在某些情况下,编译器可能会误认为我们指的是其中一种,而实际上我们指的是另一种——这会导致程序行为出现意想不到的后果。
预处理器宏的定义NULL并未在语言标准中定义。它可以定义为0、0L、((void*)0),或者完全是其他的东西。
在第11.1 课——函数重载简介中,我们讨论了函数可以重载(多个函数可以同名,只要它们可以通过参数的数量或类型来区分)。编译器可以根据函数调用中传递的参数来确定你想要调用哪个重载函数。
使用0或时NULL,这可能会导致问题:
#include <iostream>
#include <cstddef> // for NULL
void print(int x) // this function accepts an integer
{
std::cout << "print(int): " << x << '\n';
}
void print(int* ptr) // this function accepts an integer pointer
{
std::cout << "print(int*): " << (ptr ? "non-null\n" : "null\n");
}
int main()
{
int x{ 5 };
int* ptr{ &x };
print(ptr); // always calls print(int*) because ptr has type int* (good)
print(0); // always calls print(int) because 0 is an integer literal (hopefully this is what we expected)
print(NULL); // this statement could do any of the following:
// call print(int) (Visual Studio does this)
// call print(int*)
// result in an ambiguous function call compilation error (gcc and Clang do this)
print(nullptr); // always calls print(int*)
return 0;
}

在作者的电脑上(使用 clang),这段代码会输出:

当传递整数值0作为参数时,编译器会优先选择 print(int) 而不是 print(int*)。这可能会导致意想不到的结果,尤其是在我们预期 print(int*) 使用空指针参数调用函数时。
如果 NULL 被定义为 0 ,print(NULL) 则也会调用 print(int),这 print(int*) 与你对空指针字面量的预期不同。如果 NULL 未被定义为 0,则可能会出现其他行为,例如调用 print(int*) 或编译错误。
使用nullptr可以消除这种歧义(它总是会调用print(int*)),因为nullptr只会匹配指针类型。
std::nullptr_t(可选)
由于nullptr在函数重载中可以与整数值区分开来,因此它必须具有不同的类型。那么,它 nullptr 是什么类型呢?答案是,它 nullptr 的类型为 std::nullptr_t(定义在头文件 <cstddef> 中)。std::nullptr_t 只能包含一个值:nullptr。虽然这看起来有点傻,但在某些情况下却很有用。如果我们想要编写一个只接受字面量参数为nullptr的函数 ,我们可以将参数设为 std::nullptr_t。
#include <iostream>
#include <cstddef> // for std::nullptr_t
void print(std::nullptr_t)
{
std::cout << "in print(std::nullptr_t)\n";
}
void print(int*)
{
std::cout << "in print(int*)\n";
}
int main()
{
print(nullptr); // calls print(std::nullptr_t)
int x { 5 };
int* ptr { &x };
print(ptr); // calls print(int*)
ptr = nullptr;
print(ptr); // calls print(int*) (since ptr has type int*)
return 0;
}

在上面的例子中,函数调用print(nullptr)解析为print(std::nullptr_t)函数,因为它不需要转换 print(int*)。
有一种情况可能会让人有点困惑,那就是当我们调用 print(ptr)时, ptr 持有的是一个值nullptr。记住,函数重载匹配的是类型,而不是值,而 ptr 的类型T是 int*。因此, print(int*) 会被匹配。在这种情况下,print(std::nullptr_t)甚至都不在考虑范围内,因为指针类型不会隐式转换为 std::nullptr_t。
你可能永远用不到这个,但了解一下总是好的,以防万一。
只能按值传递
既然你已经了解了按引用传递、按地址传递和按值传递之间的基本区别,那么让我们暂时用简化的方式来探讨一下。😃
虽然编译器通常可以完全优化掉引用,但有些情况下无法做到这一点,引用确实必不可少。引用通常由编译器使用指针来实现。这意味着,在底层,按引用传递本质上就是按地址传递。
在上一课中,我们提到按地址传递只是将调用者的地址复制到被调用函数——这实际上就是按值传递地址。
因此,我们可以得出结论:C++ 实际上完全是按值传递参数!按地址(和引用)传递的特性仅仅来自于我们可以解引用传递的地址来改变参数,而这对于普通的值参数来说是无法做到的!

浙公网安备 33010602011771号