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

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

 定义一个空类:

class A
{
};

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

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()来初始化时间类的成员变量,但其实我们可以写一个构造函数来初始化成员变量。

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

    int _day;
};

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

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

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

    int _day;
};

int main()
{
    Date t(100);

    return 0;
}

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

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、将默认构造函数再加上

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

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、写一个全缺省构造函数

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

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

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

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

初始化列表

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方
式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成
员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。

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

易错点:

  • 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
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,这时候我们就要利用拷贝构造函数。

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

//拷贝构造函数
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对象,然后将一个对象的成员变量的值传给另一个对象,将会发生什么。 

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值的改变。所以我们要自己写一个拷贝构造函数。

解决方法:

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

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指向的就是不同的空间了


易错点:

  • 拷贝构造函数的参数只有⼀个且必须是类类型对象的引用,使用传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调用。
//拷贝构造函数
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为例:

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;
}

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

~Stack()
{
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
}

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


4、赋值运算符重载

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

赋值运算符重载的特点:

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

 赋值运算符重载的使用

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

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

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

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

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

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



//拷贝构造函数
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;
}

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-19 23:24:02       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-19 23:24:02       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-19 23:24:02       45 阅读
  4. Python语言-面向对象

    2024-07-19 23:24:02       55 阅读

热门阅读

  1. Linux的常用命令大全

    2024-07-19 23:24:02       15 阅读
  2. 监测vuex中state的变化

    2024-07-19 23:24:02       17 阅读
  3. 算法面试题五

    2024-07-19 23:24:02       20 阅读
  4. c++一句话求前缀和,不用循环

    2024-07-19 23:24:02       17 阅读
  5. 双指针算法入门 —— 常见例题

    2024-07-19 23:24:02       12 阅读
  6. 什么是云服务器?

    2024-07-19 23:24:02       17 阅读