封装、继承、多态
(1)封装
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
(2)继承
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
继承现有类 + 扩展
继承概念的实现方式有三类:实现继承、接口继承和可视继承。
实现继承是指使用基类的属性和方法而无需额外编码的能力;
接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。
(3)多态
多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
多态的作用:1.隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用;2.接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。
重载(overload),是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。在编译期间就已经确定了,是静态的(记住:是静态)。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!
结论就是:重载只是一种语言特性,与多态无关,与面向对象也无关!
覆盖(重写(override)),是指子类重新定义父类的虚函数的做法。
真正和多态相关的是“覆盖”。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态(记住:是动态!)的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚邦定)。
被重写的函数不能是static的。必须是virtual的(即函数在最原始的基类中被声明为virtual )。
重写函数必须有相同的类型,名称和参数列表(即相同的函数原型)
重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public,protected也是可以的
重定义(redefining): 子类重新定义父类中有相同名称的非虚函数(参数列表可以不同)。
总结:封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用!多态的作用,就是为了类在继承和派生的时候,保证使用“家谱”中任一类的实例的某一属性时的正确调用。
重写(override)与重载(overload)的区别
1、方法的重写是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关 系,是水平关系。
2、重写要求参数列表相同;重载要求参数列表不同。
3、重写关系中,调用那个方法体,是根据对象的类型(对象对应存储空间类型)来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。
C++中为什么要使⽤虚函数
虚函数实现动态绑定,提高程序灵活性
实现动态绑定的两个条件:
1. 相应成员函数为虚函数
2. 使用基类对象的引用或指针进行调⽤
请讲⼀下C++虚函数的底层实现机制
虚函数表:一个类的虚函数的地址表,所有对象都是通过它来找到合适的虚函数。
虚函数表指针:每个类的对象实例都拥有⼀个指针指向这张虚函数表(一般在对象实例最前面的位置),它帮助对象找到这张表的地址,然后就可以遍历其中的函数指针,调用相应的函数了.
注意:虚函数表⼀个类有⼀个,⽽不是一个对象一个
一般继承(有虚函数覆盖)
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
多重继承(有虚函数覆盖)
注意:子类的虚函数被放在了第⼀个父类中
三种调用拷贝构造函数的情况:
- 需要用一个对象去初始化同一个类的另一个新对象;
- 函数调用时,形参和实参的结合
- 函数返回值为对象时
浅拷贝是指在对象赋值时,只对对象中的数据成员进行简单的赋值,但对于存在动态成员(指针等),就会出现问题,使得两个对象的动态成员指向了同一个地址,而不是不同地址,内容相同。
构造函数为什么不能虚化:
1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。
2 虚函数对应一个vtable,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。(从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数)
3 虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
为什么基类的析构函数是虚函数?析构函数一定是虚函数吗?
对于使用new在自由存储区中实例化派生类对象,如果将其赋给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数,这可能导致资源未释放,内存泄漏等问题。
在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生。
如果我们删除一个指向派生类对象的基类指针,而基类析构函数又是非虚的话, 那么就会调用基类的析构函数,派生类的析构函数得不到调用。
如果不需要基类对派生类及对象进行操作,则不能定义虚函数,因为这样会增加内存开销.当类里面有定义虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间.所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数.
1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。
2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。
3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。
5) 纯虚函数通常没有定义体,但也完全可以拥有。
6) 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
7) 非纯的虚函数必须有定义体,不然是一个错误。
8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。
什么时候需要虚析构函数
当你的类准备给别人继承时要提供虚析构函数
通过基类的指针来删除派生类的对象时,基类的析构函数应该是虚的。否则这样的删除只能够删除基类对象,而不能删除子类对象,形成了删除一半形象,从而造成内存泄漏。
注意:
如果不需要基类对派生类及对象进行操作,则不能定义虚函数(包括虚析构函数),因为这样会增加内存开销。
拷贝构造函数在什么时候被调用?
1、对象在创建时使用其他的对象初始化
Person p(q); //此时复制构造函数被用来创建实例p
Person p = q; //此时复制构造函数被用来在定义实例p时初始化p
2、对象作为函数的参数进行值传递时
f(p); //此时p作为函数的参数进行值传递,p入栈时会调用复制构造函数创建一个局部对象,与函数内的局部变量具有相同的作用域
需要注意的是,赋值并不会调用复制构造函数,赋值只是赋值运算符(重载)在起作用
p = q; //此时没有复制构造函数的调用!
简单来记的话就是,如果对象在声明的同时将另一个已存在的对象赋给它,就会调用复制构造函数;如果对象已经存在,然后将另一个已存在的对象赋给它,调用的就是赋值运算符(重载)
3、如果函数的返回值是类的对象,函数执行完成返回调用者时.
4、需要产生一个临时类对象时。
默认的复制构造函数和赋值运算符进行的都是”shallow copy”,只是简单地复制字段,因此如果对象中含有动态分配的内存,就需要我们自己重写复制构造函数或者重载赋值运算符来实现”deep copy”,确保数据的完整性和安全性。