5. 使用类

5.1. 运算符重载

运算符函数的格式:

operator<op>(<argument-list>)

5.1.1 重载限制

  1. 重载的运算符不一定要是类成员函数,但至少有一个参数是用户定义的类型(防止用户为标准类型重载运算符)。
  2. 重载运算符不能违反运算符原来的句法规则。例如,不能将一元运算符重载为二元运算符。
  3. 不能重载新运算符。
  4. 不能重载以下运算符:
    1. sizeof
    2. .:成员运算符
    3. .*->*:成员指针运算符
    4. :::作用域解析运算符
    5. ?::条件运算符
    6. typeid:一个RTTI运算符
    7. const_cast:强制类型转换运算符
    8. dynamic_cast:强制类型转换运算符
    9. reinterpret_cast:强制类型转换运算符
    10. static_cast:强制类型转换运算符
  5. 以下运算符只能重载为成员函数:
    1. =:赋值运算符
    2. ():函数调用运算符(function object 或 functor)
    3. []:下标运算符
    4. ->:通过指针访问类成员的运算符

5.1.2 成员指针运算符

成员指针运算符.*->*也称成员解除引用运算符
成员指针运算符用来使用成员指针

class Example
{
public:
    int feet;
    int inches;
    Example();
    Example(int ft);
    ~Example();
    void show_feet() const;
    void show_inches() const;
};
Example obj1(16);
Example obj2(24);
Example* pobj = new Example(36);

int Example::*pm = &Example::inches;
cout << obj1.*pm << endl;
cout << obj2.*pm << endl;
cout << pobj->*pm <<endl;

pm = &Example::feet;
cout << obj1.*pm << endl;
cout << obj2.*pm << endl;
cout << pobj->*pm <<endl;

成员指针并不指向特定的内存单元,而是指向类的任意对象中的特定public成员变量或成员函数。
实际上,*pm相当于一个成员名。可以用来表识其他任何相同类型的成员。

注意:只有public成员变量或函数可以让成员指针引用。

将成员指针指向成员函数上:

void (Example::*pf)() const = &Example::show_feet;
(obj1.*pf)();
(obj2.*pf)();
(pobj->*pf)();

pf = &Example::show_inches;
(obj1.*pf)();
(obj2.*pf)();
(pobj->*pf)();

有几点需要注意:

  1. Example::*pf必须放在括号里。
  2. 即使是对类中的函数名,也必须使用地址运算符&
  3. 使用时要像(pobj->*pf)这样将对象名也放进括号里。

5.2 友元

5.2.1 友元函数

声明友元函数要在声明前加上friend关键字,并将声明放进类声明中。
在类外部的说明符是无效的,编译器甚至会报错。

友元函数可以访问类的私有和保护成员。

#include <iostream>
using std::cout;
using std::ostream;
using std::endl;

class Date
{
private:
    short m, d, y;
    friend void show(Date& dt);
    friend ostream& operator <<(ostream& os, const Date& dt);
public:
    Date();
    Date(short m, short d, short y);
    ~Date();
};

int main()
{
    Date today(2, 7, 2020);
    show(today);
    cout << today << endl;
}

void show(Date& dt)
{
    cout << "month: " << dt.m << ", day: " << dt.d << ", year: " << dt.y << endl;
}

ostream& operator <<(ostream& os, const Date& dt)
{
    os << "month: " << dt.m << ", day: " << dt.d << ", year: " << dt.y;
    return os;
}

// ...狗构造函数和析构函数的实现...

输出:

month: 2, day: 7, year: 2020
month: 2, day: 7, year: 2020

注意到,类中的访问控制说明对友元函数没有效果,这是因为友元函数并不在类的名称空间中。

5.2.2 友元成员函数

5.2.3 友元类

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

5.3.1 将其他类型转换为本类型(构造函数)

class Stonewt
{
    // ...
public:
    Stonewt(double d);
    // ...
};

Stonewt mycat
mycat = 19.6;

在类声明中声明了接受一个double参数的构造函数。

下面两行代码等效:

mycat = 19.6;
mycat = Stonewt(19.6);

这里隐式调用了构造函数创建了一个临时对象,并将临时对象内容复制到mycat中(临时对象会在之后的某一时刻被清除)。
这一过程称为隐式(类型)转换,也称为自动(类型)转换

隐式转换发生在以下情况中:

  • double值初始化Stonewt对象时。
  • double值赋值给Stonewt对象时。
  • double值传递给接受Stonewt参数的函数时。
  • 返回值被声明为Stonewt的函数试图返回double时。
  • 在上面任一情况中,使用可转换为double的内置类型时。

C++中还提供了explicit关键字关闭这种特性,指明需要显式写出构造函数。
语法就像这样:

explicit Stonewt(double d);

声明为explicit时,仍可以显式使用这样的构造函数来进行显式类型转换。

5.3.2 将本类型转换为其他类型(转换函数)

转换函数时C++中的一种特殊的运算符函数
基本语法如下:

operator <typename>();
class Stonewt
{
    // ...
public:
    double();
    // ...
};

Stonewt mycat
mycat = 19.6;

有趣的是,typename还可以是用户定义的类型。

class Weight
{
    double wt;
public:
    Weight(double wt){this->wt = wt;}
};

class Stonewt
{
    double st;
public:
    operator Weight(){return Weight(st);}
    Stonewt(double d){st = d;}
};

int main()
{
    Stonewt mycat(19.6);
    Weight wt = mycat;
    return 0;
}

使用转换函数有以下限制:

  • 转换函数必须是类函数。
  • 转换函数不能由用户指定返回类型。
  • 转换函数不能有参数。

如果定义了多个转换函数,还需要注意避免二义性。
这时就需要使用显式类型转换,说明要调用的转换函数。
另外,还可以使用explicit关键字。这是C++11中新增的特性。

5.3.3 友元函数与成员函数

大多数情况下,友元函数和成员函数可以用来实现对方能实现的功能,但是在以下情况下例外。

存在Stonewt类,Stonewt类中定义了构造函数Stone(double),(没有定义duoble()函数)。
现在分别用成员函数或友元函数的方式定义operator+函数。

// 成员函数的声明
Stonewt operator+(const Stonewt& st) const;
// 友元函数的声明
friend Stonewt operator+(const Stonewt& st1, const Stonewt& st2);

然后又如下代码:

Stonewt mycat(19.6);
double yourcat = 18.6;
Stonewt total;
total = yourcat + mycat;    // 只有友元函数能实现

以上代码只有用友元函数定义的operator+才能实现。
因为yourcat是一个:double类型的变量,使用友元函数的可以这样:

total = operator+(yourcat, mycat);
// friend Stonewt operator+(const Stonewt& st1, const Stonewt& st2);

虽然友元函数的第一个参数类型为Stonewt&,但是可以通过构造函数Stone(double)隐式生成一个临时对象,并将临时对象的内容复制给st1

但是double没有下面这样的成员函数(C++也不允许用户为内置类型定义成员函数):

Stonewt operator+(const Stonewt& st) const;
// 或者这样
double operator+(const Stonewt& st) const;

另外,以上问题还可以通过重载operator+的方法解决。

Stonewt operator+(double, const Stone&);

使用这种方法需要更多的工作,但是不需要进行类型转换,减少了时间和内存开销。

5.4 浅复制&深度复制

5.4.1 复制构造函数

Class_name(const Class_name&);

如果用户未定义复制构造函数,C++会自动在必要时提供一个进行浅复制的复制构造函数。
如果在类中使用了动态内存分配(有一个指针成员变量指向内存地址),浅复制只会简单地复制指针成员变量的值,而不是将指针指向的内容复制一份。
这里就需要自己定义一个复制构造函数进行深度复制

5.4.2 赋值运算符

Class_name& Class_name::operator=(const Class_name&);

用一个对象给另外一个已有的对象赋值时,会调用赋值运算符函数。
用一个对象用赋值的方式初始化另一个对象时,可能也会直接调用复制构造函数,也可能会调用赋值运算符函数。

这里总结一下,使用使用=运算符初始化总是可能会直接调用匹配的构造函数。

Stonewt new_obj = Stonewt(13.4);
Stonewt new_obj = 13.4;
// Stonewt new_obj(13.4);
// 也可能会先生成一个临时对象,然后使用复制构造函数

// 用赋值语句初始化的可能过程我也没弄清楚,以下为我的猜测
Stonewt new_obj = existed_obj;
// Stonewt new_obj(existed_obj);
// operator=(new_obj, existed_obj);

OK,回来。C++提供的默认赋值运算符也是进行浅复制,因此也可能需要由用户自定义深度复制的版本。
与复制构造函数相比,有两点需要注意:

  1. 首先要检查自我赋值,如果是自我赋值,应直接返回(返回值一般为*this,因为可能要连续赋值。
  2. 指针所指内存大小改变时,释放原来动态分配的内存,再分配新的内存。

5.5 使用动态内存分配要注意的问题

  • new对应deletenew[]对应delete[]
  • 析构函数只有一个,所以在构造函数中要么都使用new,要么都使用new[]
  • 考虑用new type_name[1]代替new type_name

5.6 定位new运算符

如果使用定位new运算符将类的对象(数组)“放在”动态分配的内存上,有两点要注意。

  1. 不要用delete([])释放类的对象(数组),因为定位new运算符只是将指针的值强制转换为void*,并没有分配内存。
  2. 如果这个类中使用了动态内存分配,要先显式地调用析构函数(其中有delete([])语句)。

猜你喜欢

转载自www.cnblogs.com/alohana/p/12293191.html