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

一,类的六个默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

二,构造函数

2.1 概念

class Data
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Data A;
    A.Init(2023, 2, 24);
    A.print();
    return 0;
}
观察以上程序,我们发现每次创造类我们都需要对其进行初始化设置日期,但每次这样是否有点麻烦,有没有什么更简单的方法呢?答案自然是有的,那就是 构造函数

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

2.2 特性

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

特征如下:

  1. 函数名与类名相同。

  1. 无返回值。

  1. 对象实例化时编译器自动调用对应的构造函数。

  1. 构造函数可以重载

class Data
{
public:
    //无参构造函数
    Data()
    {
        cout << "Data()" << endl;
    }
    //有参构造函数
    Data(int year, int month, int day)
    {
        cout << "Data(int year, int month, int day)" << endl;
        _year = year;
        _month = month;
        _day = day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    //1.无参构造函数
    Data A;//注意:不能用Data A();
    //不然会和函数声明出现冲突,导致编译器无法辨别,导致出错
    //“Data A(void)”: 未调用原型函数(是否是有意用变量定义的 ? )
    
    //2.有参构造函数
    Data B(2023, 2, 24);

}

5.  如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Data
{
public:

    如果已经有构造函数,编译器将不在生成
    //Data(int year, int month, int day)
    //{
    //    cout << "Data(int year, int month, int day)" << endl;
    //    _year = year;
    //    _month = month;
    //    _day = day;
    //}

    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Data A;
    A.print();
    return 0;
}
如上程序,当我们将自己写的构造函数屏蔽后,代码可以通过
但是当放开屏蔽的代码后,程序报错,由此可见默认的构造函数确实存在,并且一旦用户定义编译器就不在生成默认构造函数

但是细心的老铁想必已经发现,默认的构造函数并没有成功的将程序初始化,而是变成一对乱码,那么这是为什么呢?

这是因为C++数据类型分为两种,一个是内置类型(如int,long,double等),一个自定义类型如(class,struct定义的),而 默认构造函数只会对自定义的类型成员调用它的默认构造函数
class Data
{
public:
    Data(int year=2023, int month=2, int day=24)
    {
        cout << "Data(int year, int month, int day)" << endl;
        _year = year;
        _month = month;
        _day = day;
    }

    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
class fun1
{
public:
    void print()
    {
        cout <<"基本类型: "<< _val << endl;
        cout << "自定义类型: ";
        _A.print();
        cout << endl;
    }
private:
    int _val;
    Data _A;
};
int main()
{
    fun1 S;
    S.print();
    return 0;
}
ps:C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即: 内置类型成员变量在类中声明时可以给默认值
class Data
{
public:
    Data(int year=2023, int month=2, int day=24)
    {
        cout << "Data(int year, int month, int day)" << endl;
        _year = year;
        _month = month;
        _day = day;
    }

    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
class fun1
{
public:
    void print()
    {
        cout <<"基本类型: "<< _val << endl;
        cout << "自定义类型: ";
        _A.print();
        cout << endl;
    }
private:
    int _val=5;
    Data _A;
};
int main()
{
    fun1 S;
    S.print();
    return 0;
}

6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

class Data
{
public:
    //无参构造函数
    Data()
    {
        cout << "Data()" << endl;
    }
    //有参构造函数
    Data(int year=2023, int month=2, int day=24)
    {
        cout << "Data(int year, int month, int day)" << endl;
        _year = year;
        _month = month;
        _day = day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
//下面程序可以通过编译吗
int main()
{

    Data A;
    return 0;
}

三,析构函数

3.1 概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

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

3.2 特性

析构函数是特殊的成员函数,其特征如下:

1. 析构函数名是在类名前加上字符 ~

2. 无参数无返回值类型。

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

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

typedef int STDataType;
class Stack
{
public:
    // 构造函数
    Stack(STDataType x=4)
    {
        STDataType* ptr = (STDataType*)malloc(x * sizeof(STDataType));
        if (ptr == NULL)//检查开辟是否成功
        {
            perror("malloc");
            exit(-1);
        }
        _a = ptr;
        _top = 0;
        _capacity = x;
    }
    // 入栈 
    void Push(STDataType data)
    {
        if (_top == _capacity)
        {
            STDataType* ptr = (STDataType*)realloc(_a, _capacity * 2 * sizeof(STDataType));
            if (ptr == NULL)//检查开辟是否成功
            {
                perror("malloc");
                exit(-1);
            }
            _a = ptr;
            _capacity = _capacity * 2;
        }
        _a[_top++] = data;
    }
    // 获取栈顶元素 
    STDataType Top()
    {
        if (_top > 0)
            return _a[_top - 1];
        else
        {
            perror("Top");
            exit(-1);
        }
    }
    //销毁空间
    //1.调用函数销毁(但是需要自己手动调用,一旦忘记,就会导致内存泄露)
    /*void Destory()
    {
        if (_a)
        {
            free(_a);
            _a = nullptr;
            _top = 0;
            _capacity = 0;
        }
    }*/
    //2.析构函数(自动调用,内存泄露风险小)
    ~Stack()
    {
        if (_a)
        {
            free(_a);
            _a = nullptr;
            _top = 0;
            _capacity = 0;
        }
    }


private:
    STDataType* _a;
    int _top;        // 栈顶
    int _capacity;  // 容量 
};
int main()
{
    Stack A;
    A.Push(2);
    A.Push(3);

    return 0;
}

5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

class Data
{
public:
    ~Data()
    {
        cout << "~Data" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
class fun1
{
private:
    int _val=5;
    Data _A;
};
int main()
{
    fun1 S;
    return 0;
}
在main函数中并未直接创建Data类的对象,为什么最后会调用Data类的析构函数?
在main中,创建了fun1的对象S,而S中包含两个成员变量,一个是_val为内置类型成员,销毁时无需资源清理,最后系统会直接回收,而_A则是自定义类型成员,所以在销毁时 ,为了对_A内部的变量成员进行正确的销毁,随意需要调用_A的析构函数,而main无法直接调用, 所以就通过fun1进行调用,保证_A中的成员变量可以正确销毁,进而保证了fun1中的所有变量可以正确销毁。
因此,我们得知: 创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

四,拷贝构造函数

4.1 概念

class Data
{
public:
    //构造函数
    Data(int year, int month, int day)
    {
        cout << "Data(int year, int month, int day)" << endl;
        _year = year;
        _month = month;
        _day = day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Data d1(2023,2,28);
    return 0;
}
如上所示我们创建出一个日期类d1,此时我们想以d1为原型在创建出一个d2,在C++中有什么办法可以实现吗?
答案自然是有,那就是 拷贝构造函数
拷贝构造函数:只有 单个形参,该形参是 对本类类型对象的引用( 一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器 自动调用

4.2 特征

拷贝构造函数也是特殊的成员函数,其特征如下:

 1. 拷贝构造函数是构造函数的一个重载形式

 2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用

class Data
{
public:
    //构造函数
    Data(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //拷贝构造函数
    //Data(const Data d):不用引用的话会造成无限递归
    Data(const Data& d)//用const限定是因为,d是用来拷贝的,防止出现改变d的情况
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Data d1(2023,2,28);
    Data d2(d1);
    d1.print();
    d2.print();

    return 0;
}

3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

class Data
{
public:
    //构造函数
    Data(int year=2023, int month=2, int day=28)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //拷贝构造函数
    //Data(const Data d):不用引用的话会造成无限递归
    //Data(const Data& d)//用const限定是因为,d是用来拷贝的,防止出现改变d的情况
    //{
    //    _year = d._year;
    //    _month = d._month;
    //    _day = d._day;
    //}
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
class fun1
{
public:
    void print()
    {
        cout << a << endl;
        A.print();
    }
private:
    //内置类型
    int a = 10;
    //自定义类型
    Data A;
};
int main()
{
    fun1 d1;
    //fun1 d2(d1);
    fun1 d2 = d1;
    d1.print();
    d2.print();
    return 0;
}

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

typedef int STDataType;
class Stack
{
public:
    //构造函数
    Stack(STDataType x=4)
    {
        STDataType* ptr = (STDataType*)malloc(x * sizeof(STDataType));
        if (ptr == NULL)//检查开辟是否成功
        {
            perror("malloc");
            exit(-1);
        }
        _a = ptr;
        _top = 0;
        _capacity = x;
    }
    //入栈 
    void Push(STDataType data)
    {
        if (_top == _capacity)
        {
            STDataType* ptr = (STDataType*)realloc(_a, _capacity * 2 * sizeof(STDataType));
            if (ptr == NULL)//检查开辟是否成功
            {
                perror("malloc");
                exit(-1);
            }
            _a = ptr;
            _capacity = _capacity * 2;
        }
        _a[_top++] = data;
    }
    //获取栈顶元素 
    STDataType Top()
    {
        if (_top > 0)
            return _a[_top - 1];
        else
        {
            perror("Top");
            exit(-1);
        }
    }
    
    //析构函数(自动调用,内存泄露风险小)
    ~Stack()
    {
        if (_a)
        {
            free(_a);
            _a = nullptr;
            _top = 0;
            _capacity = 0;
        }
    }


private:
    STDataType* _a;
    int _top;        // 栈顶
    int _capacity;  // 容量 
};
int main()
{
    Stack fun1;
    fun1.Push(1);
    fun1.Push(2);
    fun1.Push(3);
//默认拷贝构造函数,浅拷贝
    Stack fun2(fun1);

    return 0;
}
上面的程序为什么会报错呢?
这就要牵扯到深浅拷贝的问题了.
因此,类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

上面例子正确的拷贝构造函数应该是:

//拷贝构造函数
    Stack(const Stack& d)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * d._capacity);
        if (_a == nullptr)//检查开辟是否成功
        {
            perror("malloc");
            exit(-1);
        }
        memcpy(_a, d._a, sizeof(d._top));
        _top = d._top;
        _capacity = d._capacity;
    }

5. 拷贝构造函数典型调用场景:

  • 使用已存在对象创建新对象

  • 函数参数类型为类类型对象

  • 函数返回值类型为类类型对象

class Data
{
public:
    //构造函数
    Data(int year=2023, int month=2, int day=28)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //拷贝构造函数
    //Data(const Data d):不用引用的话会造成无限递归
    Data(const Data& d)//用const限定是因为,d是用来拷贝的,防止出现改变d的情况
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
Data test(Data d)
{
    Data tmp(d);
    return tmp;
}
int main()
{
    //使用已存在对象创建新对象
    Data d1(2023, 2, 28);
    Data d2(d1);
    //函数参数类型为类类型对象
    //函数返回值类型为类类型对象
    test(d1);
    return 0;
}
ps:
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

五,赋值运算符重载

5.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字 operator后面接需要重载的运算符符号

函数原型: 返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@

  • 重载操作符必须有一个类类型参数

  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

class Data
{
public:
    //构造函数
    Data(int year = 2023, int month = 2, int day = 28)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
//private:
    int _year;
    int _month;
    int _day;
};
//当采用全局时我们发现,类中的变量时私人的,域外函数无法访问
//如果我们想让函数正确运行可以将类中变量的权限调整成公共的
//也可以直接将operator写到类里面,以后我们采用的就是这种方法
bool operator==(const Data& d1, const Data& d2)
{
    return d1._year == d2._year
        && d1._month == d2._month
        && d1._day == d2._day;
}
int main()
{
    Data d1(2023, 2, 28);
    Data d2(2023, 2, 28);
    cout << (d1 == d2) << endl;
    //d1==d2和operator==(d1,d2)的调用命令是一样的

    return 0;
}
我们通常将operator写入到类中去
class Data
{
public:
    //构造函数
    Data(int year = 2023, int month = 2, int day = 28)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    bool operator==(const Data& d)
    {
        return _year == d._year
            && _month == d._month
            && _day == d._day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

5.2 赋值运算符重载

(1) 赋值运算符重载格式

  • 参数类型:const Data&,传递引用可以提高传参效率

  • 返回值类型:Data&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

  • 检测是否自己给自己赋值

  • 返回*this :要复合连续赋值的含义

class Data
{
public:
    //构造函数
    Data(int year = 2023, int month = 2, int day = 28)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //拷贝构造函数
    Data(const Data& d)//用const限定是因为,d是用来拷贝的,防止出现改变d的情况
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    //=
    Data& operator=(const Data& d)
    {
        //防止自己给自己赋值
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
    //==
    bool operator==(const Data& d)
    {
        return _year == d._year
            && _month == d._month
            && _day == d._day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Data d1(2023, 2, 28);
    Data d2(2024, 2, 28);
    Data d3(2025, 2, 28);
    d3 = d2 = d1;
    d2.print();
    d3.print();
    return 0;
}

(2) 赋值运算符只能重载成类的成员函数不能重载成全局函数

class Data
{
public:
    //构造函数
    Data(int year = 2023, int month = 2, int day = 28)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    int _year;
    int _month;
    int _day;
};
Data& operator=(Data& d1, const Data& d2)
{
    //防止自己给自己赋值
    if (&d1 != &d2)
    {
        d1._year = d2._year;
        d1._month = d2._month;
        d1._day = d2._day;
    }
    return d1;
}
int main()
{
    Data d1(2023, 2, 28);
    Data d2(2024, 2, 28);
    d1 = d2;
    return 0;
}
原因:赋值运算符如果不在类中显示出现,编译器会生成一个默认的赋值运算符,此时默认的赋值运算符会和全局的赋值运算符冲突,所以赋值运算符只能是类的成员函数

(3) 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Data
{
public:
    //构造函数
    Data(int year = 2023, int month = 2, int day = 28)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //拷贝构造函数
    Data(const Data& d)//用const限定是因为,d是用来拷贝的,防止出现改变d的情况
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    //=
    Data& operator=(const Data& d)
    {
        //防止自己给自己赋值
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
    //==
    bool operator==(const Data& d)
    {
        return _year == d._year
            && _month == d._month
            && _day == d._day;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
class fun
{
private:
    void print()
    {
        cout << a << endl;
        A.print();
    }
public:
    //基本类型
    int a = 10;
    //自定义类型
    Data A;
};
int main()
{
    fun d1;
    fun d2;
    d1 = d2;
    return 0;
}
同样的由于编译器生成的默认赋值运算符重载属于是根据字节拷贝的浅拷贝,所以和拷贝构造函数一样,需要注意:
如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

5.3 前置++和后置++重载

class Data
{
public:
    //构造函数
    Data(int year = 2023, int month = 2, int day = 28)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //前置++,返回的是已经++后的值
    //引用返回是因为,this指向的对象不会销毁,引用返回效率高
    Data& operator++()
    {
        _day += 1;
        return *this;
    }
    //后置++,返回的是++前的值,所以需要先吧++前的值保存起来
    //C++规定,后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
    //自动传递
    //由于tmp在返回后就销毁了,所以不能用引用返回
    Data operator++(int)
    {
        Data tmp(*this);
        _day += 1;
        return tmp;
    }
    void print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Data d1(2023, 2, 5);
    Data d2=d1++;
    Data d3=++d1;
    d1.print();
    d2.print();
    d3.print();

    return 0;
}

六,const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际 修饰该成员函数隐含的this指针,表明 在该成员函数中不能对类的任何成员进行修改
应用场景如下
class Date
{
public:
    Date(int year=2023, int month=3, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    void print() const
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
void fun1(const Date A)
{
    A.print();
}
int main()
{
    Date A;
    fun1(A);
    return 0;
}
如上所示,在fun1中A被const修饰,只可读,22行代码调用A.print()函数,此时如果函数没有被const修饰,那么*this的权限就变成了可读可写,但此时A的权限只可读,这属于是权限放大,会出错。
ps:在后面写类和对象时, 建议类中的成员函数,如果对*this不修改,那么都加上const修饰,避免出错

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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

总结

本篇文章主要是深入了解类的默认成员函数的各个特性,方便在日后更加灵活的使用类。
  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值