c++之继承(上)

时间:2024-10-06 11:38:40

c++之继承

  • 1. 继承的概念及定义
    • 1.1 继承的概念
    • 1.2 继承定义
      • 1.2.1 定义格式
      • 1.2.2 继承基类成员访问⽅式的变化
    • 1.3 继承类模板
  • 2. 基类和派⽣类间的转换
  • 3. 继承中的作⽤域
    • 3.1 隐藏规则:
    • 3.2 考察继承作⽤域相关选择题
      • 3.2.1 A和B类中的两个func构成什么关系()
      • 3.2.1 下⾯程序的编译运⾏结果是什么()
  • 4. 派⽣类的默认成员函数
    • 4.1 4个常⻅默认成员函数

1. 继承的概念及定义

1.1 继承的概念

继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有
类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承
呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的
复⽤,继承是类设计层次的复⽤。

简单的来说继承就是对代码的复用,例如下面的代码:

class student
{
public:

private:
	string _name;
	string _address;
	string _tel;
	int _age;
	int _stuid;//学号
};

class teacher
{
public:
private:
	string _name;
	string _address;
	string _tel;
	int _age;
	string _tea;//职称
};

teacher这个类与student这个类有共同的成员变量,甚至我们也可以给定一些共同的成员函数,大家都知道重复不是好的方式,为了解决重复问题,之前的学习引入了模版的概念,但是在这里不能用模版,因为模版是两个相同的东西,只有操作数的类型不一样,而这里是有不同的东西的。

class person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "zhangsan";
	string _address;
	string _tel;
	int age;
};//相同的部分,这一部分我们将其复用于student与teacher里,我们一般称之为父类或者基类


class student:public person
{
public:
	void study()//学生具有学习这个行为
	{

	}
protected:
	int _stuid;//学号
};

class teacher :public person
{
public:
	void teach()//老师具有教书这个行为
	{

	}
protected:
	string _teaid;//教师职称
};

int main()
{
	teacher t;
	t.identity();

	student s;
	s.identity();
	return 0;
}

1.2 继承定义

1.2.1 定义格式

下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)
在这里插入图片描述

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

在这里插入图片描述
在日常生活中我们基本只会用到第一列,但在考试是会考其他的,所以我们也要了解。
这里其实也不需要我们去背,我们将上面的继承分为两类:
一、基类的私有成员:所有继承方式都不可见。
二、基类的其他成员:继承方式与成员类型取小即可,在这里public>protected>private。例如基类的public成员,private继承,private小所以在派生类是private成员。

class person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
private:
	string _name = "zhangsan";
protected:
	//string _name = "zhangsan";
	string _address;
	string _tel;
	int age;
};//相同的部分,这一部分我们将其复用于student与teacher里,我们一般称之为父类或者基类

//teacher与student都是公有继承,当我们的成员变量或函数是私有成员时,在子类和类外部都不能调用,
//而当其为保护成员时,子类可以调用,但类外部不能调用,这里就是c++引入保护成员的意义。

class student:public person
{
public:
	void study()//学生具有学习这个行为
	{
		
	}
protected:
	int _stuid;//学号
};

class teacher :public person
{
public:
	void teach()//老师具有教书这个行为
	{

	}
protected:
	string _teaid;//教师职称
};

int main()
{
	teacher t;
	t.identity();
	

	student s;
	s.identity();
	return 0;

}

1.3 继承类模板

这里我们用栈来模拟

这是第一种方式,指定底层为vector

namespace TSY
{
	template<class T>
	class stack:public std::vector<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();
		}

		size_t size()
		{
			return Container<T>::size();
		}
	};
}

int main()
{
	TSY::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);


	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

这是第二种方式,可以随时切换我们的底层存储结构,因为宏本质是展开,所以不为影响代码的正确性,反而更符合c++的封装

#define Container std::vector
//#define Container std::list
//#define Container std::deque
namespace TSY
{
	template<class T>
	//class stack:public std::vector<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();
		}

		size_t size()
		{
			return Container<T>::size();
		}
	};
}

int main()
{
	TSY::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);


	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

2. 基类和派⽣类间的转换

• public继承的派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

• 基类对象不能赋值给派⽣类对象。

• 基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再单独专⻔讲解,这⾥先提⼀下)

在这里插入图片描述

class person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
private:
	string _name = "zhangsan";
protected:
	//string _name = "zhangsan";
	string _address;
	string _tel;
	int age;
};

class student:public person
{
public:
	void study()//学生具有学习这个行为
	{
		
	}
protected:
	int _stuid;//学号
};

class teacher :public person
{
public:
	void teach()//老师具有教书这个行为
	{

	}
protected:
	string _teaid;//教师职称
};

int main()
{
	student stu;

	person* stu1 = &stu;
	person& stu2 = stu;
	person stu3 = stu;

	//子类给父类可以


	//父类给子类会出现问题
	person ps;

	//student stt1 = ps;
	//student stt2 = (student)ps;//强转也会出问题

	//支持父类的指针转换
	student* stt3 = (student*)stu1;
	//student* stt4 = dynamic_cast<student>(stu1);//这里涉及到虚函数与多态后面再讲
	return 0;
}

3. 继承中的作⽤域

3.1 隐藏规则:

  1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。
  2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
    在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
class person
{
public:
	
	void print()//同名函数
	{
		cout << "person::()" << endl;
		cout << _size;
	}
private:
	string _name = "zhangsan";
protected:
	//string _name = "zhangsan";
	string _address;
	string _tel;
	int age;//同名成员变量

	int _size = 100;
};

class student :public person
{
public:
	void print()//同名函数
	{
		cout << "student::()" << endl;
		cout << _size;
	}
protected:
	int _stuid;//学号
	int _size = 1000;//同名成员变量
};



int main()
{
	student stu;
	stu.print();
	return 0;
}

在这里插入图片描述

3.2 考察继承作⽤域相关选择题

3.2.1 A和B类中的两个func构成什么关系()

A. 重载 B. 隐藏 C.没关系

3.2.1 下⾯程序的编译运⾏结果是什么()

A. 编译报错 B. 运⾏报错 C. 正常运⾏

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;
};

这里只需要牢记我们上面的基本原则基本不会出错。

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

4.1 4个常⻅默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这⼏个成员函数是如何⽣成的呢?

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

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

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

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

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

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

  7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。

构造,拷贝构造,复制构造,析构
class Person
{
public:
	Person(const char* name = "peter")
		: _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;
	}
protected:
	string _name; // 姓名
};


class student:public Person
{
public:
	student(const char*name,int num):
	Person(name),_num(num){}//构造函数一般情况是不需要写的,这里的person我们可以将其理解为内置类型
	//什么情况需要我们写呢?例如我们前面类里学的需要额外申请空间的时候需要写,写的时候必须传name调用父类的构造


	// 严格说Student拷贝构造默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	student(const student& stu):
		Person(stu),_num(stu._num)
	{

	}//这里就用到上面的切片的概念


	// 严格说Student赋值重载默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	student operator=(const student& stu)
	{
		if (this != &stu)
		{
			Person::operator=(stu);

			_num = stu._num;
		}
	}


	~student()
	{
		//这里会自动调用父类的析构函数,这是为了不破坏先析构子类再父类的顺序
		//如果交由使用者的可能会出现先析构父类再析构子类的情况。
	}

protected:
	int _num;//学号
};

int main()
{
	return 0;
}

在这里插入图片描述
在这里插入图片描述

这里需要注意的是第七点,析构在反汇编里会被转换为destructor,与基类构成隐藏。

好了继承第一节内容就简单的完结<^^>了。