首页 > 编程语言 >【C++】继承

【C++】继承

时间:2024-10-08 20:18:01浏览次数:10  
标签:继承 C++ class Person Student 基类 public

C++的继承

  • 1.继承的概念及定义
    • 1.1继承的概念
    • 1.2 继承定义
      • 1.2.1定义格式
  • 2.基类和派生类对象赋值转换
  • 3.继承中的作用域
  • 4.派生类的默认成员函数
  • 5.继承与友元
  • 6.继承与静态成员
  • 7.复杂的菱形继承及菱形虚拟继承
    • 7.1 单继承、多继承与菱形继承
    • 7.2 虚继承
    • 7.3 虚继承的原理

1.继承的概念及定义

1.1继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

class Person
	{
	public:
		void Print()
		{
			cout << "name:" << _name << endl;
			cout << "age:" << _age << endl;
		}
	protected:
		string _name = "peter"; // 姓名
		int _age = 18;  // 年龄
	};
	// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
	Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
		以看到变量的复用。调用Print可以看到成员函数的复用。
		class Student : public Person
	{
	protected:
		int _stuid; // 学号
	};

	class Teacher : public Person
	{
	protected:
		int _jobid; // 工号
	};
	int main()
	{
		Student s;
		Teacher t;
		s.Print();
		t.Print();
		return 0;
	}

1.2 继承定义

1.2.1定义格式

在这里插入图片描述

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
//以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
	int _stuid; // 学号
};
class Teacher : public Person
{
protected:
	int _jobid; // 工号
};
int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	
	return 0;
}

继承关系和访问限定符:
在这里插入图片描述
总结:
1.基类的私有成员在派生类不可见。父类的私有成员子类不能直接使用,但是可以间接使用:get/set方法、调用父类中的公有/保护方法。
2.子类继承父类后,访问方式:在访限定符和继承方式中,取权限小的。例如,我的访问权限是公有,但是你私有继承,取权限小的就是私有。
3.保护和私有的区别:保护也可以在子类中使用,其他地方不可以。私有是子类和其他地方都不能使用。例如:下面代码我的父类成员name就是一个保护,但是子类继承后可以通过函数访问,注意:类内通过它自己的实例对象访问。
4.常用的访问限定符:成员公有保护,继承一般都是公有继承。
5.struct 也可以继承,且继承方式可以不写,struct默认继承方式是公有,class默认继承方式是私有。但是一定要写清楚!

2.基类和派生类对象赋值转换

在之前我们讲过,当不同类型的两个变量之间进行赋值时,会发生隐式类型转换。所谓隐式类型转换,是指不需要用户干预,编译器默认进行的类型转换行为。

看到以下代码:

int main()
{
	//内置类型
	double d = 1.1;
	int a = d;//中间会产生临时变量
	int& i = d;//编译不通过,中间会产生临时变量,临时变量具有常性
	//自定义类型
	string str = "xxxx";//隐式类型转换
	string& pstr = "xxxx";//编译不通过,中间会产生临时变量,临时变量具有常性
}

运行代码会发现编译通不过,是因为double类型转换为int类型,中间产生临时变量。也就是说当不同类型的两个变量之间进行赋值时,中间会产生临时变量。

再看到下面的代码:

class Person
{
public:
	void Print()
	{}
private:
	int _age; // 年龄
};
 
class Student : public Person
{
protected:
	int _stunum; // 学号
};

int main()
{
	Student s;
	Person p = s;
	Person& rp = s;
	return 0;
}

运行之后编译通过,因为基类Person继承以public继承方式继承给子类Student,父类和子类是is-a的关系。在main函数中,创建一个子类对象s,把s赋值给一个父类对象p。这里的public继承,派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用,我们把他叫做父子类赋值兼容规则,也叫切割或者切片。

在这里插入图片描述

注意:
1.基类对象不能赋值给派生类对象。
2.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};
void Test()
{
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;
	//2.基类对象不能赋值给派生类对象
	sobj = pobj;//error
	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj;
	Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;

	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
	ps2->_No = 10;//error
}

3.继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
	string _name = "小李子"; // 姓名
	int _num = 111; // 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout<<" 姓名:"<<_name<< endl;
		cout<<" 身份证号:"<<Person::_num<< endl;
		cout<<" 学号:"<<_num<<endl;
	}
protected:
	int _num = 999; // 学号
};
void Test()
{
	Student s1;
	s1.Print();
};

// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" <<i<<endl;
	}
};
void Test()
{
	B b;
	b.fun(10);
};

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

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
    的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加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)
	{
		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;
	}
	//由于多态的原因,析构函数统一被处理为destructor
	//父子类析构函数构成隐藏
	//为什么最好不在子类中调用父类的析构函数?
	//为了保证析构安全,编译器处理为先子后父
	//父类析构函数不需要显示调用,子类析构函数结束时会自动调用父类析构,保证先子后父!
	~Student()
	{
		Person::~Person();
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};
void Test()
{
	Student s1("zhangsan", 18);
	Student s2(s1);
	Student s3("lisi", 17);
	s1 = s3;
}

5.继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
	//"Student::_stuNum”: 无法访问 protected 成员(在“Student”类中声明)
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

6.继承与静态成员

基类定义了static静态成员,静态成员在所有类对象中共享一份,所以整个继承体系中只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

class Person
{
public :
	Person () {++ _count ;}
protected :
	string _name ; // 姓名
public :
	static int _count; // 统计人的个数。
};
int Person :: _count = 0;

class Student : public Person
{
protected :
	int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
	string _seminarCourse ; // 研究科目
};
void TestPerson()
{
	Student s1 ;
	Student s2 ;
	Student s3 ;
	Graduate s4 ;
	cout <<" 人数 :"<< Person ::_count << endl;
	Student ::_count = 0;
	cout <<" 人数 :"<< Person ::_count << endl;
}

7.复杂的菱形继承及菱形虚拟继承

7.1 单继承、多继承与菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
在这里插入图片描述
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
在这里插入图片描述

那么为什么存在多继承?
因为有些事物单继承是无法解决的,比如说西红柿即使蔬菜又是水果,所以多继承的存在比较合理。

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性问题,在Assistant的对象中Person对象成员会包含两份。

class Person
{
public :
	string _name ; // 姓名
};
class Student : public Person
{
protected :
	int _num ; //学号
};
class Teacher : public Person
{
protected :
	int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
	string _majorCourse ; // 主修课程
};
void Test ()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a ;
	a._name = "张三";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "小张";
	a.Teacher::_name = "老张";
}

在这里插入图片描述

7.2 虚继承

虚拟继承(添加virtual关键字)可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

7.3 虚继承的原理

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。

class A
{
public:
	int _a;
};
class B : public A
//class B : virtual public A
{
public:
	int _b;
};
class C : public A
//class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	return 0;
}

在这里插入图片描述
将代码再次转换为虚继承,查看虚继承的内存成员对象模型,这里我们分析出,D对象中将A放到了所有对象组成的最下面,这个A同时属于B和C,那么B和C如何找到公共的A呢?这里是通过B和C两个指针,指向的一张表。这两个指针叫做虚基表指针,两张表叫做虚基表。虚基表中存放的是偏移量。如图中黄色方框部分,B通过偏移14(十六进制,十进制表示为20)个字节找到A,C通过偏移0c(十六进制,十进制为12)个字节找到A。

标签:继承,C++,class,Person,Student,基类,public
From: https://blog.csdn.net/Lehjy/article/details/142595390

相关文章

  • 【C++】多态
    文章目录1.多态的概念1.1概念2.多态的定义及实现2.1多态构成的条件2.2虚函数2.3虚函数的重写2.4C++11override和final2.5重载、覆盖(重写)、隐藏(重定义)的对比3.抽象类3.1概念3.2接口继承和实现继承4.多态的原理4.1虚函数表与多态原理4.2动态绑定与静......
  • C++ day03(作用域限定符、this、static、const)
    目录【1】作用域限定符::1》名字空间2》类内声明,类外定义 【2】this关键字1》概念2》调用成员3》区分重名的成员变量与局部变量4》链式调用 【3】static关键字1》静态局部变量2》静态成员变量3》静态成员函数【4】const关键字1》修饰成员函数2》修饰对象 ......
  • 如何在VSCode上运行C/C++代码
    诸神缄默不语-个人CSDN博文目录我是Win10,其他系统仅供参考。文章目录1.下载所需插件2.安装编译器3.不借助编辑器的cpp代码执行3.建立VSCodecpp项目3.1c_cpp_properties.json3.2settings.json3.3tasks.json4.运行C++代码参考资料1.下载所需插件2.安装......
  • C++刷题:RGB色值转Integer
    问题描述:实现一个函数,输入为长度为三的rgb字符串,返回为十六进制HEX格式字符串。输入格式:字符串输出格式:数字输入样例:"rgb(192,192,192)"输出样例:12632256问题分析:    首先要进行字符串的处理。输入"rgb(192,192,192)",想办法将三个192提取出来,再将192192......
  • C++刷题:加一操作
    问题描述小W拥有一项魔法,可以对任意数字字符串进行加一的操作,比如当他拿到“798”这样的数字字符串,每一次操作,他会将其中每一个字符进行加一,比如经过一次操作后得到了“8109”。他想知道操作`k`次后,这个数字将会变成多少,由于答案可能很大,最终结果需要对1000000007取......
  • 2024年华为OD笔试机试E卷- 补种未成活胡杨 (java/c++/python)
    华为OD机试E卷2024真题目录(java&c++&python)本人习惯先看输入输出描述,可以明确知道哪些数据已知,需要去得到什么结果,再代入更有目的性地阅读题干内容,快速理解,所以把输入输出描述放在前面,你可以试下这样阅读对你是否有帮助。输入描述N总种植数量1≤N≤100000M......
  • 2024年华为OD笔试机试E卷- 关联子串 (java/c++/python)
    华为OD机试E卷2024真题目录(java&c++&python)本人习惯先看输入输出描述,可以明确知道哪些数据已知,需要去得到什么结果,再代入更有目的性地阅读题干内容,快速理解,所以把输入输出描述放在前面,你可以试下这样阅读对你是否有帮助。输入描述输入两个字符串,分别为题目中描述的......
  • C++——有一个Date类,私有成员:月、日、年,公有成员:函数,其作用输出月/日/年,一个构造函数
    没注释的源代码#include<iostream>usingnamespacestd;classDate{private:  intmonth;  intday;  intyear;public:  voiddisplay()  {    cout<<month<<"/"<<day<<"/"<<year<<endl;......
  • C++——有Date类,私有成员:月日年,公有成员:函数,其作用输出月日年。一个构造函数有三个参
    没注释的源代码#include<iostream>usingnamespacestd;classDate{private:  intmonth;  intday;  intyear;public:  voiddisplay()  {    cout<<month<<"/"<<day<<"/"<<year<<endl;......
  • C++——将一个数组中的数循环左移两位,例如:数组中原来的数为:1 2 3 4 5,移动后变成 3 4 5
    没注释的源代码#include<iostream>usingnamespacestd;intmain(){   inta,b[100];   cout<<"请输入数组个数:";   cin>>a;   cout<<"请输入"<<a<<"个数组:";   for(inti=0;i<a;i++)   {       cin&......