[C++]继承初次理解

目录

1 继承是什么

2 继承方式

3 父子类的转换关系

4 父子类中同变量的对应关系

5 子类/派生类的默认成员

6 友元和静态成员


1 继承是什么

        咱们都知道C++作为一款面向对象编程的编程语言,而面向对象需要封装、继承、多态这三个重要的手段,封装咱们已经在类与对象讲过,今天的重点就是继承

        要理解继承十分简单,对比我们的现实生活,我们很容易就能想到孩子继承大人的家产、知识、颜值,又或则是债务等等一些列的身外之物。那么对于我们的代码来说,那就变成了实现面向对象程序设计是代码可以复用的最重要的手段。

        继承允许我们在保持原有类特性的基础上进行扩展,增加功能,产生新的类,被称为派生类。而我们的原有类也被称为基类或则是父类。

        上述的讲解有些抽象,咱们程序员还是用代码说话更清晰。

//父类/基类
class Person
{
public:
	void print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "zhangshan";
	int _age = 18;
private:
	int secret = 20;
};

//子类/派生类
class Student :public Person
{
protected:
	int _stuid;
	int _grade;
};

class Teacher :public Person
{
protected:
	int _jobid;
};

         上述的代码很好的展示了继承到底是做什么的,可以看到我们有Student和Teacher两个派生类,也就是学生和老师,那么他们都有什么相同的特征呢?他们都是人,作为人那么他们一定有自己的名字和年龄,所以我们就可以将这一部分变量抽象出来,作为基类的成员,再由他们各自的类对其特有的属性进行封装维护。

        所以对于继承来说,我们首先要做的就是先要逻辑抽象出不同对象的相同特征,在对这些特征整合作为基类,再由各自的派生类对其对象进行特殊化完善

2 继承方式

        看到我们上方代码的派生类继承的方式:如下

class Student :public Person

class Teacher :public Person

         公式为:class 派生类名 :继承方式  基类名。

        对于派生类名和基类名我们都能理解,那么这个继承方式是个什么?继承方式其实很好理解,他就像是我们类与对象里面写的访问权限一样,有公有(public)、保护(protected)、私有(private)这三种方式。具体请对应下表:

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

        我以保护继承为例进行讲解,如果我们是保护继承,那么父类的公有成员在子类中称为保护成员,也就是外部是不能访问的(在父类对象当中,外界是可以访问的),保护成员依然是保护成员,私有成员是属于父类私有的在子类当中是无法访问的。

        注意到,我这里说的是什么?无法访问,不是没有,这说明了什么?继承时父类是将所有的数据都给了子类,但是因为访问限定符的原因,会产生无法访问到的情况,如上方代码:

void test1()
{
	//二者都继承于父类,那么他们都有父类的成员
	//只有父类的私有成员不能使用,其余的成员都能使用(对于public继承而言)
	_test1::Student s;
	_test1::Teacher t;
	_test1::Person  p;
	s.print();
	t.print();

	//计算子类的大小,需要加上父类的所有成员,无论是私有成员还是其余成员
	//那么就表示了子类会完全继承所有成员,但是因为权限的原因,导致不能使用
	cout << sizeof(s) << endl;
	cout << sizeof(p) << endl;
			//派生类		//继承方式			 //基类
	//class Student : (public/protected/private) Persion

	//继承方式和访问限定符都有三种方式,访问限定符受到继承方式的限制,继承方式也受到访问限定符的限制
	//例如:public继承,不能显示出private成员,protected继承,会将基类的public成员变为protected访问
}

         看到了吗,上述代码中,我并没有对两个子类进行初始化,但是通过继承,我们不仅能够在实例化子类后使用父类的成员函数,还能对父类的成员初始化。

        并且通过sizeof函数,可以看到子类和父类的大小分别是44和36,而子类当中只有两个int变量也就是8个字节,44-8 = 36,也就是子类其余的大小全部继承于父类,不管父类当中的成员是什么访问属性,都被继承下来了。

        那么此时我们可以得出以下结论:

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

        对于继承方式来说,我们平时使用、或者是在工作中做项目时,基本都是使用公有继承,因为私有继承和保护继承确实有些鸡肋,保护继承都还好,私有继承才是难顶,我们都已经继承下来了一个类,但是你却不让我使用这个类的成员,这不纯纯的脱裤子放屁——多此一举嘛。更可况继承还要额外开辟父类大小的空间,这点确实不太好,所以我们记住之后使用时,只用公有继承就好。

3 父子类的转换关系

        看到下方的代码:

    double dd = 1.1;
    int i = dd;

        我们在编辑器下写出这样一段代码,实际上编辑器在int i = dd这里为我们作为隐式类型转换,这就是先将dd转换为整型,再由i去接收,其中是由临时变量的参与的,而临时变量具有常量特性,也就是不可更改,如何证明呢,请看下方代码:

         上述错误证明了不同类型赋值是有隐式类型转换的:实际上我们写的代码应该是如下:

    double dd = 1.1;

    const int temp = (int)dd
    int i = temp;

         如果我们加上const引用就没有问题了:

         那么对应我们的父类和子类是否也有同样的特性呢?请看代码:

// 实例演示三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name = "zhangshan"; // 姓名
private:
	int _age; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
public:
	void Print_s()
	{
		cout << _name << endl;
		cout << _stunum << endl;
	}
protected:
	int _stunum = 123; // 学号
};

         上述的定义好像没有任何错误欸,不管是引用还是用指针都正确了,并且编译也没有报错,那么只能证明编辑器是支持这样的写法的,那么它是如何支持的呢?如下图:

         也就是父类向子类赋值父类只会要自己有的那一部分变量,对于子类特有的变量它是不会要的,那么这样做有什么用吗?有大用,但是这里我不讲解,等到后面实现一个完整的继承我再来讲解。

        我们称上面的操作为切片,我个人喜欢叫做截断,所以我们能够理解为子类赋值到父类其实就是一个同类型之间的赋值,并不会产生临时变量。

        但是我们将顺序反过来,也就是用父类赋值给子类呢?

         就错咯,为啥呢,这也挺好理解的,子类从常规角度来讲,都是会比父类多变量的,父类的变量比之类少,那么赋值时就会导致什么,野指针问题咯,这种情况肯定是不允许出现的,所以呢,就不能咯。

        那如果非要实现这种赋值怎么搞?很简单,直接强转类型就好:

       这有极大的可能出现野指针问题,所以尽量别用吧,这是我的建议。

4 父子类中同变量的对应关系

        对于一个变量来说,他是有一个他专属的作用域的,出了这个作用域,也就没有他了,比如我们在main函数中定义一个int a = 0;又在全局区域定义一个int a = 2;请问这两个变量冲突了嘛?很明显没有,因为他们的作用域不同,编辑器能够识别我们要找的是哪一个a,所以是不会报错的。那么对于父类和子类之间是否还有这样的特性呢?请看下方代码:

// 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; // 学号
};

        上述代码中,父类和子类都有一个_num,那么我们能成功输出吗?

         答案很明显,能行,通过作用域限定符,还是能够找到对应的变量输出,不过呢,对于继承而言,如果我们不用作用域限定符,那么就只会找到该类的变量,而不是父类的那个变量,我们称这个现象为隐藏或则重定义,当然不仅仅是变量更构成这个现象,函数也是可以的。

// 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)
	{
		cout << "func(int i)->" << i << endl;
	}
};

     //成员函数构成重载
    _test5::B b;
    b.fun(1);
    //添加访问限定符才能找到
    b.A::fun();

         函数的隐藏于重载不相同,重载只会出现在两个函数处于同一块作用域才会出现,而且对于重载的要求是名字相同,但是函数里的参数类型不同才行,而隐藏不一样,隐藏的触发条件是只要两个函数的名字相同就会触发,然后根据不同的作用域操作就能找到对应的函数。

5 子类/派生类的默认成员

//派生类的默认成员函数
namespace yf
{
	class Person
	{
	public:
		Person(const char* name = "peter")
			:_name(name)
		{
			cout << "Person()" << endl;
		}

		Person(const Person& p)
			:_name(p._name)
		{
			cout << "Person(&)" << endl;
		}

		Person& operator=(const Person& p)
		{
			cout << "operator=(&)" << 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(const char* name, int num)" << 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;
	};
}

         上述代码的上半段为父类,下半段为子类,其中子类当中的拷贝构造和赋值重载都用到了一个我上面讲解的重要知识,切片操作:

:Person(s)

Person::operator=(s);

        咱们直接传入一个子类的对象作为一个父类对象去构造或则赋值,这其中用到了不就是我们的类型转换吗?不就是切片吗?是不是很有用这个操作。

        请看子类对象实例化的步骤:

void test6()
{
	yf::Student s1("zhangshan",123456);
	cout << "&&&&&&&&&&&&&&&&&&&&&&&&" << endl;
	yf::Student s2 = s1;
	cout << "&&&&&&&&&&&&&&&&&&&&&&&&" << endl;
	yf::Student s3("lishi",654321);
	s3 = s2;
}

         根据我的切分,我们能够很清晰的看到不同的子类操作步骤到底做了什么。采用默认构造时,先调用了父类的默认构造,然后再构造自己。拷贝构造时,先调用父类拷贝,再拷贝自己的成员。使用赋值重载时,先去调用了父类的重载和自己的重载。这里的父类子类的顺序不重要,我们可以根据自己的需要调整,重要的是子类的默认函数可以在父类的基础上添加,极大的方便了我们。

        上面的三个默认函数都是需要我们手动添加的,但是对于析构函数却不同,子类调用析构函数之后,一定会调用父类的析构函数,所以我们在子类当中是不需要添加关于父类的任何析构操作的。

6 友元和静态成员

        对于类来说,友元关系是不能继承的,要解释为什么很简单,因为友元的出现本身就已经破坏了一个类的封装,也就是友元能够突破访问限定符的限制,访问任何成员,那么我的子类并不相被任何外部访问,要是从父类继承一个友元,那不就破坏了子类的封装了。

        静态成员对于一个类来说只有一个变量,也就是整个类都是用这一个变量,继承也不会破坏这个操作,他也会跟着父类只用这一个变量,不会新添加。

        下方代码可以证明:

namespace _test7
{
	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;
	}
}
void test7()
{
	_test7::Person p;
	_test7::Student s;
	_test7::Display(p, s);
}


namespace _test8
{
	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 test8()
{
	_test8::Student s1;
	_test8::Student s2;
	_test8::Student s3;
	_test8::Graduate s4;

	//在基类当中定义了一个静态成员,它的所有类包括派生类都是用同一个static实例
	cout << " 人数 :" << _test8::Person::_count << endl;
	_test8::Student::_count = 0;
	cout << " 人数 :" << _test8::Person::_count << endl;
}

        以上就是我对继承的全部理解了,希望能够帮助到大家。

猜你喜欢

转载自blog.csdn.net/weixin_52345097/article/details/129782605
今日推荐