操作符重载(友元函数和成员函数)
在C++中,运算符重载是一种强大的功能,可以为自定义类型创建与内置类型相似的行为。在运算符重载中,我们可以使用成员函数或友元函数来实现操作符的重载。然而,成员函数对操作符的使用有一些限制,而友元函数则可以解决这些限制。
成员函数要求左操作数必须是该类型的对象。这意味着我们只能在类的成员函数中重载运算符,并使用该类的对象作为左操作数。这对于一些情况可能会有限制。
然而,友元函数可以允许对左操作数进行隐式转换。这意味着我们可以在友元函数中重载运算符,并允许使用其他类型的对象作为左操作数。这样就能更灵活地使用运算符重载。
举个例子,假设我们创建了一个BigInt类,并为其创建了一个成员函数operator+,以接受一个BigInt类型的右操作数。现在假设BigInt类有一个构造函数,可以接受一个普通的int类型参数,并且该构造函数不是显式的(explicit关键字)。这意味着C++可以隐式地将int转换为BigInt。
当我们有了这些条件后,我们可以这样做:
BigInt foo(5);
BigInt bar;
bar = foo + 5;
但是,我们不能这样做:
BigInt foo(5);
BigInt bar;
bar = 5 + foo;
然而,如果我们使用友元函数而不是成员函数,那么两种情况下都可以正常工作。
因此,友元函数可以允许我们在运算符重载中更灵活地处理操作数类型,解决了成员函数的限制。通过使用友元函数,我们可以实现更多的运算符重载的用途和灵活性。
运算符重载的友元函数与成员函数之间的区别在于,友元函数实际上在全局范围内,因此您不需要是类的实例就可以访问它。
运算符重载是一种特殊的函数重载,它允许我们为自定义类型定义运算符的行为。在C++中,我们可以使用友元函数或成员函数来实现运算符重载。
友元函数是一个独立于类的函数,但它可以访问类的私有成员。友元函数可以在类的内部或外部定义,但必须在类的声明中进行声明。友元函数可以通过在函数声明前加上关键字"friend"来声明。
成员函数是属于类的函数,它可以访问类的私有成员。成员函数可以在类的内部或外部定义。在类的声明中,我们可以使用关键字"operator"来定义成员函数,表示它是一个运算符重载函数。
友元函数和成员函数之间的主要区别在于作用域。友元函数在全局范围内定义,因此不需要类的实例就可以访问它。而成员函数是类的一部分,只能通过类的实例来调用。
解决这个问题的方法是根据具体的需求来选择使用友元函数还是成员函数。如果运算符重载需要访问类的私有成员,那么友元函数是一个不错的选择。如果运算符重载只需要访问类的公有成员,那么成员函数就足够了。
下面是一个示例代码,演示了使用友元函数和成员函数来实现运算符重载:
#includeclass MyClass { private: int value; public: MyClass(int v) : value(v) {} // 友元函数的运算符重载 friend MyClass operator+(const MyClass& obj1, const MyClass& obj2) { return MyClass(obj1.value + obj2.value); } // 成员函数的运算符重载 MyClass operator-(const MyClass& obj) { return MyClass(value - obj.value); } void display() { std::cout << "Value: " << value << std::endl; } }; int main() { MyClass obj1(5); MyClass obj2(3); MyClass obj3 = obj1 + obj2; obj3.display(); // 输出:Value: 8 MyClass obj4 = obj1 - obj2; obj4.display(); // 输出:Value: 2 return 0; }
在上面的示例中,我们定义了一个名为`MyClass`的类,其中包含一个私有成员`value`。我们使用了友元函数和成员函数来实现运算符重载。
友元函数`operator+`接受两个`MyClass`对象作为参数,并返回它们的和。我们可以直接在`main`函数中通过`obj1 + obj2`来调用这个友元函数。
成员函数`operator-`接受一个`MyClass`对象作为参数,并返回差值。我们通过`obj1 - obj2`来调用这个成员函数。
通过使用友元函数和成员函数,我们可以根据具体的需求实现运算符重载,并且能够方便地访问类的私有成员。
友元函数在类内部具有对该类的访问权限,但它实际上并不在类内部,并且其他所有人都可以访问它。对于不是类成员的运算符重载(也称为自由函数),参数和操作数相同。对于作为类成员的运算符重载,第一个操作数是“隐式参数”,它变为“this”。
隐式参数与自由函数的第一个参数在几个方面不同:
- 其类型是类的引用,而自由函数可以为其第一个参数声明任何类型。
- 它不参与隐式类型转换。(它不会成为由转换构造函数初始化的临时对象。)
- 它参与虚拟重写解析。(虚拟重载将根据第一个操作数的动态类型选择,而自由函数没有额外的代码无法实现这一点。)
对于一元、二元或n元运算符(例如operator()
),情况是相同的。
对于改变第一个操作数的运算符(例如+=
、=
、前缀++
),应该将其实现为成员函数,并且应该只实现所有重载的实质部分。后缀++
是一个次要的成员;它被实现为Obj ret = *this; ++ this; return ret;
。注意,有时这也适用于复制构造函数,它可能包含*this = initializer
。
只有可交换的运算符(例如/
)应该是自由函数;所有其他运算符(例如一元运算符)应该是成员函数。可交换的运算符本质上会复制对象;它们被实现为Obj ret = lhs; ret @= rhs; return ret;
,其中@
是可交换的运算符,lhs
和rhs
分别是左操作数和右操作数。
避免使用友元。友元污染设计语义。重载的推论是:如果遵循上述规则,那么友元是无害的。使用友元定义重载的模板让它们放在class {
大括号内。
请注意,某些运算符不能是自由函数:=、->、[]和(),因为标准明确规定了这一点,位于第13.5节。我认为这就是全部…我原以为一元&
和*
也是如此,但显然我错了。不过,它们应该总是作为成员重载,只有在经过深思熟虑之后才能使用。
为什么一元运算符不能是友元?
因为没有必要。友元运算符是允许将类类型放在二元运算符的右边的。由于一元运算符没有右边,所以不需要将它们定义为友元。
总体而言,您不需要放宽访问权限。如果您将operator+=
定义为成员函数(虽然不是必需的,但似乎很合适,因为它是一种赋值操作),那么您可以定义X operator+(X lhs, X const & rhs) { return lhs += rhs; }
而无需放宽访问权限。另一方面,在类大括号内定义自由函数(必须是友元)的优势(我从未发现它有什么劣势)是它们不会参与该类型(也就是说,编译器不会考虑对两个参数执行隐式转换来调用该函数)。
我之前曾经阅读过这方面的内容,但现在找不到引用了。我简要地浏览了标准,最接近的是11.4/5节在类内定义的友元函数在定义它的类的(词法)范围内,但我并不确定11.4/5的全部含义,因为它还说函数属于封闭的命名空间…我有点不确定,但它在实践中是有效的…
今天我在处理两个类的一组相当复杂的运算符,这些运算符接受来自5个不同类型的参数(也是类)。在某个时候,我使用了友元,但它们没有正常工作。也就是说,编译器无法解析这些运算符。不使用友元却完全正常。所以,能够看到可见性上的明显差异。