深入探索C++对象模型

7 篇文章 0 订阅
类中的函数
  • 类中的函数不占用内存空间
  • 每一个非内联函数只会诞生一个函数实例
  • 而内联函数会在其每一个使用者模块身上产生一个函数实例
指针
  • 指针都是占据的四字节,那么是如何区分指针指向的对象类型呢?
  • 关键在于如何解释地址中的内容及其大小
  • 其中cast其实是编译器指令,并不改变地址和内容,而是改变,被指出的内存的大小和其内容的解释方式

static_cast和dynamic_cast 的区别
  • 首先如下:
class animal
{
	.....
};
class bear : public animal
{
	int cell;
	...
};

bear  a;
animal *p1 = &a;
animal p2;
//此时,即使动态绑定,也是无法通过p1访问cell的,因为动态绑定是虚函数的
//那么如何通过p1访问cell呢?
(static_cast<bear *> (p1) ) -> cell;
(dymanic_cast<bear *> (p1) ) -> cell;
//此时,算是强制转换,将p1变成了bear对象类型;
//这里我们看不出区别,都是正确的,因为p1是指向a的指针,两者指向同一内存,只是解释的方式不同,内存中都有cell变量

//但是,如果这样
(static_cast<bear *> (p2) ) -> cell;
(dymanic_cast<bear *> (p2) ) -> cell;
//p2本身就是个基类,不存在cell变量
//此时静态转换不会报错, 但是我们知道,一旦访问,就会出错
//动态转换会进行检查,此时就会报错,避免发生这样的错误


默认构造函数
  • 如果我们没有写类A的构造函数
  • 当编译器认为需要的时候,就会生成默认构造函数(是编译器需要,而不是程序需要)
  • 如果写了,那么就不会再生成
  • 1 如果类的成员,具有默认构造函数,那么就会为类生成默认构造函数
  • 比如
  • (1)类A内,含有成员变量类B,如果类B有默认构造函数,而我们没有为A写构造函数
  • 那么编译器就会自动生成一个A的默认构造函数
  • (2)如果我们有类A的构造函数,但是没有在构造函数内初始化类B
  • 那么编译器会自动将我们写的构造函数进行扩张
  • 添加B的默认构造函数
  • 不是我们上面说的,如果写了,那么就不会再生成
  • 2如果类派生自一个有默认函数的基类
  • 那么这个继承类的默认函数也会被自动合成出来
  • 而,如果基类有构造函数,那么在编译器看来,其和基类的默认构造函数没有差别
  • 3.带有虚函数的类
  • 4.带有一个虚基类的类

  • 编译器合成出来的默认构造函数,均不会对类中每个成员赋值

带有默认构造函数的类,是其他类的成员
class Foo { 
public: 
	Foo();
	Foo(int);
	...
};

class Bar
{
public:
	Foo foo;//内涵,不是继承!
	char * str;
};

void func()//一个函数,用到了Bar
{
	Bar bar;
	/*
	bulabulabulabula...
	*/
	if(bar.str){...}
}

//被合成的Bar默认构造函数,能够调用class Foo的默认构造函数来处理Bar::foo
//但是,它并不会产生任何代码来初始化Bar::str。
//将Bar::foo初始化式编译器的责任,但是初始化Bar::str是程序员的责任


//于是我们又创建了Bar的默认构造函数
Bar::Bar() { str = 0; }
//现在程序的需求被满足了
//但是编译器还是要初始化成员对象foo
//但是由于Bar的默认构造函数我们已经显式的定义出来了
//编译器不能再合成第二个,该怎么办?

//于是,编译器就扩张了已存在的构造函数
//使得在程序员的代码被执行之前,先调用必要的默认构造函数foo
//如下
Bar::Bar()
 {
 	foo.Foo();  //编译器构造函数
 	str = 0; //程序员代码
 }


//但是,如果Bar类,有很多构造函数
//唯独没有默认构造函数,那该如何?
//答案就是,对每一个构造函数,都进行如上的扩张
//将程序员代码加进去

拷贝构造函数

  • 默认逐个成员初始化
  • 当一个类A是另一个类B的成员时
  • 在进行拷贝初始化时
  • 先逐个初始化B中已有的成员
  • 然后再对A进行逐个成员初始化
  • 如果A的成员中,还有类,那就如上循环
  • 谓之递归

拷贝时虚函数表的指针


class zooanimal
{
public:
	zooanimal();//默认构造函数
	virtual ~zooanimal();//析构函数

	virtual void draw();
private:
	//draw所需的数据
};

class bear : public zooanimal
{
	bear();
	//virtual ~bear(); //不知道为何,有析构的话,下面赋值会报错???
	void draw();
private:
	//draw 所需的数据
};


bear yogi;
bear winnie = yogi;//会显示不能访问析构函数???什么意思

//yogi会被默认构造函数初始化
//其vpyr设定指向bear class的虚函数表
//将yogi的vptr直接拷贝至winnie的,是安全的

//但是,下面这种呢?
zooanimal franny = yogi; //派生类向基类,这里会发生切割
draw(yogi);//调用bear::draw()
draw(franny);//调用zooanimal::draw()

//如果franny的vptr还和上面一样被设定成指向bear的虚函数表就会出错
//因为都将指向bear::draw(),而实际上他本应该指向zoanimal::draw()
//因此,在此时的拷贝构造时,franny的vptr呗重设了,指向了zooanimal的虚函数表,而非bear的了

Data Member

空类占据几个字节?那么空类的继承类呢?
  • 一个空类占据一个字节
  • 空类也可能需要生成实例对象
  • 但如果是空的,就没有任何标记用于区分不同实例了
  • 因此,分配给空类一个字节的空间
  • 当实例化成对象时,这一个字节的内存就用于区分不同的空类实例
class empty
{
	
};
empty test;
cout<< sizeof(test); //1byte
  • 那么继承之后的空类,占据几个字节呢
  • VS中的编译器是有优化的
  • 通常在类和结构体中,都会进行字节对齐(边界调整alignment)
  • 如下,如果进行字节对齐,那输出就是8
class test
{
	char a;
	int b;
}T;

cout<<sizeof(T);//8
  • 那么继续讲
  • 有两种编译器
  • 一种编译器,在继承之后,会将空基类中的那个字节优化掉
  • 上面讲到,这一字节是作用为了区分空类的不同的实例对象
  • 下面B,已经有了一个int 可以区分不同实例了
  • 因此空基类添加的那一个字节也就失去了意义,可以优化删除掉
  • 因此只占位一个int,输出4
  • 另一种编译器,则不会进行这样的优化
  • 则根据字节对齐,就是8

  • 虚继承为什么也是4字节呢?
  • 因为虚继承会有一个虚函数表
  • 他是一个指针,4字节
  • 因此C,D也是4
  • 若不优化,则就是8

  • 而在E双继承C,D之后
  • 因为相同的基类不会同时出现在继承类中
  • 因此,只有一个A的空类一字节
  • CD的两个指针4+4字节
  • 此处也不需要这一个字节来区分实例,因此E就是8字节
  • 如果没有优化,加上字节对齐,就是4+4+1+(3)=12

  • 因此,有时,类的大小会让人吃惊
class A
{

}a;

class B :public A
{
	int a;
}b;

class C :public virtual A
{

}c;


class D :public virtual A
{

}d;

class E :public C , public D
{

}e;

int main()
{
	cout << "a:" << sizeof(a) << endl;
	cout << "b:" << sizeof(b) << endl;
	cout << "c:" << sizeof(c) << endl;
	cout << "d:" << sizeof(d) << endl;
	cout << "e:" << sizeof(e) << endl;

	system("pause");
	return 0;

}

在这里插入图片描述


Data Member 的绑定

需要注意的点!!!

一定要将nested type声明,放在类的起始处!!

所以可以直接养成习惯,先数据,后函数?

  • 我们直觉认知,肯定是用的类内的length
  • 但是结果却出乎意料
typedef int length;

class point3d
{
public:
	void mumble(length val) { _val = val; }
	length mumble() { return _val; }
	void out(length _val) { cout << "_vla:" << sizeof(_val) << endl; }
	length _len;
private:
	typedef double length;
	length _val;
};

int main()
{
	point3d point;
	point.out(1);
	cout << "len:"<<sizeof(point._len)<<endl;
	system("pause");
	return 0;
}

在这里插入图片描述


  • 只有改成这样的,才是我们想要的结果
typedef int length;

class point3d
{
private:
	typedef double length;
	length _val;

public:
	void mumble(length val) { _val = val; }
	length mumble() { return _val; }
	void out(length _val) { cout << "_vla:" << sizeof(_val) << endl; }
	length _len;
};

int main()
{
	point3d point;
	point.out(1);
	cout << "_len:"<<sizeof(point._len)<<endl;
	system("pause");
	return 0;
}

在这里插入图片描述


Data Member 的布局

  • 标准规定,在同一个区段中(public,private,protect)
  • 成员的排列只需符合,较晚出现的成员在类对象中有较高的地址即可
  • 而不必非得连续。
  • 为什么不连续呢?还有什么能介于类成员变量之间呢?前面讲的字节对齐
  • 在含有虚函数/继承虚类的类中,还需要一个指针,指向虚函数表,这个是编译器自己合成的
  • 一般虚函数表存在于前面那些成员的总体的最前或最后

Data Member 的存取

static member
  • 静态变量,是属于类的,而不是属于对象的
  • 不论实例化多少个对象,静态变量只有一个,存储在静态存储区,而不是每个对象中
  • 因此,不论经过了多么复杂的继承,或者什么
  • 对于静态变量的访问都是不变的,静态存储区访问
nonstatic member
  • 每个nonstatic member在对象中的偏移位置,在编译期间就可获知
  • 甚至如果member属于一个派生类或者多重继承串也是一样的
  • 因此存取一个nonstatic member其效率和存取一个非继承类的member是一样的

继承与Data Member

class point2d
{
private:
	float x;
	float y;
};

class point3d
{
private:
	float x;
	float y;
	float z;
};

//他们的布局结构,和struct一样
---
float x
float y
---

---
float x
float y
float z
---

只继承不多态

class concrete
{
private:
	int val;
	char c1;
	char c2;
	char c3;
};

//实例化对象后的分布
---
1 int val
2
3
4
---
1 char c1
---
1 char c2
---
1 char c3
---
1 padding
---

//但是如果这样写呢?
class concrete1
{
private:
	int val;
	char c1;
};

class concrete2 : public concrete1
{
private:
	char c2;
};

class concrete3 : public concrete2
{
private:
	char c3;
};

//分布
//因为需要字节对齐和边界对齐
---
1 int val
2
3
4
---
1 char c1
2 padding
3 padding
4 padding
---
1 char c2
2 padding
3 padding
4 padding
---
1 char c3
2 padding
3 padding
4 padding
---

加上多态

单一继承

  • 多态就是多了虚函数
  • 因此只在成员对象的后面添加了一个指向虚函数表的指针
内存分布
class point2d
{
public:
	//...虚函数
protected:
	float _x,_y;
};

class point3d : public point2d
{
public:
	//...虚函数
protected:
	float _z;
};

//分布,一般虚函数表指针放在数据成员后
---
float _x
float _y
vptr_point2d 
float _z
----
//前三行是2d的范围
//全体的,是3d的范围
//我们可以发现,继承类和基类的开始地址是相同的
//其差别只是继承类比继承大一些,因为多了成员对象

多重继承

//代码接上
class vertex
{
public:
	//..虚
protected:
	vertex *next;
};

class vertex3d: public pointed3d, public vertex
{
public:
	//..无虚
protected:
	float mumble;
};
  • 对于多重派生对象,将其地址指定最左端的基类指针,此处就是point3d
  • 这时的情况将和前面的单一继承相同
  • 不过vertex的地址,就需要单独修改,需要便宜介于中间的类对象的大小
vertex3d v3d;
vertex *pv;
point2d *p2d;
point3d *p3d;

pv = &v3d;

//这个指定的内部转化
//pv = (vertex*)(((char* )&v3d) + sizeof(point3d));

p2d = &v3d;
p3d = &v3d;//这些就属于单一继承的情况,无需转化

//如果有指针如下;
vertex3d *pv3d;
vertex *pv;

//那么如下操作
pv = pv3d;
//不能简单的转换
/pv = (vertex*)(((char* )&v3d) + sizeof(point3d));
//这里为了防止v3d为0的情况,所以需要修改为:
/pv = pv3d
? (vertex*)(((char* )&v3d) + sizeof(point3d))
: 0;

//分布
---
1 float _x
2 float _y
3 vptr_point2d 
4 float _z
5 vertex* next
6 vptr_vertex
7 float mumble
---

1-3为point2d
7-4为point3d
5-6为vertex
1-7为vertex3d

Function 语义学


构造,析构,拷贝语义学

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值