Cpp重难点问题
1、关于c++中钩子的介绍与使用
钩子故名思义,是一种连锁的程序,将某个功能与原功能联合起来。
钩子程序主要用于截取外部的输入量,来完成内部接口的控制。
举个例子来说:QQ登录
钩子绑定的是键盘的输入,键盘输入QQ密码,如果输入错误,则钩子绑定的接口程序,执行显示错误的信息,如果键盘输入正确,则执行正常登录的程序。
那么钩子程序在c++中如何实现的呢?
首先得说明下c++程序中的重载和覆盖
重载指的是不同的函数有相同的函数名,一般指的非虚函数,一般是静态绑定
覆盖指的后面的函数覆盖前面的函数,一般是虚函数,一般是动态绑定
先来说覆盖,覆盖很简单,虚函数的动态绑定的性质,虚函数的根据指向的对象绑定对应的函数实体
例如,初始化子类,则指向之类的覆盖的虚函数实体。初始化父类,则绑定父类的虚拟化。
再来说重载,重载是静态绑定的性质,重载绑定的函数实体对应的声明的静态类型。同时需要考虑的是,继承架构中,寻找重载函数是从子类到父类顶端的过程。
举个例子:
如果初始化定义的子类的声明,呢么静态绑定子类的函数实体,如果子类的函数实体没有找到,则向上寻找,绑定上一级父类的实体。
钩子程序在c++中是由虚函数来完成的。虚函数进行动态绑定,如果子类有覆盖,则钩子对应到子类上,否则默认对应父类中。
2.再探私有/公有静态成员变量与私有静态成员方法
问题1: 为什么在类内的静态成员定义后,要到类的外部在定义和初始化?
答:首先这句话就是错的,在类内的静态成员变量只是一定声明,并没有分配相应的内存空间;在类外,相当于定义加上初始化,如果只是定义,也是能够编译成功的,因为分配了内存。等价于全局变量,但只属于类。
为什么,因为静态成员不属于类的任何一个对象,所以它不是在创建类对象的时候被定义的,不是由类的构造函数初始化的。
问题2:为什么类的静态成员在类外部的定义只能一次?
答:好比全局变量的多重定义
另外,任何变量都只进行一次初始化。局部变量在程序块结束时生存期就结束了,下次再调用这个程序块时从原理上说声明的是另一个变量了(分配到的地址也不一定一样)。
PS:在不同编译器的不同编译情况时,实际的内存分区可能不同。例如TC的Small模式下堆和栈区是重合的,而Tiny模式下连静态区域和动态区域都是重合的。
问题3:私有的静态成员变量如何初始化,访问权限还是私有么?
答:显然,也是在类的外部进行初始化,但是访问权限是存在的。比如不能在程序执行的过程中直接用类名加作用域来访问此私有的静态成员。需要通过类公有的外部接口来访问,包括静态与非静态接口。
问题4:在程序执行过程中,其他类的对象改变静态成员变量的值,那么相应的在建立另一个对象时的静态成员值会是初始化的值还是上一次改变的值?
答:是上一次改变的值,因为初始化是分配内存,赋初值。
问题5:类的非静态成员函数中能访问静态成员变量么?
答:是可以访问的,前提是要对静态的进行外部的初始化。如果为初始化,则会报错,是链接错误,显然在类内部只是声明,并没有在类的外部进行定义与初始化,未分配内存。
静态成员不属于这个类对象,但是可以被类对象共享,属性也保持着,可以被静态成员函数或者非静态成员函数访问。
问题6:类的静态成员函数中能访问非静态成员变量么?
答:不能,解析不了。非静态成员,不是一个特定的对象的,既没有this指针,指向,根本找不到。
问题7:
静态私有成员在类外不能被访问,可通过类的静态成员函数来访问;
当类的构造函数是私有的时,不像普通类那样实例化自己,只能通过静态成员函数来调用构造函数。
对象之间通过类的静态成员变量来实现数据的共享的。静态成员变量占有自己独立的空间不为某个对象所私有。
3. C++中名字的查找和继承
我们常规的调用一个类函数:p->mem(),依次会进行以下4个步骤:
1.首先确定p的静态类型,因为调用都 是一个成员,所以该类型必须是一个类的类型。
2.在p的静态类型对应的类中去找,如果找不到,则依次在直接基类中不断查找直到继承链的顶端,如果找遍了及其基类还找不到,则编译器就报错。如果找到了,那么查找过程就结束,进入类型检查。
3.一旦找到了mem,这个时候进行常规类型检查,确认当前找到的mem是否合法,
4.如果合法,则编译器个根据调用的是否是虚函数而产生不同的代码:
——如果是虚函数,则产生代码为动态绑定
——如果不是虚函数,则产生一个常规的函数调用
注意:隐藏的时候,名字查找优先于类型查找,也就是说子类的同名函数会隐藏父类的同名函数,及时两个同名函数形参列表不一致。
虚函数——动态绑定,根据调用虚函数的指针和引用的类型来判断
非虚函数——静态绑定,根据对象或者指针引用的类型来判断
4.在构造函数和析构函数中是不能调用虚函数的
1.虚函数的作用是什么?是实现部分或默认的功能,而且该功能可以被子类所修改。如果父类的构造函数设置成虚函数,那么子类的构造函数会直接覆盖掉父类的构造函数。而父类的构造函数就失去了一些初始化的功能。这与子类的构造需要先完成父类的构造的流程相违背了。而这个后果会相当严重。
2.虚函数的调用是需要通过“虚函数表”来进行的,而虚函数表也需要在对象实例化之后才能够进行调用。在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则。
3.虚函数的调用是由父类指针进行完成的,而对象的构造则是由编译器完成的,由于在创建一个对象的过程中,涉及到资源的创建,类型的确定,而这些是无法在运行过程中确定的,需要在编译的过程中就确定下来。而多态是在运行过程中体现出来的,所以是不能够通过虚函数来创建构造函数的,与实例化的次序不同也有关系。
那么虚够函数为什么可以设计成虚函数呢?由于虚函数是释放对象的时候才执行的,所以一开始也就无法确定析够函数的。而去由于析构的过程中,是先析构子类对象,后析构父类对象。所以,需要通过虚函数来指引子类对象。所以,如果不设置成虚函数的话,析构函数是无法执行子类的析构函数的。
5.智能指针
1.make_shared函数
#include <memory>
make_shared是一个非成员函数,具有给共享对象分配内存,并且只分配一次内存的优点,和显式通过构造函数初始化(new)的shared_ptr相比较,后者需要至少两次分配内存。这些额外的开销有可能会导致内存溢出的问题。
最安全的使用动态内存的方法是使用一个make_shared的函数。
此函数在动态内存中分配一个对象并初始化,返回指向此对象的shared_ptr。
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论我们拷贝一个share_ptr,计数器都会递增。
当我们给一个shared_ptr赋值或者shared被销毁,计数器就会递减。
当用一个shared_ptr初始化另外一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值(赋值给其他的),计数器都会递增,一旦一个share_ptr的计数器变为0,它就会释放自己所管理的对象。
!注意标准库是用计数器还是其他数据结构来记录有多少个指针共享对象由标准库来决定,关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。
2.shared_ptr 自动销毁所管理的对象
当指向对象的最后一个shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。它是通过特殊的成员函数析构函数来控制对象销毁时做什么操作。
shared_ptr 的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr 的函数就会销毁对象,并释放它占用的资源。
对于一块内存,shared_ptr 类保证只要有任何shared_ptr 对象引用它,它就不会被释放
如果我们忘记了销毁程序不再需要的shared_ptr,程序仍然会正确运行,但会浪费内存
注意!:如果你将shared_ptr存放于一个容器中,而后不在需要全部元素,而只使用其中的一部分,要记得调用erase删除不再需要的那些元素。
注意!:将一个shared_ptr 赋予另一个shared_ptr 会递增赋值号右侧的shared_ptr 的引用计数,而递减左侧shared_ptr 的引用计数,如果一个shared_ptr 引用技术
变为0时,它所指向的对象会被自动销毁。
6. C++隐式转换
C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。
C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。
1 是个构造;2 是个默认且隐含的类型转换操作符。
所以, 有时候在我们写下如 AAA = XXX, 这样的代码, 且恰好XXX的类型正好是AAA单参数构造器的参数类型, 这时候编译器就自动调用这个构造器, 创建一个AAA的对象。
这样看起来好象很酷, 很方便。 但在某些情况下, 却违背了程序员的本意。 这时候就要在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用。
解析:explicit构造函数是用来防止隐式转换的。请看下面的代码:
1. #include <iostream>
2. using namespace std;
3. class Test1
4. {
5. public :
6. Test1(int num):n(num){}
7. private:
8. int n;
9. };
10. class Test2
11. {
12. public :
13. explicit Test2(int num):n(num){}
14. private:
15. int n;
16. };
17.
18. int main()
19. {
20. Test1 t1 = 12;
21. Test2 t2(13);
22. Test2 t3 = 14;
23.
24. return 0;
25. }
编译时,会指出 t3那一行error:无法从“int”转换为“Test2”。而t1却编译通过。注释掉t3那行,调试时,t1已被赋值成功。
注意:当类的声明和定义分别在两个文件中时,explicit只能写在在声明中,不能写在定义中。