C++ 继承:面向对象编程的核心概念(一)

发布于:2025-03-27 ⋅ 阅读:(31) ⋅ 点赞:(0)

引言

在C++中,继承是面向对象编程的核心概念之一,它允许根据现有的类定义新类。这种机制不仅简化了应用程序的创建和维护,还实现了代码重用和执行效率的提升。当你创建一个新类时,无需重写数据成员和成员函数,只需指明新类继承了现有类的成员即可。现有的类称为基类,新创建的类称为派生类。继承与函数重载、模版相比,前者是类设计层次的复用,后者是函数层次的复用。

// 继承的实现意义
class Student
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 学习
	void study()
	{
		// ...
	}
protected:
	string _name = "peter"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
	int _stuid; // 学号
};

class Teacher
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _name = "张三"; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};

int main()
{
	return 0;
}

在上面的代码中,我们看到没有继承的类Student 和 Teacher,Student 和 Teacher都有一部分相同的成员和函数(_name、_address、_tel、_age、identity),这部分函数分别实现在两个类里面是冗余的。但是这两个类又有不同的部分(学生:study、_stuid 和 老师:teaching、_title)。这就导致我们没办法彻底把两个类合并。

这个时候就可以使用继承,把公共的部分分别继承给两个类,提高程序的实现效率:

// 继承的使用
// 基类
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}

protected:
	string _name = "张三"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
};

// 派生类1
class Student : public Person
{
public:
	// 学习
	void study()
	{
		// ...
	}

protected:
	int _stuid; // 学号
};

// 派生类2
class Teacher : public Person
{
public:
	// 授课
	void teaching()
	{
		//...
	}

protected:
	string title; // 职称
};

int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

1. 继承的基本知识

1.1 继承的关键词的区别

首先,我们需要知道继承根据不同的关键词,基类成员在派生类中的访问权限不同。

在这里插入图片描述

  • 基类private成员虽然成功继承在派生类中。但是派生类无论以什么方式都是不可访问private成员的。
  • 基类protected成员可以在派生类中被访问,但是无法在类外被访问。
  • 使用class定义的类,在不显示写继承方式时,默认的继承方式是private;使用struct定义的类,默认的继承方式是public。一般都需要显示写继承方式。
  • 一般继承的方式都是public继承,不建议使用其他继承。
// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name; // 姓名
private:
	int _age; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:
	int _stunum; // 学号
};

1.2 继承类模版

template<class T>
class stack : public std::vector<T>
{
public:
	void push(const T& x)
	{
		// 基类是类模板时,需要指定⼀下类域,std::
		// 否则编译报错:error C3861: “push_back”: 找不到标识符 
		// 因为stack<int>实例化时,也实例化vector<int>了
        // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
		vector<T>::push_back(x);
		// push_back(x);
	}

	void pop()
	{
		vector<T>::pop_back();
	}

	const T& top()
	{
		return vector<T>::back();
	}

	bool empty()
	{
		return vector<T>::empty();
	}
};

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

  • public继承的派生类对象,可以定义指向基类的指针。可以理解成切片:指针指向的不是原本的基类,而是从派生类中切片出的基类部分。
  • 基类对象不能定义指向派生类的指针。
  • 基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用。但是必须是基类的指针是指向派生类对象的时候才是安全的。
class Person
{
protected :
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

class Student : public Person
{
	public :
	int _No; // 学号
};

int main()
{
	Student sobj;
	// 1.派⽣类对象可以赋值给基类的指针/引⽤
	Person* pp = &sobj;
	Person& rp = sobj;
	// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
	Person pobj = sobj;
	//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
	sobj = pobj;

	return 0;
}

3. 继承中的作用域

在继承中基类和派生类都有其独立的作用域。就算指明作用域也无法给相关成员赋值。派生类和基类中有同名成员时,派生类将屏蔽基类对派生类同名成员的访问,这种情况叫隐藏。
如果是成员函数的话,只需要同名就构成隐藏。所以在继承中注意尽量不定义同名的成员/函数。

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected :
	string _name = "⼩李⼦"; // 姓名
	int _num = 111; // ⾝份证号
};

class Student : public Person
{
public :
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		// 没有指明类域默认访问派生类中的_num
		cout << " ⾝份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}

protected:
	int _num = 999; // 学号
};

int main()
{
	// 主函数中指明作用域也无法使用
	//Person::_name;
	//Student::_num;

	Student s1;
	s1.Print();

	return 0;
}

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

4.1 默认成员函数的规则

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化。
  • 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 派生类对象初始化先调用基类构造,再调用派生类构造。
  • 派生类对象析构先调用派生类析构,再调用基类析构。
  • 基类析构函数不加virtual的情况下,编译器会对析构函数名进行特殊处理,处理成destructor(),派生类析构函数也会在运行时重命名为destructor(),所以会和基类析构函数构成隐藏关系。
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)
	{
		cout << "Student()" << endl;
	} 

	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	} 
	
	Student& operator= (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			// 构成隐藏,所以需要显⽰调⽤
			Person::operator =(s);
			_num = s._num;
		}

		return* this;
	} 
	
	~Student()
	{
		cout << "~Student()" << endl;
	}

protected:
	int _num; //学号
};

int main()
{
	// 构造函数
	// 先调用基类的构造函数,再调用派生类的
	Student s1("jack", 18);
	
	// 拷贝构造
	// 先调用基类的拷贝构造,再调用派生类的
	Student s2(s1);
	
	// 拷贝构造
	Student s3("rose", 17);
	
	// operator=
	// 先调用派生类的=,再调用基类的=
	s1 = s3;

	// 析构函数先调用派生类的,再调用基类的
	return 0;
}

4.2 自己实现成员函数

当默认成员函数不能满足我们的需求时,可以自己实现成员函数。

// 自己实现成员函数的要点:子类的成员函数需要带上父类的成员函数
// 特点:子类中继承下来的父类成员当做一个整体对象
// 构造:子类成员 内置类型(有缺省值就用,没有不确定)和自定义类型(默认构造) + 父类成员(必须调用父类默认构造) 
//
// 拷贝构造:子类成员 内置类型(值拷贝)和自定义类型(这个类型拷贝构造) + 父类成员(必须调用父类拷贝构造)
// 
// 赋值重载:类似拷贝构造
// 
// 析构:子类成员 内置类型(不处理)和自定义类型(调用他的析构) + 父类成员(调用他的析构)
// 自己实现的话,注意不需要显示调用父类析构,子类析构函数结束后,会自动调用父类析构

class Person
{
public:
	// 基类构造函数
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	// 基类const构造函数
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	// operator=重置
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	// 编译时重写为destructor()
	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	// 派生类构造函数
	Student(int num, const char* address, const char* name)
		:_num(num)
		,_address(address)
		,Person(name)
	{
		cout << "Student()" << endl;
	}

	// 派生类拷贝构造函数。初始化列表调用基类的拷贝构造函数
	Student(const Student& s)
		: Person(s)
		, _num(s._num)
		,_address(s._address)
	{
		cout << "Student(const Student& s)" << endl;
	}

	// operator=重置
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			_num = s._num;
			_address = s._address;
			// 子类赋值重载调用父类赋值重载
			Person::operator=(s);
		}

		return *this;
	}

	// 编译器在编译的时候,会将析构函数的名字重写为destructor()。
	// 这样析构函数的名字就会重复
	~Student()
	{
		// 不需要写,子类析构函数结束后,会自动调用父类析构
		//Person::~Person();
		cout << "~Student()" << endl;
		// delete[] _ptr;
	}

protected:
	int _num; //学号
	string _address;

	//int* _ptr = new int[10];
};

int main()
{
	Student s1(18, "张三", "西安");
	Student s2(s1);

	Student s3(19, "张四", "西安");
	s1 = s3;

	return 0;
}

4.3 实现一个不能被继承的基类(基本不用)

  • 方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
  • 方法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。
// C++11的⽅法。方法2
class Base final
{
public :
	void func5() { cout << "Base::func5" << endl; }

protected:
	int a = 1;

private:
	// C++98的⽅法。方法1
	/*Base()
	{}*/
}

class Derive :public Base
{
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

未完待续。
剩下的内容可以跳转到:C++ 继承:面向对象编程的核心概念(二)