C++ Primer Plus (6th) Chap11 使用类 摘录

本章进一步探讨类的特征,重点是类设计技术,而不是通用原理。

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

11.1 运算符重载

例如,将*运算符用于地址,将得到存储在这个地址中的值;但将它用于两个数字时,得到的将是它们的乘积。C++根据操作数的数目和类型来决定采用那种操作。

要重载运算符,需使用被称为运算符函数的特殊函数形式。其格式如下:

operator op(argument-list)

以重载的加号运算符为例:

// 运算符表示法,运算符左侧的对象是调用对象,右侧是作为参数传递
district2 = sid + sara;

// 函数表示法
district2 = sid.operator=(sara);

11.2 计算时间--一个运算符重载的例子

不要返回指向局部变量或临时对象的引用。函数执行完毕后,局部变量和临时变量将消失,引用将指向不存在的数据。

11.2.2 重载限制

重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。

        1. 这将防止用户为标准类型重载运算符;

        2. 使用运算符时不能违反运算符原来的句法规则(例如,需要二元操作数变成需要一元操作数);

        3. 不能修改运算符的优先级;

        4. 不能创建新的运算符;(例如,定义**运算符来求幂)

        5. 不能重载下面的运算符;

sizeofsizeof运算符
.成员运算符
.*成员指针运算符
::作用域解析运算符
?:条件运算符
typeid一个RTTI运算符
const_cast强制类型转换运算符
dynamic_cast强制类型转换运算符
reinterpret_cast强制类型转换运算符
static_cast强制类型转换运算符

        5. 下面运算符只能通过成员函数进行重载;

=赋值运算符
[]索引运算符
()函数调用运算符
->通过指针访问类成员运算符

可重载的运算符:

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

11.3 友元

C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径。但是,C++还提供了另一种形式的访问权限:友元。友元分3种:

        友元函数;

        友元类;

        友元成员函数;

通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

为何需要友元?在为类重载二元运算符时常常需要友元。

在前面的Time类示例中,重载的乘法运算符与其他两种重载运算符的差别在于,它使用了两种不同的类型。乘法运算符将一个time值与一个double值结合在一起。

A = B * 6.25;  // 运算符左侧为调用对象,此时B为对象,乘号可以调用成员函数。
A = 6.25 * B; // 此时6.25为调用者,它只是double类型,而非类对象,无法使用类成员函数。

此时,因使用非成员函数。非成员函数不是由对象调用的,它使用的所有值都是显式参数。

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

对于非成员重载运算符函数而言,运算符表达式左边的操作数相当于运算符函数的第一个参数,运算符表达式右边的操作数相当于运算符函数的第二个参数。

使用非成员函数可以按需的顺序获取操作数(先是double,后是Time类)。但其中有一个问题,非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,友元函数可以。

// Time.h
#ifndef CLASSTIMEE_H_
#define CLASSTIMEE_H_

class Time
{
private:
	int hours;
	int minutes;

public:
	Time();
	~Time();
	Time operator *(double m) const;

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

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


#endif

// Time.cpp
#include<iostream>
#include"classTime.h"

Time::Time()
{
	hours = 0;
	minutes = 0;
}

Time Time::operator*(double m) const
{
	Time result;
	long totalMinutes = hours * m + minutes * m;
	result.hours = totalMinutes / 60;
	result.minutes = totalMinutes % 60;
	return result;
}


Time operator*(double m, const Time& t)
{
    // 显式访问了time类t.hour,对于非成员函数的话,只能在声明时加上friend
	Time result;
	long totalMinutes = t.hours * m + t.minutes * m;
	result.hours = totalMinutes / 60;
	result.minutes = totalMinutes % 60;
	return result;

}

// 将友元函数编写成非友元函数形式
Time operator*(double m, const Time& t)
{
	return t * m;
}

应该将友元函数看成类的扩展接口的组成部分。例如,从概念上看,double乘以Time和Time乘以double是完全相同的。而前一个需要友元函数,后一个使用成员函数,这是C++句法的结果。

请记住,只有类声明可以决定那些函数可以是友元函数,因此类声明依旧控制了那些函数可以访问私有数据。

如果要为类重载运算符,并将非类的项作为第一个操作数,则可以用友元函数来反转操作数顺序。

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

1. << 的第一种重载版本

要使Time类知道使用cout,必须使用友元函数。因为:

Time t;
t << cout; // 形式与常规使用cout不一致

通过友元函数后,cout形式正常:

void operator <<(ostream& os, const Time& t)
{
    os << ...;
}

是否需要将<<运算符也同时设计成ostream类的友元函数? 不需要,因为从始至终,<<函数都只是将ostream类当成整体使用,并没有访问ostream类的私有成员。

若想连续使用<<运算符?则需要将上式返回值改成对ostream对象的引用;

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

(cout << x) << y;

由于<<返回ostream对象的引用,因此,(cout<<x)本身就是一个ostream对象cout,从而可以位于<<运算符左侧。

只有在类声明中的原型中才能使用friend关键字,除非函数定义就是函数原型,否则不能在函数定义中使用该关键字。

11.4 重载运算符:成员or非成员函数

除=,[], (), ->这四个运算符只能采用成员函数形式,其他运算符两者形式都可。

非成员版本的重载运算符函数所需的形参数目与运算符使用使用的操作数数目相同;而成员版本所需要的参数少一个,因为其中一个操作数是被隐式地传递给调用对象--this指针。

对于同一个重载运算符函数而言,不能同时定义成员版本与非成员版本。因为会出现二义性。

11.5 再谈重载,一个矢量类

在类中定义枚举,可以作为状态成员使用。比如,矢量的直角坐标与极坐标状态。

class Vector
{
public:
    enum Mode {RECT, POL};
...
};

Vector类的友元函数访问enum量时,需要加上类限制符,比如Vector::RECT,而不能像成员函数一样,直接使用RECT,却不加Vector::。

11.6 类的自动类型转换和强制类型转换

C++是如何处理内置类型转换的。将一个标准类型变量的值赋给另一个标准类型的变量时。如果这两种变量是兼容的,则C++自动将这个值转换为接收变量的类型。

// 兼容类型自动转换
int a = 1;
double b = a;

// 不兼容对象,不会自动转换
int* p = a; // 出错


// 不兼容对象,强制类型转化
int* p = (int*)a;


C++不自动转换不兼容类型。然而,在无法自动转换时,可以使用强制类型转换。

可以将类定义成与基本类型或另一个类相关,使得从一种类型转换为另一种类型是有意义的。

在C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。

Stonenew(double lbs); // 类Stonenew构造函数
Stonenew myCat;       // 创建类对象myCat


// 程序先使用Stonenew构造对象创建一个临时对象,再将16.5作为其初始值。
// 然后,采用逐成员赋值方式将该临时对象的内容赋值到myCat中。这一过程称为隐式转换,因为它是自动进行的。
myCat = 16.5;         

只有接收一个参数的构造函数才能作为转换函数,或者构造函数的其他参数均有默认值。

Stonenew(double n, int a = 0); // 该构造函数也可以作为转换函数

但有时自动转换会带来困扰,所以C++也能使用关键字explicit来关闭接收单参数的构造函数作为转换函数的特性。

explicit Stonenew(double lbs);

只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用了关键字explicit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。

编译器在什么时候将使用Stonenew(double)函数呢? 如果在声明中使用了关键字explicit,则Stonenew(double)将只用于显式强制类型转换,否则还可用于下面的隐式类型转换:

        将Stonenew对象初始化为double值时;

        将double值赋给Stonenew时;

        将double值传递给接收Stonenew参数的函数时;

        返回值被声明为Stonenew的函数试图返回double时;

        在上述任意一种情况下,使用可转换为double类型的内置类型时;

当构造函数只接受一个参数时,可以使用下面的格式来初始化类对象:

Stonenew incogni = 285;

// 下面两种可接收多个参数的构造函数
Stonenew incogni(275);
Stonenew incogni = Stonenew(275);
void display(Stonenew& st, int n);

// dispaly()函数原型表示第一个参数应该为Stonenew对象,然而遇到int型实参265后,
// 编译器将查找Stonenew的构造函数,以便将int类型转换为Stonenew类型。由于没有找到
// 这样的构造函数,因此编译器寻找接收其他内置类型(int可转换为这种类型)的构造函数。Stonenew(double)
// 构造函数满足要求,因此编译器将int转换为double,然后使用Stonenew(double)将其转换为Stonenew类
display(265, 5);

11.6.1 转换函数

Stonenew wolfe(285.7);
double host = wolfe;

可以这样做,但不是使用构造函数。而是C++运算符函数--转换函数。

转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。

要转换为typeName类型,需要使用以下形式的转换函数:

operator typeName();

请注意以下几点:

        转换函数必须是类方法;

        转换函数不能指定返回类型;

        转换函数不能有参数;

例如;

operator double();

转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。

class Stonenew
{
private:
    ...
public:
    operator int() const;
    operator double() const;

}

如果同时定义多个转换函数,编译器若发现多个转换函数均可使用时,编译器将会报错!

和转换构造函数一样,转换函数也有其优缺点。提供执行自动、隐式转换的函数所存在的问题是:在用户不希望进行转换时,转换函数也可能进行转换。

为避免进行隐式转换,C++11可将转换运算符声明为显式,只需加上explicit关键字。声明为显式后,需要强制转换时将调用这些运算符。

explicit operator double() const;

C++为类提供了下面的类型转换:

        只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将int值赋给Stonenew对象时,只接收一个int参数的Stonenew类构造函数将自动被调用。然而,在构造函数声明中使用explicit可防止隐式转换,而只允许显式转换。

        被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数时类成员,没有返回类型,没有参数,名为typeName(),其中,typeName是对象将被转换成的类型。

11.6.2 转换函数和友元函数

过多的转换函数将导致二义性。

经验是将加法定义为友元可以让程序更容易适应自动类型转换。原因在于,两个操作数都成为函数参数,因此与函数原型匹配。

实现加法时的选择有两个。第一种将函数定义为友元函数,让Stonenew(double)构造函数将double类型的参数转换为Stonenew类型的参数。第二种是将加法运算符重载为一个使用double类型的参数的函数。

class Stonenew
{
    ...
public:
    firend Stonenew operator+(const Stonenew&, const Stonenew&); // 第一种 -- 友元函数
    Stonenew operator+(double); // 第二种 -- 加法运算符重载
    ...
}

     第一种方法优点(依赖隐式转换)使程序简单,因为定义的函数较少。这也使程序员工作量少,出错的机会较小。缺点在于每次需要转换时,都将调用转换构造函数,这增加时间和内存开销。第二种方法(增加一个显式的匹配类型的函数)则正好相反。它使程序较长,程序员需要完成的工作更多,但允许速度较快。

如果程序经常使用double值与Stonenew对象相加,则重载更合适;如果只是偶尔使用,则依赖自动类型更简单,但为了更保险,可以使用显式转换。

11.7 总结

访问私有类成员的唯一方法是使用类方法。C++使用友元函数来避开这种限制。要让函数成为友元。需要在类声明中声明该函数,并在声明前加上关键字friend。

运算符函数可以是类成员函数,也可以是友元函数。但=,[], ->, ()四个运算符函数只能是类成员函数。

对于重载的运算符函数而言,其格式如下:

*** operator op(arguments-list);

如果运算符函数是类成员函数,则第一个操作数是调用对象,它不在arguments-list中。

如果要使第一个操作数不是类对象,则必须使用友元函数。这样就可以将操作数按所需的顺序传递给函数了。

最常见的运算符重载任务之一就是定义<<运算符,它与cout一起使用,来显示对象的内容。要让ostream对象成为第一个操作数,则需要将运算符函数定义为友元;要使其重新定义的运算符能与其自身拼接,需要将返回类型声明为ostream&。

C++允许指定在类和基本类型之间进行转换的方式。首先,任何接收唯一一个参数的构造函数都可被用作转换函数,将类型与该该参数类型相同的值转换为类。

如果在构造函数的声明前加上关键字explicit,则该构造函数只能用于显式转换。

如果要将对象转换为其他类型,必须定义转换函数,指出如果进行这种转换。转换函数必须是成员函数。将类对象转换为typeName类型的转换函数的原型如下;

operator typeName();

注意,转换函数没有返回类型,没有参数,但必须返回转换后的值(虽然没有声明返回类型)。例如,将Vector转换为double类型的函数:

Vector:: operator double()
{
    ...
    return a_double_value;
}

经验表明,最好不要依赖于这种隐式转换函数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值