第10章 对象和类——对象和类(二) 类的构造函数和析构函数

本文详细介绍了C++中的构造函数、默认构造函数以及析构函数的概念、用法和区别,包括如何显式和隐式调用,以及它们在对象生命周期中的作用。
摘要由CSDN通过智能技术生成

本文章是作者根据史蒂芬·普拉达所著的《C++ Primer Plus》而整理出的读书笔记,如果您在浏览过程中发现了什么错误,烦请告知。另外,此书由浅入深,非常适合有C语言基础的人学习,感兴趣的朋友可以自行阅读此书籍。

构造函数

C++的目标之一是让使用类对象就像使用标准类型一样。

还是以上一节的类声明为例。

// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;
class Student{
private:
    string id;             //为方便处理,将符数组改为string类型
    string name;           //为方便处理,将字符数组改为string类型
    int yuwen;
    int shuxue;
    int yingyu;
    int total;
    int avr;
    void set_total() { total = yuwen + shuxue + yingyu;}
    void set_avr() { avr = total/3;}
public:
    void set_info();
    void display();
};

#endif

我们无法像初始化int或结构那样初始化Student对象,如下面的方式:

int year = 2024;                    //正确
struct Animal{
  char *name;
  int age;
}; 
Animal am = {"cat", 3};             //正确
Student st = {"00003", "xiaodong", 32, 34, 34, 0, 0};   //错误

有个疑问是,为什么struct可以但是class不行?
那是因为struct的数据成员,默认是public的,因此外部函数可以直接访问,所以可以初始化成功。
而class的数据成员,我们指定了是private的,因此外部函数不可以访问,所以不可以初始化成功。

可以将类改成这种样子(不能这么做,只是为了说明):

// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;

class Student{
public:
    string id;             
    string name;          
    int yuwen;
    int shuxue;
    int yingyu;
    int total;
    int avr;
    void set_total() { total = yuwen + shuxue + yingyu;}
    void set_avr() { avr = total/3;}
    void set_info();
    void display();
};

#endif

这样的话,就可以直接初始化了,但这样使数据成员公有,不满足类的一个主要原则:数据隐藏。,因此我们不能这么做。

那么应该如何初始化呢?

C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。

构造函数的原型和函数头的特征是,既没有返回值,也没有被声明为void类型。

声明和定义构造函数

我们Student类有7个数据成员,需要初始化的是id、name、yuwen、shuxue和yingyu,total和avr需要计算得到,因此不需要提供此值。

构造函数原型如下:

Student(const string &id, const string &name,
             int yuwen = 0, int shuxue = 0, int yingyu = 0);

有个隐藏问题,构造函数中,形参列表表示不是类成员,而是赋给成员的值。因此参数名不能够与类成员相同,否则代码将是这样的:

yuwen = yuwen;

为了避免这种问题,一种常见的做法是在数据成员命中使用m_前缀。

我们把构造函数原型放在类声明中,同时修改数据成员名称:

// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;

class Student{
private:
    string m_id;
    string m_name;
    int m_yuwen;
    int m_shuxue;
    int m_yingyu;
    int m_total;
    int m_avr;
    void set_total() { m_total = m_yuwen + m_shuxue + m_yingyu;}
    void set_avr() { m_avr = m_total/3;}
public:
    Student(const string &id, const string &name, 
            int yuwen = 0, int shuxue = 0, int yingyu = 0);
    void display();
};

#endif

另外,我们再实现构造函数:

//student.cpp
#include <iostream>
#include <string>
#include "student.hpp"
using  namespace std;

Student::Student(const string &id, const string &name, int yuwen, int shuxue, int yingyu)
{
    m_id = id;
    m_name = name;
    m_yuwen = yuwen;
    m_shuxue = shuxue;
    m_yingyu = yingyu;
}

void Student::display()
{
   cout << "show info: " << endl; 
   cout << m_id << " " << m_name << endl 
        << "yuwen: " << m_yuwen << endl 
        << "shuxue: " << m_shuxue << endl 
        << "yingyu: " << m_yingyu<< endl 
        << "total : " << m_total << endl
        << "avr : " << m_avr << endl; 
}

使用构造函数

C++提供了两种使用构造函数来初始化对象的方式:
  1. 显示地调用构造函数。
  2. 隐式地调用构造函数。

我们先来看如何显示调用:

Student st1 = Student("00001", "wangdamao", 89, 90, 87);

再看看隐式调用:

Student st2("00002", "xiaoming", 81, 96, 91);

这种方式更加紧凑,并且与显示调用等价。

每次创建类对象时,C++都会使用类构造函数,即使是使用new动态分配内存时。

Student * pst = new Student("00003", "hanmeimei", 83, 92, 95);

这条语句创建一个Student对象,将其初始化为参数提供地值,并将该对象的地址赋给pst指针。在这种情况下,对象没有名称,但可以使用指针来管理该对象。(关于对象指针,我们将在第11章进一步学习)

需要注意的一点是,在构造函数构造出对象之前,对象是不存在的。也就是说,构造函数被用来创建对象,而不能通过对象来调用。

默认构造函数

我们可以定义一个整型变量,但是不给它赋初始值。如:
int x;

因此,我们的Student类型,也应该支持这种写法:

Student st;

但是在我们上面的例子中,如果使用Student类时,想要定义一个没有初始值的对象,编译却会报错。

那是因为,如果我们没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作。但是如果我们提供了构造函数,那么C++就不会自动提供默认构造函数了。

在上面的Student类中,我们定义了这样的构造函数:


Student::Student(const string &id, const string &name, 
                int yuwen, int shuxue, int yingyu, 
                int total = 0, int avr = 0)

那么,我们就只能满足这个构造函数的要求来定义对象。如果我们想要使用Student st的方式。那么我们需要在类中定义默认构造函数。

定义默认构造函数的方式有两种:

  1. 给已有构造函数的所有参数提供默认值。
  2. 通过函数重载来定义另一个没有参数的构造函数。

第一种方式如下:


Student::Student(const string &id = "no id", const string &name = "no name", 
                int yuwen = 0, int shuxue = 0, int yingyu = 0, 
                int total = 0, int avr = 0)

第二种方式如下,定义一个新的构造函数,给所有的成员提供隐式初始值。

Student::Student()
{
  m_id = "no id";
  m_name = "no name";
  m_yuwen = 0;
  m_shuxue = 0;
  m_yingyu = 0;
  m_total = 0;
  m_avr = 0;
}

需要注意的一点是,这两种方式同时只能使用一种,否则会出现二义性的错误。

那么我们应该用哪种方式呢?

在设计类时,通常我们使用第二种方式,也就是通过默认构造函数的方式来对所有类成员做隐式初始化。

在增加默认构造函数之后,我们就可以使用如下的方式来声明对象变量了。

Student st1;                //隐式调用默认构造函数
Stduent st2 = Student();    //显示调用默认构造函数
Student *st = new Student;  //隐式调用默认构造函数

一个疑点,怎么区分,构造函数的调用方式是显示调用还是隐式调用呢?

最简单的一种分辨方法是,如果看到了函数的完整调用,一般是显示调用,否则是隐式调用。
比如上面第二个语句中使用的Student(),就是我们类声明中的定义的默认构造函数,所以它是显示调用。而第一和第三个语句,并没有直接使用Student(),因此它们为隐式调用。

析构函数 用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数————析构函数。析构函数完成清理工作。

如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除地代码后,提供默认析构函数的定义。

如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。如果构造函数没有使用new,那么析构函数实际上没有需要完成的任务。在这种情况下,只需让编译器生成一个什么都不做的隐式析构函数即可。

析构函数的名称比较特殊,需要在类名前加上。因此,Student类的析构函数为Student()。和构造函数一样,析构函数也没有返回值和声明类型。和构造函数不同的是,析构函数没有参数,因此Student析构函数的原型必须是如下的方式:
~Student();
而Student类的析构函数不承担任何重要的工作,因此它的函数实现如下:

Student::~Student()
{
}

什么时候应该调用析构函数呢?
这由编译器决定,通常不应在代码中显示地调用析构函数,一般来说有四种情况:

  1. 如果创建地是静态存储类对象,如static Student st1,则其析构函数将在程序结束时自动被调用。
  2. 如果创建的是自动存储类对象,如student st2,则其析构函数将在程序执行完对象所处的代码块时自动被调用。
  3. 如果对象是通过new 创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。
  4. 程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时,自动调用其析构函数。

目前我们已经知道了构造函数、默认构造函数、析构函数的使用方法,现在我们重新把Student类修改下:

// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;
class Student{
private:
    string m_id;
    string m_name;
    int m_yuwen;
    int m_shuxue;
    int m_yingyu;
    int m_total;
    int m_avr;
    void set_total() { m_total = m_yuwen + m_shuxue + m_yingyu;}
    void set_avr() { m_avr = m_total/3;}
public:
    Student();
    Student(const string &id, const string &name, 
            int yuwen = 0, int shuxue = 0, int yingyu = 0);
    ~Student();
    void display();
};

#endif

再修改下类方法的实现:

//student.cpp
#include <iostream>
#include <string>
#include "student.hpp"
using  namespace std;
Student::Student()
{
    m_id = "no id";
    m_name = "no name";
    m_yuwen = 0;
    m_shuxue = 0;
    m_yingyu = 0;
    m_total = 0;
    m_avr = 0;
    cout << "create obj: " << this->m_name << endl;
}
Student::~Student()
{
    cout << "delete obj: " << this->m_name << endl;
}
Student::Student(const string &id, const string &name, int yuwen, int shuxue, int yingyu)
{
    m_id = id;
    m_name = name;
    m_yuwen = yuwen;
    m_shuxue = shuxue;
    m_yingyu = yingyu;
    set_total();
    set_avr();
    cout << "create obj: " << this->m_name << endl;
}
void Student::display()
{
   cout << "show info: " << endl; 
   cout << m_id << " " << m_name << endl 
        << "yuwen: " << m_yuwen << endl 
        << "shuxue: " << m_shuxue << endl 
        << "yingyu: " << m_yingyu<< endl 
        << "total : " << m_total << endl
        << "avr : " << m_avr << endl; 
}

然后在main函数中,可以如下使用:

//student_main.cpp
#include <iostream>
#include "student.hpp"
using namespace std;

int main()
{
  {
     static Student st1("00001", "xiaoming");
     cout << endl;
     Student st2("00002", "Dabai");
     cout << endl;
  }
  cout << endl;
  Student st3 = Student("00003", "wangdamao", 89,98,83);  
  cout << endl;

  Student* pst4 = new Student("00004", "Lilei");
  cout << endl;

  delete pst4;
  cout << endl; 
  
  Student st5;
  cout << endl;

  return 0;
}

程序结果如下,为了方便说明情况,加了一些空白行:

create obj: xiaoming

create obj: Dabai

delete obj: Dabai

create obj: wangdamao

create obj: Lilei

delete obj: Lilei

create obj: no name

delete obj: no name
delete obj: wangdamao
delete obj: xiaoming

简单分析下main函数和程序执行结果:

我们在代码块中{}创建了两个对象,其都调用的是带参数的构造函数,一个是静态存储类对象st1(create obj: xiaoming),一个是自动存储类对象st2(create obj: Dabai),我们看到,代码块结束后,st2自动调用了析构函数(delete obj: Dabai)。而st1未被析构。

代码块结束后,我们接着继续创建了自动存储类对象st3(create obj: wangdamao) 和 使用new创建的对象指针pst4(create obj: Lilei),接着我们又使用delete pst4的方式,销毁了这个对象(delete obj: Lilei)。

最后我们创建了一个调用默认构造函数的对象st5,因为不带参数,因此打印(create obj: no name)。

当程序结束后,按照先构造的后析构,后构造的先析构的原则,依次自动析构了st5、st3、st1。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值