前言
先前的博客里有提到,面向对象的特性有三个:封装、继承与多态。其中继承与多态是最重要,也是内容比较多的部分,我会分三到四期来记录和总结自己的个人所学。主要的参考视频是浙江大学翁恺老师的《面向对象设计C++》,他的视频解答了我许多一直以来在面向对象上的迷惑,感兴趣的同学可以去B站搜一下他的视频。
当代码需求的功能比较多、规模比较大的时候,面向过程的编程思想就会暴露其项目代码臃肿,难以阅读,维护成本高,不易拓展功能等等的弊端,做个项目去实际体会一下就知道了。在这种情况下,面向对象的编程思想就能够很好地解决这些问题,当然这也说明了他需要大体量的项目才能较好地体现他的优势。总的来说各有各的好处,都是必须要理解和掌握的编程思想。
继承与派生
描述
继承与派生是同一个过程的不同角度,我们将保持已有类的特性从而构筑新类的过程叫做继承,在已有类的基础上增加自己新类自己的特性叫做派生。被继承的已有类被称为基类(父类),派生出的新类叫做派生类(子类)。
目的
- 继承的目的:实现代码重用
- 派生的目的:当新的问题出现时,原有的程序不能解决问题(或者不能完全解决问题)时,就要对原有的程序进行改造,这里的改造不是指修改源码。
派生类的声明与定义
假设这里有个基类:
class base
{
public:
void who_am_i()
{
cout << "I'm base class" << endl;
}
};
另一个类继承base类,并增加自己的属性和方法:
class derived: public base //继承base类
{
public:
void who_am_i()
{
cout << "I'm derived class" << endl;
do_something();
}
void do_something() //派生类新增的方法
{
cout << "I inherit from base class" << endl;
}
private:
int m_int; //派生类新增的成员
};
派生类也可以有多个基类,其声明方法如下:
class derived: public base1, public base2, .....
{
.....
};
protected访问权限
之前我们讲过了public和private访问权限,这个protected访问权限之所以留在这里讲是因为他是专为继承而生的访问权限。他跟private一样,限制类的对象对其成员的直接访问,但是在该类的派生类内又可以像public一样直接访问。
class base
{
public:
void who_am_i()
{
cout << "I'm base class" << endl;
}
protected:
int m_pro_int;
private:
int m_pri_int;
};
class derived: public base
{
public:
void who_am_i()
{
cout << "I'm derived class" << endl;
do_something();
}
void do_something()
{
cout << m_pro_int << endl;
//cout << m_pri_int << endl; //error
}
private:
int m_int;
};
int main()
{
base b;
//b.m_pro_int = 10; //error
return 0;
}
继承方式
大家可能注意到了class derived冒号后面跟着的public,那要是换成protected或者private可不可以?当然是可以的,这是继承的三种方式:公有继承,私有继承和保护继承。具体描述如下:
- 公有继承(public):基类中的public和protected成员的访问权限在派生类中保持不变。 派生类可以直接访问基类的public和protected成员,但private成员不能直接访问。通过派生类的对象,只能访问基类的public成员。
- 私有继承(private):基类中的public和protected成员都以private的访问权限出现在派生类中。 派生类可以直接访问基类的public和protected成员,但private成员不能直接访问。通过派生类的对象,不能直接访问基类的任何成员。
- 保护继承(protected):基类中的public和protected成员都以protected的访问权限出现在派生类中。 派生类可以直接访问基类的public和protected成员,但private成员不能直接访问。通过派生类的对象,不能直接访问基类的任何成员。
基类的构造函数
有时候基类并没有默认(无参)构造函数,需要我们给基类传一定的参数,或者我们需要特地去调用基类的某个构造函数。如果不显示地调用的话,编译器会默认你是调用的是基类的默认构造函数(如果有的话,没有编译就会直接报错)。具体操作如下:
class base
{
public:
base(int m_int)
: m_pri_int(m_int)
{
cout << "in function base(int m_int)" << endl;
}
private:
int m_pri_int;
};
class derived: public base
{
public:
derived(int m_int)
:base(m_int)
{
cout << "in function derived(int m_int)" << endl;
}
// 编译会报错,除非补充了base类的默认构造函数
// derived()
// {
// cout << "in function derived()" << endl;
// }
};
提醒一下:如果派生类有多个基类时,会从左向右调用各个基类的构造函数,不管你在初始化列表是以什么样的顺序调用的基类构造函数。如果有特殊情况需要控制基类的构造顺序,注意继承的顺序就好了:
class derived: public base1, public base2, public base3 //基类构造顺序
{
public:
derived()
: base2()
, base1()
, base3()
{
//调用顺序还是1,2,3
}
};
顺带一提,成员变量的初始化是按照你的声明顺序从上到下的进行的,也不会受初始化列表顺序的影响。
另外如果基类定义了自己的拷贝构造函数,在定义派生类的拷贝构造函数的时候,记得要调用基类的拷贝构造函数。他会先调用基类的拷贝构造函数,再调用派生类的:
class base
{
public:
//其他函数省略...
base(const base& another)
{
cout << "base(const base& another)" << endl;
}
};
class derived
{
public:
//其他函数省略...
derived(const derived& another)
: base(another)
{
cout << "derived(const derived& another)" << endl;
}
};
int main()
{
derived d1;
derived d2 = d1;
// 输出:
// base(const base& another)
// derived(const derived& another)
return 0;
}
深浅拷贝及其造成内存泄漏的原因在我的另外一篇博客有详细说明,感兴趣可以去看一下。
作用域限定
如果你的派生类继承了base1类和base2类,而这两个类中有一个变量名一样的成员变量,这时候你在派生类中使用的该基类成员变量是base1类中的还是base2类中的?编译器不知道你到底是使用哪一边的,这就产生了二义性问题,编译会报错,而作用域操作符(::)就是解决这个问题的。
class base1
{
public:
base1() : m_pro_int(1) {}
protected:
int m_pro_int;
};
class base2
{
public:
base2() : m_pro_int(2) {}
protected:
int m_pro_int;
};
class derived: public base1, public base2
{
public:
void do_something()
{
//cout << m_pro_int << endl; //error
cout << base1::m_pro_int << endl; //1
cout << base2::m_pro_int << endl; //2
}
};
这个作用域操作符还有别的用处,比如使用某个类中定义公有的枚举类型,或者是使用某个命名空间定义的类或函数,都要用到这个操作符。我们经常在main.cpp文件的开头写的using namespace std;意思就是使用std(标准)命名空间,这样我们可以直接使用里面的东西,比如cout其实是std::cout,endl也应该写成std::endl。还有各种STL,vector,string,map,都在这个命名空间里。命名空间也可以自己去定义,这样在开发的过程中就可以避免与他人的变量或者函数重名。作为小知识了解一下,这里就不展开细谈了。
棱形继承问题
上面产生的二义性问题是显而易见的,但是有一种情况的继承就不那么明显,而且可能会被经常使用,那就是棱形继承的情况。考虑如下代码:
class base0
{
public:
int var0;
};
class base1 : public base0
{
public:
int var1;
};
class base2 : public base0
{
public:
int var2;
};
class derived: public base1, public base2
{
public:
int var;
};
那么derived类的内存模型就是是这样的:
很明显,var0产生了二义性问题,这是棱形继承所带来的问题。当然这个问题也可以通过作用域操作符来解决,但是一般不会去这么做。像这种问题应该采用虚继承的方式去解决:
class base0
{
public:
int var0;
};
class base1 : virtual public base0
{
public:
int var1;
};
class base2 : virtual public base0
{
public:
int var2;
};
class derived: public base1, public base2
{
public:
int var;
};
注意到base1类和base2类对base0类的继承中多了个virtual,这是C++的一个关键字,意思就是“虚拟的”,而这样的继承我们称其为“虚继承”。这个关键字的作用我会在下一篇中详细说明,他是多态的关键,这里先接触了解一下。
通过使用virtual关键字,derived类的内存模型会变成这样:
可以看到derived类中并没有两个var0成员,而是多出了两个指向base0类的指针,这样访问var0就能够清楚地定位到base0类的var0。(这里我少画了derived类新增的成员var,他在var2和var0之间)
总结
本篇大概总结了我知道的继承与派生的用法,写得比较随意,有时间的小伙伴可以自己测试一下。这些只是基础的东西,要全面理解和掌握之后,才能够理解多态及其应用。