第六节 day04_C++学习内容


目录

1.深拷贝和浅拷贝

2.初始化列表

3.对象作为成员

4.动态对象创建

5.new operator

6.delete operator

7.对象数组的创建与使用

8.谨慎使用delete *void


内容

#include 
using namespace std;
#include
#include

/****************************************************************************************************
 * 30.深拷贝与浅拷贝:
 *     ① 浅拷贝:同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,且两个对象仍然相互独立,但是两个对象的内
 *     容存储是同一个,也就是说两个对象的地址都指向堆区的某同一位置。
 *        一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放
 *     的处理,会导致内存问题:
 *        因为两个对象的存储是同一个,所以第一个对象释放完空间,第二个对象没有什么可以释放了,虽然不会报错,但这样做不
 *     合理。
 *     ② 深拷贝:同一类型的两个对象不仅相互复制了表象的值,同时也为被复制的对象开辟了自己的存储空间。此时,当类中有指
 *     针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,释放的时
 *     候也就互不干扰。
 *        深拷贝使用:自定义拷贝构造函数,并在拷贝构造内为拷贝对象申请空间,完成深拷贝动作。
 *     ③ 如果没有自定义拷贝构造函数,系统会自动调用C++提供的默认拷贝构造函数(单纯的值拷贝),完成浅拷贝。
 ****************************************************************************************************/
class Person05{
public:
    // 无参构造
    Person05(){
        pName = NULL;
        mAge = 0;
        cout << "无参构造函数,输出:姓名——" << pName << ",年龄——" << mAge << endl;
    }
    // 有参构造
    Person05(char* name, int age){
        // 为参数name申请存储空间,+1是因为统计字符串长度时不包括“\0”这个结束符,但是我们要为它分配存储空间
        pName = (char*)malloc(strlen(name) + 1);
        // 如果空间分配失败,则输出“构造失败!”。
        if(pName == NULL){ cout << "构造失败" << endl; }
        strcpy(pName, name);
        cout << "空间申请成功,大小为:" << strlen(name)+1 << endl;
        mAge = age;
        cout << "有参构造函数,输出:姓名——" << pName << ",年龄——" << mAge << endl;
    }
    //拷贝构造函数
    Person05(const Person05& person){
        pName = (char*)malloc(strlen(person.pName) + 1);    // 在拷贝构造内为拷贝对象申请空间,完成深拷贝动作
        // pName = (char*)calloc(1, strlen(person.pName) + 1);    // 此句和上面一句作用相同,1是表示只需要一块空间
        // 如果空间分配失败,则输出“构造失败!”。
        if(pName == NULL){ cout << "构造失败" << endl; }
        strcpy(pName, person.pName);
        cout << "空间申请成功,大小为:" << strlen(pName)+1 << endl;
        mAge = person.mAge;
        cout << "拷贝构造函数,输出:姓名——" << pName << ",年龄——" << mAge << endl;
    }
    ~Person05(){
        if (pName != NULL){
            free(pName);
            cout << "空间已被释放!" << endl;
        }
        cout << "析构函数被调用~"<< endl;
    }
private:
    char* pName;
    int mAge;
};

void test26(){
    char name[7] = "Edward";
    Person05 p1(name,30);    // 调用有参构造

    //用对象p1初始化对象p2,
    Person05 p2 = p1;    // 问题是多次释放同一空间
}

/****************************************************************************************************
 * 31.初始化列表:
 *     构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。初始化列表简单使用:
 *     注意:初始化成员列表(参数列表)只能在构造函数使用。
 ****************************************************************************************************/
class Person06{
public:
    #if 0
    //传统方式初始化
    Person06(int a,int b,int c){
        mA = a;
        mB = b;
        mC = c;
    }
    #endif
    /**自定义有参构造函数**
    Person06(int a, int b, int c)
    {
        mA = a;
        mB = b;
        mC = c;
        cout << "有参构造函数!" << endl;
    }*/
    //初始化列表方式初始化,上面这个有参构造函数的功能和下面初始化类别的有参构造函数功能一模一样
    Person06(int a, int b, int c):mA(a),mB(b),mC(c){cout << "有参构造函数!" << endl;}

    void PrintPerson(){
        cout << "mA:" << mA << endl;
        cout << "mB:" << mB << endl;
        cout << "mC:" << mC << endl;
    }
    // 注意:没有指针就不会指向堆区空间,没有指向堆区空间就不会涉及到空间释放的概念,所以我们不需要有析构函数
private:
    int mA;
    int mB;
    int mC;
};

void test27()
{
    Person06 pp(10, 20, 30);
    pp.PrintPerson();
}

/****************************************************************************************************
 * 32.类的对象作为成员:在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员。
 *     ① 废话:C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,C++编译器必须确保调用了所有子对象的构造函数。
 * 如果所有的子对象没有自定义的构造函数,编译器会自动调用默认构造函数。但是如果子对象定义了多个构造函数,且想指定调用某个
 * 构造函数怎么办? 那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们没办法
 * 访问并完成初始化。
 *     ② 解决办法:对于子类调用构造函数,C++为此提供了专门的语法,即构造函数初始化列表。
 *     ③ 废话:当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数,对这些
 * 对象初始化,最后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数。析构函数和构造函数调用顺
 * 序相反,先构造,后析构。
 *     ④ 注意:如下,目前两个成员对象的成员变量都是私有的,我们使用初始化类别就可以完成成员对象的初始化和构造函数的调用,
 * 但是由于成员对象的成员变量时私有的,我们在Person07对象中依然不能访问。所以我们下面注释的几句:
 *         void GoWorkByCar(){ cout << mName << "开着" << mCar.carName << "去上班!" << endl; }
 *         void GoWorkByTractor(){cout << mName << "开着" << mTractor.mName << "去上班!" << endl;}
 *         person.GoWorkByCar();
 *         person.GoWorkByTractor();
 *     依然只有成员对象的成员变量变成共有才可以执行。
 ****************************************************************************************************/
//汽车类
class Car{
public:
    Car(){
        cout << "Car 默认构造函数!" << endl;
        mName = "大众汽车";
    }

    Car(string name){
        cout << "Car 带参数构造函数!" << endl;
        mName = name;
    }
    ~Car(){ cout << "Car 析构函数!" << endl;}
private:
    string mName;
};
//拖拉机
class Tractor{
public:
    Tractor(){
        cout << "Tractor 默认构造函数!" << endl;
        mName = "爬土坡专用拖拉机";
    }
    Tractor(string name){
        cout << "Tractor 带参数构造函数!" << endl;
        mName = name;
    }
    ~Tractor(){
        cout << "Tractor 析构函数!" << endl;
    }
private:
    string mName;
};
//人类
class Person07{
private:
    string mName;
    Car mCar;    // 对象成员
    Tractor mTractor;    // 对象成员
public:

    //类 mCar 不存在合适的构造函数
    Person07(string name){
        mName = name;
    }

    //初始化列表,显示调用成员对象的构造函数:成员对象名(参数)
    //mCar(carName), mTractor(tracName), mName(name)的顺序允许和string carName,
    // string tracName, string name的顺序不一致,但是我们最好写一致
    Person07(string carName, string tracName, string name) :
        mName(name), mCar(carName), mTractor(tracName) {
        cout << "Person 构造函数!" << endl;
        cout << mName << "开着" << carName << "去上班!" << endl;
        cout << mName << "开着" << tracName << "去上班!" << endl;
    }
//    void GoWorkByCar(){ cout << mName << "开着" << mCar.carName << "去上班!" << endl; }
//    void GoWorkByTractor(){cout << mName << "开着" << mTractor.mName << "去上班!" << endl;}
    ~Person07(){
        cout << "Person 析构函数!" << endl;
    }
};

void test28()
{
    Person07 person("刘能");    // 系统默认调用的是成员对象的无参构造函数
//    Person07 person("宝马", "东风拖拉机", "赵四");    // 使用初始化列表,显示调用成员对象的构造函数
//    person.GoWorkByCar();
//    person.GoWorkByTractor();
}

/****************************************************************************************************
 * 33.动态对象创建:当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,
 * 会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
 *     为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然C早就提供了动态内存分配(dynamic
 * memory allocation)函数 malloc 和 free ,可以在运行时从堆中分配和释放存储单元。然而这些函数在C++中不能很好的运
 * 行,因为它不能帮我们完成对象的初始化工作。
 *     当创建一个C++对象时会发生两件事:
 *         ① 为对象分配内存;
 *         ② 调用构造函数来初始化那块内存;
 *     第一步我们能保证实现,需要我们确保第二步一定能发生。C++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重
 * 要原因。为了在运行时动态分配内存,C在他的标准库中提供了一些函数 malloc 以及它的变种 calloc 和 realloc 用于动态分
 * 配内存空间,和 free 函数用于释放内存空间,这些函数是有效的、但是原始的,需要程序员理解和小心使用,如下问题:
 *         ① 程序员必须确定对象的长度;
 *         ② m/c/re-alloc 返回一个 void 指针,C++不允许将 void 赋值给其他任何指针,必须强转;
 *         ③ m/c/re-alloc 可能申请内存失败,所以必须判断返回值,来确保内存分配成功;
 *         ④ m/c/re-alloc 不会调用构造函数,free不会调用析构函数;
 *         ④ 用户在使用对象之前必须要对它进行初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘
 * 记调用初始化函数。
 *     C的动态内存分配函数太复杂,容易令人混淆,是不可接受的,如下,为了使用C的动态内存分配函数,在堆上创建一个类的实例,
 * 我们必须这样做:(C语言代码)
 ****************************************************************************************************/
class Person{
public:
    int mAge;
    char* pName;
public:
    Person(){
        mAge = 20;
        pName = (char*)malloc(strlen("john")+1);
        strcpy(pName, "john");
    }
    void Init(){
        mAge = 20;
        pName = (char*)malloc(strlen("john")+1);
        strcpy(pName, "john");
    }
    void Clean(){
        if (pName != NULL){
            free(pName);
        }
    }
};
void test29(){
    //分配内存
    Person* person = (Person*)malloc(sizeof(Person));
    if(person == NULL){ cout << "空间分配失败"<< endl; }
    //调用初始化函数
    person->Init();
    //清理对象
    person->Clean();
    //释放 person 对象
    free(person);
}

/****************************************************************************************************
 * 为解决上述(从堆区申请空间,并释放不方便的)问题:C++中我们推荐使用运算符 new 和 delete。
 *
 * 34.new operator:C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用 new
 * 创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。如下:
 *         Person* person = new Person;    // C++代码
 *     相当于test29()中的这三句:
 *         Person* person = (Person*)malloc(sizeof(Person));
 *         if(person == NULL){ return 0;}
 *         person->Init();    // 构造函数
 *     New 操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。并且,它带有内置的长度计
 * 算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。
 *
 * 35.delete operator:该表达式先调用析构函数,然后释放内存。
 *     ① 正如 new 表达式返回一个指向对象的指针一样,delete 需要一个对象的地址。
 *     ② delete 只适用于由 new 创建的对象。
 *     ③ 如果使用一个由 malloc 或者 calloc 或者 realloc 创建的对象使用 delete(这个行为是未定义的),很可能出现
 * 没有调用析构函数就释放了内存的情况。因为大多数 new 和 delete 的实现机制都使用了 malloc 和 free。
 *     ④ 如果正在删除的对象的指针是 NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为 NULL,以免对它删除
 * 两次,因为对一些对象删除两次,可能会产生某些问题。
 *     ⑤ 注意:如果在 new 表达式中使用[],必须在相应的 delete 表达式中也使用[](否则的话,可能到时内存释放不完全,
 * 即释放完第一个值或前几个值系统就不会自动释放同数组中的其他变量了);如果在 new 表达式中不使用[], 一定不要在相应的
 * delete 表达式中使用[]。
 *
 * 36.当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造
 * 函数。
 *     ① 定义对象数组的时候,系统会自动给数组中的每个元素调用构造函数。
 *     ② 人为调用有参构造函数,初始化部分自动调用有参构造函数,未初始化部分自动调用无参构造函数;
 *
 * 37.delete void*可能会出错:
 *     ① void*:万能指针,可以保存任何类型的指针;
 *     ② 如果对一个 void* 指针执行 delete 操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将
 * 不执行析构函数(因为void类型的对象没有析构函数,所以delete也就无法寻访到相应的析构函数),未对内存进行释放,导致可
 * 用内存减少:
 ****************************************************************************************************/
// 基本类型的new和delete操作
void test30()
{
    /****** 给基本int型数据申请空间 ******/
    int *p = NULL;

    // 1.以前申请和释放空间的操作是
//    p = (int *)calloc(1, sizeof (int));    // 申请空间
//    cout << "*p = " << *p << endl;
//    free(p);    // 释放空间

    // 2.现在我们可以使用new和delete申请和释放空间
    // p = new int(100);    // 申请空间大小为100,这一句相当于下面两句
    p = new int;
    *p = 100;
    cout << "*p = " << *p << endl;
    delete p;

    /****** 使用 new 和 delete 在堆上创建数组非常容易。******/
    // 1.以前申请空间的方法
    int *arr1 = NULL;
    arr1 = (int *)calloc(5, sizeof (int));
    free(arr1);

    // 2.使用new和delete申请堆区空间
    int *arr2 = NULL;
    arr2 = new int[5];    // 注意:申请空间的时候,内容没有初始化,所以值是随机值
    delete [] arr2;

//    char* pStr1 = new char[100]{"hehe"};    //创建字符数组并初始化,该方法的初始化有局限性,报错
    //error: invalid conversion from 'const char*' to 'char' [-fpermissive]
    char* pStr2 = new char[100]{'h', 'e', 'h', 'e'};    //创建字符数组并初始化
    char* pStr3 = new char[100];    //创建字符数组,之后需要的时候在进行赋值:strcpy(pStr3, "hehe")
    int* pArr1 = new int[100];    //创建整型数组
    int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };    //创建整型数组并初始化(存在局限性)
    //释放数组内存: delete [] 数组名;
//    delete [] pStr1;
    delete [] pStr2;
    delete [] pStr3;
    delete [] pArr1;
    delete [] pArr2;
}

/****** 使用 new 和 delete 在堆上给对象申请和释放空间 ******/
class Person08{
public:
    Person08(){
        cout << "无参构造函数!" << endl;
        pName = (char*)malloc(strlen("undefined") + 1);
        strcpy(pName, "undefined");
        mAge = 0;
    }
    Person08(char* name, int age)
    {
        cout << "有参构造函数!" << endl;
        pName = (char*)malloc(strlen(name) + 1);
        strcpy(pName, name);
        mAge = age;
    }
    void ShowPerson08(){ cout << "Name:" << pName << " Age:" << mAge << endl;}
    ~Person08(){
        cout << "析构函数!" << endl;
        if (pName != NULL)
        {
            delete pName;
            pName = NULL;
        }
    }
private:
    char* pName;
    int mAge;
};

void test31(){
    /****** 34&35.new和delete定义一般的单个指针对象 ******/
    Person08* person1 = new Person08;
    Person08* person2 = new Person08("John", 33);
    person1->ShowPerson08();    // 因为person1是地址,所以调用函数使用的是->而不是.(普通对象使用.)
    person2->ShowPerson08();
    delete person1;
    delete person2;

    /****** 36.对象数组:本质是数组,只是数组的每个元素是对象而已。 ******/
    // 1. 定义对象数组的时候,系统会自动给数组中的每个元素调用构造函数。
//    Person08 arr[5];    // 自动调用5次无参构造函数。
    // 2. 人为调用有参构造函数,初始化部分自动调用有参构造函数,未初始化部分自动调用无参构造函数
    //栈聚合初始化:
    Person08 person[5] = { Person08("John", 20), Person08("Smith", 22) };
    person[0].ShowPerson08();
    (*(person+0)).ShowPerson08();    // 此句执行效果和上面一句一样,(person+0)是地址,(*(person+0))就是普通变量
    (person+0)->ShowPerson08();    // 此句作用和上面一句一样
    person[1].ShowPerson08();
    (*(person+1)).ShowPerson08();    // 此句执行效果和上面一句一样
    //创建堆上对象数组必须提供构造函数
    Person08* workers = new Person08[3];

    /****** 37. delete void* ******/
//    void* person3 = new Person08("John",20);    // void*是万能指针,可以保存任何类型的指针
//    delete person3;
}


// 主函数
int main()
{
    //test26();
    //test27();
    //test28();
    //test29();
//    test30();
//    test31();

    return 0;
}