C++中的构造函数与析构函数

原文链接:https://www.cnblogs.com/mr-wid/archive/2013/02/19/2917911.html

一、构造函数介绍

1.构造函数的作用

构造函数主要用来在创建对象时完成对对象属性的一些初始化等操作, 当创建对象时, 对象会自动调用它的构造函数。一般来说, 构造函数有以下三个方面的作用:

  • 给创建的对象建立一个标识符;
  • 为对象数据成员开辟内存空间;
  • 完成对象数据成员的初始化。

2.默认构造函数

​ 当用户没有显式的去定义构造函数时, 编译器会为类生成一个默认的构造函数, 称为 “默认构造函数“, 默认构造函数不能完成对象数据成员的初始化, 只能给对象创建一标识符, 并为对象中的数据成员开辟一定的内存空间。

3.构造函数的特点

无论是用户自定义的构造函数还是默认构造函数都主要有以下特点:

  1. 在对象被创建时自动执行;
  2. 构造函数的函数名与类名相同;
  3. 没有返回值类型、也没有返回值;
  4. 构造函数不能被显式调用。

二、构造函数的显式定义

由于在大多数情况下我们希望在对象创建时就完成一些对成员属性的初始化等工作, 而默认构造函数无法满足我们的要求, 所以我们需要显式定义一个构造函数来覆盖掉默认构造函数以便来完成必要的初始化工作, 当用户自定义构造函数后编译器就不会再为对象生成默认构造函数。

在构造函数的特点中我们看到, 构造函数的名称必须与类名相同, 并且没有返回值类型和返回值, 看一个构造函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

using namespace std;

class Point
{
public:
Point() //声明并定义构造函数
{
cout<<"自定义的构造函数被调用...\n";
xPos = 100; //利用构造函数对数据成员 xPos, yPos进行初始化
yPos = 100;
}
void printPoint()
{
cout<<"xPos = " << xPos <<endl;
cout<<"yPos = " << yPos <<endl;
}

private:
int xPos;
int yPos;
};

int main()
{
Point M; //创建对象M
M.printPoint();

return 0;
}

编译运行的结果:

1
2
3
4
5
6
自定义的构造函数被调用...
xPos = 100
yPos = 100

Process returned 0 (0x0) execution time : 0.453 s
Press any key to continue.

代码说明:

在Point类的 public 成员中我们定义了一个构造函数 Point() , 可以看到这个Point构造函数并不像 printPoint 函数有个void类型的返回值, 这正是构造函数的一特点。在构造函数中, 我们输出了一句提示信息, “自定义的构造函数被调用…”, 并且将对象中的数据成员xPos和yPos初始化为100。

在 main 函数中, 使用 Point 类创建了一个对象 M, 并调用M对象的方法 printPoint 输出M的属性信息, 根据输出结果看到, 自定义的构造函数被调用了, 所以 xPos和yPos 的值此时都是100, 而不是一个随机值。

需要提示一下的是, 构造函数的定义也可放在类外进行。

三、有参数的构造函数

在上个示例中实在构造函数的函数体内直接对数据成员进行赋值以达到初始化的目的, 但是有时候在创建时每个对象的属性有可能是不同的, 这种直接赋值的方式显然不合适。不过构造函数是支持向函数中传入参数的, 所以可以使用带参数的构造函数来解决该问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;

class Point
{
public:
Point(int x = 0, int y = 0) //带有默认参数的构造函数
{
cout<<"自定义的构造函数被调用...\n";
xPos = x; //利用传入的参数值对成员属性进行初始化
yPos = y;
}
void printPoint()
{
cout<<"xPos = " << xPos <<endl;
cout<<"yPos = " << yPos <<endl;
}

private:
int xPos;
int yPos;
};

int main()
{
Point M(10, 20); //创建对象M并初始化xPos,yPos为10和20
M.printPoint();

Point N(200); //创建对象N并初始化xPos为200, yPos使用参数y的默认值0
N.printPoint();

Point P; //创建对象P使用构造函数的默认参数
P.printPoint();

return 0;
}

编译运行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
自定义的构造函数被调用...
xPos = 10
yPos = 20
自定义的构造函数被调用...
xPos = 200
yPos = 0
自定义的构造函数被调用...
xPos = 0
yPos = 0

Process returned 0 (0x0) execution time : 0.297 s
Press any key to continue.

代码说明:

在这个示例中的构造函数 Point(int x = 0, int y = 0) 使用了参数列表并且对参数进行了默认参数设置为0。在 main 函数中共创建了三个对象 M, N, P。

M对象不使用默认参数将M的坐标属性初始化10和20;

N对象使用一个默认参数y, xPos属性初始化为200;

P对象完全使用默认参数将xPos和yPos初始化为0。

四、构造函数的重载

构造函数也毕竟是函数, 与普通函数相同, 构造函数也支持重载, 需要注意的是, 在进行构造函数的重载时要注意重载和参数默认的关系要处理好, 避免产生代码的二义性导致编译出错, 例如以下具有二义性的重载:

1
2
3
4
5
6
7
8
9
10
Point(int x = 0, int y = 0)     //默认参数的构造函数
{
xPos = x;
yPos = y;
}
Point() //重载一个无参构造函数
{
xPos = 0;
yPos = 0;
}

在上面的重载中, 当尝试用 Point 类重载一个无参数传入的对象 M 时, Point M; 这时编译器就报一条 error: call of overloaded ‘Point()’ is ambiguous 的错误信息来告诉我们说 Point 函数具有二义性, 这是因为 Point(int x = 0, int y = 0) 全部使用了默认参数, 即使我们不传入参数也不会出现错误, 但是在重载时又重载了一个不需要传入参数了构造函数 Point(), 这样就造成了当创建对象都不传入参数时编译器就不知道到底该使用哪个构造函数了, 就造成了二义性。

五、初始化表达式

对象中的一些数据成员除了在构造函数体中进行初始化外还可以通过调用初始化表来进行完成, 要使用初始化表来对数据成员进行初始化时使用 : 号进行调出, 示例如下:

1
2
3
4
Point(int x = 0, int y = 0):xPos(x), yPos(y)  //使用初始化表
{
cout<<"调用初始化表对数据成员进行初始化!\n";
}

在 Point 构造函数头的后面, 通过单个冒号 : 引出的就是初始化表, 初始化的内容为 Point 类中int型的 xPos 成员和 yPos成员, 其效果和 xPos = x; yPos = y; 是相同的。

与在构造函数体内进行初始化不同的是, 使用初始化表进行初始化是在构造函数被调用以前就完成的。每个成员在初始化表中只能出现一次, 并且初始化的顺序不是取决于数据成员在初始化表中出现的顺序, 而是取决于在类中声明的顺序。

此外, 一些通过构造函数无法进行初始化的数据类型可以使用初始化表进行初始化, 如: 常量成员和引用成员, 这部分内容将在后面进行详细说明。使用初始化表对对象成员进行初始化的完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

class Point
{
public:
Point(int x = 0, int y = 0):xPos(x), yPos(y)
{
cout<<"调用初始化表对数据成员进行初始化!\n";
}

void printPoint()
{
cout<<"xPos = " << xPos <<endl;
cout<<"yPos = " << yPos <<endl;
}

private:
int xPos;
int yPos;
};

int main()
{
Point M(10, 20); //创建对象M并初始化xPos,yPos为10和20
M.printPoint();
return 0;
}

六、析构函数

与构造函数相反, 析构函数是在对象被撤销时被自动调用, 用于对成员撤销时的一些清理工作, 例如在前面提到的手动释放使用 new 或 malloc 进行申请的内存空间。析构函数具有以下特点:

  • 析构函数函数名与类名相同, 紧贴在名称前面用波浪号 ~ 与构造函数进行区分, 例如: ~Point();
  • 构造函数没有返回类型, 也不能指定参数, 因此析构函数只能有一个, 不能被重载;
  • 当对象被撤销时析构函数被自动调用, 与构造函数不同的是, 析构函数可以被显式的调用, 以释放对象中动态申请的内存。

当用户没有显式定义析构函数时, 编译器同样会为对象生成一个默认的析构函数, 但默认生成的析构函数只能释放类的普通数据成员所占用的空间, 无法释放通过 new 或 malloc 进行申请的空间, 因此有时我们需要自己显式的定义析构函数对这些申请的空间进行释放, 避免造成内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <cstring>

using namespace std;

class Book
{
public:
Book( const char *name ) //构造函数
{
bookName = new char[strlen(name)+1];
strcpy(bookName, name);
}
~Book() //析构函数
{
cout<<"析构函数被调用...\n";
delete []bookName; //释放通过new申请的空间
}
void showName() { cout<<"Book name: "<< bookName <<endl; }

private:
char *bookName;
};

int main()
{
Book CPP("C++ Primer");
CPP.showName();
return 0;
}

编译运行的结果:

1
2
3
4
5
Book name: C++ Primer
析构函数被调用...

Process returned 0 (0x0) execution time : 0.266 s
Press any key to continue.

代码说明:

代码中创建了一个 Book 类, 类的数据成员只有一个字符指针型的 bookName, 在创建对象时系统会为该指针变量分配它所需内存, 但是此时该指针并没有被初始化所以不会再为其分配其他多余的内存单元。在构造函数中, 我们使用 new 申请了一块 strlen(name)+1 大小的空间, 也就是比传入进来的字符串长度多1的空间, 目的是让字符指针 bookName 指向它, 这样才能正常保存传入的字符串。

在 main 函数中使用 Book 类创建了一个对象 CPP, 初始化 bookName 属性为 “C++ Primer”。从运行结果可以看到, 析构函数被调用了, 这时使用 new 所申请的空间就会被正常释放。

自然状态下对象何时将被销毁取决于对象的生存周期, 例如全局对象是在程序运行结束时被销毁, 自动对象是在离开其作用域时被销毁。

如果需要显式调用析构函数来释放对象中动态申请的空间只需要使用 对象名.析构函数名(); 即可, 例如上例中要显式调用析构函数来释放 bookName 所指向的空间只要:

1
CPP.~Book();

参考文章:

C++ 构造函数总结

Author: wnxy
Link: https://wnxy.github.io/2021/07/15/cpp_constructor_and_destructor/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.