继承本质是类层次的复用。
学生和老师的类中包含Person类的内容。
Print函数也是继承下来的。
上述代码中,Peraon叫父类,也叫基类,Student叫子类也叫派生类。
public叫继承方式,继承方式包括公有保护和继承。
基类中的私有本质是不想被继承。
父类中的私有成员,子类中不能访问。父类中的保护成员,子类可以访问。
但是age在studen类中,就是不能被访问。
基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
去掉继承方式,class默认是私有继承,子类外部不能访问继承成员,struct默认是公有继承,子类外部能访问继承成员。
子类会先构造父类,然后构造子类,析构时先析构子类,再析构父类。
父类和子类赋值转换。
子类对象 可以赋值给 父类的对象 / 父类的指针 / 父类的引用。
int main()
{
Person p;
Student s;
p=s;
}
其中p=s中间不存在类型转换,没有产生临时变量。
而底下这个i=d会产生临时变量,临时变量是int类型。
所以底下没有const修饰的引用会报错。在前面加上const就好了。
父类 ++后,子类也++
赋指针也行,指针指向子类中的父类部分。
- 继承中的作用域
下面这个_num访问的是Student本身的_num
访问父类的话可以指定作用域。
子类和父类有同名成员时,子类隐藏了父类成员。
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。
两个fun构成隐藏关系。
构成隐藏关系。
父类和子类函数名相同就构成隐藏。
重载要求必须在同一个作用域。 两个fun不构成重载。
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "B::func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
如果去掉fun调用时的参数,就会编译报错。
因为两个fun构成隐藏关系,去掉fun的参数,就要访问A中的fun,但因为被隐藏,直接访问不行。
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "B::func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun();
};
这样改就行了。
建议继承中不要定义同名成员。
- 派生类的默认成员函数
为什么调用Student会调用Person中的函数并执行?
子类中,父类成员调用父类构造函数完成初始化。
父类没有默认构造函数会报错。
规定不能在子类初始化列表直接初始化父类成员变量。
必须要调用父类的成员函数。
下面看看拷贝构造。
父类部分调用父类拷贝构造。
而如果是自定义类型,需要我们写个拷贝构造。
我们看到父类中拷贝构造函数参数是父类。
在这里如何找到要拷贝的Person对象?
我们直接传s就可以,
子类传给父类发生了赋值转换。s传的就是Person中的内容。
s赋值给p 不会产生临时对象。而是把s中p的部分切出来后赋值给p。但需要调用父类的拷贝构造。
rp可以引用s。 因为中间没产生临时变量,是直接切出来的。
我们来看看赋值。
我们不写,也会默认调用父类赋值。
显示写就这样写。
但这样运行会报栈溢出错误。
因为这个赋值和父类赋值构成隐藏关系,这里面的operator=(s)调用的是自己,所以会栈溢出。
这样改就好了。
派生类的默认成员函数
这样写析构会错。
这样就对了
因为子类析构函数和父类析构函数构成隐藏关系(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
而且这里面显示写父类析构的话,父类析构会调用两次。
调用两次的后果是,如果这样写就会报错。
为了让子类先析构父类后析构,子类析构函数不需要显示调用父类析构函数,子类析构后回自动调用父类析构。
- 继承和友元
友元关系不能继承。
在子类中也声明友元就好了。
父类中的普通成员,子类和父类生成的 对象中这两个成员不是同一个,而父类中的静态成员,子类和父类生成的 对象中这两个成员是同一个。
静态成员属于整个类,所有对象,同时也属于所有派生类及其对象。
而且可以这样访问。
这个父类中的静态count也可以这样初始化,但很不建议这样用。
再看下面的例子
第一个ptr->_name中,ptr是解引用,因为_name在对象中,而第二个ptr就不是解引用,因为成员函数不存在对象中。成员函数在代码段中。
ptr会传递一个空的this指针。
此时,下面划线这三行中,第一行会打印空指针,第二行会报错,第三行会正常打印。
下面这样写也不会报错。因为它都是通过类去寻找的。
- 多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承。
菱形继承有数据冗余和二义性的问题,Assistant对象中Person成员会有两份。
指定作用域可以进行初步解决。
但我们没有彻底地解决这个问题。
我们可以用关键字virtual进行虚继承。
下面这样写,_name就是一个了。
如果不虚继承的话B和C里面的_a是不一样的。
如果B和C虚继承A后,下面的_a就是同一个l。
那么红线部分的是什么呢?
我们用内存窗口查看一下。
我们发现这是他们地址相差的数值。它是距离虚基类的偏移量。
B C就是虚基类。
- 继承和组合
它俩都进行了复用,但是使用的复用方式不一样。
组合中,M的保护成员N不能看到。
继承是白箱复用,组合是黑箱复用。
但是组合有一个较大的优势是耦合度较低。
比如说X和M中各有80个保护成员和20个公有成员,那么X中的任何一个成员调整可能都会影响Y,而M中只有公有成员调整会影响N。
继承像is-a,比如学生是个人,而组合像has-a,比如头上有眼镜。
如果继承和组合都可以使用那么优先选择组合,因为它耦合度低。