接下来了解一下构造函数。
一、构造函数(Constructor)
1. 什么是构造函数
在C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。
2. 定义一个构造函数
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。例如:
#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
就是错误的,因为类中包含了构造函数,而创建对象时却没有调用。我们可以修改实例,添加一个构造函数:
#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 类,默认生成的构造函数如下:
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 怎么使用
构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表。但是使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。
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 使用初始化列表,其他成员变量还是一一赋值:
student_t::Student(char *name, int age, float score): m_name(name){
m_age = age;
m_score = score;
}
5.2 列表中变量顺序
注意,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。请看代码:
#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;
}
运行会得到如下结果:
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,所以构造函数和下面的代码等价:
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 类,用于模拟变长数组,请看下面的代码:
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 修饰,只能使用初始化列表的方式赋值,如果写作下面的形式是错误的:
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 类来模拟变长数组,它使用一个构造函数为数组分配内存,这些内存在数组被销毁后不会自动释放,所以非常有必要再添加一个析构函数,专门用来释放已经分配的内存。
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,析构函数就不会被执行。
可以看一个实例:
#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;
}
将会看到以下输出:
~demo_t m_len:5
~demo_t m_len:3
~demo_t m_len:10