C++面向对象三大特性 —— 继承

本文详细阐述了面向对象编程中的继承机制,包括概念、定义、继承关系、访问限定符,以及派生类的构造函数、析构函数、赋值操作等。重点讨论了多继承、菱形继承的问题和解决方案,以及继承与组合的区别。文章指出C++中继承的利弊及其对类设计的影响。

目录

一,什么是继承?

1.1概念

1.2定义

1.3继承关系和访问限定符

二,基类和派生类对象赋值转换

三,作用域

四,继承中派生类的默认成员函数

4.1构造函数

4.2拷贝构造函数

4.3赋值运算符重载

4.4析构函数

五,继承与友元

六,继承与静态成员

七,菱形继承与虚拟菱形继承

7.1三种继承方式

7.1.1单继承

7.1.2多继承

7.1.3菱形继承

7.2菱形继承存在的问题

7.2.1数据冗余问题

7.2.2二义性问题

7.3虚拟继承

7.3.1解决二义性问题

7.3.2解决数据冗余问题

八,继承与组合

九,总结


一,什么是继承?

1.1概念

继承(inheritance)是面向对象程序设计的重要手段,它可以让程序员复用曾经写过的代码,在原有类保持其自身特性的基础上进行扩展,增加新的功能。

继承呈现了面向对象程序设计的层次结构,为代码实现由简单到复杂提供了认知过程和途径。与曾经的函数复用不同,继承是层次设计的复用。

1.2定义

在一个类名后面加上“ : ”,后面再加上继承关系和另一个类名,组成继承

class A
{

protected:
	int _numberA
};

//此处构成继承
class B: public A //这里的B类称为派生类或子类,public称为继承关系,A称为基类
{
protected:
	int _numberB
};



下面是继承的一个应用场景

#include<iostream>
using namespace std;

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "张三";
	int _age = 18;
};

class Student : public Person // 此处完成继承
{
protected:
	int _stuid;//学号
};

class Teacher : public Person // 此处完成继承
{
protected:
	int _jobid;//工号
};

int main()
{
	Student S;
	Teacher T;
	S.Print();
	T.Print();//这里Student和Teacher虽然没有定义Print函数,但是由于继承可以使用Person里的Print
	return 0;
}

1.3继承关系和访问限定符

继承方式有三种,每种继承方式的访问限定符也是三种,具体关系如下

关于这张表,有一下几点需要注意:

1,基类privated成员在派生类中不可见,是指基类的privated成员虽然被继承到了派生类中,但对于派生类来说该成员处于“隐身”状态,从语法上限制了派生类对象不管是在类外面还是类里面店铺无法访问该成员。

2,使用关键字class时默认继承方式是“privated”,使用struct时默认“public”。

3(重要),在实际运用中,由于采用protected/private继承方式继承下来的成员多多少少会受到一些来自语法的限制导致派生类无法正常访问,所以一般采用上面图片中红色框框内的继承关系

二,基类和派生类对象赋值转换

派生类对象可以赋值给基类的对象/指针/引用。这里有个形象的说法叫做切片/切割,就是把派生类中基类的那部分切出来赋值过去。基类对象不能赋值给派生类对象。如下图

class Person
{
protected:
	string _name;//姓名
	string _sex;//性别
	int _age;//年龄
};

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

int main()
{
	Student s;
	//子类对象可以赋值给父类对象/指针/引用
	Person p = s;
	Person* pp = &s;
	Person& rp = s;

	//基类的指针经过强转后可以赋值给派生类的指针
	Student* ps = (Student*)pp;
	ps->_stuid = 10;//通过强转后的指针访问派生类成员
	return 0;
}

三,作用域

1,在继承体系中,基类派生类都有各自独立的作用域

2,当子类和父类有同名成员时,子类成员会屏蔽父类同名成员,这种情况叫做“隐藏”或“重定义”。

3,如果是成员函数隐藏,只要函数名相同就隐藏,不论是否重载。

class Person
{
protected:
	string _name = "张三";//成员与子类同名
};
class Student : public Person
{
public:
	void Print()
	{
		cout << "子类姓名:" << _name << endl;
		cout << "父类姓名:" << Person::_name << endl;//当同名时,父类需要加上作用域限定符::来访问父类成员
	}
protected:
	string _name = "李四";//成员与父类同名
};

int main()
{
	Student S;
	S.Print();
	return 0;
}

由于当定义相同成员名时非常容易混淆,所以应该注意在之后的继承体系中,应尽量避免命名同名成员或成员函数!

四,继承中派生类的默认成员函数

4.1构造函数

派生类自己的成员,调用自己的构造函数,基类的成员调用基类的构造函数

class Person
{
public:
	Person(const char* name = "peter")//构造函数
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name;
};
 
class Student : public Person
{
public:
	Student(const char* name, int num)//构造函数
		:Person(name)
		, _num(num)
	{}
protected:
	int _num;
};

int main()
{
	Student s("张三",18);
	return 0;
}

4.2拷贝构造函数

和构造函数一样,各自调用各自的拷贝构造函数

class Person
{
public:
	Person(const char* name = "peter")//构造函数
		:_name(name)
	{}
	Person(const Person& p)//拷贝构造
		:_name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
protected:
	string _name ;
};
 
class Student : public Person
{
public:
	Student(const char* name, int num)//拷贝函数
		:Person(name)
		, _num(num)
	{}

	Student(const Student& s)//拷贝构造
		:Person(s)
		, _num(s._num)
	{}
protected:
	int _num ;
};

int main()
{
	Student s1("张三",18);
	Student s2(s1);
	return 0;
}

4.3赋值运算符重载

和前两个一样

class Person
{
public:
	Person(const char* name = "peter")//构造函数
		:_name(name)
	{}
	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;
	}
protected:
	string _name ;
};
 
class Student : public Person
{
public:
	Student(const char* name, int num)//拷贝函数
		:Person(name)
		, _num(num)
	{}

	Student(const Student& s)//拷贝构造
		:Person(s)
		, _num(s._num)
	{}
	Student& operator=(const Student& s)//运算符重载
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num ;
};

int main()
{
	Student s1("张三",18);
	Student s2 = s1;
	return 0;
}

4.4析构函数

继承体系中,析构函数较为特殊,子类中不需要显示调用父类的析构函数,因为子类自身调用析构函数时会自动调用父类析构函数,这样才能保证先析构子类再析构父类。并且子类析构函数跟父类析构函数构成隐藏关系,由于多态的需要,析构函数名字会被统一处理为destructor()

class Person
{
public:
	Person(const char* name = "peter")//构造函数
		:_name(name)
	{}
	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)
	{}

	Student(const Student& s)//拷贝构造
		:Person(s)
		, _num(s._num)
	{}
	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("张三",18);
	Student s2 = s1;
	return 0;
}

完整代码如上述代码

五,继承与友元

友元关系无法继承,父类友元函数不能访问子类私有成员

class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _stuid; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuid << endl;//报错,显示无法访问
}
void main()
{
	Person p;
	Student s;
	Display(p, s);
}

六,继承与静态成员

如果基类定义了一个static成员,那么在整个继承体系里只会储存在一个这样的成员

七,菱形继承与虚拟菱形继承

7.1三种继承方式

7.1.1单继承

一个子类只有一个直接父类的继承关系叫做单继承

7.1.2多继承

一个子类有两个或以上父类的继承关系叫做多继承

7.1.3菱形继承

菱形继承是多继承的一种特殊情况

7.2菱形继承存在的问题

7.2.1数据冗余问题

如下图,可以发现在Assistant中Person的数据有两份,存在数据冗余的问题

再看如下代码

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};

class C : public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

运行结果如下,可以发现,我通过d.B和d.C同时改变a的值,但最后却又给我创建了一个a出来,造成数据冗余

7.2.2二义性问题

如下代码

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

结果程序报错,显示:

7.3虚拟继承

7.3.1解决二义性问题

在继承关系前面加上“virtual”,即可完成虚拟继承,解决二义性问题:

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

7.3.2解决数据冗余问题

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

class C : virtual public A
{
public:
	int _c;
};

class D :public B, public C
{
public:
	int _d;
};

同样的,在继承关系前面加上“virtual”,运行后结果如下,可以看到虚拟继承过后a就只有一个了。

但是,为什么3和4的前面带了一串地址呢,这串地址是什么,有什么用呢?

可以看到,这个地址指向的区域有一个数字,该数字表示的都是该指针的地址到_a的距离,我们叫做偏移量,可以通过这个找到共同的_a

八,继承与组合

1,public继承是一种is-a的关系,比如说学生是人,花是植物

2,组合是一种has-a的关系,比如说车有轮胎,脑袋有心灵的窗户

3,也有一些类可以同时用继承和组合,比如说铁锅是铁,也可以说铁锅有铁。

但是,如果遇到了可以同时用继承和组合的类,尽量使用组合关系。因为继承一定程度上破坏了基类的封装,基类改变的话对派生类有很大影响,耦合度很高;而组合类之间没有很强的依赖关系,

耦合度低,也保证了类的封装。

九,总结

继承是面向对象三大特征之一,提高了代码的复用性,扩展了类的原有属性。但是,C++也作为早期的高级语言,也存在一定缺陷,多继承就是C++的缺陷之一,因为有了多继承,就存在菱形继承,也就有了数据冗余和二义性等问题,由此有有了虚拟继承,但随之而来的就是底层实现的更加复杂,代码量增多。

虽然有很多缺陷,但不能撼动C++的地位,我们可以抱怨,可以吐槽,但不能以偏概全,必须保持对前人的尊敬。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值