C++继承

目录

 

什么是继承

继承方式

基类和派生类的赋值转换

 继承中的作用域

派生类的默认成员函数

继承与友元

继承与静态成员

菱形继承及菱形虚拟继承

继承和组合


什么是继承

继承是代码复用的手段之一,使用继承可以定义相似的类型,并对其相对关系建模。例如有这样的场景:在一个宠物管理系统中,有各种各样的小动物如:小狗、小猫、宠物羊,宠物猪等,它们都有姓名、品种、身高、体重的属性,吃饭、喝水、运动的行为!

 如果不使用继承,在代码的实现上就会造成大量的重复:

class cat
{
public:

	void eat()
	{
		cout << "吃饭" << endl;
	}
	void drink()
	{
		cout << "喝水" << endl;
	}
	void move()
	{
		cout << "移动" << endl;
	}
private:
	string _name = "小花";
	string _variety = "金渐层";
	double _height = 50.0;
	double _weight = 30.0;
};

class dog
{
public:

	void eat()
	{
		cout << "吃饭" << endl;
	}
	void drink()
	{
		cout << "喝水" << endl;
	}
	void move()
	{
		cout << "移动" << endl;
	}
private:
	string _name = "大黄";
	string _variety = "土狗";
	double _height = 50.0;
	double _weight = 30.0;
};
class sheep
{
public:
	void eat()
	{
		cout << "吃饭" << endl;
	}
	void drink()
	{
		cout << "喝水" << endl;
	}
	void move()
	{
		cout << "移动" << endl;
	}
private:
	string _name = "肖恩";
	string _variety = "绵羊";
	double _height = 50.0;
	double _weight = 30.0;
};
class pig
{
public:
	void eat()
	{
		cout << "吃饭" << endl;
	}
	void drink()
	{
		cout << "喝水" << endl;
	}
	void move()
	{
		cout << "移动" << endl;
	}
private:
	string _name = "佩奇";
	string _variety = "小香猪";
	double _height = 50.0;
	double _weight = 30.0;
};

根据上述代码大量重复的情况使用继承解决,将宠物相同的特性构建一个基类(父类)pet:

class pet
{
public:
	void eat()
	{
		cout << "吃饭" << endl;
	}
	void drink()
	{
		cout << "喝水" << endl;
	}
	void move()
	{
		cout << "移动" << endl;
	}
private:
	string _name = "name";
	string _variety = "pet";
	double _height = 50.0;
	double _weight = 30.0;
};
所有动物继承pet,这样就避免了代码的重复。 继承基类(父类)的类称为派生类(子类)!继承后父类的pet的成员(成员函数+成员变量)都会变成子类的一部分,不同的继承方式成员访问方式也不一样,后面会继续讨论,这里熟悉继承的语法。

class cat : public pet
{

};
class dog : public pet
{

};
class sheep : public pet
{

};
class pig : public pet
{

};
派生类在保持原有基类特性的基础上可以进行扩展增加功能。例如:不同动物间有不同的特性,小猫会捕捉老鼠,小狗的嗅觉很厉害,绵羊头上有犄角.......
class cat : public pet
{
public:
	void cat_fun1() {};
	void cat_fun2() {};
	//...
private:
	string _cat_trait1 = "111";
	int _cat_trait2 = 222;
	//...
};

创建一个cat对象c1通过监视窗口观察发现,小猫具有基类pet的属性和本身独有的属性。

 cat c1;

小总结:

1.继承机制是面向对象程序设计使代码可以复用的最重要的手段。
2.继承是类设计层次的复用。
3.在层次关系根部的类叫做基类(父类),继承基类的类叫做派生类(子类)。
4.派生类允许在保持原有类特性的基础上进行扩展,增加功能。

继承方式

//基类
class person{};
//公有继承
class student1 : public person {};
//保护继承
class student2 : protected person {};
//私有继承
class teacher : private person {};

基类中包含不同访问限定符修饰的类成员,基类三种不同的访问限定符和派生类三种继承关系有下表成员访问关系的变化:

●看到上表这么多内容你的头肯定大了,但在实际运用中一般情况下都是public继承,因为其余两种继承方式继承下来的成员只能在派生类的类中访问。

●观察上表也能发现,基类的private成员在派生类中都是不可见的,与继承方式无关。需要注意的是:基类的private成员还是继承到了派生类中,但是在类内和类外都不能去访问它。

基类protected成员不能在类外直接访问,但是可以在派生类中访问。这里也体现了protected限定符存在的价值。

●当我们省略掉继承方式的时候,class定义的类默认继承方式是private,struct定义的类默认继承方式是public。

class person{};
//继承方式不写 默认private继承
class student1 : person{};
//继承方式不写 默认public继承
struct student2 : person {};

基类和派生类的赋值转换

派生类对象可以赋值给基类的对象、基类的指针、基类的引用,这种赋值也被叫做切割(切片)。在赋值的过程中是将派生类中基类部分切割赋值给基类对象。

class Person
{
private:
	string _name; // 姓名
	string _sex; //性别
    int _age; // 年龄
};
class Student : public Person
{
public:
	int _id; // 学号
};
int main()
{
	Student s1;
	// 1.派生类对象可以赋值给基类对象/指针/引用
	Person p = s1;
	Person* pp = &s1;
	Person& rp = s1;

	return 0;
}

注意:赋值方向是单向的,基类对象不能赋值给派生类对象!

	//父类对象不能赋值给子类
    Person p2;
	Student	 s2 = p2;

 继承中的作用域

基类和派生类都有其独立的作用域。我们知道局部变量和全局变量,当变量名相同时,首先在局部去找,其次在查找全局。当派生类和基类的成员名相同时派生类成员会屏蔽掉基类对同名成员的访问,这种情况叫做隐藏。

class Person
{
protected:
	string _name = "张喜阳"; // 姓名
	int _num = 155;//身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "身份证号:" << _num << endl;
		cout << "学号:" << _num << endl;
	}
protected:
	int _num = 2020; // 学号
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
}

上述代码中的身份证号_num和学号_num构成了隐藏。看下运行结果:

 如果在派生类中想要访问基类同名成员,可以采用基类::基类成员显示访问。

cout << "身份证号:" << Person::_num << endl;

当基类成员函数名和派生类函数名相同的时候,不关心参数是否相同,只要函数名相同就构成隐藏。不要理解成函数重载,构成函数重载的其中一个条件就是在同一作用域中。

class Person
{
public:
	void Print()
	{
		cout << "基类成员函数" << endl;
	}
};
class Student : public Person
{
public:
	void Print(int i = 10)
	{
		cout << "派生类成员函数" << endl;
	}
protected:
	int _num = 2020; // 学号
};
int main()
{
	Student s1;
	s1.Print();
	s1.Person::Print();
	
	return 0;
}

派生类的默认成员函数

回顾时间:复习一下六个我们不写编译器会自动生成的默认成员函数。

类和对象(中)_Bug程序员小张的博客-CSDN博客类和对象默认成员函数,构造函数,析构函数,拷贝构造,赋值运算符重载,运算符重载,日期类的实现,const成员。取地址及const取地址操作符重载 。https://blog.csdn.net/weixin_59351791/article/details/128449190

派生类的构造函数会调用基类的默认构造完成继承部分的初始化工作,如果基类没有提供默认构造,这时需要手动在初始化列表显示调用。派生类对象初始化先调用基类构造在调用派生类构造

 基类有默认构造函数:

class preson
{
public:
	preson (const char* name = "张三", int id = 111)
		:_name(name)
		,_id(id)
	{
		cout << "preson::构造函数调用" << endl;
	}
protected:
	string _name;
	int _id;
};
class student : public preson
{
public:
	student(int num)
		:_num(num)
	{
		cout << "student::构造函数调用" << endl;
	}
private:
	int _num;
};

void Test1()
{
  student s1(222);
}

 基类没有默认的构造函数,需要在派生类初始化列表显示调用。

//基类没有默认构造,需要在派生类的初始化列表部分显示调用
class preson
{
public:
	preson(const char* name, int id)
		:_name(name)
		, _id(id)
	{
		cout << "preson::构造函数调用" << endl;
	}
protected:
	string _name;
	int _id;
};
class student : public preson
{
public:
	student(const char* name, int id,int num)
		:preson(name,id)
		,_num(num)
	{
		cout << "student::构造函数调用" << endl;
	}
private:
	int _num;
};

void Test1()
{
	student s1("王五",520, 1314);
}

 ●派生类的拷贝构造会调用基类的拷贝构造完成基类部分的拷贝,在对派生类新增成员进行拷贝。

//基类没有默认构造,需要在派生类的初始化列表部分显示调用
class preson
{
public:
	//基类构造
	preson(const char* name, int id)
		:_name(name)
		, _id(id)
	{
		cout << " preson::构造函数调用" << endl;
	}
	//基类拷贝构造
	//s2(s1)
	preson(const preson& p)
		:_name(p._name)
		,_id(p._id)
	{
		cout << " preson::拷贝构造调用" << endl;
	}
protected:
	string _name;
	int _id;
};
class student : public preson
{
public:
	//派生类构造
	student(const char* name, int id,int num)
		:preson(name,id)
		,_num(num)
	{
		cout << "student::构造函数调用" << endl;
	}
	//派生类拷贝构造
	student(const student& s)
		:preson(s)
		,_num(s._num)
	{
		cout << "student::拷贝构造调用" << endl;
	}
private:
	int _num;
};

void Test1()
{
	student s1("王五",520, 1314);

	student s2(s1);
}

 ●派生类的赋值重载需要调用基类的赋值重载完成赋值。

//基类没有默认构造,需要在派生类的初始化列表部分显示调用
class preson
{
public:
	//基类构造
	preson(const char* name, int id)
		:_name(name)
		, _id(id)
	{
		cout << " preson::构造函数调用" << endl;
	}
	//基类拷贝构造
	//s2(s1)
	preson(const preson& p)
		:_name(p._name)
		,_id(p._id)
	{
		cout << " preson::拷贝构造调用" << endl;
	}
	//基类的赋值重载
	//s1 = s2
	//s1 = s1
	preson& operator=(const preson& p)
	{
		cout << " preson::赋值重载调用" << endl;
		if (this != &p)
		{
			_name = p._name;
			_id = p._id;
		}
		return *this;
	}
protected:
	string _name;
	int _id;
};
class student : public preson
{
public:
	//派生类构造
	student(const char* name, int id,int num)
		:preson(name,id)
		,_num(num)
	{
		cout << "student::构造函数调用" << endl;
	}
	//派生类拷贝构造
	student(const student& s)
		:preson(s)
		,_num(s._num)
	{
		cout << "student::拷贝构造调用" << endl;
	}
	//派生类的赋值重载
	student& operator=(const student& s)
	{
		cout << "student::赋值重载调用" << endl;
		if (this != &s)
		{
			//调用基类的赋值重载
			preson::operator=(s);
			_num = s._num;
		}
		return *this;
	}
private:
	int _num;
};

void Test1()
{
	student s1("王五",520, 1314);
	student s2(s1);

	student s3("李四",198,199);
	s2 = s3;
}

  ●派生类析构先调用派生类析构在调用基类的析构,清理顺序也就是先清理派生类成员,在清理基类成员。

//基类没有默认构造,需要在派生类的初始化列表部分显示调用
class preson
{
public:
	//基类构造
	preson(const char* name, int id)
		:_name(name)
		, _id(id)
	{
		cout << " preson::构造函数调用" << endl;
	}
	//基类拷贝构造
	//s2(s1)
	preson(const preson& p)
		:_name(p._name)
		,_id(p._id)
	{
		cout << " preson::拷贝构造调用" << endl;
	}
	//基类的赋值重载
	//s1 = s2
	//s1 = s1
	preson& operator=(const preson& p)
	{
		cout << " preson::赋值重载调用" << endl;
		if (this != &p)
		{
			_name = p._name;
			_id = p._id;
		}
		return *this;
	}
	~preson()
	{
		cout << " person::调用析构函数" << endl;
	}
protected:
	string _name;
	int _id;
};
class student : public preson
{
public:
	//派生类构造
	student(const char* name, int id,int num)
		:preson(name,id)
		,_num(num)
	{
		cout << "student::构造函数调用" << endl;
	}
	//派生类拷贝构造
	student(const student& s)
		:preson(s)
		,_num(s._num)
	{
		cout << "student::拷贝构造调用" << endl;
	}
	//派生类的赋值重载
	student& operator=(const student& s)
	{
		cout << "student::赋值重载调用" << endl;
		if (this != &s)
		{
			//调用基类的赋值重载
			preson::operator=(s);
			_num = s._num;
		}
		return *this;
	}
	//派生类析构
	~student()
	{
		cout << "student::调用析构函数" << endl;
	}
private:
	int _num;
};

void Test1()
{
	student s1("王五",520, 1314);
}

●关于析构函数有两点需要注意:1.派生类析构先调用,基类析构后调用。在派生类的析构函数中不用显示调用基类析构函数派生类析构结束后会自动调用基类析构。2.派生类析构和基类析构构成隐藏关系,它们的函数名看起来不相同,其实所有析构函数的函数名都被处理成了相同的。

继承与友元

这个部分很容易理解,友元关系是不能继承的,基类的友元函数不能去访问子类的私有和保护成员!假设我和老王是拜把子兄弟,他家的钱我随便花,因为我们私交甚好。但是我不能去老王的儿子(子类)家去找东西。

class A
{
	friend void Fun();
private:
	int _num = 0;
};
class B : public A
{
private:
	int _id = 24;
};

void Fun()
{
	A aa;
	cout << aa._num << endl;
}
int main()
{
	Fun();
	return 0;
}

 子类不是Fun()函数的友元!

	B bb;
	cout << bb._id << endl;

继承与静态成员

基类中的静态成员,整个继承体系只有一个。

class Person
{
public:
	//没创建一个对象都会调用默认构造
	Person() { ++_count; }
protected:
	string _name; 
public:
	static int _count; // 统计学生个数
};
int Person::_count = 0;


class Student1 : public Person
{
protected:
	int _stuNum; // 学号
};
class Student2 : public Person
{
protected:
	int _stuNum; // 学号
};


void TestPerson()
{
	Student1 s1;
	Student1 s2;
	Student1 s3;
	cout << "一班人数 :" << Person::_count << endl;
	
	Student2::_count = 10;
	cout << "二班人数 :" << Person::_count << endl;

}

菱形继承及菱形虚拟继承

上述继承的关系模型都是单继承,相当于“代代单传”,一个派生类只能有一个基类。

 如上图,除了单继承外还有多继承继承关系,多继承是指一个派生类同时继承了两个或者两个以上的基类:

class A
{
protected:
	int _a = 1;
};

class B
{
protected:
	int _b = 2;
};
class C : public A, public B
{
private:
	int _c = 3;
};

int main()
{
	C c;
	return 0;
}

 派生类对象c同时继承了基类A的成员变量和基类B的成员变量。这样的继承方式看起来非常的巧妙,但是同时带来了比较头痛的问题:菱形继承!

class A
{
public:
	int _a = 1;
};

class B : public A
{
protected:
	int _b = 2;
};
class C : public A
{
protected:
	int _c = 3;
};

class D : public B, public C
{
private:
	int _d = 4;
};
int main()
{
	D dd;
	dd._a = 100;
	return 0;
}

 上述代码的访问存在_a二义性的问题:

解决方法1:访问前明确作用域

int main()
{
	D dd;
	dd.B::_a = 100;
	dd.C::_a = 200;
	return 0;
}

解决方法2:B和C继承A时采取虚拟继承的方式,注意:虚拟继承是在菱形的“腰部”加上virtual关键字。

class A
{
public:
	int _a = 1;
};

class B : virtual public A
{
protected:
	int _b = 2;
};
class C : virtual public A
{
protected:
	int _c = 3;
};

class D : public B, public C
{
protected:
	int _d = 4;
};
int main()
{
	D dd;
	dd.B::_a = 100;
	dd.C::_a = 200;
	dd._a = 300;
	return 0;
}

调试窗口为了我们便于观察,优化成了上图的效果。实际上虚拟继承后只有一个_a,接下来通过内存窗口来观察对象成员的模型。

int main()
{
	D dd;
	dd.B::_a = 1;
	dd.C::_a = 11;
	dd._b = 2;
	dd._c = 3;
	dd._d = 4;
	return 0;
}

菱形继承:

 菱形虚拟继承:

int main()
{
	D dd;
	dd.B::_a = 1;
	dd.C::_a = 11;
	dd._b = 2;
	dd._c = 3;
	dd._d = 4;

	dd._a = 6;
	return 0;
}

 通过观察发现,确实只有一份_a,但是在B和C中多了两份地址,它们保存的是B和C找到_a的偏移量。在打开两个内存窗口访问多出的两份地址:

综上所述:多出来的两份地址存储的是A和B找到_a的偏移量。 

继承和组合

除了继承之外,对象组合也是代码复用的一种选择,而且组合类之间没有很强的依赖关系,代码耦合度低。组合一般适合has - a的场景,比如头上有双眼睛、一个嘴巴、一个鼻子.........

class A
{
private:
	int _a1;
	int _a2;
};

class B
{
private:
	A _a;
	int _b;
};

满足is - a的场景比较适合用继承,比如学生是人、狗是动物........,除此之外多态的实现也必须要用到继承。

总结:继承在一定程度上破坏了基类的封装,基类的内部细节对派生类类可见。基类的改变对派生类有很大的影响,派生类和基类间的依赖关系很强,代码的耦合度较高。而组合类之间没有很强的依赖关系,对象的内部细节是不可见的,耦合度低。综上所述,当类之间的关系用继承和组合都能实现的时候,优先选择组合更好一些。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值