堆与拷贝构造函数

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

原文地址:https://www.cnblogs.com/yjds/p/8597281.html