c++核心技术04(继承)

继承和派生的概念

  譬如说我们写代码的时候,想要定义一个人类和一个男人类,已知人类和男人类有共同属性和方法,那么直接去写男人类的话,就会造成代码有很多重复的地方,这就会破坏代码的可维护性,所以这里我们就要c++的三大核心特性之一:继承。
  c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。

  那么派生是什么呢?派生和继承,本质是相同的,只是从不同角度来描述。假设有一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。用一句话概括就是,父亲"派生"出儿子,儿子"继承"自父亲。
  派生类中的成员,包含两大部分:
  1、一类是从基类继承过来的,一类是自己增加的成员。
  2、从基类继承过过来的表现其共性,而新增的成员体现了其个性。
  下图是继承和派生在UML中的表示:

  注意事项:
  除了“构造函数”和“析构函数”,父类的所有成员函数,以及数据成员,都会被子类继承。(这句话也比较好理解,就相当于父亲和儿子的出生方式和死亡方式不同)


继承和派生的代码实现

  下面让我们初步了解一下继承和派生
  Father.h:

#pragma once
#include <string>

using namespace std;

class Father
{
public:
	Father(const char*name,int age);
	~Father();

	string getName();
	int getAge();
	string description();

private:
	int age;

protected:
	string name;
};


  Son.h

#include "Son.h"
#include <iostream>
#include <sstream>
//创建Son对象,会调用构造函数!
//会先调父类的构造函数,用来初始化从父类继承的数据
//再调用自己的构造函数,用来初始化自己定义的数据
Son::Son(const char* name, int age, const char* game):Father(name,age)//用儿子类的两个参数去调用父类的构造函数
{
	cout << __FUNCTION__ << endl;//打印函数名称
	//没有体现父类的构造函数,那就会自动调用父类的默认构造函数!
	this->game = game;//这个对象的game被赋值为这个参数的game
}

Son::~Son() {

}

string Son::getGame() {
	return game;
}

string Son::description()
{
	stringstream ret;

	//子类的成员函数中,不能直接访问从父类继承的private成员
	ret << "name:" << getName() << "age:" << getAge()
		<< "game:" << game;
	return ret.str();
}

  Father.cpp:

#include "Father.h"
#include <sstream>
#include <iostream>

Father::Father(const char*name,int age)
{
	cout << __FUNCTION__ << endl;
	this->name = name;
	this->age = age;
}

Father::~Father()
{

}

int Father::getAge()
{
	return age;
}

string Father::getName()
{
	return name;
}

string Father::description() 
{
	stringstream ret;
	ret << "name:" << name << "age:" << age;
	return ret.str();
}

  Son.cpp:

#include "Son.h"
#include <iostream>
#include <sstream>
//创建Son对象,会调用构造函数!
//会先调父类的构造函数,用来初始化从父类继承的数据
//再调用自己的构造函数,用来初始化自己定义的数据
Son::Son(const char* name, int age, const char* game):Father(name,age)//用儿子类的两个参数去调用父类的构造函数
{
	cout << __FUNCTION__ << endl;//打印函数名称
	//没有体现父类的构造函数,那就会自动调用父类的默认构造函数!
	this->game = game;//这个对象的game被赋值为这个参数的game
}

Son::~Son() {

}

string Son::getGame() {
	return game;
}

string Son::description()
{
	stringstream ret;

	//子类的成员函数中,不能直接访问从父类继承的private成员
	ret << "name:" << getName() << "age:" << getAge()
		<< "game:" << game;
	return ret.str();
}

  main.cpp:

//父类 子类
#include <iostream>
#include "Father.h"
#include "Son.h"

using namespace std;

int main(void) {
	Father wjl("王健林", 68);
	Son wsc("王思聪", 32,"电竞");

	cout << wjl.description() << endl;
	
	//子类对象调用方法时,先在自己定义的方法中去寻找,如果有,就调用自己定义的方法
	//如果找不到,就到父类的方法中去找,如果有,就调用父类的这个同名方法
	//如果还是找不到,就是发生错误!
	
	cout << wsc.description() << endl;

	return 0;
}

派生类访问控制

  派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
  派生类的访问权限规则如下:

公有派生私有派生保护派生
基类属性派生类权限基类属性派生类权限基类属性派生类权限
私有不能访问私有不能访问私有不能访问
保护保护保护私有保护保护
公有公有公有私有公有保护

  访问权限总结:
  public:外部可以直接访问,可以通过对象来访问这个成员
  private:外部不可以访问,自己的成员函数内可以访问
  protected:和private非常相似,他们的唯一区别是,protected对子类的成员函数可以直接被访问,private对子类的成员函数不可以访问,即如果一个类的成员希望被子类直接访问,但是又不想被外部访问,那么就可以把这些成员定义为protected访问权限

继承中的构造和析构

调用父类的哪个构造函数

  代码如下:

class Son:public Father {
public:
	//在子类的构造函数中,显示调用父类的构造函数
	Son(const char*name,int age,const char *game):Father(name,age){
		this->game = game;
	}
	//没有显示的调用父类的构造函数,那么会自动调用父类的默认构造函数
	Son(const char *name,const char *game) {
		this->game = game;
	}
};

子类和父类的构造函数的调用顺序

  代码如下:

#include <iostream>
#include <stdio.h>

using namespace std;


class M {
public:
	M() {
		cout << __FUNCTION__ << endl;//打印这个函数的名字
	}
};

class N
{
public:
	N() {
		cout << __FUNCTION__ << endl;
	}
};

class A {
public:
	A()
	{
		cout << __FUNCTION__ << endl;
	}
};

class B :public A {
public:
	B()
	{
		cout << __FUNCTION__ << endl;
	}
private:
	M m1;
	M m2;
	static N ms;
};

N B::ms;//静态成员

int main()
{
	B b1;
	printf("*****************\n");
	B b2;
}

  运行结果如下图所示:
在这里插入图片描述
  所以如上述所示:当创建子类对象时,构建函数的调用顺序:静态数据成员的构造函数->父类的构造函数->非静态的数据成员的构造函数->自己的构造函数

  注意:无论创建几个对象,该类的静态成员只构建一次,所以静态成员的构造函数只调用1次!!!

子类的析构函数

  代码如下:

//子类的构造和析构
#include <iostream>
#include <stdio.h>

using namespace std;


class M {
public:
	M() {
		cout << __FUNCTION__ << endl;//打印这个函数的名字
	}
	~M() {
		cout << __FUNCTION__ << endl;
	}
};

class N
{
public:
	N() {
		cout << __FUNCTION__ << endl;
	}
	~N() {
		cout << __FUNCTION__ << endl;
	}
};

class A {
public:
	A() {
		cout << __FUNCTION__ << endl;
	}
	~A() {
		cout << __FUNCTION__ << endl;
	}
};

class B :public A {
public:
	B() {
		cout << __FUNCTION__ << endl;
	}
	~B() {
		cout << __FUNCTION__ << endl;
	}
private:
	M m1;
	M m2;
	static N ms;
};

N B::ms;//静态成员

int main()
{
	{
		B b1;
		//printf("*****************\n");
		//B b2;
	}
}

  运行结果如下:
在这里插入图片描述
  由此可见:子类的析构函数的调用顺序与子类的构造函数的的调用顺序相反;

 &emps;注意:静态对象在程序终止时被销毁,所以:静态成员的析构函数,在程序结束前,是不会被调用的!


子类型关系

  代码如下:

#include <iostream>

using namespace std;

class A {
public:
	A(){}
	~A(){}
	void kill() { cout << "A.kill." << endl; }
};

class B :public A {
public:
	B(){}
	~B(){}
	void kill() { cout << "B.kill." << endl; }
};

void test(A a)
{
	a.kill();//调用的是A类对象
}

void test2(B b)
{
	b.kill();
}

int main()
{
	A a;
	B b;
	test(a);
	test(b);
	//test2(a);//父类型不能转换给子类对象
	return 0;
}

 &emsp;运行结果:

在这里插入图片描述

  由上述代码可知,子类型的作用是在需要父类对象的任何地方,可以使用“公有派生”的子类的对象来替代,从而可以使用相同的函数统一处理基类对象和公有派生类对象
即:形参为基类对象时,实参可以是派生类对象
  那么由此我们得知子类型的应用有如下三种:
  1) 1. 基类(父类)的指针,可以指向这个类的公有派生类(子类型)对象。

  Son yangGuo;
  Father * f = &yangGuo;

  2) 公有派生类(子类型)的对象可以初始化基类的引用
  

   Son yangGuo;
  Father &f2 = yangGuo;

  3) 公有派生类的对象可以赋值给基类的对象

  Son yangGuo;
  Father f1 = yangGuo;

   注意:以上的应用,反过来就会编译失败


继承中同名成员的处理方式

  代码如下:

#include <iostream>

using namespace std;

class TestA
{
private:
	int m_a;
public:
	void show()
	{
		cout<<"this is TestA"<<endl;
	}
};
class TestB:public TestA
{
private:
	int m_a;
public:
	void show()
	{
		cout<<"This is TestB"<<endl;
	}
};
int main(int argc, const char *argv[])
{
	TestB tb;
	cout<<sizeof(tb)<<endl;
	tb.show(); //同名,默认调用派生类成员函数,基类的同名成员会被隐藏
	tb.TestA::show();
	return 0;
}

  由上述代码可知:

  1. 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
  2. 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)

继承中静态成员特性

  代码如下:

#include <iostream>

using namespace std;

class Father
{
public:
	static int getSum()
	{
		return sum;
	}
	static int getSum(int a)
	{
		return sum + a;
	}
public:
	static int sum;
};

int Father::sum = 10;

class Son :public Father
{
public:
	static int getSum()
	{
		return sum;
	}

	static int getSum(int a)
	{
		return sum + a;
	}
public:
	static int sum;
};

int Son::sum = 13;

void test()
{
	//1.通过对象访问
	Son son;
	cout << "Son的sum=" << son.sum << endl;
	cout << "Father的sum=" << son.Father::sum << endl;
	//2.通过类名访问
	cout << "Son的sum = " << Son::sum << endl;

}

void test2()
{
	//通过对象
	Son son;
	cout << "Son的Sum的值" << son.getSum() << endl;
	
	cout << "Father的Sum的值" << son.Father::getSum() << endl;

	//通过类名
	cout << "Son的Sum的值" << Son::getSum() << endl;
	
}

int main()
{
	test();
	test2();
	return 0;
}

  运行结果如下:
在这里插入图片描述

  所以由此我们可以知道静态成员函数和非静态成员函数的共同点:

  1. 他们都可以被继承到派生类中。
  2. 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
  3. 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。

多继承

多重继承的定义

  我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。所以在Java、c#等语言中直接取消了多继承的概念,以避免复杂性。

多重继承的使用

  代码如下:
  Father.h:

#pragma once
#include <iostream>
#include <string>

using namespace std;

class Father
{
public:
	Father(const char* lastName = "无姓", const char* firstName = "无名");
	~Father();
	void playBasketball();//打篮球

private:
	string lastName;
	string firstName;
};

  Mother.h:

#pragma once
#include <iostream>
#include <string>

using namespace std;

class Mother
{
public:
	Mother(const char* food, const char* lastName = "无姓", const char* firstName = "无名");
	~Mother();

	void dance(); //跳舞
private:
	string lastName;
	string firstName;

	string food;
};

  Son.h:

#pragma once
#include "Father.h"
#include "Mother.h"

class Son:public Father,public Mother
{
public:
	Son(const char* lastName, const char* firstName, const char* food, const char* game);
	~Son();

	void playGame();
private:
	string game;
};


  Father.cpp:

#include "Father.h"

Father::~Father()
{

}

Father::Father(const char* lastName, const char* firstName)
{
	this->lastName = lastName;
	this->firstName = firstName;
}

void Father::playBasketball()
{
	cout << "我要投三分球" << endl;
}

  Mother.cpp:

在这里插入代码片#include "Mother.h"

Mother::~Mother()
{

}

Mother::Mother(const char* food,const char* lastName , const char* firstName)
{
	this->food = food;
	this->lastName = lastName;
	this->firstName = firstName;
}

void Mother::dance()
{
	cout << "一起跳舞" << endl;
}```

&emsp;&emsp;Son.cpp:

```cpp
#include "Son.h"

Son::Son(const char* lastName, const char* firstName, const char* food, const char* game) :Father(lastName, firstName), Mother(food)
{
	this->game = game;
}

void Son::playGame()
{
	cout << "一起玩" << game << "吧" << endl;
}


Son::~Son()
{

}

  main.c:

#include "Father.h"
#include "Mother.h"
#include "Son.h"

int main()
{
	Son wsc("王", "思聪", "川菜", "吃鸡");

	wsc.playBasketball();
	wsc.dance();
	wsc.playGame();

	return 0;
}

  运行结果如下:
在这里插入图片描述


多继承的构造函数的调用顺序

  基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。


多继承的二义性问题

  什么叫做多重继承的二义性问题呢?就是如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本。
  首先我们可以用上述代码进行测试,例如在父亲的成员函数里面也加一个跳舞方法,并定义为我要跳广场舞,并将母亲的跳舞方式改为我要跳芭蕾进行区分,其他代码不变,这里只展示改动的代码,则代码如下:
  Father类:

class Father
{
public:
	Father(const char* lastName = "无姓", const char* firstName = "无名");
	~Father();
	void playBasketball();//打篮球
	void dance();
private:
	string lastName;
	string firstName;
};

  Father.cpp的dance方法:

void Father::dance()
{
	cout << "我要跳广场舞" << endl;
}

  Mother.cpp的dance的方法:

void Mother::dance()
{
	cout << "一起跳芭蕾" << endl;
}

  那么会出现如下图所示的问题,也就是我们一开始所说的,通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本。
在这里插入图片描述
  那么我们该如何解决呢?
  方法一:
  使用 “类名::” 进行指定, 指定调用从哪个基类继承的方法

wsc.Father::dance();
wsc.Mother::dance();

  方法二:
  在子类中重新实现这个同名方法, 并在这个方法内部, 使用基类名进行限定,来调用对应的基类方法

wsc.dance();
void Son::dance() {
  Father::dance();
  Mother::dance();
  cout << “跳街舞” << endl;
}


虚基类和虚继承

菱形继承

  讲到虚继承,就不得不说菱形继承,因为虚继承就是为了解决菱形继承所产生问题的,那么什么是菱形继承呢,菱形继承如下图所示:
在这里插入图片描述
  由图所示:菱形继承就是由两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。并且这种继承会带来如下两个问题:

  1. 座机继承了电话的数据和函数,手机同样继承了电话的数据和函数,当无线座机调用函数或者数据时,就会产生二义性。
  2. 无线座机继承自电话的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
      代码如下图所示:
#include <iostream>
#include <string>

using namespace std;

//电话类
class Tel
{
public:
	Tel() {
		this->number = "未知";
	}
protected:
	string number;
};

//座机类 
class FixedLine :public Tel {
	
};


//手机类
class MobilePhone :public Tel
{

};

//无线座机类
class WirelessTel :public FixedLine, public MobilePhone
{
public:
	void setNumber(const char* number) {
		this->number = number;
	}
};

int main()
{
	WirelessTel phone;

	system("pause");
	return 0;
}

  则上述代码会报错,报错如下图所示:
在这里插入图片描述


虚基类和虚继承

  那么既然有上述问题,我们就可以采取虚继承里解决,代码如下:

#include <iostream>
#include <string>

using namespace std;

//电话类
//此时共同的祖先Tel类,就成为"虚基类"
class Tel
{
public:
	Tel() {
		this->number = "未知";
	}
protected:
	string number;
};

//座机类 
class FixedLine :virtual public Tel {//虚继承
	
};


//手机类
class MobilePhone :virtual public Tel
{

};

//无线座机类
class WirelessTel :public FixedLine, public MobilePhone
{
public:
	void setNumber(const char* number) {
		this->number = number;
	}

	string getNumber() {
		return this->number;
	}
};

int main()
{
	WirelessTel phone;

	phone.setNumber("3842942940");
	cout << phone.getNumber() << endl;
	system("pause");
	return 0;
}

  运行结果如下:
在这里插入图片描述
  注意:最好少用多继承

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值