C++三大特性-继承

Posted 水澹澹兮生烟.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++三大特性-继承相关的知识,希望对你有一定的参考价值。

1.继承与派生的概念及定义

继承是一个进程。通过一个进程,一个对象可以获得另一个对象的属性(这个属性包括数据和函数),且可以加入属于自己的方法和类型称之为继承,从另一个角度上说,从已有的类中产生一个新类,称之为派生;派生类是基类的具体化,而基类是派生类的抽象。继承机制是面向对象程序设计使代码复用的重要手段。它允许程序员在保持原有类型的基础上进行扩展,增加功能,这样产生新的类,称之为派生类。继承呈现了面向对象的程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。

class pet{
public:
	void getname(){
		cout<<"name"<<name<<endl;
	}
private:
	string type;
	string name;
	int age;
};
class dog : public pet{
public:
	void getvoice(){
		cout<<"汪汪"<<endl;
	}
private:
	string size;
};
int main(){
	dog dd;
	dd.getname();
	dd.getvoice();
}

2.派生类的声明与构成

(1).声明

声明方式一般为:

class 派生类名:[继承方式] 基类名{
    派生类新增的成员
};

(2).构成

  • 从基类接受的成员。

派生类将基类全部的成员继承过来(但不包括构造和析构函数),因此是没有选择的。这样就会造成数据冗余,因此有些类是专为其设计的。

  • 调整从基类接受的成员。

注意:如果子类和基类中具有相同名称的成员变量时,不管成员变量的类型是否相同,都优先访问子类同名成员变量。不能够通过子类对象直接访问基类中同名的成员变量,就相当于子类同名成员变量将基类的同名成员变量隐藏起来了。重点:如果必须通过子类对象访问基类中的同名成员变量该如何呢?

告诉编译器我需要访问的是基类中的同名成员变量,则加上访问限定符::。例如:

// 1. 成员变量同名
// 2. 成员方法
// 如果通过子类对象调用相同名称的成员时,优先访问子类的,基类同名的成员永远无法通过
// 子类对象直接调用到,相当于子类同名成员将基类的同名成员隐藏了
// 如果想要同名子类对象访问基类中同名的成员,只需在成员前加上基类名称::
// 如果在子类成员函数中,想要访问基类同名的成员,只需在基类成员前加上基类名称::
class Base{
public:
	void SetBase(int b){
		_b = b;
	}

	void fun(){
    	cout << "Base::fun()" << endl;
	}

//protected:
public:
	int _b;
	char _c;
};

class Derived : public Base{
public:
	void SetDeirved(int b, int d){
		_c = 100;
		Base::_c = 100;
		__super::_c = 100;
		SetBase(b);
		_d = d;
	}

	void fun(int a){
		cout << "Derived::fun(int)" << endl;
	}

//protected:
public:
	int _d;
	int _c;
};

int main(){
	Base b;
	b.SetBase(10);
	Derived d;
	d.SetDeirved(100, 200);
	/*
	如果子类和基类中具有相同名称的成员变量时,不管成员变量的类型是否相同,都优先访问子类的同名成员变量
	不能通过子类对象直接访问子类和父类中同名的成员变量,就相当与子类同名的成员变量将基类的同名成员变量隐藏了
	*/
	d._c = 'A';   // d对象中有两个_c
	// 有些情况下可能需要通过子类对象访问基类中同名的成员变量
	d.Base::_c = 'B';
	// d.fun();   // 编译报错
	d.fun(10);
	d.Base::fun();
	return 0;
}
  • 在声明派生类时增加的成员。 

3.派生类成员中的访问属性

类成员/继承方式public继承protected继承

private继承

基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

重点总结:

  1. 基类的private成员在派生类中无论怎样都是不可见的,基类的私有成员被继承到了派生类当中,但是却不能去访问它,无论是在类内还是类外。
  2. 因为基类的private成员在派生类中不能被访问,但是如果需要在派生类中访问基类的成员但是又不允许在类直接被访问,那么就可以将其定义成protected,由此可知保护乘员限定符是因为继承才出现的。
  3. 由上表我们可以得出,基类的私有成员在派生类中都是不可见的,所以有:public > protected > private
  4. 直接使用关键字class默认继承方式一般都是private继承,使用struct默认继承方式一般都是public
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced / private继承,因为protetced / private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

4.派生类的默认成员函数

(1)构造函数

1. 如果基类没有显式定义任何构造函数,则子类也可以不用定义;在子类构造函数初始化列表的位置不能直接初始化从基类继承的成员变量

2.基类定义了自己的构造函数,但是基类构造函数是默认的构造,在这种场景下,子类的构造方法可以定义,也可以不用定义。

class Base{
public:
	Base(int b = 10)
		: _b(b){
		cout << "Base(int)" << endl;
	}
protected:
	int _b;
};
class Derived : public Base{
public:
	Derived(int d)
	  : Base(100)
		: _d(d){
		cout << "Derived(int)" << endl;
	}
protected:
	int _d;
};
int main(){
	Base b;
	Derived d(20);
	Derived d;
	return 0;
}

注意:

  • 如果子类没有显示构造函数,则编译器会给子类生成一个默认的构造函数,而且在编译器给子类生成的默认构造函数初始化列表的位置会显式调用基类的默认构造函数,完成子类对象中从基类继承下来成员的初始化工作,如下图所示。
  • 如果子类定义了自己的构造函数,在其初始化列表的位置基类和子类新增的成员都需要初始化,在子类构造函数初始化列表的位置不能显式初始化基类成员变量

3.如果基类定义了自己的非默认构造函数,子类必须要显示定义自己的构造函数,而且在其初始化列表的位置要显示调用基类构造函数,以完成子类对象中从基类部分继承下来成员的初始化工作。

默认成员函数的总结 

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造
  6. 派生类对象析构清理先调用派生类析构再调基类的析构

上面的六点,我们给出代码如下:

class Base{
public:
	Base(int b)
		: _b(b)
	{}
	// 拷贝构造函数
	Base(const Base& b)
		: _b(b._b){}
	Base& operator=(const Base& b){
		if (this != &b){
			_b = b._b;
		}
		return *this;
	}
	~Base(){
		cout << "Base::~Base()" << endl;
	}
protected:
	int _b;
};

class Derived : public Base{
public:
	Derived(int b, int d)
		: Base(b)
		, _d(d){}

	Derived(const Derived& d)//拷贝构造
		: Base(d)
		, _d(d._d){}
	Derived& operator=(const Derived& d){
		if (this != &d){
			// 给基类部分成员赋值
			// *this = d;  不是这么调用
			Base::operator=(d);
			// 给子类部分成员赋值
			_d = d._d;
		}
		return *this;
	}
	~Derived(){
		cout << "Derived::~Derived()" << endl;

		// 编译器在编译代码时,会在子类析构函数最后一条有效语句之后,
		// 条件调用基类析构函数的语句
		// call Base::~Base();
	}
protected:
	int _d;
};

void TestDerived(){
	Derived d(10, 20);
}

int main(){
	TestDerived();
	Derived d(10, 20);
	Derived d1(d);
	Derived d2(30, 40);
	d1 = d2;
	return 0;
}

注意!!!这句话是错误的:子类对象在创建时先调用基类构造函数,然后调用派生类构造函数 ;在析构时先调用派生类析构函数,再调用基类的构造函数。

我们要遵循:创建哪个类对象,就先调用那个类的构造函数;析构那个类对象就先调用那个析构函数。

5.不同继承方式 

注意!!!基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一 个static成员实例

单继承:一个子类只有一个直接父类称之为单继承。

多继承:一个子类有;两个或者两个以上的直接父类时,称之为多继承。

再多继承体系中存在二义性问题,如果有同名的数据成员那么就无法通过变量名进行读取,需要通过作用域符(::)进行区分。 

(1)菱形继承

菱形继承是多继承的一种特殊情况。

class N{
public:
	int a;
	void display();
};
class A:public N{
public:
	int a1;
};
class B:public N{
public:
	int a2;
};
class C:public A,public B{
public:
	int a3;
	void display();
};

菱形继承的问题

观察上面代码,不难看出菱形继承中存在着数据的二义性。这里的二义性是因为假设我们访问A类从基类N中继承下来的成员,用C类的对象对基类成员进行调用,这样无法区分是从A类中继承下来的成员还是从B类中继承下来的成员。因此产生二义性。除此之外,菱形继承还会产生数据冗余。解决这种问题就引入了虚基类。

(2)虚基类

在一个类中保留间接共同基类的多分成员时是有必要的,但是同时会造成数据冗余与数据的二义性,C++提供虚基类的方法,使得在继承间接共同积累时只保留一份成员。而且虚基类一般只在菱形继承下使用。

现在我们将上面的菱形继承的代码采用虚基类的方法,将N作为虚基类。

class N{
public:
    N(int i){}//基类构造函数中有一个参数
	int n;
	void display();
};
class A:virtual public N{//声明类A是类N的公用派生类,N类是A类的虚基类
public:
    A(int n):N(N){}
	int a1;
};
class B:virtual public N{//声明类B是类N的公用派生类,N类是B类的虚基类
public:
     B(int n):N(N){}//在初始化表中对虚基类进行初始化
	int a2;
};
class C:public A,public B{
public:
	int a3;
     C(int n):N(n),A(n),B(n){}//在初始化表中对所有基类初始化
	void display();
};

 在这里我们需要注意三点:

  • 虚基类并不是在声明基类时进行声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生命一个派生类时作为虚基类,但是在声明另一个派生类是不作为虚基类。
  • 当基类通过多条派生路径被一个派生类继承,该派生类只继承该基类一次,也就是说,基类成员只保留一次。
  • 为了保证虚基类在派生类中只保留一次,应该在该基类中的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。
  • 在上面代码中C++编译系统只执行最后的派生类对虚基类的构造函数调用,而忽略虚基类的其他派生类(例如A类与B类)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

6.基类和派生类对象复制转换

不同数据之间在一定条件下可以进行类型转换,这种不同数据类型之间的自动转换和赋值称之为赋值兼容。赋值兼容规则的前提时必须是public继承,因为公有继承会使所有基类能够实现的功能派生类都能实现。而基类与派生类也存在着赋值兼容关系。具体表现在以下几个方面:

1.可以将派生类对象直接复制给基类对象,反之则不行

int main(){
	A a;//基类
	B b;//派生类
	a = b;//用派生类对象对基类对象进行赋值
}

 我们应该注意,子类型关系是单向的,不可逆的。只能用子类对象对基类对象进行赋值,而不能用基类对象向子类对象进行赋值,理由是因为基类对象不包含派生类的成员,无法对派生类的成员进行赋值。且同一个基类的不同子类对象之间的也不能进行赋值。

注意:对象模型,即对象在内存中的存储方式,也就是说一个对象中成员变量在内存中的布局。

2.可以让基类的指针或者引用指向子类的对象,反之则不行

  •  派生类可以替代基类对象向基类对象的引用进行赋值或者初始化。如下代码,因为定义基类对象的引用r,如果将其用a进行赋值,那么r和a共享同一段内存单元,因此可以用子类对象进行初始化r。
    int main(){
        A a;
        B b;
        A& r = b;//定义基类A对象的引用r,并用派生类B对象进行初始化
    }
  • 如果函数参数是基类的对象或者基类对象的引用,相应的实参可以用子类对象。但是只能输出派生类中继承基类的成员的值。
    void test(A& r){
        cout<<"hello"<<endl;
    }
    int main(){
        A a;//基类
        B b;//派生类
        test(b);
    }
  • 派生类对象的地址可以赋值给基类对象的指针变量,也就是说,指向基类的指针变量也可以指向派生类对象。注意:基类的指针只能访问派生类对象中从基类继承下来的成员。
    int main(){
    	A a;//基类
    	B* b = &a;
    }

7.继承与组合

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 优先使用对象组合,而不是类继承 。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关 系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。 

以上是关于C++三大特性-继承的主要内容,如果未能解决你的问题,请参考以下文章

C++三大特性---继承

C++三大特性(继承封装多态)

C++三大特性-继承

C++三大特性-继承

[C++] 面向对象语言的三大特性--继承

C++三大特性之继承,由浅入深全面讲解,由基础语法到深度刨析。