0
点赞
收藏
分享

微信扫一扫

C++对象在内存中是如何分布的

吃面多放酱 2022-01-28 阅读 68

对象在内存中的分布

函数引用返回分析内存

这里我们允接前面拷贝构造中使用过的代码,可以去回顾前面的文字方便理解
C++中的拷贝构造

class Object
{
	int value;
public:
	Object()
	{
		cout << "Object::Object" << this << endl;
	}
	Object(int x = 0) :value(x)
	{
		cout << "Object::Object" << this << endl;
	}
	~Object()
	{
		cout << "Objecet::~Object" << this << endl;
	}

	Object(Object& obj) :value(obj.value)
	{
		cout << "Copy Create" << this << endl;
	}
	void SetValue(int x) { value = x; }
	int GetValue() const { return value; }
};
Object fun(Object obj)
{
	int val = obj.GetValue();
	Object obja(val);
	return obja;
}

int main()
{
	Object objx(0);
	Object objy(0);
	objy = fun(objx);
	return 0;
}

我们对上面的代码进行修改,希望构建的对象更少

Object::int& Value() {return value;}
Object::const int& Value() const {return value;}
//两份代码使得程序通用性更强

//Create MIN Object
//Object fun(Object obj)
Object fun(const Object& obj) //const取决于是否需要通过形参去修改实参
{
	int val = obj.Value();
	Object obja(val); //3
	return obja; //4
}

int main()
{
	Object objx(0); //1
	Object objy(0); //2
	objy = fun(objx);
	return 0;
}

我们设计了两个Value函数,加强了程序的通用性;并且对fun传参修改为引用,使得减少一次对象的创建
在这里插入图片描述
在这里插入图片描述
上面的代码我们没有按引用返回fun函数,是这样的情况:

  • 进入主函数分配栈帧,创建objx,objy对象空间,空间创建并没有对象;在此空间一次构建对象objx,objy
  • 调动fun函数,开辟fun函数栈帧;创建obj引用,底层实际为指针指向objx,给出val值为10,然后构建出obja对象值为10
  • return obja对象,创建将亡值对象,在主函数栈帧的上面(主函数调用fun),并把将亡值对象地址送入eax,将亡值对象构造完成;fun函数结束,函数空间栈帧归还给堆区,并且析构对象obja
  • 从fun函数回到主函数,将eax指向的将亡值对象赋值给objy;随后将亡值对象析构
  • 调动return 0主函数结束;我们析构objy然后析构objx
  • 析构完成从主程序退出

这次我们使用引用特性返回

//Object fun(const Object& obj)
Object& fun(const Object& obj)
{
	int val = obj.Value() + 10;
	Object obja(val);
	return obja;
}

int main()
{
	Object objx(0);
	Object objy(0);
	objy = fun(objx);
	cout << objy.Value() << endl;
	return 0;
}

在这里插入图片描述
主函数栈帧创建与函数调用的过程都相同,但是函数返回有差异

  • 进入主函数分配栈帧,创建objx,objy对象空间,空间创建并没有对象;在此空间一次构建对象objx,objy
  • 调动fun函数,开辟fun函数栈帧;创建obj引用,底层实际为指针指向objx,给出val值为10,然后构建出obja对象值为10
  • return obja引用,不会创建将亡值对象,而是把obja的地址给eax,随后fun函数栈帧空间归还堆区,obja对象析构
  • 回到主函数,我们通过eax解引用指向对象讲值给到objy,但是obja已经被析构且空间归还给堆区
  • 若该空间未受到侵扰,可以返回得到10,若被侵扰返回随机值;我们从已死亡的对象上获取数据是不牢靠的

所以在这里我们的引用返回是没有意义的,想通过引用返回从而减少对象的创建这样是错误的,相当于从死人身上去取东西

为什么不能以引用返回

我们设计下面的对象,并写出缺省的拷贝构造函数和赋值函数

class Object
{
private:
	int num;
	int ar[5];
public:
	Object(int n, int val = 0) :num(n)
	{
		for (int i = 0; i < n; ++i)
		{
			ar[i] = val;
		}
	}
};

int main()
{
	Object obja(5, 23);
	Object objb(obja);
	Object objc(5);
	objc = obja;

	return 0;
}

接下来我们对缺省的拷贝构造函数、以及赋值函数进行编写:

  • 首先是拷贝构造
Object(const Object& obj)
	{
	    //memmove(this, &obj, sizeof(Object));
	    //memset(this,0,sizeof(Object));
		num = obj.num;
		for (int i = 0; i < obj.num; i++)
		{
			ar[i] = obj.ar[i];
		}
	}

在这里我们不允许使用memmove(this, &obj, sizeof(Object));进行拷贝,这是因为:如果说我们其中的某些方法是虚函数,我们创建对象的时候,对象上面部分有4个字节指向虚表
在这里插入图片描述
同样使用memset(this,0,sizeof(Object));也是不对的,当类型拥有虚表的时候,虚表存在于对象最上面的4个字节,虚表在进入到构造函数或拷贝构造函数之前就进行了虚表设置;在然后如果我们进行上面的初始化会将虚表也清零

所以在C++中,任何成员函数中都要谨慎的使用内存拷贝函数

  • 赋值函数
Object& operator=(const Object& obj)
{
	if (&obj != this)
	{
		num = obj.num;
		for (int i = 0; i < obj.num; i++)
		{
			ar[i] = obj.ar[i];
		}
	}
	return *this;
}

当我们不去写拷贝构造或赋值函数的时候,系统是怎么做的呢?

对于 objc = obja , 若没有虚函数,系统会抓住obja的首地址和objc的首地址,然后依次将obja的数据值依次拷进objc;若存在虚函数,那么系统就会如同我们上面所写一样的方式进行拷贝

当我们没有写拷贝构造和赋值函数时

class Object
{
private:
	int num;
	int ar[5];
public:
	Object(int n, int val = 0) :num(n)
	{
		for (int i = 0; i < n; ++i)
		{
			ar[i] = val;
		}
	}
	void Print() const
	{
		cout<< num << endl;
		for(int i = 0;i < 5;i++)
		{
			cout << ar[i] << endl;
		}
	}
};
Object& fun()
{
	Object objx(5,100);
	return objx;
}
int main()
{
	Object obja(5, 23);
	Object objb(obja);
	Object objc(5);
	objc = fun();

	objc.Print();

	return 0;
}

我们之前讲过,当我们引用返回一个对象;并不会创建将亡值,而是将fun栈帧中的objx地址传递给eax,并把值给到objc,在我们没有写拷贝构造函数与赋值函数的时候,系统会按照obja和objc的首地址依次将值进行传递,那么我们可以得到正确的数值
在这里插入图片描述
如果我们有赋值语句,则会调度赋值语句,会开辟栈帧并且覆盖残存在栈帧中的objx;继而打印出来的都是随机值
在这里插入图片描述
我们再总结一下原因:当代码中并没有写赋值语句,这时候系统就会缺省一个赋值语句,此缺省语句并没有函数调用过程,也就不会有现场保护;系统在生成缺省语句的时候检测类型是否是一个简单类型(没有继承关系、没有虚函数、成员仅有基础类型,没有我们设计的类型),也就不会生成赋值语句的函数,仅仅是将内存进行拷贝,不会对残留在栈帧中的数据进行侵扰,所以我们可以取得这个数据;当我们再代码中写了赋值语句,那么就会调度赋值语句,继而将会扰乱原本残留数据的栈帧

我们上面提到的一切都建立在:

  • 当函数返回引用,会将原本函数栈帧中对象的地址进行传递,随后函数结束该空间释放,继而能否得到正确的数值都在于该空间是否会被侵扰
  • 所以无论如何都不要将一个对象以引用返回
    在这里插入图片描述

缺省构造与缺省赋值

class Object
{
	int num;
	int* ip;
public:
	Object(int n, int val = 0) : num(n)
	{
		ip = (int*)malloc(sizeof(int) * n);
		for (int i = 0; i < num; i++)
		{
			ip[i] = val;
		}
	}
	~Object()
	{
		free(ip);
		ip = NULL;
	}
};

int main()
{
	Object obja(5, 23);
	Object objb(obja);
	Object objc(8);
}

下面是我们系统给出的缺省拷贝构造和缺省赋值语句:

Object(const Object& obj)
{
	num = obj.num;
	ip = obj.ip;
}
Object& operator=(const Object& obj)
{
	if (this != &obj)
	{
		num = obj.num;
		ip = obj.ip;
	}
	return *this;
}

在这里插入图片描述
我们在这里对objb进行拷贝构造,会将objb中的ip直接指向obja中的ip空间,而不是单独开辟一个空间;同样在objc = obja的时候,直接将objc中的ip指向obja中的ip地址,那么原本属于objc的内存空间会丢失,并且在我们析构对象时候,会对同一个空间进行二次释放

所以在这里我们的缺省构造以及缺省函数无法满足我们的要求,需要对其进行重写

举报

相关推荐

0 条评论