深入篇【C++】C++<继承>特性详细总结(单继承/多继承)
- Ⅰ.继承理解
- Ⅱ.继承方式
- Ⅲ.基类派生类对象赋值转化
- Ⅳ.继承中同名成员
- Ⅴ.派生类的默认成员函数
- Ⅵ.继承中友元与静态
- Ⅶ.多继承<菱形继承问题>
- Ⅷ.继承与组合
Ⅰ.继承理解
继承的本质就是复用,而在C++中这种继承是类层次的复用。
基类和派生类就是父类和子类之间的关系。子类继承父类,子类在保持原有父类的基础上进行扩展,增加功能。
class person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
//protected:
string _name = "xiaotao";
int _age = 19;
};
class student :public person//子类student,继承父类person的成员变量和成员函数
{
protected:
int _stuid;
};
class teacher :public person//子类teacher,继承父类person的成员变量和成员函数
{
protected:
int _joined;
};
int main()
{
person p;//父类
student s;//子类
teacher t;//子类
}
Ⅱ.继承方式
继承的方式很简单,只要在子类的后面加上冒号和要继承的方式,还有要继承的父类即可。
要注意基类的私有成员在派生类中都是不可见的,不是说没有继承下来,而是不允许访问。这与我们所理解私有限定符不同,私有限定符是在类里可以访问,而在类外无法访问,这里一旦父类成员私有,则派生类类里类外都无法访问。即父类的私有成员,子类无法"继承"。(并不是没有继承,是不能访问)。
基类中访问限定符有三种类型,而派生类中继承方式又有三种类型,这组合起来就有9种方式,不过这九种组合后的规则是有规律可循的。
1.继承方式和访问限定符两两组合后,最终的决定权在于这两个种权限比较小的那种。
权限:private<protected<public.
比如protected与public组合,最终的权限是protected。private与public组合,最终的权限是private。
private与任何权限组合最终仍然是private。
不过在C++继承中种很少使用private权限,都已经要继承了,还能就给一部分?在继承中大多数都是使用public权限的。
2.protected成员就和正常类中的私有成员一样,在类里可以访问,而在类外无法访问。即如果想让基类的成员被派生类继承后,派生类类里可以访问,而在类外无法访问,就可以用protected。
3.注意class定义的类成员默认是私有,而struct定义的类成员默认是公有。不过最好在写时,显式的标注成员的访问方式。
Ⅲ.基类派生类对象赋值转化
1.父类对象是不可以赋值给子类对象,子类对象可以赋值给父类对象/父类引用/父类指针。
2.子类对象赋值给父类对象之间发生赋值兼容转化(切片,切割)。
3.正常来说,不同类型之间赋值会产生临时变量,而子类对象赋值给父类对象,中间没有产生临时变量。而是发生切片,直接将子类中父类的那一部分之间拷贝一份切割过去赋给父类对象。子类可以看成一个特殊的父类,赋值时将相同的部分切割拷贝过去。
4.子类对象赋值给父类对象,中间不会产生临时变量,就没有常性,这就允许子类可以成为父类对象的引用/指针。
5.父类对象就可以根据引用或指针,来改变子类中父类的数据。
int main()
{
person p;
student s;
teacher t;
//发生赋值兼容转化:切片,切割
//不同类型之间赋值正常来说是会发生类型转化
p = s;
//而子类赋给父类直接将子类中跟父类对象相同的部分切割拷贝过去
person p1 = s;
person& p2 = s;
//中间不会产生临时变量,所以可以将子类赋给父类变名
//将子类中跟父类对应那一部分切割拷贝过来
p2._name = "xiaoyao";
//可以通过别名,指针来找到子类中的跟父类相对应的数据
//子类是一种特殊的父类
person* p3 = &s;
p3->_name = "hehehe";
}
Ⅳ.继承中同名成员
1.当派生类中出现与基类相同的成员函数时,要注意这时这两个函数不是构成函数重载。因为函数重载要求在同一作用域中。而这明显是两个作用域了。这里这两个函数构成隐藏(重定义)。
2.隐藏(重定义):子类和父类中有同名成员变量/函数时,子类的成员会隐藏父类的成员。也就是当调用子类对象中该成员函数时,会先在子类中找,如果在子类找不到再去父类中找。
3.父类和子类中只要有相同名字的函数就会构成隐藏,不管函数参数是否相同。
4.在子类成员函数中如果想要访问父类的同名成员,可以使用基类::基类成员 ,显式的访问。
class person
{
public:
void fun()
{
cout << "person::fun()" << endl;
}
protected:
string _name = "heeheh";
int _num = 111;
};
class student :public person
{
public:
void fun()
{
cout << "student::fun()" << endl;
}
void Print()
{
cout << "姓名:" << _name << endl;
cout << "学号" << _num << endl;
cout << person::_num << endl;
}
protected:
int _num = 999;
};
int main()
{
student s;
s.Print();
s.fun();
//默认会先到子类找,子类找不到就会报错
s.person::fun();//想调用父类中的同名函数,需要显式的调用。
}
Ⅴ.派生类的默认成员函数
1.默认成员函数就那几个:构造函数,拷贝构造函数,析构函数………
子类对象要初始化肯定要在初始化列表初始化,而父类对象初始化可以用它的默认构造初始化,但如果父类没有默认构造函数,则需要在子类的初始化列表,显式的初始化父类对象。
(类似于匿名对象初始化)。而且父类对象需要先初始化,因为父类对象先声明(从父类中继承下来,就说明先声明父类)。
2.子类的拷贝构造函数,需要显式的初始化父类对象。因为如果不显式的初始化父类,那么父类就会自动调用父类默认构造函数,这时对象初始化的结果可能不是你期待的,所以需要手动显式初始化父类,即调用父类的拷贝构造函数。
3.析构函数,就不需要改动,就正常析构子类即可。因为父类先构造,子类后构造,这就要求子类先析构,父类后析构。为了满足这个条件,析构函数被改成了隐藏(重定义)。那么这样当子类的析构函数结束,会自动去父类找父类的析构函数去析构父类对象。(隐藏:子类中找不到后再去父类中找) 所以如果自己写析构函数,就无法保证析构的顺序。
①派生类的构造函数必须调用基类的构造函数来初始化基类的那一部分。父类有默认成员函数时,派生类初始化会自动调用。当父类没有默认成员函数时,需要在派生类的初始化列表显式初始化基类。
②派生类的拷贝构造必须调用基类的拷贝构造函数来完成父类的拷贝。
③派生类的赋值必须调用基类的赋值完成父类部分赋值。
④析构函数析构子类即可,当结束时,会自动调用父类析构。
⑤要注意父类先声明先构造,子类后声明后构造。
继承中的默认成员函数:
class person
{
public:
person(const char* name)
:_name(name)
{
}
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="SDSAD", int id=123)
:person(name)//需要显式调用的初始化父类,父类的构造
, _id(id)
{
}
student(const student& s)
:person(s)//显式的调用父类中的拷贝构造
, _id(s._id)
{
}
student& operator=(const student& s)
{
if (this != &s)
{
//父类中的赋值运算符与子类中的赋值运算符重载构成的隐藏,必须显式的调用父类中的赋值重载来赋值父类的数据
person::operator=(s);
_id = s._id;
}
return *this;
}
~student()
{
//多态中将父类和子类的析构弄成隐藏,先找子类再找父类
//应该先析构子类再析构父类
}
protected:
int _id;
};
Ⅵ.继承中友元与静态
1.父类中的友元,子类是无法继承下来的。如果子类想用父类的友元类,需要手动显式的将这个友元变成自己的友元。
2.继承中的静态变量,是共同属于父类和子类的。父类中的静态成员,在子类中不会单独的拷贝一份,而是继承它的使用权。
//继承中的静态变量
class person
{
public:
person()
{
++_cout;
}
//protected:
string _name;
public:
static int _cout;
};
int person::_cout = 1;
class student :public person
{
protected:
int _num;
};
//静态成员是共同属于父类和子类的,子类继承静态成员不是将父类的静态成员拷贝过来,而是
//继承父类静态成员的使用权
int main()
{
person p;
student s1;
student s2;
cout << &s1._name << endl;
cout << &s2._name << endl;
cout << &p._cout << endl;
cout << &s1._cout << endl;
cout << &s2._cout << endl;
cout << &person::_cout << endl;
cout << &student::_cout << endl;
}
Ⅶ.多继承<菱形继承问题>
多继承常见的就是双继承了,双继承就是一个派生类有两个基类。
正常情况下是没有问题的。不过多继承还是引发了一个问题:菱形继承
菱形继承是什么意思呢?
就是派生类的两个基类,又是从同一个基类继承下来的。
菱形继承存在的问题:<数据冗余>和<二义性问题>。
数据冗余:因为基类1和基类2继承下来父类的数据是一样的。当派生类再从基类1和基类2中继承时,那么最初的基类的数据就继承了两份下来。这样就造成了空间浪费。
二义性问题:与上面同理,派生类继承了两份一样的数据,那么当对数据访问时,该访问的是基类1中的父类数据还是该访问基类2中的父类的数据呢?这就造成就访问不明确的问题。
解决方法: 虚继承
采用虚继承后就可以解决菱形继承带来的问题:数据冗余和二义性。
在继承方式前面加上关键字virtual即可变成虚继承。
//双继承
class person
{
public:
string _name;
int _age;
};
class student :virtual public person//虚继承
{
protected:
int _num;
};
class teacher :virtual public person//虚继承
{
protected:
int _id;
};
//双继承--->存在问题:菱形继承
//person ,student ,teacher 都没事,只有assistant有事,存在两个person中的数据
//解决方法:虚继承
class assistant :public student, public teacher
{
protected:
string _marjor;
};
int main()
{
assistant a;
a.student::_age = 10;
a.teacher::_age = 20;
//使用虚继承后,person中的数据就只有一个了
//改变一个另一个父类数据也会改动
a._age = 31;
}
虚继承的原理比较复杂,是采用一个叫虚基表的东西来完成的。编译器会将公用数据放在内存的最下面,虚假表里存的是偏移量,这个偏移量的左右就是为了让两个基类找到相同的数据,这样一个基类访问该数据就可以通过相同的模型来完成。
菱形继承复杂的一批,大家最好没事不用搞这玩意,一旦出错,你会被坑的裤衩子都没有。
Ⅷ.继承与组合
《优先使用组合而不是继承》
1.复用有两种方式,一种是继承复用,一种是组合复用。
2.继承复用,基类的内部细节派生类是可以可见的。这样的复用,依赖关系更强,耦合度高。
3.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。组合后,基类的细节是不可见的,这样的复用,依赖关系不强,耦合度低。
4.实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。