C++继承保姆级讲解上

        相信大家一定听说过面向对象三大特点,封装,继承和多态。今天就介绍其中的一大特点:封装,相信大家在看完一定会有所收获的。

        

为什么要有继承

        首先假设一种情况,我们要编写一个学校人员管理系统,那么必定会创造不同的类来代表不同的人,其中可以将人分为,老师,学生等,不同种类的人。不同种类的人的信息如下图。

        由此我们便可以创建两个类来表示不用的人,就可以写出如下代码。

class Teacher
{
public:
	Teacher(string name="", int age=0, string workName="")
	{
		_name = name;
		_age = age;
		_workName = workName;
	}

private:
	string _name;
	int _age;
	string _workName;
};


class Student
{
public:
	Student(string name = "", int age = 0, string major = "")
	{
		_name = name;
		_age = age;
		_major = major;
	}

private:
	string _name;
	int _age;
	string _major;
};

        仔细分析上面的代码我们会发现,老师类与学生类有十分相同的成员,如年龄,姓名。如下图。

        我们这只是简单的举例,每个人的信息还不是特别的多,这样给每个类写固定的成员还可以接受,但当类复杂起来时,面对大量相同的代码,本贾尼博士为了减少程序员的负担,提高代码的复用性,就开发出了继承的方法。

        于是我们就可以将上述的代码简化成如下的代码。(这里只是举例,不必看懂全部语法,后序会深入讲)

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

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
	}
private:
	string _workName;
};


class Student:public Person
{
public:
	Student(string name = "", int age = 0, string major = "")
		:Person(name,age)
	{
		_major = major;
	}

private:
	string _major;
};

        从学生类和老师类中提取出了相同的成员,年龄和名字,将其封装在一个Person类中,这样Teacher类继承Person后,就不需要单独写年龄和名字成员了,形象的理解就是把Person类中代码拷贝一份到Teacher种,不过这次执行拷贝的对象时编译器罢了。

        这样我们就复用了原来的代码,简化了程序员的负担,理解了为什么要有继承语法后,我们接着来细谈语法。

继承的概念

        像上面的例子中,我们使用老师类与学生类继承了person类,在C++中有特定的名字,关系如下图

        其中派生类又常被称为子类,基类又称为父类,就像现实中继承的关系一样,儿子可以继承父亲的一些资产,从而帮助自己未来的道路。就行麻辣王子一样,大学毕业发现自己是富二代,继承父亲家业。大家感兴趣可以看看。

麻辣“王子”被隐瞒亿万家产20年,“爽文男主”竟是自己?_澎湃号·湃客_澎湃新闻-The Paper

       继承的语法十分简单,就是在类名后面加上继承方式和基类,如果有多个继承中间以逗号隔开。 继承的方式有三种分别是 private ,public ,protected。在一个基类中成员的访问限定符也有三种 private ,public ,protected,这就形成了九种情况,我们来一一的来看。

public继承       

         首先就是public继承父类,这也是我们用的最多的,此时在子类中对于父类public ,protected成员访问权限不变,就相当于子类自己的public ,protected成员一样。

        子类就可以调用父类的Print函数

        而对于父类的private成员就不一样了,子类对于他是不可见状态,这个不可见状态不是不存在,只是无法在子类中直接访问。我们将原来的代码稍作修改。

        在父类person种加了个private成员,其他方面不变,如下代码。

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		_name = name;
		_age = age;
	}
	void Print(void)
	{
		cout << _name << " " << _age << endl;
	}
protected:
	string _name;
	int _age;
private:
	int num;
};

        此时我们在VS打开调试--窗口--监视如下图。

        在创建的子类te中有num成员变量,但在类中无法访问num

        如果强行写num,就会编译报错。如下图。

        此时num对于派生类就是不可见状态,子类中继承有num成员,但是直接访问不了,那我们真的就没有办法使用他了么?

        答案是否定的,我们虽然不可以直接的访问它,却可以间接的访问他,就像最开始我们只有一个类时,对于private变量,我们无法在类的外部直接修改他,却可以通过公有的函数间接修改他,在继承时,也可以才用这种技巧。修改后如下代码。

        

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		_name = name;
		_age = age;
	}
	void Print(void)
	{
		cout << _name << " " << _age << endl;
	}
	void setNum(int x=0)
	{
		num = x;
	}
	int getNum(void)
	{
		return num;
	}
protected:
	string _name;
	int _age;
private:
	int num;
};

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
		setNum(2);
	}
private:
	string _workName;
};

        我们通过两个函数setNum,getNum就可以在子类中实现对父类private成员的访问与修改!!

        到此我们便了解完public继承方式,简单来说就像把public ,protected成员变成自己的一样使用,而对于private便是不可见状态。

private继承

        如果子类对于父类是private继承,那么父类的public ,protected成员对于子类而言相当于私有成员,只可以在子类里面访问,外部不可以访问,对于父类的private成员是不可见的状态,那么此时在子类中无法直接访问到父类的private成员,这种情况用的较少些。

protected继承

       如果子类对于父类是protected继承 ,此时对于父类public ,protected成员在子类的访问权限是protected,在子类中可以使用,但在子类外面不可以访问,而对于父类private成员任然是不可见的状态。

        把 Teacher类的继承方式改为protected,

class Teacher:protected Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
		setNum(2);
	}
private:
	string _workName;
};

        那么下面程序就会报错。 Teacher继承person类的Print函数的权限是protected,只能在子类里面使用,而不能在外部访问。

继承总结

        对于继承后的访问权限是什么,我们可以简单的理解为,权限趋向于缩小的,权限的大小为public >protected>private,而在子类中的访问权限是MIN(继承方式,父类访问权限)。对于基类的private成员在子类中不可见,这一点要记住。此时再看这张图是不是就十分容易记住了。

        还有一个细节是,继承时使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,虽然有默认情况,大家最好还是显示的写出来。

        继承就类似于拷贝了父类一份代码到子类里面。

        接下来我们继续学习继承

基类与派生类的作用域

        我们在使用了继承过后,子类的作用域与父类的作用域是两个不同的作用域,但又有所关联,如下图

        类似于学生类中有个Person类的声明。当我们访问student中的成员时,会先在student中寻找,找不到再去person中寻找。

        有了上面的基础认识后,下面的代码是否会编译报错呢?

#include<iostream>
using namespace std;

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		_name = name;
		_age = age;
	}
	void Print(void)
	{
		cout << _name << " " << _age << endl;
	}

protected:
	string _name;
	int _age;

};

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
	}
private:
	string _workName;
};


int main()
{
	Teacher te("李老师", 18);
	te.Print();

	return 0;
}

        在这里我们实例化了一个Teacher类,然后在使用te.Print();函数,但在Teacher类中我们没有实现Print()函数,于是编译器就会像父类中寻找,如果父类中也没有找到就会报错,好在这个实例中我们在Person种实现了Print();函数,他便可以正常运行。

        下面将Teacher类稍作修改。

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
	}
	void Print(void)
	{
		cout << _name << " " << _age <<" "<< _workName << endl;
	}
private:
	string _workName;
};
int main()
{
	Teacher te("李老师", 18,"高级教师");
	te.Print();

	return 0;
}

        加了Print函数,这次运行会报错么?

        答案是不会的,结果如下。

        这次在子类中找到了Print函数就不会去父类中去找Print函数了,所以打印了高级教师。

        那么Teacher中的Print与Person中的Print构成函数重载么?答案是不构成的!Teacher类域与Person类域是两个不同的类域,函数重载必须要在同一个类域里面。只不过由于继承的关系在Teacher类域中找不到成员可以去Person类域中寻找罢了。

        其次从另外一方面函数的参数类型一样也可以判断出他们一定不是重载的关系。

        在C++中子类与父类的成员名相同(对于函数而言只需要函数名相同即可),导致访问的时候优先使用子类成员的行为叫做隐藏。

        我们会优先使用子类的成员,但我们可以通过指定类域的方式,显示的访问父类的成员。如下代码

int main()
{
	Teacher te("李老师", 18,"高级教师");

	te.Print();
	te.Teacher::Print();
	te.Teacher::Person::Print();
	te.Person::Print();

	return 0;
}

        其中默认是子类的函数,其中最后两种指定类域的方式都可以。都指向了Person类域。我们便可以使用被隐藏了的函数,当然这个函数只能是public修饰的了。

基类与派生类赋值转换

        这个时C++规定的一种特殊的语法,主要是用于实现多态,多态在这里我就不多讲了,准备下一篇文章在细讲,这里就先简单的了解下特殊转换。

        下面我们看个简单的代码

int a = 1.1;

        相信大家一定见到过这样的代码,按照C++语法的规定,操作符两边的类型必须相同,但在这个代码中a的类型是int,而1.1是float,这两个类型明显不同,但不会编译报错,可能会有所警告.这是为什么呢?

        原因是编译器帮我们做了一切,操作符两边的类型必须相同这个是铁律不会变,是编译器帮我们生成了一个中间变量t=1,然后将t的值赋给a,相当于int a = (int)1.1;强制类型转换。如下图

        内置类型基本支持强制类型转换,到了C++类这里,本贾尼设计了一种新的类型转换,基类与派生类赋值转换,父类的指针或引用可以接受子类的对象,当然对于父类本身也是可以接受的。

        将原来的代码修改如下。

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		_name = name;
		_age = age;
	}
	void Print(void)
	{
		cout << _name << " " << _age << endl;
	}

protected:
	string _name;
	int _age;

};

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
	}
	void Print(void)
	{
		cout << _name << " " << _age <<" "<< _workName << endl;
	}
private:
	string _workName;
};
void Print(Person & self)
{
	self.Print();
}

int main()
{
	Teacher te("李老师", 18, "高级教师");
	Person pe("张三", 18);

	Print(te);
	Print(pe);


	return 0;
}

        添加了一个全局函数Print,多少一句这个全局函数不与类中Print构成重载,他们不在同意个作用域内。

        上述代码的结果如下,编译没有报错!先不论结果,Print(Person & self)函数可以接受类型不同的参数就十分令人震惊,当然也许有人觉得这是编译器帮我们建立了一个零时对象,我们可以在函数中验证下。

        我们在Person类中增加了一个set函数,可以将名字重置,如果Print(Person & self)函数接受的是零时对象,那么修改后,原来对象的名字应该是不变的。

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		_name = name;
		_age = age;
	}
	void Print(void)
	{
		cout << _name << " " << _age << endl;
	}
	void set(string tmp = "")
	{
		_name = tmp;
	}
protected:
	string _name;
	int _age;

};
void Print(Person & self)
{
	self.Print();
	self.set("新名字");
}

int main()
{
	Teacher te("李老师", 18, "高级教师");
	Person pe("张三", 18);

	Print(te);

	te.Print();


	return 0;
}

        上述的运行结果如下,我们在void Print(Person & self)函数中修改了te的名字,它实际情况也确实被修改了,那么这就说明我们引用就是te本身而不是临时变量!!!这有个形象的名字叫做切片,如下图。

        这个更多的具体用法见多态文章,在这里就不过多赘述了。

友元函数

        友元函数不可以被继承,基类也不能访问子类的私有或着受保护的成员。友元函数不属于类的成员,只是被允许使用类的成员的函数,所以不可以被继承,简单的来说就是你爸爸的朋友不是你的朋友。

        下面就是错误的代码,友元函数访问子类的私有成员。

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		_name = name;
		_age = age;
	}
	friend void Print(Person self);
protected:
	string _name;
	int _age;
};

void Print(Person self)
{
	cout << self._name << self._age <<self._workName << endl;
}

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
	}
private:
	string _workName;
};

        假如反过来,友元函数是子类的,那么他就可以像正常子类的函数一样访问子类与父类的成员,其实就可以看为子类中有一份父类的代码,那么此时访问子类中的父类不是理所当然么?

class Person
{
public:
	Person(string name = "", int age = 0)
	{
		_name = name;
		_age = age;
	}	
protected:
	string _name;
	int _age;
};

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
	}
	friend void Print(Teacher& self);
private:
	string _workName;
};

void Print(Teacher& self)
{
	cout << self._name << self._age << self._workName << endl;
}

        但在这里有个特殊情况,假如我们穿的是父类对象,那么就不可以访问父类的成员,我们只可以通过子类对象间接的访问父类作用域的对象!!

        如下代码就是错误的,我们可以抽象的理解为我的朋友不是我父亲的朋友,无法访问父亲的成员,但我的朋友可以访问我父亲留给我的遗产。

class Teacher:public Person
{
public:
	Teacher(string name="", int age=0, string workName="")
		:Person(name,age)
	{
		_workName = workName;
	}
	friend void Print(Teacher& self);
	friend void Print(Person& self);
private:
	string _workName;
};

void Print(Teacher& self)
{
	cout << self._name << self._age << self._workName << endl;
}

void Print(Person& self)
{
	cout << self._name << self._age << endl;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值