《C++PrimerPlus 6th Edition》第11章 使用类 要点记录
“轻松使用这种语言。不要觉得必须使用所有的特性,不要在第一次学习时就试图使用所有的特性。”
—— Bjarne Stroustrup
本章内容
- 运算符重载
- 友元函数
- 重载<<运算符,以便于输出
- 状态成员
- 使用rand()生成随机值
- 类的自动转换和强制类型转换
- 类转换函数
11.1 运算符重载
11.2 计算时间:一个运算符重载示例
-
Time类的成员方法:Time operator+(const Time&) const;
两种调用方式:total = coding.operator+(fixing);
total = coding + fixing;
-
重载限制:
- 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符;如,不能将减法运算符重载为计算两个
double
的和,而不是它们的差。 - 使用运算符时不能违反运算符原来的句法规则,如不能将求余运算符重载为使用一个操作数;同样,不能修改运算符的优先级。
- 不能创建新运算符。例如,不能定义
operator**()
函数来表示求幂。 - 不能重载以下的运算符:
- sizeof:sizeof运算符
- . :成员运算符
- .*:成员指针运算符
- :::作用域解析运算符
- ?::条件运算符
- typeid:一个RTTI运算符
- const_cast | dynamic_cast | reinterpret_cast | static_cast:强制类型转换运算符
- 一些只能通过成员函数进行重载的运算符:
- =:赋值运算符
- ():函数调用运算符
- []:下标运算符
- ->:通过指针访问类成员
- 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符;如,不能将减法运算符重载为计算两个
-
同时列出可以重载的运算符:
+ | - | * | \ | % | ^ |
---|---|---|---|---|---|
& | | | ~= | ! | = | < |
> | += | -= | *= | /= | %= |
^= | &= | |= | << | >> | >>= |
<<= | == | != | <= | >= | && |
|| | ++ | - - | , | ->* | -> |
() | [] | new | delete | new[] | delete[] |
注:除了以上正式限制外,还应在重载运算符时遵循一些明智的限制。例如,不要将*运算符重载成交换两个对象的数据成员。最好定义一个其名称具有说明性的类方法,如Swap();
11.3 友元
-
友元的三种形式:①友元类;②友元函数;③友元成员函数。通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
-
为何需要友元:示例: A ∗ 2.75 A*2.75 A∗2.75和 2.75 ∗ A 2.75 * A 2.75∗A,后者如何才能合法?下面有两种方案,但都存在局限性:
方式1:告知每个人只能按照 A ∗ 2.75 A*2.75 A∗2.75的格式编写,但这是服务器友好-客户警惕的(server-friendly, client-beware)解决方案,与OOP无关。
方式2:非成员函数Time operator*(double m, const Time& t);
不能直接访问类的私有数据。 -
创建友元的步骤:
- 将其原型放在类声明中,并在原型声明前加上关键字
friend
:
friend Time operator*(double m, const Time& t);
该原型意味着下面两点:- 虽然它是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符调用;
- 虽然它不是成员函数,但它与成员函数的访问权限相同。
- 编写函数定义。因为它不是成员函数,所以不要使用
Time::
限定符。另外,不要在函数定义时使用关键字friend
总之,类的友元函数是非成员函数,其访问权限与成员函数相同。
- 将其原型放在类声明中,并在原型声明前加上关键字
-
友元并不有悖于OOP。看似友元违反了OOP数据隐藏的原则,这种观点太片面,应当把友元函数看作类的扩展接口的组成部分。类方法和友元函数只是表达类接口的两种不同机制。
-
常用的友元:重载
<<
运算符ofstream
对象:cout
,cerr
(将输出发送到标准输出流——默认为显示器)cout<<a<<b<<...;
从左向右读取输出语句,即(((cout<<a)<<b)<<...;
),对应的调用:ostream& operator<<(ostream& os, data);
这个通过习题体会会更深。
11.4 重载运算符:作为成员函数还是非成员函数
- 定义运算符时,必须选择其中一种格式,而不能同时选择这两种格式。因为这两种格式都与同一个表达式匹配,同时定义这两种格式将被视为二义性错误,导致编译错误。
- 选择成员函数还是非成员函数?对于某些运算符来说,成员函数是唯一合法选择;其他情况下,两者差不多,需根据具体情况作选择。
★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;
}
}
-
将接口与实现分离是OOP的目标之一,这样允许对实现进行调整,而无需修改使用这个类的程序中的代码。
-
是否把一些计算结果存储起来需要考虑时空权衡,一般原则:频繁用的就存起来,否则额外用空间进行存储。
-
矢量类的应用——随机漫步
#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;
- 只有接受一个参数的构造函数才能作为转换函数;
- 如果接受两个参数,但给第二个参数提供默认值,它便可用于转换
int
; - 关键字
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
- 编译器调用
Stonewt(double)
函数的时机:如果在声明中使用了关键字explicit
,则该构造函数将只用于显示强制类型转换,否则还可以用于下面的隐式转换:- 将
Stonewt
对象初始化为double
值时; - 将
double
值赋给Stonewt
对象时; - 将
double
值传递给接受Stonewt
参数的函数时; - 返回值被声明为
Stonewt
的函数试图返回double
值时; - 在上述任意一种情况下,使用可转换为
double
类型的内置类型时,如int
。但进行这种二步转换的条件是——转换不存在二义性。比如,从int
转换为Stonewt
,如果这个类还定义了构造函数Stonewt(long)
,则编译器会拒绝这些语句,因为int
可被转换为long
或double
,因此调用存在二义性。
- 将
- C++运算符函数——转换函数:前面的构造函数只能实现从某种类型的函数到类类型的转换。要进行相反的转换,必须使用C++的转换函数,格式为
operator typeName();
其中typeName
指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。
比如从Stonewt
到int
的转换函数如下:Stonewt::operator int() const{ return int(this->pounds + 0.5); // round-off };
- 注意转换函数的二义性,有时候需要显式类型转换。
- 转换函数可能引发的问题:在用户不希望进行转换时,转换函数也可能进行转换。例如,假设您在睡眠不足时编写了下面的代码:
由于类中定义了int ar[20]; ... Stonewt temp(14, 4); ... int Temp = 1; ... cout<<ar[temp]<<"!\n"; //used temp instead of Temp
operator int();
,输出语句中的temp
将被转换为int
类型数,并用作数组索引。因此,原则上说,最好用显式转换,避免用隐式转换。 C++98
中,关键字explicit
不能用于转换函数,但C++11
消除了这种限制。因此,在C++11
中,可将转换运算符声明为显式的:class Stonewt{ ... //conversion functions explicit operator int() const; explicit operator double() const; };
- 转换函数和友元函数:
- 过多的转换会导致二义性,如既定义了
double
转Stonewt
类的转换函数,又定义了Stonewt
类转double
的转换函数,这时候会造成混乱。 - 将加法定义为友元可以让程序更容易适应自动类型转换。原因在于,两个操作数都成为函数参数,因此与函数原型匹配。
- 如果程序经常需要将
double
值与Stonewt
对象相加,则重载加法更合适;如果程序只是偶尔使用这种加法,则依赖于自动转换更简单,但为了保险,可以使用显式转换。
- 过多的转换会导致二义性,如既定义了
11.7 总结
- 如果要使其第一个操作数不是类对象,则必须使用友元函数;
- 如果类包含这样的方法,它返回需要显示的数据成员的值,则可以使用这些方法,无需在
operator<<()
中直接访问这些成员。这种情况下,函数不必(也不应该)是友元; - 任何接受唯一一个参数的构造函数都可被用作转换函数。但是,如果该构造函数的声明前加上了关键字
explicit
,则该构造函数将只能用于显式转换;
习题
见我的github(暂未上传)。
欢迎各位大佬们于评论区进行批评指正~