前言

在这一章里将介绍基类与继承、多态公有继承、静态联编和动态联编、访问控制protected以及继承和动态内存分配。这是类系列的第四部分,这是最后一个部分,故也会复习前面的重要知识。

  1. (其一):对象与类
  2. (其二):使用类
  3. (其三):类和动态内存分配
  4. (其四):类继承

基类与继承

我们在使用类的时候,已经写好了一个基本的类,我们还需要一个基于这个类的新类。我们可能想通过复制代码,修改达到目的,但C++提供了新的机制——继承机制只需提供新特性,甚至不需要访问源代码就可以派生出类。

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。下面通过一个有趣的例子引入:

一个俱乐部提供了一个TableTennisPlayer类,来管理会员。

#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
    string firstname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer (const string & fn = "none",
                       const string & ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};
#endif
#include "tabtenn0.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer (const string & fn, 
    const string & ln, bool ht) : firstname(fn),
	    lastname(ln), hasTable(ht) {}
    
void TableTennisPlayer::Name() const
{
    std::cout << lastname << ", " << firstname;
}

TableTennisPlayer类只是记录会员的姓名以及是否有球桌。构造函数使用了成员初始化列表语法,它等价于下列语句:

TableTennisPlayer::TableTennisPlayer (const string & fn, const string & ln, bool ht)
{
    firstname=fn;
    lastname(ln);
    hasTable(ht);
}

我们使用这个类的时候,例如:

#include <iostream>
#include "tabtenn0.h"

int main (void)
{
    using std::cout;
    TableTennisPlayer player1("Chuck", "Blizzard", true);
    TableTennisPlayer player2("Tara", "Boomdea", false);
    player1.Name();
    if (player1.HasTable())
        cout << ": has a table.\n";
    else
        cout << ": hasn't a table.\n";
    player2.Name();
    if (player2.HasTable())
        cout << ": has a table";
    else
        cout << ": hasn't a table.\n";
    return 0;
}

俱乐部的一些成员曾经参加过当地的乒乓球锦标赛,需要这样一个类,它能包括成员在比赛中的比分。与其从零开始,不如从TableTennisClass类派生出一个类。首先将RatedPlayer类声明为从TableTennisClass类派生而来:

class RatedPlayer : public TableTennisClass
{
...
};

冒号指出RatedPlayer类的基类是TableTennisplayer类。上述特殊的声明头表明TableTennisPlayer是一个公有基类,这被称为公有派生。派生类对象包含基类对象,使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

Ratedplayer对象将具有以下特征:

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口)。

因此,RatedPlayer对象可以存储运动员的姓名及其是否有球桌。另外,RatedPlayer对象还可以使用TableTennisPlayer类的Name( )、hasTable( )和ResetTable( )方法。

此外,除了继承以外,还需要注意:

  • 派生类需要自己的构造函数。构造函数必须给新成员(如果有的话)和继承的成员提供数据。
  • 派生类可以根据需要添加额外的数据成员和成员函数。例如我们添加一个私有成员rating,和使用rating的一些方法。
  • 派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。

第三点例如,RatedPlayer构造函数不能直接设置继承的成员(firstname、lastname和hasTable),而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类构造函数。

创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。

在写派生类的构造函数的时候,我们需要这么写,例如:

RatedPlayer::RatedPlayer(const string & fn, const string & ln, bool ht, 
    unsigned int r) : TableTennisClass(fn,ln,ht)
{
    rating=r;
}

其中TableTennisPlayer(fn,ln,ht)调用TableTennisPlayer构造函数。参数r的值赋给RealPlayer类中rating成员。它们之间的关系见下图:

有关派生类构造函数的要点如下:

  • 首先创建基类对象;
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
  • 派生类构造函数应初始化派生类新增的数据成员

创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。

同理,派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。

要使用派生类,程序必须要能够访问基类声明。我们把两个类放在一起来看如何使用派生类。

#ifndef TABTENN1_H_
#define TABTENN1_H_
#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
    string firstname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer (const string & fn = "none",
                       const string & ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};

// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
    unsigned int rating;
public:
    RatedPlayer (unsigned int r = 0, const string & fn = "none",
                 const string & ln = "none", bool ht = false);
    RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
    unsigned int Rating() const { return rating; }
    void ResetRating (unsigned int r) {rating = r;}
};

#endif
#include "tabtenn1.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer (const string & fn, 
    const string & ln, bool ht) : firstname(fn),
	    lastname(ln), hasTable(ht) {}
    
void TableTennisPlayer::Name() const
{
    std::cout << lastname << ", " << firstname;
}

// RatedPlayer methods
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)//method 1
{
    rating = r;
}

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)//method 2
    : TableTennisPlayer(tp), rating(r)
{}

接下来是使用这两个类的方法例子:

#include <iostream> 
#include "tabtenn1.h"

int main (void)
{
    using std::cout;
    using std::endl;
    TableTennisPlayer player1("Tara", "Boomdea", false);
    RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
    rplayer1.Name();          // derived object uses base method
    if (rplayer1.HasTable())
        cout << ": has a table.\n";
    else
        cout << ": hasn't a table.\n";
    player1.Name();           // base object uses base method
    if (player1.HasTable())
        cout << ": has a table";
    else
        cout << ": hasn't a table.\n";
    cout << "Name: ";
    rplayer1.Name();
    cout << "; Rating: " << rplayer1.Rating() << endl;
// initialize RatedPlayer using TableTennisPlayer object
    RatedPlayer rplayer2(1212, player1);
    cout << "Name: ";
    rplayer2.Name();
    cout << "; Rating: " << rplayer2.Rating() << endl;
    return 0;
}

我们看到,使用的时候,初始化rplayer1的时候,其使用的是method1的构造函数,而在初始化rplayer2的时候,其使用的是method2的构造函数。传过去了一个TableTennisPlayer类的引用。

method1中RealPlayer构造函数将把实参“Mallory”、“Duck”和true赋给形参fn、In和ht,然后将这些参数作为实参传递给TableTennisPlayer构造函数。于是创建一个嵌套TableTennisPlayer对象,并将数据“Mallory”、“Duck”和true存储在该对象中。然后,程序进入RealPlayer构造函数体,完成RealPlayer对象的创建,并将参数r的值(即1140)赋给rating。此时使用成员初始化列表的方法,先执行了传递数值并调用基类的构造函数。

如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。成员初始化列表只能用于构造函数。

method2中,由于tp的类型为TableTennisPlayer &,因此将调用基类的复制构造函数。基类没有定义复制
构造函数,所以程序自动生成一个复制构造函数此时执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配。也可以对派生类成员使用成员初始化列表语法,正如例子那样,即我们在列表中使用成员名,而不是类名进行初始化。

派生类与基类之间,除了派生类对象可以使用基类的方法外,还有下面两点:

  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象
  • 类引用可以在不进行显式类型转换的情况下引用派生类对象

例如:

RatedPlayer rplayer(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer;
TableTennisPlayer * pt = &replayer;
rt.Name();//OK
pt->Name();//OK

然而,基类指针或引用只能用于调用基类方法,因此,不能使用rt或pt来调用派生类的ResetRanking方法。

而反过来,不可以将基类对象和地址赋给派生类引用和指针。

我们知道了这两个特性,我们就可以做到定义一个函数,参数是基类的引用,而我们调用函数的时候,基类对象和派生类对象都可以作为参数传入进行函数。

派生类和基类之间的特殊关系是基于C++继承的底层模型的。实际上,C++有3种继承方式:公有继承、保护继承和私有继承。保护继承和私有继承在这章中不做解释。

公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。


多态公有继承

RatedPlayer继承示例很简单。派生类对象使用基类的方法,而未做任何修改。然而,可能会遇到这样的情况,即希望同一个方法在派生类和基类中的行为是不同的。换句话来说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态——具有多种形态。

例如我们需要开发两个类。一个类用于表示基本支票账户——Brass Account,另一个类用于表示代表Brass Plus支票账户,它添加了透支保护特性。

如果用户签出一张超出其存款余额的支票——但是超出的数额并不是很大,银行将支付这张支票,对超出的部分收取额外的费用,并追加罚款。

下面是用于Brass Account支票账户的信息:

  • 客户姓名;
  • 账号;
  • 当前结余。

下面是可以执行的操作:

  • 创建账户;
  • 存款;
  • 取款;
  • 显示账户信息。

银行希望Brass Plus支票账户包含Brass Account的所有信息及如下信息:

  • 透支上限;
  • 透支贷款利率;
  • 当前的透支总额。

两种操作的实现不同:

  • 对于取款操作,必须考虑透支保护;
  • 显示操作必须显示Brass Plus账户的其他信息。

接下来我们开始编写程序:

#ifndef BRASS_H_
#define BRASS_H_
#include <string>
// Brass Account Class
class Brass
{
private:
    std::string fullName;
    long acctNum;
    double balance;
public:
    Brass(const std::string & s = "Nullbody", long an = -1,
                double bal = 0.0);
    void Deposit(double amt);
    virtual void Withdraw(double amt);
    double Balance() const;
    virtual void ViewAcct() const;
    virtual ~Brass() {}
};

//Brass Plus Account Class
class BrassPlus : public Brass
{
private:
    double maxLoan;
    double rate;
    double owesBank;
public:
    BrassPlus(const std::string & s = "Nullbody", long an = -1,
            double bal = 0.0, double ml = 500,
            double r = 0.11125);
    BrassPlus(const Brass & ba, double ml = 500, 
		                        double r = 0.11125);
    virtual void ViewAcct()const;
    virtual void Withdraw(double amt);
    void ResetMax(double m) { maxLoan = m; }
    void ResetRate(double r) { rate = r; };
    void ResetOwes() { owesBank = 0; }
};

#endif

Brass类和BrassPlus类都声明了ViewAcct( )和Withdraw( )方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的,不同类对象使用的方法各自不同;

Brass类在声明ViewAcct( )和Withdraw( )时使用了新关键字virtual。这些方法被称为虚方法
(virtual method);Brass类还声明了一个虚析构函数,虽然该析构函数不执行任何操作。

此时,如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法

举个例子,如果没有用虚方法,例如:

//without virtual method
Brass dot;
BrassPlus dos;
Brass & r1=dot;//use Brass::
Brass & r2=dos;//use Brass::

我们在之前的学习知道,基类引用可以在不进行显式类型转换的情况下引用派生类对象。所以,此时引用类型均为Brass类,则都会使用Brass类型的方法。

如果使用了虚方法,例如:

//with virtual method
Brass dot;
BrassPlus dos;
Brass & r1=dot;//use Brass::
Brass & r2=dos;//use BrassPlus::

此时,引用r2的类型虽然是Brass,但是我们使用它的时候却是使用BrassPlus方法。

所以经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。

此外,银行还提出了条件,让我们编写构造函数的时候用。

  • Brass Plus账户限制了客户的透支款额。默认为500元,但有些客户的限额可能不同;银行可以修改客户的透支限额;
  • Brass Plus账户对贷款收取利息。默认为11.125%,但有些客户的利率可能不同;银行可以修改客户的利率;
  • 账户记录客户所欠银行的金额(透支数额加利息)。用户不能通过常规存款或从其他账户转账的方式偿付,而必须以现金的方式交给特定的银行工作人员。如果有必要,工作人员可以找到该客户。欠款偿还后,欠款金额将归零。

那我们由这些要求来编写构造函数和其他方法:

#include <iostream> 
#include "brass.h"
using std::cout;
using std::endl;
using std::string;

// formatting stuff
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f, precis p);

// Brass methods

Brass::Brass(const string & s, long an, double bal)
{
    fullName = s;
    acctNum = an;
    balance = bal;
}

void Brass::Deposit(double amt)
{
    if (amt < 0)
        cout << "Negative deposit not allowed; "
             << "deposit is cancelled.\n";
    else
        balance += amt;
}

void Brass::Withdraw(double amt)
{
    // set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    if (amt < 0)
        cout << "Withdrawal amount must be positive; "

             << "withdrawal canceled.\n";
    else if (amt <= balance)
        balance -= amt;
    else
        cout << "Withdrawal amount of $" << amt
             << " exceeds your balance.\n"
             << "Withdrawal canceled.\n";
    restore(initialState, prec);
}
double Brass::Balance() const
{
    return balance;
}

void Brass::ViewAcct() const
{
     // set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);
    cout << "Client: " << fullName << endl;
    cout << "Account Number: " << acctNum << endl;
    cout << "Balance: $" << balance << endl;
    restore(initialState, prec); // Restore original format
}

// BrassPlus Methods
BrassPlus::BrassPlus(const string & s, long an, double bal,
           double ml, double r) : Brass(s, an, bal)
{
    maxLoan = ml;
    owesBank = 0.0;
    rate = r;
}

BrassPlus::BrassPlus(const Brass & ba, double ml, double r)
           : Brass(ba)   // uses implicit copy constructor
{
    maxLoan = ml;
    owesBank = 0.0;
    rate = r;
}

// redefine how ViewAcct() works
void BrassPlus::ViewAcct() const
{
    // set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    Brass::ViewAcct();   // display base portion
    cout << "Maximum loan: $" << maxLoan << endl;
    cout << "Owed to bank: $" << owesBank << endl;
    cout.precision(3);  // ###.### format
    cout << "Loan Rate: " << 100 * rate << "%\n";
    restore(initialState, prec); 
}

// redefine how Withdraw() works
void BrassPlus::Withdraw(double amt)
{
    // set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    double bal = Balance();
    if (amt <= bal)
        Brass::Withdraw(amt);
    else if ( amt <= bal + maxLoan - owesBank)
    {
        double advance = amt - bal;
        owesBank += advance * (1.0 + rate);
        cout << "Bank advance: $" << advance << endl;
        cout << "Finance charge: $" << advance * rate << endl;
        Deposit(advance);
        Brass::Withdraw(amt);
    }
    else
        cout << "Credit limit exceeded. Transaction cancelled.\n";
    restore(initialState, prec); 
}

format setFormat()
{
    // set up ###.## format
    return cout.setf(std::ios_base::fixed, 
                std::ios_base::floatfield);
} 

void restore(format f, precis p)
{
    cout.setf(f, std::ios_base::floatfield);
    cout.precision(p);
}

几个构造函数都使用成员初始化列表语法,将基类信息传递给基类构造函数,然后使用构造函数体初始化BrassPlus类新增的数据项。

非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法

关于多态性,可以创建指向Brass的指针数组,这样,每个元素的类型都相同,但由于使用的是公有继承模型,因此Brass指针既可以指向Brass对象,也可以指向BrassPlus对象。这样就实现了多态性。


静态联编和动态联编

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。在C++中,由于函数重载的缘故,这项任务比较复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。

又因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamicbinding),又称为晚期联编(late binding)。

将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。该规则是is-a关系的一部分。

将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。

还有关于虚函数,构造函数不能是虚函数,因为虚构函数不继承;析构函数应当是虚函数,除非类不用做基类;友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。

如果派生类没有重新定义函数,将使用该函数的基类版本。


访问控制

我们已经学习了public和private来控制对类成员的访问。。还存在另一个访问类别,这种类别用关键字protected表示。关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来:派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。

即没有存在继承的情况下,一般保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。


继承和动态内存分配

如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧。

派生类不使用new

假设基类使用了动态内存分配,且定义了显式析构函数、复制构造函数和赋值运算符,则派生类则不需要定义显式析构函数、复制构造函数和赋值运算符。

派生类使用new

在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。


总结

继承通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码。公有继承建立is-a关系,这意味着派生类对象也应该是某种基类对象。派生类继承基类的数据成员和大部分方法,但不继承基类的构造函数、析构函数和赋值运算符。

派生类可以直接访问基类的公有成员和保护成员,并能够通过基类的公有方法和保护方法访问基类的私有成员。可以在派生类中新增数据成员和方法,还可以将派生类用作基类,来做进一步的开发。每个派生类都必须有自己的构造函数。

程序创建派生类对象时,将首先调用基类的构造函数,然后调用派生类的构造函数;程序删除对象时,将首先调用派生类的析构函数,然后调用基类的析构函数。

如果希望派生类可以重新定义基类的方法,则可以使用关键字virtual将它声明为虚的。这样对于通过指针或引用访问的对象,能够根据对象类型来处理,而不是根据引用或指针的类型来处理。基类的析构函数通常应当是虚的。


类重要技术复习

这里是本专题的最后部分,将总结类中重要的技术知识。类的内容很多而且比较复杂,可以看着总结来复习。这里不复习很基础的部分,基础部分重新看前面的章节。

构造函数

默认构造函数

如果没有提供任何构造函数,C++将创建默认构造函数。没有提供任何构造函数时,则编译器会提供默认的构造函数,它是一个不接受任何参数,也不执行任何操作的构造函数。它的形式是:

//declear
Class_Name();//in public of class
//define
Class_Name::Class_Name() { }

还有一种默认的构造函数,就是带有参数值,但是所有值均有默认值的形式

//declear
Class_Name(int a=0, double b=0.0);
//define
Class_Name::Class_Name(int a=0, double b=0.0){
...
}

这种形式我们可以在里面对对象进行赋值,进行初始化。

上面有两种默认构造函数的形式,但只能同时有一个默认构造函数。

普通构造函数

如果我们想自己写构造函数,给对象初始化,则我们写的是普通构造函数。普通构造函数可以和默认构造函数同构,但不能完全一样。构造函数不能是虚函数。

例如,下面这个就是一个简单的构造函数:

//declear
Line();
//define
Line::Line(void)
{
    cout << "Object is being created" << endl;
}

我们可以对其赋值,也许需要参数,可以这么写:

//declear in Class Line
public:
    Line(double len);
private:
    double length;
//define
Line::Line( double len)
{
    cout << "Object is being created, length = " << len << endl;
    length = len;
}

此时我们可以使用下面语句创建对象,并对其初始化赋值。

Line l=15.6;//means l.length=15.6

此时在构造函数内没有使用对象.成员的形式,是因为在公有方法里存在this指针,默认指向对象

注意:普通构造函数存在时,编译器不会自动生成默认构造函数。如果我们每次创建对象都希望对其进行初始化,而有时候并不赋值,有时候赋值的话,建议自己声明一个默认构造函数和一个普通函数,它们是重载的关系。

此外,在构造函数中,还使用初始化列表来初始化字段,例如上面的定义可以改为:

Line::Line( double len): length(len)
{
    cout << "Object is being created, length = " << len << endl;
}

此时,初始化的时候格式是函数体 : 成员(参数)函数体 : 基类构造函数(参数/或没有)

复制构造函数

复制构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。这么说可能有点模糊,它一般用于下面的情况:下面情况出现时都会使用复制构造函数

  • 通过使用另一个同类型的对象来初始化新创建的对象。例如Class_Name obj1=obj2;
  • 复制对象把它作为参数传递给函数。(对象作为参数,传值调用
  • 复制对象,并从函数返回这个对象。例如obj3=obj4; return obj;

如果在类中没有定义复制构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个复制构造函数。它的一般形式:

classname (const classname &obj) {
   // 复制构造函数的主体
}

编译器默认生成的复制构造函数是会根据其成员进行编写,对对象逐个复制非静态成员。这种复制方式叫浅拷贝。

如果我们的类成员中存在指针(包括字符串char*,数组指针等)或动态分配内存的时候,为了防止被提前释放指针空间或内存空间,我们一般需要自己编写复制构造函数,进行深拷贝

即我们在函数中应该再生成一个新的空间,再将原值赋值到新的空间,让复制后的对象指向新的空间。

析构函数

析构函数会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

//declear
~Line();
//define
Line::~Line(void)
{
    cout << "Object is being deleted" << endl;
}

我们经常拿虚构函数来释放空间,例如:

//declear
public:
    ~Line();
private:
    char *str;
    char var;
//define
Line::~Line(void)
{
    cout << "Object is being deleted" << endl;
    delete []str;//if new char[...] at beginning
    delete var;//if new char at beginning
}

虚析构函数

我们用关键字virtual将析构函数声明为虚的,一般在类继承中使用。

//declear
~virtual Line();
//define
Line::~Line(void)
{
    cout << "Object is being deleted" << endl;
}

运算符重载

运算符重载是一种形式的C++多态,赋予C++运算符多种含义。要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:

<返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}

例如,我们重载一个“+”,让它实现,两个对象相加,指两个对象的时间相加,返回一个对象。

//declear
Time operator+(const Time & t) const;

//define
Time Time::operator+(const Time & t) const  //相当于给加号赋予下面的语句进行重载
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

//use
total = coding + fixing;
//alse use by this
total = morefixing.operator+(total);

我们发现它的形式上和复制构造函数很像,因为此时我们要返回的是一个不能被修改的引用。我们把运算符重载函数作为类的成员函数的重载叫类内重载。

当我们把运算符重载函数当做友元函数的时候,这样的重载称为类外重载。

友元函数不是成员函数,却有和成员函数一样的访问权限。例如:

//define
friend Time operator*(double m, const Time & t)
{ return t * m; }   // inline definition

这是一个使用友元函数的情况。它需要注意下面的内容:

  • 虽然operator *( )函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
  • 虽然operator *( )函数不是成员函数,但它与成员函数的访问权限相同。
  • 注意声明放在放在类声明中!而且是在public里。
  • 定义的时候不需要用成员函数的定义法,即定义的时候不要使用Time::限定符。
  • 定义中不需要使用friend关键字

我们一般需要使用友元函数来做到一个普通类型<运算符>对象这种情况,因为对象<运算符>普通类型我们可以通过类内重载定义参数,而反过来则做不到。不过不用再写一次,我们可以直接想上面内联函数这么写,将他们位置调换。

我们最经典的例子就是重载<<运算符,作为cout输出了。我们一般用这样的格式:

//declear
friend std::ostream & operator<<(std::ostream & os, const Time & t);//返回ostream&
//define
std::ostream & operator<<(std::ostream & os, const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os; 
}

因为返回的是ostream &,即返回的还是cout,我们就可以继续嵌套输出。


类型转换

其他类型 to 对象

我们在给对象赋值的时候可能像让它和普通变量一样赋值,例如:

//declear
class tmp
{
public:
    tmp(double);
private:
    double v;
}
//define
tmp::tmp(double d)
{
    v=d;
}

那我们在main函数中:

tmp t;
t=1.23;//OK

像普通变量一样赋值,此时发生了隐式转换,即自动生成了一个临时对象,执行临时对象(1.23),再把这个临时对象赋值给对象t。这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换

当然只有一个参数的构造函数才能作为转换函数,那如果构造函数中有若干个变量,只有一个变量没有默认值,也是可以的。

但是这样的转换有时候会出现问题,C++新增了关键字explicit可以让它强制显式转换。即构造函数的声明要改为:

explicit tmp(double);

这样的转换叫显式强制类型转换

对象 to 其他类型

此时我们需要使用特殊的运算符函数——转换函数。转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。

它的形式如下:

operator typeName();

很特殊的是:

  • 转换函数必须是类方法;
  • 转换函数不能指定返回类型;
  • 转换函数不能有参数。

typename指出了要转换成的类型,因此不需要指定返回类型。因为它是类方法,所以它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。

例如我们想对象返回上面的double型的数据,我们可以这么写:

//declear
operator double();
//define
tmp::operator double()
{
    return v;//这个函数我们不用定义它的返回类型,但它有返回值
}

在使用中我们就可以做到:

double value;
tmp t(1.23);
value=t;

同样的我们可以对它进行强制显式转换,就是:

explicit operator double();

类的继承

多态继承,我们主要记住:

  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象
  • 类引用可以在不进行显式类型转换的情况下引用派生类对象
  • 当我们使用virtual关键字标记的时候,程序对象优先,不然则类型优先。

具体例子就前面已经讲述得比较清楚了。


后记

这就是C++类专题的全部部分了,复习部分只包括了重要技术,之后关于考试内容中类的知识还会在期末复习专题再涉及一点。

Reference:《C++ Primer Plus 6th》第10-13章

这里的一切都有始有终,却能容纳所有的不期而遇和久别重逢。
最后更新于 2024-01-14