一、什么是继承
继承是面对对象复用的重要手段,继承是类型之间的关系建模。我们通过继承定义一个类,共享共有的东西实现各自不同的功能。我们看一个例子:
在这个例子中,可以看到Person类是父类,而Student类是子类;子类以公有的继承关系继承了父类的成员,也就是说父类的成员变成了子类的一部分。
那么继承关系对子类中继承到的成员有什么影响呢?
答:
- 基类中的私有成员在派生类中不可见,即它的派生类是无法访问基类中的私有成员的。若想要访问该成员变量,就要把该成员设置为protected。
- 不论哪种继承方式,子类可以访问基类的公有和保护成员;而不能访问基类的私有成员。
- 使用struct默认的继承方式是public,而class默认的继承方式是private。
- public继承是一个接口继承,保持is-a的原则。即每个父类的可用成员对子类也可用;每个子类对象也是一个父类对象。
- protected、private继承是实现继承,保持has-a的原则。
二、赋值与转换(赋值兼容规则--public继承)
前面我们从继承关系中得到,public继承是一个接口继承,保持is-a的原则,所以就出现了子对象与父对象的赋值。它们遵循下列规则:
- 子对象可以赋值给父类对象(切片),但父类对象不可以赋值给子类对象。
- 父类对象的指针/引用可以指向子类对象(切片);但子类对象的指针/引用不可以指向父类对象。
Person p;
Student s;
//子类对象可以赋值给父类对象
p = s;
//父类对象的指针可以指向子类对象
Person* p1;
p1 = &s;
//父类对象的引用可以指向子类对象
Person& p2 = s;
三、继承体系中的作用域
- 在继承体系中,子类与父类都有独立的作用域;
- 当子类和父类有同名的成员时,子类会屏蔽对父类该成员的直接访问。这种现象叫做隐藏/重定义。
class Person
{
public:
void show()
{
cout << _name << endl;
}
public:
string _name;
int _num;
};
class Student : public Person
{
public:
int _num;
};
int main()
{
Person p;
p._num = 10;//访问父类成员
Student s;
s._num = 20;//访问子类成员
s.Person::_num = 30; //若想访问父类成员,可以指定作用域
return 0;
}
练习1:在下面的代码中,会发生什么?
class Person
{
public:
void show()
{
cout << _name << endl;
}
void f1()
{}
protected:
string _name;
int _num;
};
class Student : public Person
{
public:
void f1(int)
{}
protected:
int _num;
};
int main()
{
Student s;
s.f1();
return 0;
}
A f1()构成重载 B f1()构成隐藏 C 代码编不过 D 以上都不对
答:BC
首先,子类与父类中有同名函数f1,构成隐藏。其次子类对象调用成员函数f1调的是子类的f1(该f1有参),我们在调用时未能传入参数,所以调不到该成员函数,代码编不过。
练习2:运行下面的代码,会发生什么?
class Student
{
public:
void f1()
{
cout << "f1()" << endl;
}
protected:
int _num;
};
int main()
{
Student* s = NULL;
s->f1();
return 0;
}
A 代码编不过 B 可以编译通过,但程序会崩溃 C 可以编译通过,并且正常输出 D 以上都不对
答:C
四、派生类的默认成员函数
我们知道,类有6个默认成员函数,在程序员没有定义的时候,系统会自动生成。那么派生类呢?
- 派生类的默认成员函数如果没有定义,系统会默认合成。
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
Person(const Person& p)
{
cout << "Person(const Person& p)" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
return *this;
}
protected:
string _name;
int _num;
};
class Student : public Person
{
protected:
int _num;
};
void test()
{
Student s1;
Student s2(s1);
s2 = s1;
}
int main()
{
test();
system("pause");
return 0;
}
- 在定义子类的构造函数和赋值运算符的重载函数时,需要显示的调用父类的构造和赋值函数;
- 但在定义子类的析构函数是,不需要显示调用父类的析构。子类的析构函数调用完后,出了作用域,系统会自动调用父类的析构函数。
class Student : public Person
{
public:
Student(int num = 010)
:Person()
, _num(num)
{}
Student(const Student& s)
:Person()
,_num(s._num)
{}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student()
{}
protected:
int _num;
};
五、菱形继承
单继承与多继承:
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或两个以上的直接父类
菱形继承:
class AA
{
protected:
int _a;
};
class BB : public AA
{
protected:
int _b;
};
class CC : public AA
{
protected:
int _c;
};
class DD : public BB, public CC
{
protected:
int _d;
};
菱形继承的对象模型:
可以看到_a在类DD的对象中保存了2份,使得菱形继承存在二义性和数据冗余的问题。二义性我们可以通过制定作用域来解决,可是数据冗余呢?这时候,可以通过虚继承的方式来解决二义性和数据冗余的问题。
虚继承:
那么虚继承是如何解决这些问题的呢?在vs2013下探索:
定义四个类AA,BB,CC,DD,BB类和CC类继承了AA类,DD类继承了BB类和CC类。
class AA
{
public:
AA(int a = 1)
:_a(a)
{}
protected:
int _a;
};
class BB : virtual public AA
{
public:
BB(int b = 2)
:_b(b)
{}
protected:
int _b;
};
class CC : virtual public AA
{
public:
CC(int c = 3)
:_c(c)
{}
protected:
int _c;
};
class DD : public BB, public CC
{
public:
DD(int d = 4)
:_d(d)
{}
protected:
int _d;
};
针对上面的代码,在加关键字virtual时和不加关键字virtual时,分别探索内存:
上图的左边展示的是不加virtual时的内存分配,右边展示的是虚继承的内存。
可以看出虚继承将AA中的成员放在了最下面,在原本放AA类中成员的地方存放了一个指针,该指针指向的位置存放的是一个偏移地址,这样就可以通过这个偏移地址找到AA类中的成员。同时因为AA类中的成员在DD类中只保存了一份,也就解决了数据冗余和二义性的问题。
六、其它注意事项
- 友元关系不能继承,也就是说基类的友元不能访问派生类的私有和保护成员。
- 基类定义了static成员后,整个继承体系中只有一个该成员;无论派生出多少个子类,都只有一个该static成员实例。