目录
一、概念和定义:
1、概念:
继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。 这样做,也达到了重用代码功能和提高执行效率的效果。 当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。
2、定义:
格式
基类:(父类)
基类里面的公有或者保护成员给子类
子类:(子类)
子类里面可以访问父类的公有或者保护成员
注意:
1、上述的不可见的意思是,基类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它。
2、如果在父类中,想让一个成员在类外面不能被访问,但是在继承的子类中想要被访问就需要在父类中用protect进行修饰。
3、三者的修饰的限制度:public > protect > private
4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5、事实上,就红框里面的用的是最多的。
二、子类和父类对象的赋值转换
首先通过代码:
cpp
class Person
{
protected:
string _name;
};
class Student : public Person
{
protected:
int _id;
};
如上所示,定义一个基类person,然后一个student类继承它,那么在赋值的时候,
是可以将子类赋值给父类,但是不能够将父类赋值给子类,这里涉及到切片原理:
如上所示,从子类往父类赋值的话,可以将父类的所有成员都赋存在的值,但是如果父类给子类赋值,那么如上面的_id父类就没有,就会存在随机值的问题。
这就是切片原则。
不仅仅是对象的赋值,还支持指针赋值,引用赋值等等。
三、继承中的作用域
理解:
在继承中,子类和父类都有其独立的作用域。
隐藏:
如果在子类和父类中有同名的成员对象或者函数,那么子类成员将屏蔽父类对同名成员对象或者函数的直接访问,这就是隐藏,也可以是重定义,如果想要访问父类的成员,那么就可以使用父类::父类成员访问
注意:
如果是相同的成员函数名,即使形参不同,那么也够成隐藏,所以函数隐藏只需函数名相同即可。
四、子类的默认成员函数
默认成员函数是一个类中自动有的,那么继承后的子类的默认成员函数和父类的默认成员函数有着什么样的关系呢?
cpp
class Person
{
public:
Person(const char* name = "张三")
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
{
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 age)
:Person(name)
,_age(age)
{
cout << "Student(const char* name,int age)" << endl;
}
Student(const Student& p)
:Person(p)
,_age(p._age)
{
cout << "Student(const Student& p)" << endl;
}
Student& operator=(const Student& p)
{
cout << "Student& operator=(const Student& p)" << endl;
if (this != &p)
{
Person::operator=(p);
_age = p._age;
}
return *this;
}
~Student()
{
Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _age;
};
int main()
{
Student s1("zhangsan", 18);
Student s2(s1);
Student s3("lisi", 17);
s1 = s3;
return 0;
}
通过上述代码可以看到,三个Student变量对应右边的六个构造函数,但是发现析构函数有很多,那么为啥会造成这种原因呢?
理解:1、子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认
的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
2、子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
3、子类的operator=必须要调用父类的operator=完成父类的复制。
4、子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能
保证子类对象先清理子类成员再清理父类成员的顺序。
所以上述如果在子类的析构函数中调用了父类的析构函数,那么就会存在多次析构调用,所以在子类析构中就不用主动调用父类的析构了。
这样之后,析构函数的次数就对应上了。
并且,通过上述实验,我们可以看到,子类对象初始化先调用父类构造再调子类构造,子类对象析构先调用子类析构再调父类的析构。
注意:
因为后续多态需要重写,那么编译器会对析构函数名进行特殊处理,将子类和父类的析构函数名都处理成destrutor(),此时子类析构函数和父类析构函数构成隐藏关系
五、继承与友元:
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员,如果想访问,那么就需要在子类中声明友元。
六、继承与静态成员:
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
cpp
class A
{
public:
A()
{
_num++;
}
public:
static int _num;
};
int A::_num = 0;
class AA : public A
{
public:
AA()
{}
};
int main()
{
AA a;
AA b;
AA c;
cout << a._num << endl;
cout << &A::_num << endl;
cout << &AA::_num << endl;
return 0;
}
如上代码,每创建一个变量,_num 就会+1,上面我们创建了3个变量,在主函数中,就打印了_num的值,已经通过类访问类里面成员的地址,这样来看看静态变量_num有几个。
通过上述代码可以看到,静态成员变量在整个继承体系中是指存在一个的,毕竟地址都一样
七、菱形继承以及虚继承分析:
引入问题:
首先要了解什么是单继承,什么是多继承。
这个就是单继承,一个子类只有一个父类
这就是多继承,一个子类有至少两个父类。
这就是菱形继承,类D多继承B和C,同时,B和C都继承于A,这时,D中就有两份A类的成员了,就会产生二义性的问题。
如上就是它们继承的逻辑关系,若如上代码,访问_name就不知道是访问的Student还是Teacher的_name,就会存在访问不明确问题。这就叫做二义性。
但是可以指定类域来进行访问:
解决方式:
使用虚拟继承就可以了,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
虚拟继承就是在第一步继承中加上关键字virtual
cpp
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a;
a.Student::_name = "student";
a.Teacher::_name = "teacher";
a._name = "zhangsan";
return 0;
}
通过上述调试代码可以看到,加上虚拟继承以后,就只存在一份之前冗余的成员了,不管是通过类域访问,还是直接访问,都是访问的同一个成员。
底层是怎样解决的:
首先,构造出一个简单的菱形继承来:
cpp
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B,public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 6;
d.C::_a = 5;
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
这个时候如下,但是不能直接访问,B,C类中的_a是不相同的
如果加上虚继承解决后:
cpp
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B,public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 6;
d.C::_a = 5;
d._b = 2;
d._c = 3;
d._d = 4;
d._a = 7;
return 0;
}
通过上述监视窗口可以看到,_a只存在一份了,通过任何方式改变_a,都会使_a同时改变,
那么底层是怎么解决的呢?
接下来看底层:
通过上图,可以看到,在进行虚继承后,D中的结构发生了很大的变化,
它将_a存储在一个不在原先B,C中,而是存储在另一个位置,在B,C中取而代之的是指针
接下来输入这两个指针来看:
通过上图,我们可以看到在该指针所指向的位置下方存储着一个数字14,这是十六进制的,
转化为十进制后就是20字节,这个就是偏移量,接着返回原来的地方,向下偏移20个字节就在B类中找到了_a。
同样的道理,在C类中同样存储着一个指针,找到所指向的地址,找到偏移量为12(十进制)在回来通过偏移量向下找12个字节后就找到了_a。
同样,在进行虚继承之后,不仅仅是D类,B类也发生了变化,成为了和D类一样的处理方式,先通过偏移量找到原来位置距离_a多远,然后在向下找偏移量的位置后就找到_a。
下面是D类中的逻辑图:注意:
右边两个_a 偏移量的地址是不一样的。
最后看一个:
如上图,在下面中两个ptr->_a++是怎样实现的?
这二者指向的位置都不一样,但是在汇编中,二者的操作是一样的
直接看汇编:
通过上面可以看到,二者的找到_a并且++方式是一模一样的,第三行中的+4是因为地址存储在指向位置的下一个位置的。
二者的访问都是先取到偏移量,计算_a 在对象中的地址,最后在访问,++
总结:
少用多继承,能不设计出菱形继承就不设计菱形继承。
继承和组合:public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
植物----花,可以说花是植物
而组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
车----轮胎,可以说车有轮胎
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系如果既可以用继承,也可以用组合,就用组合。