继承的基础概念:
简单来说,继承是面向对象程序设计使代码可以复用的手段。通过继承可以对原有类进行拓展,继承体现出层次结构的复用。
为什么要有继承的概念:
比方有下列三个对象
我们会发现他们都有一些共同的部分,他们都有姓名电话,同时有一些不同的信息存放在一起。 为了使得看起来更加简洁,更加方便,我们可以使用到继承。
创建一个新的类用来存放这些共同的特性,我们将这个类称为父类也叫作基类。 后边继承该父类的类叫作派生类也叫作子类。 在后面要用到父类中的信息,就可以直接使用,而不用每一个子类都要写重复的部分了。
继承的用法以及注意事项
继承使用的格式:
首先来看一组代码体现继承的运用
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18;
};
class Student :public Person
{
protected:
int _stuid; // 学号
};
int main()
{
Student st;
st.Print();
return 0;
}
代码结果:
这组代码,我们对Student这个类采用了继承Person的方式,使得Student创建的对象st可以直接访问到Person类中的Print函数。
注意事项:
1.继承方式和访问限定符很有讲究!
基类中的其他成员在派生类中的访问方式是通过继承方式和访问限定符中,取最小的那个决定的!!
2.先了解一下protected和private访问限定符的区别:
private成员以为着无论是在派生类还是在类外都不可以被访问,只允许在类本身访问。
protected成员可以在类本身里和派生类中被访问,不允许类外访问。
public就是类内,派生类,类外都能访问了。
所以,如果我们希望父类中成员在派生类中被访问,而又不希望在类外被访问,我们就可以使用到protected来修饰了。
3.使用关键词class默认继承方式是private,使用struct默认是public
例如这里我们去掉子类继承方式public,让它使用默认的private,那就不能访问父类public中的成员了!
平常使用继承,我们都是推荐使用public的!毕竟已经使用继承了,肯定希望能访问到父类不是。
基类和派生类对象赋值转换
①派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
②基类对象不能赋值给派生类对象
如果非要将基类对象赋值给派生类对象,就需要用到强制类型转换
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
void Test()
{
Student sobj;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
//sobj = pobj;
//基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj;
Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
}
继承中的作用域
①首先,在继承体系中基类和派生类都有独立的作用域
②其次, 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
成员函数的隐藏,只需要函数名相同就构成隐藏(不要求参数,返回值也一样)
③但注意在实际中在继承体系里面最好不要定义同名的成员!
函数只要函数名相同就可以构成隐藏!
派生类的默认成员函数
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3.派生类的operator=必须要调用基类的operator=完成基类的复制。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
为什么析构一定是先清理派生类再清理基类?并且为什么是自动调用基类的析构?
首先,先清理派生类是因为:倘若,基类先被清理,而我们还在派生类中访问基类中的成员,那岂不是会出错
其次,因为我们没法实现手动清理派生类再去清理基类,如果派生类已经被清理,那我们在派生类中就无法调用基类的析构函数了。 所以,在我们清理掉派生类之后,系统会自动得调用基类得析构函数!
接下来,看具体的代码。 分别是Person和Student中的构造,拷贝构造,赋值,析构。
尤其要注意上面说的点,必须在派生类中对基类进行构造,拷贝构造和赋值的操作。
还要特别特别注意派生类中赋值的细节!
class Person
{
public:
Person(const char* name = "peter")
: _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) // 拷贝构造,将Student对象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); //先对基类进行复制操作,这里有个要格外注意的点。
// 我们必须加上Person类域,否则这里会自动调用Student中的赋值,
//也就会导致不断不断得赋值,直到栈溢出
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
void Test()
{
//Student st1; //这里我们得进行初始化,因为默认构造函数只分为3种。
//啥都不写系统默认生成的、全缺省的,无参的。
Student st1("Zhangsan", 20); //构造
Student st2(st1); // 拷贝
Student st3("LiSi", 17);
st1 = st3; //赋值、
Person p1;
p1 = st1; //进行切割
}
继承与友元
按惯例思想会认为,派生类既然是继承的基类,那么基类中的友元函数,派生类也可以使用,也就是友元函数可以访问派生类中的成员,但是不然。
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。
复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
我们会发现,如果出现菱形继承,数据是很冗余的,并且出现了二义性的问题
什么是二义性
我们可以看到Teacher在Student类中有一份数据,同时又在Assistant中有一份数据,他们都有一份相同的Person,那我们访问Person中数据的话,我们究竟是通过Student访问的还是Assistant访问,这就出现了二义性。
为了解决这个问题,我们有一个办法,就是加上virtual来虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题
但我们要记住,我们的virtual是加在腰部,在这里就是Student和Teacher同时指向Person,那我们就在这个地方加上virtual就可以让Assistant改变二义性和数据冗余的问题。
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person //加上virtual就可以解决该问题
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person //加上virtual就可以解决该问题
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
我们通过内存来看一看加virtual和不加virtual的区别:
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A //腰部加virtual
{
public:
int _b;
};
class C : public A
//class C : virtual public A //腰部加virtual
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
不加virtual:
我们可以看到,B,C,D类都是紧挨在一起的,A对应在B,C, D类中都有一份。与预期结果是一样的!
加virtual:
我们可以看到B,C,D依旧是紧挨在一起的,但可以看到A被单独拿出来放在最下面了,此时的A是被共用的,首先改变了B中的a为1,又通过C类将a改成了2,最后呈现的结果就是2。此时,就只有一个A了,同时B和C内存中第一行多了四字节的内容,这个就是一个指针,指向的内容反应了该类于A的偏移量,例如C类中的偏移量为12,那么C类只需要通过往后找12字节就可以找到我们的A了!
这个内容了解就可以了,不用深究!
继承和组合
简单来说,继承是is-a的关系,组合是has-a的关系
那么,我们什么时候使用继承什么什么时候使用组合就一目了然了。
这种情况下,属于组合的情况,我们就是使用组合就好了。
如果继承和组合同时可以使用的话,我们还是推荐使用组合。 因为组合的耦合性更低!
耦合性和内聚性
耦合性就是各个组块之间的联系紧密度,内聚性就是一个组块内各项的联系紧密度。
我们更期望的是低耦合高内聚的情况,因为如果是高耦合,也就意味着可能出现“牵一发而动全身的情况”。一个组块的代码出现问题,修复一个组块很可能导致其他组块受到影响!
而高内聚,而有利于一个组块的管理。
继承还有一些用处,在后面的多态也会用到,这个后续再说!