关于堆的知识
一般情况下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对象没有影响,所以有个明文规则:如果类的析构函数会被用来归还对象的申请的资源时,则它也需要一个拷贝构造函数。
拷贝构造函数细节
为啥要用引用:
在函数调用中,具有非引用类型的参数要进行拷贝初始化,这也就解释了为什么拷贝构造函数的参数必须是引用的了,如果不是,那么它就相当于一个普通的函数,需要调用拷贝构造函数来初始化它的非引用的参数,从而就是一个无限循环了。