【C++】类和对象(2)(默认成员函数--构造与析构)

用户没有显式实现,编译器会生成的成员函数称为默认成员函数
6个默认成员函数的主要功能是:初始化和清理,拷贝和复制,对普通对象和const修饰的对象取地址重载。
本文注重介绍构造与析构。

默认成员函数

构造函数

构造函数定义

构造函数是特殊的成员函数,构造函数虽然名叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

举例:

class Data
{
public:

Data()     //无参构造函数
{
_year=1;
_month=1;
}

Data(int year,int month)    //有参构造函数
{
_year=year;
_month=month;
}

void Print()
{
cout<<_year<<_"-"<<_month<<endl;
}

private:

int _year;
int _month;
};

int main()
{
Date d1;    //通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
d1.Print();

Date d2(2024,4);
d2.Print();
}

构造函数的语法及特点:

  1. 函数名和类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载
  1. 如果类里面没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数,用户若显式定义那么编译器就不再生成
  1. 编译器生成的默认构造函数对内置类型不做处理,对自定义类型调用其默认构造函数,所以C++11对默认构造函数对基本类型不做处理这一缺陷做了改进,可以对内置类型声明给缺省值。
class Date
{
private:
 // 基本类型(内置类型)
 int _year = 1970;  //声明给缺省值;
 int _month = 1;
 int _day = 1;
 // 自定义类型
 Time _t;
};

补充知识–内置类型

那么什么是内置类型和自定义类型呢?

内置类型指的是编程语言中已经定义好的基本数据类型,通常由编译器直接支持。常见的内置类型包括整型(int、long、short等)、浮点型(float、double等)、字符型(char)、布尔型(bool)等。
自定义类型指的是通过用户自己定义的数据类型。在很多编程语言中,可以使用结构体(struct)或类(class)等机制来定义自己的数据类型。

默认构造函数

不传参就可以调用的构造函数是默认构造函数,默认构造函数只能有一个,常见的三种:无参构造函数全缺省构造函数编译器自己生成的构造函数

对如何使用构造函数的理解

其实自定义类型无非是一些内置类型的组合,最后对数据的处理还是对内置类型处理。
如果我们没写任何一个构造函数,编译器就会自动生成一个默认的无参构造,且默认生成的构造函数,对于内置类型不做处理,对于自定义类型会去调用它的默认构造。
所以需要写构造函数就自己写,不用编译器会自己生成。

初始化列表

引入

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

> class Date
{
public:
Date(int year, int month, int day)
 {
     _year = year;
     _month = month;
     _day = day;
      // 这里可以再次对成员变量进行赋值
    _year = _year + 1;
 }
private:
int _year;
int _month;
int _day;
};

在构造函数体内,可以通过多次赋值语句为对象的成员变量赋值。这意味着在构造函数体内,可以通过多个赋值语句来修改对象的成员变量的值。

在上述例子中,_year 成员变量在构造函数体内首先被赋值为传入的 year 参数,然后再赋值为 _year + 1。这样,对象在创建时 _year 的初始值为 year + 1。

虽然在构造函数体内可以进行多次赋值,但这并不是进行多次初始化的意思,而只是为成员变量赋予不同的值。

初始化和赋值的区别在于:

初始化是在对象创建的时候为成员变量分配内存并设置初始值。
赋值是在对象创建后,已经有了初始值的情况下将新的值赋给成员变量。

C++中引入了初始化列表用来更高效地初始化对象的成员变量。

1.在初始化列表中,可以直接对成员变量进行初始化,而不需要先创建一个默认构造函数再通过赋值语句进行初始化。
2.初始化列表可以按照成员变量的声明顺序来初始化成员变量,而不是按照成员变量在构造函数体内的赋值顺序来初始化。这在某些情况下可以避免潜在的问题,比如成员变量之间存在依赖关系时。
3.对于某些成员变量,可能存在只能通过初始化列表来初始化的情况。

初始化列表的用法

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
举例:

#include <iostream>

class Person {
public:
    Person(const std::string& name, int age) 
        : _name(name)
        , _age(age) {
        //可以在里面对变量进行赋值
        //_age+=1;
    }
    
    void printInfo() {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }
    
private:
    std::string _name;
    int _age;
};

int main() {
    Person john("John", 25);
    john.printInfo();
    
    return 0;
}
 
注意要点:

使用初始化列表时注意:

  1. 每个成员变量在初始化列表中只能出现一次(因为初始化只能初始化一次)

  2. 类中包含以下类型的成员,必须放在初始化列表位置进行初始化:
    引用成员变量
    const成员变量 (因为创建了就不能被修改了,只能在初始化时进行赋值)
    自定义类型成员(且该类没有默认构造函数时)

  3. 尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,一定会优先使用初始化列表初始化。

  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

析构函数

析构函数定义

析构函数不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

析构函数的特点:

  1. 函数名前加 ~
  2. 无参无返回值
默认的析构函数
  1. 一个类就只能有一个析构函数,析构函数不可以重载,若未显式定义,系统会自动生成默认的析构函数。与构造函数类似,编译器生成的默认析构函数对内置类型成员不做处理,对自定义类型成员调用它的析构函数。

可以结合下面的例子来理解一下:

class Time
{
public:
 ~Time()
 {
 cout << "~Time()" << endl;
 }
private:
 int _hour;
 int _minute;
 int _second;
};
class Date
{
private:
 // 基本类型(内置类型)
 int _year = 1970;
 int _month = 1;
 int _day = 1;
 // 自定义类型
 Time _t;
};
int main()
{
 Date d;
 return 0;
}

运行结果最后是~Time(),从结果不难看出,编译器调用了Time类的析构函数。可是并没有定义一个Time类的变量,为什么会调用它的析构函数呢?
这是因为main函数中创建了Date d,没有显示定义Date类的析构函数,编译器会给Date类生成一个默认的析构函数。
d中包括了三个内置类型成员变量_year,_month,_day,对于内置类型成员来说,销毁时不需要资源清理,最后系统会直接将其内存回收;但对于自定义类型成员变量_t来说,会调用其对应的析构函数。
(有种套娃的感觉,其实与构造函数类似,对自定义类型的处理归根结底还是对内置类型的处理)

  1. 对象生命周期结束时,编译器系统自动调用析构函数(调用顺序为局部->局部静态->全局(后定义的先析构))
显示定义析构函数
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数;有资源申请时,一定要写,否则会造成资源泄漏。一个类中若存在资源的申请,如堆内存的动态分配、文件的打开等,那么默认生成的析构函数就不能满足类的需求了。这是因为默认析构函数只会释放对象中的内存空间,并不会主动释放类所申请的资源。

Date 类是一个简单的日期类,它只包含三个整数类型的成员变量 yearmonthday,并没有使用动态内存分配或其他资源的申请。在这种情况下,编译器生成的默认析构函数就足够了,不需要额外编写析构函数。因为在对象销毁时,这些成员变量也会被销毁,所以不需要额外的清理动作。

接下来,我们考虑一个有资源申请的类 StackStack 类是一个栈类,它使用动态内存分配来管理栈的元素。在 Stack 类的构造函数中,会通过 new 运算符申请一个动态数组作为栈的存储区域。在这种情况下,我们需要手动编写析构函数来释放这个动态数组,以避免内存泄漏。

class Stack {
private:
    int* stackArray;
    int top;
    int size;
public:
    Stack(int size) {
        this->size = size;
        stackArray = new int[size];
        top = -1;
    }
    ~Stack() {
        delete[] stackArray;
    }
};

在上面的代码中,使用了析构函数 ~Stack() 来释放 stackArray 动态数组所占用的内存。在对象销毁时,析构函数将会被调用,从而保证资源的正确释放。

总结来说,如果一个类中没有资源的申请,可以使用编译器生成的默认析构函数;而如果一个类中存在资源的申请,必须手动编写析构函数来释放这些资源,以防止资源泄漏。

小结

在这里插入图片描述

本文主要介绍默认成员函数中的构造与析构,从定义,特点,使用说明等方面具体阐述,尤其注意的是构造函数的初始化列表在后续的学习中会经常使用,后续接着介绍拷贝构造与赋值重载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值