• C++学习——构造函数、析构函数


    以下内容源于C语言中文网的学习与整理,非原创,如有侵权请告知删除。

    一、构造函数的详解

    1、构造函数的定义

    C++中有一种特殊的成员函数,它的名字和类名相同,可以有形参,但没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。

    2、构造函数的作用

    构造函数在创建对象时自动执行,因此我们可以写一个构造函数,在该函数内部,将构造函数的形参赋值给成员变量,这样一来,在创建对象的同时也对成员变量进行了赋值。构造函数在实际开发中会大量使用,它往往用来做一些初始化工作,例如对成员变量赋值、预先打开文件等,当然你也可以让构造函数什么也不做,或者做一些提示工作。

    博文中,我们通过成员函数 setname()、setage()、setscore() 分别为成员变量 name、age、score 赋值,这样做虽然有效,但显得有点麻烦。有了构造函数,我们就可以简化这项工作,在创建对象的同时为成员变量赋值。

    请看下面的示例1代码:

    1. #include
    2. using namespace std;
    3. class Student{
    4. private:
    5. char *m_name;
    6. int m_age;
    7. float m_score;
    8. public:
    9. //声明构造函数
    10. Student(char *name, int age, float score);
    11. //声明普通成员函数
    12. void show();
    13. };
    14. //定义构造函数
    15. Student::Student(char *name, int age, float score){
    16. m_name = name;
    17. m_age = age;
    18. m_score = score;
    19. }
    20. //定义普通成员函数
    21. void Student::show(){
    22. cout<"的年龄是"<",成绩是"<
    23. }
    24. int main(){
    25. //创建对象时向构造函数传参
    26. Student stu("小明", 15, 92.5f);
    27. stu.show();
    28. //创建对象时向构造函数传参
    29. Student *pstu = new Student("李华", 16, 96);
    30. pstu -> show();
    31. return 0;
    32. }
    1. 小明的年龄是15,成绩是92.5
    2. 李华的年龄是16,成绩是96

    (1)该例在 Student 类中定义了一个构造函数Student(char *, int, float),它的作用是给三个 private 属性的成员变量赋值。要想调用该构造函数,就得在创建对象的同时传递实参,并且实参由英文小括号( )包围,和普通的函数调用非常类似。

    (2)在栈上创建对象时,实参位于对象名后面,例如Student stu("小明", 15, 92.5f);在堆上创建对象时,实参位于类名后面,例如new Student("李华", 16, 96)

    (3)构造函数必须是 public 属性的,否则创建对象时无法调用。当然,设置为 private、protected 属性也不会报错,但是没有意义。

    (4)构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着:

    • 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许;
    • 函数体中不能有 return 语句。

    3、构造函数的重载

    和普通成员函数一样,构造函数是允许重载的。

    一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数,而且必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。

    构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。比如对于上面示例代码,如果写作Student stu或者new Student就是错误的,因为类中包含了构造函数,而创建对象时却没有调用。

    如下面的示例2代码:

    1. #include
    2. using namespace std;
    3. class Student{
    4. private:
    5. char *m_name;
    6. int m_age;
    7. float m_score;
    8. public:
    9. Student();
    10. Student(char *name, int age, float score);
    11. void setname(char *name);
    12. void setage(int age);
    13. void setscore(float score);
    14. void show();
    15. };
    16. Student::Student(){
    17. m_name = NULL;
    18. m_age = 0;
    19. m_score = 0.0;
    20. }
    21. Student::Student(char *name, int age, float score){
    22. m_name = name;
    23. m_age = age;
    24. m_score = score;
    25. }
    26. void Student::setname(char *name){
    27. m_name = name;
    28. }
    29. void Student::setage(int age){
    30. m_age = age;
    31. }
    32. void Student::setscore(float score){
    33. m_score = score;
    34. }
    35. void Student::show(){
    36. if(m_name == NULL || m_age <= 0){
    37. cout<<"成员变量还未初始化"<
    38. }else{
    39. cout<"的年龄是"<",成绩是"<
    40. }
    41. }
    42. int main(){
    43. //调用构造函数 Student(char *, int, float)
    44. Student stu("小明", 15, 92.5f);
    45. stu.show();
    46. //调用构造函数 Student()
    47. Student *pstu = new Student();
    48. pstu -> show();
    49. pstu -> setname("李华");
    50. pstu -> setage(16);
    51. pstu -> setscore(96);
    52. pstu -> show();
    53. return 0;
    54. }
    1. 小明的年龄是15,成绩是92.5
    2. 成员变量还未初始化
    3. 李华的年龄是16,成绩是96

    构造函数Student(char *, int, float)为各个成员变量赋值,构造函数Student()将各个成员变量的值设置为空,它们是重载关系。根据Student()创建对象时不会赋予成员变量有效值,所以还要调用成员函数 setname()、setage()、setscore() 来给它们重新赋值。

    4、默认构造函数

    如果用户没有定义构造函数,则编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。比如上面的 Student 类,默认生成的构造函数如下:

    Student(){}

    一个类必须要有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。比如 Student 类已经有了一个自定义的构造函数Student(char *, int, float),则编译器不会再额外添加构造函数Student()

    实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。

    最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于示例2的代码,在栈上创建对象可以写作Student stu()Student stu,在堆上创建对象可以写作Student *pstu = new Student()Student *pstu = new Student,它们都会调用构造函数 Student()。

    以前我们就是这样做的,创建对象时都没有写括号,其实是调用了默认的构造函数。

    5、初始化列表

    构造函数的一项重要功能是对成员变量进行初始化。为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表,它会使得代码更加简洁。

    比如下面的示例代码3:

    1. #include
    2. using namespace std;
    3. class Student{
    4. private:
    5. char *m_name;
    6. int m_age;
    7. float m_score;
    8. public:
    9. Student(char *name, int age, float score);
    10. void show();
    11. };
    12. //采用初始化列表
    13. Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
    14. //TODO:
    15. }
    16. void Student::show(){
    17. cout<"的年龄是"<",成绩是"<
    18. }
    19. int main(){
    20. Student stu("小明", 15, 92.5f);
    21. stu.show();
    22. Student *pstu = new Student("李华", 16, 96);
    23. pstu -> show();
    24. return 0;
    25. }

    (1)定义构造函数时其函数体为空(可以有其他语句),在函数首部与函数体之间添加冒号:,后面紧跟m_name(name), m_age(age), m_score(score)语句,这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;语句,也是赋值的意思。

    (2)使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。

    (3)初始化列表可以用于全部成员变量,也可以只用于部分成员变量。下面的示例只对 m_name 使用初始化列表,其他成员变量还是一一赋值:

    1. Student::Student(char *name, int age, float score): m_name(name){
    2. m_age = age;
    3. m_score = score;
    4. }

    (4)成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关,比如下面代码:

    1. #include
    2. using namespace std;
    3. class Demo{
    4. private:
    5. int m_a;
    6. int m_b;
    7. public:
    8. Demo(int b);
    9. void show();
    10. };
    11. Demo::Demo(int b): m_b(b), m_a(m_b){ }
    12. void Demo::show(){ cout<", "<
    13. int main(){
    14. Demo obj(100);
    15. obj.show();
    16. return 0;
    17. }
    -858993460, 100

    在初始化列表中,我们将 m_b 放在了 m_a 的前面,看起来是先给 m_b 赋值,再给 m_a 赋值,其实不然!成员变量的赋值顺序由它们在类中的声明顺序决定,在 Demo 类中,我们先声明的 m_a,再声明的 m_b,所以构造函数和下面的代码等价:

    1. Demo::Demo(int b): m_b(b), m_a(m_b){
    2. m_a = m_b;
    3. m_b = b;
    4. }

    给 m_a 赋值时,m_b 还未被初始化,它的值是不确定的(obj 在栈上分配内存,成员变量的初始值是不确定的),所以输出的 m_a 的值是一个奇怪的数字;给 m_a 赋值完成后才给 m_b 赋值,此时 m_b 的值才是 100。

    初始化 const 成员变量

    构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const 成员变量的唯一方法就是使用初始化列表。

    例如 VS/VC 不支持变长数组(数组长度不能是变量),我们自己定义了一个 VLA 类,用于模拟变长数组,请看下面的代码:

    1. class VLA{
    2. private:
    3. const int m_len;
    4. int *m_arr;
    5. public:
    6. VLA(int len);
    7. };
    8. //必须使用初始化列表来初始化 m_len
    9. VLA::VLA(int len): m_len(len){
    10. m_arr = new int[len];
    11. }

    VLA 类包含了两个成员变量,m_len 和 m_arr 指针。需要注意的是 m_len 加了 const 修饰,只能使用初始化列表的方式赋值,如果写作下面的形式是错误的:

    1. VLA::VLA(int len){
    2. m_len = len;
    3. m_arr = new int[len];
    4. }

     

    二、析构函数的详解

    1、析构函数的定义

    析构函数(Destructor)是一种特殊的成员函数,它的名字是在类名前面加一个~符号,没有返回值,没有参数,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。

    2、析构函数的作用

    创建对象时系统会自动调用构造函数进行初始化工作,销毁对象时系统也会自动调用析构函数来进行清理工作,例如释放分配的内存、关闭打开的文件等。

    比如上节我们定义了一个 VLA 类来模拟变长数组,它使用一个构造函数为数组分配内存,这些内存在数组被销毁后不会自动释放,所以非常有必要再添加一个析构函数,专门用来释放已经分配的内存。请看下面的完整示例:

    1. #include
    2. using namespace std;
    3. class VLA{
    4. public:
    5. VLA(int len); //构造函数
    6. ~VLA(); //析构函数
    7. public:
    8. void input(); //从控制台输入数组元素
    9. void show(); //显示数组元素
    10. private:
    11. int *at(int i); //获取第i个元素的指针
    12. private:
    13. const int m_len; //数组长度
    14. int *m_arr; //数组指针
    15. int *m_p; //指向数组第i个元素的指针
    16. };
    17. VLA::VLA(int len): m_len(len){ //使用初始化列表来给 m_len 赋值
    18. if(len > 0){ m_arr = new int[len]; /*分配内存*/ }
    19. else{ m_arr = NULL; }
    20. }
    21. VLA::~VLA(){
    22. delete[] m_arr; //释放内存
    23. }
    24. void VLA::input(){
    25. for(int i=0; m_p=at(i); i++){ cin>>*at(i); }
    26. }
    27. void VLA::show(){
    28. for(int i=0; m_p=at(i); i++){
    29. if(i == m_len - 1){ cout<<*at(i)<
    30. else{ cout<<*at(i)<<", "; }
    31. }
    32. }
    33. int* VLA::at(int i){
    34. if(!m_arr || i<0 || i>=m_len){ return NULL; }
    35. else{ return m_arr + i; }
    36. }
    37. int main(){
    38. //创建一个有n个元素的数组(对象)
    39. int n;
    40. cout<<"Input array length: ";
    41. cin>>n;
    42. VLA *parr = new VLA(n);
    43. //输入数组元素
    44. cout<<"Input "<" numbers: ";
    45. parr -> input();
    46. //输出数组元素
    47. cout<<"Elements: ";
    48. parr -> show();
    49. //删除数组(对象)
    50. delete parr;//第53行
    51. return 0;
    52. }
    1. Input array length: 5
    2. Input 5 numbers: 11 22 33 44 55 66 #这里输入6个数,但根据代码会被截断
    3. Elements: 11, 22, 33, 44, 55

    (1) ~VLA()就是 VLA 类的析构函数,根据它函数体的代码,可知它的作用是在删除对象(第 53 行代码)后释放已经分配的内存。函数名是标识符的一种,原则上标识符的命名中不允许出现~符号,在析构函数的名字中出现的~可以认为是一种特殊情况,目的是为了和构造函数的名字加以对比和区分。

    (3)注意:at() 函数只在类的内部使用,所以将它声明为 private 属性;m_len 变量不允许修改,所以用 const 进行了限制,这样就只能使用初始化列表来进行赋值。

    (3)C++ 中的 new 和 delete 分别用来分配和释放内存,它们与C语言中 malloc()、free() 最大的一个不同之处在于:用 new 分配内存时会调用构造函数,用 delete 释放内存时会调用析构函数。构造函数和析构函数对于类来说是不可或缺的,所以在C++中我们非常鼓励使用 new 和 delete。 

    3、析构函数的执行时机

    构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。

    (1)在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数

    (2)在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。

    (3)new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。

    下面的例子演示了析构函数的执行。

    1. #include
    2. #include
    3. using namespace std;
    4. class Demo{
    5. public:
    6. Demo(string s);
    7. ~Demo();
    8. private:
    9. string m_s;
    10. };
    11. Demo::Demo(string s): m_s(s){ }
    12. Demo::~Demo(){ cout<
    13. void func(){
    14. //局部对象
    15. Demo obj1("1");
    16. }
    17. //全局对象
    18. Demo obj2("2");
    19. int main(){
    20. //局部对象
    21. Demo obj3("3");
    22. //new创建的对象
    23. Demo *pobj4 = new Demo("4");
    24. func();
    25. cout<<"main"<
    26. return 0;
    27. }
    1. 1
    2. main
    3. 3
    4. 2

    分析可知,func函数执行结束时,会调用(在func这个函数中创建的对象)obj1这个对象的析构函数,因此输出1;

    然后main函数中输出代码输出“main”;

    接着因为“retunr 0”,main函数执行结束时,会调用(在main这个函数中创建的对象)obj3这个对象的析构函数,因此输出3;

    最后整个程序执行结束时,会调用(在所有函数之外创建的对象)obj2这个对象的析构函数,因此输出2。

    4、析构函数的其他说明

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

    (2)如果用户没有定义,编译器会自动生成一个默认的析构函数。

     

  • 相关阅读:
    Vue面试题
    nodejs开发的小程序 老年人健康预警系统
    Dubbo学习
    centos7.7 安装glibc的问题
    物流快递信息查询管理系统网站(JSP+HTML+MySQL)
    【计算机网络】UDP/TCP协议
    第六篇:常用Linux命令
    数据库索引
    回收站永久删除了如何恢复正常?
    Go 多版本管理工具
  • 原文地址:https://blog.csdn.net/oqqHuTu12345678/article/details/133816031