关于堆的知识
一般情况下c程序会存放在rom或flash中,运行再拷贝到对应的内存中。c++程序中内存分别存放不同的信息,
(1)全局数据区:存放全局变量、常量、静态数据
(2)代码区:存放程序的代码
(3)栈区:存放局部变量、函数的参数、返回数据、返回地址等
(4)堆区(自由存储区):作为其他操作的使用的资源

当我们的程序通过new或者malloc申请到了一些堆内存时,我们就有责任去回收它们,否则会造成内存泄漏,另外c++管理这些内存时很辛苦的一件事情,频繁的申请和释放内存,会产生大量的堆碎块

堆区和栈区的比较
栈区:
栈是向下增长的,存放的是一些局部变量,参数,返回地址、返回值等,栈中分配局部变量空间,(由系统分配)
堆区:
堆是向上增长的,用于分配程序申请的内存区。

区别
堆区和栈区的区别就是在于内存分配的方式上,另外就是内存空间回收方式上,栈区的内存也是系统自动完成的,当函数运行完后,栈上的内容就被释放了。堆中的内容只要程序不去释放,它就一直都在,但是这样会造成内存泄漏。

申请后的反应
栈区:
只要栈区中有大于申请的空间的内存,系统就会为程序提供内存,否则,会发生异常
堆区:
操作系统中有一个记录闲时的内存地址的链表,当系统收到空间申请时,就会去查找那个表,找到第一个空闲空间大于申请空间的结点,然后把该结点从该链表中删除,并将该结点的空间分配给程序。另外,多数系统会在该结点的首地址记录分配的内存的大小,从而可以在delete时,释放正确的内存空间。

申请效率比较
栈区:
系统自动分配,速度快,但是空间有限,WINDOWS下只有2M,
堆区:
由于分配堆区后,还有很多后续工作,如删除结点,记录分配的空间的大小等。所以效率较低,但是空间充足,程序员可以很好的控制它。

需要new和delete的原因
以前看过一个文章写的是“学好c++必须做到的50条”,我很同意里面的一句话:“不要因为c和c++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样;”,这句话正好在这里可以显示其正确性,
c++中不能使用malloc的一个原因就是:它在为指针对象分配空间时,不能够调用构造函数。一个类的对象的建立包括三个部分:

  1. 分配空间
  2. 构造结构
  3. 初始化
    但是上述三个部分统一由构造函数完成的。于是我们就需要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;
}

输出结果:
c++的堆与拷贝构造函数_堆

另外从堆中还可以分配对对象数组,下面给个范例:

#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;
}

输出结果:
c++的堆与拷贝构造函数_c++类的基础知识_02

但是这里有一个需要注意的地方,如果我们自己为类提供了一个有参的构造函数时,但是你又要构建一个对象数组的时候,你就需要手动的为类添加一个无参的构造函数,否则程序将会报错,如下图所示:
c++的堆与拷贝构造函数_拷贝构造函数_03
报错的原因是,我们在声明一个对象的数组时,最后面是跟数组的大小,无法再添加构造函数的参数了,所以它只能够调用无参构造函数了。
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;
}

输出结果:
c++的堆与拷贝构造函数_c++_04

通过上述代码的输出结果,我们可以发现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++的堆与拷贝构造函数_堆_05

我们都知道函数参数的传递有两种方式:

  1. 值传递:会产生实参的副本,将副本传给函数
  2. 引用传递:不产生副本,直接把自己传给函数
    所以需要产生副本,那么就需要调用拷贝构造函数啦。

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;
}

输出结果:
c++的堆与拷贝构造函数_c++类的基础知识_06
注释:
第一行:创建主函数中的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;
}

输出结果:
c++的堆与拷贝构造函数_拷贝构造函数_07
第一行:调用构造函数,创建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;
}

输出结果:
c++的堆与拷贝构造函数_c  类的基础知识_08
第一行:调用构造函数,创建了对象s
第二行:调用了析构函数,输出了了name,析构对象s1。
第三行:同样是析构函数中的内容,
第四行:打算析构对象s,首先还是先输出name,由于在析构s1时,我们就已经归还了资源,此时输出就是一堆乱码,
最后就是程序想再次归还已经归还的资源,程序就爆出来异常。

浅拷贝可以用下图形象的表示:
c++的堆与拷贝构造函数_堆_09

(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;
}

输出结果:
c++的堆与拷贝构造函数_拷贝构造函数_10
这次是在codeBlocks中跑出来的结果,因为对vs不是很熟悉,总是会跑出异常,
第一行:创建s时,调用了构造函数
第二行:创建s1时,调用了拷贝构造函数
最后就是分别析构两个对象了,我们可以发现这两个对象的析构是一样,当s1被析构后,它只是归还了其申请的内存,对s对象没有影响,所以有个明文规则:如果类的析构函数会被用来归还对象的申请的资源时,则它也需要一个拷贝构造函数。

拷贝构造函数细节
为啥要用引用:
在函数调用中,具有非引用类型的参数要进行拷贝初始化,这也就解释了为什么拷贝构造函数的参数必须是引用的了,如果不是,那么它就相当于一个普通的函数,需要调用拷贝构造函数来初始化它的非引用的参数,从而就是一个无限循环了。