继承
简单的继承关系:
基类成员在派生类中的访问属性:
继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 |
---|---|---|---|
public继承 | 仍为public成员 | 仍为protected成员 | 不可见 |
protected继承 | 变为protected成员 | 变为protected成员 | 不可见 |
private继承 | 变为private成员 | 变为private成员 | 不可见 |
总结:
1.基类中的公有成员。
公有继承时,基类中的所有公有成员在派生类中仍以公有成员的身份出现。
保护继承时,基类中的所有公有成员在派生类中仍以保护成员的身份出现。
私有继承时,基类中的所有公有成员在派生类中仍以私有成员的身份出现。
2.基类中的保护成员
公有继承时,基类中的所有保护成员在派生类中仍以保护成员的身份出现。
保护继承时,基类中的所有保护成员在派生类中仍以保护成员的身份出现。
私有继承时,基类中的所有保护成员在派生类中仍以私有成员的身份出现。
3.基类中的私有成员。
无论哪种继承方式,基类中的私有成员不允许派生类继承,即在派生类中是不可直接访问的。
如果不显式的给出继承方式关键字,系统默认为私有继承。
类的继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限。
从基类继承来的成员在派生类中的访问属性是由继承方式控制的。
继承与转换–赋值兼容规则–public继承
- 子类对象可以赋值给父类对象/父类的指针/父类的引用(切割/切片)
- 父类对象不能赋值给子类对象
- 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
- 父类的指针/引用可以指向子类对象
子类的指针/引用指向父类对象时发生强制转换时存在着安全隐患:
Base b2;
Derived pd2 = (Derived)&b2;
存在的问题是:
通过pd2能够访问的内存范围“扩张”了4个字节,如果访问_c就可能发生运行时错误,因为pd2指向的对象根本就没有成员_c的空间。
重载、覆盖(重写)、隐藏(重定义)
重载: 根据参数列表确定调用哪个函数。不关心函数返回值。
- 具有相同的作用域(即在同一类定义中)
- 函数名相同
- 参数列表不同(参数类型、顺序或数目不同(包括const参数和非const参数))
- virtual关键字可有可无
class A
{
public:
void f1(int i);
void f1(double d); //重载
void f1(int i, double d);//重载
void f1(double d, int i);//重载
int f1(int i); //不是重载,判断是否重载不关心返回值类型
};
覆盖(重写):指子类重新实现了父类的成员函数。 子类调用时会调用子类的重写函数,不会调用父类的函数。
- 不同作用域(分别在父类和子类中)
- 函数名称相同
- 参数列表完全相同
- 基类的函数必须是虚函数
class Person
{
public:
virtual void f1()
{
cout << "Person::f1()" << endl;
}
int _stunum;
};
class Student : public Person
{
public:
virtual void f1()//隐藏
{
cout << "student::f1()" << endl;
}
};
int main()
{
Student* p = new Student;
p->f1(); //studnet::f1
return 0;
}
隐藏(重定义):子类会屏蔽父类中任何同名的成员函数或成员变量。
- 子类的函数与父类的函数同名,但是参数列表有所差异,此时,不论有无virtual关键字,父类的函数在子类中将被隐藏。
- 子类的函数与父类的函数同名,参数列表也相同,但是父类函数没有virtual关键字。此时,父类的函数在子类中将被隐藏。
只要是同名函数,不管参数列表是否相同,父类函数都会被隐藏。
1、同名成员变量
class Person
{
public:
string _name;//姓名
};
class Student : public Person
{
public:
string _name;//姓名
int _num;//学号
};
2、同名成员函数
class Person
{
public:
void f1()
{
cout << "Person::f1()" << endl;
}
int _stunum;
};
class Student : public Person
{
public:
void f1(int x)//隐藏
{
cout << x << endl;
cout << "Student::f1()" << endl;
}
void f2()
{
cout << "Student::f2()" << endl;
}
};
int main()
{
Student s;
s.f1(); //“Student::f1”: 函数不接受 0 个参数
s.f1(1); //两个f1构成隐藏,要调用父类的方法,必须通过::运算符
return 0;
}
派生类的默认成员函数
在继承关系里面,在子类中如果没有显示定义这六个成员函数,编译系统则会默认合成这六个默认的成员函数。
(1)构造函数
子类的构造函数必须在初始化列表调用父类的构造函数。
当父类的构造函数不带参数时,子类不一定要定义构造函数;然而当父类的构造函数哪怕只带有一个参数时,它的所有的子类都必须定义构造函数。甚至所定义的子类的构造函数的函数体可能为空,它仅仅起到参数的传递作用。
(2)拷贝构造函数
对子类的拷贝构造,先对子类从父类继承来的成员变量拷贝构造,再对子类自己的成员变量拷贝构造。
(3)赋值运算符重载函数
赋值运算符的函数名都为operator=, 所以子类和父类的赋值运算符重载函数构成隐藏(重定义),在子类中要调用父类的赋值运算符重载函数,就要指定作用域。
(4)析构函数
虽然子类和父类的函数名不同,但在编译器中会对析构函数进行处理,全部变为函数名为distructor的函数,所以析构函数也构成隐藏(重定义),如果想要调用父类的析构函数,必须指定作用域,但一般不显示的调用父类的析构函数,因为编译器在出了这个作用域后会自动调父类的析构,若是显示调父类的析构,会出现析构两次的情况。
在一个继承关系中,析构函数被调用的顺序正好与构造函数的调用顺序相反。当创建子类对象时,首先调用父类的构造函数,随后再调用子类的构造函数;当撤销子类对象的时候,则先调子类的析构函数,再调父类的析构函数。
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); //与父类的operator构成隐藏(重定义),
//需要指定的作用域调用operator =
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
//Person::~Person();不用显示调父类的析构函数,
//虽然子类和父类的析构构成隐藏
//但编译器在出了这个作用域后会自动调父类的析构,
//若是显示调父类的析构,会出现析构两次的情况
}
private:
int _num; //学号
};