13-10 传递和返回结构体

考虑一个由 3 个松散变量表示的员工:

int main()
{
    int id { 1 };
    int age { 24 };
    double wage { 52400.0 };

    return 0;
}

如果我们想把这个员工信息传递给一个函数,我们需要传递三个变量:

#include <iostream>

void printEmployee(int id, int age, double wage)
{
    std::cout << "ID:   " << id << '\n';
    std::cout << "Age:  " << age << '\n';
    std::cout << "Wage: " << wage << '\n';
}

int main()
{
    int id { 1 };
    int age { 24 };
    double wage { 52400.0 };

    printEmployee(id, age, wage);

    return 0;
}

虽然传递 3 个单独的员工变量不算太糟糕,但试想一下,如果我们需要传递 10 个或 12 个员工变量,逐个传递会非常耗时且容易出错。此外,如果我们给员工添加一个新属性(例如姓名),我们就必须修改所有函数的声明、定义和函数调用,以接受新的参数!


传递结构体(按引用)

使用结构体而非单个变量的一大优势在于,我们可以将整个结构体传递给需要操作其成员的函数。为了避免复制,结构体通常按引用传递(通常是常量引用)。

#include <iostream>

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

void printEmployee(const Employee& employee) // note pass by reference here
{
    std::cout << "ID:   " << employee.id << '\n';
    std::cout << "Age:  " << employee.age << '\n';
    std::cout << "Wage: " << employee.wage << '\n';
}

int main()
{
    Employee joe { 14, 32, 24.15 };
    Employee frank { 15, 28, 18.27 };

    // Print Joe's information
    printEmployee(joe);

    std::cout << '\n';

    // Print Frank's information
    printEmployee(frank);

    return 0;
}

在上面的例子中,我们将整个传递Employee给printEmployee()(两次,一次用于joe,一次用于frank)。

上述程序输出结果:

image

因为我们传递的是整个结构体对象(而不是单个成员),所以无论结构体对象有多少成员,我们都只需要一个参数。而且,将来如果我们决定向Employee结构体添加新成员,也不需要修改函数声明或函数调用!新成员会自动包含在内。

相关内容:
在第 12.6 课中,我们讨论了何时按值传递结构体,何时按引用传递结构体——按常量左值引用传递。


传递临时结构体

在前面的例子中,我们joe在将 Employee 变量传递给函数之前就创建了它printEmployee()。这样我们就可以给 Employee 变量命名,这对于文档编写很有用。但这同时也需要两条语句(一条用于创建joe,一条用于使用joe)。

如果某个变量只使用一次,却需要为其命名并分别创建和使用该变量,这会增加代码的复杂性。在这种情况下,使用临时对象可能更为合适。临时对象并非变量,因此它没有标识符。

以下是与上面相同的示例,但我们用临时对象替换了joe变量frank:

#include <iostream>

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

void printEmployee(const Employee& employee) // note pass by reference here
{
    std::cout << "ID:   " << employee.id << '\n';
    std::cout << "Age:  " << employee.age << '\n';
    std::cout << "Wage: " << employee.wage << '\n';
}

int main()
{
    // Print Joe's information
    printEmployee(Employee { 14, 32, 24.15 }); // construct a temporary Employee to pass to function (type explicitly specified) (preferred)

    std::cout << '\n';

    // Print Frank's information
    printEmployee({ 15, 28, 18.27 }); // construct a temporary Employee to pass to function (type deduced from parameter)

    return 0;
}

image

我们可以通过两种方式创建临时对象Employee。第一种方式是使用 Employee { 14, 32, 24.15 } 语法。这会告诉编译器创建一个Employee临时对象,并使用提供的初始化器对其进行初始化。这是首选语法,因为它明确地表明了我们要创建的临时对象类型,编译器不会误解我们的意图。

在第二次调用中,我们使用了语法{ 15, 28, 18.27 }。编译器足够智能,能够理解提供的参数必须转换为 Employee 类型,函数调用才能成功。请注意,这种形式被视为隐式转换,因此在只接受显式转换的情况下将不起作用。

相关内容
我们将在第14.13 课——临时类对象中更多地讨论类类型的临时对象和转换。

关于临时对象还有几点需要注意:它们在定义时创建并初始化,并在创建它们的完整表达式结束后销毁。临时对象的求值是一个右值表达式,因此只能在接受右值的地方使用。当临时对象用作函数参数时,它只能绑定到接受右值的参数。这包括按值传递和按常量引用传递,但不包括按非常量引用传递和按地址传递。


返回结构体

考虑这样一种情况:我们有一个函数需要返回三维笛卡尔空间中的一个点。这个点有三个属性:x 坐标、y 坐标和 z 坐标。但是函数只能返回一个值。那么,我们如何才能将所有三个坐标都返回给用户呢?

一种常见的方法是返回一个结构体:

#include <iostream>

struct Point3d
{
    double x { 0.0 };
    double y { 0.0 };
    double z { 0.0 };
};

Point3d getZeroPoint()
{
    // We can create a variable and return the variable (we'll improve this below)
    Point3d temp { 0.0, 0.0, 0.0 };
    return temp;
}

int main()
{
    Point3d zero{ getZeroPoint() };

    if (zero.x == 0.0 && zero.y == 0.0 && zero.z == 0.0)
        std::cout << "The point is zero\n";
    else
        std::cout << "The point is not zero\n";

    return 0;
}

打印出来的内容:

image

函数内部定义的结构体通常按值返回,以免返回悬空引用。

在getZeroPoint()上面的函数中,我们创建了一个新的命名对象(temp),只是为了能够返回它:

Point3d getZeroPoint()
{
    // We can create a variable and return the variable (we'll improve this below)
    Point3d temp { 0.0, 0.0, 0.0 };
    return temp;
}

对象名称(temp)在这里并没有提供任何文档价值。

我们可以通过返回一个临时(未命名/匿名)对象来稍微改进我们的函数:

Point3d getZeroPoint()
{
    return Point3d { 0.0, 0.0, 0.0 }; // return an unnamed Point3d
}

在这种情况下,会创建一个临时对象Point3d,将其复制回调用者,然后在表达式结束时销毁它。请注意这种方式多么简洁(一行代码而不是两行,而且无需考虑是否temp多次使用)。

相关内容
我们将在第14.13 课——临时类对象中更详细地讨论匿名对象。


推断返回类型

如果函数具有明确的返回类型(例如 int Point3d),我们甚至可以省略返回语句中的类型:

Point3d getZeroPoint()
{
    // We already specified the type at the function declaration
    // so we don't need to do so here again
    return { 0.0, 0.0, 0.0 }; // return an unnamed Point3d
}

这被认为是一种隐式转换。

另请注意,由于本例中我们返回的都是零值,因此我们可以使用空花括号来返回一个已初始化的 Point3d 对象:

Point3d getZeroPoint()
{
    // We can use empty curly braces to value-initialize all members
    return {};
}

结构是重要的组成部分

结构体本身就很有用,而类(C++ 和面向对象编程的核心)则直接建立在我们这里介绍的概念之上。深入理解结构体(尤其是数据成员、成员选择和默认成员初始化)将大大简化你过渡到类的过程。

测验时间

问题 1

你运营着一个网站,现在想计算广告收入。编写一个程序,允许你输入以下三项数据:

广告观看次数。
有多少百分比的用户点击了广告?
每次点击广告的平均收益。
将这三个值存储在一个结构体中。将该结构体传递给另一个函数,该函数打印每个值。打印函数还应打印出你当天的收入总额(将这三个字段的值相乘)。

显示提示:如果将百分比存储为整数,则在计算当天收入时,还需要除以 100。

显示解决方案

#include <iostream>

// First we need to define our Advertising struct
struct Advertising
{
    int adsShown {};
    double clickThroughRatePercentage {};
    double averageEarningsPerClick {};
};

Advertising getAdvertising()
{
    Advertising temp {};
    std::cout << "How many ads were shown today? ";
    std::cin >> temp.adsShown;
    std::cout << "What percentage of ads were clicked on by users? ";
    std::cin >> temp.clickThroughRatePercentage;
    std::cout << "What was the average earnings per click? ";
    std::cin >> temp.averageEarningsPerClick;

    return temp;
}

void printAdvertising(const Advertising& ad)
{
    std::cout << "Number of ads shown: " << ad.adsShown << '\n';
    std::cout << "Click through rate: " << ad.clickThroughRatePercentage << '\n';
    std::cout << "Average earnings per click: $" << ad.averageEarningsPerClick << '\n';

    // The following line is split up to reduce the length
    // We need to divide ad.clickThroughRatePercentage by 100 because it's a percent of 100, not a multiplier
    std::cout << "Total Earnings: $"
        << (ad.adsShown * ad.clickThroughRatePercentage / 100 * ad.averageEarningsPerClick) << '\n';
}

int main()
{
    // Declare an Advertising struct variable
    Advertising ad{ getAdvertising() };
    printAdvertising(ad);

    return 0;
}

image

问题 2

创建一个结构体来存储分数。该结构体应该包含一个整数分子和一个整数分母。

编写一个函数,从用户处读取一个分数,并使用该分数读取两个分数对象。编写另一个函数,将两个分数相乘,并将结果以分数的形式返回(无需约分)。编写最后一个函数,打印一个分数。

程序的输出应该与以下内容相符:

Enter a value for the numerator: 1
Enter a value for the denominator: 2

Enter a value for the numerator: 3
Enter a value for the denominator: 4

Your fractions multiplied together: 3/8

两个分数相乘,所得分数的分子是这两个分数的分子的乘积,所得分数的分母是这两个分数的分母的乘积。

显示解决方案

#include <iostream>

struct Fraction
{
    int numerator{ 0 };
    int denominator{ 1 };
};

Fraction getFraction()
{
    Fraction temp{};
    std::cout << "Enter a value for numerator: ";
    std::cin >> temp.numerator;
    std::cout << "Enter a value for denominator: ";
    std::cin >> temp.denominator;
    std::cout << '\n';

    return temp;
}

constexpr Fraction multiply(const Fraction& f1, const Fraction& f2)
{
    return { f1.numerator * f2.numerator, f1.denominator * f2.denominator };
}

void printFraction(const Fraction& f)
{
    std::cout << f.numerator << '/' << f.denominator << '\n';
}

int main()
{
    Fraction f1{ getFraction() };
    Fraction f2{ getFraction() };

    std::cout << "Your fractions multiplied together: ";

    printFraction(multiply(f1, f2));

    return 0;
}

image

问题 3

在上一道测验题的解答中,为什么是getFraction()按值返回而不是按引用返回?

显示解决方案

由于tempFraction 是一个局部变量,它会在函数结束时超出作用域。如果我们以temp引用的形式返回它,就会将一个悬空引用返回给调用者。
posted @ 2025-12-24 07:24  游翔  阅读(15)  评论(0)    收藏  举报