C++面向对象编程一些拾遗

虽然说是拾遗,但是这里有一大部分是我以前没有看过的,简书的markdown不支持生成目录,可能需要手动来一个了。

一. this指针

当我们面对对象的时候,很容易能看到这个对象里面的数据成员以及成员函数,那么这个对象本身呢?这就是this指针了。每一个对象都有一个this指针,指向自己的地址。this指针并不是对象的一部分,this指针所占的内存大小是不会反应在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型。

另外,this指针只能在成员函数中使用,全局函数或者静态函数都不能使用this指针,原因也很明显,静态成员本身并不是对象的属性。

Q1: this指针是什么时候创建的? this在成员函数的开始执行前构造的,在成员的执行结束后清除。

Q2:this指针如何传递给类中函数的?绑定?还是在函数参数的首参数就是this指针? this指针是作为首参传递给成员函数的,this指针在对象实例后就生成了,在调用前生成。并且并不需要显示的传递this指针。

Q3:this指针的类型? this指针的类型是: 类名 *const;

为什么this指针是必须的呢?

1. 可以当做函数的返回类型。

当我们希望一个成员函数的返回类型是对象本身时,可以通过返回this指针来达到这个目的。

2. 级联操作使用this指针。

比如我们有下面一个类,Ball类的四个成员函数分别控制Ball的移动。

class Ball   //BallL类,用来描述一个小球的移动
{
public:
void moveLeft(int dept);  //向左移动函数
void moveRight(int dept); //向右移动函数
void moveDown(int dept);  //向下移动函数
void moveUp(int dept);    //向上移动函数
}

理想情况下我们希望可以给用户提供下面形式的命令方式:ba.moveLeft(1).moveDown(2); 等价于:

ba.moveLeft(1);
ba.moveDown(2);

在这种需求下,那么函数就必须有一个返回值是对象本身,这个时候this指针就很好用:

class Ball   //BallL类,用来描述一个小球的移动
{
public:
Ball & moveLeft(int dept);  //向左移动函数
Ball & moveRight(int dept); //向右移动函数
Ball & moveDown(int dept);  //向下移动函数
Ball & moveUp(int dept);    //向上移动函数
}

每个函数都要返回对象的引用,这个时候用this指针就好了!

all & Ball::moveLeft(int dept)
{
  //移动当前的位置,省略代码
  return *this;
}

二. 友元。

友元有三种: 友元函数(非成员函数) 友元函数(成员函数) 友元类

1. 友元函数。

一个函数虽然不是类的成员函数却需要访问类的所有成员,这样的函数可以定义为类的友元函数。

#include <iostream>

using namespace std;

class A
{
public:
    friend void set_show(int x, A &a);      //该函数是友元函数的声明
private:
    int data;
};

void set_show(int x, A &a)  //友元函数定义,为了访问类A中的成员
{
    a.data = x;
    cout << a.data << endl;
}
int main(void)
{
    class A a;

    set_show(1, a);

    return 0;
}

2.友元类。

当需要一个类去访问另一个类的所有成员时,可以将此类声明为另一个类的友元类。当A是B的友元类时,A可以访问B类的所有成员,包括私有成员和保护成员。 三点需要注意: 1) 友元关系是单向的,A是B的友元,B 不一定是A的友元。 2) 友元关系不能够被继承。 3) 友元关系不具有传递性。

3. 友元成员函数

可以把一个类的成员函数声明为另一个函数的友元,值得注意的是,如果需要把B的成员函数声明做A的友元,首先需要声明类A,然后定义类B,B的成员函数要使用A,然后再去定义类A,定义了类A之后才能去定义B中的成员函数(提前声明),因为只有定义了类A在B的成员函数中才能使用A的成员。

#include <iostream>

using namespace std;

class A;    //当用到友元成员函数时,需注意友元声明与友元定义之间的互相依赖。这是类A的声明
class B
{
public:
    void set_show(int x, A &a);             //该函数是类A的友元函数
};

class A
{
public:
    friend void B::set_show(int x, A &a);   //该函数是友元成员函数的声明
private:
    int data;
    void show() { cout << data << endl; }
};

void B::set_show(int x, A &a)       //只有在定义类A后才能定义该函数,毕竟,它被设为友元是为了访问类A的成员
{
    a.data = x;
    cout << a.data << endl;
}

int main(void)
{
    class A a;
    class B b;

    b.set_show(1, a);

    return 0;
}

三.拷贝构造函数和赋值操作符。

拷贝构造函数在以下几种情况下会调用拷贝构造函数:

  1. 利用一个对象作为参数去初始化另外一个对象。
Sales_item b("1234");
Sales_item c(b);      //调用拷贝构造函数  
Sales_item d=b;     //调用拷贝构造函数    
Sales_item e;
e=b;            // 赋值构造函数
  1. 动态创建对象
Sales_item *p=new obj(b);  //调用拷贝构造函数
  1. 函数按值传递。
void fun(Sales_item s);

实参传递会调用拷贝构造函数,用引用的话不用。

  1. 标准库容器使用的时候。
vector<Sales_item>  v_s(4,b);

这种情况下会调用四次拷贝构造函数。但是使用数组的时候不会这样。

#include<iostream>
#include<string>
#include<vector>
 
using  namespace std;

class sales_item {
public:
    sales_item() :units_sold(0),revenue(0.0) {}
    sales_item(const string &book) :isbn(book), units_sold(0) { cout << "构造函数被调用!" << endl; }
    //拷贝构造函数,或者是复制构造函数
    sales_item(const sales_item &orig)
        :isbn(orig.isbn),
        units_sold(orig.units_sold),
        revenue(orig.revenue) {
        cout << "拷贝函数被调用!!" << endl; //通常来说,拷贝构造函数是不用写函数体的
    }
    //复制构造操作符
    sales_item& operator = (const sales_item &rhs)
    {
        isbn = rhs.isbn;
        units_sold = rhs.units_sold;
        revenue = rhs.revenue;
        cout << "赋值操作符被调用" << endl;
        return *this;
    }

private:
    std::string isbn;
    unsigned units_sold;
    double revenue;
};

void func(sales_item s)
{
    cout << "函数传值" << endl;
}

int main()
{
    sales_item a("1234");   //构造函数
    sales_item b = a;       //拷贝构造
    sales_item c(a);        //拷贝构造
    sales_item e;
    e = a;                  //赋值操作符
    sales_item *p = new sales_item(a);   //拷贝构造函数被调用
    
    cout << "函数调用" << endl;
    func(a);          //拷贝构造函数被调用

    cout << "vector容器:" << endl;
    vector<sales_item> v_sales(5, a);   //这样使用容器的时候也会发生拷贝构造

    cout << "数组" << endl;            //这里会调用构造函数而不会调用拷贝构造
    sales_item primes_eds[] = { string("dasjkhjfka"),
                            string("dajgakdfasf"),
                            string("dasdasfdfsd") };
    return 0;
}

赋值操作符在用等号初始化对象的时候会发生!详见示例代码!

noteL: 一般而言是不需要我们自己写拷贝构造函数和赋值操作符的,C++编译器会自动帮我们生成这样的功能函数,但是有一种情况我们必须定义自己的拷贝构造函数和赋值操作符,那就是:当数据成员有指针的时候 当数据成员有指针的时候,合成拷贝构造函数在进行拷贝的时候会把一个对象的指针拷贝到另外一个对象的指针,这样的话两个对象的指针就指向了同一个内容,修改一个对象的指针指向的内容,另外一个对象也受到了影响,在某些情况下这样的操作我们显然是不希望看到的,这个时候我们就需要定义自己的拷贝构造函数和赋值操作符。 具体的做法是取出指针里的内容,用其重新动态申请一片内存存入,然后再赋值给新对象的指针。

class NoName {
public:
    NoName():pstring(new std::string),i(0),d(0.0){}  //无参构造函数,指针指向一个动态创建的字符串
    NoName(const NoName &other):pstring(new string(*(other.pstring))),   //取出内容用其new一个字符串,然后再拷贝
                                        i(other.i),
                                        d(other.d){
        cout << "NoName 的拷贝构造函数被调用" << endl;
    } 
    NoName &operator=(const NoName &rhs)
    {
        pstring = new string(*rhs.pstring);
        i = rhs.i;
        d = rhs.d;
        cout << "赋值操作符被调用!" << endl;
        return *this;
    }
private:
    std::string *pstring;
    int i;
    double d;
};

一般而言,拷贝构造函数和赋值操作符,要么都写,要么都不写,这个一般都是同步的。

四.析构函数。

析构函数和构造函数是一对,构造函数用来创建对象,析构函数用来毁灭对象。构造函数一旦写了,C++就不会合成构造函数,而且构造函数可以重载,析构函数则只能写一个,而且即使我们写了自己的析构函数,C++还是会有一个析构函数。 什么时候一定需要自己写构造函数和析构函数呢?

  1. 需要构造的时候打开文件,析构的时候关闭文件。
  2. 需要构造的时候动态分配内存,析构的时候回收动态内存。 可能还有其他的情况。

三原则: 如果写了析构函数,那么拷贝构造函数和赋值操作符都必须写上。 五原则: 如果需要拷贝构造函数,也需要赋值操作符,反之亦然,但是无论拷贝构造函数还是赋值操作符的必要性都不一定意味着析构函数的必要性。

所以,当我们决定一个类是否需要定义它自己版本的拷贝控制成员时,一个原则是首先考虑其是否需要一个析构函数,通常,对析构函数的需求比对拷贝构造函数和赋值运算符的需求更为明显,如果需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个赋值运算符。

其实很容易明白为什么需要析构的时候一般会需要一个拷贝构造函数和赋值构造函数,比如我们的类里面有指针,构造的时候我们给其分配了动态内存,所以我们定义了自己的析构函数以便在析构的时候销毁内存。如果我们不定义自己的拷贝构造函数和赋值操作符,就会引发严重错误:这些函数简单拷贝指针成员,就会导致多个对象的指针指向同一片内存空间,当我们使用自己的析构函数时,一个对象被析构的时候可能导致其他对象的指针成员称为野指针(因为这片空间被释放掉了)。这个时候就需要特别注意了!

使用default 如果我们希望显式地要求编译器提供合成版本的拷贝控制器,可以使用default来做这件事。

class sales_data{
public:
sales_data()=default;
sales_data(const sales_data &)=default;
sales_data &operaotr=(const sales_data &);
~sales_data()=default;
};
sales_data &sales_data::operaotr=(const sales_data &)=const;

如果我们在类内定义为default,则其时内联的,我们也可以在内外定义(比如上面的赋值操作符),则不是内联的,要狐疑的是,我们只能对具有合成版本的成员函数使用default操作,即构造函数和拷贝控制成员。

五.阻止拷贝。

特殊的需求下,类必须采用某种机制阻止拷贝或者复制,比如iostream类,以避免多个对象写入或者读取相同的IO缓冲,为了阻止拷贝,看起来只要不定义这些操作就可以了,但是实际上即使这样编译器还是会默认的来合成。有几种方式可以阻止拷贝。

  1. 定义删除的函数。 新标准下,我们可以将拷贝构造函数和赋值运算符定义为删除的函数(deleted function)来阻止拷贝,删除的函数的意思是:我们虽然定义了他们,但不希望以任何形式来使用他们。
class sales_data{
public:
sales_data()=default;
sales_data(const sales_data &)=delete;    //阻止拷贝
sales_data &operaotr=(const sales_data &)=delete;   //阻止赋值
~sales_data()=default;
};

与default不同,我们可以把任何成员函数定义成delete的(析构函数除外),虽然一般而言我们只是在控制拷贝的时候才是用delete,但是,确实是可以这么做,希望引导函数匹配的过程时,也可以把一些函数设置成delete。 一旦析构函数被设置成delete的,就无法销毁此类型的对象的,编译器将不允许该类型的变量或者创建该类的临时变量。

合成的拷贝控制成员可能是被删除的:如果类有数据成员不能默认构造,拷贝,复制或者销毁,那么,对应的成员函数被定义成删除的。

  1. 可以通过将拷贝构造函数或者赋值运算符声明为private的来阻止拷贝。 这样是可以理解的,因为对象并不能直接访问类的私有成员,可以通过这样的操作来阻止拷贝。

六. 深拷贝,浅拷贝(位拷贝)(深复制和浅复制)

为了说明这个问题,我们写一个简单的类:

class Cdemo {
public:
    Cdemo(int pa, char *str)
    {
        this->a = pa;
        this->str = new char[100];   //动态分配空间
        strcpy(this->str, str);      //然后把字符串拷贝过来
    }
    //private:  应该是私有的,为了测试方便,设计为共有的
    //不写拷贝构造函数的话,就会生成一个拷贝构造函数
public:
    int a;
    char *str;
};

我们简单使用一下:

    Cdemo A(10, "hello");
    Cdemo B(A);     //使用默认的构造函数
    cout << A.a << "," << A.str << endl;
    cout << B.a << "," << B.str << endl;

运行结果如下:

继续:

        cout << "修改后:" << endl;
    B.a = 8;
    B.str[0] = 'k';
    cout << A.a << "," << A.str << endl;
    cout << B.a << "," << B.str << endl;

结果:

这就是一件很恐怖的事了,我们改了B的字符串,A的也被改掉了,这就是因为深复制和浅赋值的区别导致的:

也就是说,自动合成的构造函数是一个很简单的构造函数,对于指针类的成员,就把指针简单复制过来了,两个指针指向的是同一个字符串,这样的拷贝就是浅复制。 如果要进行深复制,我们需要自己定义拷贝构造函数:

Cdemo(const Cdemo & cd)
    {
        this->a = cd.a;
        this->str = new char[100];    //动态分配内存
        if (str != 0)
            strcpy(this->str, cd.str);    //然后把原来的内容拷贝过来
    }

结果:

所以,一般而言,如果我们一个类中如果有动态分配的内存,或者调用了系统的资源,我们都应该自己定义拷贝构造函数来进行深复制。这个就是刚才在上面说的,如果进行了浅复制,析构一个对象会导致另外一个对象的指针成员称为野指针。为了避免这一情况,需要管理指针成员。

七. 管理指针成员。

如何避免悬垂指针:使用智能指针或考虑用深复制。

但我们并不总是想要进行深复制,对于占用空间较大的对象来说,进行值复制(深复制)会占用内存资源,并且复制也会带来计算消耗。 关于智能指针的使用可以参考智能指针的使用方式,这里不说了,一定要理解这一套逻辑。

八.继承和派生。

为了避免写大量的重复代码以及提高程序的可读性,C++提供了继承机制。 简单来说,允许一个类继承另外一个类的成员来当做在自己的一部分来组成一个新的类,这种关系通常被描述为继承和派生。 被继承的类成为基类,继承的类成为派生类。

继承一共有三种:公有继承,保护继承,私有继承

  • 公有继承: 相当于是直接复制下来的,成员属性是不变的。
  • 保护继承: 公有成员和保护成员变为保护成员,私有成员属性不变。
  • 私有继承: 所有继承来的成员变为自己的私有成员。

保护成员: 这个是专门为继承来设计的,对于当前类来说,相当于私有成员,自己可以使用,类外无法使用。对于派生类来说,私有成员被继承之后在派生类中是无法使用的,所以设计了保护成员来继承,公有继承之后还是保护的,在派生类中可以使用,而且可以继续派生,所以说保护成员是为了继承而生的,这个说法也一点都不为过。

note: 构造函数和析构函数是不能被继承的!!!正因为如此,我们还需要研究派生类的构造函数和析构函数。

九.派生类的构造函数和析构函数。

  1. 派生类的构造函数。 派生类构造前,会先调用基类的构造函数来构造继承来的成员,当一个派生类有多个基类时,那么按照类定义的时候的继承顺序来依次调用基类的构造函数。总的来说:
  • 执行基类的构造函数,当有多个基类时,按照类定义时的继承顺序来。
  • 执行成员对象的构造函数,当类有成员是对象时,构造完基类后,会调用成员对象的构造函数进行构造。
  • 执行派生类的构造函数。
  1. 派生类的析构函数。 和对象构造的时候刚好是相反的顺序。
  • 对派生类的新增普通成员进行清理。
  • 调用成员对象的析构函数。
  • 调用基类析构函数。

十.继承和虚函数。

1. 覆盖基类的函数。

如果我们觉得继承来的函数并不适合当前的类,而且我们确实需要一个适合当前类的同名函数,一种方法可以通过重写来覆盖掉继承来的函数,这种称之为覆盖基类函数。 但是覆盖的时候有可能把基类的函数给隐藏了。 eg:DOG类中的speak函数就是把基类的speak函数给覆盖掉!

enum BREED
{
    GOLDEN,
    CAIRN,
    LAB,
    TADI
};

class Mammal {
public:
    Mammal() {
        cout << "inline 构造函数被调用!" << endl;
    }
    ~Mammal() { cout << "inline的析构 函数被调用" << endl; }

    void Speak() const { cout << "mammal的叫声!" << endl; }
    void Sleep() const { cout << "shh,i am sleeping!" << endl; }
protected:
    int itsAge;
    int itsWeight;
};


class Dog :public Mammal {
public:
    Dog() { cout << "dOG的构造函数被调用!!" << endl; }
    ~Dog() { cout << "Dog的析构函数被调用" << endl; }
    void WagTail() const { cout << "Tail wagging!" << endl; }
    void begFood() const { cout << "i am begging for food!" << endl; }
    void Speak() const { cout << "woof!!!woof!!" << endl; }        //把继承来的覆盖掉,我们认为狗的叫声和哺乳动物是不一样的!
private:
    BREED itsBreed;
};

2. 隐藏基类的函数。

当基类包含多个同名成员函数时,派生类重写一个时会把其他的成员函数隐藏掉,这种情况叫做隐藏基类的函数。 比如:我们在mammal中增加两个成员函数。

class mammal{
public: 
//其他成员函数
void Move() const { cout << "Mammal move one step!" << endl; }
void Move(int distance) const
{
    cout << "Mammal move " << distance << " steps" << endl;
}
}

并在DOG类中重写其中的一个:

class dog{
public: 
//其他成员函数
void Move() const { cout << "Mammal move one step!" << endl; }
}

这种时候DOG的对象就不能再去调用Move(int)的函数了(如果这么做编译是通不过的),这种情况就是称作被隐藏掉了。当然我们可以通过重写所有的函数来避免这种情况,不过是有点太麻烦了!可以通过写上基类的名字来调用。

dog dd;
dd.mammal::Move(3);
//这样是可以的!

3. 子类型关系

定义为: 有一个特定的类型S,当且仅当它提供类型T的行为时,成S为类型T的子类型。 共有继承可以实现子类型关系,及派生类是基类的子类型,子类型关系具有传递性但不可逆。 子类型关系有一些兼容规则:

class base {

public:
    void print() const { cout << "Base PRINT 被调用!" << endl; }
};
class Derivedl :public base
{
    void print() const { cout << "派生类的print被调用" << endl; }
};

void fun(const base &ba)   //函数接受基类的引用
{
    ba.print();
}

定义一个基类及其派生类,并且定义一个函数接受基类的引用。那么下列的使用都是合法的。

base ba;
fun(ba);
Derivedl zi;
fun(zi);           //这个函数接受的是基类,传入派生类也可以,但是在print的时候却是基类的print,理想情况下我们是应该想要

ba = zi;           //这两个是可以相等的。可以用派生类赋值基类
    
base &ref = zi;    //可以用基类的引用来引用派生类

简单的来讲,就是你爸爸能去的地方你都能去!

4. 多态和虚函数。

多态的意思就是多种形态,当调用成员函数时,编译器会根据不同的对象类型来选择不同的成员函数来调用。 在前面的例子中我们看到了,当派生类有包含基类同名函数时,基类的同名函数可能会被隐藏或者覆盖,并且当具有子类型关系时,接受基类的函数传入派生类的对象认为调用基类的函数,这个时候,也需要使用多态来保证是我们想要的结果。 实现多态要使用虚函数。 比如,我们把上面基类的print来定义为虚函数:

virtual void print() const { cout << "Base PRINT 被调用!" << endl; }

那么输出则会不同:

我们可以使用子类型关系结合虚函数来实现多态:

class Mammal {
public:
    Mammal() {
        cout << "inline 构造函数被调用!" << endl;
    }
    ~Mammal() { cout << "inline的析构 函数被调用" << endl; }

    virtual void Speak() const { cout << "mammal的叫声!" << endl; }
    void Sleep() const { cout << "shh,i am sleeping!" << endl; }
    void Move() const { cout << "Mammal move one step!" << endl; }
    void Move(int distance) const
    {
        cout << "Mammal move " << distance << " steps" << endl;
    }
protected:
    int itsAge;
    int itsWeight;
};


class Dog :public Mammal {
public:
    Dog() { cout << "dOG的构造函数被调用!!" << endl; }
    ~Dog() { cout << "Dog的析构函数被调用" << endl; }
    void WagTail() const { cout << "Tail wagging!" << endl; }
    void begFood() const { cout << "i am begging for food!" << endl; }
    virtual void Speak() const { cout << "woof!!!woof!!" << endl; }        //基类中就是虚的,所以这里是继承了
    void Move() const { cout << "DOG Move 5 steps!" << endl; }       //把mammal里的move给覆盖掉了
private:
    BREED itsBreed;
};

class Cat :public Mammal {
public:
    virtual void Speak() const { cout << "miao-miao!!!" << endl; }
};

class Horse :public Mammal {
public:
    virtual void Speak() const { cout << "Winnie!!!" << endl; }
};

class Pig :public Mammal {
public:
    virtual void Speak() const { cout << "lalala!!" << endl; }
};

示例:

Mammal *theArray[5];
theArray[0] = new Dog;
theArray[1] = new Cat;
theArray[2] = new Pig;
theArray[3] = new Horse;
theArray[4] = new Mammal;

for (int i = 0; i < 5; i++)
{
    theArray[i]->Speak();
}

如上,因为子类型关系,我们可以把让基类的指针指向派生类的对象,并且结合虚函数,可以实现多态!

虚函数由于虚函数表的存在,有可能会比一般的函数要慢一点。利用虚函数表的技术,可以在运行的时候动态的查找虚函数表,查找适合自己的版本,这被称作为动态绑定。 相对于使用重载或者模板实现的多态,这种技术更加灵活。 在使用虚函数的时候必须通过指针或者引用来调用才会触发虚函数的多态机制。 比如:

void ValueFunc(Mammal Ma)
{
    Ma.Speak();
}

我们定义这样的一个函数,传入对象的话:

ValueFunc(*theArray[1]);

这样的话还是会调用基类的speak成员,不能实现多态。多态必须通过引用或指针调用才会实现。

5.虚析构函数。

如果我们的类中有虚函数,那么析构函数也必须做成虚的。如果析构函数不做成虚的,有可能产生比较严重的问题。但构造函数不能是虚的。 原因是因为再进行多态的时候可能是用一个基类类型的指针来指向一个派生类的对象。定义成虚函数的好处是,当我们准备析构这个指针所指向的对象时,可以根据指针所指对象的类型(基类还是派生类)来执行不同的析构函数,防止内存泄漏。详细参见:对于虚析构函数的理解。

6. 虚拷贝构造函数(虚复制构造函数)

由于构造函数不能是虚的,但是在某些需求下:需要通过传入一个指向基类指针(可以指向派生类对象)来获取派生类的拷贝,这个时候就需要自己定义一个虚的clone()函数来实现这种需求,我们称之为虚拷贝构造函数。

virtual Mammal* clone() { return new Mammal(*this); }   //把自身克隆一份,我们把其定义成虚的
//后面这几个分别是在各自的类中定义的,返回类型都是基类类型的指针,但是指向的类型是派生类的,这就是子类型方法带来的好处,这样的形式可以实现多态。
virtual Mammal* clone() {
        return new Dog(*this);
    }
virtual Mammal* clone() {
        return new Cat(*this);
    }
virtual Mammal* clone() {
        return new Horse(*this);
    }
virtual Mammal* clone() {
        return new Pig(*this);
    }

十一.多继承和虚基类。

1.多继承。

多继承是比较强大也比较复杂,Java和c#都已经取消了多继承。 多继承:就是一个派生类可能继承来了多个基类,这样的继承方式称之为多继承。看一个简单的例子。

/多继承,虚基类示例。
class horse       //马
{
public:
    horse(int height);
    virtual ~horse() { cout << "马的析构函数被调用" << endl; };    //析构函数是虚的
    virtual void Whinny() const { cout << "Whinny!!" << endl; }
    virtual int getHeight() const { return this->height; }
private:
    int height;
};

horse::horse(int height) :height(height)
{
    cout << "马的构造函数被调用" << endl;
}

class bird        //鸟
{
public:
    bird(bool migrat);
    virtual ~bird() { cout << "鸟的析构函数被调用!" << endl;}
    virtual void chirp() const { cout << "chirp!" << endl; }
    virtual void Fly() const { cout << "i believe i can fly" << endl; }
    virtual bool gerMigration() const { return itsMigration; }
private:
    bool itsMigration;   //是否是候鸟

};

bird::bird(bool migrat):itsMigration(migrat)
{
    cout << "鸟的构造函数被调用!" << endl;
}

class flyHorse:public horse,public bird    //继承了马和鸟
{
public:
    void chirp() const { Whinny();}
    flyHorse(int, bool, long);
    ~flyHorse() { cout << "flyhorse的析构函数被调用!" << endl; }
    virtual long gerNumberBelievers() const
    {
        return itsNumberBelieves;
    }
private:
    long itsNumberBelieves;
};

flyHorse::flyHorse(int height, bool migra, long numberBelieve)
    :horse(height), bird(migra), itsNumberBelieves(numberBelieve)   //构造的时候必须把基类的先构造出来
{
    cout << "飞马的构造函数被调用" << endl;
}

这里有一个flyhorse类继承了两个类,分别是horse和bird,注意下其构造函数的写法。这个例子中就是一个简单的多继承问题,我们把可能被继承的函数都写成虚的了。

2.二义性。

上面写的是比较简单的,两个基类中并没有重名的函数被继承,如果两个基类中有重名的函数且均被继承,就会产生二义性的问题。 比如我们改写上面的程序,给bird和horse类都增加一个color成员,并且都给一个getcolor成员函数:

enum color {
    red,
    blue,
    black,
    yellow
};
class horse       //马
{
public:
    horse(int height,color col);
    virtual ~horse() { cout << "马的析构函数被调用" << endl; };    //析构函数是虚的
    virtual void Whinny() const { cout << "Whinny!!" << endl; }
    virtual int getHeight() const { return this->height; }
    virtual color getcolor() const { return itsColor; }
private:
    int height;
    color itsColor;
};

horse::horse(int height,color col) :height(height),itsColor(col)
{
    cout << "马的构造函数被调用" << endl;
}

class bird        //鸟
{
public:
    bird(bool migrat,color col);
    virtual ~bird() { cout << "鸟的析构函数被调用!" << endl;}
    virtual void chirp() const { cout << "chirp!" << endl; }
    virtual void Fly() const { cout << "i believe i can fly" << endl; }
    virtual bool gerMigration() const { return itsMigration; }
    virtual color getcolor() const { return itsColor; }
private:
    bool itsMigration;   //是否是候鸟
    color itsColor;

};

bird::bird(bool migrat,color col):itsMigration(migrat),itsColor(col)
{
    cout << "鸟的构造函数被调用!" << endl;
}

class flyHorse:public horse,public bird    //继承了马和鸟
{
public:
    void chirp() const { Whinny();}
    flyHorse(color,int, bool, long);
    ~flyHorse() { cout << "flyhorse的析构函数被调用!" << endl; }
    virtual long gerNumberBelievers() const
    {
        return itsNumberBelieves;
    }
private:
    long itsNumberBelieves;
};

flyHorse::flyHorse(color Col, int height, bool migra,long numberBelieve)
    :horse(height, Col), bird(migra, Col), itsNumberBelieves(numberBelieve)   //构造的时候必须把基类的先构造出来
{
    cout << "飞马的构造函数被调用" << endl;
}

当我们对派生类试图调用getcolor函数时,就会出现二义性问题,因为两个函数都被继承了。如图,VS可以自动检查出这种错误。这个错误与基类的同名函数是否是虚函数是没有关系的。

一种简单的解决方法:强制的指定是哪个基类的函数。

cout << peg->bird::getcolor() << endl;

还有一种二义性的产生原因。->菱形继承

看下面这个图,bird和horse同时以animal作为基类,可能继承了相同的成员函数或数据成员,由于类的不同,使用虚函数的话可以产生多态,但是flyhorse同时继承了bird和horse的话,两个类中的同名函数被一个类多继承,这时候也会产生二义性。

3.虚基类。

虚基类就是专门为了解决菱形继承产生的二义性问题。我们把上面菱形继承写出来,然后分析。

enum color {
    red,
    blue,
    black,
    yellow
};

class Animal
{
public:
    Animal(int);
    virtual ~Animal() { cout << "Animal的析构函数被调用---\n"; }
    virtual int getAge() { return itsAge; }
    virtual void setAge(int age) { itsAge = age; }

private:
    int itsAge;
};
Animal::Animal(int age) :itsAge(age)
{
    cout << "Animal的构造函数被调用------"<<endl;
}

class horse:public Animal      //马
{
public:
    horse(int height,color col,int age);
    virtual ~horse() { cout << "马的析构函数被调用" << endl; };    //析构函数是虚的
    virtual void Whinny() const { cout << "Whinny!!" << endl; }
    virtual int getHeight() const { return this->height; }
    virtual color getcolor() const { return itsColor; }
private:
    int height;
    color itsColor;
};

horse::horse(int height,color col,int age) :height(height),itsColor(col),Animal(age)
{
    cout << "马的构造函数被调用" << endl;
}

class bird:public Animal       //鸟
{
public:
    bird(bool migrat,color col,int age);
    virtual ~bird() { cout << "鸟的析构函数被调用!" << endl;}
    virtual void chirp() const { cout << "chirp!" << endl; }
    virtual void Fly() const { cout << "i believe i can fly" << endl; }
    virtual bool gerMigration() const { return itsMigration; }
    virtual color getcolor() const { return itsColor; }
private:
    bool itsMigration;   //是否是候鸟
    color itsColor;

};

bird::bird(bool migrat,color col,int age):itsMigration(migrat),itsColor(col),Animal(age)
{
    cout << "鸟的构造函数被调用!" << endl;
}

class flyHorse:public horse,public bird    //继承了马和鸟
{
public:
    void chirp() const { Whinny();}
    flyHorse(color,int, bool, long,int age);
    ~flyHorse() { cout << "flyhorse的析构函数被调用!" << endl; }
    virtual long gerNumberBelievers() const
    {
        return itsNumberBelieves;
    }
private:
    long itsNumberBelieves;
};

flyHorse::flyHorse(color Col, int height, bool migra,long numberBelieve,int age)
    :horse(height, Col,age), bird(migra, Col,age), itsNumberBelieves(numberBelieve)  //构造的时候必须把基类的先构造出来
{
    cout << "飞马的构造函数被调用" << endl;
}

如上这样,在我们使用getAge()的时候就会出现二义性的问题:

还是看刚才的菱形继承的图,当我们试图去构建一个flyhorse对象时,发生了几次构造: flyHorse *peg = new flyHorse(red,5, true,10,20); 结果如图:

可以看出,我们在构造一个flyhorse对象的时候,发生了五次构造,其中基类被构造了2次,二义性就是从这里产生的。 C++解决这种问题的方法是采用虚基类的方法,也可以称作为虚继承。 具体的做法是:多继承的类在继承基类的时候采用虚继承的方式:

class horse:virtual public Animal 
class bird:virtual public Animal       //鸟

也就是说,中间类(我就这样做吧),不会去调用基类的构造函数来构造基类(因为是虚继承的),最终的派生类会调用基类的构造函数来构造基类,所以在派生类的构造函数上要加上基类的构造函数!

flyHorse::flyHorse(color Col, int height, bool migra,long numberBelieve,int age)
    :horse(height, Col,age), bird(migra, Col,age), itsNumberBelieves(numberBelieve),Animal(age)

另外,中间类的构造函数也会调用基类的构造函数,但是不会被执行,因为是虚基类继承。

horse::horse(int height,color col,int age) :height(height),itsColor(col),Animal(age)
{
    cout << "马的构造函数被调用" << endl;
}
bird::bird(bool migrat,color col,int age):itsMigration(migrat),itsColor(col),Animal(age)
{
    cout << "鸟的构造函数被调用!" << endl;
}

4. 虚基类的构造函数。

采用虚基类之后,构造函数的写法上也有变化: 不采用虚基类的时候,每一个类只负责其基类的构造函数调用,这种调用具有传递性,不允许跨层调用。 采用虚基类的时候,每个类都要负责虚基类的构造函数的调用,比如flyhorse的构造函数也要负责Animal构造函数的调用,这个时候允许跨层调用,而且这种调用是必须的。

这样的话,我们既可以使用多继承,又用虚继承的方式避免了二义性的问题,这个问题也是比较复杂的,一般情况下,尽可能的使用单继承,尽量避免使用多继承。

十二. 纯虚函数与抽象类。

1. 纯虚函数。

纯虚函数只能是用来继承的,任何包含一个或者多个纯虚函数的类被称作抽象类。 抽象类是不能够创建对象的,只是用来继承。 在虚函数的声明后面加上=0就可以声明一个虚函数为纯虚函数。如下,定义了一个或者多个纯虚函数的数类称为抽象类。

class shape {
public:
    shape(){}
    virtual ~shape() {}
    virtual double getArea() = 0;
    virtual double getPerim() = 0;     
    virtual void draw() = 0;     //写上等于0就是纯虚函数,这些函数的实现

};

2. 抽象类。

  • 不能创建抽象类的对象,只能继承它。
  • 继承的时候务必覆盖掉继承来的纯虚函数。 note:如果派生类没有覆盖掉继承来的所有纯虚函数,那么其就还是一个抽象类,不能实例化。 下面看一个简单的例子,结构如下:

我们定义了一个名为shape的抽象类用来继承,在shape的派生类中必须覆盖掉继承来的纯虚函数(因为抽象类中的纯虚函数一般是不做定义的,只是为了继承达到多态的作用)。代码及测试代码如下:

class shape {
public:
    shape(){}
    virtual ~shape() {}
    virtual double getArea() = 0;
    virtual double getPerim() = 0;     
    virtual void draw() = 0;     //写上等于0就是纯虚函数,这些函数的实现

};
//一般而言,纯虚函数的定义,可以不写,一般情况下也不写

//void shape::draw()
//{
//  cout << "-----" << endl;
//}

class Circle :public shape
{
public:
    Circle(double radius):itsRadius(radius) { cout << "circle的构造函数被调用" << endl; }
    virtual ~Circle() {}
    virtual double getArea() { return 3.14*itsRadius*itsRadius; }
    virtual double getPerim() { return 2 * 3.14*itsRadius; }
    virtual void draw() { cout << "it is a circle!" << endl; }
private:
    double itsRadius; 
};

class Rectangle :public shape 
{
public:
    Rectangle(double h, double w) :height(h), width(w) { cout << "Rectangle的构造函数被调用!" << endl; }
    virtual ~Rectangle() {};
    virtual double getArea() { return height*width; }
    virtual double getPerim() { return 2 * (height + width); }
    virtual void draw()
    { 
        cout << "it is a rectangle!" << endl; 
    }
    virtual double getWidht() { return width; }
    virtual double gerHeight() { return height; }

private:
    double height;
    double width;
};

class Square :public Rectangle
{
public:
    Square(double x) :Rectangle(x, x) { cout << "Square 的构造函数被调用" << endl; }   //调用其基类的构造函数
    virtual ~Square() {};
    
    virtual void draw() { cout << "its a square!" << endl; }
};

测试代码:

    int choice;
    bool fQuit = false;
    shape *sp=nullptr;     //新标准不允许这里不做初始化直接用!
    while (fQuit == false)
    {
        cout << "(1)circle  (2)rectangle  (3)square  (0)quit" << endl;
        cout << "请输入选项;" << endl;
        cin >> choice;
        switch (choice)
        {
        case 1:
            sp = new Circle(5);
            break;
        case 2:
            sp = new Rectangle(4, 6);
            break;
        case 3:
            sp = new Square(5);
            break;
        case 0:
            fQuit = true;
        default:
            break;
        }
        if (fQuit == false)
        {
            sp->draw();       //多态
            delete sp;       //指针是通过new创建的对象的话,一定要用delete把对象删除
        }
    }

同样的,我们使用基类的指针,可以指向不同的派生类,利用纯虚函数的继承来实现多态。 有一点值得注意:基类的指针是可以指向派生类,但是只能访问派生类的继承部分,包括继承的数据成员(符合访问规则:共有)以及成员函数(使用虚函数可以实现多态),但是不能访问新增数据成员及新增成员函数。

3. 纯虚函数的实现。

一般情况下,我们可以不用写纯虚函数的实现(只写声明就可以),但是在有些情况下可以写。值得注意的是,如果我们要为纯虚函数提供定义,必须写在类定义的外边。我们写一个下面这种结构的继承关系类,其中animal中有五个纯虚函数。

mammal只重写了Animal的一个纯虚函数,继承来的还有四个纯虚函数,所以它还是抽象类。

class Animal1
{
public:
    Animal1(int age);
    virtual ~Animal1() {}
    virtual int getAge() const { return itsage; }
    virtual void setAge(int age) { this->itsage = age; }
    virtual void sleep() const = 0;
    virtual void eat() const = 0;
    virtual void Rproduce() const = 0;
    virtual void Move() const = 0;
    virtual void Speak() const = 0;           //五个纯虚函数

private:
    int itsage;
};

Animal1::Animal1(int age) :itsage(age) { cout << "animal的构造函数被调用!" << endl; }


class mammal1 :public Animal1
{
public:
    mammal1(int age) :Animal1(age)
    {
        cout << "MAMMAL的构造函数被调用!" << endl;
    }
    virtual ~mammal1() { cout << "mammal的析构函数被调用!" << endl; }
    virtual void Rproduce() const {
        cout << "mammal reproduction depicted...\n";     //只重写了一个纯虚函数,其他的纯虚函数被继承了,所以也是抽象类
    }
};


class fish:public Animal1
{
public:
    fish(int age) :Animal1(age)
    {
        cout << "fish的构造函数被调用" << endl;
    }
    virtual ~fish()
    {
        cout << "fish的析构函数被调用" << endl;
    }



    virtual void sleep() const { cout << "fish is sleeping" << endl; }
    virtual void eat() const { cout << "fish is eating" << endl; }
    virtual void Rproduce() const { cout << "fish is rproducing!!" << endl; }
    virtual void Move() const { cout << "fish is moving" << endl; }
    virtual void Speak() const { cout << "fish cant speak!!" << endl; }       //五个纯虚函数都做了重写覆盖

};



class Horse1 :public mammal1
{
public:
    Horse1(int age, color col) :mammal1(age), itsColor(col) {
        cout << "Horse的构造函数被调用" << endl;
    }
    virtual ~Horse1()
    {
        cout << "Horse的析构函数被调用" << endl;
    }


    virtual void sleep() const { cout << "Horse is sleeping" << endl; }
    virtual void eat() const { cout << "Horse is eating" << endl; }
    //virtual void Rproduce() const { cout << "fish is rproducing!!" << endl; }   //这个在mammal里已经做了覆盖不是纯虚了
    virtual void Move() const { cout << "Horse is moving" << endl; }
    virtual void Speak() const { cout << "Horse is speaking!!" << endl; }       //五个纯虚函数都做了重写覆盖
    virtual color GetItsColor() const { return itsColor; }
protected:
    color itsColor;

};

基本上先这么多了,这一部分应该是C++面向对象编程中最难的一部分了,常看常新吧!共勉!!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java帮帮-微信公众号-技术文章全总结

【Java提高三】三大特性-多态

【Java提高】三大特性-多态 面向对象编程有三大特性:封装、继承、多态。 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构...

40090
来自专栏小勇DW3

Java之static作用的全方位总结

 引用一位网友的话,说的非常好,如果别人问你static的作用;如果你说静态修饰 类的属性 和 类的方法 别人认为你是合格的;如果是说 可以构成 静态代码块,...

24720
来自专栏WindCoder

Java基础小结(二)

继承可以使用 extends 和 implements 这两个关键字来实现继承, 而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两...

12010
来自专栏微信公众号:Java团长

Java基础01 从HelloWorld到面向对象

Java是完全面向对象的语言。Java通过虚拟机的运行机制,实现“跨平台”的理念。我在这里想要呈现一个适合初学者的教程,希望对大家有用。

10110
来自专栏前端知识分享

javascript易混淆的split()、splice()、slice()方法详解

很多时候,一门语言总有那么些相似的方法,容易让人傻傻分不清楚,尤其在不经常用的时候。而本文主要简单总结了JavaScript中的关于字符串和数组中三个容易混淆的...

15820
来自专栏积累沉淀

JavaScript对象和数组

学习要点: 1.Object类型 2.Array类型 3.对象中的方法 什么是对象,其实就是一种类型,即引用类型。而对象的值就是引用类型的实例。 一...

33450
来自专栏从流域到海域

《Java程序设计基础》 第8章手记Part 2

第八章内容 Part 2 - … - 抽象类和抽象方法 - 接口及接口的实现 - 利用接口实现类的多重继承 - 内部库和匿名类 ...

22790
来自专栏微信公众号:Java团长

Java抽象类与oop三大特征

在了解抽象类之前,先来了解一下抽象方法。抽象方法是一种特殊的方法:它 只有声明,而没有具体的实现 。抽象方法的声明格式为:

15440
来自专栏C/C++基础

C++纯虚函数与抽象类

为什么说虚函数是C++最重要的特性之一呢,因为虚函数承载着C++中动态联编的作用,也即多态,可以让程序在运行时选择合适的成员函数。虚函数必须是类的非静态成员函数...

35920
来自专栏Java3y

归并排序就这么简单

归并排序就这么简单 从前面已经讲解了冒泡排序、选择排序、插入排序,快速排序了,本章主要讲解的是归并排序,希望大家看完能够理解并手写出归并排序快速排序的代码,然后...

66870

扫码关注云+社区

领取腾讯云代金券