导图社区 使用类
提供一些案例代码帮助理解,对书上概括到位,让你对C语言类的使用有一个清晰的认识。
编辑于2021-01-02 19:36:05第11章 使用类
11.1 运算符重载
运算符函数的格式如下: operatorop(argument-list) op必须是有效的C++运算符,不能虚构出一个新的符号。
例如: operator+()重载+运算符;operator*重载*运算符
假设有一个S类,并为它定义了一个operator+()成员函数。设a,b和c都是S类,便可以写这样的等式: c = a + b; 编译器发现,操作数是S类对象,因此使用相应的运算符函数替换上述运算符: c = a.operator+(b);
11.2 计算时间:一个运算符重载示例
11.2.1 添加加法运算符
class Time { private: ... public: ... Time operator+(const Time& t) const; } Time Time::operator+(const Time& t) const { Time sum; ... return sum; }
可以直接调用operator+()方法: c = a.operator+(b); 也可以使用运算符表示该方法: c = a + b;
当使用连加时: d = a + b + c; 编译器将逐步转换为如下代码: d = a.operator+(b+c); d = a.operator+(b.operator+(c)); 因此可以实现连加
11.2.2 重载限制
1. 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符
2. 使用运算符时不能违反运算符原来的句法规则
3. 不能修改运算符的优先级
4. 不能创建新的运算符
5. 不能重载这些运算符
sizeof
sizeof运算符
:
成员运算符
.*
成员指针运算符
::
作用域解析运算符
?:
条件运算符
typeid
一个RTTI运算符
const_cast
强制类型转换运算符
dynamic_cast
强制类型转换运算符
reinterpret_cast
强制类型转换运算符
static_cast
强制类型转换运算符
6. 只能通过成员函数进行重载的运算符
=
赋值运算符
()
函数调用运算符
[]
下标运算符
->
通过指针访问类成员的运算符
11.2.3 其他重载运算符(*)
11.3 友元
概述
三种友元
友元函数
友元类
友元成员函数
问题
重载二元运算符时,常常需要友元。 A = B * 2.5; 将被转换为下面的成员函数调用: A.operator*(2.5); 但下面的语句呢? A = 2.5 * B; 由于第一个表达式不对应于成员函数,因此编译器不能通过使用成员函数来替换该表达式
解决方案
使用非成员函数(不使用对象调用),它使用的所有值(包括对象)都是显示参数 A = 2.75 * B; 与下面的非成员函数调用匹配: A = operator*(2.75, B); 该函数的原型如下: Time operator*(double m, const Time& t); 这里的参数列表是一一对应的,但是对象的私有成员数据无法直接访问。因此需要使用友元函数
11.3.1 创建友元
1. 第一步
将其原型放在类声明中,并在原型声明前加上关键字friend: friend Time operator*(double m, const Time& t);
说明
· 虽然operator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
· 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同
2. 第二步
编写函数定义
说明
· 不需要使用Time::限定符
· 不要在定义中使用关键字friend
11.3.2 常用的友元:重载<<运算符
概述
cout是一个ostream对象,能够识别所有的C++基本类型。这是因为对于每种类型,ostream中都包含了相应的重载operator<<()定义。
1. <<的第一种重载版本
void operator<<(ostream& os, const Time& t) { os << t.hours << " hours, " << t.minutes << minutes"; }
这里函数operator<<()是Time的类友元函数,因为它访问了Time类的私有成员数据,因此其声明应放在Time类之中。但它不是ostream类的友元函数,因为函数将ostream对象当做一个整体使用,因此也不必修订ostream的定义
通常情况下,os引用cout对象,如表达式cout << trip所示,但也可以将正运算符用于其他ostream对象,在这种情况下,os将引用相应的对象
2. <<的第二种重载版本
上述版本存在一个问题,就是不允许像通常那样将重新定义的<<运算符与cout一起连用: cout << "Trip time: " << trip << " (Tuesday)\n"; // can't do
修改operator<<()函数: ostream& operator<<(ostream& os, const Time& t) { os << t.hours << " hours, " << t.minutes << minutes"; return os; }
这个版本还可以用于将输出写入文件中: #include<iostream> ... ofstream fout; fout.open("savetime.txt"); Time trip(12, 40); fout << trip; 其中最后一条语句被替换为这样: operator<<(fout, trip); 类继承属性让ostream引用能够指向ostream对象和ofstream对象
3. cout操作的相关知识
int x = 5; int y = 8; cout << x << y;
C++从左到右读取输出语句,意味着上述输出语句等于: (cout << x) << y; 正如iostream中定义那样,<<运算符要求左边是一个ostream对象。上述表达式满足这种需求,ostream类将operator<<()函数实现返回一个指向ostream对象的引用。具体地说,它返回一个指向调用对象(这里是cout)的引用。因此表达式(cout << x)本身就是ostream对象cout,从而实现连读。
4. 其他ostream对象(cerr)
cerr是ostream的另一个对象,它将输出发送到标准输出流——默认为显示器,但在UNIX、Linux和Windows命令环境中,可将标准错误流重定向文件。
11.4 重载运算符:作为成员函数还是非成员函数
· 一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据
· 对于某些运算符来说,成员函数是唯一的选择在其他情况下,这两种格式没有太大的区别
· 有时,根据类设计,使用非成员函数版本更好(尤其是为类定义类型转换时)
11.6 类的自动转换和强制类型转换
概述
隐式数制转换
long count = 8; double time = 11; int side = 3.33;
不兼容的类型
int* p = 10; 虽然计算机内部可能使用整数来表示地址,但从概念上来说,整数和指针完全不同。然而,在无法自动转换时,可以使用强制类型转换: int* p = (int*) 10; 上述语句将10强制转换为int指针类型(int*类型),将指针设置为地址10。这种复赋值是否有意义是另一回事
Stonewt类定义
下面的构造函数将double类型的值转换为Stonewt类型: Stonewt(double lbs); 也就是说可以写这样的代码: Stonewt myCat; myCat = 19.6; 程序将使用构造函数Stonewt(double)来创建一个临时的Stonewt对象,并将19.6作为初始化值。随后,将采用逐成员赋值的方式将该临时对象的内容复制到myCat中。这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换
只有接受一个参数的构造函数才能作为转换函数。下面的构造函数有两个参数,因此不能用来转化类型: Stonewt(int stn, double lbs); // not a conversion function 然而,如果给第二个参数提供默认值,它便可用于转换int: Stonewt(int stn, double lbs = 0);// int-to Stonewt conversion
关键字explicit
explicit Stonewt(double lbs); // no implicit conversion allowed 这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显示强制类型转换: 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类型的内置变量时
函数原型化提供的参数匹配过程中,允许使用Stonewt(double)构造函数来转换其他数值类型。也就是说,下面两条语句都首先将int转换为double,然后使用Stonewt(double)构造函数: Stonewt Jumb(7000); Jumb = 7300; 然而,当且仅当转换不存在二义性时,这种转换才会执行。也就是说,如果这个类还定义了构造函数Stonewt(long),则编译器将拒绝这些语句。
11.6.1 转换函数
1. 介绍
我们可以将double类型转换为自定义的Stonewt类型,那么,我们可以做相反的转换吗? Stonewt wolfe(285.7); double host = wolfe; // ?? possible ?? 可以这样做,需要使用C++特殊的运算符函数——转换函数
转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。例如,如果用户定义了从Stonewt到double的转换函数,就可以使用下面的转换: Stonewt wolfe(285.7); double host = double (wolfe); // syntax #1 double thinker = (double) wolfe; // syntax #2 也可以让编译器来决定如何做: Stonewt wells(20, 3); double star = wells; // implicit use of conversion function
转换函数形式: operator typename(); // convert typename 注意以下几点: · 转换函数必须是类方法 · 转换函数不能指定返回类型 · 转换函数不能有参数 例如,转换为double类型的函数原型如下: operator double();
2. 示例
在下面的语句中: cout << "Poppins: " << poppins << " pounds.\n"; 程序还会像在下面的语句中那样使用隐式转换吗? double p_wt = poppins; 答案是否定的。因为并没有指出应转换为int类型还是double类型,在缺少信息时,编译器将指出使用了二义性转换。 有趣的是,如果类定义中只定义了double转换函数,则编译器将接受该语句。因为只有一种转换可能,因此不存在二义性
赋值的情况与上述类似,对于当前的类声明来说,编译器将认为下面的语句有二义性而拒绝它: long gone = poppins; 因为在C++中,int和double值都可以被赋给long变量,编译器不想承担选择转换函数的责任(tmd),因而拒绝。若删除其中一个转换函数,则编译器将接受,但仍可以用显式强制类型转换来指出要使用哪个转换函数: long gone = (double) poppins; long gone = int (poppins);
#ifndef STONEWT_H #define STONEWT_H class Stonewt { private: enum {Lbs_per_stn = 14}; int stone; double pds_left, pounds; public: Stonewt(double lbs); Stonewt(int stn, double lbs); Stonewt(); ~Stonewt(); void show_lbs() const; void show_stn() const; // conversion functions operator int() const { return int (pounds + 0.5); } operator double() const { return pounds; } }; #endif
3. 说明
问题产生
假如你在睡眠不足时写了以下代码: int ar[20]; ... Stonewt temp(14, 4); ... int Temp = 1; ... cout << ar[temp] << "!\n"; // used temp instead of Temp 因为Stonewt定义了一个int转换函数,因此编译器将不会报错。
方法一
原则上来说,最好使用显式转换,而避免隐式转换。在C++98中,不能使用关键字explicit,但是在C++11中,可将转换运算符声明为显式的: class Stonewt { ... // conversion functions explicit operator int(); explicit operator double(); }; 有了这些声明后,需要强制转换时才调用这些运算符
另一种方法
用一个功能相同的非转换函数替换该转换函数即可: int Stonewt::Stone_to_Int() { return int (pounds + 0.5); } 这样,下面的语句也是合法的: int plb = poppins; 但如果确实需要这种转换,可以这样做: int plb = poppins.Stone_to_Int();
11.6.2 转换函数和友元函数
1. 成员函数和友元函数重载加法运算符
成员函数
Stonewt Stonewt::operator+(const Stonewt& st) const { double pds = pounds + st.pounds; Stonewt sum(pds); return sum; }
友元函数
Stonewt operator+(const Stonewt& st1, const Stonewt& st2) const { double pds = st1.pounds + st2.pounds; Stonewt sum(pds); return sum; }
2. double和Stonewt的相加
如果提供了Stonewt(double)函数,则可以这样做: Stonewt jennySt(9, 12); double kennyD = 176.0; Stonewt total; total = jennySt + pennyD;
total = pennySt + kennyD; 被转换为: total = pennySt.operator+(kennyD); 然后pennyD被转换为Stonewt对象传给函数。 但是如果顺序反了: total = kennyD + pennySt; 则编译无法通过,因为C++不会试图将pennyD转换为Stonewt对象。
但是只有友元函数才允许这样做: Stonewt jennySt(9, 12); double kennyD = 176.0; Stonewt total; total = pennyD + jennySt;
total = pennyD + pennySt; 被转换为: total = operator+(pennyD, pennySt); 然后pennyD被转换为Stonewt对象传给函数。
这里的经验是,将加法定义为友元可以让程序更容易适应自动类型转换。原因在于,两个操作数都成为函数参数,因此与函数原型匹配
3. 实现加法的选择
方法一
将加法函数定义为友元函数: Stonewt operator+(const Stonewt& st1, const Stonewt& st2) const;
· 程序简短,出错机会少
· 每次转换调用转换函数,增加时间开销和内存开销
方法二
将加法重载为一个显示使用double类型参数的函数: Stonewt operator+(double x); friend Stonewt operator+(double x, Stonewt& s);
· 程序较长,工作量更大
· 运行速度快
11.5 再谈重载:一个矢量类
概述
程序中4个报告分量值的函数是在类声明中定义的,将自动成为内联函数。因为这些函数非常短,因此适用于声明为内联函数
方法reset()并非必不可少的,下面的代码: shove.reset(100, 300); 可以使用构造函数来获得相同的结果: shove = Vector(100, 300);
11.5.1 使用状态成员
1. 枚举RECT表示直角坐标模式(默认值)、POL表示极坐标模式。 这样的成员称为状态成员
2. 标识符POL的作用域为类,因此类定义可以表示可使用未限定的名称。这里全局限定名为VECTOR::Vector:POL。
3. 下面的代码将不能通过编译,因为诸如2等整数不能隐式地转换为枚举类型: Vector rector(20.0, 30.0, 2); // type mismatch 然而,机智而好奇的用户可尝试下面这样的代码: Vector rector(20.0, 30.0, VECTOR::Vector::Mode(1)); // type cast 就这里而言,编译器将发出警告。
4. 由于operator<<()函数是一个友元函数,而不在类作用域内,因此必须使用Vector::RECT,而不能使用RECT。但这个友元函数在名称空间VECTOR中,因此无需使用全限定名VECTOR::Vector::RECT
11.5.2 为Vector类重载算术运算符
概述
对于某些重载运算符函数来说,使用构造函数来完成工作将更简单,更可靠: Vector Vector::operator+(const Vector& b) const { return Vector(x + b.x, y + b.y); }
1. 乘法
定义两种不同顺序的乘法: Vector Vector::operator*(double n) const { return Vector(n * x, n * y); } Vector operator*(double n, const Vector& a) { return a * n; }
2. 对已重载的运算符进行重载
声明: Vector operator-(const Vector& b) const; Vector operator-() const;
定义: Vector operator-(const Vector& b) const { return Vector(x - b.x, y - b.y); } Vector operator-() const { return Vector(-x, -y); }
11.5.3 对实现的说明
1. 两种实现
(1)使用上述实现,类中存储x、y、mag、ang四个分量,每次修改都更新四个四个分量。
(2)类中仅存储两个数据:x和y。这样在接口处应做出修改。对于相应的极坐标方程,接口需要通过计算得到x和y,再进行相应操作。
2. 差别
第一种读取数据的速度很快。而第二种储存空间较小
如果只是偶尔使用极坐标,则第二种实现更好; 如果程序需要经常访问矢量的两种表示,则第一种实现更好
可以在一个程序中使用一种实现,而在另一个程序中使用另一种实现,但是他们的用户接口相同
11.5.4 使用Vector类来模拟随机漫步
标准ANSI C库(C++也有)中有一个rand()函数,它返回一个从0到某个值(取决于实现)之间的随机整数。该程序使用求模操作来获得一个0~359的角度值: direction = rand() % 360; rand()函数将一种算法用于一个初始种子值来获得随机数,该随机值将作用于下一次函数调用的种子。以此类推,这些数实际上是伪随机数,因为10次连续的调用通常生成10个同样的随机数(具体值取决于实现)。然而,srand()函数允许覆盖默认的种子值,重新启动另一个随机数序列。
该程序使用time(0)的返回值来设置种子。time(0)函数返回当前时间,通常为从某一个日期开始的秒数(更广义地,time()接受time_t变量的地址,将时间放到该变量中并返回。将0用作地址参数,可以省略time_t变量声明)。因此,下面的语句在每次运行该程序时都将设置不同的种子,使随机输出看上去更为随机: srand(time(0));
头文件cstdlib包含了srand()和rand()的原型,而ctime包含了time()的原型
相关定义如下: #ifndef VECTOR_H_ #define VECTOR_H_ #include <iostream> namespace VECTOR { class Vector { public: enum Mode {RECT, POL} private: double x, y, mag, ang; Mode mode; 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; // friends friend Vector operator*(double n, const Vector& a); friend std::ostream& operator<<(std::ostream& os, const Vector& v); }; } #endif