【三】【C++】类与对象(二)

类的六个默认成员函数

在C++中,有六个默认成员函数,它们是编译器在需要的情况下自动生成的成员函数,如果你不显式地定义它们,编译器会自动提供默认实现。这些默认成员函数包括:

  1. 默认构造函数 (Default Constructor): 如果你没有为类显式定义任何构造函数,编译器将生成一个无参的默认构造函数。用于创建对象而不需要提供任何参数。例如:ClassName obj;

  2. 析构函数 (Destructor): 如果你没有为类定义析构函数,编译器将生成一个默认的析构函数。用于在对象被销毁时释放资源、进行清理工作等操作。例如:当对象超出作用域、被删除或者动态分配的对象被 delete 时。

  3. 拷贝构造函数 (Copy Constructor):(具有缺陷---浅拷贝 如果你没有为类定义拷贝构造函数,编译器将生成一个默认的拷贝构造函数。用于创建一个对象作为另一个对象的副本,通常在赋值、传递参数等情况下调用。例如:ClassName obj2 = obj1;

  4. 赋值运算符重载 (Copy Assignment Operator):(具有缺陷---浅拷贝 如果你没有为类定义赋值运算符重载函数,编译器将生成一个默认的赋值运算符重载函数。用于将一个对象的值复制给另一个对象,通常在赋值操作中调用。例如:obj2 = obj1;

  5. 移动构造函数 (Move Constructor)

  6. 移动赋值运算符 (Move Assignment Operator)

主要常见的是前四个,本篇文章只介绍前四个默认成员函数,最后两个有兴趣的小伙伴可以自己去查一下别的资料。

初始化操作

 
/*1.正常使用Data类初始化*/
#include <iostream>
using namespace std;
class Data {
    public:
        void Init(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        void ShowInfo() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
 };
int main() {
    Data d1;
    d1.Init(2024, 1, 29);
    d1.ShowInfo();

    Data d2;
    d2.Init(2024, 1, 28);
    d2.ShowInfo();

    return 0;
 }

每当我们定义一个自定义类型,创造对象的时候,一定需要对其进行初始化操作。既然对于所有的对象都需要进行初始化操作,那么每次显示调用Init函数就显得非常冗余。如果每当我们创建一个对象,自动对其进行初始化,就省去显示调用Init函数的步骤。

构造函数

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

 
/*2.使用构造函数进行初始化*/
#include <iostream>
using namespace std;
class Data {
    public:
        Data(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        void ShowInfo() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
 };
int main() {
    Data d1(2024,1,29);
    d1.ShowInfo();

    Data d2(2024,1,28);
    d2.ShowInfo();

    return 0;
 }

这样我们每创建一个对象,编译器就会自动调用对应的构造函数对其进行初始化操作。也就是说构造函数是为了方便我们对实例化对象进行初始化而创造出来的。

构造函数的特性

构造函数是一种特殊的成员函数。它的特性是:

  1. 函数名与类名相同。

  2. 无返回值。

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

  4. 构造函数可以重载。

 
/*3.初步介绍构造函数*/
#include <iostream>
using namespace std;
class Data {
public:
    Data(){

    }
    Data(int year,int month,int day){
        _year=year;
        _month=month;
        _day=day;
    }
    void ShowInfo() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
 };
void TestDate(){
    Data d1;
    Data d2(2024,1,28);
 }
int main() {
    TestDate();
    return 0;
 }

探究构造函数的规则---创建对象自动调用构造函数

 
/*4.探究构造函数的规则---创建对象自动调用构造函数*/
#include <iostream>
using namespace std;
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 ShowInfo() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
 };
void TestDate(){
    Data d1;
    Data d2(2024,1,28);
    d2.ShowInfo();
 }
int main() {
    TestDate();
    return 0;
 }

当我们创建d1对象时,后面不加括号表示调用无参的构造函数。后面带括号,括号里带参数,表示调用有参的构造函数。创建对象的时候系统会自动调用对应的构造函数对其进行初始化操作。

探究构造函数的规则---编译器默认生成无参构造函数

 
/*5.探究构造函数的规则---编辑器默认有无参构造函数*/
#include <iostream>
using namespace std;
class Data {
public:
    void ShowInfo() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
 };
void TestDate(){
    Data d1;
    d1.ShowInfo();
 }
int main() {
    TestDate();
    return 0;
 }

当我们没有显示编写构造函数的时候,创建完对象后我们发现对象内年月日的初始值是0。

如果类中没有显示定义构造函数,编译器会自动生成一个无参的默认构造函数,但是一旦我们显示定义了构造函数,编译器就不会自动生成无参构造函数。

探究构造函数的规则---自己创建有参的构造函数时,编辑器不会自动创建无参构造函数

 
/*6.探究构造函数的规则---自己创建有参的构造函数时,编辑器不会自动创建无参构造函数*/
#include <iostream>
using namespace std;
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 ShowInfo() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
 };
void TestDate(){
    Data d1;
    Data d2(2024,1,28);
    d2.ShowInfo();
 }
int main() {
    TestDate();
    return 0;
 }

我们显示定义了有参的构造函数,此时编译器不会自动生成无参的构造函数。当我们创建对象,后面不加括号的时候,系统会自动调用无参的构造函数,但是类类型中没有无参的构造函数,编译器没有生成,所以就报错了。解决的办法就是自己再写一个无参的构造函数。

探究构造函数的规则---默认构造函数具体情况---无参,全缺省

 
/*7.探究构造函数的规则---默认构造函数具体情况---无参,全缺省*/
#include <iostream>
using namespace std;
class Data {
public:
    Data(){
        cout<<"Data()"<<endl;
        _year=1999;
        _month=1;
        _day=1;
    }
    Data(int year=2024,int month=2,int day=1){
        cout<<"Data(int year,int month,int day)"<<endl;
        _year=year;
        _month=month;
        _day=day;
    }
    void ShowInfo() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
 };
void TestDate(){
    Data d1;
    d1.ShowInfo();
 }
int main() {
    TestDate();
    return 0;
 }

当我们没有显示的定义构造函数时,编译器会默认帮我们定义一个无参的构造函数。全缺省的构造函数等价于无参的构造函数。因此不可以同时存在,当我们创建对象后面不加括号时,系统调用无参的构造函数,此时编译器不知道调用无参的构造函数还是全缺省的构造函数。

问题产生的本质是缺省参数导致重载模棱两可,可以理解为缺省参数不存在,此时含有缺省参数的函数与另一个函数不构成重载,即使编译器没有报错。

 
/*7.缺省参数形成冲突*/
#include <iostream>
using namespace std;
class Data {
    public:

        Data(int year, int month = 2, int day = 1) {
            cout << "Data(int year,int month,int day)" << endl;
            _year = year;
            _month = month;
            _day = day;
        }
        Data(int year) {
            _year = year;
            _month = 1;
            _day = 1;
        }
        void ShowInfo() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
 };
void TestDate() {
    Data d1(2024);
    d1.ShowInfo();
 }
int main() {
    TestDate();
    return 0;
 }

当我们创建对象传入2024一个参数的时候,编译器不知道调用有缺省参数的函数还是没有缺省参数的函数,缺省参数导致重载模棱两可,可以理解为缺省参数不存在,即Data(int year, int month = 2, int day = 1) 等价于Data(int year) 与下面的 Data(int year)不构成重载,即使编译器不会报错。

清理操作

 
/*8.正常使用栈,进行清理工作*/
#include <iostream>
using namespace std;
typedef int DataType;
class Stack {
    public:
        Stack(int capacity = 4) {
            _array = (DataType*)malloc(sizeof(DataType) * capacity);
            _capacity = capacity;
            _size = 0;
        }
        void CheckCapacity(){
            if(_capacity==_size){
                int newcapacity=_capacity*2;
                _array=(DataType*)realloc(_array,sizeof(DataType)*newcapacity);
                _capacity=newcapacity;
            }
        }
        void push(DataType data) {
            CheckCapacity();
            _array[_size++] = data;
        }
        void pop() {
            _size--;
        }
        void Destroyed() {
            free(_array);
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
        bool empty() {
            return _size == 0;
        }
        DataType top() {
            return _array[_size - 1];
        }
    private:
        DataType* _array;
        int _capacity;
        int _size;
 };
void TestStack() {
    Stack st;
    st.push(1);
    st.push(2);
    st.push(3);
    st.push(4);
    st.push(5);
    while (!st.empty()) {
        cout << st.top() << endl;
        st.pop();
    }
    st.Destroyed();

 }
int main() {
    TestStack();
    return 0;
 }

当我们使用创建对象时,使用完之后都需要显示调用Destroyed销毁函数进行清理工作,防止内存泄漏。既然对于所有的对象都需要进行清理操作,那么每次显示调用Destroyed函数就显得非常冗余。如果每当我们创建一个对象,对象出了作用域之后自动对其进行销毁,就省去显示调用Destroyed函数的步骤。

析构函数

 
/*9.使用析构函数,进行清理工作*/
#include <iostream>
using namespace std;
typedef int DataType;
class Stack {
    public:
        Stack(int capacity = 4) {
            _array = (DataType*)malloc(sizeof(DataType) * capacity);
            _capacity = capacity;
            _size = 0;
        }
        void CheckCapacity() {
            if (_capacity == _size) {
                int newcapacity = _capacity * 2;
                _array = (DataType*)realloc(_array, sizeof(DataType) * newcapacity);
                _capacity = newcapacity;
            }
        }
        void push(DataType data) {
            CheckCapacity();
            _array[_size++] = data;
        }
        void pop() {
            _size--;
        }
        ~Stack() {
            free(_array);
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
        bool empty() {
            return _size == 0;
        }
        DataType top() {
            return _array[_size - 1];
        }
    private:
        DataType* _array;
        int _capacity;
        int _size;
 };
void TestStack() {
    Stack st;
    st.push(1);
    st.push(2);
    st.push(3);
    st.push(4);
    st.push(5);
    while (!st.empty()) {
        cout << st.top() << endl;
        st.pop();
    }

 }
int main() {
    TestStack();
    return 0;
 }

C++引入了一个新的概念,析构函数。析构函数是一个特殊的成员函数,名字与类名相同,名字前面添加‘~’,类类型对象出作用域时由编译器自动调用。

析构函数的特性

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

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

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

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

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

 
/*10.初步介绍析构函数*/
#include <iostream>
using namespace std;
class Date {
    public:
        Date(int year, int month, int day) {
            cout << "Date(int year, int month, int day)" << endl;
            _year = year;
            _month = month;
            _day = day;
        }
        void Show() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
        ~Date() {
            cout << "~Date()" << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
 };

void Test() {
    Date d1(2024, 1, 29);
    d1.Show();

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

拷贝构造

 
/*11.正常用一个已有的变量拷贝构造一个新变量---内置类型*/
#include <iostream>
using namespace std;

int main() {
    int a=10;
    int b=a;
    cout<<b<<endl;
    return 0;
 }

我们用已经存在的a变量去构造新的b变量,很明显如果a、b都是内置类型,我们很容易理解。但是如果我们希望用一个已经存在的自定义类型去构造新的自定义类型对象,应该如何操作?

 
/*12.正常用一个已有的变量拷贝构造一个新变量---自定义类型*/
#include <iostream>
using namespace std;
class Date {
    public:
        Date() {
        }
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        void Show() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
        ~Date() {
            cout << "~Date()" << endl;
        }
        void assignment(const Date& d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
    private:
        int _year;
        int _month;
        int _day;
 };

void Test() {
    Date d1(2024, 1, 29);
    //d2=d1?
    Date d2;
    d2.assignment(d1);

    d2.Show();

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

我们可以在类里面定义一个函数,函数的参数是自定义类型的引用,作用是进行赋值,这样我们创建一个新对象后,调用这个对象的赋值函数,就可以实现我们想要的效果。我们可以发现对于每一个自定义类型来说,我们都希望可以这样实现"自定义类型 d2=d1",每个自定义类型都在类里面定义一个函数,外部调用会显得特别冗余,且外部调用形式是 Date d2; d2.assignment(d1); 先创建对象再进行赋值,而我们的期望是创建对象和赋值一体化,这两者似乎有所区别。

拷贝构造函数

 
/*12.使用拷贝构造函数拷贝构造一个新变量---自定义类型*/
#include <iostream>
using namespace std;
class Date {
    public:
        Date() {
        }
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        void Show() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
        ~Date() {
            cout << "~Date()" << endl;
        }
        Date(const Date& d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
    private:
        int _year;
        int _month;
        int _day;
 };

void Test() {
    Date d1(2024, 1, 29);
    //d2=d1?
    Date d2(d1);
    d2.Show();
 }
int main() {
    Test();
    return 0;
 }

C++引入了一个新的概念,拷贝构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

拷贝构造函数的特性

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

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

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

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

探究拷贝构造函数的规则---编译器默认生成拷贝构造函数

 
/*13.默认生成的拷贝构造函数*/
#include <iostream>
using namespace std;
class Date {
    public:
        Date() {
        }
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        void Show() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
        ~Date() {
            cout << "~Date()" << endl;
        }

    private:
        int _year;
        int _month;
        int _day;
 };

void Test() {
    Date d1(2024, 1, 29);
    //d2=d1?
    Date d2(d1);
    d2.Show();
 }
int main() {
    Test();
    return 0;
 }

即使我们没有显示编写拷贝构造函数,我们依旧可以用Date d2(d1);拷贝构造d2对象。因为编译器会自动生成对应的拷贝构造函数。

探究拷贝构造函数的规则---编译器默认生成拷贝构造函数的缺陷(浅拷贝)

 
/*14.默认生成的拷贝构造函数---本质*/
#include <iostream>
using namespace std;
class f1 {
    public:
        f1(int x) {
            p = (int*)malloc(sizeof(int));
            *p = x;
        }
        void Show() {
            cout << *p << endl;
            cout << p << endl;
        }
        ~f1() {
            if (p) {
                free(p);
            }
        }
    private:
        int* p;
 };

void Test() {
    f1 x1(1);
    f1 x2(x1);
    x1.Show();
    x2.Show();

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

运行上面的代码我们会发现程序崩掉了。如果在28行打一个断点,进入调试模式,我们发现一直运行到28行处程序都没有发生问题。此时我们得到,

我们发现x1的地址和x2的地址是一样的。很容易知道接下来出Test函数后,x1和x2对象都需要调用析构函数,此时同一个地址会释放两次空间,程序就崩了。

编译器默认生成的拷贝构造函数按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。可以理解为浅拷贝是将自定义类型类内成员变量进行简单的等号赋值。即x2.p=x1.p;。此时指针就会指向同一段空间。而我们希望的是指针会新开辟一个空间,只是指针的值是相同的,空间不是相同的。

 
/*15.默认生成的拷贝构造函数---本质*/
#include <iostream>
using namespace std;
class f1 {
    public:
        f1(int x) {
            p = (int*)malloc(sizeof(int));
            *p = x;
        }
        void Show() {
            cout << p << endl;
            cout << *p << endl;
        }
        ~f1() {
            if (p) {
                free(p);
            }
        }
        f1(const f1& x) {
            p = (int*)malloc(sizeof(int));
            *p = *x.p;
        }
    private:
        int* p;
 };

void Test() {
    f1 x1(1);
    f1 x2(x1);
    x1.Show();
    x2.Show();
 }
int main() {
    Test();
    return 0;
 }

此时我们显示定义拷贝构造函数,新开辟空间,只是让指针上的值相等。这样就可以达到我们想要的效果,指针指向的空间不同,而指针上的值是相等的。

探究拷贝构造函数的规则---拷贝构造函数的使用场景(编译器优化、返回对象生命周期延长)

拷贝构造函数是C++中的一个特殊函数,用于创建一个新对象并将其初始化为另一个对象的副本。它通常在以下情况下被调用:

  1. 对象的初始化:当你创建一个新对象并将其初始化为另一个对象的副本时,拷贝构造函数被调用。

  2. 函数参数传递:当你将一个对象作为参数传递给一个函数时,拷贝构造函数可以被用来创建传递给函数的副本。

  3. 函数返回值:当函数返回一个对象的副本时,拷贝构造函数被用来创建返回的副本。(编译器优化可能省略)

 
/*16.拷贝构造函数---的使用场景*/
#include <iostream>
using namespace std;
class Date {
    public:
        Date(int year, int month, int day) {
            cout << "Date(int year,int month,int day):" << this << endl;
        }
        Date(const Date&d) {
            cout << "Date(const Date&d);" << this << endl;
        }
        ~Date() {
            cout << "~Date():" << this << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
 };
Date Test(Date d) {
    Date temp(d);

    return temp;
 }
int main() {
    Date d1(2024, 1, 29);
    Date  d2 =Test(d1);
    return 0;
 }

Date d1(2024, 1, 29);我们调用构造函数创建对象d1。Test(d1)此时调用Test函数,将d1传给形成d这一过程,调用拷贝构造函数,用已经存在的d1创建并初始化d对象。(函数参数传递:当你将一个对象作为参数传递给一个函数时,拷贝构造函数可以被用来创建传递给函数的副本。)在Test函数中,显示调用拷贝构造函数,用已经存在的d创建并初始化temp对象。(对象的初始化:当你创建一个新对象并将其初始化为另一个对象的副本时,拷贝构造函数被调用。在Test函数作用域结束,调用对象d的析构函数。在主函数作用域结束,调用temp的析构函数接着调用d2的析构函数。

你可能会产生疑问,在Test函数调用结束,temp作为函数返回值,此时应该还会调用拷贝构造函数。(函数返回值:当函数返回一个对象的副本时,拷贝构造函数被用来创建返回的副本。)为何运行代码并没有显示拷贝构造函数被调用。

实际上temp作为返回值,此时还会调用拷贝构造函数,但是经过编译器优化过,这一步的拷贝构造函数被省略掉了。尽管你在 Test 函数中创建了一个 temp 对象并返回它,但在实际的编译过程中,编译器会进行优化,避免调用拷贝构造函数,直接将 temp 对象的值传递给 d2。这是一种性能优化,可以减少不必要的对象拷贝操作,提高程序效率。

我对代码进行调试,注意看我的光标的位置,我的光标处于主函数中,但是析构函数只调用了一次,显然这个析构函数是对Test中d对象进行析构,而temp和d2对象并没有发生析构。显然temp在Test函数中作为返回值,此时temp对象的生命周期得到延伸,与d2同步,但最后编译器会依照前后顺序先调用temp的析构函数再调用d2的析构函数。虽然拷贝构造函数被省略了,但是 temp 对象的生命周期和 d2 对象的初始化完全一致,因此 temp 对象的析构函数会在 d2 对象的析构函数之前被调用,从而保证资源的正确释放。

运算符重载

运算符重载(Operator Overloading)是C++的一个重要特性,允许你为用户自定义的数据类型定义和重新定义运算符的行为。通过运算符重载,你可以使用C++内置运算符来执行自定义数据类型的操作,使代码更具可读性和表现力。

内置类型判断相等很简单,例如a,b变量都是int类型,判断相等只需要a==b?即可。但是如果我们想要定义自定义类型进行判断是否相等。我们希望也可以像“a==b”这样使用,对于ab都属于自定义类型,此时编译器显然不能像内置类型那样操作。于是我们引入运算符重载的概念,使得自定义类型也可以像内置类型那样进行操作。

 
/*17.运算符重载*/
#include <iostream>
using namespace std;
class Date {
public:
Date(int year,int month,int day){
    _year=year;
    _month=month;
    _day=day;
 }
bool operator==(const Date& d2){
    return _year==d2._year
    && _month==d2._month
    && _day==d2._day;
 }
private:
    int _year;
    int _month;
    int _day;
 };


int main() {
    Date d1(2024,1,29);
    Date d2(d1);
    cout<<(d1==d2)<<endl;
    return 0;
 }

当我们使用d1==d2这段代码的时候,实际上会转化为d1.operator==(d2) 此时调用d1对象的operator==函数,并传入d2对象作为参数。此时d1的地址会作为this指针,operator==函数会隐藏一个this指针,this指针指向的地址是d1的地址,_year等价于this->_year,_month等价于this->_month,_day等价于this->_day。

赋值运算符重载

如果我们想要以这种形式“a=b”对一个自定义类型进行赋值,那么我们就需要对“=”进行运算符重载。

赋值运算符重载格式:

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

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值

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

 
/*18.赋值运算符重载*/
#include <iostream>
using namespace std;
class Date {
    public:
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        Date& operator=(const Date&d) {
            if (this != &d) {
                _year = d._year;
                _month = d._month;
                _day = d._day;
            }
            return *this;
        }
        void Show() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
 };


int main() {
    Date d1(2024, 1, 29);
    Date d2 = d1;
    d2.Show();
    return 0;
 }

实际上当我们没有显示定义赋值运算符重载的时候,编译器会生成一个默认的赋值运算符重载,以值的方式逐字节拷贝,即浅拷贝。我们知道浅拷贝当成员变量是指针的时候,p1=p2此时指针指向的地址会相同,而我们希望的是指针指向的地址不同,指针指向地址上的值相同,因为往往涉及到指针问题我们都需要显示定义赋值运算符重载,将浅拷贝变成深拷贝。

默认赋值运算符重载的缺陷

 
/*19.默认赋值运算符重载的缺陷*/
#include <iostream>
using namespace std;
class f1 {
    public:
        f1(int x) {
            p = (int*)malloc(sizeof(int));
            *p = x;
        }
        void Show() {
            cout << *p << endl;
            cout << p << endl;
        }
        ~f1() {
            if (p) {
                free(p);
            }
        }

    private:
        int* p;
 };

void Test() {
    f1 x1(1);
    f1 x2 = x1;
    x1.Show();
    x2.Show();
 }
int main() {
    Test();
    return 0;
 }

x1,x2对象中p成员指向的地址是相同的,当x1,x2对象出作用域时,会对同一块地址调用两次析构函数,此时程序就会崩掉。此时就需要显示定义赋值运算符重载,将浅拷贝变成深拷贝。

前置++和后置++的重载

 
/*20.前置++和后置++的重载(代码逻辑有缺陷)*/
#include <iostream>
using namespace std;
class Date {
    public:
        Date() {}
        Date(int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
        }
        Date& operator++() {
            _day += 1;
            return *this;
        }
        Date operator++(int) {
            Date temp(*this);
            _day++;
            return temp;
        }
        void Show() {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
 };
void Test() {
    Date d;
    Date d1(2024, 1, 29);
    d = d1++;
    d.Show();
    d1.Show();
    d = ++d1;
    d.Show();
    d1.Show();
 }
int main() {
    Test();
    return 0;
 }

 
Date operator++(int) {
            Date temp(*this);
            _day++;
            return temp;
        }

这是后置++,为了区分前置++和后置++,规定后置++参数中写一个int,用来表示后置++。

前置++先对day进行+1操作,然后把处理过的自定义类型返回,此时返回引用,用引用可以提高效率,因为不用引用还需要进行一次拷贝构造操作,用引用就不需要进行拷贝构造操作。

后缀++返回的值是++前的值,因此我们先拷贝构造一个副本,对本体的day进行++操作,返回副本的值,注意此时不能够返回引用,因为副本出了作用域就消失了,所以只能不传引用。

d1++会被转化为d1.operator++(int),从而调用后置++的函数。++d1会被转化为d1.operator++(),从而调用前置++的函数。

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/358914.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

设计模式之框架源码剖析(实战+图解)

Java设计模式 1&#xff0c;概述 随着软件开发人员人数的增多&#xff0c;一些公司急需一些高端人才。作为一个高端人才&#xff0c;设计面向对象软件是必不可少的能力&#xff0c;而软件设计是需要很深的功力&#xff0c;设计模式就要求你必须掌握。 2&#xff0c;本章特色…

中国地区cetos7.9 install kubeadmin

第 1 步&#xff1a;禁用 SELinux&#xff08;可选但推荐&#xff09; 如何在 CentOS 7 上查找 SELinux 状态 sestatus另一种选择是运行以下 cat 命令&#xff1a; vi /etc/selinux/config SELINUXdisabled rebootcentos7 linux 安装k8s前下面操作的作用是&#xff1f; cat…

基于JAVA的河南软件客服系统 开源项目

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统管理人员2.2 业务操作人员 三、系统展示四、核心代码4.1 查询客户4.2 新增客户跟进情况4.3 查询客户历史4.4 新增服务派单4.5 新增客户服务费 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的河…

day38_MySQL

今日内容 0 复习昨日 1 引言 2 数据库 3 数据库管理系统 4 MySQL 5 SQL语言 0 复习昨日 1 引言 1.1 现有的数据存储方式有哪些&#xff1f; Java程序存储数据&#xff08;变量、对象、数组、集合&#xff09;&#xff0c;数据保存在内存中&#xff0c;属于瞬时状态存储。文件&…

Google Chrome 常用的几个参数

1 右键--Google Chrome--属性--目标 参数作用--disable-infobars此计算机将不会再收到 Google Chrome 更新&#xff0c;因为 Windows XP 和 Windows Vista 不再受支持。适用于 xp、2003 的 49.x.x.x 版本。示例1--ingore-certificate-errors忽略证书错误--disable-background-…

开源知识库:让企业低成本实现知识管理

管理和利用企业内部知识已经成为提升效率和竞争力的重要手段。而对于大多数企业&#xff0c;尤其是中小企业而言&#xff0c;如何在有限的预算下&#xff0c;实现高效的知识管理&#xff0c;仍是一项挑战。面对这一问题&#xff0c;开源知识库应运而生。今天&#xff0c;我们将…

Linux - 数据流重定向、管道符、环境变量配置文件的加载

概述 想了解Linux编程&#xff0c;shell脚本是绕不开的关键知识点&#xff0c;原计划写一个整篇来分享shell的来龙去脉&#xff0c;但知识点过于繁杂&#xff0c;先分享一下学习shell的准备工作&#xff0c;数据流重定向、管道符、环境变量配置文件的加载&#xff0c;有助于知…

jdk17新特性—— instanceof的模式匹配

目录 一、instanceof模式匹配的概述二、instanceof模式匹配代码示例2.1、jdk17之前 instanceof用法 代码示例2.2、jdk17及之后 instanceof用法 代码示例一2.3、jdk17及之后 instanceof用法 代码示例二 一、instanceof模式匹配的概述 instanceof增加了模式匹配功能&#xff0c;…

Vulnhub靶机:niveK

一、介绍 运行环境&#xff1a;Virtualbox 攻击机&#xff1a;kali&#xff08;10.0.2.15&#xff09; 靶机&#xff1a;niveK&#xff08;10.0.2.41&#xff09; 目标&#xff1a;获取靶机root权限和flag 靶机下载地址&#xff1a;https://download.vulnhub.com/hms/nive…

算法训练第一周考试(思维性题目)

目录 第一题.满足约束 第二题&#xff1a;传递信息 第三题&#xff1a;无线替换 第四题&#xff1a;环球旅行 第五题&#xff1a;求和游戏 第六题&#xff1a;大相径庭数组 总结&#xff1a;其实这次考试主要都是一些思维性的题集&#xff0c;并没有过难的东西&#xff…

五、防御保护---防火墙出口选路篇

五、防御保护---防火墙智能选路篇 一、就近选路二、策略路由选路1.策略路由的概念1.1匹配条件&#xff08;通过ACL定义&#xff09;1.2动作 三、智能选路 --- 全局路由策略1.基于链路带宽的负载分担2.基于链路质量进行负载分担3.基于链路权重进行负载分担4.基于链路优先级的主备…

shell - sed命令和awk命令

一.sed 的高级用法 sed 中除了模式空间&#xff0c;还另外支持保持空间&#xff0c;利用此空间&#xff0c;可以将模式空间中的数据&#xff0c;临时保存至保持空间&#xff0c;从而后续接着处理&#xff0c;实现更为强大的功能。 常见命令&#xff1a; 选项含义P(大)打印模…

【MySQL】学习如何通过DQL进行数据库数据的基本查询

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-KvH5jXnPNsRtMkOC {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

Docker部署Plik系统并结合内网穿透实现远程访问本地上传下载文件

文章目录 1. Docker部署Plik2. 本地访问Plik3. Linux安装Cpolar4. 配置Plik公网地址5. 远程访问Plik6. 固定Plik公网地址7. 固定地址访问Plik 本文介绍如何使用Linux docker方式快速安装Plik并且结合Cpolar内网穿透工具实现远程访问&#xff0c;实现随时随地在任意设备上传或者…

Leetcode刷题笔记题解(C++):1117. H2O 生成(多线程)

思路&#xff1a; 解法二&#xff1a;生产者-消费者解法 1.把 hydrogen 线程看作生产者&#xff0c;oxygen 线程看作消费者&#xff0c;缓冲队列大小为2。 2.hydrogen 把生成的氢放入队列&#xff1b;oxygen 线程每次从队列里消费两个氢元素。 3.生产者生产两个氢元素后会因为…

找不到xinput1_4.dll怎么办?xinput1_4.dll丢失的6种解决方法对比

无法找到或缺失xinput1_4.dll文件可能会引发一系列问题&#xff0c;这一现象在计算机系统中并不罕见。首先&#xff0c;它直接影响到某些应用程序的正常运行&#xff0c;特别是那些依赖于DirectX环境的游戏和软件&#xff0c;因为xinput1_4.dll是DirectX工具包中的一个重要组成…

ElementUI组件:Button 按钮

button按钮 点击下载learnelementuispringboot项目源码 效果图 el-button.vue页面效果图 项目里el-button.vue代码 <script> export default {name: "el_button",// 注意这里的名称不能和 router inex.js里的name一样methods: {sendMsg() {// alert(1)xthi…

皮层肌肉相干性(CMC)的介绍和实现

皮层肌肉相干性CMC的介绍和实现 0 引言1 CMC定义2 CMC实现(Python)3 总结欢迎来稿0 引言 皮质肌肉相干性(CMC)是研究大脑皮层控制肌肉活动机制的常用且有用的方法。它揭示了肌肉持续收缩期间皮层和肌肉之间的功能联系。CMC的起源是初级运动皮层和肌肉之间皮质脊髓通路的通…

飞桨大模型分布式训练技术

今天我为大家介绍飞桨大模型分布式训练技术&#xff0c;内容分为以下几个部分&#xff1a; 首先&#xff0c;我会介绍大模型训练面临的重点难题&#xff1b;然后&#xff0c;为大家介绍飞桨在大模型训练领域的特色分布式训练技术和优化方案&#xff1b;最后&#xff0c;伴随着…

【STM32】STM32学习笔记-SPI通信外设(39)

00. 目录 文章目录 00. 目录01. SPI简介02. SPI特征03. SPI外设简介04. SPI框图05. SPI基本结构06. 主模式全双工连续传输07. 非连续传输08. 软件/硬件波形对比09. 附录 01. SPI简介 在大容量产品和互联型产品上&#xff0c;SPI接口可以配置为支持SPI协议或者支持I2S音频协议。…