构造和析构函数

构造函数和析构函数,这两个函数将会被编译器自动调用,构造函数完成对象的初始化动作,析构函数在对象结束的时候完成清理工作。

注意:

对象的初始化和清理工作是编译器强制我们做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认操作,只是这个默认初始化操作不会做任何事。

构造函数

实例化对象的时候系统自动调用。

  1. 函数名和类名相同,
  2. 没有返回类型,
  3. 可以有参数,
  4. 可以重载。
#include <iostream>

class Data {
private:
    int num;
public:
    // 构造函数(无参的构造
    Data(): num(0) {
        std::cout << num << std::endl;
        std::cout << "无参的构造函数" << std::endl;
    }
    //构造函数 有参
    Data(int n): num(n) {
        std::cout << num << std::endl;
        std::cout << "有参的构造函数" << std::endl;
    }

    // 析构函数
    ~Data() {
        std::cout << "析构函数" << std::endl;
    }

};

void test01() {
    // 类实例化对象 系统自动调用构造函数
    Data o1;

//    函数结束之后 局部对象o1 被释放 系统自动调用析构函数

}

int main() {
    using std::cout;
    using std::endl;

    cout << "------001-------" << endl;
    test01();
    cout << "------002-------" << endl;

    return 0;
}

1. 分类

按参数分类

  1. 无参构造函数
  2. 有参构造函数

按类型分类

  1. 普通构造函数
  2. 拷贝构造函数(复制构造函数)

2. 调用

void test02() {
//    调用无参 或默认构造 (隐式调用
    Data ob1;
    // 调用 无参构造 (显示调用
    Data ob2 = Data();

    // 调用有参构造 (隐式调用
    Data ob3(10);
    // 调用有参构造 (显式调用
    Data ob4 = Data(20);

    // 隐式转换 调用有参构造(针对只有一个数据成员
    // 尽量不用!
    Data ob5 = 30;

    // 匿名对象 当前语句结束 立即释放
    Data(49);
    std::cout << "-----" << std::endl;
}

结果:

------001-------
0
无参的构造函数
0
无参的构造函数
10
有参的构造函数
20
有参的构造函数
30
有参的构造函数
49
有参的构造函数
析构函数
-----
析构函数
析构函数
析构函数
析构函数
析构函数
------002-------

析构函数

对象释放的时候自动调用。

  1. 函数名是类名前面加 ~
  2. 没有返回类型
  3. 不能有参数
  4. 不能重载

构造和析构顺序是相反的。

拷贝构造函数

书写

class Data {

public:
// 拷贝构造函数
    Data(const Data &ob) {
        // 拷贝构造 是 ob2 调用
        num = ob.num;
        std::cout << "拷贝构造函数" << std::endl;
    }
};

调用

void test03() {
    Data ob1(10);
    std::cout << "ob1.num = " << ob1.num << std::endl;

    // 隐式调用拷贝构造函数
    Data ob3(ob1);
    // 显式调用拷贝构造函数
    Data ob4 = Data(ob1);
    // 隐式转换调用
    Data ob2 = ob1;
    // 调用拷贝构造函数
    // 如果用户不实现拷贝构造 系统将调用默认的拷贝构造
    // 默认的拷贝构造: 单纯的整体赋值(浅拷贝
    // 如果用户实现 拷贝构造,则系统调用用户实现的拷贝构造
    std::cout << "ob2.num = " << ob2.num << std::endl;

}

旧对象 初始化新对象 才会调用构造函数

Data ob1(10);

Data ob2(ob1); // 拷贝构造
Data ob3 = Data(ob1); // 拷贝构造
Data ob4 = ob1; // 拷贝构造

ob2 = ob1; // 不会调用拷贝构造 
// 单纯对象赋值

构造函数的的调用规则

系统会给任何类提供3个函数成员函数:

默认构造函数(空) 默认析构函数(空) 默认拷贝构造函数(浅拷贝)

  1. 如果用户提供了一个有参构造函数 将屏蔽系统的默认构造函数
  2. 如果用户提供了有参构造 不会屏蔽 系统的默认拷贝构造函数
  3. 如果用户提供了拷贝构造函数 将屏蔽 系统的默认构造函数 默认拷贝构造函数

总结

对于构造函数:用户一般要实现

  1. 无参构造
  2. 有参构造
  3. 拷贝构造
  4. 析构

深拷贝和浅拷贝

浅拷贝

同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种 情况称为浅拷贝。一般情况下,浅拷贝没 有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。

深拷贝

//
// Created by muk on 12/16/20.
//

#include "Person.h"

Person::Person() {
    m_name = nullptr;
    m_num = 0;
    std::cout << "无参构造" << std::endl;
}

Person::Person(char *name, int num) {
    // 为 m_name 申请空间
    m_name = (char *)calloc(1, strlen(name) + 1);
    if (m_name == nullptr) {
        std::cerr << "分配失败" << std::endl;
        m_num = 0;
    } else {
        strcpy(m_name, name);
        m_num = num;
    }

    std::cout << "有参构造" << std::endl;
}

Person::Person(const Person &another_person) {
    if (another_person.m_name != nullptr) {
        m_name = (char *)calloc(1, strlen(another_person.m_name) + 1);
        if (m_name == nullptr) {
            std::cerr << "构造失败" << std::endl;
        }
        strcpy(m_name, another_person.m_name);
    }

    m_num = another_person.m_num;
}

Person::~Person() {
    // 释放空间
    if (m_name != nullptr) {
        free(m_name);
        m_name = nullptr;
    }
}

std::ostream &operator<<(std::ostream &output, Person& person) {
    output << "m_name = " << person.m_name
           << "; m_num = " << person.m_num;

    return output;
}

多个对象构造和析构

初始化列表

构造函数和其他函数不同,除了名字,参数列表,函数体之外还有初始化列表。

class Data {
private:
    int m_a;
    int m_b;
    int m_c;
public:
    // 成员名(形参名)
    Data(int a, int b, int c)
            : m_a(a), m_b(b), m_c(c) {
        std::cout << "构造函数" << std::endl;
    }
    ~Data() {
        std::cout << "析构函数" << std::endl;
    }

private:
    friend std::ostream& operator<<(std::ostream &output, Data & data);
};

std::ostream & operator<<(std::ostream& output, Data &data) {
    output << "m_a = " << data.m_a
           << "; m_b = " << data.m_b
           << "; m_c = " << data.m_c << std::endl;

    return output;
}

初始化成员列表(参数列表)只能在构造函数使用。

类的对象作为另一个类的成员

在类中定义的数据成员一般都是基本的数据类型。

但是类中的成员也可以是对象,叫做对象成员。

C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,C++编译器确保调用类所有子对象的构造函数。

如果所有的子对象有默认构造函数,编译器可以自动调用它们。

C++提供类专门的 语法,构造函数初始化列表。

对象的构造顺序

  1. 对象成员的构造
  2. 自己的构造函数
  3. 析构自己
  4. 析构对象成员

系统默认调用的是 对象成员的无参构造

如果需要调用有参构造,使用初始化成员 调用有参构造。

对象成员的初始化顺序与定义位置有关,与初始化列表顺序无关。

注意

  1. 按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数
  2. 先调用对象成员的构造函数,再调用本身的构造函数。析构函数和构造函数调用顺序相反,先构造,后析构

explicit 关键字

C++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。

Data ob5 = 30; // 隐式转换

explicit 用于修饰构造函数,防止隐式转化。是针对单参数的构造函数(或者除类第一个参数外其余参数都有默认值的多参构造)而言。

动态对象创建

当我们创建数组的时候,总是需要提前预订数组的长度,然后编译器分配预订长度的数组空间,在使用数组的时候,会有这样的问题,数组也许空间太大类,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小就再好不过。

所以动态的意味着不确定性。

在c中提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。然而这些函数在C++中不能很好的运行,氤在它不能帮我们完成对象的初始化工作。

对象的创建

当创建一个c++对象时发生两件事:

  1. 对对象分配内存

  2. 调用构造函数来初始化那块内存,第一步确保第二步一定能发生。

    C++强迫我们这么做是因为使用为初始化的对象是程序出错的重要原因。

malloc 和 free 在 C++的问题:

  1. 程序员必须确定对象的长度
  2. malloc 返回一个void 指针,C++不允许将void 赋值给其他任何指针,必须强转。
  3. malloc 可能申请内存失败,所以必须判断返回值来确保内存分配成功
  4. 用户在使用对象之前必须记住对他初始化,构造函数不能显式调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数

malloc 不能调用构造函数,free 不会调用析构函数

C的动态内存分配函数太复杂,容易令人混淆,是不可接受的,C++中推荐使用运算符 newdelete

new operator

C++中解决动态内存分配的方案是创建一个对象所需要的操作都结合在一个称为 new 的运算符里。

当用 new 创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。

C++中解决动态内存的方案是把创建一个对象所需要的操作都结合在一个称为 new 的运算符里。当用 new 创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。

  1. new 给基本 类型申请空间

    void test01() {
        // 基本类型
        int *p = nullptr;
        p = new int;
        *p = 100;
        std::cout << "p = " << *p << std::endl;
        delete p;
    }
  2. 用于数组的 new 和 delete

    void test02() {
        // 申请 int 数组
        int *arr = nullptr;
        // arr = (int *)calloc(5, sizeof(int));
        // new b表示申请空间
        arr = new int[5]; // 初始化取决于编译器
        for (int i = 0; i<5; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    
        delete [] arr;
    }

    释放数组加 []

    void test03() {
        // 初始化
    //    char *arr = new char[32]{"hehe"};  //  错误
        char *arr = new char[32]{'h', 'e', 'l', 'l', 'o'};
    
        std::cout << arr << std::endl;
        delete [] arr;
    }

    注意:

    1. new 没有加[] delete 释放的时候 就不加
    2. new 加[] delete 释放的时候加 []
  3. new delete 给类的对象申请空间

    class Person {
    private:
        char m_name[32];
        int m_num;
    
    public:
        Person(char *name, int num) {
            strcpy(m_name, name);
            m_num = num;
            std::cout << "构造函数" << std::endl;
        }
    
        void showPerson() {
            std::cout << "m_name = " << m_name << std::endl
                      << "m_num = " << m_num << std::endl;
        }
    
        ~Person() {
            std::cout << "析构函数" << std::endl;
        }
    };
    
    void test04() {
        // new 按照Person申请空间,如果申请成功,自动调用构造函数。
        auto *p = new Person("name", 1);
    
        p->showPerson();
    
        // 先调用析构函数 在释放堆区空间
        delete p;
    }
    // 堆区实例话对象
  4. 对象数组

    本质是数组,每个元素是对象

    void test05() {
        // 自动调用无参的构造函数
        // 后自动调用析构函数
        Person arr[5];
    }

    如果像让对象数组中的元素调用有参构造,必须人为地使用有参构造初始化

    void test06() {
        Person arr[5] = {
                Person("lucy", 5),
                Person("hello", 6)
        };
    }

    用 new 申请对象数组

    void test07() {
        Person *arr = nullptr;
        arr = new Person[5];
    
        delete [] arr;
    
        // 第二种方式
        // 初始化的元素 调用有参构造 没有初始化 的调用无参构造
        Person *arr2= new Person[5]{
            Person("lucy", 18),
            Person("hello", 12)
        };
    
        (*(arr2+0)).showPerson();
        arr2[0].showPerson();
        (arr2+1)->showPerson();
    
        delete [] arr2;
    }
  5. 尽量不用 delete 释放 void*

  6. malloc free 和new delete不能混搭使用

静态成员

static 修饰的成员

成员: 成员变量 成员函数

static 修饰成员变量 修饰成员函数

static

不管类创建类多少对象,静态成员只有一份拷贝。这个拷贝被所有属于这个类的对象共享。

static成员

静态成员属于类而不是对象。

静态变量,是在编译阶段分配空间,对象还没创建时,就已经分配空间

静态变量必须在类中声明,在类外定义。

静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。

class Data {
public:
    int num; // 普通成员变量
    static int data; // 类内声明 静态成员变量
};

int Data::data = 100;

static 修饰静态成员函数

(数据应该保证私有)

class Data {
    private:
    static int data;
    public:
    static int getData();
}

注意

  1. 静态函数目的,操作静态成员数据
  2. 静态成员函数不能访问非静态数据
  3. 普通成员函数可以操作静态成员数据和非静态成员数据
  4. 静态成员变量和静态成员函数都有权限之分

const 静态成员属性

如果一个类的成员,既要实现共享(static),又要实现不可改变(const),那就用 static const 修饰。

定义静态 const 数据成员时,最好在类内部初始化

静态成员案例

案例1:静态成员 统计类的实例化对象的个数

#include <iostream>

using std::cout;
using std::endl;

class Data{
public:
    Data() {
        ++count;
        cout << "无参构造" << endl;
    }

    Data(const Data& data) {
        ++count;
        cout << "拷贝构造" << endl;
    }

    ~Data(){
        --count;
        cout << "析构函数" << endl;
    }

public:
    static int getCount() {
        return count;
    }

private:
    static int count;
};

// 类外定义
int Data::count = 0;

int main() {
    Data ob1;
    Data ob2;
    {
        Data ob3;
        Data ob4;
        cout << "对象的个数 " << Data::getCount() << endl;
    }
    cout << "对象的个数 " << Data::getCount() << endl;

    return 0;
}

案例2: 单例模式(重要)

单例模式是一种常用的软件设计模式。

在它的核心结构中只包含一个被称为单例的特殊类。

通过单例模式可以保证系统中的一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。

如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

Singleton (单例)

在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它唯一实例;

为了防止在外部对其实例化,将其默认构造函数和拷贝构造函数设计为私有;

在单例类内部定义一个 Singleton 类型的静态队形,作为外部共享的唯一实例。

实现

用单例模式模拟一个打印机。

  1. 在单例类内部定义了一个 Singleton 类型的静态变量,作为外部共享的唯一实例
  2. 提供一个静态的方法,让客户可以访问它的唯一实例
  3. 为类防止在外部实例化其他对象,将其默认构造函数和拷贝构造函数设计为私有
#include <iostream>

using std::cout;
using std::endl;

class Printer {
public:
    // 2 提供一个方法来访问单例指针
    static Printer* getSinglePrint() {
        return singlePrint;
    }

    // 5. 设置功能函数
    void printText(char *str) {
        ++count;
        cout << "打印: " << str << endl;
    }

    int getCount() {
        return count;
    }
private:
    // 1. 定义一个静态的 对象指针变量 保存唯一个的实例
    static Printer *singlePrint;

    int count{0};

    // 3. 防止在外部实例化其他对象
    // 4. 将所有构造函数私有
private:
    Printer() = default;
    Printer(const Printer &ob) = default;
};

Printer *Printer::singlePrint = new Printer;

int main() {
    auto *p1 = Printer::getSinglePrint();
    p1->printText("入职报告1");
    p1->printText("入职报告2");
    p1->printText("离职声明");

    auto *p2 = Printer::getSinglePrint();
    p2->printText("入职报告1");
    p2->printText("入职报告2");
    p2->printText("离职声明");

    cout << "打印任务数量为: " << p2->getCount() << endl;

    return 0;
}

this 指针(重要)

C++的封装性:将数据 和 方法封装在一起。

  1. 数据和方法是分开存储的
  2. 每个对象 拥有独立的数据
  3. 每个对象 共享同一个方法

(数据是独立的,方法是共享的)

(数据是对象独有 方法是对共享)

this 指针的引入

当一个对象调用方法时,就会在方法中产生一个 this 指针,this 指向所调用的方法的对象

哪个的对象调用方法,那么方法中的 this 就指向哪个对象

注意

  1. this 指针是隐藏在对象成员函数内的一种指针

  2. 成员函数通过 this 指针即可知道操作的是哪个对象的数据

  3. 静态成员函数内部没有 this 指针,静态成员函数不能操作非静态成员变量

    (静态成员函数 是属于类,其函数内部没有 this 指针。this 是针对对象实例来说的。)

this 指针的使用

1. 当形参和成员变量同名时,可用this指针来区分

class Data {
public:
    int num;
    void setNum(int num) {
        this->num = num;
    }
};

2. 在类的普通成员函数中返回对象本身(*this)

#include <iostream>

class MyCout {
public:
    MyCout& myCout(char *str) {
        std::cout << str;
        return *this;
    }
};

int main() {
    MyCout ob;
    // 链式
    ob.myCout("你好").myCout(",").myCout("\n").myCout("哈哈");
    return 0;
}