C++继承

1、什么是继承

1.1、概念引入

我们在定义两个类student和teacher时,学生和老师都有共同属性——姓名,年龄.....学生特有学号,教师特有教工号,但是如果我们在student类定义sting name,int age,string id三个成员变量;给teacher类定义也sting name,int age,string tid三个成员变量。我们可以看到,这两个类的成员变量有两个重合的——name和age。

class student
{
private:
	string _name = "zhangsan";
	int _age = 0;
	string _id = "1";
};
class teacher
{
private:
	string _name = "zhangsan";
	int _age = 0;
	string _tid = "1";
};

这只是两个类,如果后面再引入校长、班长、教导主任等等,那我们重合的地方会很多很多。C++这门语言,非常忌讳重复的代码,所以这个时候,我们引入继承这个概念。我们可以封装一个类Person,student和teacher归根结底也是一个人,我们在Person类里面把他们共有的成员放进去——name,age;然后我们在定义student和teacher类时,直接继承Person类的成员,再在student,teacher类里面,定义自己特有的成员就可以了。

1.2、继承的概念

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

1.3、继承的用法

下面的代码就是我们定义一个Person类,把重复的属性都放在Person类里面,并在student和teacher类里面定义自己特有的成员变量。

​class Person
{
protected:
	string _name = "zhangsan";
	int _age = 0;
};
class student:public Person
{
private:
	string _id = "1";
};
class teacher :public Person
{
private:
	string _tid = "1";
};

​

我们把Person类叫基类,俗称父类;把student和teacher这两个类都叫派生类,俗称子类。我们看到Person里面堆成员变量的访问限定词跟以前的不一样:protected。我们知道,如果访问限定词是public,代表在类里面外面都可以访问这个成员,如果是private代表只能在类里面对该成员访问,类外面不可以。而我们用继承地目的就是为了在派生类里面用基类的成员,所以如果我们访问限定符用private的话岂不是在派生类里面不能访问了吗,但如果用public的话,类外面也可以随意访问,如果在类外面不小心把数据给修改了呢,所以为了安全起见,这里再引入一个访问限定词:protected,它表示该成员只能在类里面和子类使用。

我们看到这里:

class student:public Person

student类和teacher类后面都有:public Person

public代表继承方式,后面跟的内容表示继承的哪个类。

所以,类的继承可以这么表示

1.4、继承关系和访问限定符

继承基类成员访问方式的变化
类成员 / 继承方式
public 继承
protected 继承
private 继承
基类的 public 成员
派生类的public 成员
派生类的 protected成员
派生类的 private成员
基类的 protected 成员
派生类的 protected成员
派生类的 protected成员
派生类的 private成员
基类的 private
在派生类中不可见
在派生类中不可见
在派生类中不可见
总结:
1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的 不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它
2. 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为 protected 可以看出保护成员限定符是因继承才出现的
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min( 成员在基类的访问限定符,继承方式 ) public > protected
> private
4. 使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public 不过
最好显示的写出继承方式
5. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承 ,也不提倡
使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。

2、基类和派生类对象复制转换

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
class Person
{
protected:
	string _name; // 姓名
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _id; // 学号
};
void Test()
{
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

	//2.基类对象不能赋值给派生类对象
	//sobj = pobj; err

}

3、继承中的作用域

1. 在继承体系中 基类 派生类 都有 独立的作用域
2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。 (在子类成员函数中,可以 使用 基类 :: 基类成员 显示访问
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
4. 注意在实际中在 继承体系里 面最好 不要定义同名的成员
class Person
{
public:
	void fun()
	{
		cout << _name << endl;
	}
   
protected:
	string _name = "zhangsan";
	int _age = 0;
};
class student:public Person
{
public:
	void fun()
	{
		cout << _id << endl;
	}
private:
	string _id = "1";
};
int main()
{
	student s;
	s.fun();//基类派生类都有fun(),子类会屏蔽掉父类的同名函数;
	s.Person::fun();//显示调用父类fun();
	return 0;
}

​
class Person
{
public:
	void fun()
	{
		cout << _name << endl;
	}
   
public://为了演示隐藏,方便在类外面调用同名成员变量,这里用public
	string _name = "zhangsan";
	int _age = 0;
	int _num = 11111;//身份证号
};
class student:public Person
{
public:
	void fun()
	{
		cout << _id << endl;
	}
	public://为了演示隐藏,方便在类外面调用同名成员变量,这里用public
	string _id = "1";
	string _num = "222222222222";//学籍号
};
void test1()
{
	student s;
	cout << s._num << endl;
	cout << s.Person::_num << endl;
}

int main()
{
	test1();
	return 0;
}

​

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

	~Student()
	{
		cout << "~Student()" << endl;//编译器为了保证先子后父,编译器会在程序结束时自动调用父类析构
		//(因为子类中有父类的成员,所以不能先析构父类)
	}
protected:
	int _num; //学号
};
void test()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
}
int main()
{
	test();
	return 0;
}

5、静态成员

基类定义了 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;
}
int main()
{
	TestPerson();
	return 0;
}

6、多继承

如果一个类,继承多个类,这种场景就叫多继承。

class Student 
{
protected:
	int _num; //学号
};
class Teacher 
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

这个助教Assistant同时继承student和teacher,即同时可以使用student和teacher两个类的成员。

但是多继承可能引发一个非常严重的后果——菱形继承

上图所表达的就是菱形继承。菱形继承会导致一个现象,B继承A,拥有A的成员,C继承A,拥有A的成员,D继承B,C拥有B,C的成员,则,D会有两份A的成员,这会导致数据冗余和二异性。

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 = "peter";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}
int main()
{
	Test();
	return 0;
}

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

class Person
{
public:
	string _name; // 姓名
};
class Student : virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void Test()
{
	Assistant a;
	a._name = "peter";
}

虽然我们用虚继承解决了菱形继承的问题,但是日常我们如果要用多继承的话,尽量避免菱形继承,菱形继承底层结构复杂,没有必要去给自己找麻烦。

最后补充一点,友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

  • 39
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值