使用类

使用类

运算符重载

格式如下:

operatorop(argument_list)

例如operator+()这将重载“+”运算符。如果A、B、C都是类D的对象就可以这样用:A=B+c。其中,运算符左侧的对象是调用对象,右侧的则是作为参数被传递的对象。
重载限制:

  1. 重载后的运算符必须至少有一个操作数是用户定义的类型
  2. 使用运算符时不能违反运算符原来的句法规则,同样,不能修改运算符的优先级
  3. 不能创建新运算符
  4. 不能重载某些运算符
  5. 某些运算符只能通过成员函数重载的运算符

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

例1
  • 自动转换 int i=3.3 最终i会自动转换成3
  • 强制类型转换 int i=int(3.3) i最终也为3.3
    对于类来说也是如此, 将类放到与int,double相同的高度上,规则大体相同。
例2

假设Stonewt类有一个构造函数Stonewt(double lbs)//可加explicit关闭隐式转换

Stonewt example//声明一个对象
example=1.1//自动(隐式)转换
example=Stonewt(1.1)//强制(显式)转换

现在我们将类对象转换成特定的数据类型,那么,反过来呢?
我们需要转换函数
注意:

  1. 转换函数必须是类方法
  2. 转换函数不能指定返回类型
  3. 转换函数不能有参数
例3
class{
    .
    .
    .
    .
    .
Public:
    operator int() const;//转换函数//可在前面加上关键字explicit来关闭隐式转换
    .
    .
    .
};
Stonewt::operator int() const
{
    Return int (pound+0.5)
}

类和动态内存分配

class{
    Staic int num_strings;//静态储存类
    .
    .
    .
    .
}

它有一个特点,无论创建了多少个对象,程序都只创建一个静态类变量副本,也就是说所有的类对象共用一个静态成员。它不能在类中声明,因为这样会分配内存而类不可,不好在头文件中声明因为这会导致多次声明,最好在表示方法的文件中声明。
Such as:int stringbad::num_strings=0;
很少为人所知的是,编译器会帮我们生成许多自动生成的成员函数,从而造成一些意料之外的结果。如:复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中,它的原型通常如下:
Class_name(const Class_name &);
为什么隐式复制构造函数在一些情况下会出问题?
首先, 对于一些静态储存类,复制构造函数的执行不会对它造成改变,而有的时候正是需要
它的改变,这时候就会出问题。其次,一些类成员使用的是new初始化的,指向数据的指针,而不是数据本身。所以当对对象使用delete操作时,新构造出的函数很容易就会受到牵连。
下面给出一个复制构造函数的例子:

StringBad::StringBad(const StringBad &st)
{
    Num_strings++;
    Len=st.len;
    Str=new char [len+1];
    Std::strcpy(str,st.str);
    Cout<<num_strings<<":\""<<str
    <<"\"object created\n";
}

有关默认的赋值运算符的问题

与复制构造函数类似,当存在两个已经初始化的对象,我们想将一个对象的值赋给另一个时,默认的赋值运算符就会起作用,其作用机理与复制构造函数相同,产生的问题也大致相同,解决的方法也相似,下面提供赋值运算符(进行深度复制)定义。
e.g:

StringBad & StringBad::operator=(const StringBad &st)
{
    If(this==&st)
        Return *this;
    Delete [] str;
    Len=st.len;
    Str=new char[len+1];
    Std::strcpy(str,st.str);
    Return *this;
}

比较成员函数

e.g:

Bool operator<(const String &st1,const String &st2)
{
    if(std::strcmp(st1.str,st2.str)<0)
        return true;
    else
        return false;
}

使用中括号表示法访问字符

总的说来就是重载[]运算符,为了确保不修改数据,可以用Const char & string::operator[](int i) const的形式来声明函数。

静态类成员函数

我们可以将成员函数声明为静态的static int Howmany(){ return num_strings;}。这样做的后果是,不能通过对象来调用此函数,此函数也只能使用静态数据成员。
使用方法:String::Howmany();其中String为类名,Howmany为函数名

使用指向对象的指针

ps:cpp经常使用指向对象的指针
e.g:
string * favorite = new String(sayings[choice]);

再谈定位new运算符

可以用定位new运算符声明对象
Such as:pc1=new (buffer) JustTesting;
但要注意的是,如果是在同一块内存块上声明对象,不要使它们相互重叠而造成错误。还有就是对于用定位new运算符创建的对象要显示的调用析构函数
e.g:pc3->~justtesting();//justtesting为pc3所指向的类的名称。
还需注意的是晚创建的应该先删除

Chapter 13 && Chapter 14

几种继承的方法

  • 公有继承(is-a关系):
    eg:
class A:{
    ........
};

如何继承?

Class B:public A
{
    ......
};

即可,当然其中构造函数有些许的不同。
稍特殊情况:多态公有继承
即方法的行为取决于调用该方法的对象
两种方法:

  1. 在派生类中重新定义基类的方法
  2. 使用虚方法
  • 私有继承
    基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们
    e.g
Class student:private std::string,private std::valarray<double>
{
public:
    .............
};
  • 保护继承
    保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:
    e.g:
Class student:protected std::string,
Protected std::valarray<double>
{
    ........
};

使用保护继承时,基类的公有成员都将成为派生类的保护成员。

关于公有继承的具体细节

  1. 派生类构造函数必须使用基类构造函数
  2. 基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示
    类型转换的情况下引用派生类对象,反之,派生类指针不能指向基类对象。但基类指针或引用
    只能用于调用基类方法。
  3. 关于多态公有继承的问题:在派生类中声明了一个基类中已经存在的方法,如show()函
    数,在show()函数中又调用基类的show()函数,这很自然,可以减少工作量,要记得使
    用作用域解析运算符,不然会是一个无穷尽的递归函数。
  4. 虚析构函数的重要性
    若派生类中有动态分配内存的一些操作,就需要基类有虚析构函数,这样可以确保程序首先调
    用派生类中的析构函数,再调用基类的析构函数,使内存可以正常的释放。
  5. 关于重新定义将隐藏方法的问题
    e.g:
Class Dwelling
{
public:
    Virtual void showperks(int a) const;
    ...
};
Class Hovel:public Dwelling
{
public:
    Virtual void showperks() const;
    ....
};

下面的showperks函数将隐藏上面的,这是很自然的。但是有时候很容易出现问题。当你想
调用上面的某个函数时,你就发现不能调用了。所以,如果要保证不出现此类问题,最好的方
法是把上一个类的虚方法的所有重载的版本在这个类中也全部写一遍。

访问控制:protected

我们已经用过public和private来控制对类成员的访问,其实还存在一种访问类别,那就是protected。protected与private较为相似。对于外部世界来说(相对的),它们都是不可访问的。但是派生类的成员可以直接访问protected中的成员。
警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派
生类能够访问基类数据。

抽象基类

抽象基类也是为了表示一种特定的关系而存在着的。比如我们有篮球和足球这两种球,它们可以独立成两个类别,但它们有很多共性,那么我们就可以使用一个叫做‘球’的类来作为抽象基类,再通过球类来派生出篮球类和足球类。
e.g

class AcctABC
{
private:
    std::string fullName;
    long acctNum;
    double balance;
protected:
    struct Formatting
    {
        std::ios_base::fmtflags flag;
        std::streamsize pr;
    };
    const std::string &FullName() const { return fullName; }
    long AcctNum() const { return acctNum; }
    Formatting SetFormat() const;
    void Restore(Formatting &f) const;
    public:
    AcctABC(const std::string &s = "Nullbody", long an = -1, double bal = 0.0);
    void Deposit(double amt);
    virtual void Withdraw(double amt) = 0;
    double Balance() const { return balance; };
    virtual void ViewAcct() const = 0;
    virtual ~AcctABC() {}
};
class Brass :public AcctABC
{
public:
    Brass(const std::string &s = "Nullbody", long an = -1,
    double bal = 0.0) :AcctABC(s, an, bal) {}
    virtual void Withdraw(double amt);
    virtual void ViewAcct() const;
    virtual ~Brass() {}
};
class BrassPlus : public AcctABC
{
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.10);
    BrassPlus(const Brass &ba, double ml = 500, double r = 0.1);
    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; }
};

其中AcctABC是抽象类,Brass和BrassPlus是具体类。AcctABC包含Brass和BrassPlus类共有的所有方法和数据成员,而那些在BrassPlus类和Brass类中的行为不同的方法应被声明为虚函数。至少应有一个虚函数是纯虚函数,这样才能使AcctABC成为抽象类。抽象基类的特点,抽象基类的作用相对于公有继承的基类来说作用有点单调,它只能提供一个垫板,本身作为类并不能声明对象。并且它一定要有纯虚函数的存在,它的纯虚函数可以定义也可以不定义,如果它的派生类没有定义纯虚函数,那么它也成为了抽象基类,知道有的类定义了这些函数,那么它就成为了实体类,可以声明对象。

继承和动态分配内存的细节

  1. 派生类不使用new
    这种方式的复杂度可以说是最低了(如果有更低的,原谅我的孤陋寡闻),不需要进行一些所谓的额外的操作。
  2. 派生类使用new
    在这种情况下,必须为派生类定义显示析构函数、复制构造函数和赋值运算符。派生类析构函数自动调用基类的析构函数,故其自身的职责是清除自身调用的内存。派生类的复制构造函数必须调用基类的复制构造函数来处理基类的数据。对于显示赋值运算符也是如此。
    如何使用基类的友元函数?
    解决方法是使用强制类型转换
    示例代码块:
std::ostream&operator<<(std::ostream &os,const hasDMA & hs)
{
    os<<(const baseDMA &) hs;
    os<<“Style:<<hs.style <<endl;
    Return os;
}

如上例所示,想要使用的基类的重载<<运算符的友元函数,只要加上(const baseDMA &)即可。

包含、组合或层次化

使用这样的类成员,本身是另一个类的对象
e.g:

Classs student:
{
    private:
    String name;
    Valarray<double> scores;
    ...
};

使用构造函数时,对于对象成员的处理:使用初始化列表,调用对象名即可而不是对象的类名。假定scores是一个对象成员,要调用该成员的方法,只需scores.function()即可。

私有继承

使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味这基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
使用构造函数:
e.g:

Student(const char *str, const double * pd,int n)
:std::string(str),ArrayDb(pd,n){}

使用私有继承时将使用类名和作用域解析运算符来调用方法。
访问基类对象(强制类型转换)

Const string &student::name() const
{
    Return (const string &) *this;
}

访问基类的友元函数
用类名显示地限定函数名不适合友元函数,这是因为友元不属于类。然而,可以通过显示地转换为基类来调用正确的函数。
e.g:

Ostream &operator<<(ostream &os, const student &stu)
{
    os<<“scores for<<(const string &)stu<<:\n“;
    .....
}

保护继承

保护继承是私有继承的变体。使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类中派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
使用using来重新定义访问权限
e.g
Using std::valarray::min;
这使得valarray::min可用,就像它们是student的公有方法一样

多重继承的麻烦

比如说有一个worker的基类,从其中派生出singer类和waiter类,至此,整个代码是没有问题的,singer和waiter都获得了worker的一些数据成员,它们统称为work。当我们从singer和waiter中派生出一个singerwaiter类时,问题就出现了。首先,singerwaiter将有两个有关于worker的数据,那么,进行赋值时要把值赋给谁呢?cpp解决了这个问题,引入了虚基类(virtual base class)
e.g:

Class singer: virtual public Worker {...};
Class wiater: public virtual worker {...};

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。特别地使用构造函数

singerwaiter(const worker &wk, int p=0, int v=singer::other)
:worker(wk),waiter(wk,p),singer(wk,v){}

worker是虚基类,则singerwriter使用构造函数时要显式的使用worker的构造函数。还有就是方法的问题,如果singer和waiter都使用了show()方法,那么singerwaiter要使用哪个show()方法呢?
可以用作用域解析运算符来澄清编者的意图
Newwhire.singer::show();
还有一种方法可以解决这个问题
e.g

void Worker::Data() const
{
    cout << "Name: " << fullname << "\n";
    cout << "Employee ID: " << id << "\n";
}
void Waiter::Data() const
{
    cout << "Panache rating: " << panache << "\n";
}
void Singer::Data() const
{
    cout << "Vocal range: " << pv[voice] << "\n";
}
void SingingWaiter::Data() const
{
    Singer::Data();
    Waiter::Data();
}
void SingingWaiter::Show() const
{
    cout << "Category: singing waiter\n";
    Worker::Data();
    Data();
}

这样,singingwaiter的show函数就能完美地完成任务了。但要记得将上述基类的data函数
设置为受保护的,这样可以放置不被当前的类对象调用。

类模板

e.g

template <class Type>
class Stack
{
private:
    enum { MAX = 10 };
    Type items[MAX];
    int top;
    public:
    Stack();
    bool isempty();
    bool isfull();
    bool push(const Type &item);
    bool pop(Type &item);
};
template <class Type>
Stack<Type>::Stack()
{
    top = 0;
}
template <class Type>
bool Stack<Type>::isempty()
{
    return top == 0;
}
template <class Type>
bool Stack<Type>::isfull()
{
    return top == MAX;
}
template <class Type>
bool Stack<Type>::push(const Type &item)
{
    if (top < MAX)
    {
        items[top++] = item;
        return true;
    }
    else
        return false;
}
template <class Type>
bool Stack<Type>::pop(Type &item)
{
    if (top > 0)
    {
        item = items[--top];
        return true;
    }
    else
        return false;
}

上述提供了类模板的一个例子,类模板函数最好与类在同一个文件中。
使用类模板的例子:

Stack<int> kernels;
Stack<string> colonels;

如果要使用指针和涉及动态内存分配的东西,可能需要重新设计一下类模板。我们在上面已经可以看到template 的存在了,实际上,类模板可以使用的不仅仅时Typename这一个参数,比如,它可以是template,n可以称为时表达式参数。表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。另外,模板代码不能修改参数的值,也不能使用参数的地址。
模板多功能性
e.g

Array< Stack<int> > asi;
Array< Array<int,5>, 10> twodee;

可以使用多个类型参数
e.g
Template
可以为类型参数提供默认值
Template
模板具体化

  1. 隐式实例化
    不做额外的操作
  2. 显示实例化
    声明必须位于模板定义所在的名称空间中
    E.g
    Template class ArrayTP;
    在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。
  3. 显示具体化
    提供一个为具体类型定义的模板
    e.g
Template <> class SortedArray<const char char*>
{

}

这样就可以专门地为该种数据类型设计函数。
4. 部分具体化
即部分限制模板的通用性
e.g
Template class Pair {};

  • 成员模板
    一个模板类将另一个模板类和模板函数作为其成员
    将模板用作参数
    e.g
    Template