Skip to content

接下来了解一下构造函数。

一、构造函数(Constructor)

1. 什么是构造函数

在C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。

2. 定义一个构造函数

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。例如:

cpp
#include <iostream>

using namespace std;

class student_t {
  private: // 私有的
    const char *m_name;
    int         m_age;
    float       m_score;

  public: // 共有的
    // 声明构造函数
    student_t(const char *name, int age, float score);
    // 声明普通成员函数
    void show();
};

// 定义构造函数,
student_t::student_t(const char *name, int age, float score)
{
    m_name = name;
    m_age = age;
    m_score = score;
}
void student_t::show()
{
    cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
}

int main()
{
    // 创建对象时向构造函数传参
    student_t stu("小明", 15, 92.5f);
    stu.show();

    // 创建对象时向构造函数传参
    student_t *pstu = new student_t("李华", 16, 96);
    pstu->show();

    return 0;
}

构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着:

  • 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许;
  • 函数体中不能有 return 语句。

3. 构造函数重载

和普通成员函数一样,构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。

构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。

对于上面的实例,如果写作student_t stu或者new student_t就是错误的,因为类中包含了构造函数,而创建对象时却没有调用。我们可以修改实例,添加一个构造函数:

cpp
#include <iostream>

using namespace std;

class student_t {
  private: // 私有的
    const char *m_name;
    int         m_age;
    float       m_score;

  public: // 共有的
    // 声明构造函数
    student_t();
    student_t(const char *name, int age, float score);
    // 声明普通成员函数
    void setname(const char *name);
    void setage(int age);
    void setscore(float score);
    void show();
};

/*定义构造函数*/
student_t::student_t()
{
    m_name = NULL;
    m_age = 0;
    m_score = 0;
}
student_t::student_t(const char *name, int age, float score)
{
    m_name = name;
    m_age = age;
    m_score = score;
}

// 成员函数的定义
void student_t::setname(const char *name)
{
    m_name = name;
}
void student_t::setage(int age)
{
    m_age = age;
}
void student_t::setscore(float score)
{
    m_score = score;
}

void student_t::show()
{
    if (m_name == NULL || m_age <= 0)
    {
        cout << "成员变量还未初始化" << endl;
    }
    else
    {
        cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
    }
}

int main()
{
    // 调用构造函数 Student(char *, int, float)
    student_t stu("小明", 15, 92.5f);
    stu.show();
    // 调用构造函数 Student()
    student_t *pstu = new student_t();//这里也可以不要()
    pstu->show();
    pstu->setname("李华");
    pstu->setage(16);
    pstu->setscore(96);
    pstu->show();
    delete pstu; // 释放内存
    pstu = NULL; // 指针置空
    return 0;
}

构造函数student_t(char *, int, float)为各个成员变量赋值,构造函数student_t()将各个成员变量的值设置为空,它们是重载关系。根据student_t()创建对象时不会赋予成员变量有效值,所以还要调用成员函数 setname()、setage()、setscore() 来给它们重新赋值。

4. 默认构造函数

如果用户自己没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。比如上面的 Student 类,默认生成的构造函数如下:

cpp
student_t(){}

一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。在上面的第一个实例中,student_t 类已经有了一个构造函数student_t(char *, int, float),也就是我们自己定义的,编译器不会再额外添加构造函数student_t(),在第二个实例中我们才手动添加了该构造函数。

实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。

最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于第二个实例的代码,在栈上创建对象可以写作student_t stu()student_t stu,在堆上创建对象可以写作student_t *pstu = new student_t()student_t *pstu = new student_t,它们都会调用构造函数 student_t()。

以前很多时候就是这样做的,创建对象时都没有写括号,其实是调用了默认的构造函数。

5. 初始化列表

5.1 怎么使用

构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表。但是使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。

cpp
class student_t {
  private: // 私有的
    const char *m_name;
    int         m_age;
    float       m_score;

  public: // 共有的
    // 声明构造函数
    student_t(const char *name, int age, float score);
    // 声明普通成员函数
    void show();
};

/*定义构造函数*/
student_t::student_t(const char *name, int age, float score) : m_name(name), m_age(age), m_score(score)
{
    // TODO:
}

定义构造函数时并没有在函数体中对成员变量一一赋值,其函数体为空(当然也可以有其他语句),而是在函数首部与函数体之间添加了一个冒号:,后面紧跟m_name(name), m_age(age), m_score(score)语句,这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;语句,也是赋值的意思。

初始化列表可以用于全部成员变量,也可以只用于部分成员变量。下面的示例只对 m_name 使用初始化列表,其他成员变量还是一一赋值:

cpp
student_t::Student(char *name, int age, float score): m_name(name){
    m_age = age;
    m_score = score;
}

5.2 列表中变量顺序

注意,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。请看代码:

cpp
#include <iostream>

using namespace std;

class student_t {
  private: // 私有的
    int m_a;
    int m_b;

  public: // 共有的
    // 声明构造函数
    student_t(int b);
    void show();
};

/*定义构造函数
    (1)构造函数必须是 public 属性的,必须与类名相同, 否则创建对象时无法调用。
    (2)没有变量来接收返回值,即使有也毫无用处, 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是
   void也不允许;函数体中不能有 return 语句。
    (3)如果用户自己没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作
       student_t::student_t(){}
    (4)成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
*/
student_t::student_t(int b) : m_b(b), m_a(m_b)
{
    // TODO:
}
/* 我们将 m_b 放在了 m_a 的前面,看起来是先给 m_b 赋值,再给 m_a 赋值,其实不是
   成员变量的赋值顺序由它们在类中的声明顺序决定,在 student_t 类中,我们先声明的 m_a,再声明的
   m_b,所以构造函数和下面的代码等价: student_t::student_t(int b) : m_b(b), m_a(m_b)
    {
        m_a = m_b;
        m_b = b;
    }
*/
void student_t::show()
{
    cout << "m_a=" << m_a << ", m_b=" << m_b << endl;
}

int main()
{
    // 调用构造函数 student_t()
    student_t *pstu = new student_t(100);
    pstu->show();

    delete pstu; // 释放内存
    pstu = NULL; // 指针置空
    return 0;
}

运行会得到如下结果:

bash
m_a=2130567168, m_b=100 
# 或者
m_a=0, m_b=100

其实m_a的值是随机的,也不一定是上面的值,来分析一下为什么,在初始化列表中,我们将 m_b 放在了 m_a 的前面,看起来是先给 m_b 赋值,再给 m_a 赋值,其实不然!成员变量的赋值顺序由它们在类中的声明顺序决定,在 student_t 类中,我们先声明的 m_a,再声明的 m_b,所以构造函数和下面的代码等价:

cpp
student_t::student_t(int b): m_b(b), m_a(m_b){
    m_a = m_b;
    m_b = b;
}

给 m_a 赋值时,m_b 还未被初始化,它的值是不确定的,所以输出的 m_a 的值是一个奇怪的数字;给 m_a 赋值完成后才给 m_b 赋值,此时 m_b 的值才是 100。

对象在栈上分配内存,成员变量的初始值是不确定的。

5.3 初始化const变量

构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const 成员变量的唯一方法就是使用初始化列表。例如 VS/VC 不支持变长数组(数组长度不能是变量),我们自己定义了一个 array_t 类,用于模拟变长数组,请看下面的代码:

cpp
class VLA{
  private:
    const int m_len;
    int *m_arr;
  public:
    VLA(int len);
};
//必须使用初始化列表来初始化 m_len
VLA::VLA(int len): m_len(len){
    m_arr = new int[len];
}

array_t 类包含了两个成员变量,m_len 和 m_arr 指针,需要注意的是 m_len 加了 const 修饰,只能使用初始化列表的方式赋值,如果写作下面的形式是错误的:

cpp
class array_t{
  private:
    const int m_len;
    int *m_arr;
  public:
    VLA(int len);
};
array_t::array_t(int len){
    m_len = len;
    m_arr = new int[len];
}

二、析构函数

1. 什么是析构函数

创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。

析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。

Tips:注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。

2. 定义一个析构函数

前面我们定义了一个 array_t 类来模拟变长数组,它使用一个构造函数为数组分配内存,这些内存在数组被销毁后不会自动释放,所以非常有必要再添加一个析构函数,专门用来释放已经分配的内存。

cpp
class array_t {
  private:
    const int m_len; // 数组长度
    int      *m_arr; // 数组指针
    int      *m_p;   // 指向数组第i个元素的指针
  public:
    // 构造函数和析构函数
    array_t(int len); // 构造函数
    ~array_t();       // 析构函数
    // 成员函数
    void input(); // 从控制台输入数组元素
    void show();  // 显示数组元素
  private:
    int *at(int i); // 获取第i个元素的指针
};
// 构造函数
array_t::array_t(int len) : m_len(len)
{
    // 使用初始化列表来给 m_len 赋值
    if (len > 0)
    {
        m_arr = new int[len]; /*分配内存*/
    }
    else
    {
        m_arr = nullptr;
    }
}
// 析构函数
array_t::~array_t()
{
    cout << "~array_t delete[] m_arr[" << m_len << "]" << endl;
    delete[] m_arr; // 释放内存
}

~array_t()就是 array_t 类的析构函数,它的唯一作用就是在删除对象后释放已经分配的内存。函数名是标识符的一种,原则上标识符的命名中不允许出现~符号,在析构函数的名字中出现的~可以认为是一种特殊情况,目的是为了和构造函数的名字加以对比和区分。

Tips:

C++中的 new 和 delete 分别用来分配和释放内存,它们与C语言中 malloc()、free() 最大的一个不同之处在于:用 new 分配内存时会调用构造函数,用 delete 释放内存时会调用析构函数。构造函数和析构函数对于类来说是不可或缺的,所以在C++中我们非常鼓励使用 new 和 delete。

3. 执行时机

析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。

在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。

new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。

可以看一个实例:

cpp
#include <iostream>
using namespace std;
class demo_t {
  private:
    int m_len;
  public:
    // 构造函数和析构函数
    demo_t(int len); // 构造函数
    ~demo_t();       // 析构函数
};
// 构造函数
demo_t::demo_t(int len) : m_len(len){}
// 析构函数
demo_t::~demo_t() { cout << "~demo_t m_len:" << m_len << endl; }

void func(void)
{
    demo_t array(3); // 局部对象
    return;
}

int main()
{
    // 局部对象
    demo_t arr(10);
    // new创建的对象
    demo_t *parr = new demo_t(5); 
    delete parr; 
    // 函数中创建的对象
    func();
    return 0;
}

将会看到以下输出:

bash
~demo_t m_len:5
~demo_t m_len:3
~demo_t m_len:10

莫道桑榆晚 为霞尚满天.