C++入门篇之6大默认函数

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++入门篇之6大默认函数相关的知识,希望对你有一定的参考价值。

前言

上一章,博主介绍了类的该来以及使用,现在我们开始更加深入的章节吧,那就是类的默认函数,而我们要着重讲解的将会是前4个默认函数,至于最后两个,因为最后两个几乎没什么用


默认函数的引入

上一节我们已经知道了类,现在博主有个小问题,就是如果我们定义一个空类,比如下面:

class Date
{
};

Date类是否真的就是空的呢?答案是否定的,在这个空类里面还有6个成员函数哦,并且他们都是编译器自动调用~

这六个成员函数分别是构造函数,析构函数,拷贝构造,赋值构造以及取地址重载(有两个).


构造函数

概念:

注意哦~,大家不要被这个名字给误导了,构造函数的作用不是构造,而是对类对象进行初始化.

它是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次

特征:

  • 无返回值(注意注意…,不是说返回值为void,而是指不写)
  • 函数名与类名相同
  • 编译器自动调用
  • 支持重载

按照上面的特征,我们写一个日期类,并且加上一个构造函数试试:

class Date
{
public:
    Date()        //定义一个无参构造函数
    {
        _year = 1999;  
        _month = 8;
        _day = 26;
    }
    Date(int year, int month, int day)    //定义一个有参数构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

构造函数的调用方式为:定义对象时,在对象后面加括号,然后按照构造函数的定义依次传参,如下

int main()
{
    Date d1(2021,12,13); //对d1进行了初始化,让year等于2021  month等于12  day等于13
    return 0;
}

效果如上图

注意点:如果我们还定义了无参数构造函数,那么在定义对象的时候,只需要写对象名就行,不需要加括号,否则就变成了函数声明.如下

Date d2;  //这样系统便会自动调用无参构造函数.
Date d3();  //注意哦,如果这样写,就变成了一个函数声明,表示一个叫d3的函数,没有形参,返回值类型为 Date

注意点1

如果我们在定义类时,没有显示的定义构造函数(手动构造的意思),那么C++编译器便会自动生成一个无参构造函数,相反,只要我们手动写了任何形式的构造函数,编译器都不会生成默认函数

class Date
{
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    return 0;
}

像上面的类一样,我们并没有显示定义构造函数,那么编译器就会自动生成一个构造函数,对数据进行初始化,调试效果如下:

咦?说好的自动生成构造函数,并且进行初始化呢?结果好像并不是想象中的那样哎~,其实并不是编译器没有处理,只是这是C++的一个小缺陷,它初始化成员属性时用的值刚好也是 基础类型变量没初始化时编译器给其赋的值,所以我们才看到如上效果.

但是其构造函数会对自定义类型进行一定处理,什么处理呢?那就是通过自身的构造函数去调用自定义类型的构造函数.

class MMM
{
public:
    MMM()
    {
        da = 0;
    }
private:
    int da;
};
class Date
{
private:
    int _year;
    int _month;
    int _day;
    MMM a;
};
int main()
{
    Date d1;
    return 0;
}

可以清晰的发现,a的构造函数被调用了


注意点2

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、编译器自动生成的构造函数,都可以认为是默认成员函数,这一点一定要记清楚哦

我们仍然以日期类举例

class Date
{
public:
    Date()        //定义一个无参构造函数
    {
    }
    Date(int year = 1999, int month = 12, int day = 13)    //定义一个有参数构造函数
    {
    }
private:
    int _year;
    int _month;
    int _day;
};

像上面这样,无参构造和全缺省构造只能存在一个,大家可以想想为什么?原因是会引起歧义,比如我们定义类对象时,这样写Date d1;这种形式,编译器到底该调用哪一个构造函数呢?是不是不知道了?


析构函数

析构函数的作用清理资源(释放动态内存),其写法和构造函数及其相似

特征:

  • 函数名是~类名,并且~在前面.

  • 无参数,无返回值,无返回值什么意思构造函数讲解了哦~

  • 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数

  • 对象生命周期结束时,C++编译系统自动调用析构函数

typedef int DataType;
class SeqList
{
public :
    SeqList (int capacity = 10)   //构造函数
    {
        _pData = (DataType*)malloc(capacity * sizeof(DataType));
        assert(_pData);
        _size = 0;
        _capacity = capacity;
    }
    ~SeqList()                  //析构函数
    {
        if (_pData)
        {
            free(_pData ); // 释放堆上的空间
            _pData = NULL; // 将指针置为空
            _capacity = 0;
            _size = 0;
        }
    }
private :
    int* _pData ;
    size_t _size;
    size_t _capacity;
};

注意点:析构函数和构造函数类似,对于基础类型(int,char等)相当于没做处理,而对于自定义类型来说,该类对象的析构函数会调用其自定义类型的析构函数.


拷贝构造

顾名思义,就是在定义类对象时候,可以把另外同类型对象的值,拷贝进正在定义的对象中.

为了更能理解拷贝构造以及定义,博主在这里先介绍其使用.

使用方法:括号法和赋值法

括号法:

Date d1(2012,12,13);   //创建一个对象,并初始化.
Date d2(d1);  //这就代表在调用拷贝构造,作用是把d1的值复制一份给d2

赋值法:

Date d1(2012,12,13);   //创建一个对象,并初始化.
Date d2 = d1;  //这就代表在调用拷贝构造,作用是把d1的值复制一份给d2

现在熟悉了其使用以后,怎么进行显示定义呢?看下面

特征:

  • 属于构造函数的一种,也就是说函数名和类一样
  • 只有一个参数,并且参数类型与函数名一样

所以,这样定义是否对呢?比如有一个类叫做Date

Date(Date date)
{
}

答案是否定的,因为这样就陷入了一个死递归状态,为何?

假设我们开始使用拷贝构造了,如下:

Date d1(2012,12,13);  
Date d2(d1);

在调用过程中,d1会传给date形参,也就是等价于date = d1,好家伙,这一步又相当于在调用拷贝构造,于是又有一个形参date,然后date = d1无限死递归下去,下图更好解释:

所以,应该怎么解决这种情况呢?没错,这就需要用到我们前面讲过的引用,因为引起这种死递归的原因不就是值传递吗,我们把参数设置成引用就不会发生值传递了.

Date(const Date& date)
{
    _year = date._year;
    _month = date._month;
    _day = date._day;
}

我们知道,拷贝构造函数也是类的6大默认成员函数之一,那如果我们不写显示拷贝构造,编译器自动生成的拷贝构造会怎么样呢?如下:

class Date
{
public:
    Date(int year, int month, int day)  
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1(2021,8,15);
    Date d2(d1);
    return 0;
}

现在进行监视:

我们发现,即使我们不写拷贝构造函数,编译器会自动帮我们进行拷贝,那是不是说,拷贝函数我们就不需要手写了呢?请看下面例子:

class String
{
public:
    String(const char* str = "jack")
    {
        _str = (char*)malloc(strlen(str) + 1);
        strcpy(_str, str);
    }
    ~String()
    {
        cout << "~String()" << endl;
        free(_str);
    }
private:
    char* _str;
};
int main()
{
    String s1("hello");
    String s2(s1);
    return 0;
}

我们运行,就会发现程序会崩溃,至于崩溃的原因就是第一次定义对象时,调用的是构造函数,在堆区开辟了一块空间,但是第二次定义对象时候,调用的是拷贝构造,也就是说,这一次并没有再开辟空间,而是直接把s1的内容复制一份给s2,那么s1和s2的成员都是指向了一块空间,但是会调用两次析构函数,同一块内存空间不可调用两次,所以会崩溃,这涉及到浅拷贝和深拷贝,博主会在后面的章节中单独介绍.这里我们只需要记住,如果只有基础类型,而不涉及动态空间,就可以不用写显示拷贝,反之需要.


赋值运算符重载构造

在讲赋值构造之前,我们先讲解一下运算符重载构造.


运算符重载

我们知道1 + 1 = 2,编译器同样也知道,因为他们是基础类型,都是int,那如果有一个类了.他们定义的对象这样相加减,什么意思呢?,比如:

Date d1(2012,12,13);
Date d2(2008,8,6);
d1 + d2;  

两个日期对象相加,是year相加,month相加,还是day相加呢?或者说全都相加?这会造成严重的理解偏差,所以才有运算符重载,他的意思就时再定义一下,使得对象运算有规定意义.

语法规则:

返回值类型 operator 操作符 (形参列表)

注意哦,新参列表是隐藏了一个this指针的哦,我们写形参列表时候,就要注意数量了,它往往比操作符需要的操作数数量少1.什么意思呢?比如+需要两个操作数,但是我们写运算符重载时,只需要写一个形参列表,因为还隐藏了一个this指针.

示例: 假设我们需要实现对象加整数,其意义是整数是加在month上,也就是实现+=.

void operator+=(int n)
{
    _month = _month + n;
}

那么我们指向操作d1+=6(原来d1日期是2020.2.3)后,d1将会变成2020.8.3,我们验证一下:

注意哦,虽然我们写的是+=,但是编译器在处理时是吧d1+=6翻译成了d1.operator+=(&d1,6);,这样才能明白为何会达到这样的效果.

赋值重载

现在我们明白了运算符重载什么意思,那我们试试赋值重载吧,应该怎样设计?

日期类为例:

void operator=(Date date)
{
    _year = date._year;
    _month = date._month;
    _day = date._day;
}

通过上面的例子,我们几乎可以认定为这是成功的,但是真的吗? 还记得=还有一个特性吗?那就是连续赋值,比如a =b =c=d;

如果我们也这样对类进行操作,可以吗?很明显,发生错误.

为什么会发生这样的错误呢?因为d2=d1时,编译器会翻译成d2.operator=(&d2,d1),而这个函数并没有返回值,那么d3也就接收不到参数了,所以会报错.修改如下:

Date operator=(Date date)
{
    _year = date._year;
    _month = date._month;
    _day = date._day;
    return *this;  //大家一定要记得this指针哦,它是被隐藏了的,但是支持我们显示写
}

这样就不会有问题了.但是大家想想上面是否还可以再修改呢? 答:可. 我们可以形参和返回值变为引用,这样就减少了形参拷贝.

再次修改:

Date& operator=(const Date& date)
{
    _year = date._year;
    _month = date._month;
    _day = date._day;
    return *this;
}

有人会问,我们不是已经有了拷贝构造函数吗?为何还需要赋值重载,直接拷贝不行吗?其实这里有一个误区哦~,拷贝构造使用地方是创建对象时,而赋值重载适用地方是已经存在两个同类型对象,然后把其中一个对象的值复制给另一个对象.下面例子刚好区分了他们:

Date d1(1,2,3);  //这是在构造初始化
Date d2();

运算符重载练习

我们需要一个日期类.

要求:

  • 实现日期加减整数,代表日期前进或回退整数天(即分别实现operator+(int day),operator-(int day),operator+=(int day),operate-=(int day))
  • 可以实现日期的比较,代表知道哪个日期在前或在后(即分别实现operator<(Date& date),operator<=(Date& date).....)
  • 实现日期减去日期,代表两个日期之间相距多少天(即分别实现operator-(Date& date)).
  • 需要实现一个输入当年当月可以知道当月有多少天的函数.
  • 实现日期的前置++,后置++
  • 实现日期的前置–,后置–
  • 实现赋值运算符重载

实现内容点击这


const成员

什么意思呢?我们先看看下面的例子:

class Date
{
public:
    void print()
    {
        cout<<"year是"<<_year<<endl;
        cout<<"month是"<<_month<<endl;
        cout<<"day是"<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    const Date d2;
    
    d1.print();
    d2.print();
    return 0;
}

大家运行一下,看看是什么结果,博主的运行结果是下面这样:

我们会发现,一直报d2的错误,那么d2到底哪里错了呢?其实这就是const的问题.我们知道d2的类型是const Date,那么d2在调用print函数时,系统便会传给print一个this指针(上一章节讲过).而this指针的类型是Date*,但是d2的地址类型是const Date*,将一个const修饰的指针传给普通指针接收,将会扩大权限这种情况和常量引用相似,所以为了解决这种办法,我们需要加上const修饰this指针,而加的地方就是在函数后面.

    void print() const   //注意看哦,是加在这里的
    {
        cout<<"year是"<<_year<<endl;
        cout<<"month是"<<_month<<endl;
        cout<<"day是"<<_day<<endl;
    }

同时,大家一定要注意,const加成员函数后面修饰的只有this指针,而不会修饰其他形参.


const4问

  1. const对象可以调用非const成员函数吗?

答:很明显,不可以,上面的d2调用print出错就是验证,权限被放大了

  1. 非const对象可以调用const成员函数吗?

答:很明显,可以的,因为未被const修饰的指针可以传给const修饰的指针,比如

int a = 10;
const int* b = &a;   //这样是可以的,权限可以缩小

const int c = 10;
int* d = &c;        //这样是不可以的,权限不能被放大
  1. const成员函数内可以调用其它的非const成员函数吗?

答:很明显,不可以,这个和第一问类似,只要调用成员函数就一定传this指针,但是被const修饰的函数去调用未被修饰的,就会扩大权限

  1. 非const成员函数内可以调用其它的const成员函数吗?

答:可以的,这和第二问类似,权限可以被缩小.


取地址及const取地址操作符重载

取地址重载其实就说使用operator&进行操作,这个很好实现

Date* operator&()
{
    return this;
}

而const取地址重载,其实就是说上面那个函数被const修饰了,我们只需要对返回值修改即可

const Date* operator&() const
{
    return this;
}

但是这两个函数即使我们不写,编译器也会自动生成,所以说这两个函数几乎没有太大作用.

以上是关于C++入门篇之6大默认函数的主要内容,如果未能解决你的问题,请参考以下文章

C++入门篇之关键字,命名空间,输入输出和函数重载

C++入门篇之模板基础讲解

C++入门篇之模板基础讲解

C++入门篇之string使用

C++入门篇之string使用

C++入门篇之内存处理