考虑一个由3个独立变量表示的员工:
int main()
{
int id { 1 };
int age { 24 };
double wage { 52400.0 };
return 0;
}
如果我们要将这个员工传递给一个函数,我们需要传递3个变量:
#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个员工变量的函数。独立传递每个变量不仅耗时,而且容易出错。此外,如果我们为员工添加一个新的属性(例如名字),我们就需要修改所有函数的声明、定义以及函数调用,以接受新的参数和参数值!
通过引用传递结构体
使用结构体而不是独立变量的一个巨大优势是,我们可以将整个结构体传递给需要操作其成员的函数。结构体通常通过引用传递(通常是通过const
引用),以避免复制。
#include <iostream>
struct Employee
{
int id {};
int age {};
double wage {};
};
void printEmployee(const Employee& employee) // 注意这里通过引用传递
{
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 };
// 打印Joe的信息
printEmployee(joe);
std::cout << '\n';
// 打印Frank的信息
printEmployee(frank);
return 0;
}
在上述示例中,我们将整个Employee
对象传递给printEmployee()
(两次,一次传递joe
,一次传递frank
)。
上述程序的输出为:
ID: 14
Age: 32
Wage: 24.15
ID: 15
Age: 28
Wage: 18.27
因为我们传递的是整个结构体对象(而不是单独的成员),所以无论结构体对象有多少成员,我们只需要一个参数。而且,将来如果我们决定为Employee
结构体添加新成员,我们就不必更改函数的声明或函数调用了!新成员将自动包含在内。
相关内容
我们在第12.6课——通过const
左值引用传递中讨论了何时通过值传递结构体,何时通过引用传递结构体。
传递临时结构体
在前面的示例中,我们在将joe
传递给printEmployee()
函数之前创建了Employee
变量joe
。这允许我们为Employee
变量命名,这在文档编写方面可能会很有用。但它也需要两条语句(一条用于创建joe
,一条用于使用joe
)。
在某些情况下,我们只使用一个变量一次,那么给变量命名并将变量的创建和使用分开会增加复杂性。在这种情况下,使用临时对象可能更可取。临时对象不是变量,因此它没有标识符。
以下是与上面相同的示例,但我们用临时对象替换了变量joe
和frank
:
#include <iostream>
struct Employee
{
int id {};
int age {};
double wage {};
};
void printEmployee(const Employee& employee) // 注意这里通过引用传递
{
std::cout << "ID: " << employee.id << '\n';
std::cout << "Age: " << employee.age << '\n';
std::cout << "Wage: " << employee.wage << '\n';
}
int main()
{
// 打印Joe的信息
printEmployee(Employee { 14, 32, 24.15 }); // 创建一个临时Employee对象传递给函数(明确指定类型)(推荐)
std::cout << '\n';
// 打印Frank的信息
printEmployee({ 15, 28, 18.27 }); // 创建一个临时Employee对象传递给函数(类型从参数推导)
return 0;
}
我们可以用两种方式创建一个临时Employee
对象。在第一次调用中,我们使用Employee { 14, 32, 24.15 }
的语法。这告诉编译器创建一个Employee
对象,并用提供的初始化器初始化它。这是推荐的语法,因为它清楚地表明我们正在创建什么类型的临时对象,编译器不可能误解我们的意图。
在第二次调用中,我们使用{ 15, 28, 18.27 }
的语法。编译器足够聪明,能够理解提供的参数必须转换为一个Employee
对象,以便函数调用能够成功。请注意,这种形式被认为是一种隐式转换,因此在只接受显式转换的情况下,它将无法工作。
相关内容
我们在第14.13课——临时类对象中更详细地讨论了类类型的临时对象和转换。
关于临时对象的其他几点:它们在定义点被创建和初始化,并在创建它们的完整表达式的末尾被销毁。临时对象的求值是一个右值表达式,它只能用在接受右值的地方。当临时对象作为函数参数使用时,它只会绑定到接受右值的参数。这包括按值传递和按const
引用传递,不包括按非常量引用传递和按地址传递。
返回结构体
考虑一个函数需要返回三维笛卡尔空间中的一个点的情况。这样的点有3个属性:一个x坐标、一个y坐标和一个z坐标。但函数只能返回一个值。那么我们如何将所有3个坐标返回给用户呢?
一种常见的方式是返回一个结构体:
#include <iostream>
struct Point3d
{
double x { 0.0 };
double y { 0.0 };
double z { 0.0 };
};
Point3d getZeroPoint()
{
// 我们可以创建一个变量并返回该变量(我们稍后会改进这一点)
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;
}
这将打印:
The point is zero
在函数内部定义的结构体通常按值返回,以避免返回悬挂引用。
在上面的getZeroPoint()
函数中,我们创建了一个新的命名对象(temp
),只是为了返回它:
Point3d getZeroPoint()
{
// 我们可以创建一个变量并返回该变量(我们稍后会改进这一点)
Point3d temp { 0.0, 0.0, 0.0 };
return temp;
}
对象的名称(temp
)在这里并没有提供任何文档价值。
我们可以通过返回一个临时(无名/匿名)对象来使函数稍微更好一些:
Point3d getZeroPoint()
{
return Point3d { 0.0, 0.0, 0.0 }; // 返回一个无名的Point3d
}
在这种情况下,一个临时的Point3d
对象被构造,复制回调用者,然后在表达式的末尾被销毁。注意这有多简洁(一行对两行,且无需理解temp
是否被使用超过一次)。
相关内容
我们在第14.13课——临时类对象中更详细地讨论了匿名对象。
推导返回类型
在函数有一个显式的返回类型(例如Point3d
)的情况下,我们甚至可以在返回语句中省略类型:
Point3d getZeroPoint()
{
// 我们已经在函数声明中指定了类型
// 因此这里不需要再指定
return { 0.0, 0.0, 0.0 }; // 返回一个无名的Point3d
}
这被认为是一种隐式转换。
另外请注意,由于在这种情况下我们返回的都是零值,我们可以使用空的大括号来值初始化一个Point3d