接下来我们细说一下继承

发布时间:2025-06-24 18:39:57  作者:北方职教升学中心  阅读量:393


我们前面接触到的都是函数层次的复用,遇到过的层次的复用有模板,而继承是类层次的一种新的复用

int main(){	B b;	b.fun(10);	return 0;};

如果就是想要父类里的fun函数,直接指定定义域调用。

class Student //学生{public:	void identity() //身份认证	{		//...	}protected:	string _name; //姓名	size_t _age;  //年龄	string _add;  //住址	string _tel;  //电话};
class Teacher  //老师{public:	void identity() //身份认证	{		//...	}protected:	string _name; //姓名	size_t _age;  //年龄	string _add;  //住址	string _tel;  //电话};

学生特有的变量是学号和学习相关的成员函数。

那我们把公共的信息提取出来,放在一个Same类里面,Student和Teacher这两个类复用这个类,就不用重复定义了。

4.1.4 析构

  • 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。

在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤
protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实
际中扩展维护性不强。
  • 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected
  • 如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤

    Student& operator=(const Student& s){	if (this != &s)	{		Same::operator=(s);//显示调用基类的赋值重载		_num = s._num;		_add = s._add;		//深拷贝逻辑	}	return *this;}

    显示调用父类(基类)的operator=时,要指定类域,因为同名函数子类的会把父类的隐藏,屏蔽基类对同名成员的直接访问,如果不指定类域,会造成栈溢出。

class Same final //基类加final后不可被继承{public:	//成员函数protected:	string _name;};

本次分享见到这里,我们下篇见~

class Same //基类{public:	Same(const char* name) //此时基类没有默认构造		: _name(name)	{		cout << "Same()" << endl;	}protected:	string _name;};
class Student : public Same{public:	Student(const char* string, int num, const char* add)		:Same(string) //在初始化列表阶段显示调用		, _num(num)		,_add(add)	{}protected:	int _num;	string _add;};

然后我们传参,就可以了。 

  • 编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系

    //学生class Student : public Same{public:	void study() //学习	{		//...	}protected:	string _stuid;//学号 };
    //老师class Teacher : public Same {public:	void teaching() //教书	{		//...	}protected:	string _title;//职称};

    这就是继承的意义。年龄等成员变量,都有身份认证相关成员函数。

    #define CONTAINER std::vector //宏template<class T>class stack : public CONTAINER<T> //继承{public:	void push(const T& x)	{		CONTAINER<T>::push_back(x);	}	void pop()	{		CONTAINER<T>::pop_back();	}	const T& top()	{		return CONTAINER<T>::back();	}	bool empty()	{		return CONTAINER<T>::empty();	}};

    宏替换,就可以改变stack的底层逻辑,可以换成list,deque。

    class Student //学生{public:	void identity() //身份认证	{		//...	}	void study() //学习	{		//...	}protected:	string _name; //姓名	size_t _age;  //年龄	string _add;  //住址	string _tel;  //电话	string _stuid;//学号 };

    老师特有的变量是职称和教书相关的成员函数。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。这⾥有个形象的说法叫切⽚或者切割

  • 需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。
  • 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏

所以我们如果需要在子类(派生类)自己写析构函数时,不可以像下面这样。接下来我们细说一下继承。

~Student(){	//资源释放逻辑...	//自动调用父类析构}

 

 

4.2 实现一个不能被继承的类

⽅法1:将基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。老师和学生都有姓名、

而调用析构次数太多会出问题。

如果我们不写继承方式,使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。)

这里stack的实现就是用了继承来实现。

如果我们要在派生类(子类)中显示地写拷贝构造,写法如下。

int main(){	Student st1("张三", 0, "Chain");	Student st2("李四", 1, "LA");	st1 = st2;	return 0;}

也是内置类型值拷贝,自定义类型调用自己的operator=,继承父类成员看作整体,调用父类的operator=。(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)

  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  • 比如说现在有 Student类的指针ptr1Same类指针ptr2,ptr1赋值给ptr2,就是派生类(子类)对象赋值给基类(父类),ptr2只会指向ptr1中基类有的部分。

    1.2.2 继承基类成员访问⽅式的变化

    由上面的表我们可以观察到:

    • 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的

      Student(const Student& s) //显示地写子类的拷贝构造	:Same(s)//父类的拷贝构造	,_add(s._add)	,_num(s._num){	//假设里面是深拷贝资源的拷贝逻辑}

      Same是父类,Same后面的括号里应该传父类的对象,但是我们没有父类的对象,只有子类的对象s,为什么可以直接传s过去?

      这里用到的就是前面说过的  子类和父类对象赋值兼容转换  。protected继承。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。自己实现的话,写法如下。

      template<class T>class stack : public std::vector<T> //继承vector{public:	void push(const T& x)	{		vector<T>::push_back(x);	}	void pop()	{		vector<T>::pop_back();	}	const T& top()	{		return vector<T>::back();	}	bool empty()	{		return vector<T>::empty();	}};

      (注意:基类是类模板时,需要指定⼀下类域, 否则编译报错:error C3861: “push_back”: 找不到标识符   相关的错误。

      int main(){	Student st("张三", 0, "Chain");	return 0;}

      4.1.2 拷贝构造

      1.对内置类型 -> 值拷贝

      2.对自定义类型 -> 调用自己的拷贝构造函数

      3.对于继承成员看作一个整体对象,要求调用父类的拷贝构造

      • 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。可以看出保护成员限定符是因继承才出现的。

        如果我们就想访问基类(父类)里的_name,可以直接用 基类::基类成员 显⽰访问,如下。这个代码还可以进行改进,如下。

      class Same //基类{public:	Same(const char* name = "peter") //默认构造		: _name(name)	{		cout << "Same()" << endl;	}	Same(const Same& p) //拷贝构造		: _name(p._name)	{		cout << "Same(const Same& p)" << endl;	}	Same& operator=(const Same& p) //赋值重载	{		cout << "Same& operator=(const Same& p)" << endl;		if (this != &p)			_name = p._name;		return *this;	}	~Same() //析构	{		cout << "~Same()" << endl;	}protected:	string _name;};

      派生类(子类)默认生成的析构函数就够了,如果有需要资源释放的时候才需要自己实现。

      int main(){	B b;	b.fun(10);	b.A::fun(); //指定	return 0;};

       

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

      4.1 四个常见的默认成员函数

      4.1.1 默认构造

      我们不写,编译器默认生成的构造函数的行为:

      1.对内置类型->是否初始化是不确定的。

    3.2 相关练习

    class A{public:	void fun()	{		cout << "func()" << endl;	}};class B : public A{public:	void fun(int i)	{		cout << "func(int i)" << i << endl;	}};int main(){	B b;	b.fun(10);	b.fun();	return 0;};

    答案: B.隐藏

    为什么不是重载?因为函数构成重载的要求是两个函数在同一作用域

    1.2 继承的定义

    1.2.1 定义的格式

    前面定义的Same类就是一个父类,也称作基类;Student类是子类,也称作派生类。

    Student st;//子类对象Same sa = st; //子类对象赋值给父类对象Same* psa = &st;//子类对象赋值给父类指针Same& rsa = sa; //子类对象赋值给父类引用

    (这里所有的赋值都不会产生临时变量,因为子类直接做了切片给父类)

    父类(基类)对象不能赋值给子类(派生类)。

     假如我们不显示写基类的默认构造,就必须在派生类显示调用。

     如果有需要深拷贝的资源,才需要自己实现。而父类和子类有独立的作用域。

     假如现在我们模拟校园环境,设计老师(Teacher)和学生(Student)两个类。

    class Same //基类{public:	Same(const char* name = "peter") //默认构造		: _name(name)	{		cout << "Same()" << endl;	}	Same(const Same& p) //拷贝构造		: _name(p._name)	{		cout << "Same(const Same& p)" << endl;	}	Same& operator=(const Same& p) //赋值重载	{		cout << "Same& operator=(const Same& p)" << endl;		if (this != &p)			_name = p._name;		return *this;	}protected:	string _name;};
    class Student : public Same //派生类{public:	Student(const char* string, int num, const char* add)		:Same(string) //在初始化列表阶段显示调用		, _num(num)		,_add(add)	{}	    //没有实现operator=protected:	int _num;	string _add;};

    operator=和拷贝构造差不多,我们不在派生类(子类)中显示地写赋值重载时,编译器自动生成的就够用。但是必须是基类的指针

    是指向派⽣类对象时才是安全的。

    我们先实现一个基类(父类)的赋值重载。

    int main(){	Student st1("张三", 0, "Chain");	Student st2(st1);	return 0;}

    _num是内置类型,进行值拷贝;_add是自定义类型string,调用string自己的拷贝构造;_name是父类继承成员,调用父类Same的拷贝构造。地址、

     比如说Same类是基类,我们显示写它的默认构造,Student类为派生类,不显示写。 

    ~Student(){	~Same(); //错误的写法	//资源释放逻辑...}

    既然构成隐藏,调用就需要指定类域调用。

    class Teacher : public Same{public:	void Print()	{		cout << Same::_name << endl; //指定作用域访问	}protected:	string _name = "456";};

    • 在继承体系中基类和派⽣类都有独⽴的作⽤域
    • 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。基类的其他成员在派⽣类的访问⽅式取权限小的:public > protected > private

      如果传参,像下面这样,就会调用子类里的fun函数。

    class Same //基类{public:	Same(const char* name = "peter") //默认构造		: _name(name) 	{		cout << "Same()" << endl;	}	Same(const Same& p) //拷贝构造		: _name(p._name)	{		cout << "Same(const Same& p)" << endl;	}protected:	string _name;};
    class Student : public Same //派生类{public:	Student(const char* string, int num, const char* add)		:Same(string) //在初始化列表阶段显示调用		, _num(num)		,_add(add)	{}    //拷贝构造一般不用自己写protected:	int _num;	string _add;};

    一般情况下,派生类(子类)默认生成的拷贝构造就够用了,不用自己写,如果有需要深拷贝的资源,才需要自己写。(这里也体现出private和protected的区别)

    • 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有(private)成员在派⽣类都是不可⻅。现在我们就介绍一下继承。

      2.父类和子类对象赋值兼容转换

      public继承的派⽣类对象可以赋值给基类的对象 / 基类的指针 / 基类的引⽤
    • 派⽣类对象初始化调⽤基类构造派⽣类构造。

      这里引用的就是子类对象中切出来的父类的那一部分

      class Same //基类{public:	Same(const char* name = "peter")		: _name(name)	{		cout << "Same()" << endl;	}protected:	string _name;};
      class Student : public Same  //派生类{public:	//没有显示写默认构造,编译器自己生成protected:	int _num;	string _add;};

      按照规则,_num是内置类型,可能初始化也可能不初始化;_add是自定义类型,调用string自己的初始化;继承还要调用父类的默认构造,所以_name应该被初始化为peter。

       继承方式有三个:public继承、

      class Same //基类{ protected:	string _name = "123";	size_t _age; };
      class Teacher : public Same //派生类{public:	void Print()	{		cout << _name << endl;	}protected:	string _name = "456";};

      那我们在派生类中访问_name的时候到底访问的是哪个?

      int main(){	Teacher t;	t.Print();	return 0;}

       结果是显示派生类(子类)里的_name。

      1.继承的概念及定义

      1.1 继承的概念

              继承机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段。(等以后细说)