T:2019/11/29 W:五 10:34:36 [HTML]: @TOC
虚函数在基类和子类中,遭遇强转(dynamic_cast)后的调用分析
- Time: 2019/10/12
- 基类强转子类:
- 将基类指针经过强制转换成子类指针后,由于eat()函数是虚函数,fish类的指针fh1调用的函数eat()实际上是基类的函数eat()。
- 分析,虽然fish类的指针fh1调用了函数eat(),但是,由于fish类的指针fh1是由基类指针强制转换后得到的,所以在运行时,依据对象创建时的实际类型来调用相应的函数。(迟绑定技术)
- 子类强转基类:
- 将子类指针转换成基类指针后,由于eat()函数是虚函数,animal类的指针pAn调用的函数eat()实际上是子类的函数eat()。
-
分析,虽然animal类的指针pAn调用了函数eat(),但是在运行时,依据对象创建时的实际类型来调用相应的函数。(迟绑定技术)
在堆/栈上建立对象
- Time: 2019/10/12
- 在C++中,类的对象建立分为两种,
- 一种是静态建立,如A a;
- 另一种是动态建立,如A* ptr=new A。
- 静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
- 动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
只在堆上建立
- 只在堆上建立
- 类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。
- 容易想到将构造函数设为私有。
- 在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。
- 然而,前面已经说过,new运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载operator new()函数,而operator()函数用于分配内存,无法提供构造功能。因此,这种方法不可以。
- 答案是析构函数设为私有
- 当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。
- 当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。
- 编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?
- 比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。
- 所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,
- 其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。
- 如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。
- 因此,将析构函数设为私有,类对象就无法建立在栈上了。
- 代码如下:
class A { public : A(){} void destory(){ delete this ;} private : ~A(){} };
- 分析
- 试着使用A a;来建立对象,编译报错,提示析构函数无法访问。
- 这样就只能使用new操作符来建立对象,构造函数是公有的,可以直接调用。
- 类中必须提供一个destory函数,来进行内存空间的释放。
- 类对象使用完成后,必须调用destory函数。
- 上述方法的缺点:
- 无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。
- 类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。
- 解决方案:
- 问题1:将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。
- 问题2: 可以将构造函数设为protected,然后提供一个public的static函数来完成构造(其实就是上一节讲的假构造函数),这样不使用new,而是使用一个函数来构造,使用一个函数来析构。
- 代码如下:
class A { protected : A(){} ~A(){} public : static A* create() { return new A(); } void destory() { delete this ; } };
- 这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。
只在栈上建立
- 只在栈上建立
- 只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。
- 代码如下:
class A { private : void * operator new ( size_t t){} // 注意函数的第一个参数和返回值都是固定的 void operator delete ( void * ptr){} // 重载了new就需要重载delete public : A(){} ~A(){} };
二重调度问题
- Time: 2019/10/12
- 参考: 让函数根据一个以上的对象来决定怎么虚拟
- 参考: 让函数根据一个以上的对象类型来决定如何虚化
- 问题描述:
- 正在编写一个小游戏,游戏的背景是发生在太空,有宇宙飞船、太空船和小行星,它们可能会互相碰撞,而且其碰撞的规则不同,如何用C++代码处理物体间的碰撞。
- 代码的框架如下:
class GameObject{...}; class SpaceShip:public GameObject{...}; class SpaceStation:public GameObject{...}; class Asteroid:public GameObject{...}; void checkForCollision(GameObject& obj1,GameObject& obj2) { if(theyJustCollided(obj1,obj2)) { processCollision(obj1,obj2); } else { ... } }
- 当调用processCollision()时,obj1和obj2的碰撞结果取决于obj1和obj2的真实类型,但我们只知道它们是GameObject对象 (碰撞结果取决于发生碰撞的类型)。相当于我们需要一种作用在多个对象上的虚函数。这类型问题,在C++中被称为二重调度问题.
解决方案: 虚函数+RTTI
- 基本原理:
- 将processCollision()定义为虚函数,解决一重调度,
-
然后利用RTTI来检测对象的类型(需要对象存在虚函数),再利用if…else语句来调用不同的处理方法。
-
注意processCollision 和 collide 都是基类中定义的虚函数,所以第一重和第二重调用都是子类中的方法, 参考虚函数在基类和子类中,遭遇强转(dynamic_cast)后的调用分析
- 具体实现如下:
- 该方法的实现简单,容易理解,其缺点是其扩展性不好。如果增加一个新的类时,我们必须更新每一个基于RTTI的if…else链以处理这个新的类型。 ```cpp class GameObject{ public: virtual void collide(GameObject& otherObject) = 0; … }; class SpaceShip:public GameObject{ public: virtual void collide(GameObject& otherObject); … };
class CollisionWithUnknownObject{ public: CollisionWithUnknownObject(GameObject& whatWehit); … }; void SpaceShip::collide(GameObject& otherObject) { const type_info& objectType = typeid(otherObject); if(objectType == typeid(SpaceShip)) { SpaceShip& ss = static_cast<SpaceShip&>(otherObject); process a SpaceShip-SpaceShip collision; } else if(objectType == typeid(SpaceStation)) { SpaceStation& ss = static_cast<SpaceStation&>(otherObject); process a SpaceShip-SpaceStation collision; } else if(objectType == typeid(Asteroid)) { Asteroid& a = static_cast<Asteriod&>(otherObject); process a SpaceShip-Asteroid collision; } else { throw CollisionWithUnknownObject(otherObject); } } ```
解决方案:只使用虚函数
- 基本原理:
- 用两个单一调度实现二重调度,也就是有两个单单独的虚函数调用
- 第一次决定第一个对象的动态类型,
- 第二次决定第二个对象动态类型。
- 用两个单一调度实现二重调度,也就是有两个单单独的虚函数调用
- 其具体实现如下:
- 注意调用过程中
otherObject.collide(*this);
*this返回SpaceShip; otherObject.collide 调用子类中实现的方法
- 参考: 虚函数在基类和子类中,遭遇强转(dynamic_cast)后的调用分析
- 与前面RTTI方法一样,该方法的缺点扩展性不好。每个类都必须知道它的同胞类,当增加新类时,所有的代码都必须更新。 ```cpp class SpaceShip; class SpaceStation; class Asteroid; class GameObject{ public: virtual void collide(GameObject& otherObject) = 0; virtual void collide(SpaceShip& otherObject) = 0; virtual void collide(SpaceStation& otherObject) = 0; virtual void collide(Asteroid& otherObject) = 0; … }; class SpaceShip:public GameObject{ public: virtual void collide(GameObject& otherObject); virtual void collide(SpaceShip& otherObject); virtual void collide(SpaceStation& otherObject); virtual void collide(Asteroid& otherObject); … };
void SpaceShip::collide(GameObject& otherObject) { otherObject.collide(this); } void SpaceShip::collide(SpaceShip& otherObject) { process a SpaceShip-SpaceShip collision; } void SpaceShip::collide(SpaceStation& otherObject) { process a SpaceShip-SpaceStation collision; } void SpaceShip::collide(Asteroid& otherObject) { process a SpaceShip-Asteroid collision; } // 调用过程 void SpaceShip::collide(GameObject& otherObject) { // *this返回SpaceShip; otherObject.collide 调用子类中实现的方法 otherObject.collide(this); } ```
解决方案:模拟虚函数表
- 注意调用过程中
- 编译器通常创建一个函数指针数组(vtbl)来实现虚函数,并在虚函数被调用时在这个数组中进行下标索引。
- 基本原理
- 借鉴编译器虚拟函数表的方法,建立一个对象到碰撞函数指针的映射,然后在这个映射中利用对象进行查询,获取对应的碰撞函数指针,进行函数调用。
- 难点有如下:
- 构建虚拟函数表
- 查表函数lookup
- 具体实现如下:
// 基类 class GameObject { public: virtual void collide(GameObject& otherObject) = 0; ... }; // 子类 即功能函数 class SpaceShip:public GameObject { public: virtual void collide(GameObject& otherObject); virtual void hitSpaceShip(GameObject& spaceShip); virtual void hitSpaceStation(GameObject& spaceShip); virtual void hitAsteroid(GameObject& spaceShip); private: static HitMap* initializeCollisionMap(); // 虚拟表 }; // 功能函数 void SpaceShip::hitSpaceShip(GameObject& spaceShip){ SpaceShip& otherShip = dynamic_cast<SpaceShip&> (spaceShip); // dynamic_cast process a SpaceShip-SpaceShip collision; } void SpaceShip::hitSpaceStation(GameObject& spaceShip){ SpaceStation& otherShip = dynamic_cast<SpaceStation&> (spaceShip);// dynamic_cast process a SpaceShip-SpaceStation collision; } void SpaceShip::hitAsteroid(GameObject& spaceShip){ Asteroid& otherShip = dynamic_cast<Asteroid&> (spaceShip);// dynamic_cast process a SpaceShip-Asteroid collision; } // 二级调用 void SpaceShip::collide(GameObject& otherObject){ HitFunctionPtr hfp = lookup(otherObject); if(hfp) (this->*hfp)(otherObject); else throw CollisionWithUnknowObject(otherObject); } // lookup实现 SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit){ HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name()); if(mapEntry == collisionMap.end()) return 0; return (*mapEntry).second; } // mapinit实现 SpaceShip::HitMap* SpaceShip::initializeCollisionMap(){ HitMap* phm = new HitMap; (*phm)["SpaceShip"] = &hitSpaceShip; (*phm)["SpaceStation"] = &hitSpaceStation; (*phm)["Asteroid"] = &hitAsteroid; return phm; }
解决方案:使用“非成员函数”的碰撞处理函数(略)
解决方案:将自行仿真的虚函数表格初始化(略)
VC内存对齐准则(Memory alignment)
- Time: 2019/10/13
- 参考: VC内存对齐准则(Memory alignment)
- 本文所有内容在建立在一个前提下:使用VC编译器。
- 着重点在于:
- VC的内存对齐准则;
- 同样的数据,不同的排列有不同的大小;
- 在有虚函数或虚拟继承情况下又有如何影响?
- 内存对齐?!What?Why?
- 对于一台32位的机器来说如何才能发挥它的最佳存取效率呢?当然是每次都读4字节(32bit),这样才可以让它的bus处于最高效率。实际上它也是这么做的,即使你只需要一个字节,它也是读一个机器字长(这儿是32bit)。更重要的是,有的机器在存取或存储数据的时候它要求数据必须是对齐的,何谓对齐?它要求数据的地址从4的倍数开始,如若不然,它就报错。还有的机器它虽然不报错,但对于一个类似int变量,假如它横跨一个边界的两端,那么它将要进行两次读取才能获得这个int值。比方它存储在地址为2-5的四个字节中,那么要读取这个int,将要进行两次读取,第一次读取0-3四个字节,第二次读取4~7四个字节。但是如果我们把这个整形的起始地址调整到0,4,8…呢?一次存取就够了!这种调整就是内存对齐了。我们也可以依次类推到16位或64位的机器上。
结论:
- 假设规定对齐量为4,
- char(1byte)变量应该存储在偏移量为1的倍数的地方,
- int(4byte)则是从偏移量为4的倍数的地方,
- double(8 byte)也同样应存储在偏移量为4的倍数的地方
- 结构体整体的大小也应该对齐,对齐依照规定对齐量与最大数据成员两者中较小的进行。
-
Vptr影响对齐而VbcPoint(Virtual base class pointer)不影响。
- 边界该如何调整
- 对于32位的机器来说,它当然最渴望它的数据的大小都是4 Byte或者4的倍数Byte,这样它就能最有效率的存取数据,当然如果数据小于4Byte,那也是没问题的。那么编译器要做的便是尽量满足这个要求。
- VC中的一些实验,并总结如下三条准则,这并非来自微软的官方文档,但哪位老哥以为这些准则或许不全但应该是正确的:
- 变量存放的起始位置应为变量的大小与规定对齐量中较小者的倍数。
- 例如,假设规定对齐量为4, 那么char(1byte)变量应该存储在偏移量为1的倍数的地方, 而整形变量(4byte)则是从偏移量为4的倍数的地方, 而double(8 byte)也同样应存储在偏移量为4的倍数的地方,为什么不是8?因为规定对齐量默认值为4,而4 < 8。在VC中默认对齐量为8,而非4。
- 结构体整体的大小也应该对齐,对齐依照规定对齐量与最大数据成员两者中较小的进行。
- Vptr影响对齐而VbcPoint(Virtual base class pointer)不影响。
- 变量存放的起始位置应为变量的大小与规定对齐量中较小者的倍数。
- 一个实例T
class T { char c; int i; double d; }
- 将其sizeof输出后的大小为16,其内存布局如图T.变量c从偏移量为0开始存储,而整形i第一个符号条件的偏移量为4,double型d的第一个符号条件的为8。整个对象的大小为16,不需要再进行额外的对齐。
- T的内存布局:
- 又一实例L(同样的数据,不同的大小)
class L { char c; double d; int i; }
- 它sizeof后的结果或许会令你大吃一惊,或许不会(如果你有认真读前面的两条准则)。L sizeof后的结果是24!同样是一个int,一个char,一个double却整整多出了8个字节。这期间发生了什么?我们依据前面两条规则来看看。C存储于0的位置,1-7都不能整除8,所以d存储在8-15,16给i正好合适,i存储在16~19。总共花费了20个字节,抱歉不是8的倍数,还得补齐4个。现在你可以看看图L的关于类L的内存布局,再比较一下类L和类T的内存布局。
- 类L的内存布局
- 我得出了这样一条并不权威的结论,因为我还没听有人这样说过:在声明数据成员的时候,按照从小到大排列变量[^1],切忌不要将大小差距很大的类型交替声明。 [^1]: 此处有一点问题,这个问题由独酌逸醉提出,他认为将最小的数据放在最前面可能会更好,我们有进行过讨论,但可惜的是由于在2011/11/24日数据库丢失,我只能用备份还原,所以丢失了一些数据,无疑,本文的评论也在其中。不过我对这个问题映像深刻,因为我在写这篇博客的时候便困惑于到底成员是应该放在之前还是之后,因为这两种情况我都找不到强有力的理由来支撑它们。后来使我确信从大到小排列好于从小到大排列的理由在于,从大到小排列一般无需成员之间的对齐,唯一的对齐工作是最后进行的整个结构体对齐的工作。毫无疑问的是,这应该是最节省内存的方式。再之后,独酌提出从小到大可能好些,虽然没有给出有说服力的理由,但却使我无比困惑,我当时虽然认为从大到小的排列更有优势,但却实在想不出一个实例能使得它优于从小到大排列的。不过最终我击垮了自己的理由,在继承状况下从大到小排列很容易被打破,比方,基类的成员为一个char,继承类的成员为double,int,char虽然基类和继承类都是按从大到小的顺序排列的,但是继承类的内存布局最终会使char,double,int,char,此时既不能避免成员对齐,又导致后面的结构体对齐。暂时获得的最终结果是从小到大排列是更好的一种排列方式。
- Vptr影响对齐而VbcPoint(Virtual base class pointer)不影响
- 前面的实例只涉及前两条准则,现在我们来看看第三条的两个实例:
class X{char a;}; class Y: virtual public X{};
- Y的大小为:a占一个字节,VbcPoint(我称他为虚基类指针)占四个字节。我们不论a与VbcPoint的位置如何摆放,如果将VbcPoint等同于一个成员数据来看的话,sizeof(Y)都应该为8. 实际上它是5! 就我目前的水平,我只能先将其解释为VbcPoint不参与对齐。
- 前面的实例只涉及前两条准则,现在我们来看看第三条的两个实例:
- 对于Vptr这个问题则不存在(sizeof(X)的大小确实为8.):
class X{ char a; virtual int vfc(){};}
面向对象的关系:继承,关联,聚合,组合,依赖 T:2019/10/24 W:4 19:32:27
- 继承体现的是类与类之间的纵向关系
- 其他四种体现的是类与类之间的横向关系
- 耦合强弱:
依赖
<关联
<聚合
<组合
- 从语义上来看
- 继承
(A is B)
- 关联、聚合、组合
(A has B)
- 依赖
(A use B)
- 继承
- 当组合与依赖结合时,可以替代继承 组合+依赖(基于对象) vs 继承(面向对象)
- 聚合和关联关系一样,都是通过实例变量实现的。但是关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,另一个代表部分
继承
- 继承(泛化)
- 关系:
a
继承自b
,a is b
- 如图,
Benz
,Audi
,Lamborghini
都继承自Car
,都有轮子wheel
,都可以前进,与停止。关联
- 关系:
- 关联
- 关系:
a has b
- 整体部分不负责局部对象的销毁,二者的生命周期没有关联
聚合
- 关系:
- 聚合
- 关系:
a has b
- 整体部分不负责局部对象的销毁,二者的生命周期没有关联
组合
- 关系:
- 组合
- 关系:
a has b
- 整体部分负责局部对象的销毁
依赖
- 关系:
- 依赖
- 关系:
a use b
多线程下变量的客观性
- 栈上对象,不可能被别的线程看到,所以读取始终是线程安全的。
- 因为每一个线程都有自己的栈
- 堆上对象,可以被别的线程看见,所以要互斥访问
如果一个表达式既包含有符号数也包含无符号数,那么会被隐式转换成无符号数进行比较
- 【读薄 CSAPP】壹 数据表示
- 如果一个表达式既包含有符号数也包含无符号数,那么会被隐式转换成无符号数进行比较
-1 > 0U // 无符号数
这是正确的-1 < 0 // 有符号数
这是正确的强符号和弱符号问题
- C++】强符号和弱符号 - 王磊的博客 - CSDN博客
- 强符号: 函数和初始化的全局变量
- 弱符号: 未初始化的全局变量
1) 不允许强符号被多次定义,也即不同的目标文件中不能有同名的强符号;如果有多个强符号,那么链接器会报符号重复定义错误。
2) 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
3) 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
共享库和静态库的区别
- 静态库,共享库和动态链接库的区别与联系
- 静态库优势: 1) 静态库相当于复制一份库文件到项目代码中,所以没有像动态库那样需要有动态加载,识别依赖函数地址的开销,速度快。 2) 同样的,静态连接库文件比动态链接库文件需要更少的内存去搜寻函数在动加载或共享库中的地址。 3) 不会生成复杂的依赖关系
-
静态库缺点: 1) 增加应用程序可执行文件的大小,因为它不能仅仅提取仅仅依赖的库函数到应用程序中。 2) 库文件的更新不会反映到应用程序中,除非应用程序重新编译新的静态库。
- 共享库的优点: 1) 相对于静态库,共享库能够在任何时候更新(修复bug,增加新的功能),并且能够被反映到应用程序中。 2) 显著减少应用程序可执行文件占用的硬盘空间。
- 共享库缺点: 1) 使应用程序在不同平台上移植变得更复杂,因为它需要为每每个不同的平台提供相应平台的共享库。