继承,就像字面的意思,是从别的地方获得一些本来不属于自己的东西。在c++中,继承是非常重要的对象复用手段。
在编写大型程序时,往往有很多的类,每个类都有自己的成员变量和成员函数,但有些类之间的数据成员和函数却相同,为了节省代码量和工作时间,只需继承父类中的数据成员和函数即可。
有了继承,继承对象可以使用被继承对象的成员变量和成员函数,这样大大提高了代码的复用性。
继承的简单实例:
class person//父类(基类)
{
public:
void show()
{
cout<<_name<<'-'<<_age<<endl;
}
protected:
string _name;
int _age;
}
class student : public person //子类(派生类)
{
protected:
int _num;
}
在上面这段代码中:
被继承的类是person,被继承类也叫父类/基类,他有成员函数show()和两个被保护的成员变量_name和_age。
继承的类是student,继承类也叫子类/派生类,他有一个被保护的成员变量_int。
他们之间的继承关系是公有继承public,这个在下面会细讲,先记住这个写法。
现在在student内部就已经可以访问person类内的成语函数和变量了。
继承的种类
继承分三种:公有继承(public),私有继承(private),保护继承(protected)。
(在实际生活中,绝大多数继承都属于公有继承)。
属于何种继承,主要看子类类名后面的部分,我们再来看一下之前上面的代码:
如图可以得知,子类student继承了父类person的内容,继承的类别是公有继承。
关于父类成员的访问限定符与子类的继承方式有如下表格:
- 我们发现,当基类的成员访问限定符是private时,不管你是何种继承,子类内部都不能访问父类成员,否则会报错,这里的不可见不是说父类的成员不存在,这点需要注意。
- 而当继承方式是公有继承或保护继承时,子类内都可以访问父类成员,但成员的访问限定符可能会发生改变
- 如果你想让父类中成员不想被父类对象访问,而又想在子类对象内使用,那么定义成保护继承就没错了
- 继承方式可以缺省,使用关键字class时默认的继承方式是private继承,而使用struct默认的继承方式是public,所以最好还是显式写出继承的种类,防止出错。
但是,有人就会问了,到底怎么判断子类中父类成员的访问限定符呢? 有一个技巧:
子类中父类成员的访问限定符永远是父类成员本身限定符和继承类型中取范围小的那个。
而各个访问限定符的范围大小时 public最大,protected次之,private最小。
比如:某类a的成员访问限定符是 protected,又有一个以公有继承方式继承他的类b,那么,在子类b中,a的成员访问限定符就是protected,因为public>protected。
继承与转换–赋值兼容性规则–public继承
1.子类对象可以赋值给父类对象(切割/切片)
2.父类对象不能赋值给子类对象
3.父类指针/引用可以指向子类对象
4.子类指针/引用不可以指向父类对象
来看下面这张图:
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;
}
继承体系中的作用域& 隐藏
来看看这段代码:
class Person
{
public :
Person(const char* name = "", int id = 0)
: _name(name )
, _num(id)
{}
protected :
string _name ; // 姓名
int _num ; // 身份证号
};
class Student : public Person
{
public :
Student(const char* name, int id, int stuNum )
: Person(name , id )
, _num(stuNum)
{}
void DisplayNum ()
{
cout<<" 身份证号: "<<Person ::_num<< endl;
cout<<" 学号"<< _num<<endl ;
}
protected :
int _num ; // 学号
};
void Test()
{
Student s1 ("paul", 110, 1);
s1.DisplayNum ();
};
此时调用DisplayNum( )函数时,到底是打印子类中的_num还是父类中的_num呢?
这里, 就要介绍一下隐藏这个概念了。
隐藏:
在子类和父类中有同名的成员时,子类成员将屏蔽对父类成员的直接访问。如果想要父类成员,则需要加上基类::基类成员 确定访问作用域。
隐藏同样适用于成员函数,而且只要函数名相同就构成隐藏,不用管参数的个数和类型,如果想调用父类中的成员函数,那么也需要加上基类::基类成员确定访问作用域。
看了隐藏的定义,是不是觉得他和c++中的函数重载很像?他们直接按又有什么区别呢?
其实很简单:
重载:在同一作用域下,函数名相同,参数类型或个数不同。
隐藏:在不同作用域下,函数名相同,参数不用考虑。
派生类的默认成员函数
在继承关系里 ,如果派生类如果没有显式的定义默认成员函数,系统会默认合成这些默认成员函数。
什么叫合成?
当子类调默认成员函数时,属于父类的那部分跳转至父类的默认成员函数,自己的那部分调用子类的默认成员函数。(有点像前面讲的切割/切片)
其中,各类默认成员函数都有要注意的地方:
1.构造函数:先调用父类的构造函数,再调用子类的构造函数。
2.拷贝构造函数:在拷贝一个子类对象时,该子类对象中的父类部分会由父类的拷贝构造来完成(切片/切割)
3.赋值运算符重载:一定要确定作用域!,在赋值过程中,父类部分会调用父类的赋值运算符重载函数,但是,子类的和父类的函数名相同,都是operator=,会构成隐藏,变成子类不停的调用自己的赋值运算符重载函数(父类函数被屏蔽),最终会造成栈溢出!
4.析构函数:先析构子类对象,再析构父类对象(注意!,再写派生类的析构函数时,不用自己调用父类的析构函数,毕竟先析构子类,父类会有操作系统自己来调用)
其实构造和析构函数很好理解:要构造一个子类对象,他的一部分来自父类,所以先构造好父类的那一部分,在构造剩余子类自己的那部分。又因为后构造的先析构(满足栈的特性),所以先析构子类对象那部分,再析构父类对象那部分。
class Person//父类
{
public :
Person(const char* name)//构造函数
: _name(name )
{
cout<<"Person()" <<endl;
}
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 )
{
cout<<"Student()" <<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);//此处一定要确定作用域,原因见上文第3点
_num = s ._num;
}
return *this ;
}
~Student()//析构函数,,先析构子类对象,再析构父类对象
{
cout<<"~Student()" <<endl;//(不用自己调用父类的析构函数)
}
private :
int _num ; //学号
};
void Test ()
{
Student s1 ("jack", 18);
Student s2 (s1);
Student s3 ("rose", 17);
s1 = s3 ;
}