Skip to content

1. 构造函数和析构函数

1.1 构造函数

构造函数是一种特殊的函数,用来在对象实例化的时候初始化对象的成员变量。在创建(实例化)一个新对象的时候一定会调用!!!

  • 函数名和类名相同,没有返回类型
  • 构造函数在创建对象时,自动被调用,而不能像普通成员函数一样直接去调用

功能:专门用于对象的初始化工作,在类的对象创建时定义初始状态

特点

  • 构造函数的名字和类名是相同的
  • 构造函数是没有返回值类型的,也不能写 void。可以有形参(也可以重载)
  • 在创建对象的时候,会自动调用。而且是一定会调用,但是只会调用一次,不能通过已有对象手动调用构造函数。
  • 如果一个类中没有显示声明的构造函数,那么编译器会自动生成一个无参构造函数,自动生成的构造函数,函数体为空且没有参数。
  • 但是如果程序员写了至少一个构造函数,那么编译器就不会生成了。
    • 在存在多个构造函数的情况下,编译器会在调用构造函数的时候,根据构造函数的参数自动匹配相应的构造函数进行调用。

构造函数格式

cpp
class 类名
{
    类名(构造形参表)
    {
        //主要负责对象的初始化,即初始化成员变量
    }
};

以学生类为例:

cpp
#include<iostream>
using namespace std;

class Student
{
public:
    /*void */Student(const string name, const int age, const int id) //构造函数不能有返回值
    {
        cout << "构造函数" << endl;
        m_name = name;
        m_age = age;
        m_id = id;
    }
    void who(void)
    {
        cout << "我叫" << m_name << ", 今年" << m_age
             << "岁, 学号是" << m_id << endl;
    }

private:
    string m_name;
    int m_age;
    int m_id;
};

int main()
{
    //创建对象,构造函数将自动被调用
    //(...): 指明构造函数需要的实参
    Student s("贾修", 18, 1);
    //Student s = Student("贾修", 18, 1);//与上面的写法相同
    s.who();
    //Error: 构造函数不能显示调用
    //s.student("贾修", 18, 1);
    return 0;
}

练习:实现一个电子时钟类,要求使用构造函数初始化当前的时间,以秒为单位运行

cpp
#include<iostream>
#include<string>
#include<windows.h>
using namespace std;

class Clock
{
public:
    Clock(const time_t t)
    {
        //C 库函数 time.h
        //struct tm* localtime(const time_t * timer)
        tm* local = localtime(&t);
        m_hour = local->tm_hour;
        m_min = local->tm_min;
        m_sec = local->tm_sec;
    }

    void run(void)
    {
        while (1)
        {
            //打印当前时间
            //计时+1秒:
            cout << m_hour << ":" << m_min << ":" << m_sec << endl;
            m_sec++;
            if (m_sec == 60)
            {
                m_min++;
                m_sec = 0;
            }
            if (m_min == 60)
            {
                m_hour++;
                m_min = 0;
            }
            if (m_hour == 24)
            {
                m_hour = 0;
            }
            //休眠1s windows下头文件windows.h
            Sleep(1000);
            //清屏
            system("cls");
        }
    }

private:
    int m_hour;
    int m_min;
    int m_sec;
};

int main()
{
    Clock c(time(NULL));
    c.run();
    return 0;
}
1.1.1 对象的创建
  • 在栈区创建单个对象
cpp
类名 对象(构造参数);
类名 对象 = 类名(构造参数); //和上面相同
cpp
#include<iostream>
using namespace std;

class Student
{
public:
    Student(const string name, const int age, const int id)
    {
        cout << "构造函数" << endl;
        m_name = name;
        m_age = age;
        m_id = id;
    }
    void who(void)
    {
        cout << "我叫" << m_name << ",今年" << m_age
             << "岁,学号是" << m_id << endl;
    }
private:
    string m_name;
    int m_age;
    int m_id;
};

int main()
{
    Student s = Student("蔡徐坤", 18, 10001);
    //Student s("蔡徐坤", 18, 10001);
    s.who();
    return 0;
}
  • 在栈区创建多个对象(对象数组)
cpp
类名 对象数组[元素个数] = {类名(构造实参表),...}
cpp
//在栈区创建多个对象
Student sarr[3] =
{
    Student("古丽娜扎",20,10002),
    Student("迪丽热巴",22,10003),
    Student("马尔扎哈",25,10004)
};
sarr[0].who();
sarr[1].who();
sarr[2].who();
  • 在堆区创建单个对象
    创建:
cpp
类名 *对象指针 = new 类名(构造实参表);

销毁:

cpp
delete 对象指针;
cpp
//在堆区创建单个对象
Student *ps = new Student("林黛玉",19,10005);
ps->who();//(*ps).who() 间接
delete ps;
ps = NULL;
  • 在堆区创建对象数组
    创建:
cpp
类名 *对象指针 = new 类名[元素个数]{类名(构造实参表),...};

销毁:

cpp
delete[] 对象指针;
cpp
//在堆区创建多个对象
Student* parr = new Student[3]
{
    Student("潘金莲",30,10006),
    Student("唐三娘",35,10007),
    Student("孙三娘",31,10008)
};
parr[0].who();//(parr + 0)->who()
parr[1].who();//(parr + 1)->who()
parr[2].who();//(parr + 2)->who()

delete[] parr;
parr = NULL;
  • malloc和new的区别
    • malloc申请的内存区域返回类型是 void* 类型,需强制转换为对应类型才可使用;而 new 则是自动生成所需要的类型。malloc 申请内存需要手动计算大小,而 new 则自动计算相应的大小
      cpp
      int *p = (int *)malloc(sizeof(int));
      int *p = new int;
    • new 会调用类构造函数为对象初始化,而 malloc 不会。
cpp
#include<iostream>
using namespace std;

class Student
{
public:
    Student(const string name, const int age, const int id)
    {
        cout << "构造函数" << endl;
        m_name = name;
        m_age = age;
        m_id = id;
    }
    void who(void)
    {
        cout << "我叫" << m_name << ",今年" << m_age
             << "岁,学号是" << m_id << endl;
    }
private:
    string m_name;
    int m_age;
    int m_id;
};

int main()
{
    Student* ps = new Student("林黛玉",19, 10005);
    ps->who();//(*ps).who() 间接

    Student* ps1 = (Student*)malloc(sizeof(Student));//不要这样写代码
    ps1->who();
    free(ps1);
    ps1 = NULL;
    delete ps;
    ps = NULL;
    return 0;
}

explicit关键字用于修饰构造函数(一般修饰只有一个参数的构造函数,如果有多个参数,其他参数必须有默认值),它的作用是表示该构造函数的调用必须是“显示的”(明确调用),防止构造函数隐式转换。默认的情况下类的构造函数默认是隐式的,即构造函数可以被隐式调用。

cpp
#include <iostream>

class Int
{
public:
    /*explicit*/ Int(int a)
    {
        std::cout << "构造函数" << std::endl;
        m_a = a;
    }
private:
    int m_a;
};

int main()
{
    Int a = 10; // 隐式转换
    return 0;
}
1.1.2 构造函数参数的缺省

C++中的函数参数支持缺省,即给定默认值。构造函数和普通函数类似,也可以带有缺省参数。可防止出现脏数据。

cpp
#include<iostream>
using namespace std;

class Student
{
public:
    Student(const string name, const int age = 0, const int id = 0)
    {
        m_name = name;
        m_age = age;
        m_id = id;
    }
    void who(void)
    {
        cout << "我叫" << m_name << ", 今年" << m_age
             << "岁, 学号是" << m_id << endl;
    }

private:
    string m_name;
    int m_age;
    int m_id;
};

int main()
{
    Student s("abc", 18, 10001);
    s.who();
    Student s2("def");
    s2.who();

    return 0;
}
1.1.3 构造函数的重载

C++中的函数参数支持重载,即函数名相同,但是参数列表不同,与返回值无关。构造函数和普通函数类似也可以重载。

cpp
#include<iostream>
using namespace std;

class Student
{
public:
    Student(const string name, const int age, const int id)
    {
        cout << "构造函数1" << endl;
        m_name = name;
        m_age = age;
        m_id = id;
    }
    Student(const string name, const int age)
    {
        cout << "构造函数2" << endl;
        m_name = name;
        m_age = age;
    }
    Student(const string name)
    {
        cout << "构造函数3" << endl;
        m_name = name;
    }
    void who(void)
    {
        cout << "我叫" << m_name << ", 今年" << m_age
             << "岁, 学号是" << m_id << endl;
    }

private:
    string m_name = "qq";
    int m_age = 0;
    int m_id = 0;
};

int main()
{
    Student s1("abc", 18, 10001);
    s1.who();
    Student s2("def", 19);
    s2.who();
    Student s3("hij");
    s3.who();

    return 0;
}
1.1.4 缺省(默认)构造函数

类中可以包含其他类类型的成员变量:

cpp
class A
{
public:
    int m_i;
};

class B
{
public:
    int m_j; //基本类型
    A m_a;   //类类型(成员子对象)
    string str; //类类型
};

如果类中没有定义任何构造函数(无参构造函数、含参构造函数),编译器会为该类提供一个缺省(无参构造函数):

  • 对于基本类型的成员变量不能初始化
  • 对于类类型的成员变量,会自动调用相应的无参构造函数来初始化
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(/*int num*/void)
    {
        cout << "A:A(void)" << endl;
        m_i = 1234;
    }
    int m_i;
};

class B
{
public:
    //编译器会为B类提供类似下面的缺省构造函数
    //B(void)行
    int m_j; //基本类型
    A m_a;   //类类型(成员子对象)
    string str;
};

int main()
{
    B b;
    cout << b.m_j << endl;      //未知
    cout << b.m_a.m_i << endl;  //1234
    if (b.str.size())
    {
        printf("NOT NULL\n");
    }
    else
    {
        printf("NULL\n");
    }
    return 0;
}

如果类中自己定义了构造函数,无论是否有参数,编译器都不会再提供缺省的无参构造函数了:

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int num) //已经定义了构造函数。编译器都不会再提供缺省的无参构造函数了
    {
        cout << "A::A(void)" << endl;
        m_i = 1234;
    }
    int m_i;
};

class B
{
public:
    int m_j; //基本类型
    A m_a;   //类类型(成员子对象)
    string str;
};

int main()
{
    B b; // 错误:A类没有无参构造函数
    return 0;
}

1.2 初始化表

除了使用构造函数来初始化类内的成员外,还有一种方式——初始化表:

cpp
class 类名
{
    类名(形参表):成员变量(初值),...{//构造函数体}
};

初始化表方式来初始化类内成员:

cpp
#include<iostream>
using namespace std;

class Student
{
public:
    //在定义成员变量时,同时初始化
    Student(const string name, const int age, const int no)
    :m_name(name), m_age(age), m_no(no) {}

    void who(void)
    {
        cout << m_name << "," << m_age << "," << m_no << endl;
    }

private:
    string m_name;
    int m_age;
    int m_no;
};

int main()
{
    Student s("abc", 18, 001);
    s.who();
    return 0;
}

多数情况下使用初始化表和在构造函数体中赋初值没有太大区别,可以任选一种方式,但是在某些特殊情况下必须要使用初始化表:

  1. 如果有类类型的成员变量,而该类又没有无参构造函数,则必须通过初始化表来初始化该变量
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(const int data)
    {
        cout << "A的构造函数" << endl;
        m_data = data;
    }
    int m_data;
};

class B
{
public:
    B(void) :m_a(1234)
    {
        cout << "B的构造函数" << endl;
    }
    A m_a; //类类型成员变量
};

int main()
{
    B b;
    cout << b.m_a.m_data << endl; //1234
    return 0;
}
  1. 如果类中包含 const引用 型的成员变量,必须要使用初始化表来初始化
cpp
#include<iostream>
using namespace std;

int num = 100;
class A
{
public:
    A(void) :m_c(200), m_r(num) 
    {
        // m_r = num; // 错误:引用必须在初始化时绑定
    }
    const int m_c;
    int& m_r;
};

int main()
{
    A a;
    cout << a.m_c << "," << a.m_r << endl;
    return 0;
}

练习:修改电子时钟类,要求使用初始化表

cpp
#include<iostream>
#include<ctime>
#include<windows.h>
using namespace std;

class Clock {
public:
    Clock(tm* local) : m_hour(local->tm_hour), m_min(local->tm_min),
                       m_sec(local->tm_sec) 
    {
    }
    void run(void)
    {
        while (1)
        {
            //打印当前时间
            cout << m_hour << ":" << m_min << ":" << m_sec << endl;
            //计时+1秒:
            m_sec++;
            if (m_sec == 60)
            {
                m_min++;
                m_sec = 0;
            }
            if (m_min == 60)
            {
                m_hour++;
                m_min = 0;
            }
            if (m_hour == 24)
            {
                m_hour = 0;
            }
            //休眠1s windows下头文件windows.h
            Sleep(1000);
            //清屏
            system("cls");
        }
    }

private:
    int m_hour;
    int m_min;
    int m_sec;
};

int main()
{
    time_t t = time(NULL);
    tm* local = localtime(&t);
    Clock c(local);
    c.run();
    return 0;
}

注意:成员变量的初始化顺序是由声明顺序决定,而与初始化表的顺序无关。不要用一个成员变量去初始化另一个成员变量。

cpp
#include<iostream>
using namespace std;

class A
{
public:
    //A(int a, int b, int c) : m_a(a), m_b(b), m_c(c)
    A(int a, int b, int c) : m_a(a), m_c(m_a), m_b(m_c)
    //如果是按m_a->m_c->m_b顺序, m_a = 1, m_c = 1, m_b = 1
    //如果是按m_a->m_b->m_c顺序, m_a = 1, m_b = 随机值, m_c = 1 //按声明顺序
    {
    }
public:
    int m_a;
    int m_b;
    int m_c;
};

int main()
{
    A a(1, 2, 3);
    cout << a.m_a << ", " << a.m_b << ", " << a.m_c << endl;
    return 0;
}

当类的声明和定义分开时,初始化表应该写在定义的地方(类似于函数的):

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int a, int b, int c) /* : m_a(a), m_b(b), m_c(c)*/;

    void print();

private:
    int m_a;
    int m_b;
    int m_c;
};

A::A(int a, int b, int c) : m_a(a), m_b(b), m_c(c)
{
}

void A::print()
{
    cout << m_a << "," << m_b << ", " << m_c << endl;
}

int main()
{
    A a(1, 2, 3);
    a.print();
    return 0;
}

1.3 析构函数

与构造函数相对应,构造函数是对象创建的时候自动调用的,而析构函数就是对象在销毁的时候自动调用的。与构造函数作用刚好相反,构造函数是用来初始化成员变量,析构函数是用来释放对象占用的资源。

  • 函数名必须是 ~类名
  • 没有返回类型,也没有参数,也无法重载
cpp
class 类名
{
    ~类名(void)
    {
        //负责清理对象创建时的动态资源
    }
};

当对象销毁时,该类的析构函数会自动被执行(不建议手动调用析构):

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(const int data)
    {
        cout << "A的构造函数" << endl;
        m_data = new int(data);
    }
    ~A()
    {
        cout << "A的析构函数" << endl;
        delete m_data;
        m_data = NULL;
    }
private:
    int *m_data;
};

int main()
{
    A a(10);
    return 0;
}
  • 栈对象:当离开作用域时,其析构函数被自动调用
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A()
    {
        cout << "构造函数" << endl;
    }
    ~A()
    {
        cout << "析构函数" << endl;
    }
};

void fun()
{
    cout << "fun()" << endl;
    A a;
}

int main()
{
    cout << "调用fun()之前" << endl;
    fun();
    cout << "调用fun()之后" << endl;
    return 0;
}
  • 堆对象:堆对象的析构函数被 delete 操作符调用
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A()
    {
        cout << "构造函数" << endl;
    }
    ~A()
    {
        cout << "析构函数" << endl;
    }
};

int main()
{
    A* a = new A();
    cout << "delete之前" << endl;
    delete a; //堆区对象需要手动调用delete操作符会调用析构函数
    cout << "delete之后" << endl;
    return 0;
}
  • 如果一个类没有显式定义析构函数,那么系统会为该类提供一个缺省的析构函数:
    • 对于基本类型的成员变量,什么也不做
    • 对于类类型的成员变量,调用相应类的析构
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(void)
    {
        cout << "A::A()" << endl;
    }
    ~A(void)
    {
        cout << "A::~A()" << endl;
    }
};

class B
{
public:
    A m_a; //类类型的成员变量
};

int main()
{
    B b;
    return 0;
}
  • 对象创建和销毁的过程
    1. 创建过程:
      • 分配内存
      • 构造成员子对象(按声明顺序)
      • 执行构造函数代码
    2. 销毁过程:
      • 执行析构函数代码
cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(void)
    {
        cout << "A::A()" << endl;
    }
    ~A(void)
    {
        cout << "A::~A()" << endl;
    }
};

class B
{
public:
    B(void)
    {
        cout << "B::B()" << endl;
    }
    ~B(void)
    {
        cout << "B::~B()" << endl;
    }
};

class C
{
public:
    C(void)
    {
        cout << "C::C()" << endl;
    }
    ~C(void)
    {
        cout << "C::~C()" << endl;
    }
public:
    A m_a; //类类型的成员变量
    B m_b; //类类型的成员变量
};

int main()
{
    C c;
    return 0;
}

1.4 this指针

定义:类中的内部都隐藏一个该类类型的指针参数,名为 this

  • 对于普通的成员函数,this 指向调用该函数的对象
  • 对于构造函数,this 指向正在创建的对象

this 指针是隐式传递给每个非静态成员函数的,它允许成员函数访问对象的成员变量和调用其他成员函数。
this 指针的类型是指向类类型的指针,其类型是类类型名加上一个星号(*)。例如,如果类名为 Student,那么 this 指针的类型就是 Student*
this 指针在成员函数内部是隐式可用的,你不需要显式地声明或传递它。你可以在成员函数中通过 this 指针来访问对象的成员变量和其他成员函数。

cpp
#include<iostream>
using namespace std;

class Student
{
public:
    Student(const string& name, int age)
    :m_name(name), m_age(age)
    {
        cout << "构造函数" << this << endl;
        this->m_age = 18; // 显式使用this指针
    }
    void who(void)
    {
        cout << m_name << ", "<< m_age << endl;
        cout << this->m_name << ", "<< this->m_age << endl; // 显式使用this指针
    }

private:
    string m_name;
    int m_age;
};

int main()
{
    Student stu1("蔡徐坤", 25);
    cout << "&stu1 = " << &stu1 << endl;
    Student stu2("鹿晗", 26);
    cout << "&stu2 = " << &stu2 << endl;
    stu1.who(); // 相当于 Student::who(&stu1);
    stu2.who(); // 相当于 Student::who(&stu2);
    return 0;
}

需要显式使用 this 指针的场景:

  1. 区分作用域
  2. 返回调用对象的自身
  3. 从类的内部销毁对象自身
cpp
#include<iostream>
using namespace std;

class Counter
{
public:
    Counter(int num = 0) //区分作用域
    {
        this->num = num;
    }
    Counter& add(void) //返回调用对象本身
    {
        ++num;
        cout << "num = "<< num << endl;
        //this指向调用对象
        //*this就是调用对象自身
        return *this; //返回自引用
    }
    void destroy(void) //从类的内部销毁对象自身
    {
        cout << "this = "<< this << endl;
        delete this;
    }
    int num;
};

int main()
{
    Counter cn(1);
    cn.add().add().add(); // 链式调用
    cout << cn.num << endl; //4

    Counter* pc = new Counter(123);
    cout << pc->num << endl; //123
    cout << "pc = "<< pc << endl;
    pc->destroy(); // 对象自我销毁
    //delete pc; // 已经释放过了,不需要再释放

    return 0;
}

知识如风,常伴吾身