对象在内存中的分布
函数引用返回分析内存
这里我们允接前面拷贝构造中使用过的代码,可以去回顾前面的文字方便理解
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的内存空间会丢失,并且在我们析构对象时候,会对同一个空间进行二次释放
所以在这里我们的缺省构造以及缺省函数无法满足我们的要求,需要对其进行重写