• 堆与拷贝构造函数


        在C++中,堆分配的概念得到了扩展,不仅C++的关键字new和delete可以分配和释放堆空间。而且通过new建立的对象要调用构造函数,通过delete删除对象也要调用析构函数。另外,当对象被传递给函数或者对象从函数返回的时候,会发生对象的拷贝。

    1、关于堆:

    c++程序的内存格局通常分为4个区:(1)全局数据区(2)代码区(3)栈区(4)堆区

    (1)全局数据区:全局变量,静态数据,常量

    (2)代码区:类成员函数,非成员函数,代码存放在代码区

    (3)栈区:为运行函数分配的局部变量,函数参数,返回数据,返回地址,等存放在栈区

    (4)堆区:剩下的地址全部是堆区

        new和delete,在操作堆区时,如果分配了内存就有责任回收他,否则运行的程序将会造成内存泄漏。这与函数中在栈区分配局部变量有本质的不同。

        对C++来说,管理堆区是一件十分复杂的工作,频繁的分配和释放不同大小的堆空间,将会产生堆内碎块。

    2、需要new和delete的原因:

        从C++的立场看,不能用malloc()函数的一个原因就是,他在分配内存空间的时候不能调用构造函数。类对象的建立是分配空间,构造结构,以及初始化的三位一体,他们统一由构造函数来完成。

        malloc仅仅是一个函数调用,它没有足够的信息来调用一个构造函数,他要接受的类型是一个unsigned long类型。

        为此,需要在内存分配之后在进行初始化。

    1
    2
    3
    4
    5
    6
    7
    void fn()
    {
        data * pd;
        pd=(data* )malloc(sizeof(data));
        pd->setdata();
        free(pd);
    }

    这个从根本上来说,并不是一个类对象的创建,因为他跳过了构造函数。

    另外,再分配内存申请的时候,总是知道分配的空间派什么用,而且分配空间大小总是某个数据类型(包括类类型)的整数倍。因而c++使用new来代替c的malloc是必然的。

    3、分配堆对象:

        C++的new和delete机制更加的简单易懂

    1
    2
    3
    4
    5
    6
    void fn()
    {
        data *ps;
        ps=new data;//分配堆空间并构造他
        delete ps;//先析构,然后将内存空间返还给堆
    }

    不必显示的指出,从new返回的指针类型,因为new知道要分配对象的类型是data。而且new还必须知道对象的类型,因为它要籍此调用构造函数。

        如果是分配局部变量,则在该局部对象退出作用域时自动调用析构函数。但是堆对象的作用域是整个程序的生命期,所以除非程序运行完毕,否则堆对象作用域不会到期。堆对象析构是在释放堆对象语句delete执行时。C++自动的调用其析构函数。

        构造函数可以有参数,所以跟在new后面的类型也可以跟参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    #include<iostream>
    #include<string>
    #ifndef DATA_H
    #define DATA_H
    class data
    {
    public:
        data(int m,int d,int y);
        ~data();
     
    private:
        int month;
        int day;
        int year;
    };
     
    data::data(int m,int d,int y)
    {
        if (m > 0 && m < 13)
        {
            month = m;
        }
        if (d>0 && d < 32)
        {
            day = d;
        }
        if (y>0 && y < 3000)
        {
            year = y;
        }
    }
     
    void fn()
    {
        data* pd;
        pd = new data(111198);//使new去调用了构造函数data(int,int,itn)。
        //new是根据参数匹配的原则来调用构造函数的。
        delete pd;
    }
     
    data::~data()
    {
    }
    #endif

    从堆中还可以分配对象数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    #include<iostream>
    #include<string>
    #ifndef DATA_H
    #define DATA_H
    class data
    {
    public:
        data(int m,int d,int y);
        data(char *);
        ~data();
     
    private:
        int month;
        int day;
        int year;
        char name[40];
    };
     
    data::data(int m,int d,int y)
    {
        if (m > 0 && m < 13)
        {
            month = m;
        }
        if (d>0 && d < 32)
        {
            day = d;
        }
        if (y>0 && y < 3000)
        {
            year = y;
        }
    }
     
    data::data(char * pname="no name")
    {
        strcpy(name, pname);
    }
    void fn()
    {
        data* pd;
        pd = new data(111198);//使new去调用了构造函数data(int,int,itn)。
        //new是根据参数匹配的原则来调用构造函数的。
        delete pd;
    }
    void fn()
    {
        data * ps = new data[n-1];//n代表次数
        delete ps[];
    }
    data::~data()
    {
    }
    #endif

    分配过程将激发n次构造函数的调用,从0~n-1次。调用构造函数的顺序依次为ps[0],ps[1],ps[2]。。。

        由于分配数组时,new的格式是类型后面跟[元素个数],不能再跟构造函数参数,所以,从堆上分配对象数组,只能使用默认的构造函数,不能调用其他任何构造函数。

        如果该类没有默认构造函数,则不能分配对象数组。

        delete[]ps中的【】是要告诉C++,该指针,指向的是一个数组。如果【】中填上了数组的长度信息。C++编译系统将忽略,并把他作为【】对待。如果没有写【】C++编译系统将会报错。

        一般来说,堆空间相对其他内存空间比较空闲,随要随拿,给程序运行带来了较大的自由度,使用堆空间,往往由于:

    (1)直到运行时才能知道需要多少对象空间,

    (2)不知道对象的生存周期到底有多长,

    (3)直到运行时,才知道一个对象需要多少内存空间

    4、拷贝构造函数:

        可用一个对象去构造另一个对象,或者说,用另一个对象值初始化一个新构造的对象,

    1
    2
    student s1(“wangshuai”);
    student s2=s1;//用s1的值去初始化s2

    对象作为函数参数传递时,也要涉及对象的拷贝。

    1
    2
    3
    4
    5
    6
    7
    void fn(student )
    {}
    void main()
    {
        student ms;
        fn(ms);
    }

        函数fn()的参数传递的方式是传值,参数类型是student,调用时,实参ms传给了形参fs,ms在传递的过程中是不会变的,形参fs是ms的一个拷贝。这一切是在调用开始完成的,也就是说,形参fs用ms的值进行构造。

        这时候,调用构造函数student(char *)就不合适,新的构造函数的参数因该是student &,也就是

    student(student &)

        为什么C++要使用上面的拷贝构造函数,而他自己不会做下面的事情,即:

    int a=6;

    int b=a;

        应为对象的种类多种多样,不象基本数据类型这么简单,有些对象还申请了系统资源,系统资源归属不清,将引起资源管理的混乱。

    拷贝构造函数的用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #include<iostream>
    #include<string>
    using namespace std;
    #ifndef STUDENT_H
    #define STUDENT_H
    class Student
    {
    public:
        Student(char * pname,int ssid);
        Student(Student &);//拷贝构造函数
        ~Student();
     
    private:
        char name[40];
        int id;
    };
     
    Student::Student(char * pname = "no name"int ssid)
    {
        strcpy(name, pname);
        cout << "construct new student " << pname << endl;
    }
    Student::Student(Student &s)//拷贝构造函数
    {
        cout << "construct new " << s.name << endl;
        strcpy(name, "copy of");
        strcat(name, s.name);
        id = s.id;
    }
    Student::~Student()
    {
    }
    #endif

        randy对象的创建调用了普通的构造函数,产生了第一行的信息,随之便输出第二行信息,main()调用fn(randy)时,发生了从实参randy到形参s的拷贝构造,于是调用拷贝构造函数s被析构,所以产生了第五行信息,回到主函数后,输出第六行信息,最后主函数结束时,randy对象被析构,所以产生了七行信息。

    5、默认拷贝构造函数:

        在类的定义中,如果没有提供自己的拷贝构造函数,则C++提供一个默认拷贝构造函数,就像没有提供构造函数时,C++提供默认构造函数一样。

        c++提供的默认拷贝构造函数工作的方法是,完成一个成员一个成员的拷贝。如果没有成员是类对象,则调用其拷贝构造函数或者默认拷贝构造函数。

    6、浅拷贝与深拷贝:

        一个类可能会拥有资源,当其构造函数分配了一个资源(例如堆内存)的时候,会发生什么?如果拷贝构造函数简单的制作了一个该资源的拷贝,而不对它本身分配,就得面临一个麻烦的局面,两个对象都拥有一个资源。当对象析构时,该资源将经历两次资源返还。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #include<iostream>
    #include<string>
    using namespace std;
    class Person
    {
    public:
        Person(char * pn);
        ~Person();
     
    private:
        char * pname;
    };
     
    Person::Person(char * pn)
    {
        cout << "construcrting " << pn << endl;
        pname = new char[strlen(pn) + 1];
        if (pname != 0)
        {
            strcpy(pname, pn);
        }
    }
     
    Person::~Person()
    {
        cout << "destructing " << pname << endl;
    //
        pname[0] = '';
        delete(pname);
     
    }
    void main()
    {
        Person p1("ready");
        Person p2 = p1;
        system("pause");
        return ;
    }

        程序开始运行时,创建p1对象,p1对象的构造函数从堆中分配内存空间并赋值给数据成员pname,同时,产生第一行的数据输出;执行p2=p1时,因为没有定义拷贝构造函数,于是就使用默认拷贝构造函数,使得p2与p1完全一样(如果没有自定义拷贝构造函数,则调用默认拷贝构造函数,将两者完全复制,相等,但是内存资源并没有给),并没有新分配堆内存空间给p2。

        创建p2时,对象p1被复制给了p2,单资源并未复制,因此,p1和p2指向同一个资源,这称为浅拷贝。

        当一个对象创建时,分配了资源,这时,就需要定义自己的拷贝构造函数,使之不但拷贝成员,也拷贝资源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    #include<iostream>
    #include<string>
    using namespace std;
    class Person
    {
    public:
        Person(char * pn);
        Person(Person &p);
        ~Person();
     
    private:
        char * pname;
    };
     
    Person::Person(char * pn)
    {
        cout << "construcrting " << pn << endl;
        pname = new char[strlen(pn) + 1];
        if (pname != 0)
        {
            strcpy(pname, pn);
        }
    }
    Person::Person(Person &s)
    {
        cout << "copying " << s.pname << " into its own block " << endl;
        pname = new char[strlen(s.pname) + 1];
        if (pname != 0)
        {
            strcpy(pname, s.pname);
        }
    }
     
    Person::~Person()
    {
        cout << "destructing " << pname << endl;
    //
        pname[0] = '';
        delete(pname);
     
    }
    void main()
    {
        Person p1("ready");
        Person p2 = p1;
        system("pause");
        return ;
    }

        拷贝构造函数中,不但复制了对象空间,也复制资源(内存空间)。

    堆内存并不是唯一需要拷贝构造函数的资源,但它是最常用的一个。打开文件,占有硬件设备服务等也需要深拷贝。他们是析构函数必须返还的的资源类型。因此,一个很好的经验是:如果你的类需要析构函数,则他也需要一个拷贝构造函数。

    因为,通常对象是自动被析构的。如果需要一个自定义的析构函数,那么就意味着有额外资源要在被析构之前释放。此时,对象的拷贝就不是前拷贝了。

    7、临时对象;

    当函数返回一个对象时,要创建一个临时的对象存放在返回对象的内存中。例如下面的代码中,返回的ms对象对象将产生一个临时对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    student fn()
    {
        student ms("randy");
        return ms;
    }
    void main()
    {
        student s;
        s=fn();
    }

    在这里,系统调用拷贝构造函数价格ms拷贝到新创建的临时对象中。

        一般规定,创建的临时对象,在整个创建他们的外部表达式范围内有效,否则无效。也就是说,s=fn();这个外部表达式,当fn()返回时产生的临时对象拷贝给s后,临时对象就析构掉了。

    例如下面的代码中,引用refs就失效了:

    1
    2
    3
    4
    void main()
    {
        student & refs=fn();
    }

    这就意味着refes的实体已不复存在,所以接下去的任何对refs的引用都是错的。

    fn()返回时,创建临时对象作为fn2()的实参,此时,在fn2()中一直有效,当fn2()返回一个int值参与计算表达式时,那个临时对象仍有效,一旦计算完成,赋值给x后,则临时对象被析构。

    8、无名对象:

    可以直接用构造函数产生无名对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class student
    {
        public:
            student(char *);
    }
    void fn()
    {
        student("randy");//此处就是一个无名对象
    }

    无名对象(1)可以作为实参传递给函数,(2)可以拿来拷贝构造一个新的对象,(3)也可以初始化一个引用的声明。

    1
    2
    3
    4
    5
    6
    7
    void fn(student &s);
    void main()
    {
        student & refs=student("randy");
        student s=student("randy");
        fn(student("randy"));
    }

    9、构造函数用于类型转换。

        C++可以用来从一种类型转换成另一种类型,这是C++从类机制中获得的附加性能。但要注意以下两点:

    (1)只会尝试含有一个参数的构造函数

    (2)如果有二义性,则放弃尝试

    小结:

        运算符new分配堆内存,如果成功,则返回指向该内存的空间,如果失败,则返回NULL。

    所以每次使用运算符new动态分配内存时,都因该测试new的返回指针值,以防分配失败。

        堆空间的大小是有限的,视其操作系统和编译设置的不同而不同。当程序不再使用所分配的堆空间时,应及时使用delete释放他们。

        由C++提供的默认拷贝函数只是对对象进行浅拷贝复制。如果对象数据成员包括指向堆空间的指针,就不能使用这种拷贝方式,此时必须自定义拷贝构造函数,为创建的对象分配内存空间。

  • 相关阅读:
    回流与重绘
    事件循环的一些小啰嗦
    async与await的那些事儿
    数组去重的方法们
    Object的一些常用方法
    JS事件流的一些理解
    关于Ajax的那些记录
    call、bind与apply函数的区别
    继承的一些细碎记载
    计算机基础的小贴士(1)
  • 原文地址:https://www.cnblogs.com/yjds/p/8597281.html
Copyright © 2020-2023  润新知