【C++ 学习总结】- 02 - 类的认识:构造函数 & 析构函数

【C++ 学习总结】- 02 - 类的认识:构造函数 & 析构函数,第1张

【C++ 学习总结】- 02 - 类的认识:构造函数 & 析构函数
  • 一、基本概念
    • 1. 构造函数(Constructor)
    • 2. 析构函数(Destructor)
  • 二、函数特性
    • 1. 构造函数
    • 2. 析构函数
  • 三、构造函数的使用
    • 1. 无参构造函数
    • 2. 带参构造函数(内部赋值)
    • 3. 带参构造函数(初始化列表)
    • 4. 拷贝构造函数
  • 四、析构函数的使用
  • 五、析构函数的执行时机
  • 六、const 成员变量的初始化
    • 1. 使用初始化列表
    • 2. 直接赋值
  • 要点总结(未完待续)


一、基本概念 1. 构造函数(Constructor)

  构造函数(Constructor) 是一种特殊的成员函数:它的函数名与所属类的类名完全相同,没有返回值(不会返回任何类型,包括 void ),用户不需要也不能调用,在创建对象时会自动执行。
  我们知道: ① 类是描绘结构的蓝图,不能被初始化;
        ② 对象在创建后需要对成员变量初始化;
  根据构造函数的性质,结合前面的内容:类只是模板不能初始化、对象在创建后需要初始化、封装的设计要求使得类的成员变量的初始化变得麻烦,我们可以很明显地看出构造函数的主要用途:对新建对象进行初始化。


2. 析构函数(Destructor)

  析构函数(Destructor) 也是一种特殊的成员函数,与构造函数相对应,正如 相对应。有创建时需要的初始化工作,就有销毁时需要的清理工作,与构造函数十分相似的析构函数正是用来处理对象被销毁时的清理工作(释放分配的内存、关闭打开的文件等)的。
  析构函数与构造函数有一些相同点:没有返回值、不需要也不能被程序员调用;也有不同点:析构函数在对象销毁时自动执行、没有参数、不能被重载,且析构函数的名字是类名前面加上一个 " ~ " 符号。( " ~ " 是取反运算符 )

  • 构造函数与析构函数的关系就如同 的关系一样,相互对应。



二、函数特性 1. 构造函数
  • 默认构造函数(Default Constructor)
      一个类必须有构造函数,如果程序员没有定义,编译器就会自动声明一个 默认的构造函数(Default Constructor),这个构造函数没有形参、函数体为空、不执行任何 *** 作;但只要程序员定义了任意一个构造函数,编译器就不会再添加默认的构造函数。
      需要注意的是:如果我们定义了带参的构造函数而没有无参构造函数,那么我们只能通过带参方式来创建对象,因为此时类是没有任何无参构造函数的,编译器并不会自动添加一个默认的构造函数。这一点也需要在后续谈论的继承关系中加以注意。
    (实际上,编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。)

  • 「构造函数」 的权限修饰
      构造函数必须声明为 public ,否则创建对象时无法调用。声明为 private、protected 也不会报错,但是没有意义。

  • 「构造函数」 没有返回值。
      因为构造函数没有变量来接收返回值,所以即使定义返回类型也毫无用处,这意味着:
        ① 不管是声明还是定义,函数名前都不能有任何返回值类型,包括 void ;
        ② 函数体中不能有 return 语句。

  • 「构造函数」 的不同调用方法
      在栈上创建对象时,实参位于对象名后,例如: Student stu("小明", 15, 92.5f);
      在堆上创建对象时,实参位于类名后, 例如: new Student("李华", 16, 96);

  • 「构造函数」 的重载
      构造函数是可以重载的,一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。
    构造函数的调用是强制性的,一旦定义了,创建对象时就一定要调用,不调用会导致错误。若有多个重载的构造函数,创建对象时提供的实参必须和其中一个相匹配,这也也意味着创建对象时只有一个构造函数会被调用。

2. 析构函数
  • 默认析构函数(Default Destructor)
      一个类必须有析构函数,如果程序员没有定义析构函数,编译器就会自动声明一个 默认的析构函数(Default Destructor)。

  • 析构函数没有参数,不能被重载,因此,一个类只有一个析构函数。


三、构造函数的使用

  构造函数主要有三种使用方法,分别是:
    1. 无参构造函数
    2. 带参构造函数(内部赋值、初始化列表)
    3. 复制构造函数

1. 无参构造函数

  定义类时如果不编写构造函数,编辑器就会自动生成一个默认的无参数且空函数体的构造函数。除了系统自动生成以外,我们也可以自己定义一个无参数的构造函数,定义的语法格式如下:

	// 无参构造函数的定义
	className () {
		// Function Content
	}
	
	// 无参构造函数的调用
	className objName;
  • 需要注意:调用无参构造函数时,不应添加括号 " ( ) " 。
      添加括号的写法 Student stu(); 会识别为 定义一个无参函数stu,其返回值是Student类型

  通过一个示例来理解无参构造函数的使用:

================================ 定义示例 ================================
/* 定义一个 Student 类, 包含三个成员变量和一个成员函数 */
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 */
	Student stu;		// 创建对象时, 构造函数自动执行, 完成初始化工作
	stu.query();		// 调用方法查询学生数据
	
	return 0;
}
================================ 运行结果 ================================
-> Type-1 : 无参构造函数已执行
-> 学生姓名: (null),  年龄: 0,  成绩: 0.00



2. 带参构造函数(内部赋值)

  带参构造函数有两种写法,一种是 “内部赋值” ,即在构造函数的函数体内部使用赋值语句对成员变量进行初始化。定义带参构造函数(内部赋值)的语法格式如下:

	// 带参构造函数(内部赋值)的定义
	className (type var1, type var2, ... ) {
		memVar1 = var1;		// Assignment Lists
		memVar2 = var2;
		......
		// Other Operations
	}

  使用带参构造函数(内部赋值)的完整示例:

================================ 定义示例 ================================
/* 定义一个 Student 类, 包含三个成员变量和一个成员函数 */
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 */
	Student stu("张三", 16, 84.5f);			// 创建对象时, 根据传入参数, 构造函数自动执行完成初始化工作
	stu.query();		// 调用方法查询学生数据
	
	return 0;
}
================================ 运行结果 ================================
-> Type-2 : 带参构造函数(内部赋值)已执行
-> 学生姓名: 张三,  年龄: 16,  成绩: 84.50



3. 带参构造函数(初始化列表)

  带参构造函数的另一种写法是使用初始化列表,使用这种方法的构造函数更加简洁高效。
  其写法是:在构造函数的形参表后跟一个冒号 " : " ,然后紧跟着参数的初始化列表,使用 成员变量 + 括号包含要赋予的形参来进行赋值指定,每个成员变量的赋值指定之间使用逗号 " , " 间隔开,最后一个成员变量后不加逗号 " , ",之后是用花括号 " { } " 构成的函数体。

  • 「初始化列表」 的效率更高
      C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。这意味着:如果采用初始化列表,构造函数本体实际上不需要有任何 *** 作,因此效率更高。

  • 「初始化列表」 的灵活性
      初始化列表可以任意指定需要包含的成员变量,并非需要包含所有成员变量。你可以指定其中某几个变量使用初始化列表进行初始化,然后对剩余的变量使用内部赋值的方式初始化或是忽视它们。

  • 「初始化列表」 的初始化顺序
      成员变量的初始化顺序与初始化列表中的变量顺序无关,只与成员变量在类中的声明顺序有关。

  • 构造函数初始化列表还有一个很重要的作用:初始化 const 成员变量 ,具体见下文 《const 成员的初始化》。

  • 由于初始化列表的优秀特性(简洁明了,效率更高),建议在工程中使用初始化列表的构造函数。

  定义参构造函数(初始化列表)的语法格式:

	// 1.初始化列表一行写法
	className (type var1, type var2, ... ) : memVar1(var1), memVar2(var2), ... {
		// Other Operations;
	}
		
	// 2.初始化列表分行写法
	className (type var1, type var2, ... ) : 
		memVar1(var1), 		// Initialization List
		memVar2(var2),
		...... 
	{
		// Function Content
	}
	
	// 初始化列表的调用
	className Object(parameters);

  使用带参构造函数(初始化列表)的完整示例:

================================ 定义示例 ================================
/* 定义一个 Student 类, 包含三个成员变量和一个成员函数 */
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 */
    Student stu("李四", 17, 86.5f);		// 创建对象时,根据传入参数,构造函数自动执行完成初始化工作
    /* 调用方法查询学生数据 */
    stu.query();
    
	return 0;
}
================================ 运行结果 ================================
-> 带参构造函数(初始化列表)已执行
-> 学生姓名: 李四,  年龄: 17,  成绩: 86.50



4. 拷贝构造函数

  拷贝构造函数 也叫 复制构造函数。拷贝构造函数的参数为类的对象本身的引用,主要用途是根据一个已存在的对象复制出一个新的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。

  定义拷贝构造函数的语法格式:

	// 拷贝构造函数的定义
	className (const className & Object) {
		mem_var1 = Object.mem_var1;		// Assignment Lists
		mem_var2 = Object.mem_var2;
		......
		// Other Operations;
	}
		
	// 拷贝构造函数的调用
	className classVar(Object);

  使用拷贝构造函数的完整示例:

================================ 定义示例 ================================
/* 定义一个 Student 类, 包含三个成员变量和一个成员函数 */
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 */
	Student stu1("王五", 18, 88.5f);      // 调用带参构造函数(初始化列表)
    stu1.query();
    
	/* 创建 Student 类的对象 stu2 */
	Student stu2(stu1);                   // 调用拷贝构造函数
	stu2.query();
}

================================ 运行结果 ================================
-> Type-3 : 带参构造函数(初始化列表)已执行
-> 学生姓名: 王五,  年龄: 18,  成绩: 88.50
-> Type-4 : 拷贝构造函数已执行
-> 学生姓名: 王五,  年龄: 18,  成绩: 88.50
  • 拷贝构造函数通常用于:
    • 通过使用另一个同类型的对象来初始化新创建的对象。
    • 复制对象把它作为参数传递给函数。
    • 复制对象,并从函数返回这个对象。



四、析构函数的使用

  对象在使用完之后有一些清理工作需要完成,如:释放分配的内存、关闭打开的文件等等,而这些工作会在对象被销毁时由系统自动调用析构函数来完成。

  定义析构函数的语法格式:

	// 析构函数的定义
	~className () {
		// Function Content
	}

  析构函数的完整使用示例:

================================ 定义示例 ================================
/* 定义一个 Matrix 类, 包含三个成员变量和两个成员函数 */
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);          // 创建 Matrix 类的对象 m
    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,析构函数就不会被执行。

  我们通过下例来深入了解析构函数的执行时机:

================================ 定义示例 ================================
/* 定义一个 Demo 类, 包含 */
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);
        }
};

================================ 程序示例 ================================
/* 全局对象 d1 */
Demo d1("Num.1");

/* 局部对象 d3 */
void Func (void) {
    Demo d3("Num.3");
}

int main()
{
	/* 主函数内的局部对象 d2 */
    Demo d2("Num.2");
	/* 主函数内所调用的函数的局部对象 d3 */
    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 成员变量的方法可参考下例:

================================ 定义示例 ================================
/* 定义一个类 Time, 拥有2个const变量 */
class Time {
	private:
		int m_sec;
		int m_min;
		int m_hou;
		const int m_day;
		const int m_week;

	public:
		/* 无参构造函数 */				// Type-1 : 没有形参, 直接指定常量
		Time() : m_day(10), m_week(1) {
			printf("Type-1 : 构造函数已执行 \n");
		}

        /* 带参构造函数 1 */				// Type-2 : 没有对应形参, 直接指定常量
		Time (int second, int minute, int hour) : m_day(20), m_week(2) {
			printf("Type-2 : 构造函数已执行 \n");
        }

        /* 带参构造函数 2 */				// Type-3 : 使用形参传入的数值来初始化
		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();
    
    /* 调用带参构造函数 1 */
    Time t2(10, 20, 30);
    t2.query();
    
    /* 调用带参构造函数 2 */
    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 成员变量是一个固定不变的已知数值,可直接在类的定义中对其直接赋值,参考下例:

================================ 定义示例 ================================
/* 定义一个类 Time, 拥有2个const变量 */
class Time {
	private:
		int m_sec;
		int m_min;
		int m_hou;
		const int m_day = 40;		// 直接对const成员变量赋初值
		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. 构造函数的特性
    (1)对象创建时自动执行
    (2)函数名和类名完全一致;
    (3)没有任何返回值,包括void,故而不能定义返回类型;
    (4)可以有形参,可以被重载,可以有多个;
    (5)一个类必须有构造函数,故若自己没有定义,系统会声明一个默认的构造函数

  2. 析构函数的特性
    (1)对象销毁时自动执行
    (2)函数名为类名前添加一个 " ~ " ;
    (3)没有任何返回值,包括void,故而不能定义返回类型;
    (4)没有形参,不能重载,只能有一个;
    (5)一个类必须有析构函数,故若自己没有定义,系统会声明一个默认的析构函数

  3. 推荐使用初始化列表的构造函数来进行对象的初始化:
    (1)列表让数据更直观,逻辑更清晰,提高代码的可读性
    (2)根据 C++ 标准,列表法的效率更高,开销更低
    (3)const 变量仅可使用初始化列表的方法初始化(不考虑直接赋值)
    (4)需要注意:成员变量的初始化顺序跟列表中的变量排列顺序无关,而是与成员变量的声明顺序有关

  4. 析构函数的执行时机
    (1)函数内创建的对象是局部变量,位于栈区,函数执行结束时,销毁这些对象,并调用析构函数
    (2)在所有函数之外创建的对象是全局变量,位于 .bss段,程序结束时销毁这些对象,并调用析构函数
    (3)通过 创建的对象位于堆区,必须使用 才能销毁,然后调用析构函数


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/866782.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-12
下一篇 2022-05-12

发表评论

登录后才能评论

评论列表(0条)

保存