C++PrimerPlus 学习笔记 | 第十一章 使用类 | 2.友元 & 3.运算符重载使用成员还是非成员函数 & 4. 矢量类

本文详细阐述了C++中的友元函数如何帮助解决类内运算符重载问题,如时间类的*运算符重载。通过友元函数,非成员函数得以访问私有数据,同时保持了面向对象的封装性。讨论了友元函数在OOP中的合理应用,以及在类定义和重载运算符中的使用策略。
摘要由CSDN通过智能技术生成

友元

C++控制对类私有部分发的访问,但是在有些时候这些控制过于严格,以至于不适用于特定的编程问题,在这种情况下C++提供类另外一种形式的访问权限友元

  1. 友元函数
  2. 友元类
  3. 友元成员函数

通过让函数成为友元可以让他拥有与类成员相同的访问权限,下面主要介绍友元函数。

需要注意的是友元函数仍是普通函数,不是成员函数所以没有this指针!!!。

为什么需要友元函数,我们不妨来思考下述,问题

存在一个时间类,需要重载*运算符,让其内部的时间可以按照给定的系数q,倍乘

如果重载了乘法运算符,这个主要差异在两者类型不同一个是double一个Time

Time operator*(double);

如果按照如下形式调用

a = b * 10;
会被转换为如下;
A = b.operator*(10); // 这样没有问题
但是如果形式如下;
a = 10 * b;
// 从乘法交换律来说这两者应该完成相同,但是在重载运算符中,左边的操作数是调用对象,但是显然10无法完成完成调用,这怎么办呢?

解决这个问题的一种方法是告诉所有人只能按照 B * 2.75 的方式编写,不能写成 2.75 * B,这是一种服务器友好,客户警惕的解决方法

然而第二方法就是非成员函数(大多数运算符多可以通过成员或者非成员函数重载)由于非成员函数不是由对象调用的,所以他使用的所有值都是现实函数,所以编译器能将上述两个表达式完美匹配

但是出现了第二个问题,非成员无法访问类的私有数据,至少常规非成员函数不能访问。然而有一类特殊的非成员函数可以访问类的私有成员,被称为友元函数。

创建友元

创建友元函数的第一步就是将函数在类声明中声明,在函数原型前面加上friend

friend Time operator*(double m,const Time & t);

这意味两点

  1. 虽然函数是在类声明中声明的但是他不是成员函数也自然不具备this指针
  2. 虽然函数不是成员函数,但是他与成员函数访问权限相同,即可以可以通过该类对象访问私有成员

第二部编译函数定义,首先由于不是成员函数,不需要作用域修饰

Time operator*(double m,const Time & t){
    // do something
}

总之类的友元函数是非成员函数,其访问权限与成员函数相同

如果要为类重载运算符,并非类的项作为第一个操作数,可以使用友元函数来自动反转和适配操作数


友元是否有悖于OOP?

结论是当然是不违背,可能乍一看,允许非成员函数访问类私有成员违反了OOP数据隐藏的原理,但是这个观点太片面了,相反应该讲友元函数看作类的拓展接口的组成接口的组成部分,而且只有类声明才可以决定那个友元函数,因此类声明仍然可以控制那些函数可以访问私有数据,总之类方法和友元只是表达类接口的两种不同机制


常用的友元: 重载 << 运算符

一个类很有用的特性就是重载 << 运算符使之与cout来一起显示内容

要想让类使用cout运算符必须要使用友元函数,否则则只能类似一下形式使用 <<

time << cout; // time.operator<<(cout) 形式很奇怪
cout << time; // cout.operator<<(time) 我们需要修改iostream 这是一个很愚蠢的决定

但是通过友元函数就可以很舒适使用

void operator<<(ostream & os,const Time & t){
    os << t.hours << t.minutes << endl;
}
// 调用
cout << time ;// operator<<(cout,t) 完美!

前面的友元函数将会导致下面的语句出错

cout << time1 << time2;

其实原理很简单,根据从左往右读取输入的顺序 C++首先处理

(cout << time1) << time2

但是根据友元函数,返回void就变成如下

(void) << time2

像void输出显然是荒唐的,所以我们只需要让前一个输出项目返回一个ostream对象就可以解决这个问题

ostream & operator<<(ostream & os,const Time & t){
    os << t.hours << t.minutes << endl;
    return os;
}

那么流程就会成如下

cout << time1 << time2;
(cout << time1) << time2;
cin << time2;
cin; // 结束

由于类的继承属性,让ostream属性不仅可以用于标准输入输出也可以用于文件输入输出等等等等。。

我们可以得出一般化的重载<<模版

ostream & operator<<(ostream & os,const T & t){
    // do something
    return os;
}

记住友元函数只在声明时使用friend关键字,除非该函数在声明处定义,否则在定义处不能使用friend关键字

重载运算符作为成员函数还是非成员函数

对于很多运算符来说可以成员函数或者非成员函数来完成运算符重载,一般来说非成员函数时友元函数,这样他才能直接使用类私有数据。

一般来说如果两个操作数地位完全相同,但是类型不同的情况下,可以选择非成员函数实现,来处理操作数顺序的问题,如果处理的两个操作数的地位不等,不能随意交换,一般可以使用成员函数。

对于操作数数量而言,成员函数重载运算符少一个操作数,一个由this指针隐式传递,而非成员函数操作数数量完全与操作符所需操作数一致。

但是在定义运算符的时候必须选择其中一种格式,而不能选择两种,因为两种格式如果同时匹配,会导致二义性错误

矢量类

包含大小和方向的量,一般由两种表达方式,(本类只描述二维矢量)

  1. 可以用大小和长度描述矢量
  2. 可以用x,y分量描述

有些时候第一种更方便,有些时候第二种更方便,我们可以通过使用类,提供抽象接口的方式来想用户提供任何一种形式的访问

// Vector.h

#ifndef VECTOR_H
#define VECTOR_H
#include <ostream>
namespace VECTOR{
    typedef double Number_t;
    class Vector
    {
        Number_t x{};
        Number_t y{};
    public:
        enum class Vector_Mode {POL,RECT};
        Vector(double v1,double v2,Vector_Mode mode = Vector_Mode::RECT);
        void reset(double v1, double v2, Vector_Mode mode = Vector_Mode::RECT);
        ~Vector();
        double get_x() const;
        double get_y() const;
        double get_ang() const;
        double getmag() const;

        Vector operator+(const Vector &) const;
        Vector operator-(const Vector &) const;
        Vector operator-() const;
        Vector operator*(double ) const;

        friend Vector operator*(double n,const Vector & a);
        friend std::ostream & operator<<(std::ostream &,const Vector &);
    };
}

#endif //VECTOR_H
#include "Vector.h"
#include <cmath>
#include <iostream>

VECTOR::Vector VECTOR::operator*(double n, const VECTOR::Vector & a)
{
    return a * n;
}

std::ostream & VECTOR::operator<<(std::ostream & os, const VECTOR::Vector & a)
{
    os << "x: " << a.x << " y: " << a.y << std::endl;
    return os;
}

VECTOR::Vector::Vector(double v1, double v2, VECTOR::Vector::Vector_Mode mode)
{
    reset(v1, v2, mode);
}

void VECTOR::Vector::reset(double v1, double v2, VECTOR::Vector::Vector_Mode mode)
{
    if (mode == Vector_Mode::RECT) {
        x = v1;
        y = v2;
    }
    else if (mode == Vector_Mode::POL) {
        constexpr double RAD_TO_DEG = 57.29577951;
        v2 /= RAD_TO_DEG;
        x = v1 * cos(v2);
        y = v1 * sin(v2);
    }
    else {
        std::cout << "Invalid vector mode,mode is setting to RECT,x,y is initalized to 0,0" << std::endl;
        x = y = 0;
    }
}

VECTOR::Vector::~Vector()
= default;

double VECTOR::Vector::get_x() const
{
    return x;
}

double VECTOR::Vector::get_y() const
{
    return y;
}

double VECTOR::Vector::get_ang() const
{
    if (x == 0 && y == 0) {
        return 0.0;
    }
    return atan2(y, x);
}

double VECTOR::Vector::getmag() const
{
    return sqrt(pow(x, 2) + pow(y, 2));
}

VECTOR::Vector VECTOR::Vector::operator+(const VECTOR::Vector & a) const
{
    return {x + a.x, y + a.y};
}

VECTOR::Vector VECTOR::Vector::operator-(const VECTOR::Vector & a) const
{
    return (*this + (-a));
}

VECTOR::Vector VECTOR::Vector::operator-() const
{
    return {-x, -y};
}

VECTOR::Vector VECTOR::Vector::operator*(double n) const
{
    return Vector{x * n, y * n};
}

这个类的设计决策遵守了OOP的传统,即将类接口放在其本质上,也就是矢量的本质上,而隐藏细节,这样当用户使用该类的时候,无需关心类内部的实现结构,只需要考虑其如何使用,从而忽略其底部如何实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值