C++继承详解二——默认成员函数

  提前声明一下,这篇文章转载自李寻(lixungogogo)  在这一篇文章开始之前,我先解决一个问题。
  在上一篇C++继承详解之一——初探继承中,我提到了在派生类中可以定义一个与基类成员函数同名的函数,这样派生类中的函数就会覆盖掉基类的成员函数。
  在谭浩强的C++程序设计这本书第十一章,351页最下面有这么一段话:

可在派生类中声明一个与基类成员同名的成员函数,则派生类中的新函数会覆盖基类的同名成员,但应注意:如果是成员函数,不仅应是函数名相同,而且函数的参数表(参数的个数和类型)也应相同,如果不相同,就会成为函数重载而不是覆盖了、用这样的方法可以用新成员取代基类的成员。

  但是经过我的实验,这段话就是错误的,派生类中定义与基类成员函数同名不同参数表的函数是不能构成函数重载的,先上代码:

class A
{
public:
    int a_data;
    void a()
    {
        cout << "A" << endl;
    }
};
class B
{
public:
    int b_data;
    void b()
    {
        cout << "B" << endl;
    }
};
class C :public A, public B
{
public:
    int c_data;
        void a(int data)//重载A类中的a()函数
    {
        cout << "C" << endl;
    }
};
int main()
{
    C c;
    c.a();
    return 0;
}

编译后,编译器会报错

Error   1   error C2660: 'C::a' : function does not take 0 arguments    e:\demo\继\way\project1\project1\source.cpp 86  1   Project1
    2   IntelliSense: too few arguments in function call    e:\DEMO\继\way\Project1\Project1\Source.cpp 86  6   Project1

错误表明:编译器并没有将c.a()看做C类继承自A类的a()函数,而是报错没有给a函数参数,即不构成函数重载,如果给c.a(1)一个参数,编译通过。输出:C

那么我们不给C类中定义同名函数呢

class A
{
public:
    int a_data;
    void a()
    {
        cout << "A" << endl;
    }
};
class B
{
public:
    int b_data;
    void b()
    {
        cout << "B" << endl;
    }
};
class C :public A, public B
{
public:
    int c_data;
    //void a(int data)
    //{
    //  cout << "C" << endl;
    //}
};
int main()
{
    C c;
    c.a();
    return 0;
}

编译通过,运行输出:A
  以上两个例子,完全可以说明,当我们在派生类中定义一个同名函数的时候,编译器是将同名函数隐藏了,不管参数表是否相同。即不会构成函数重载,直接为函数覆盖。
  那么问题来了,为什么不会构成函数重载呢?
  一定要注意,函数重载的条件是在同一个作用域中才会构成函数重载,而派生类和基类是两个类域,一定不会构成函数重载的

  现在进入这篇文章的主题,派生类成员函数的解答,因为开篇我们讲的这个例子,我先从函数覆盖写起。

一、函数覆盖、函数隐藏、函数重载

  先分别讲一下函数覆盖,隐藏和重载分别是什么:

1.成员函数被重载的特征

(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
//virtual关键字在这里看不懂也可以,在下一篇文章中我会详细解答

覆盖是指派生类函数覆盖基类函数,特征是

(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
  当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖。
  派生类对象调用的是派生类的覆盖函数
  指向派生类的基类指针调用的也是派生类的覆盖函数
  基类的对象调用基类的函数

“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下

  (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

//这里就是我们开篇举得那个例子
  (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
  有关函数覆盖与隐藏的详细解释,我会放在下一篇或者下下一篇文章中详细解释,这篇是为了提一下,将基本知识点讲完后再回来分析这个问题。

二、派生类的默认成员函数

  下面用公有继承举例
  派生类对象包含基类对象,使用公有继承,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

class Base
{};
class Derive:public Base    //公有继承
{};

  那么上面的代码完成了哪些工作呢?

Derive类具有以下特征:

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

那么我们还需要给派生类添加什么呢?
1.派生类需要自己的构造函数
2.派生类可以根据需要添加额外的数据成员和成员函数。

1.构造函数

  派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
  即派生类构造函数必须使用基类构造函数。

  构造函数不同于其他类的方法,因为它创建新的对象,而其他类的方法仅仅是被现有的对象调用,这是构造函数不能被继承的一个原因。
  继承意味着派生类对象可以使用基类的方法,然而构造函数在完成其工作之前,对象并不存在。
  在创建派生类对象时,程序首先创建基类对象,即基类对象应当在程序进入派生类构造函数之前被创建。
现在看下面的代码就可以理解这个顺序:

class Base
{
public:
    Base(int a = 0,int b = 0,int c = 0)
        :_pub(a)
        , _pro(b)
        , _pri(c)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    int _pub;
protected:
    int _pro;
private:
    int _pri;
};
class Derive :public Base
{
public:
    Derive()
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
private:
    int d_pri;
protected:
    int d_pro;
public:
    int d_a;
};
int main()
{
    Derive a;
    return 0;
}

运行结果为:
这里写图片描述
  这就说明了,在创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数,而析构的顺序相反。
  这是因为在创建时是在栈内进行的,栈有着先进后出的属性,所以先创建的后析构,后创建的先析构。
  在上面的代码中是单继承的情况,那么多继承的情况呢?
  看下面的代码:

class Base
{
public:
    Base(int a = 0,int b = 0,int c = 0)
        :_pub(a)
        , _pro(b)
        , _pri(c)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    int _pub;
protected:
    int _pro;
private:
    int _pri;
}; 
class Base1
{
public:
    Base1()
    {
        cout << "base1" << this << endl;
    }
    ~Base1()
    {
        cout << "~Base1" << endl;
    }
};
class Derive :public Base,public Base1
{
public:
    Derive()
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
private:
    int d_pri;
protected:
    int d_pro;
public:
    int d_a;
};
int main()
{
    Derive a;
    return 0;
}

运行结果为:
这里写图片描述
  从运行结果可以看到,在多继承时,调用构造函数的顺序与继承列表的顺序也是有关的,如果我们将代码中派生类的继承列表改为:

class Derive:public Base1,public Base
{/*不变*/};

结果为:
这里写图片描述
  这就是在上一篇我曾讲过的,多继承中继承列表与派生类对象模型的关系。多继承时派生类的对象模型是与继承列表的顺序相关的。
  也可以理解为,因为派生类的对象模型中,基类成员在模型的最上面,所以要先调用基类的构造函数,再调用派生类的构造函数。
  在上述代码中,基类是由默认的构造函数的,我们在Base的构造函数中给了它缺省值,那么,如果基类没有默认的构造函数,可以吗?
  我们将代码改为:

class Base
{
public:
    Base(int a,int b ,int c )//不给缺省参数
        :_pub(a)
        , _pro(b)
        , _pri(c)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    int _pub;
protected:
    int _pro;
private:
    int _pri;
}; 
class Derive :public Base
{
public:
    Derive()
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
private:
    int d_pri;
protected:
    int d_pro;
public:
    int d_a;
};
int main()
{
    Derive a;
    return 0;
}

编译不通过,给出的错误为:

Error   1   error C2512: 'Base' : no appropriate default constructor available  e:\demo\继承\way\project1\project1\source.cpp 28  1   Project1
    2   IntelliSense: no default constructor exists for class "Base"    e:\DEMO\继承\way\Project1\Project1\Source.cpp 29  2   Project1

基类中没有可用的构造函数。
  那么在创建派生类对象时,如果没有默认的构造函数,我们如何在创建派生类对象之前,先创建基类对象呢?
  还记得C++中的成员初始化列表吗?
  在C++中,成员初始化列表句法可以完成这个工作。
  我们在定义派生类时,将代码改为:


class Base
{
public:
    Base(int a ,int b ,int c )
        :_pub(a)
        , _pro(b)
        , _pri(c)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void Show()
    {
        cout << "_pri" << _pri << endl;
        //_pri成员不能在派生类中被访问
        //Show函数的目的是在派生类中也能输出基类私有成员的状态
        cout << "_pro" << _pro << endl;
        cout << "_pub" << _pub << endl;
    }
    int _pub;
protected:
    int _pro;
private:
    int _pri;
}; 
class Derive :public Base
{
public:
    Derive()
        :Base(1,2,3)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Display()
    {
        Show();
        //派生类不能访问基类中的私有成员
        //要是想打印出基类私有成员的状态,只能在基类中定义成员函数
        //再在派生类成员函数中调用它
    }
private:
    int d_pri;
protected:
    int d_pro;
public:
    int d_a;
};
int main()
{
    Derive a;
    a.Dispay();
    return 0;
}

运行成功,结果为:
这里写图片描述
如果我们要在类外给基类成员赋值,那么将派生类定义改为:


class Derive :public Base
{
public:
    Derive(int a,int b,int c)
        :Base(a,b,c)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Display()
    {
        Show();
    }
private:
    int d_pri;
protected:
    int d_pro;
public:
    int d_a;
};
int main()
{
    Derive a(1,2,3);
    a.Display();
    return 0;
}

运行成功,结果为:
这里写图片描述
这个时候的关系为:
这里写图片描述

  总结以下上面的内容:
1.基类对象首先被创建
2.派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
3.派生类构造函数应初始化派生类新增的数据成员(这个在上面代码中没有体现)**

  创建派生类对象时,程序首先调用基类的构造函数,然后在调用派生类的构造函数,(与派生类对象模型有关),基类构造函数负责初始化派生类继承的数据成员,派生类的构造函数主要用于初始化新增的数据成员。
  派生类的构造函数总是滴啊用一个基类构造函数。
  可以使用初始化列表句法指明要使用的基类构造函数,否则将使用默认的基类构造函数。
  派生类对象析构时,程序首先调用派生类析构函数,再调用基类析构函数。

2.拷贝构造函数(也称复制构造函数)

 拷贝构造函数接受其所属类的对象为参数。
  在下述情况下,将使用拷贝构造函数

  1. 将新的对象初始化为一个同类对象
  2. 按值将对象传递给函数
  3. 函数按值返回对象
  4. 编译器生成临时对象

      如果程序没有显式定义拷贝构造函数,编译器将自动生成一个。

      当然,如果想在派生类中构造基类对象,那么不仅仅可以用构造函数,也可以用拷贝构造函数

class Derive :public Base
{
public:
    Derive(const Base &tp)
            :Base(tp)//拷贝构造函数
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Display()
    {
        Show();
    }
private:
    int d_pri;
protected:
    int d_pro;
public:
    int d_a;
};
int main()
{
    Base b(1, 2, 3);
    Derive a(b);
    a.Display();
    return 0;
}

运行成功,结果为:
这里写图片描述
  这里我没有给基类定义拷贝构造函数,但是编译器自动给基类生成了一个拷贝构造函数,因为我基类中定义的没有指针成员,所以浅拷贝可以满足我的要求,但是如果在基类成员中有指针变量,必须要进行显式定义拷贝构造函数,即进行深拷贝。不然会造成同一块内存空间被析构两次的问题。

3.赋值操作符

  默认的赋值操作符用于处理同类对象之间的赋值,赋值不是初始化,如果语句创建新的对象,则使用初始化,如果语句修改已有对象的值,则为赋值。
  注意:赋值运算和拷贝构造是不同的,赋值是赋值给一个已有对象,拷贝构造是构造一个全新的对象

class Base
{};
int main()
{
    Base a;
    Base b = a;//初始化
    Base c;
    c = a;//赋值
}

  赋值运算符是不能被继承的,原因很简单。派生类继承的方法的特征与基类完全相同,但赋值操作符的特征随类而异,因为它包含一个类型为其所属类的形参。
  如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值操作符。这个操作符的默认版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。
  如果对象属于派生类,编译器将使用基类赋值操作符来处理派生对象中基类部分的赋值,如果显示的为基类提供了赋值操作符,将使用该操作符。

1.将派生类对象赋给基类对象

class Base
{};
class Derive
{};
int main()
{
    Base a;
    Derive d;
    a = d;
}

上面的a=d;语句将使用谁的赋值操作符呢。
实际上,赋值语句将被转换成左边的对象调用的一个方法

a.operator=(d);
//左边的为基类对象

简而言之,可以将派生对象赋给基类对象,但这只涉及到基类的成员。
这里写图片描述

2.基类对象赋给派生类对象

class Base
{};
class Derive
{};
int main()
{
    Base a;
    Derive d;
    d = a;
}

上述赋值语句将被转换为:

d.operator=(a);
//Derive::operator=(const Derive&)

左边的对象为派生类对象,不过派生类引用不能自动引用基类对象,所以上述代码不能运行。或者运行出错。
这里写图片描述
除非有下面的函数

Derive(const Base&)
{}

  总结:

  1. 是否可以将基类对象赋给派生类对象,答案是也许。如果派生类包含了转换构造函数,即对基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生对象。
  2. 派生类对象可以赋给基类对象。

三、继承与转换——赋值兼容规则(public继承)

公有继承条件下

  派生类和基类之间的特殊关系为:

1.派生类对象可以使用基类的方法,条件是基类的方法不是私有的
2.基类指针可以在不进行显示类型转换的情况下指向派生类对象
3.基类引用可以再不进行显示类型转换的情况下引用派生类对象,但是基类指针或引用只能用于调用基类的方法,不能用基类指针或引用调用派生类的成员及方法

void FunTest(const Base&d)
{

}
void FunTest1(const Derive&d)
{

}
int main()
{
    Derive d;
    Base b(0);
    b = d;//可以
    d = b;//不行,访问的时候会越界
    //上面两行代码在上一条中已经解释过了
    FunTest(b);
    FunTest(d);
    FunTest1(b);    //不可以
    FunTest1(d);
    Base* pBase = &d;
    Derive*pD = &b;//错了
    //如果非要这么做只能通过强制类型转换
    Derive*pD = (Derive*)&b;//如果访问越界,会崩溃
}

  通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是个例外。但是这个例外是单向的,即仅仅不可以将基类对象和地址赋给派生类引用和指针。

  如果允许基类引用隐式的引用派生类对象,则可以使用基类引用为派生类对象调用基类的方法,因为派生类继承了基类的方法,所以这样不会出现问题。
  但是如果可以将基类对象赋给派生类引用,那么派生类引用能够为积累对象调用派生类方法,这样做会出现问题,例如:用基类对象调用派生类中新增的方法,是没有意义的,因为基类对象中根本没有派生类的新增方法。

阅读更多
文章标签: c++ 继承
个人分类: c++
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

C++继承详解二——默认成员函数

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭