继承
继承概念:
一个类中包含成员变量和成员函数,不同的类它们的数据和函数往往是不同的,但是有时候两个类的内容基本相同或者有一部分相同,例如以下两个类:
第一个类:
class student
{
public:
void display()
{
cout<<num<<endl;
cout<<name<<endl;
cout<<sex<<endl;
}
private:
int num; //学生基本信息
char name[20];
char sex;
};
第二个类:
class student1
{
public:
void display();
{
cout<<num<<endl;
cout<<name<<endl;
cout<<sex<<endl;
cout<<age<<endl;
}
private:
int num; //第一个类中也有
char name[20]; //第一个类中也有
char sex; //第一个类中也有
int age; //新增内容
};
所以继承就是在一个已经存在的类的基础上建立一个新类,原来的类称为 基类 或者父类,产生的新类叫做 派生类或者 子类。
继承机制是面向对象中 重复使用代码 的重要手段。
如图:
继承格式
(以代码一和代码二为例)
新类名称:继承权限 基类名称
{
};
class student1:public student
{
public:
void dispaly()
{
cout<<age<<endl;
}
private:
int age ;
};
继承权限
注意继承时的第一行,**class student1:public student **,这里的public是继承权限,类的成员变量和成员函数都有三种权限,public,private和protect,继承权限也有三种:public,private和protect
-
public继承:基类中的公有成员和受保护成员保持原有的访问属性,基类的私有成员,在派生类中不可见(存在,但不能访问)。
-
protect继承:基类中的公有成员和受保护的成员在派生类中成为受保护状态,基类的私有成员,依旧处位基类私有状态
-
private继承:基类中的公有成员和受保护成员在派生类中成为私有成员,基类的私有成员,依旧为基类的私有成员。
记忆方法总结:权限大小排序 ,private<protect<public,继承的三种权限和成员的三种访问权限,派生类成员究竟处于什么状态,取决于那个权限小的。(例如,公有成员,私有继承,则公有成员在派生类中为私有,受保护成员 公有继承,则受保护成员在派生类中为受保护成员),觉大多数情况下继承为公有继承。
不管哪种继承,基类中的私有成员在子类中都不可见。 关键字为struct的类,继承默认为public,关键字为class的类,继承默认private。
has a 与is a
is a(公有继承)
众所周知,C++具有三种继承:公有继承、私有继承、保护继承。
最常见的就是公有继承,它建立一种is-a的关系。
如何理解is-a呢?即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。
例如:
有一个水果Fruit类,可以保存水果的重量和热量;
香蕉是一种水果,所以可以从Fruit类派生出Banana类;
Banana类继承了父类的所有数据成员,
因此,Banana对象将包含表示香蕉重量和热量的成员;
此外,Banana类可以添加专门用于香蕉的成员。
但是,Banana类不能删除基类的Fruit的属性。
因此Banana is a kind of Fruit,即我们所说的is-a关系。
class Banana:public Fruit
{
......
};
那么has-a关系呢?组合关系
同样举个例子:
午餐可能包括水果,但是午餐并不是水果;
所以不能从Fruit公有派生出Lunch类;
在午餐中加入水果的正确方法是将其作为一种has-a关系。
那么如何实现has-a呢?
通常的是有两种方法:包含和私有继承。
所谓的包含就是将Fruit对象作为Lunch类的数据成员,即新的类包含一个类的对象。
所谓的私有继承就是class Lunch:private Fruit。
题:
class A
{
public:
A()
{
a = 1; //a是公有,b是私有,两个地址是连续的吗?
b = 2;
}
int a;
private:
int b;
};
a,b地址是连续的,说明访问限定符对数据在内存中存储是不影响的。
那么,在这种情况下继承时,父类中的私有数据对子类是不可见的,子类一旦访问,就会报错?
要说清楚这个问题,我们需要知道,程序本身是存放在磁盘中的,一旦运行,就会加载到内存中,此时会为数据开辟空间,但是,在程序运行前,需要经历编译,汇编,链接过程,c++用法规定父类的私有数据,子类不能访问,所以在编译期间就会报错,不会通过,所以对于内存来说,私有公有数据连续存放也没有关系,并不是说由内存来判断哪些数据子类可以访问,哪些数据,子类不可以访问。
赋值兼容-----切片
子类赋值给父类,多余的数据会被“切掉”。
-
子类对象可以赋值给父类对象(切割/切片,因为子类对象数据通常多)
-
父类对象不能赋值给子类对象
-
父类的指针/引用可以指向子类对象
-
子类的指针/引用不能指向父类对象(可以通过强制类型转换完成,但不能调用成员函数,会崩溃)
试着执行下面的代码,观察结果
class Person
{
public :
void Display ()
{
cout<<_name <<endl;
}
protected :
string _name ; // 姓名
};
class Student : public Person
{
public :
int _num ; // 学号
};
void Test ()
{
Person p ;
Student s ;
// 1.子类对象可以赋值给父类对象(切割 /切片)
p = s ;
// 2.父类对象不能赋值给子类对象
//s = p;
// 3.父类的指针/引用可以指向子类对象
Person* p1 = &s;
Person& r1 = s;
//4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
Student* p2 = (Student*)& p;
Student& r2 = (Student&) p;
// 这里会发生什么?
p2->_num = 10;
r2._num = 20;
}
继承中的作用域
-
** 在继承体系中基类和派生类都有独立的作用域。**
-
子类和父类中有同名成员,子类成员将屏蔽父类的同名成员。(在子类成员函数中,可以使用 基 类::基类成员 访问) 这种现象叫隐藏,也叫重定义 。
同名隐藏: 在基类和派生类中,具有相同名称的成员(成员函数 /成员变量),如果用派生类的对象去访问这个同名成员,则只能访问到派生类自己的,这就叫重定义。只能通过加基类作用域的方法去访问同名成员的基类成员。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
派生类的6个默认成员函数
继承体系下,派生类中如果没有显式定义这六个默认成员函数,编译器则会合成这六个默认的成员函数,但重点关注4个:构造函数,拷贝构造函数。赋值运算符重载,析构。
构造函数的主要作用就是进行初始化,在派生类中,要对新增成员进行初始化就要定义新的派生类构造函数,同时,还要对继承下来的基类成员进行初始化,这是由父类的构造函数完成。但基类构造函数和析构函数不能被继承,所以要在派生类的构造函数中调用父类的构造函数,派生类的清理工作也要定义新的析构函数
在继承时,子类不仅有父类的内容,也有自己的内容,那么,如何初始化父类中的内容?在子类的构造函数中调用父类的构造函数。
定义一个派生类对象时,基类和派生类构造函数调用的先后顺序:先构造 基类,在构造派生类。
析构时先析构派生类(清空派生类资源),在析构基类(清空基类资源)。
基类代码
class person
{
public:
person(int n) //父类构造
{
age= n;
cout << "this is base\n" << endl;
}
~person() //父类析构
{
cout << "base destrucct\n" << endl;
}
person(const person &n) //父类拷贝构造
{
age = n.age;
cout << "copy succcessful\n" << endl;
}
person &operator=(const person &p) //父类赋值运算符重载
{
if (this != &p)
{
age = p.age;
}
cout << "operator successful\n" << endl;
return *this;
}
private:
int age;
};
派生类
class student:public person
{
public:
//派生类构造函数的定义
/*
派生类名(参数总表)
:基类名(基类的参数)
*/
student(int age, int n) //派生类构造函数
:person(age) //先构造基类
,num(n)
{
cout << "deriver class" << endl;
}
~student() //派生类析构
{
cout << "desturct deriver" << endl;
}
student(const student&s) //派生类拷贝构造
:person(s)
,num(s.num)
{
cout << "devire copy successful\n" << endl;
}
student&operator=(const student &s) //派生类的运算符重载
{
cout << "devire operator succsefull\n";
if (this != &s)
{
person::operator = (s);
num = s.num;
}
}
private:
int num; //学号
};
- 派生类的构造函数定义
基类没有构造函数,则派生类的构造函数可以定义也可以不定义。
基类有带有形参的构造函数,则派生类一定要定义构造函数。
构造一个类的对象之前,必须先构造其中的嵌套类对象,若没给嵌套类传参数,则调用嵌套类的默认构造函数,否则调用嵌套类的带参数的构造函数
派生类名(参数总表)
:基类名(基类的参数)
//派生类的拷贝构造函数若没有定义,编译器会自定生成拷贝构造函数,自动调用拷贝父类的拷贝构造函数(不管是自定义的函数自动生成的)
-
派生类的析构函数
清理完派生类的资源后,在清理基类的资源。 -
派生类的赋值运算符重载
和基类里面的赋值运算符重载构成重定义,使用时注意加作用域。