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)。
上述程序输出结果:

因为我们传递的是整个结构体对象(而不是单个成员),所以无论结构体对象有多少成员,我们都只需要一个参数。而且,将来如果我们决定向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;
}

我们可以通过两种方式创建临时对象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;
}
打印出来的内容:

函数内部定义的结构体通常按值返回,以免返回悬空引用。
在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;
}

问题 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;
}

问题 3
在上一道测验题的解答中,为什么是getFraction()按值返回而不是按引用返回?
显示解决方案
由于tempFraction 是一个局部变量,它会在函数结束时超出作用域。如果我们以temp引用的形式返回它,就会将一个悬空引用返回给调用者。

浙公网安备 33010602011771号