您的位置:首页 > 编程语言 > C语言/C++

C++继承具体解释之二——派生类成员函数具体解释(函数隐藏、构造函数与兼容覆盖规则)

2017-08-05 21:55 691 查看

  在这一篇文章開始之前。我先解决一个问题。
  在上一篇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;//赋值
}

  赋值运算符是不能被继承的,原因非常easy。派生类继承的方法的特征与基类全然同样,但赋值操作符的特征随类而异,由于它包括一个类型为其所属类的形參。
  假设编译器发现程序将一个对象赋给同一个类的还有一个对象,它将自己主动为这个类提供一个赋值操作符。这个操作符的默认版本号将採用成员赋值,即将原对象的对应成员赋给目标对象的每一个成员。
  假设对象属于派生类,编译器将使用基类赋值操作符来处理派生对象中基类部分的赋值。假设显示的为基类提供了赋值操作符,将使用该操作符。

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++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是个例外。可是这个例外是单向的,即仅仅不能够将基类对象和地址赋给派生类引用和指针。

  假设同意基类引用隐式的引用派生类对象,则能够使用基类引用为派生类对象调用基类的方法。由于派生类继承了基类的方法,所以这样不会出现故障。
  可是假设能够将基类对象赋给派生类引用。那么派生类引用能够为积累对象调用派生类方法,这样做会出现故障,比如:用基类对象调用派生类中新增的方法,是没有意义的。由于基类对象中根本没有派生类的新增方法。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐