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