继承

继承的概念与定义

继承的概念

1:继承的机制是面向对象程序设计,使代码可以复用的手段,它允许我们保持原有类的基础上,可以新增方法(成员函数)和属性(成员变量)这样产生的新类称为“派生类”,而被继承的类称为“基类”。继承呈现了面向对象程序设计的结构层次。

#include<iostream>
using namespace std;

class Person
{
public:
	void Func()
	{
		cout << "void Func()" << endl;
	}
protected:
	string _name;//名字
	string _address;//地址
	string _tel;//电话
	int _age;
};

class Students :public Person
{
public:
	void Study()//学习
	{

	}

protected:
	int Stuid;//学号
};

class Teacher :public Person
{
public:
	void teach()//授课
	{

	}
protected:
	int _Teachid;//教师编号
};

int main()
{
	Students s;
	Teacher t;

	s.Func();
	t.Func();
	return 0;
}

Students 和Teacher都继承了Person,Students和Teacher类中都有自己的成员函数,可以进行自主的学习和授课,也可以调用基类中的函数进行实现。

继承的定义

class Student :public Person
{
public:
	int _stuid;//学号
	int _majuc;//专业
};

Student是派生类

public是继承方式

Person是基类

继承的方式和访问

继承方式和访问
类成员/继承方式public继承protected继承private继承
基类的public成员继承基类的public成员继承基类的protected成员继承基类的private成员
基类的protected成员继承基类的protected成员继承基类的protected成员继承基类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

总结:基类的私有成员(private)都是不能被派生类访问的,但是基类的其他成员都是按照最小的访问方式进行访问Min(public>protected>private)继承方式和基类成员的定义,如果哪个是Min就按照Min的方式进行访问

class Person
{
public:
	void Func()
	{
		cout << "void Func()" << endl;
	}
protected:
	string _name = "张三";//名字
	string _address;//地址
	string _tel;//电话
	int _age;
private:
	int a;
};

class Students :public Person
{
public:
	void Study()//学习
	{
		cout << _name << endl;
		//cout << a << endl;//基类中的private成员是不能被访问
	}

protected:
	int Stuid;//学号
};

int main()
{
	Students s;
	s.Func();
	s.Study();//基类中的protected成员是可以被访问的
	return 0;
}

继承中的作用域

1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。

2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

class Person
{
protected:
	string _name = "李四";
	int _num = 111;
};

class Student :public Person
{
public:
	void Print()
	{

		cout << "姓名:" << _name << endl;
		cout << "学号:" << _num << endl;
		cout << "学号:" << Person::_num << endl;

	}
protected:
	int _num = 9999;
};

int main()
{

	Student s;
	s.Print();
	return 0;
}

这里的Student中的_sum和Person中的_sum就构成了隐藏的关系

派生类的默认成员函数

1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。 

2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。

3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域

4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派 ⽣类对象先清理派⽣类成员再清理基类成员的顺序。

5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。

6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构

class Person
{
public:
	Person(const char* name = " ")//有缺省值的默认构造
		:_name(name)
	{
		cout << "Person" << endl;
	}

	Person(const Person& p)//拷贝构造
		:_name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
		delete[] _str;
	}
protected:
	string _name;
	char* _str = new char[10] {'x', 'y', 'z'};
};

class Student :public Person
{
public:
	Student(const char* name = " ", int x = 0,const char*address=" ")
		:_x(x)
		,_address(address)
		,_name(Person::_name+'x')
		,Person(name)
//初始化列表会按照声明的顺序优先构造父类
//构造时只能先父后子,因为子类构造初始化时会用到父类成员,如果此时没有初始化父类就会形成随机值
	{ }

	Student(const Student& st)
		:Person(st)
		, _x(st._x)
		, _address(st._address)
	{ }

	Student& operator=(const Student& st)
	{
		if (this != &st)
		{
			Person::operator=(st);
			_x = st._x;
			_address = st._address;
		}

		return *this;
	}

	//由于多态,析构函数名字会被统一处理成destructor()
	//基类析构不能够显示调用,显示调用就不能保证先子后父
	~Student()
	{
		cout << "~Student()" << endl;
		//cout << _str << endl;
	}
//析构时必须先子后父,因为子类可能用到父类的成员如果先析构父类就会导致重复析构出现问题
//所以父类析构时不能显示调用,子类析构之后会自动调用
protected:
	int _x = 1;
	string _address = "河南省郑州市";
	string _name;
};

//构造时是先父后子
//析构时是先子后父

int main()
{
	Student s;
	Student s1("张三", 1, "河南省郑州市");
	return 0;
}

子类默认生成的构造函数

1.父类成员(整体)--默认构造

2.子类自己的内置成员--不处理

3.子类自己的自定义成员--默认构造

子类默认生成的拷贝构造  赋值重载和拷贝构造类似

1.父类成员整体 --调用父类的拷贝构造

2.子类自己的内置成员 --值拷贝

3.子类自己的自定义成员 --调用它的拷贝构造

一般都不需要自己写,如果子类成员涉及到深拷贝,就必须自己实现

子类默认生成的析构

1.父类成员(整体)--调用父类的析构

2.子类自己的内置成员--不处理

3.子类自己的自定义成员--调用析构

继承与友元

友元关系不能继承,基类的友元函数不能被派生类访问私有和保护成员

class Student;//这里必须前向声明,是为了解决名字尚未出现就被使用的问题
class Person
{
public:
	friend void Display(const Person& p, const Student& s);//在这里Student被使用,前向声明是告诉编译器,Student是一个类后面会被定义
protected:
	string _name;
};

class Student :public Person
{
public:
	friend void Display(const Person& p, const Student& s);

protected:
	int _stuid;
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuid << endl;
}

int main()
{
	Person p;
	Student s;

	Display(p, s);
	return 0;
}

如果Student类未使用Display的友元,那么就会报错,无法访问到Student的protected的成员,唯一的解决办法就是将Display也定义成Student的友元函数

继承与静态成员

基类定义了一个static成员,则整个继承体系都有这一个成员,无论后面继承多少个派生类,都只有一个static成员

class Person
{
public:
	string _name;
	static int count;//声明
};

int Person::count = 0;//定义//static变量存在于静态区,属于整个程序

class Student :public Person
{
protected:
	int _sutid;
};

int main()
{
	Person p;
	Student s;
	int m = 0;//栈的成员变量存在与栈内存,出作用域就自动销毁

	int* n = new int[0];//堆成员变量,存储于堆内存,出作用域不消毁
	delete[]n;//只能手动销毁

	cout << &p._name << endl;
	cout << &s._name << endl;
	//_name两者的地址是不一样的,因为子类继承父类的成员对象时又拷贝了一份

	cout << &p.count << endl;
	cout << &s.count << endl;
	//两者的地址相同说明二者共用一份成员
	return 0;

}

多继承和菱形继承

单继承:一个派生类只继承了一个基类就是单继承

class Person
{}:
class Student:public Person
{}:

多继承:一个派生类继承了两个或两个以上就是继承

class Student
{};
class Teacher
{};

class Assiert:public Student,public Teacher
{};

菱形继承:菱形继承是多继承的一种特殊形式,菱形继承的问题在Assiert类中有两份Person的对象,导致数据出现了数据冗余和二义性的问题

class Person
{
public:
	string _name;
};

class Student :public Person
{
protected:
	int _sum;
};

class Teacher :public Person
{
protected:
	int _id;
};

class Assistant :public Student, public Teacher
{
protected:
	string _majiny;
};

int main()
{
	Assistant a;
	//a._name="lisi";//编译报错,因为继承了两个类中都有name的变量,导致调用不明确
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";//需要显示指定访问基类的作用域
    cout<<a._name<<endl;//还是不能够访问
	
	return 0;
}


虚继承

虚继承可以解决数据冗余和二义性的问题

class Person
{
public:
	string _name;
};

class Student : virtual public Person
{
protected:
	int _sum;
};

class Teacher : virtual public Person
{
protected:
	int _id;
};

class Assistant :public Student, public Teacher
{
protected:
	string _majiny;
};

int main()
{
	Assistant a;


	a._name = "李四";
	a.Student::_name = "李同学";
	a.Teacher::_name = "李老师";
	cout << a._name << endl;

}

让继承自同一虚基类的所有派生类只共享一份虚基类子对象,而不是各自保留一份副本。virtual加入到能够产生数据重复的类中;

继承和组合

继承:继承是is-a的关系,说明每一个派生类对象都是一个基类对象

组合:组合是has-a的关系,如果A组合了B,说明B中都有一个A类的对象

class Tire
{
protected:
	string _brand = "米其林";//品牌
	size_t _size = 19;//尺寸
};
class Car
{
protected:
	string _colour = "白色";//颜色
	string _num = "京A00001";//车牌

	Tire _t1;// 轮胎
	Tire _t2;
	Tire _t3;
	Tire _t4;
};

class BMW : public Car
{
public:
	void Dirve()
	{
		cout << "好开--操控" << endl;
	}
};

class Audi : public Car
{
public:
	void Dirve()
	{
		cout << "舒适--稳定" << endl;
	}
};

int main()
{
	BMW b;
	Audi a;
	b.Dirve();
	a.Dirve();

	return 0;
}

组合就是里面有一个功能,而不是是一个功能,一辆车有轮胎和引擎而不是一辆车是轮胎和引擎

  • 组合不会破坏封装,不会暴露父类实现细节。

  • 组合可以在运行时动态替换行为(策略模式、依赖注入)。

  • 继承应仅用于真正的“is-a”关系,且父类设计为继承而设计(如抽象类、模板方法)

template<class T>
class vector
{ };

//Stack和vector既符合is-a的关系也符合has-a的关系
template<class T>
class stack : public vector<T>
{ };

template<class T>
class stack
{
public:
	vector<T> _v;
};

int main()
{
	return 0;
}

虚函数的调用:

编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

class A
{
  public:
   virtual void f()
  {
    cout<<"A::f()"<<endl; 
  }
};

class B:public A
{
  private:
   virtual void f()
   {
     cout<<"B::f()"<<endl;
  }

};

int main()
{
  A*p=new B;
  
 pa->f();//B::f()
 return 0;
}

通过基类指针pa调用,访问权限以指针的静态类型A为准,而生成对象的地址是new B指向B

静态类型是编译器在编译期间根据变量,指针 ,引用的声明看到的类型,与运行时指向的类型无关,静态看声明,动态看对象

如果改为B*p=new B,则f()在private中就无法被外部访问编译报错

因为f()是虚函数,所以运行时跟动态类型决定的调用有关B::f()

虚函数是根据动态类型实现运行时多态的机制

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐