关于构造函数,拷贝构造函数与析构函数的自动调用的问题分析


/*
-----------------------------------------------------------------------------
modified by quanspace
2012年4月2日15:18:29
-----------------------------------------------------------------------------
*/
# include <iostream>
using namespace std;
//点类的界面部分   class interface
class Point
{
public:
Point(int = 0, int = 0); //成员函数 构造函数的声明
Point(const Point&); //把引用形参声明为 const参数 以保证所引用的对象不被修改
void displayxy(); 
~Point(); //析构函数声明
private:
int X, Y;
};

//点类的实现部分  class implementation
Point::Point(int x, int y)  //构造函数
{
X = x;
Y = y;
cout<<"Constructor is called!";
displayxy();
}

Point::Point(const Point& p) //拷贝构造函数
{
X = p.X;
Y = p.Y;
cout<<"Copy Constructor is called!";
displayxy();
}
Point::~Point()
{
cout<<"Destructor is called!";
displayxy();
}


void Point::displayxy()   //显示点的坐标
{
cout<<"("<<X<<","<<Y<<")"<<endl;
}
 //定义一个返回值与形参都为点类类型的函数
Point func(Point p) 
{
int x = 10*2;
int y = 10*2;
Point pp(x,y);  //定义一个局部的对象
return pp;
}

int main()
{
Point p1(3,4);  //初始化对象p1
Point p2 = p1; //用类的一个对象p1去初始化类的另一个对象
p2 = func(p1); //函数的形参与返回值都是类的对象
return 0;
}
        我认为想要完全看懂这个程序,必须先了解什么是函数的显示调用与隐式(动态)调用的概念.
显示调用:直接使用函数,只要事先声明该函数的所在库就可以了,此时编译后关于该函数的信息会在输入表里.PE文件被载入到内存的时候WINDOWS会把所需要的函数地址填充到内存的指定地方.也就是说必须是用户自己事先要说明.
隐式调用: 也就是动态的调用函数,不需要人为说明,编译器自动的调用,是随机的.至于具体的关于如何实现隐式调用,这里涉及编译原理.我也搞不懂.总之.我们现在为了看懂上述函数,只要明白一点,那就是constructor, copy constructor , destructor 在此程序中都是隐式调用. 所以函数中加入一个输出语句和display函数以便于我们分析.


        不难看出,上面这个程序中首先是定义了一个简单的点类以及一个名为func的子函数. 现在我们从主函数开始分析. 

        当compiler执行语句 Point p1(3, 4); 时, 将自动调用构造函数(constructor) Point, 从而创建一个点类的对象p1并初始化为(3,4). 我们看到此构造函数中还有一个输出语句display函数,当其此时被调用时将输出:

 Constructor is called! (3,4).

        调用完Point(int, int)后,compiler又返回main()执行语句 Point p2 = p1; 此时将创建一个点类对象p2,. compiler此时将自动调用另一特殊函数 拷贝构造函数(Copy constructor)  Point (const Point &), 输出 :

Copy constructor is called! (3,4).

       
        我们知道如果一个带参函数被调用,必须由主函数提供一个argument给parameter, 即p1传给p. 那么问题又来了,主函数中的变量(对象)p1该如何传给子函数的参量p呢?
这里就要说说 函数的参数传递问题. 在参数传递中有两种调用机制: 传值调用和引用调用.   传值调用只是使用了 argument的值,在被调函数中对parameter的操作不会改变主函数中argument的值. 而引用则是传argument的地址,对parameter的操作将改变主函数中变量的属性.

        现在回来分析,不难看出对于func来说是是属于值传递.我们不希望p1跟p一样,在函数func调用完后一起死亡(被释放), 我们要让对象p1一直活到return 0; 那么,如何将argument p1的值赋给parameter p呢?  可以给p一个p1的副本就行了. 所以此刻compiler就会自动调用Point (const Point &), 输出:

 Copy constructor is called! (3,4)

传参完成后,执行func函数体, x,y 变为20, 创建局部变量pp并初始化 , 自动调用constructor 输出:

 Constructor is called! (20,20).

        接下来执行语句return pp;( 函数返回一个对象值时,会产生临时对象,函数中的返回值会以值拷贝的形式拷贝到被调函数栈中的一个临时对象。)将函数的返回值pp赋给p2.当函数执行完return pp; 局部对象将被释放(调用析构函数).问题就来了,pp都被"析构"了,怎么将p2改变了? 此刻compiler一定创建了pp的副本.调用 Point(const Point& p) 将其成员数据赋给对象p2. 输出:

 Copy constructor is called!(20,20). 
         到此p2 = func(p1); 就执行完毕了.  
 
    当compiler执行到return 0; 后就该destructor 大显身手啦! 析构函数像个大杀手废掉一切过期的对象, 关于在主函数,子函数中释放空间具体调用顺序又是怎样的呢? 一般教材上指出 析构函数与构造函数的调用顺序正好相反.   想要进一步了解就必须知道程序运行时刻(run-time environment)的存储分配策略和内存的组成即区域划分.这儿涉及到编译原理和操作系统等后续课程的知识,具体我也搞不太懂. 大致是在函数被调用时,把为其建立的活动记录下来,推入运行栈中,该栈包括局部数据域,为局部变量分配存储空间,当函数执行完毕后返回调用程序时,该被调用的函数的活动记录也将被推上去,为局部变量非配的存储区域不复存在.   

       如果构造是从外到里, 就像析构从里到外 . 构造由全局对象到局部对象, 析构则相反.   析构先从子函数 func开始. 先释放pp副本 , 在释放形参p ,然后是 局部对象pp ,接着是第二创建的p2,最后是第一个创建的对象 p1. 

 现在我们看程序的输出结果:    


     







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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值