一、基本概念
1. 构造函数(Constructor)
??构造函数(Constructor) 是一种特殊的成员函数:它的函数名与所属类的类名完全相同,没有返回值(不会返回任何类型,包括 void ),用户不需要也不能调用,在创建对象时会自动执行。 ??我们知道: ???① 类是描绘结构的蓝图,不能被初始化; ???② 对象在创建后需要对成员变量初始化;因为 根据构造函数的性质,结合前面的内容:类只是模板不能初始化、对象在创建后需要初始化、封装的设计要求使得类的成员变量的初始化变得麻烦,我们可以很明显地看出构造函数的主要用途:对新建对象进行初始化。
2. 析构函数(Destructor)
??析构函数(Destructor) 也是一种特殊的成员函数,与构造函数相对应,正如 <new> 与 <delete> 相对应。有创建时需要的初始化工作,就有销毁时需要的清理工作,与构造函数十分相似的析构函数正是用来处理对象被销毁时的清理工作(释放分配的内存、关闭打开的文件等)的。 ??析构函数与构造函数有一些相同点:没有返回值、不需要也不能被程序员调用;也有不同点:析构函数在对象销毁时自动执行、没有参数、不能被重载,且析构函数的名字是类名前面加上一个 " ~ " 符号。( " ~ " 是取反运算符 )
- 构造函数与析构函数的关系就如同 <new> 与 <delete> 的关系一样,相互对应。
二、函数特性
1. 构造函数
-
默认构造函数(Default Constructor) ??一个类必须有构造函数,如果创建者没有定义,编译器就会自动生成一个 默认的构造函数(Default Constructor),这个构造函数的没有形参、函数体是空的、不执行任何操作。如,对于上文的类 class Student 会生成一个默认的构造函数 Student() {} 。 (实际上,编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。) -
构造函数必须声明为 public ,否则创建对象时无法调用。声明为 private、protected 也不会报错,但是没有意义。 -
构造函数没有返回值。因为没有变量来接收返回值,即使定义返回类型也毫无用处,这意味着: ??① 不管是声明还是定义,函数名前都不能有任何返回值类型,包括 void ; ??② 函数体中不能有 return 语句。 -
在栈上创建对象时,实参位于对象名后,例如: Student stu("小明", 15, 92.5f); 在堆上创建对象时,实参位于类名后,?例如: new Student("李华", 16, 96); -
构造函数的重载 ??构造函数是可以重载的,一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。 构造函数的调用是强制性的,一旦定义了,创建对象时就一定要调用,不调用会导致错误。若有多个重载的构造函数,创建对象时提供的实参必须和其中一个相匹配,这也也意味着创建对象时只有一个构造函数会被调用。
2. 析构函数
三、构造函数的使用
??构造函数主要有三种使用方法,分别是: ????1. 无参构造函数 ????2. 带参构造函数(内部赋值、初始化列表) ????3. 复制构造函数
1. 无参构造函数
??定义类时如果不编写构造函数,编辑器就会自动生成一个默认的无参数且空函数体的构造函数。除了系统自动生成以外,我们也可以自己编写一个无参数的构造函数。
??语法格式:
className () {
}
??使用无参构造函数的示例:
================================ 定义示例 ================================
class Student {
private:
char * m_name;
int m_age;
float m_score;
public:
Student () {
m_name = NULL;
m_age = 0;
m_score = 0;
printf("-> Type-1 : 无参构造函数已执行 \n");
}
void query (void) {
printf("-> 学生姓名: %s, 年龄: %d, 成绩: %.2f \n", m_name, m_age, m_score);
}
};
================================ 程序示例 ================================
int main (void)
{
Student stu;
stu.query();
return 0;
}
================================ 运行结果 ================================
-> Type-1 : 无参构造函数已执行
-> 学生姓名: (null), 年龄: 0, 成绩: 0.00
- 需要注意:调用无参构造函数时,不应添加括号 " ( ) " 。
??添加括号的写法 Student stu(); 会识别为 定义一个无参函数stu,其返回值是Student类型 。
2. 带参构造函数(内部赋值)
??带参构造函数有两种写法,其中一种是 “内部赋值” ,即在构造函数的函数体内部使用赋值语句进行初始化成员变量的初始化:
??语法格式:
className (type var1, type var2, ... ) {
mem_var1 = var1;
mem_var2 = var2;
......
}
??使用带参构造函数(内部赋值)的示例:
================================ 定义示例 ================================
class Student {
private:
char * m_name;
int m_age;
float m_score;
public:
Student (char * name, int age, float score) {
m_name = name;
m_age = age;
m_score = score;
printf("-> Type-2 : 带参构造函数(内部赋值)已执行 \n");
}
void query (void) {
printf("-> 学生姓名: %s, 年龄: %d, 成绩: %.2f \n", m_name, m_age, m_score);
}
};
================================ 程序示例 ================================
int main (void)
{
Student stu("张三", 16, 84.5f);
stu.query();
return 0;
}
================================ 运行结果 ================================
-> Type-2 : 带参构造函数(内部赋值)已执行
-> 学生姓名: 张三, 年龄: 16, 成绩: 84.50
3. 带参构造函数(初始化列表)
??带参构造函数的另一种写法是使用初始化列表,这种方法写的构造函数更加简洁。 ??其写法是:在构造函数的形参表后跟一个冒号 " : " ,然后紧跟着参数的初始化列表,使用 成员变量 + 括号包含要赋予的形参来进行赋值指定,每个成员变量的赋值指定之间使用逗号 " , " 间隔开,最后一个成员变量后不加逗号 " , ",之后是用花括号 " { } " 构成的函数体。
-
C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。这意味着:如果采用初始化列表,构造函数本体实际上不需要有任何操作,因此效率更高。 -
初始化列表可以任意指定需要包含的成员变量,并非需要包含所有成员变量。你可以指定其中某几个变量使用初始化列表进行初始化,然后对剩余的变量使用内部赋值的方式初始化或是忽视它们。 -
成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关 -
构造函数初始化列表还有一个很重要的作用:初始化 const 成员变量 ,具体见下文 《const 成员的初始化》。 -
由于初始化列表的优秀特性(简洁明了,效率更高),建议在工程中使用初始化列表的构造函数。
??语法格式:
className (type var1, type var2, ... ) : mem_var1(var1), mem_var2(var2), ... {
}
className (type var1, type var2, ... ) :
mem_var1(var1),
mem_var2(var2),
......
{
}
??使用带参构造函数(初始化列表)的示例:
================================ 定义示例 ================================
class Student {
private:
char * m_name;
int m_age;
float m_score;
public:
Student (char * name, int age, float score) :
m_name(name),
m_age(age),
m_score(score)
{
printf("-> 带参构造函数(初始化列表)已执行 \n");
}
void query (void) {
printf("-> 学生姓名: %s, 年龄: %d, 成绩: %.2f \n", m_name, m_age, m_score);
}
};
================================ 程序示例 ================================
int main (void)
{
Student stu("李四", 17, 86.5f);
stu.query();
return 0;
}
================================ 运行结果 ================================
-> 带参构造函数(初始化列表)已执行
-> 学生姓名: 李四, 年龄: 17, 成绩: 86.50
4. 拷贝构造函数
??拷贝构造函数,也叫 复制构造函数 。 ??拷贝构造函数的参数为类的对象本身的引用,主要用途是根据一个已存在的对象复制出一个新的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。
??语法格式:
className (const className & classVar_to_Copy) {
mem_var1 = classVar_to_Copy.mem_var1;
mem_var2 = classVar_to_Copy.mem_var2;
......
}
className classVar(ClassVar_to_Copy);
??使用拷贝构造函数的示例:
================================ 定义示例 ================================
class Student {
private:
char * m_name;
int m_age;
float m_score;
public:
Student (char * name, int age, float score) :
m_name(name),
m_age(age),
m_score(score)
{
printf("-> Type-3 : 带参构造函数(初始化列表)已执行 \n");
}
Student (cosnt Student & s) {
m_name = s.m_name;
m_age = s.m_age;
m_score = s.m_score;
printf("-> Type-4 : 拷贝构造函数已执行 \n");
}
void query (void) {
printf("-> 学生姓名: %s, 年龄: %d, 成绩: %.2f \n", m_name, m_age, m_score);
}
};
================================ 程序示例 ================================
int main ()
{
Student stu1("王五", 18, 88.5f);
stu1.query();
Student stu2(stu1);
stu2.query();
}
================================ 运行结果 ================================
-> Type-3 : 带参构造函数(初始化列表)已执行
-> 学生姓名: 王五, 年龄: 18, 成绩: 88.50
-> Type-4 : 拷贝构造函数已执行
-> 学生姓名: 王五, 年龄: 18, 成绩: 88.50
拷贝构造函数通常用于:
1.通过使用另一个同类型的对象来初始化新创建的对象。
2.复制对象把它作为参数传递给函数。
3.复制对象,并从函数返回这个对象。
四、析构函数的使用
??对象在使用完之后有一些清理工作需要完成,如:释放分配的内存、关闭打开的文件等等,而这些工作会在对象被销毁时由系统自动调用析构函数来完成。
================================ 定义示例 ================================
class Matrix {
private:
const int m_row;
const int m_col;
int *m_arr;
public:
Matrix (int row, int col) :
m_row(row),
m_col(col)
{
m_arr = new int[row * col];
for(int i=0; i<m_row; i++) {
for(int j=0; j<m_col; j++) {
*(m_arr + i*m_col + j) = (i*m_col + j + 1);
}
}
printf("-> 构造函数已执行 \n\n");
}
~Matrix () {
delete[] m_arr;
printf("-> 析构函数已执行,已释放内存 \n\n");
}
void query_size (void) {
printf("-> 矩阵规格: rowNum : %d \n", m_row);
printf("-> colNum : %d \n\n", m_col);
}
void output_data (void) {
printf("-> 矩阵数据: \n\n");
for(int i=0; i<m_row; i++) {
printf(" ");
for(int j=0; j<m_col; j++) {
printf("%02d ", *(m_arr + i*m_col + j));
}
printf("\n\n");
}
}
};
================================ 程序示例 ================================
int main()
{
Matrix m(8,8);
m.query_size();
m.output_data();
return 0;
}
================================ 运行结果 ================================
-> 构造函数已执行
-> 矩阵规格: rowNum : 8
-> colNum : 8
-> 矩阵数据:
01 02 03 04 05 06 07 08
09 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 38 39 40
41 42 43 44 45 46 47 48
49 50 51 52 53 54 55 56
57 58 59 60 61 62 63 64
-> 析构函数已执行,已释放内存
五、析构函数的执行时机
- 析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。
- 在所有函数之外创建的对象是全局对象,它和全局变量类似,位于全局数据区,在程序结束执行时会调用这类对象的析构函数。
- 在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时就会调用这类对象的析构函数。
- new 创建的对象位于堆区,通过 delete 销毁时才会调用析构函数;如果没有 delete,析构函数就不会被执行。
??我们通过下例来深入了解析构函数的执行时机:
================================ 定义示例 ================================
class Demo {
private:
const char *m_str;
public:
Demo (const char *str) : m_str(str) {
printf("-> --构造-- 函数已执行 : %s \n", m_str);
}
~Demo () {
printf("-> **析构** 函数已执行 : %s \n", m_str);
}
};
================================ 程序示例 ================================
Demo d1("Num.1");
void Func (void) {
Demo d3("Num.3");
}
int main()
{
Demo d2("Num.2");
Func();
printf("-> 程序结束\n");
return 0;
}
================================ 运行结果 ================================
-> --构造-- 函数已执行 : Num.1
-> --构造-- 函数已执行 : Num.2
-> --构造-- 函数已执行 : Num.3
-> **析构** 函数已执行 : Num.3
-> 程序结束
-> **析构** 函数已执行 : Num.2
-> **析构** 函数已执行 : Num.1
??d1 是位于所有函数外的全局变量,最先创建,最后销毁(在程序结束后);d2 是main函数内定义的局部变量,由于先执行它的创建语句,所以其先于 d3 创建,并且在main函数结束时销毁;d3 是main函数调用的函数内的局部变量,最先销毁(Func执行完后就被销毁)。 ??可见,析构函数的执行时机完全取决于对象的销毁时机。
六、const 成员变量的初始化
1. 使用初始化列表
??构造函数的初始化列表还有一个重要的作用:初始化 const 成员变量。在构造函数的函数体中对 const 变量进行赋值的行为是非法的,会引起编译器报错,因此不能使用内部赋值的方法初始化 const 成员变量。
??初始化列表初始化 const 成员变量的方法可参考下例:
================================ 定义示例 ================================
class Time {
private:
int m_sec;
int m_min;
int m_hou;
const int m_day;
const int m_week;
public:
Time() : m_day(10), m_week(1) {
printf("Type-1 : 构造函数已执行 \n");
}
Time (int second, int minute, int hour) : m_day(20), m_week(2) {
printf("Type-2 : 构造函数已执行 \n");
}
Time (int day, int week) : m_day(day), m_week(week) {
printf("Type-3 : 构造函数已执行 \n");
}
void query (void) {
printf("-> 常量查询: m_day is : %d \n", m_day);
printf("-> m_week is : %d \n\n", m_week);
}
};
================================ 程序示例 ================================
int main()
{
Time t1;
t1.query();
Time t2(10, 20, 30);
t2.query();
Time t3(30, 3);
t3.query();
return 0;
}
================================ 运行示例 ================================
Type-1 构造函数已执行
-> 常量查询: m_day is : 10
-> m_week is : 1
Type-2 构造函数已执行
-> 常量查询: m_day is : 20
-> m_week is : 2
Type-3 构造函数已执行
-> 常量查询: m_day is : 30
-> m_week is : 3
2. 直接赋值
??C++ 11 标准 规定,const 成员变量可以在类声明时直接赋值。因此,若 const 成员变量是一个固定不变的已知数值,可直接在类的定义中对其直接赋值,参考下例:
================================ 定义示例 ================================
class Time {
private:
int m_sec;
int m_min;
int m_hou;
const int m_day = 40;
const int m_week = 4;
public:
void query (void) {
printf("-> 常量查询: m_day is : %d \n", m_day);
printf("-> m_week is : %d \n\n", m_week);
}
};
================================ 程序示例 ================================
int main void ()
{
Time t;
t.query();
}
================================ 运行结果 ================================
-> 常量查询: m_day is : 40
-> m_week is : 4
要点总结(未完待续)
-
构造函数的特性 (1)对象创建时自动执行 (2)函数名和类名完全一致; (3)没有任何返回值,包括void,故而不能定义返回类型; (4)可以有形参,可以被重载,可以有多个; (5)一个类必须有构造函数,故若自己没有定义,系统会声明一个默认的构造函数 -
析构函数的特性 (1)对象销毁时自动执行 (2)函数名为类名前添加一个 " ~ " ; (3)没有任何返回值,包括void,故而不能定义返回类型; (4)没有形参,不能重载,只能有一个; (5)一个类必须有析构函数,故若自己没有定义,系统会声明一个默认的析构函数 -
推荐使用初始化列表的构造函数来进行对象的初始化: (1)列表让数据更直观,逻辑更清晰,提高代码的可读性 (2)根据 C++ 标准,列表法的效率更高,开销更低 (3)const 变量仅可使用初始化列表的方法初始化(不考虑直接赋值) (4)需要注意:成员变量的初始化顺序跟列表中的变量排列顺序无关,而是与成员变量的声明顺序有关 -
析构函数的执行时机 (1)函数内创建的对象是局部变量,位于栈区,函数执行结束时,销毁这些对象,并调用析构函数 (2)在所有函数之外创建的对象是全局变量,位于 .bss段,程序结束时销毁这些对象,并调用析构函数 (3)通过 <new> 创建的对象位于堆区,必须使用 <delete> 才能销毁,然后调用析构函数
|