深度探索C++对象模型

LIST特性

  • List 是个双向链表
  • 双向循环
  • G2.9中STL实现的LIST,其NODE的next 和 prev是 void型指针
  • 所需要内存是一个数据 + 两个指针

LIST的iterator

  • 后置++的实现一定是先实现了前置++
  • = * 会先调用=的重载
  • ++ *this 同样会调用 前置++重载函数
  • 前置++返回引用,后置++返回普通
    这是因为看齐整数,++++i 可以,i++++禁用。

Vector 扩充

  • 扩充只能在其他地方找空间,不能在连续的地方上直接扩充。
  • Vector 三个指针,start,finish,end_of_storage
  • 扩充是两倍扩充,扩充时,代价十分大。

Array不可扩充

  • 没有构造器,和析构

Deque

  • stack和queue都是通过Deque实现,反向封装Deque。默认使用Deque
  • stack和queue也可以选择List封装
  • stack可以用vector做封装,queue不能用vector做封装
  • stack和queue不提供iterator

Set和MultiSet

  • set不允许key重复,MultiSet允许重复key
  • 底层数据结构是红黑树

Map和MultiMap

  • 底层数据结构是红黑树

HashTable

  • 底层的buckts是用vector实现的
  • hash函数没有string类型的

迭代器

  • 迭代器分类是对象,其继承关系如下
  • 在这里插入图片描述
  • 这样的继承关系被用于减少代码,不适用enum的巧妙用法
  • 在这里插入图片描述

C++的内存布局

  • 封装并不会使得c++比起c来增加任何成本。
  • 主要额外负担是由virtual function造成。
  • 在c语言中
    • 为什么在结构体的最后加一个c[0]数组?
struct fuck{
	int a;
	int b;
	char c[0];
}

-这样每个结构体都可以拥有一个可变数组 by using code like this

struct p = (struct fuck*)malloc(sizeof(struct fuck) + sizelen);

这样p就有了长度为sizelen的可变数组大小,然后就可以用以下方式使用这个可变数组


p->c[i]//i depends on the sizelen
  • 但这样的trick不能在c++中使用,因为c++的内存模型和c不一样
  • 若要使用这样的技巧,可以用下面的方式
struct c_point{
	char c[0];
};
class Point : public c_point{
};
  • more advance method of doing this
struct c_point{
	char p[0];
};
class point{
public:
	operator c_point(){return _c_point;}
private:
	c_point _c_point;
}

OO与ADT的差异

  • OO(object-oriented)所要处理的无限可能的对象(多态),而ADT所要处理的是某个具体对象。
  • 换句话说,OO的哲理是,类型虽然有界定(animal),但其背后却有无穷可能(dog,cat,mouse)
  • 由此,想要熟练使用OO,那么就需要pointer和reference。而ADT则只需要具体的对象。(pointer是class的pointer,int * void *不会触发)
  • 为什么int *,void 无法触发多态?这是因为一个指向地址的int,其涵盖的地址空间是1000-1003。同理,我想你看到这里应该就能反映过来why,现在的我懒得写了。
  • 至于说父类对象指针,和子类对象指针的区别,那也是涵盖地址的差异

operator int*()与operator int()

  • 两个都是类型转换运算符
  • 即是将某个class的object转换为 int*,int类型。
  • 用处:
    • 撰写nullptr实现时,nullptr是一个class的object
    • 通过如下代码所示,可将一个nullptr对象,转换为T类型的空指针
    • int *p = mynullptr,隐式触发 operator T * ()函数
    • (int *) mynullptr, 显示触发 operator T * ()函数
    • operator T * ()函数将返回0,也就是将nullptr转为0
const class mynullptr_t
{
public:
    template<typename T>
    operator T*() const
    {
        cout << "T* is called" << endl;
        return 0;
    }
} mynullptr = {};
  • if(cin) ,希望将标准输入流cin对象转换为int型,用于判断。因此可以实现operator int()函数。
  • Therefore,可以使用cin << intval。<< 将被解释为左移运算符

explicit与构造函数

  • explicit用处,避免单一参数的constructor被用用作是一个conversion运算符。
  • 什么是conversion运算符?参见上文。
  • Say for instance
 CxString(int size)  
    {  
        _size = size;                
        _pstr = malloc(size + 1);      
        memset(_pstr, 0, size + 1);  
    } 
  
 In main function:
 CxString string2 = 10;
  • 因为没有加上explicit,所以这句话实际上被翻译成,CxString string2(10)
  • Therefore, explicit的关键点字是:单一参数构造函数,避免conversion运算。

编译器与构造函数(41-43)

  • 需要注意的是,所有代码讨论的都是内含,而不是继承关系。
  • 编译器需要和程序员需要
  • 如果一个object A内含了n个object,那么编译器会为A的构造器中,插入并调用其余n个object的构造函数。
  • 上述的是编译器行为,由编译器负责
  • 至于说,object A中数据成员的初始化,那就是程序员需要负责的。
  • 编译器的default constructor,不会初始化数据成员
  • 若一个class内含了另外一个object,则需要这个object提供一个默认构造函数。如果没有,那么则无法生成default constructor。
  • 如果class内,内含了一个复杂对象,那么就需要这个复杂对象提供其自身的构造函数,否则这个class无法生成default constructor
  • 默认构造函数,不会初始化数据对象

Copy constructor被唤起的时机

  1. X xx = x;//其中x为左值
  2. foo(X x)
  3. X foo_bar(){}
  • 这就是为什么大部分函数的返回都是引用而不是值,这样能避免调用 Copy constructor

  • 如果没有自己编写Copy constructor那么将会默认使用bitwise copy semantics(浅拷贝)

  • 默认构造函数生成法则:如果一个class内含了一个object,如果这个object显示的申明了自己的构造函数,拷贝构造函数,那么就会生成默认的constructor和Copy constructor。

  • 如果不写,c++并不一定就会生成默认的constructor和Copy constructor。

  • 特殊地,对于默认的Copy constructor。如果一个class声明了一个或多个virtual function时,会自动生成默认Copy constructor。这个Copy constructor会显式地设定object vptr的virtual table,而不是直接对vptr赋值。

  • 这样做是为了保证父类的vptr不改变。

  • say for instance

//franny 是父类,yogi是子类
zooAnimal franny = yogi;
draw(yogi);
draw(franny);

如果vptr直接被bitwise copy,那么draw(franny)将会调用yogi的draw,事实上我们所期望的行为是调用franny自身的zooAnimal::draw,而不是Bear::draw

  • 如下代码片段所示,传参数调用函数时。
//foo(X x0)
X xx;
foo(xx);

使用编译器可能生成的代码

X _temp0;
_temp0.X::(xx)//调用copy constructor
foo(_temp0)

问题出现在foo的声明上,虽然_temp0使用copy constructor正确的构造了自己,但是在将_temp0传入foo时,却会使用bitwise拷贝至x0中。也就说,上面所有提到的bitwise的缺点都会存在。

  • 像是这个class X存在虚函数,那么x0的vptr会直接指向_temp0。

  • Therefore,必须需要 修改foo的声明foo(X &x0)。而class X的deconstructor会销毁这个x0.

  • NRV优化提升效率,即是编译器将函数的参数统一命名,如下代码所示。but,这种优化现在却反倒会引起效率的下降。

//未开启NRV
void bar(X &_result){
	X xx;
	xx.X::X();
	//对xx操做
	_result.X::XX(xx);
	return}
//开启NRV
void bar(X &_result){
	_result.X::XX();
	//对_result操做,而不是xx。

	return}
  • 一个class如果不包含任何object,其所有数据成员都是基础数据类型,那么不显示提供copy constructor是正确的。调用bitwise拷贝既高速又安全。

Member initialization list

  • 思考以下代码
class word{
	String _name;
	int _cnt
public:
	word(){
		_name = 0;
		_cnt = 0;
	}
};//效率十分低下 why?

因为该class拥有一个string object对象,因此编译器的所生成的代码可能如下

class word{
	String _name;
	int _cnt
public:
	word(){
		//_name = 0;
		_name.String::String();
		String temp = String(0);
		_name.String::operator=(temp);
		temp.String::~String();
		
		_cnt = 0;
	}
};
  • 为什么会生成这样的代码?这是因为_name = 0,这句代码,会触发赋值函数。因为_name已经提前被定义,所以会提前调用构造,已经构造的对象,再使用=就会调用赋值函数。
  • 使用Member initialization list可以减少代码
class word{
	String _name;
	int _cnt
public:
	word():_name{0}{
		_cnt = 0;
	}
};//效率十分低下 why?

编译器所产生的代码

class word{
	String _name;
	int _cnt
public:
	word():_name{0}{
		_name.String::String(0);
		_cnt = 0;
	}
};//效率更高
  • Member initialization list中初始化成员的顺序,是按照在class中声明的顺序执行的。list中的顺序,就是个摆设
  • a dangerous example
class X{
	int i;
	int j;
public:
	X(int val)
		: j{val},i{j}
	{}
}//至于说为什么危险,现在的我就不写了。留给未来正在读的你思考2020-08-07 14:59:00
//我思考nmd,能不写清楚点,现在的我记得住个屁,以后写博客能不能不要这么装。2021-2-24 23:05

member

  • static member是不存在于object中的,而是存在于class中。虽然语法上表现出来,似乎就是在origin上一样,但是其实际上会被转述为对class中的chunksize的调用。如下述代码所示
//chunksize is a static member 
Point3d origin;
origin.chunksize; -> Point3d ::chunksize;
origin.chunksize; -> Point3d ::chunksize;
  • member存取效率的差异,参考以下代码
Point3d origin,*pt=&origin;

origin.x=0.0;
pt->x=0.0;

如果x是一个struct member,class member,单一继承,多重继承的情况下都相同。
问题就出在如果x是一个virtual base class的member。那么因为不能确定pt是具体指向哪一种class type,因此需要在运行时才能够知道。所以pt->x的存取效率会远低于origin.x。其offset在编译时就已经确定。

  • 当class A继承class B时,需要将class的alignment padding一起继承。换句话说,计算一个class的大小时,是其父类元素+padding+加自身在这里插入图片描述

  • 至于说为什么要这样设计语言?那是为了保护subobject在复制发生时,不被破坏。具体参看下图

  • 在这里插入图片描述

Virtual function

  • 当一个class被添加上Virtual 后,其所需要增加的负担如下
    • 为class导入virtual table,记录虚函数
    • 为class导入vptr指向virtual table
    • 增强constructor,使得其能够为vptr赋值
    • 增强deconstructor,使其能够消除virtual table 和 vptr

Function 语义

  • member function 和 nonmember function 不应该有执行效率上的差异,这是c++的设计准则之一。所以所有的member function都会被转换为nonmember function。//这应该是go语言的面向对象之所以要用 func (p *Class)memberfunction这样的方式去实现一个class的方法的原因把 2021/2/24 23:09
  • 这就是为什么,所有对象对member function的调用,都需要传入this指针的原因
ptr -> normalize();//normalize 为虚函数
  • 上述对虚函数调用的代码,将会被转换为如下代码
(* ptr->vptr[1]) (ptr)
  • 即使从vptr[1]中取出一个函数,最后并将ptr作为this指针传入,member function。
  • 一个class只会存在一个virtual table,所有的object使用vptr共享。(单继承下)
  • 重写虚函数,会覆盖base class的虚函数实列。
  • 继承base class时,如果新增了虚函数,那么将会扩大virtual table的size,新增一个slot,而不是再新增一个virtual table
  • 必须要定义pure virtual destructor。因为每一个derived class的destructor会被编译器扩张,以静态的方式调用其每一个virtual base class,以及上一层base class的destructor。
情况在多重继承之下,发生了变化
  • 一个derived class不只是仅有一个virtual tables,而是含有n-1个额外的virtual tables。n表示其上一层的base classes个数
  • Therefore,derived class中的每一个subobject中都会存在一个vptr、以指向其自身的virtual tables。
Base2 *phase2 = new Derived;

因为Derived中存在多个virtual tables,所以编译器会根据phase的指针类型,相应的修改其offset,使得phase指向Derived中Base2的subobject,即是如下代码

Derived *temp = new Derived;
Base2 *phase2 = temp? temp + sizeof(Base1) : 0;
  • 劝告:不要在virtual base class中声明nonstatic data members。(即最好不要在c++中使用抽象类,全做接口用)
  • inline函数被调用多次,会产生大量扩展码(即为了避免变量冲突,产生临时变量),使代码量增加
  • 劝告:不要再使用c语法那样将所有的变量声明都放在代码起始处,因为c++ constructor和destructor的缘故,会使得代码被编译器加入许多调用。所以c++语法中,要做到随用随生成。

Template代码的常见谬误

template <typename T>
class Fuck{
public:
	Fuck(T t = 1024)
	:_t(t)
	{
		if(tt!=t)
			throw ex ex;	
	}
private:
T tt;
}
  1. !=没有定义,除了typename是 int、float、double这些基础数据类型时!=可以使用之外,其他情况下,!=均无法正常使用。
  2. t=1024,同上除了int、float、double这些基础类型之外,t=1024都不一定能够正常赋值。

栈展开与其危害

  • 什么是栈展开?参考以下代码
#include <iostream>
using namespace std;
void f1() throw (int) {
  cout<<"\n f1() Start ";
  throw 100;
}

void f2() throw (int) {
  cout<<"\n f2() Start ";
  f1();
  cout<<"\n f2() End ";
}

void f3() {
  cout<<"\n f3() Start ";
  try {
    f2();
  }
  catch(int i) {
   cout<<"\n Caught Exception: "<<i;
  }
  cout<<"\n f3() End";
}
 
int main() {
  f3();
  return 0;
}

程序首先从f3()开始,从f3()中调用f2(),然后f2()再调用f1(),最后f1()抛出异常。因为f1()中没有catch异常处理,因此删除f1()栈中所有临时object,向上进入到f2()中。又因为f2()中没有cathc处理异常,因此删除f2()中所有临时object,向上进入到f3()中。在f3()中发现到了catch异常处理,最后使用f3()中的catch异常处理。

f2()中的cout<<"\n f2() End ";并不会被执行,而是直接从f1()的调用中返回,进入到f3();

  • 栈展开的危害是什么?
    在上面的叙述中提到了会删除栈中所有的临时object,对于普通object来说,栈展开的过程会调用它们的destructor但是于指针、数组变量来说,栈展开的过程并不会调用delete [] 。因此如果在栈展开过程中使用了new运算符申请了heap内存,那么就会发生内存泄露。
  • 参考如下代码
#include <iostream>
#include <memory>
using namespace std;
void autoptrtest1(){
    A* pa=new A(1);
    throw 1;
    delete pa;
}
int main(){
    try{
        autoptrtest1();
    }  
    catch(int){
        cout<<"there is no destructor invoked"<<endl;
    }
}

在函数autoptrtest1()中使用了new运算符,申请了heap内存,随后throw出异常。因此在栈展开的过程,只会删除该指针,而不会调用delete删除该指针。所以指向的heap内存会发生内存泄露。

  • 如何解决这个问题?
#include <iostream>
#include <memory>
using namespace std;
void autoptrtest2(){
    auto_ptr<A> pa(new A(2));
    pa->show();
    throw 2;
}
  
int main(){
    try{
        autoptrtest2();
    }
    catch(int){
        cout<<"A's destructor does be invoked"<<endl;
    }
}

不直接使用*指针,而是使用类模板auto_ptr生成一个具体的object。auto_ptr pa(new A(2)); 使用 new A(2)生成一个heap内存对象,将其作为参数传入auto_ptr pa的构造函数中,生成一个pauto_ptr pa对象。

随后在栈展开的过程中,就会有调用其destructor自动析构

downcast与其危害

  • 什么是downcast?
    如果存在以下继承结构
class type{}

class fct:public type{}
class gen:public type{}

那么将type*的一个变量转换至fct或是gen,这就是downcast,如下代码所示。

type* pt;
fct *pf = (fct*) pt
  • 什么是downcast的危害?
    因为type class的基类指针,可以指向任意派生类。那么在转型时,type* pt可能是指向gen类型的object。因为多态的原因,使得我们无法得知type* pt到底背后是个什么东东。因此在转型时,很有可能时一个gen的object被转换为fct object。

  • Therefore,后续所有对pf的使用都是不正确的,这样就会发生意想不到的危害。

  • 如何避免这样的危害?
    使用dynamic_cast动态检查pt指针其背后真正的object,如下代码所示

fct* pf = dynaminc_cast<fct *> pt

上述这段代码,会被编译器翻译为如下代码

((type_info*)(pt->vptr[0]))->_type_descriptor

即是通过vptr,去将virtual table中存储的_type_descriptor取出来,然后再将这个东东,传入到一个runtime library函数中进行比较。

  • 由上述代码可知,要想使用dynamic_cast首先肯定是需要vptr滴,那么vptr是怎么来滴?当然是虚函数咯,如果没有虚函数还想用dynamic_cast?那就是在想peach。

  • dynamic_cast并不能够用于reference之上。因为dynamic_cast返回0,则表示没有指针没有指向任何object。

  • 但是问题出在reference和pointer的区别上。pointer可以用0表示NULL,但是reference用0会创建一个临时对象,这个对象的初值为0。总结就是reference不能像pointer一样用0来表示NULL。

  • Therefore,想要对reference使用dynamic_cast那么就需要进行如下封装。

myconvert(const type &t){
	try{
		fct* pf = dynaminc_cast<fct *> pt
	}catch(bad_cast){
		
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值