引言
构造函数和析构函数是在对象创建和销毁时被自动调用的成员函数,构造函数一般用来初始化成员,或者申请动态内存等资源,析构函数用来释放对象申请占用的资源。
构造函数
定义完基本类型的变量,比如int x;
,要注意给它赋初值。这是因为尚未初始化的变量里面的值是乱七八糟的值,在C/C++中读取未初始化的变量的值可能会导致程序崩溃。
对于类类型的对象来说,也是一样的,Human p;
定义好对象p
以后,p
里面的成员p.age
等也是乱七八糟的值(p.name
是string
类型的对象,会由string
类型的默认构造函数初始化为空字符串)。因此要调用p.init("张三", true, 20, "123456");
这样的成员函数来给各成员赋值。
#include <iostream>
#include <string>
using namespace std;
class Human{
public:
void init(string n, bool is_m, int a, string i)
{
name = n;
is_male = is_m;
age = a;
id = i;
}
...
private:
string name;
bool is_male;
int age;
string id;
};
int main()
{
Human p;
p.init("张三", true, 20, "123456");
p.introduce();
}
如果有相应的构造函数,那直接创建对象就好了,并提供相应的值即可。
#include <iostream>
#include <string>
using namespace std;
class Human{
public:
// 带参数的构造函数
Human(string n, bool is_m, int a, string i)
{
name = n;
is_male = is_m;
age = a;
id = i;
}
private:
string name;
bool is_male;
int age;
string id;
};
int main()
{
Human p("张三", true, 20, "123456"); // 自动调用带参数的构造函数
p.introduce();
}
构造函数是与类同名,没有返回类型,也不用声明void,构造函数必须这样写。
如果只存在这样一个构造函数,那我们也只能这样创建人类对象。即必须提供姓名等属性值。
析构函数
构造函数是在创建对象时自动调用的成员函数,析构函数与构造函数调用时机相反,是在对象被销毁时自动调用的成员函数。(函数内的局部变量和参数变量在函数退出时被销毁,或者动态创建的对象被使用delete被消耗)
如果对象占用的某些资源(比如动态分配了内存),那么需要释放这些资源。往往就在析构函数中释放它们。
假设我们要自己定义类似于标准库中的string类型。那下面我们就尝试写出部分代码。代码仅是起说明构造和析构函数的用法的作用,不具有实用性。
String类要存放字符串,如果要支持变长字符串,字符数组的内存需要动态申请,动态申请的内存用完需要释放,我们可以把delete释放内存的代码放到String的析构函数中。 每个String对象在被销毁时都会调用析构函数,因此每个String对象动态申请的内存一定会被释放。
class String
{
public:
String() // 默认构造函数,空字符串
{
len = 0;
p = new char;
*p = '\0';
}
String(char *str) // 接收一个字符串参数的构造函数
{
len = strlen(str);
p = new char[len + 1];
strcpy(p, str);
}
...
~String() // 析构函数
{
cout << "String destruction called" << endl;
if (p != NULL)
{
delete []p;
p = NULL;
}
}
private:
char *p;
int len;
};
int main()
{
String s1;
String s2("hello, world");
return 0;
}
语法
参考程序员实验室-C++基础教程-4.1 类(Classes)
构造函数其它知识点
默认构造函数
创建对象时肯定会调用“某一款”构造函数。不带参数的构造函数也叫默认构造函数或者缺省构造函数。如果类里面没有定义任何构造函数,编译器会帮助生成一个默认构造函数(里面什么也没做),一旦程序员定义了任何构造函数,编译器就不会帮助生成此默认构造函数。
比起调用其它构造函数,调用默认构造函数的语法比较特殊,但我们也经常调用默认构造函数。
class Foo { public: Foo(int x_) // 带一个参数的构造函数 { x = x_; } Foo() // 默认构造函数 { x = 0; } int x; }; int main() { Foo f1(10); // 调用带一个带参数的构造函数 Foo f2; // 调用默认构造函数,不能写成 Foo f2(); }
如果类里面没有定义任何构造函数,编译器会帮助生成一个默认构造函数(里面什么也没做),一旦程序员定义了任何构造函数,编译器就不会帮助生成此默认构造函数。
class FooWithNoExplicitConstructor { public: // 没有任何构造函数,编译器会自动生成一个“什么都不做”的默认构造函数 int x; }; class Foo { public: Foo(int x_) // 带一个参数的构造函数,此时就没有自动生成的默认构造函数了 { x = x_; } int x; }; int main() { FooWithNoExplicitConstructor f1; // 调用默认构造函数(但是里面什么也没做,f1.a没有初始化) Foo f2(10); // 调用带一个参数的构造函数 Foo f3; // 编译错误,此时没有默认构造函数 }
构造函数的成员初始化列表
之前我们都是在构造函数内部给成员赋值的
class Foo
{
public:
Foo(int x_, int y_) // 带一个参数的构造函数
{
x = x_;
y = y_;
cout << "x:" << x << ",y:" << y << endl;
}
int x;
int y;
};
int main()
{
Foo f1(10, 20); // 调用带一个带参数的构造函数
return 0;
}
实际上,创建对象f1时,可以看成两步:
- 创建对象的各成员(f1.x和f1.y)。
- 调用Foo类的构造函数{}内的代码。
因此,在Foo类的构造函数{}内的代码执行之前,其成员x和y就已经被创建出来。这意味着在构造函数{}内部给成员赋值是普通的赋值语句,而不是成员的初始化。就像
int x; // 先定义变量
x = 10; // 后给变量赋值
初始化是指在创建变量的时候就给变量赋值,就像
int x = 10; // 定义变量并初始化它
怎样在创建类的成员变量的时候给它初值呢?那要使用构造函数的成员初始化列表。
class Foo
{
public:
Foo(int x_, int y_) : x(x_), y(y_) // 构造函数的成员初始化列表
{
cout << "x:" << x << ",y:" << y << endl;
}
int x;
int y;
};
int main()
{
Foo f1(10, 20);
return 0;
}
可见,使用成员初始化列表可以给类成员赋初值。对于基本类型的成员变量,是否使用成员初始化列表初始化关系不大,而对于类类型的成员变量,有时必须使用成员初始化列表来初始化它们。