c++的堆与拷贝构造函数
来源:互联网 发布:淘宝客佣金申请链接 编辑:程序博客网 时间:2024/05/19 15:40
关于堆的知识
一般情况下c程序会存放在rom或flash中,运行再拷贝到对应的内存中。c++程序中内存分别存放不同的信息,
(1)全局数据区:存放全局变量、常量、静态数据
(2)代码区:存放程序的代码
(3)栈区:存放局部变量、函数的参数、返回数据、返回地址等
(4)堆区(自由存储区):作为其他操作的使用的资源
当我们的程序通过new或者malloc申请到了一些堆内存时,我们就有责任去回收它们,否则会造成内存泄漏,另外c++管理这些内存时很辛苦的一件事情,频繁的申请和释放内存,会产生大量的堆碎块
堆区和栈区的比较
栈区:
栈是向下增长的,存放的是一些局部变量,参数,返回地址、返回值等,栈中分配局部变量空间,(由系统分配)
堆区:
堆是向上增长的,用于分配程序申请的内存区。
区别
堆区和栈区的区别就是在于内存分配的方式上,另外就是内存空间回收方式上,栈区的内存也是系统自动完成的,当函数运行完后,栈上的内容就被释放了。堆中的内容只要程序不去释放,它就一直都在,但是这样会造成内存泄漏。
申请后的反应
栈区:
只要栈区中有大于申请的空间的内存,系统就会为程序提供内存,否则,会发生异常
堆区:
操作系统中有一个记录闲时的内存地址的链表,当系统收到空间申请时,就会去查找那个表,找到第一个空闲空间大于申请空间的结点,然后把该结点从该链表中删除,并将该结点的空间分配给程序。另外,多数系统会在该结点的首地址记录分配的内存的大小,从而可以在delete时,释放正确的内存空间。
申请效率比较
栈区:
系统自动分配,速度快,但是空间有限,WINDOWS下只有2M,
堆区:
由于分配堆区后,还有很多后续工作,如删除结点,记录分配的空间的大小等。所以效率较低,但是空间充足,程序员可以很好的控制它。
需要new和delete的原因
以前看过一个文章写的是“学好c++必须做到的50条”,我很同意里面的一句话:“不要因为c和c++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样;”,这句话正好在这里可以显示其正确性,
c++中不能使用malloc的一个原因就是:它在为指针对象分配空间时,不能够调用构造函数。一个类的对象的建立包括三个部分:
- 分配空间
- 构造结构
- 初始化
但是上述三个部分统一由构造函数完成的。于是我们就需要new和delete来完成对对象的空间的分配和内存的释放。其分配的内存空间就是在堆区中的内存。另外由于类的构造函数是可以有参数的,所以new后来的类类型也是可以有参数的。
#include<iostream>using namespace std;class A{public: A(int m, int d) { cout << m << " " << d << endl; } ~A() { cout << "Delete" << endl; }};int main(){ A *p; p = new A(10, 2); //申请堆内存 //p = new A; 系统会报错,因为系统内已经没有无参构造函数 delete p; //释放堆内存,调用析构函数,如果没有这个语句,那么申请的堆区将一直有效,直达程序执行完 system("pause"); return 0;}
输出结果:
另外从堆中还可以分配对对象数组,下面给个范例:
#include<iostream>using namespace std;class A{public: A(int m, int d) { cout << m << " " << d << endl; } A() { } ~A() { cout << "Delete" << endl; }};int main(){ A *p=new A[10]; //声明了10个A的对象 delete [] p; //释放堆内存,调用析构函数,这里是释放了10个对象申请的堆内存 system("pause"); return 0;}
输出结果:
但是这里有一个需要注意的地方,如果我们自己为类提供了一个有参的构造函数时,但是你又要构建一个对象数组的时候,你就需要手动的为类添加一个无参的构造函数,否则程序将会报错,如下图所示:
报错的原因是,我们在声明一个对象的数组时,最后面是跟数组的大小,无法再添加构造函数的参数了,所以它只能够调用无参构造函数了。
delete [] p,这个是告诉系统,该指针指向一个数组,如果没有添加[],程序会产生运行错误。
类对象数组初始化的方法
(1)对象数组
A p[2]={A(10,1),A(10,2)};//A p[2];//p[0]=new A(10,1); 这种方式是错误的//A *p=new A[2];//p[0]=new A(10,1); 这种方式同样是错误的。//其实通过A p[2]和A *p来声明一个对象数组时,要初始化,只有靠其构造函数自己提供默认的初始值。
(2)指针数组
typedef A * p; p P[2]; //相当于p * P =new p[2],P相当于一个二级指针 for (int i = 0; i < 2; i++) P[i] = new A(10, i); //相当于P就是一个二级指针,所以可以对P[i]直接使用new for (int i = 0; i < 2; i++) delete P[i]; //因为P是一个二级指针,P[i]相当于就是一个指针
拷贝构造函数
因为对象的类型多种多样,不像基本数据类型这么简单,所以并不能像普通类型一样直接拷贝,如:
int a=5;int b=a; //用a的值拷贝给新建的b
类对象中,如果要用一个对象去初始化另外一个对象,则必须是调用的类的拷贝构造函数去初始化那个对象
下面是一个使用拷贝构造函数的例子
#include<iostream>#include<string>using namespace std;class Student{private: int age; string name;public: /* 构造函数 */ Student(int age, string name) { this->age = age; this->name = name; cout << "构造函数" << endl; } /* 拷贝构造函数 */ Student(const Student & r) { age = r.age; name = r.name; cout << "拷贝构造函数" << endl; } /* 析构函数 */ ~Student() { cout << "析构函数" << endl; }};void call(){ Student stu(10, "Ouyang"); Student stu1 = stu; //等价于:stu1(stu);这种方式,我们可以更容易的理解,拷贝构造函数只是特殊的一种构造函数}int main(){ call(); system("pause"); return 0;}
输出结果:
通过上述代码的输出结果,我们可以发现stu1的创建时,通过调用stu1的拷贝构造函数,从而把对象stu整个复制到stu1,
默认拷贝构造函数
c++的类定义中,如果程序中没有为类提供拷贝构造函数,那么系统提供一个默认的拷贝构造函数。
#include<iostream>#include<string>using namespace std;class Student{private: int age; string name;public: /* 构造函数 */ Student(int age, string name) { this->age = age; this->name = name; cout << "构造函数" << endl; } /* 无参构造函数 */ Student() { cout << "无参构造函数" << endl; } /* 拷贝构造函数 */ Student(const Student & r) { age = r.age; name = r.name; cout << "拷贝构造函数" << endl; } /* 析构函数 */ ~Student() { cout << "析构函数" << endl; }};class Teacher{public: Teacher(Student & r) { stu = r; //调用了一次Student的拷贝构造函数 }private: Student stu; //把Student作为Teacher的私有对象};/*函数fun把Teacher作为一个形参*/void fun(Teacher s){ cout << "" << endl;}int main(){ Student stu; Teacher tea(stu); cout << "" << endl; fun(tea); //把Teacher的一个对象,作为一个实参通过值传递到函数中,那么它将对该对象进行拷贝 cout << "" << endl; system("pause"); return 0;}
输出结果:
我们都知道函数参数的传递有两种方式:
- 值传递:会产生实参的副本,将副本传给函数
- 引用传递:不产生副本,直接把自己传给函数
所以需要产生副本,那么就需要调用拷贝构造函数啦。
c++中使用拷贝构造函数的三种情况
(1). 使用已经存在的对象去初始化另外一个对象
Student stu(10, "Ouyang");Student stu1 = stu; //等价于:stu1(stu);这种方式,我们可以更容易的理解,拷贝构造函数只是特殊
注释:两个对象必须是同类的对象,stu1都是通过调用其拷贝构造函数来初始化自己。
(2):作为函数的返回值
#include<iostream>#include<string>using namespace std;static int i = 0;class Student{private: int age; string name;public: /* 构造函数 */ Student(int age, string name) { this->age = age; this->name = name; cout << "构造函数" << endl; } /* 无参构造函数 */ Student() { cout << "无参构造函数" << endl; } /* 拷贝构造函数 */ Student(const Student & r) { age = r.age; name = r.name; cout << "拷贝构造函数" << endl; } /* 析构函数 */ ~Student() { cout << "析构函数" << endl; } void print() { cout << age << endl; }};Student fun(int age, string name){ Student stu(age, name); //这是一个局部变量,在函数执行完将会进行析构 return stu;}int main(){ Student stu; stu = fun(10, "Ouyang"); Student stu1 = fun(11, "Ouyang"); system("pause"); return 0;}
输出结果:
注释:
第一行:创建主函数中的stu对象时,调用了无参构造函数,
第二行:进入了函数fun,调用了有参构造函数,从而创建了fun中的stu对象,
第三行:生成了一个临时对象,我们就叫他为stu_copy,stu_copy是通过调用拷贝构造函数,所以输出了 第四行,完成对fun中的stu对象的拷贝,然后把stu给析构了。所以输出了第五行。
第六行:由于主函数中的stu首先是通过无参构造函数创建过一次,如果要重新对其赋值的话,就要先把原来那个对象给析构了,
第七行:同样是进入fun后,完成了第三行工作,第八行时完成了第四行的工作,第九行完成了第五行的工作。
(3):作为函数的参数进行值传递的时候
#include<iostream>#include<string>using namespace std;class Student{private: int age; string name;public: /* 构造函数 */ Student(int age, string name) { this->age = age; this->name = name; cout << "构造函数" << endl; } /* 无参构造函数 */ Student() { cout << "无参构造函数" << endl; } /* 拷贝构造函数 */ Student(const Student & r) { age = r.age; name = r.name; cout << "拷贝构造函数" << endl; } /* 析构函数 */ ~Student() { cout << "析构函数" << endl; } void print() { cout << age << endl; }};/*传值函数*/void fun( Student stu){ cout << "函数调用" << endl;}/*传引用函数*/void fun1(Student & stu){ cout << "函数调用" << endl;}int main(){ Student s(10, "Ouyang"); Student s1(10, "Ouyang "); fun(s); //传值函数的调用 fun1(s1); //传引用调用 system("pause"); //程序一直停在这里,所以后续对的析构函数没调用出来 return 0;}
输出结果:
第一行:调用构造函数,创建s对象
第二行:调用构造函数,创建s1对象
第三行:当s对象要传入fun的形参时,会先产生一个临时对象,设定就是stu,然后调用拷贝构造函数,把s对象的值拷贝到stu中。
第四行:此时就是调用函数的过程
第五行:第三行产生的临时对象,在函数调用完成时,就会被析构。
第六行:由于当我们调用函数fun1时,使用的引用传递,所以不需要产生临时对象,只有函数调用
浅拷贝和深拷贝
(1)浅拷贝
当我们使用默认拷贝函数的时,它采取的一个成员一个成员的拷贝,但是当成员涉及使用资源(堆区)时,由于默认拷贝函数只是简单的制作了一个对象对拷贝,而不对它本身进行资源分配和复制,这样就会发生一种现象就是:两个对象同时拥有同一块资源,当对象析构时候,该资源会发生两次归还。
#include<iostream>#include<string>#include<cstring>using namespace std;static int num = 0;class Student{private: int age; char * name;public: /* 构造函数 */ Student(int age, char* name) { int length = strlen(name) + 1; this->age = age; this->name = new char[length]; //申请内存空间 if (this->name != 0) strcpy_s(this->name,length,name); //调用函数对name进行赋值 this->name[length - 1] = '\0'; cout << "构造函数" << endl; } /* 无参构造函数 */ Student() { cout << "无参构造函数" << endl; } /* 析构函数 */ ~Student() { cout << name << endl; name[0] = '\0'; delete name; cout << "析构函数" << endl; } void print() { cout << age << endl; }};int main(){ Student s(10, "Ouyang"); Student s1 = s; //system("pause"); return 0;}
输出结果:
第一行:调用构造函数,创建了对象s
第二行:调用了析构函数,输出了了name,析构对象s1。
第三行:同样是析构函数中的内容,
第四行:打算析构对象s,首先还是先输出name,由于在析构s1时,我们就已经归还了资源,此时输出就是一堆乱码,
最后就是程序想再次归还已经归还的资源,程序就爆出来异常。
浅拷贝可以用下图形象的表示:
(2)深拷贝
深拷贝就是不但复制了对象的空间,也复制了对象的资源,从而不会出现两个对象共用一份资源的现象。
#include<iostream>#include<string>#include<cstring>using namespace std;static int num = 0;class Student{private: int age; char * name;public: /* 构造函数 */ Student(int age, char* name) { int length = strlen(name) + 1; this->age = age; this->name = new char[length]; //申请内存空间 if (this->name != 0) strcpy(this->name, name); //调用函数对name进行赋值 this->name[length - 1] = '\0'; cout << "构造函数" << endl; } /* 无参构造函数 */ Student() { cout << "无参构造函数" << endl; } /* 拷贝构造函数 */ Student(const Student & s) { age = s.age; int length = strlen(s.name) + 1; name = new char(length); if (name != 0) strcpy(name, s.name); name[length - 1] = '\0'; cout << "拷贝构造函数" << endl; } /* 析构函数 */ ~Student() { cout<<name<<endl; name[0]='\0'; if (name!=NULL) delete name; cout << "析构函数" << endl; }};int main(){ Student s(10, "Ouyang"); Student s1 = s; //system("pause"); return 0;}
输出结果:
这次是在codeBlocks中跑出来的结果,因为对vs不是很熟悉,总是会跑出异常,
第一行:创建s时,调用了构造函数
第二行:创建s1时,调用了拷贝构造函数
最后就是分别析构两个对象了,我们可以发现这两个对象的析构是一样,当s1被析构后,它只是归还了其申请的内存,对s对象没有影响,所以有个明文规则:如果类的析构函数会被用来归还对象的申请的资源时,则它也需要一个拷贝构造函数。
拷贝构造函数细节
为啥要用引用:
在函数调用中,具有非引用类型的参数要进行拷贝初始化,这也就解释了为什么拷贝构造函数的参数必须是引用的了,如果不是,那么它就相当于一个普通的函数,需要调用拷贝构造函数来初始化它的非引用的参数,从而就是一个无限循环了。
- c++的堆与拷贝构造函数
- 堆和拷贝构造函数--深拷贝与浅拷贝,用于类型转换的构造函数
- <C++>14.构造函数的重载与拷贝构造函数
- C++中的堆与拷贝构造函数
- 【C++】基础知识—构造函数与拷贝构造函数
- 【C++】拷贝构造函数
- C++:拷贝构造函数
- c++"拷贝构造函数
- C++: 拷贝构造函数
- 【C++】拷贝构造函数
- c++::拷贝构造函数
- 【C++】:拷贝构造函数
- 【C++】拷贝构造函数
- C++:拷贝构造函数
- C 类对象的拷贝构造函数
- [C++] 拷贝构造函数的调用
- (C++)定义类的拷贝构造函数
- [C++] C++的拷贝构造函数浅析
- 使用MediaPlayer播放项目res/raw和res/assets下的音频文件
- hdu5901
- 注解形式控制器简介
- HDU 3911 线段树区间合并+异或操作
- Linux环境执行makeclean: line 3: $'\r': command not found报错
- c++的堆与拷贝构造函数
- Spring 之 xml 文件的校验方式及问题
- AbstractStringBuilder源码学习
- 接口多态机制
- DataTable 详解
- Github 上的 iOS 开源项目
- 注解形式控制器配置(2)
- Linux命令
- hihocoder1383 The Book List 字典树