《C++PrimerPlus 6th Edition》第11章 使用类 要点记录

“轻松使用这种语言。不要觉得必须使用所有的特性,不要在第一次学习时就试图使用所有的特性。”

—— Bjarne Stroustrup

本章内容

  1. 运算符重载
  2. 友元函数
  3. 重载<<运算符,以便于输出
  4. 状态成员
  5. 使用rand()生成随机值
  6. 类的自动转换和强制类型转换
  7. 类转换函数
11.1 运算符重载
11.2 计算时间:一个运算符重载示例
  1. Time类的成员方法:Time operator+(const Time&) const;
    两种调用方式:

    • total = coding.operator+(fixing);
    • total = coding + fixing;
  2. 重载限制:

    • 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符;如,不能将减法运算符重载为计算两个double的和,而不是它们的差。
    • 使用运算符时不能违反运算符原来的句法规则,如不能将求余运算符重载为使用一个操作数;同样,不能修改运算符的优先级。
    • 不能创建新运算符。例如,不能定义operator**()函数来表示求幂。
    • 不能重载以下的运算符:
      • sizeof:sizeof运算符
      • . :成员运算符
      • .*:成员指针运算符
      • :::作用域解析运算符
      • ?::条件运算符
      • typeid:一个RTTI运算符
      • const_cast | dynamic_cast | reinterpret_cast | static_cast:强制类型转换运算符
    1. 一些只能通过成员函数进行重载的运算符:
      • =:赋值运算符
      • ():函数调用运算符
      • []:下标运算符
      • ->:通过指针访问类成员
  3. 同时列出可以重载的运算符:

+ - * \ % ^
& | ~= ! = <
> += -= *= /= %=
^= &= |= << >> >>=
<<= == != <= >= &&
|| ++ - - , ->* ->
() [] new delete new[] delete[]

注:除了以上正式限制外,还应在重载运算符时遵循一些明智的限制。例如,不要将*运算符重载成交换两个对象的数据成员。最好定义一个其名称具有说明性的类方法,如Swap();

11.3 友元
  1. 友元的三种形式:①友元类;②友元函数;③友元成员函数。通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

  2. 为何需要友元:示例: A ∗ 2.75 A*2.75 A2.75 2.75 ∗ A 2.75 * A 2.75A,后者如何才能合法?下面有两种方案,但都存在局限性
    方式1:告知每个人只能按照 A ∗ 2.75 A*2.75 A2.75的格式编写,但这是服务器友好-客户警惕的(server-friendly, client-beware)解决方案,与OOP无关。
    方式2:非成员函数Time operator*(double m, const Time& t);不能直接访问类的私有数据

  3. 创建友元的步骤:

    • 将其原型放在类声明中,并在原型声明前加上关键字friend
      friend Time operator*(double m, const Time& t);
      该原型意味着下面两点:
      • 虽然它是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符调用;
      • 虽然它不是成员函数,但它与成员函数的访问权限相同。
    • 编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在函数定义时使用关键字friend

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

  4. 友元并不有悖于OOP。看似友元违反了OOP数据隐藏的原则,这种观点太片面,应当把友元函数看作类的扩展接口的组成部分。类方法和友元函数只是表达类接口的两种不同机制。

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

    • ofstream对象:coutcerr(将输出发送到标准输出流——默认为显示器)
    • cout<<a<<b<<...; 从左向右读取输出语句,即(((cout<<a)<<b)<<...;),对应的调用:ostream& operator<<(ostream& os, data);

    这个通过习题体会会更深。

11.4 重载运算符:作为成员函数还是非成员函数
  1. 定义运算符时,必须选择其中一种格式,而不能同时选择这两种格式。因为这两种格式都与同一个表达式匹配,同时定义这两种格式将被视为二义性错误,导致编译错误。
  2. 选择成员函数还是非成员函数?对于某些运算符来说,成员函数是唯一合法选择;其他情况下,两者差不多,需根据具体情况作选择。
★11.5 再谈重载:一个矢量类

这个例子看懂了,这章就没问题了。
Vector.h

#pragma once
#include <iostream>
namespace VECTOR {
    
    
	class Vector
	{
    
    
	public:
		enum Mode{
    
    RECT, POL};
	private:
		double x;
		double y;
		double mag;
		double ang;
		Mode mode;
		//methods
		void set_mag();
		void set_ang();
		void set_x();
		void set_y();
	public:
		Vector();
		Vector(double n1, double n2, Mode form = RECT);
		void reset(double n1, double n2, Mode form = RECT);
		~Vector();
		double xval() const {
    
     return x; }
		double yval() const {
    
     return y; }
		double magval() const {
    
     return mag; }
		double angval() const {
    
     return ang; }
		void polar_mode();
		void rect_mode();
		//operator overloading
		Vector operator+(const Vector& b) const;
		Vector operator-(const Vector& b) const;
		Vector operator-() const;
		Vector operator*(double n) const;
		friend Vector operator*(double n, const Vector& a);
		friend std::ostream& operator<<(std::ostream& os, const Vector& v);
	};
}

Vector.cpp

#include "Vector.h"
#include <cmath>
#include<iostream>
using std::sqrt;
using std::sin;
using std::cos;
using std::atan;
using std::atan2;
using std::cout;

namespace VECTOR {
    
    
	const double Rad_to_Deg = 45.0 / atan(1.0); //degrees in one radian
	
	void Vector::set_mag() {
    
    
		mag = sqrt(x*x + y*y);
	}
	void Vector::set_ang() {
    
    
		if (x == 0.0 && y == 0.0)
			ang = 0.0;
		else
			ang = atan2(y, x); // x==0 ?
	}

	void Vector::set_x() {
    
    
		x = mag * cos(ang);
	}
	void Vector::set_y() {
    
    
		y = mag * sin(ang);
	}

	//public
	Vector::Vector() {
    
    
		x = y = mag = ang = 0.0;
		mode = RECT;
	}

	Vector::Vector(double n1, double n2, Mode form) {
    
    
		mode = form;
		if (mode == RECT) {
    
    
			x = n1;
			y = n2;
			set_mag();
			set_ang();
		}
		else if (mode == POL) {
    
    
			mag = n1;
			ang = n2 / Rad_to_Deg;
			set_x();
			//std::cout << x << std::endl;
			set_y();
			//std::cout << y << std::endl;
		}
		else {
    
    
			cout << "Incorrect 3rd argument to Vector() --";
			cout << "Vector set to 0\n";
			x = y = mag = ang = 0.0;
			mode = RECT;
		}
	}

	void Vector::reset(double n1, double n2, Mode form) {
    
    
		mode = form;
		if (form == RECT) {
    
    
			x = n1;
			y = n2;
			set_mag();
			set_ang();
		}
		else if (form == POL) {
    
    
			mag = n1;
			ang = n2 / Rad_to_Deg;
			set_x();
			set_y();
		}
		else {
    
    
			cout << "Incorrect 3rd argument to Vector() --";
			cout << "Vector set to 0\n";
			x = y = mag = ang = 0.0;
			form = RECT;
		}
	}

	Vector::~Vector() {
    
    

	}
	void Vector::polar_mode() {
    
    
		mode = POL;
	}
	void Vector::rect_mode() {
    
    
		mode = RECT;
	}
	//operator overloading
	Vector Vector::operator+(const Vector& b)const {
    
    
		return Vector(x + b.x, y + b.y);
	}
	Vector Vector::operator-(const Vector& b)const {
    
    
		return Vector(x - b.x, y - b.y);
	}
	Vector Vector::operator-()const {
    
    
		return Vector(-x, -y);
	}
	Vector Vector::operator*(double n)const {
    
    
		return Vector(x*n, y*n);
	}

	//friend
	Vector operator*(double n, const Vector& a) {
    
    
		return a*n;
	}
	std::ostream& operator<<(std::ostream& os, const Vector& v) {
    
    
		//std::cout << v.mag << ", " << v.ang << std::endl;
		if (v.mode == Vector::RECT)
			os << "(x, y) = (" << v.x << ", " << v.y << ")";
		else if (v.mode == Vector::POL)
			os << "(m, a) = (" << v.mag << ", " << v.ang*Rad_to_Deg << ")";
		else
			os << "Vector object mode is invalid";
		return os;
	}
}
  1. 将接口与实现分离是OOP的目标之一,这样允许对实现进行调整,而无需修改使用这个类的程序中的代码。

  2. 是否把一些计算结果存储起来需要考虑时空权衡,一般原则:频繁用的就存起来,否则额外用空间进行存储。

  3. 矢量类的应用——随机漫步

    #include "Vector.h"
    #include <iostream>
    #include <cstdlib>
    #include <ctime>
    
    int main() {
          
          
    	using namespace std;
    	using VECTOR::Vector;
    	srand(time(0));
    	double direction;
    	Vector step(5, 60, Vector::POL);
    	Vector result(0.0, 0.0);
    	
    	unsigned long steps = 0;
    	double target;
    	double dstep;
    	cout << "Enter target distance (q to quit): ";
    	while (cin >> target) {
          
          
    		cout << "Enter step length: ";
    		if (!(cin>>dstep)) {
          
          
    			break;
    		}
    
    		while (result.magval() < target) {
          
          
    			direction = rand() % 360;
    			step.reset(dstep, direction, Vector::POL);
    			result = result + step;
    			steps++;
    		}
    		cout << "After " << steps << " steps, the subject "
    			"has the following location:\n";
    		cout << result << endl;
    		result.polar_mode();
    		cout << " or\n" << result << endl;
    		cout << "Average outward distance per step = "
    			<< result.magval() / steps << endl;
    		steps = 0;
    		result.reset(0.0, 0.0);
    		cout << "Enter target distance (q to quit): ";
    	}
    	cout << "Bye!\n";
    	cin.clear();
    	while (cin.get() != '\n')
    		continue;
    	return 0;
    }
    
11.6 类的自动转换和强制类型转换
Stonewt::Stonewt(double lbs){
    
    
	//...
}
Stonewt myCat;
myCat = 19.6;
  1. 只有接受一个参数的构造函数才能作为转换函数;
  2. 如果接受两个参数,但给第二个参数提供默认值,它便可用于转换int
  3. 关键字explicit可以关闭自动类型转换的特性,但仍允许显式转换。例如:
    explicit Stonewt(double lbs);
    Stonewt myCat;
    myCat = 19.6; // not valid if Stonewt(double) is declared as explicit
    myCat = Stonewt(19.6); // ok, an explicit conversion
    myCat = (Stonewt)19.6; // ok, old form for explicit typecast
    
  4. 编译器调用Stonewt(double)函数的时机:如果在声明中使用了关键字explicit,则该构造函数将只用于显示强制类型转换,否则还可以用于下面的隐式转换:
    • Stonewt对象初始化为double值时;
    • double值赋给Stonewt对象时;
    • double值传递给接受Stonewt参数的函数时;
    • 返回值被声明为Stonewt的函数试图返回double值时;
    • 在上述任意一种情况下,使用可转换为double类型的内置类型时,如int。但进行这种二步转换的条件是——转换不存在二义性。比如,从int转换为Stonewt,如果这个类还定义了构造函数Stonewt(long),则编译器会拒绝这些语句,因为int可被转换为longdouble,因此调用存在二义性。
  5. C++运算符函数——转换函数:前面的构造函数只能实现从某种类型的函数到类类型的转换。要进行相反的转换,必须使用C++的转换函数,格式为operator typeName();其中typeName指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数
    比如从Stonewtint的转换函数如下:
    Stonewt::operator int() const{
          
          
    	return int(this->pounds + 0.5); // round-off
    };
    
  6. 注意转换函数的二义性,有时候需要显式类型转换。
  7. 转换函数可能引发的问题:在用户不希望进行转换时,转换函数也可能进行转换。例如,假设您在睡眠不足时编写了下面的代码:
    int ar[20];
    ...
    Stonewt temp(14, 4);
    ...
    int Temp = 1;
    ...
    cout<<ar[temp]<<"!\n"; //used temp instead of Temp
    
    由于类中定义了operator int();,输出语句中的temp将被转换为int类型数,并用作数组索引。因此,原则上说,最好用显式转换,避免用隐式转换
  8. C++98中,关键字explicit不能用于转换函数,但C++11消除了这种限制。因此,在C++11中,可将转换运算符声明为显式的:
    class Stonewt{
          
          
    ...
    //conversion functions
    explicit operator int() const;
    explicit operator double() const;
    };
    
  9. 转换函数和友元函数:
    1. 过多的转换会导致二义性,如既定义了doubleStonewt类的转换函数,又定义了Stonewt类转double的转换函数,这时候会造成混乱。
    2. 将加法定义为友元可以让程序更容易适应自动类型转换。原因在于,两个操作数都成为函数参数,因此与函数原型匹配。
    3. 如果程序经常需要将double值与Stonewt对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了保险,可以使用显式转换。
11.7 总结
  1. 如果要使其第一个操作数不是类对象,则必须使用友元函数;
  2. 如果类包含这样的方法,它返回需要显示的数据成员的值,则可以使用这些方法,无需在operator<<()中直接访问这些成员。这种情况下,函数不必(也不应该)是友元;
  3. 任何接受唯一一个参数的构造函数都可被用作转换函数。但是,如果该构造函数的声明前加上了关键字explicit,则该构造函数将只能用于显式转换;

习题

见我的github(暂未上传)

欢迎各位大佬们于评论区进行批评指正~


上一篇文章:《C++PrimerPlus 6th Edition》第10章 对象和类 要点记录

下一篇文章:《C++PrimerPlus 6th Edition》第12章 类和动态内存分配 要点记录

猜你喜欢

转载自blog.csdn.net/weixin_42430021/article/details/109162566