C++学习笔记(四)

一、拷贝构造函数

上篇结尾谈到,在C++中,类在定义时编译器会自动生成6个默认成员函数,如果我们自己已经实现,则编译器不会再生成。

分别是构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址操作符重载、const取地址操作符重载;

拷贝构造函数实际上是构造函数的函数重载,声明如下:

class Date
{
public:
    // 拷贝构造函数
    Date(const Date& d);
private:
    int _year;
    int _month;
    int _day;
};

拷贝构造函数无返回值类型,函数名必须与类名相同,参数只有一个,且必须是类类型对象的引用。

拷贝构造函数是用一个已经存在的对象去初始化另一个对象。

如图所示,d2在创建时用d1初始化,调用类的拷贝构造函数,将d1的值拷贝给d2。d3在创建时调用构造函数,用构造函数中的缺省值来初始化。

Date::Date(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
    cout << "Date(int year = 1900, int month = 1, int day = 1)" << endl;
    if (month < 1 || month>13 || day<1 || day>GetMonthDay(_year, _month))
    {
        cout << "输入错误" << endl;
    }
}
Date::Date(const Date& d)
{
    cout << "Date(const Date& d)" << endl;

    _year = d._year;
    _month = d._month;
    _day = d._day;
}

如图所示,d2调用了拷贝构造函数。

二、赋值运算符重载

  1. 运算符重载

在上述的日期类中,可能会需要计算某个日期再过几天之后是什么时间,例如计算2023/2/15再过100天是几月几日。我们当然可以用函数来实现;

Date Date::AddDay(int day);

调用的时候这样调用:

Date d1(2023, 2, 15);
d1.AddDay(100);

C++为了增强代码的可读性,支持了运算符重载。关键字是operator。

Date Date::operator+(int day);

// ...

Date d1(2023, 2, 15);
d1 + 100;

很明显,第二种代码的可读性更好。在运算符重载时,需要注意以下几点:

关键字operator后面接需要重载的运算符符号;

作为类成员函数重载时,形参比操作数少1个,是因为第一个参数默认是this。且传递的参数的顺序就是操作符的操作数的顺序。

Date Date::operator+(int day);

// ...

Date d1(2023, 2, 15);
d1 + 100;
//下面的情况会报错, 因为在重载+运算符时,第一个操作数是Date对象的指针,不是int
100 + d1;

有5个运算符无法重载:.* , :: , sizeof , ?: , . ;

2.赋值运算符重载

Date& operator=(const Date& d);

赋值运算符重载的声明如上,参数是一个const Date的引用,const表示d的内容无法被改变,同时,为了应对连续赋值的情况,需要一个Date类型的返回值,为了减少拷贝次数,使用引用做返回值。

Date& Date::operator=(const Date& d)
{
    cout << "operator=()" << endl;
    if (this != &d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    return *this;
}

需要注意的是,赋值运算符重载必须被定义为成员函数。当我们在全局声明赋值运算符重载时,因为在类中没有声明,所以编译器会自动生成一个默认的赋值运算符重载,在调用时两个运算符重载冲突了,所以赋值运算符重载只能是类的成员函数。

编译器默认生成的赋值运算符重载,是以值的方式逐字节拷贝,如果类中的成员有自定义成员变量,则会调用对应的赋值运算符重载。

3.前置++与后置++重载

前置++和后置++都是一元运算符,也就意味着他们的操作数都只有对象,为了能让前置++和后置++正确重载,C++规定:

后置++在重载时要多一个int类型的参数,但调用时不需要传参,由编译器自动传递;

// 前置++重载,返回++之后的值
Date& Date::operator++()
{
    *this += 1;
    return *this;
}
// 后置++重载,返回++之前的值
Date Date::operator++(int)
{
    Date d(*this);
    *this += 1;
    return d;
}

三、const成员函数

将const修饰的成员函数称之为const成员函数,const在修饰类的成员函数时,实际修饰的是该函数的隐含this指针,表明在该函数中,类的成员变量不可修改;

需要注意的是,const修饰类成员函数,需要放在函数的后面;

void Date::Print()const
{
    cout << _year << "/" << _month << "/" << _day << endl;
}

需要注意:

const对象不能调用非const成员函数,因为会导致权限的放大;

const成员函数内部不可以调用非const成员函数,也是会导致权限的放大。

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

// 取地址运算符重载
Date* Date::operator&()
{
    return this;
}
// const取地址操作符重载
const Date* Date::operator&()const
{
    return this;
}

这两个运算符一般不会由我们自己实现,只有在某些特定情况下,比如这个类不想让别人拿到它的地址。我们才需要显示实现。

五、初始化列表

一些变量在定义时必须赋初值,也就是必须初始化,例如const和引用:

    const int a = 0;
    // 下面是错误的写法
    const int b;

因为const修饰的变量的内容无法被改变,所以必须在变量定义时赋值。

有时在类中会有const成员变量;

#include <iostream>

using namespace std;

class Test
{
public:
    Test()
    {
        _n = 10;
    }
private:
    const int _n;
};

int main()
{
    Test t;
    return 0;
}

如果在构造函数中给_n赋值,显然不对;所以必须找到const成员变量是何时定义的,在定义时给const成员赋值,类中给出的成员变量都是声明,并没有定义。所以C++有了初始化列表。

C++规定,类中的成员变量一定会在初始化列表中初始化,初始化列表如下:

class Test
{
public:
    Test(int n = 0, int x = 0, int y = 0)
        :_n(n)
        , _x(x)
        , _y(y)
    {}
private:
    const int _n;
    int _x;
    int _y;
};

在构造函数参数列表与函数体直接使用: ,可以给成员变量赋值,所有成员变量都会在这里被定义,若没有显示实现初始化列表,内置类型不做处理,自定义类型调用其构造函数。

需要注意:

  1. 每个成员变量在初始化列表中最多只能出现一次;

  1. 类中如果有const、引用、自定义类型且该类没有默认构造函数时,必须使用初始化列表对其赋初值;

  1. 因为对象在定义时一定会执行初始化列表,所以应尽量在初始化列表中初始化成员变量,减少消耗;

  1. 成员变量在类中的声明顺序决定了其在初始化列表中的初始化顺序,上述类中的初始化列表顺序如果打乱,仍然会按_n、_x、_y的顺序对成员变量进行初始化。

六、explicit

1.隐式类型转换

    // i 是int类型
    int i = 10;
    // d 是double类型
    double d = i;
    // i赋给d因为类型不同,会进行一个隐式类型转换,具体过程为
    // 先创建一个临时变量,来存放i转化后的值
    // 再将临时变量中的值赋给d
    // 最后销毁临时变量
    double& rd = i;
    // rd是一个double类型的引用,因为直接给rd赋值的是i转换为double类型的临时变量
    // 这个临时变量赋完值之后就销毁了,因此这个临时变量具有常属性,即不能被修改
    // 所以上述的写法是错误的,下面的才是对的
    const double& rrd = i;

2.单参数构造函数

class Date
{
public:
    Date(int year = 1970)
        :_year(year)
    {}
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2023);
    Date d2 = d1;
    Date d3 = 2023;
    Date& d4 = 2023;
    const Date& d5 = 2023;
    return 0;
}

在上述的代码中,d1调用构造函数初始化,d2调用拷贝构造函数拷贝d1的值完成初始化;

但是d3却是用int类型直接初始化Date类型,实际上是使用了隐式类型转换;

因为Date的构造函数形参列表中只有一个int类型的形参,因此d3实际上执行的是:

先创建一个Date类型的临时变量并用int类型的2023隐式构造,再将d3调用拷贝构造函数将临时变量的值拷贝到d3中。

如果不想让隐式类型转换发生,可以在构造函数前加关键字explicit。

如图所示,用关键字explicit修饰构造函数后,可以禁止隐式类型转换发生,因此上图中第21行的代码会报错。

3.全缺省与只有一个参数未缺省的构造函数

class Date
{
public:
    Date(int year, int month = 1, int day = 1)
        :_year(year)
        , _month(month)
        , _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d = 2023;
    return 0;
}

对于全缺省或者只有一个形参未缺省的构造函数,也可以用隐式类型转换来初始化Date类型的数据;

也可以使用关键字explicit来修饰其来禁止隐式类型转换。

4.多参数构造函数

在C++11标准中,支持了多参数的隐式类型转换。

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 d = { 2023,2,23 };
    return 0;
}

如上述代码所示,实际原理与上述一样,即用{}中的内容构造一个Date类型的临时变量(隐式类型转换),在调用拷贝构造函数将临时变量的值赋给d。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王红花x

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值