C++:类的默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个。

定义一个空类:

cpp 复制代码
class A
{
};

经过编译器处理之后,类A不在为空,它会自动的生成六个默认的成员函数,即使这六个成员函数什么也不做。处理之后相当于:

cpp 复制代码
class A
{
    A();//1、构造函数

    A(const A& x);//2、拷贝构造函数

    ~A();//3、析构函数

    A& operator= (const A& x);4、赋值操作符重载

    A* operator &();//5、取地址运算符重载

    const A* operator& () const;//6、const修饰的取地址操作符重载
};

1、构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。

它的特点:

  • 函数名与类名相同。
  • 无返回值。
  • 对象实例化时系统会⾃动调用对应的构造函数。
  • 构造函数可以重载。
  • 如果类中没有显式定义构造函数,则C++编译器会自动动⽣成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。

构造函数的使用

若我们要初始化一个时间类,我们还需要写一个**初始化函数Init()**来初始化时间类的成员变量,但其实我们可以写一个构造函数来初始化成员变量。

cpp 复制代码
//用Init() 初始化成员变量
class Date
{
public:
    void Init(int day)
    {
        _day = day;
    }
private:

    int _day;
};

int main()
{
    Date t;
    t.Init(100);
    return 0;
}

在这里我们其实在创建一个类对象t的时候就调用了编译器默认生成的构造函数。

cpp 复制代码
//用Date() 初始化成员变量
class Date
{
public:
    Date(int day)
    {
        _day = day;
    }
private:

    int _day;
};

int main()
{
    Date t(100);

    return 0;
}

但我们将上面的构造函数Date()和初始化函数Init()放到一起呢?

cpp 复制代码
class Date
{
public:
    Date(int day)
    {
        _day = day;
    }

    void Init(int day)
    {
        _day = day;
    }
private:
    int _day;
};

int main()
{
    Date t;
    t.Init(100);
    return 0;
}

解决方法:

a、将默认构造函数再加上

编译将会报错,因为我们自己写了一个构造函数,所以编译器不会生成它的默认构造函数了,在创建类对象的时候,没有可以匹配的构造函数,从而导致了编译报错。我们只需要将编译器生成的默认构造函数再加上即可。

cpp 复制代码
class Date
{
public:
    Date() {}

    Date(int day)
    {
        _day = day;
    }

    void Init(int day)
    {
        _day = day;
    }
private:
    int _day;
};

int main()
{
    Date t;
    t.Init(100);
    return 0;
}
b、写一个全缺省构造函数

写一个全缺省构造函数,它既匹配了类对象的创建,又完成了类成员变量的初始化。

cpp 复制代码
class Date
{
public:
    Date(int day = 100)
    {
        _day = day;
    }
private:
    int _day;
};

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

这样我们可以完成创建一个类对象,也不需要写初始化函数Init()也可以初始化成员变量。

初始化列表

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方

式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成

员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。

cpp 复制代码
class A
{
public:
    A(int a,int b,int c)
    :_a(a)
    ,_b(b)
    ,_c(c)
    {}
private:   
    int _a;
    int& _b;
    const int _c;
};

易错点:

  • 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
cpp 复制代码
class Date
{
public:
    Date()
    {
        _day = 1;
    }
    
    Date(int day = 100)
    {
        _day = day;
    }
private:
    int _day;
};

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

但创建类对象t的时候,编译器会不知道匹配哪个构造函数,从而导致编译报错。

  • 不能写 Date t(); 这样代码,因为编译器可能会认为它是一个函数。

因为我们可以将Date看成函数的返回值,t看成函数名,()表示无参传递。


2、拷贝构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。

拷构造的特点:

  • 拷贝构造函数是构造函数的⼀个重载。
  • C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

拷贝构造函数的使用

  • 若我们创建了两个Date类,一个类对象a,一个类对象b,我们想将a的成员变量的值传给b,这时候我们就要利用拷贝构造函数。

以上面的代码为例,编译器会默认生成一个拷贝构造函数。编译器会根据类的成员变量来生成一个拷贝构造函数。(注:下面的拷贝构造函数是为了理解才写出来的,编译器默认生成的拷贝构造函数不会显示。)

cpp 复制代码
//拷贝构造函数
class Date
{    
public:
    //编译器默认生成的。
    Date(const Date& d)
    {    
        _day = d._day;
    }

    Date(int day = 100)
    {
        _day = day;
    }
private:
    int _day;
};

int main()
{
    Date a(10);
    Date b(a);

    return 0;
}
  • 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。但想Stack这样的类,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。

若我们要想上面一样创建两个Stack对象,然后将一个对象的成员变量的值传给另一个对象,将会发生什么。

cpp 复制代码
class Stack
{ 
public:
    Stack(int n = 4)
    {
        _a = new int[n];
        _capacity = n;
        _top = 0;
    }
private:
    int* _a;
    int _capacity;
    int _top;
};

int main()
{
    Stack s1;
    Stack s2(s1);
    return 0;
}

运行成功了

但其实有很大的问题的,它们的_a都指向了同一块空间,s1中_a值的改变会导致s2_a值的改变。所以我们要自己写一个拷贝构造函数。

解决方法:

用深拷贝的方式来拷贝构造函数,将它们成员变量里的值按字节的方式来拷贝即可。

cpp 复制代码
class Stack
{ 
public:
    Stack(int n = 4)
    {
        _a = new int[n];
        _capacity = n;
        _top = 0;
    }
    Stack(const Stack& st)
    { 
        _a = new int[st._capacity];
        memcpy(_a, st._a, int * st._top)
        _top = st._top;
        _capacity = st._capacity;
    }
private:
    int* _a;
    int _capacity;
    int _top;
};

int main()
{
    Stack s1;
    Stack s2(s1);
    return 0;
}

这时候它们两成员变量_a指向的就是不同的空间了


易错点:

  • 拷贝构造函数的参数只有⼀个且必须是类类型对象的引用,使用传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调用。
cpp 复制代码
//拷贝构造函数
class Date
{    
public:
    Date(const Date d)
    {    
        _day = d._day;
    }

    Date(int day = 100)
    {
        _day = day;
    }
private:
    int _day;
};

int main()
{
    Date a(10);
    Date b(a);

    return 0;
}

如上面将 Date(const Date& d) 改为Date(const Date d) 就会触发无限递归。

原理是当我们要传一个自定义类型的时候,且没有用引用传参,编译器会在实参传递给形参的时候会调用拷贝构造函数,但要调用拷贝构造函数的时候又要传参,传参的时候又要调用拷贝构造函数......所以会触发无限递归。


3、析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调⽤析构函数,完成对象中资源的清理释放工作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

析构函数的特点:

  • 析构函数名是在类名前加上字符~。
  • 无参数无返回值。(这里跟构造类似,也不需要加void)
  • 一个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  • 对象生命周期结束时,系统会自动调用析构函数。
  • 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。

析构函数的使用

在一个类里面如果没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数。若用了像malloc、cealloc、realloc或new......之类资源申请,我们一定要自己写析构函数,否则会造成资源泄漏。

以Stack为例:

cpp 复制代码
class Stack
{ 
public:
    Stack(int n = 4)
    {
        _a = new int[n];
        _capacity = n;
        _top = 0;
    }
private:
    int* _a;
    int _capacity;
    int _top;
};

int main()
{
    Stack s1;
    
    return 0;
}

单单依靠编译器生成的析构函数会导致内存泄漏的,我们要自己"手搓"一个析构函数。

cpp 复制代码
~Stack()
{
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
}

向上面的代码一样,我们有资源的申请,就要有资源的释放。


4、赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷贝赋值,这⾥要注意跟拷贝构造区分,拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象。

赋值运算符重载的特点:

  • 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参会有拷贝。
  • 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
  • 没有显式实现时,编译器会自动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值 拷贝**/浅** 拷贝**(⼀个字节⼀个字节的** 拷贝**),对⾃定义类型成员变量会调用他的** 拷贝****构造。

赋值运算符重载的使用

以Date类为例子。我们先创建三个Date类对象,我们用对象a来赋值运算符重载将值传给对象b。

cpp 复制代码
//拷贝构造函数
class Date
{    
public:
    Date(int day = 100)
    {
        _day = day;
    }
private:
    int _day;
};

int main()
{
    Date a(10);
    Date b;
    b = a;
    return 0;
}

类里没有资源的申请,单单用编译器生成的默认赋值运算符重载就可以了,但具体代码是如何实现的呢?

cpp 复制代码
//赋值运算符重载
Date operator= (const Date& d)
{
    _day = d._day;
    return *this
}

上面的代码放入到Date类里也编译器也可以运行,但如我们要再创建一个Date对象c,a赋值运算符重载对象b的同时也赋值运算符重载对象c。我们可以优化一下,将返回值改为Date&即可。

cpp 复制代码
//拷贝构造函数
class Date
{    
public:
    Date(int day = 100)
    {
        _day = day;
    }
    //赋值运算符重载
    Date& operator= (const Date& d)
    {
        _day = d._day;
        return *this
    }
private:
    int _day;
};

int main()
{
    Date a(10);
    Date b;
    Date c;
    c = b = a;
    return 0;
}
相关推荐
尘浮生几秒前
Java项目实战II基于微信小程序的移动学习平台的设计与实现(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·学习·微信小程序·小程序
小鱼仙官4 分钟前
MFC IDC_STATIC控件嵌入一个DIALOG界面
c++·mfc
神仙别闹7 分钟前
基本MFC类框架的俄罗斯方块游戏
c++·游戏·mfc
娅娅梨1 小时前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控1 小时前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
汤米粥1 小时前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾1 小时前
EasyExcel使用
java·开发语言·excel
我爱工作&工作love我1 小时前
1435:【例题3】曲线 一本通 代替三分
c++·算法
拾荒的小海螺1 小时前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式