函数参数和实参

考虑以下程序:

#include <iostream>

int getValueFromUser()
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  

    return input;
}

int main()
{
    int num { getValueFromUser() };

    std::cout << num << " doubled is: " << num * 2 << '\n';

    return 0;
}

然而,如果我们也想将输出行放入它自己的函数中呢?你可能会尝试如下代码:

#include <iostream>

int getValueFromUser()
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  

    return input;
}

// 这个函数不会编译
void printDouble()
{
    std::cout << num << " doubled is: " << num * 2 << '\n';
}

int main()
{
    int num { getValueFromUser() };

    printDouble();

    return 0;
}

这不会编译,因为printDouble函数不知道标识符num是什么。你可能会尝试在printDouble函数内部定义num变量:

void printDouble()
{
    int num{}; // 我们添加了这一行
    std::cout << num << " doubled is: " << num * 2 << '\n';
}

虽然这解决了编译器错误并使程序可编译,但程序仍然不正确(它总是打印"0 doubled is: 0")。问题的核心在于printDouble函数没有方法访问用户输入的值。

我们需要某种方式将变量num的值传递给printDouble函数,以便printDouble可以在函数体中使用该值。

函数参数和实参的基本概念

在许多情况下,将信息传递给被调用的函数是有用的,这样函数就有数据可以工作。例如,如果我们想编写一个函数来添加两个数字,我们需要某种方式告诉函数在调用时添加哪两个数字。否则,函数怎么知道要添加什么?我们通过函数参数和实参来实现这一点。

函数参数是在函数头部使用的变量。函数参数的工作方式几乎与在函数内部定义的变量相同,但有一个区别:它们是用函数调用者提供的值初始化的。

函数参数是在函数头部通过将它们放在函数名称后的括号内来定义的,多个参数用逗号分隔。

以下是一些具有不同数量参数的函数示例:

// 这个函数不接受任何参数
// 它不依赖于调用者提供任何东西
void doPrint()
{
    std::cout << "In doPrint()\n";
}

// 这个函数接受一个名为x的整数参数
// 调用者将提供x的值
void printValue(int x)
{
    std::cout << x << '\n';
}

// 这个函数有两个整数参数,一个命名为x,一个命名为y
// 调用者将提供x和y的值
int add(int x, int y)
{
    return x + y;
}

实参是在函数调用时从调用者传递给函数的值:

doPrint(); // 这个调用没有实参
printValue(6); // 6是传递给函数printValue的实参
add(2, 3); // 2和3是传递给函数add的实参

注意,多个实参也用逗号分隔。

参数和实参如何协同工作

当函数被调用时,函数的所有参数都会被创建为变量,每个实参的值都会被复制到匹配的参数中(使用复制初始化)。这个过程被称为按值传递。使用按值传递的函数参数被称为值参数。

例如:

#include <iostream>

// 这个函数有两个整数参数,一个命名为x,一个命名为y
// x和y的值由调用者传递
void printValues(int x, int y)
{
    std::cout << x << '\n';
    std::cout << y << '\n';
}

int main()
{
    printValues(6, 7); // 这个函数调用有两个实参,6和7

    return 0;
}

当函数printValues被调用并传递实参6和7时,printValues的参数x被创建并初始化为值6,printValues的参数y被创建并初始化为值7。 这将产生以下输出:

6
7

注意,实参的数量通常必须与函数参数的数量相匹配,否则编译器会抛出错误。

传递给函数的实参可以是任何有效的表达式(因为实参本质上只是参数的初始化器,而初始化器可以是任何有效的表达式)。

修复示例程序

现在我们有了修复课程开头提出的程序所需的工具:

#include <iostream>

int getValueFromUser()
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  

    return input;
}

void printDouble(int value) // 这个函数现在有一个整数参数
{
    std::cout << value << " doubled is: " << value * 2 << '\n';
}

int main()
{
    int num { getValueFromUser() };

    printDouble(num);

    return 0;
}

在这个程序中,变量num首先被初始化为用户输入的值。然后,调用函数printDouble,并将实参num的值复制到函数printDouble的值参数中。然后函数printDouble使用参数value的值。

使用返回值作为实参

在上面的问题中,我们可以看到变量num只使用了一次,用来将函数getValueFromUser的返回值传递到函数printDouble的调用实参中。

我们可以稍微简化上面的示例如下:

#include <iostream>

int getValueFromUser()
{
    std::cout << "Enter an integer: ";
    int input{};
    std::cin >> input;  

    return input;
}

void printDouble(int value)
{
    std::cout << value << " doubled is: " << value * 2 << '\n';
}

int main()
{
    printDouble(getValueFromUser());

    return 0;
}

现在,我们直接使用函数getValueFromUser的返回值作为函数printDouble的实参!

尽管这个程序更简洁(并且清楚地表明用户读取的值将不会被用于其他任何目的),你可能也会发现这种"紧凑语法"有点难以阅读。如果你更喜欢使用变量的版本,那也没关系。

参数和返回值如何协同工作

通过使用参数和返回值,我们可以创建接受数据作为输入,进行一些计算,并将值返回给调用者的函数。

这里有一个非常简单的函数示例,它将两个数字相加并返回结果给调用者:

#include <iostream>

// add()接受两个整数作为参数,并返回它们的和
// x和y的值由调用add()的函数确定
int add(int x, int y)
{
    return x + y;
}

// main不接受任何参数
int main()
{
    std::cout << add(4, 5) << '\n'; // 实参4和5被传递给函数add()
    return 0;
}

执行从main的顶部开始。当add(4, 5)被评估时,函数add被调用,参数x被初始化为值4,参数y被初始化为值5。

函数add中的返回语句计算x + y产生值9,然后返回回main。这个9的值然后被发送到std::cout在控制台上打印。

输出:

9

以图形格式: function-parameters-and-arguments

更多函数调用示例

让我们再看一些函数调用:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int multiply(int z, int w)
{
    return z * w;
}

int main()
{
    std::cout << add(4, 5) << '\n'; // 在add()中x=4,y=5,所以x+y=9
    std::cout << add(1 + 2, 3 * 4) << '\n'; // 在add()中x=3,y=12,所以x+y=15

    int a{ 5 };
    std::cout << add(a, a) << '\n'; // 计算(5 + 5)

    std::cout << add(1, multiply(2, 3)) << '\n'; // 计算1 + (2 * 3)
    std::cout << add(1, add(2, 3)) << '\n'; // 计算1 + (2 + 3)

    return 0;
}

这个程序产生以下输出:

9
15
10
7
6

第一个语句很简单。 在第二个语句中,实参是被评估的表达式,然后被传递。在这种情况下,1 + 2计算为3,所以3被复制到参数x。3 * 4计算为12,所以12被复制到参数y。add(3, 12)解析为15。

下一对语句也相对容易:

int a{ 5 };
std::cout << add(a, a) << '\n'; // 计算(5 + 5)

在这种情况下,add()被调用,其中a的值被复制到两个参数x和y。由于a的值为5,add(a, a) = add(5, 5),解析为值10。 让我们看看这组中的第一个棘手的语句:

std::cout << add(1, multiply(2, 3)) << '\n'; // 计算1 + (2 * 3)

当函数add被执行时,程序需要确定参数x和y的值。x很简单,因为我们只是传递了整数1。要得到参数y的值,它需要先评估multiply(2, 3)。程序调用multiply并初始化z = 2和w = 3,所以multiply(2, 3)返回整数值6。现在可以使用返回值6来初始化add函数的y参数。add(1, 6)返回整数7,然后传递给std::cout进行打印。

说得更简洁一些: add(1, multiply(2, 3))计算为add(1, 6)计算为7 以下语句看起来棘手,因为给add的一个实参是另一个对add的调用。

std::cout << add(1, add(2, 3)) << '\n'; // 计算1 + (2 + 3)

但这种情况与前一种情况完全相同。add(2, 3)首先解析,结果返回值为5。现在可以解析add(1, 5),计算为值6,然后传递给std::cout进行打印。

说得更简洁一些: add(1, add(2, 3))计算为add(1, 5) => 计算为6

未引用的参数和未命名的参数

在某些情况下,你会遇到有参数的函数,但这些参数在函数体中没有被使用。这些被称为未引用的参数。 作为一个简单的例子:

void doSomething(int count) // 警告:未引用的参数count
{
    // 这个函数过去用count做了一些事情,但现在不再使用了
}

int main()
{
    doSomething(4);

    return 0;
}

就像未使用的局部变量一样,你的编译器可能会警告变量count已被定义但未使用。

在函数定义中,函数参数的名称是可选的。因此,在函数参数需要存在但不在函数体中使用的场合,你可以简单地省略名称。没有名称的参数被称为未命名参数:

void doSomething(int) // 好的:未命名参数不会产生警告
{
}

Google C++风格指南建议使用注释来记录未命名参数是什么:

void doSomething(int /*count*/)
{
}

作者注解与高级内容

作者注 你可能想知道为什么我们会写一个函数,它的参数值没有被使用。这通常发生在类似以下情况:

假设我们有一个带有一个参数的函数。后来,函数以某种方式更新,参数的值不再需要。如果现在未使用的函数参数被简单地移除,那么每个现有的对函数的调用都会中断(因为函数调用将提供比函数可以接受的更多参数)。

这将要求我们找到每个对函数的调用并移除不需要的参数。这可能是一项大量的工作(并且需要大量的重新测试)。它甚至可能是不可能的(在我们不控制所有调用函数的代码的情况下)。所以,我们可能会保留参数原样,只是让它什么都不做。

对于高级读者 其他发生这种情况的情况:

运算符++和–有前缀和后缀变体(例如++foo与foo++)。未引用的函数参数用于区分这种运算符的重载是前缀还是后缀情况。我们在第21.8课–重载增量和减量运算符中介绍。

当我们需要根据类型模板参数的类型(而不是值)来确定某些事情时。

作者注 如果未命名参数对你来说仍然没有意义,不要担心。我们将在未来的课程中再次遇到它们,那时我们将有更多的上下文来解释它们何时有用。

最佳实践

最佳实践 当函数参数存在但在函数体中未使用时,不要给它命名。你可以在注释中放置一个名称。

结论

函数参数和返回值是通过可重用方式编写函数的关键机制,因为它允许我们编写可以执行任务并将检索或计算结果返回给调用者的函数,而无需事先知道具体的输入或输出是什么。

测验时间

问题 #1 这个程序片段有什么问题?

#include <iostream>

void multiply(int x, int y)
{
    return x * y;
}

int main()
{
    std::cout << multiply(4, 5) << '\n';

    return 0;
}

multiply()的返回类型是void,意味着它是一个非值返回函数。由于函数试图返回一个值(通过返回语句),这个函数将产生编译器错误。返回类型应该是int。

问题 #2 这个程序片段有两个问题?

#include <iostream>

int multiply(int x, int y)
{
    int product { x * y };
}

int main()
{
    std::cout << multiply(4) << '\n';

    return 0;
}

问题 #3 以下程序打印什么值?

#include <iostream>

int add(int x, int y, int z)
{
    return x + y + z;
}

int multiply(int x, int y)
{
    return x * y;
}

int main()
{
    std::cout << multiply(add(1, 2, 3), 4) << '\n';

    return 0;
}

multiply被调用,其中x = add(1, 2, 3),y = 4。首先,CPU解析x = add(1, 2, 3),返回1 + 2 + 3,或x = 6。multiply(6, 4) = 24,这是答案。

问题 #4 编写一个名为doubleNumber()的函数,它接受一个整数参数。该函数应该返回参数值的两倍。

int doubleNumber(int x)
{
    return 2 * x;
}

问题 #5

编写一个完整的程序,它从用户那里读取一个整数,使用你在上一个测验问题中编写的doubleNumber()函数将其加倍,然后将加倍的值打印到控制台。

#include <iostream>

int doubleNumber(int x)
{
    return 2 * x;
}

int main()
{
    std::cout << "Enter an integer value: ";
    int x{};
    std::cin >> x;
    
    std::cout << x << " doubled is: " << doubleNumber(x) << '\n';
    
    return 0;
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"