【C++ 学习 ③】- 类的六大默认成员函数

news2024/11/14 0:16:11

目录

一、 构造函数

1.1 - 概念

1.2 - 特性

二、析构函数

2.1 - 概念

2.2 - 特性

2.3 - 用栈实现队列

三、拷贝构造函数

四、运算符重载

4.1 - 双目运算符

4.2 - 单目运算符

4.3 - 赋值运算符重载

五、const 成员函数

六、取地址 和 const 取地址运算符重载


参考资料

  1. C++ 拷贝构造函数。

  2. C++中,对象作为函数参数的几种情况。

  3. C++11运算符重载详解与向量类重载实例(<<,>>,+,-,*等)。

  4. C++的const类成员函数。

在 C++ 中,创建一个类,即便这个类是空类,也会自动生成下面 6 个默认成员函数


一、 构造函数

1.1 - 概念

#include "Stack.h"
#include <iostream>
using namespace std;

int main()
{
    Stack st;
    st.Init();  // 初始化
    // 入栈顺序:1 2 3 4 5
    st.Push(1);
    st.Push(2);
    st.Push(3);
    st.Push(4);
    st.Push(5);
    cout << "当前栈中有效元素个数为:" << st.Size() << endl;  // 5
    while (!st.Empty())
    {
        cout << st.Top() << endl;
        st.Pop();
    }
    // 出栈顺序:5 4 3 2 1
    st.Destroy();  // 销毁
    return 0;
}

我们调用 Init 方法对栈进行初始化,但这样的操作依赖于程序员的自觉性以及个人修养。假设一个蹩脚的、初阶的程序员,他没有意识到初始化的重要性,甚至不理解为什么需要初始化,于是没有调用 Init 方法或者说忘记调用,程序最终就会出现问题。

   所以需要一种机制(mechanism)来避免上述问题,让程序尽可能正确地运行。

构造函数(constructor)是一个特殊的成员函数,函数名与类名相同,没有返回值,通过类创建对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次

1.2 - 特性

  1. 构造函数可以实现重载

    #include <stdlib.h>
    #include <iostream>
    using namespace std;
    ​
    typedef int STDataType;
    ​
    class Stack
    {
    public:
        // 无参的默认构造函数
        Stack()
        {
            _data = (STDataType*)malloc(sizeof(STDataType) * 5);
            if (nullptr == _data)
            {
                perror("initialization failed!");
                exit(-1);
            }
            _top = 0;
            _capacity = 5;
            cout << "Stack()::Initialization succeeded~" << endl;
        }
        
        // 带参的构造函数
        Stack(int default_capacity)
        {
            _data = (STDataType*)malloc(sizeof(STDataType) * default_capacity);
            if (nullptr == _data)
            {
                perror("initialization failed!");
                exit(-1);
            }
            _top = 0;
            _capacity = default_capacity;
            cout << "Stack(int)::Initialization succeeded~" << endl;
        }
    private:
        STDataType* _data;
        int _top;
        int _capacity;
    };
    ​
    int main()
    {
        Stack st1;  // 实例化 st1 时,编译器自动调用【默认构造函数】
        // Stack()::Initialization succeeded~
    ​
        Stack st2(4);  // 实例化 st2 时,编译器自动调用带参构造函数
        // Stack(int)::Initialization succeeded~
    ​
        // 注意:
        // Stack st1; 不能写成 Stack st1();
        // 因为后者会和函数声明冲突
        return 0;
    }
  2. 默认构造函数(default constructor)就是在没有显示提供初始化式时调用的构造函数,即如果创建某个类的对象时没有提供初始化式就会调用默认构造函数,例如 Stack st;

    无参的构造函数是默认构造函数,全缺省的构造函数也是默认构造函数。默认构造函数是可以在没有参数的情况下调用的构造函数(A default constructor is one that can be called with no arguments)

    构造函数可以重载,那么在语法上,无参的默认构造函数和全缺省的默认构造函数可以同时出现,但是在实际使用中,会造成一定的问题,例如

    typedef int STDataType;
    ​
    class Stack
    {
    public:
        // 无参的默认构造函数
        Stack()
        {
            _data = (STDataType*)malloc(sizeof(STDataType) * 5);
            if (nullptr == _data)
            {
                perror("initialization failed!");
                exit(-1);
            }
            _top = 0;
            _capacity = 5;
        }
    ​
        // 全缺省的默认构造函数
        Stack(int default_capacity = 5)
        {
            _data = (STDataType*)malloc(sizeof(STDataType) * default_capacity);
            if (nullptr == _data)
            {
                perror("initialization failed!");
                exit(-1);
            }
            _top = 0;
            _capacity = default_capacity;
        }
        private:
        STDataType* _data;
        int _top;
        int _capacity;
    };
    ​
    int main()
    {
        Stack st1(5);  // ok
        // Stack st3;  // error-->对重载函数的调用不明确
        return 0;
    }
  3. 如果类中没有显示定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成

    typedef int STDataType;
    ​
    class Stack
    {
    public:
    private:
        STDataType* _data;
        int _top;
        int _capacity;
    };
    ​
    int main()
    {
        Stack st;  // st 实例化时,调用编译器自动生成的无参的默认构造函数
        return 0;
    }

    对上述程序进行调试:

    可以发现,编译器自动生成的无参的默认构造函数并没有完成对栈的初始化,栈对象 st 的成员变量 _data/ _top/ _capacity 依旧是随机值。

    这是因为 C++ 把类型分成了内置类型(基本类型)和自定义类型。内置类型就是语言本身提供的数据类型,例如:int/ char/ float/ double...;自定义类型就是使用 class/ struct/ union 等关键字自己定义的类型

    对于内置类型的成员变量,编译器自动生成的无参的默认构造函数并不会对其处理;对于自定义类型的成员变量,则会调用其对应的默认构造函数。例如

    #include <iostream>
    using namespace std;
    ​
    class A
    {
    public:
        A(int x = 10)  // 全缺省的默认构造函数
        {
            _i = x;
        }
    ​
        void Print()
        {
            cout << _i << endl;
        }
    private:
        int _i;
    };
    ​
    class B
    {
    public:
        void Print()
        {
            cout << _j << endl;
            _a.Print();
        }
    private:
        int _j;  // 内置类型的成员变量
        A _a;  // 自定义类型的成员变量
    };
    ​
    int main()
    {
        B b;
        b.Print();  
        // 随机值 --> 说明 B 类中自动生成的默认构造函数没有对 _j 初始化
        // 10 --> 说明 B 类中自动生成的默认构造函数调用了 _a 的默认构造函数
        return 0;
    }
  4. 在 C++11 中,针对编译器自动生成的无参的默认构造函数不能对内置类型的成员变量初始化的缺陷,打了一个补丁(patch),即内置类型的成员变量在类声明时可以给缺省值

    #include <iostream>
    using namespace std;
    ​
    class Point
    {
    public:
        void Print()
        {
            cout << "(" << _x << ", " << _y << ")" << endl;
        }
    private:
        int _x = 0;
        int _y = 0;
    };
    ​
    int main()
    {
        Point p;
        p.Print();  // (0, 0)
        return 0;
    }


二、析构函数

2.1 - 概念

#include "Stack.h"
#include <iostream>
using namespace std;

int main()
{
	Stack st;
	st.Init();  // 初始化
	// 入栈顺序:1 2 3 4 5
	st.Push(1);
	st.Push(2);
	st.Push(3);
	st.Push(4);
	st.Push(5);
	cout << "当前栈中有效元素个数为:" << st.Size() << endl;  // 5
	while (!st.Empty())
	{
		cout << st.Top() << endl;
		st.Pop();
	}
	// 出栈顺序:5 4 3 2 1
	st.Destroy();  // 销毁
	return 0;
}

在 C++ 中,清理和初始化一样重要,但是比起初始化,程序员更加容易忽视清理,关于清理的意识和观念是更加淡薄的。同样是上面这个例子,如果我们不调用 Init 方法对栈进行初始化,程序最终会出现问题,但是我们不调用 Destroy 方法销毁栈,程序仍然能输出正确的结果,且正常退出。

   但隐藏的问题就是,如果在其他情况下,不进行销毁,可能会造成内存泄漏,所以此时       需要另一种机制来避免上述问题。

析构函数(destructor)也是一个特殊的成员函数,函数名是类名前面加上 ~,没有参数也没有返回值,对象生命周期结束时,编译器会自动调用析构函数

#include <stdlib.h>
#include <iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
	// 默认构造函数
	Stack(int default_capacity = 5)
	{
		_data = (STDataType*)malloc(sizeof(STDataType) * default_capacity);
		if (nullptr == _data)
		{
			perror("initialization failed!");
			exit(-1);
		}
		_top = 0;
		_capacity = default_capacity;
		cout << "initialization succeeded~" << endl;
	}
    
	// 默认析构函数
	~Stack()
	{
		free(_data);
		_data = nullptr;
		_top = _capacity = 0;
		cout << "cleanup completed~" << endl;
	}
private:
	STDataType* _data;
	int _top;
	int _capacity;
};

int main()
{
	cout << "before opening brace" << endl;
	{
		Stack st;
	}
	cout << "after closing brace" << endl;
    
    // before opening brace
	// initialization succeeded~
	// cleanup completed~
	// after closing brace
	return 0;
}

2.2 - 特性

  1. 析构函数不能重载,一个类中只能有一个析构函数。若类中没有显示定义,C++ 编译器会自动生成默认的析构函数

  2. 和编译器自动生成的默认构造函数类似,对于内置类型的成员变量,默认的析构函数并不会对其处理;对于自定义类型的成员变量,则会调用其对应的析构函数

    #include <iostream>
    using namespace std;
    ​
    class A
    {
    public:
        A(int x = 10)
        {
            _i = x;
        }
    ​
        ~A()
        {
            cout << "~A()" << endl;
        }
    ​
        void Print()
        {
            cout << _i << endl;
        }
    private:
        int _i;
    };
    ​
    class B
    {
    public:
        B(int x = 20)  // 
        {
            _j = x;
        }
    ​
        void Print()
        {
            cout << _j << endl;
            _a.Print();
        }
    private:
        int _j;
        A _a;
    };
    ​
    int main()
    {
        B b;
        b.Print();
        // 20
        // 10 --> 说明也自动调用了 _a 的默认构造函数
        // ~A() --> 说明自动调用了 _a 的析构函数
        return 0;
    }

2.3 - 用栈实现队列

class Stack 
{
public:
    Stack(int default_capacity = 5) 
    {
        _data = (int*)malloc(sizeof(int) * default_capacity);
        if (nullptr == _data)
        {
            perror("initialization failed!");
            exit(-1);
        }
        _top = 0;
        _capacity = default_capacity;
    }
​
    ~Stack() 
    {
        free(_data);
        _data = nullptr;
        _top = _capacity = 0;
    }
​
    bool empty() 
    {
        return _top == 0;
    }
​
    void push(const int& x) 
    {
        if (_top == _capacity)
        {
            int* tmp = (int*)malloc(sizeof(int) * 2 * _capacity);
            if (nullptr == tmp)
            {
                perror("realloc failed!");
                return;
            }
            _data = tmp;
            _capacity *= 2;
        }
        _data[_top++] = x;
    }
​
    void pop() 
    {
        assert(!empty());
        --_top;
    }
​
    int top() 
    {
        assert(!empty());
        return _data[_top - 1];
    }
​
    int size() 
    {
        return _top;
    }
private:
    int* _data;
    int _top;
    int _capacity;
};
​
/* -------------------------------------------------------------- */
​
class MyQueue 
{
public:
    // 由于 MyQueue 类的两个成员变量都是自定义类型,
    // 所以不需要写构造函数,也不需要写析构函数,
    // 直接使用编译器自动生成的默认构造函数和析构函数即可。
    
    bool empty() 
    {
        return _pushSt.empty() && _popSt.empty();
    }
    
    void push(int x) 
    {
        _pushSt.push(x);
    }
​
    int peek() 
    {
        assert(!empty());  // 前提是队列非空
        if (_popSt.empty())
        {
            while (!_pushSt.empty())
            {
                _popSt.push(_pushSt.top());
                _pushSt.pop();
            }
        }
        return _popSt.top();
    }
    
    int pop() 
    {
        int ret = peek();
        _popSt.pop();
        return ret;
    }
private:
    Stack _pushSt;  // 用于入队的栈
    Stack _popSt;  // 用于出队的栈
};


三、拷贝构造函数

  1. 拷贝构造函数是一种特殊的构造函数,通过类创建对象时,它是使用同一类中之前创建的对象来初始化这个新创建的对象

  2. 拷贝构造函数的参数只有一个且必须是同一类对象的引用(常用 const 修饰),把对象作为函数参数,编译器则会报错,因为会引发无穷递归调用

    #include <iostream>
    using namespace std;
    ​
    class Date
    {
    public:
        Date(int year = 1949, int month = 10, int day = 1)
        {
            _year = year;
            _month = month;
            _day = day;
        }
    ​
        Date(const Date& d)   // 拷贝构造函数
        {
            cout << "Date(cosnt Date& d)" << endl;
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
    ​
        void Display()
        {
            cout << _year << "-" << _month << "-" << _day << endl;
        }
    private:
        int _year;
        int _month;
        int _day;
    };
    ​
    int main()
    {
        Date d1(2023, 5, 1);
    ​
        Date d2(d1);  // 实例化 d2 时,编译器自动调用拷贝构造函数
        // Date(cosnt Date& d)
        d2.Display();
        // 2023-5-1
        return 0;
    }
    1. 情形一(对象作为函数参数)

      void func(Date d) 
      {
          // Date(cosnt Date& d)
          d.Dispaly();  
          // 2023-5-1
      }   
      // 对象作为函数参数时,会创建一个 Date 类的对象(形参),
      // 并调用拷贝构造函数初始化这个对象
      ​
      int main()
      {
          Date d(2023, 5, 1);
          func(d);
          return 0;
      }

      所以如果拷贝构造函数允许把对象作为函数参数,那么就会引发无穷递归

    2. 情形二(对象作为函数返回值)

      Date func() 
      { 
          Date d(2023, 5, 1); 
          return d; 
      }
      // 对象作为函数返回值时,也会创建一个 Date 类的临时对象返回,
      // 并调用拷贝构造函数初始化这个对象
      ​
      int main()
      {
          Date ret = func();
          // Date(cosnt Date& d)
          ret.Dispaly();
          // 2023-5-1
          return 0;
      }
  3. 若类中没有显示定义,C++ 编译器会自动生成默认的拷贝构造函数。对于内置类型的成员变量,默认的拷贝构造函数对其进行浅拷贝(也叫值拷贝);对于自定义类型的成员变量,则是调用其对应的拷贝构造函数

    #include <iostream>
    using namespace std;
    ​
    class A
    {
    public:
        A(int x = 10)
        {
            _i = x;
        }
    ​
        A(const A& a)
        {
            cout << "A(cosnt A& a)" << endl;
            _i = a._i;
        }
    ​
        void Print()
        {
            cout << _i << endl;
        }
    private:
        int _i;
    };
    ​
    class B
    {
    public:
        B(int x = 20)
        {
            _j = x;
        }
    ​
        void Print()
        {
            cout << _j << endl;
            _a.Print();
        }
    private:
        int _j;
        A _a;
    };
    ​
    int main()
    {
        B b1;
        B b2(b1);
        // A(const A&a)
        b2.Print();
        // 20
        // 10
        return 0;
    }
  4. 类中如果没有涉及资源申请,可以直接使用编译器自动生成的默认拷贝构造函数;而一旦涉及到资源申请,则需要显示地实现拷贝构造函数,进行深拷贝,例如

    typedef int STDataType;
    ​
    class Stack
    {
    public:
        Stack(int default_capacity = 5)
        {
            _data = (STDataType*)malloc(sizeof(STDataType) * default_capacity);
            if (nullptr == _data)
            {
                perror("initialization failed!");
                exit(-1);
            }
            _top = 0;
            _capacity = default_capacity;
        }
    ​
        ~Stack()
        {
            free(_data);
            _data = nullptr;
            _top = _capacity = 0;
        }
    private:
        STDataType* _data;
        int _top;
        int _capacity;
    };

    对于 Stack 类,若不显示实现拷贝构造函数,直接使用默认的拷贝构造函数,就会出现下面的问题:

    1. st1st2 的成员变量 _data 指向同一块动态开辟的内存空间,这就造成其中任意一个栈的改变也会改变另一个栈

    2. 并且由于栈 st1st2 的成员变量 _data 指向同一块动态开辟的内存空间,当 st2 先销毁时(因为栈的特点是后进先出,注意:这里的栈指的是 main 函数栈帧),编译器自动调用 st2 的析构函数,将那块动态开辟的内存空间给释放掉了,所以当 st1 后销毁时,编译器自动调用 st1 的析构函数,就会对同一块内存空间进行两次释放

    所以在这种情况下,需要显示实现拷贝构造函数,进行深拷贝:

    #include <stdlib.h>
    #include <string.h>
    #include <iostream>
    using namespace std;
    ​
    typedef int STDataType;
    ​
    class Stack
    {
    public:
        Stack(int default_capacity = 5)
        {
            _data = (STDataType*)malloc(sizeof(STDataType) * default_capacity);
            if (nullptr == _data)
            {
                perror("initialization failed!");
                exit(-1);
            }
            _top = 0;
            _capacity = default_capacity;
        }
    ​
        Stack(const Stack& st)  // 拷贝构造函数(实现深拷贝)
        {
            _data = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
            if (nullptr == _data)
            {
                perror("malloc failed!");
                return;
            }
            memcpy(_data, st._data, sizeof(STDataType) * st._top);
            _top = st._top;
            _capacity = st._capacity;
        }
    ​
        ~Stack()
        {
            cout << "this->_data = " << this->_data << endl;
            free(_data);
            _data = nullptr;
            _top = _capacity = 0;
        }
    private:
        STDataType* _data;
        int _top;
        int _capacity;
    };
    ​
    int main()
    {
        Stack st1;
        // this->_data = 01285438
        Stack st2(st1);
        // this->_data = 01285BC8
        // 说明 st1 和 st2 的成员变量 _data 不再指向同一块动态开辟的内存空间了
        return 0;
    }
  5. 拷贝构造函数通常用于

    • 通过使用另一个同类型的对象来初始化新创建的对象

    • 对象作为函数参数

    • 对象作为函数返回值


四、运算符重载

C++ 中预定义的运算符的操作对象只能是基本数据类型,但实际上,对于许多用户自定义类型。也需要类似的运算操作,这时就必须在 C++ 中重新定义这些运算符,赋予已有运算符新的功能,使它能够用于特定类型执行特定操作。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构造的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

运算符重载时要遵循以下规则

  1. 除了类属关系运算符 .、成员指针运算符 .*、作用域运算符 ::sizeof 运算符和三目运算符 ?: 以外,C++ 中的所有运算符都可以重载。

  2. 重载运算符限制在 C++ 语言中已有的运算符范围内的允许重载的运算符中,不能创建新的运算符。

  3. 运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载的选择。

  4. 重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。

  5. 运算符重载不能改变该运算符用于内置类型对象的含义。它只能和用户自定义类型的对象一起使用,或者用于自定义类型对象和内置类型对象的混合使用。

  6. 运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有功能相类似,避免没有目的地使用运算符重载。

4.1 - 双目运算符

双目运算符重载如果写在类外面,那么是需要两个参数的

#include <iostream>
using namespace std;
​
class Date
{
    friend bool operator==(const Date& d1, const Date& d2);
    // 友元提供了一种突破封装的方式(友元相关的内容会在后面的博客中详解)
public:
    Date(int year = 1949, int month = 10, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
​
bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
        && d1._month == d2._month
        && d1._day == d2._day;
}
​
int main()
{
    Date d1(2023, 5, 1);
    Date d2(2023, 6, 1);
    if (d1 == d2)
        cout << "d1 == d2" << endl;
    else
        cout << "d1 != d2" << endl;
    // d1 != d2
    return 0;
}

d1 == d2 等价于 operator==(d1, d2)

双目运算符重载如果写在类里面(即为类的成员函数),只能显示说明一个参数,该形参是运算符的右操作数

#include <iostream>
using namespace std;
​
class Date
{
public:
    Date(int year = 1949, int month = 10, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
​
    bool operator==(const Date& d)
    {
        return this->_year == d._year
            && this->_month == d._month
            && this->_day == d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};
​
int main()
{
    Date d1(2023, 5, 1);
    Date d2(2023, 6, 1);
    if (d1 == d2)
        cout << "d1 == d2" << endl;
    else
        cout << "d1 != d2" << endl;
    // d1 != d2
    return 0;
}

d1 == d2 等价于 d1.operator==(d2)

4.2 - 单目运算符

  1. 前置单目运算符重载为类的成员函数时,不需要显示地说明参数

  2. 后置单目运算符重载为类的成员函数时,函数要带一个整型形参,但在调用函数时,不需要传递实参,因为编译器会自动传递

#include <iostream>
using namespace std;

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

	// 拷贝构造函数
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	// 前置 ++ 运算符重载
	Date& operator++()  
	{
		_day += 1;  
		return *this;
	}

	// 后置 ++ 运算符重载
	Date operator++(int)  // 不能返回临时对象的引用
	{
		Date tmp = *this;
		_day += 1;
		return tmp;
	}

	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 5, 1);
	Date d2(2023, 5, 1);

	Date ret1 = ++d1;  // 先 ++,后使用
	Date ret2 = d2++;  // 先使用,后 ++
	// Date(const Date& d)
	// Date(const Date& d)
	// Date(const Date& d)

	ret1.Display();
	d1.Display();
	// 2023-5-2
	// 2023-5-2
	ret2.Display();
	d2.Display();
	// 2023-5-1
	// 2023-5-2
	return 0;
}

注意

  1. 简单地将 _day 加 1 是存在一定问题的,如果 _day 大于当前这个月的总天数时,还需要修改 _month,甚至需要修改 _year。这个问题留在后面的 Date 类的实现中解决

  2. 一定要弄清楚初始化(initialization)和赋值(assignment)之间的区别

    Date ret1 = ++d1;Date ret2 = d2++;Date tmp = *this 都是初始化,而不是赋值,它们等价于 Date ret1(++d1);Date ret2(d2++);Date tmp(*this);,所以调用拷贝构造函数,而非赋值运算符重载(后面会讲解)

4.3 - 赋值运算符重载

#include <iostream>
using namespace std;
​
class Date
{
public:
    Date(int year = 1949, int month = 10, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
​
    Date& operator=(const Date& d)  // 返回引用既可以提高效率,同时支持连续赋值
    {
        cout << "Date& operator=(const Date& d)" << endl;
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
​
    void Display()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
​
int main()
{
    Date d1(2023, 5, 1);
    Date d2(2023, 6, 1);
    Date d3(2023, 7, 1);
​
    d2.Display();  
    d3.Display();
    // 2023-6-1
    // 2023-7-1
    d3 = d2 = d1;
    // Date& operator=(const Date& d)
    // Date& operator=(const Date& d)
    d2.Display();
    d3.Display();
    // 2023-5-1
    // 2023-5-1
    return 0;
}

特性

  1. 赋值运算符只能重载成类的成员函数,不能重载成全局函数(对于其他的编译器会自动生成的默认成员函数也一样)。因为如果类中没有显示定义,C++ 编译器会自定生成一个默认的赋值运算符重载,此时用户再在类外自己定义一个全局的赋值运算符重载,就会和默认的赋值运算符重载冲突了

  2. 和编译器自动生成的默认的拷贝构造函数类似,对于内置类型的成员变量,默认的赋值运算符重载对其进行浅拷贝;对于自定义类型的成员变量,则是调用其对应的赋值运算符重载

    #include <iostream>
    using namespace std;
    ​
    class A
    {
    public:
        A(int x = 10) { _i = x; }
    ​
        void Print()
        {
            cout << _i << endl;
        }
    ​
        A& operator=(const A& a) 
        {
            cout << "A& operator=(const A& a)" << endl;
            if (this != &a)
            {
                _i = a._i;
            }
            return *this;
        }
    private:
        int _i;
    };
    ​
    class B
    {
    public:
        B(int x = 20) { _j = x; }
    ​
        void Print()
        {
            cout << _j << endl;
            _a.Print();
        }
    private:
        int _j;
        A _a;
    };
    ​
    int main()
    {
        B b1;
        B b2(200);
    ​
        b1.Print();
        // 20
        // 10
        b1 = b2;
        // A& operator=(const A& a)
        b1.Print();
        // 200
        // 10
        return 0;
    }
  3. 如果类中没有涉及到资源申请,可以直接使用编译器自动生成的默认赋值运算符重载;而一旦涉及到资源申请,则需要显示地实现,进行深拷贝


五、const 成员函数

面向对象程序设计中,为了体现封装,通常不允许直接修改类对象的数据成员,若要修改,应调用公有成员函数来完成。为了保证 const 对象的常量性,编译器须区分不安全与安全的成员函数,即区分试图修改类对象与不修改类对象的函数。例如

#include <iostream>
using namespace std;
​
class Date
{
public:
    Date(int year = 1949, int month = 10, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
​
    void Display()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
​
int main()
{
    Date d1(2023, 5, 1);
    d1.Display();  // 2023-5-1
​
    const Date d2(2023, 6, 1); 
    // d2.Display();  // error
    return 0;
}

在 C++ 中,只有被声明为 const 的成员函数才能被一个 const 类对象调用,所以上面定义的 Display 成员函数不是 const 成员函数,被认为是不安全的成员函数

要声明一个 const 类型的类成员函数,只需要在成员函数参数列表后加上关键字 const,例如

void Display() const
{
    cout << _year << "-" << _month << "-" << _day << endl;
}

const 修饰类成员函数,实际上修饰的是该成员函数隐含的 this 指针,因此 const 成员函数不能修改类中的数据成员


六、取地址 和 const 取地址运算符重载

这两个运算符一般不需要重载,使用编译器默认生成的重载即可,只有特殊情况,才需要重载,例如想让别人获取到指定的内容。

#include <iostream>
uing namespace std;
​
class Date
{
public:
    Date(int year = 1949, int month = 10, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
​
    Date* operator&()
    {
        cout << "Date* operator&()" << endl;
        return this;
    }
​
    const Date* operator&() const
    {
        cout << "const Date* operator&() const" << endl;
        return this;
    }
private:
    int _year;
    int _month;
    int _day;
};
​
int main()
{
    Date d1;
    cout << &d1 << endl;
    // Date* operator&()
    // 地址 1
​
    const Date d2;
    cout << &d2 << endl;
    // const Date* operator&() const
    // 地址 2
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何写软件测试简历项目经验,靠这个面试都要赶场

一、前言&#xff1a;浅谈面试 面试是我们进入一个公司的门槛&#xff0c;通过了面试才能进入公司&#xff0c;你的面试结果和你的薪资是息息相关的。那如何才能顺利的通过面试&#xff0c;得到公司的认可呢?面试软件测试要注意哪些问题呢?下面和笔者一起来看看吧。这里分享一…

如何计算旋转框的IOU

一、先将两个框转换为角点形式 一般我们得到的是中心点&#xff0c;宽高&#xff0c;和旋转角度。通过矢量旋转公式得到角点形式。 二、判断四个角点是否在对方的框里&#xff0c;并保存在里面的角点 A的角点向B的相邻的两条边投影(任意的B的两条边)&#xff0c;使用向量点积得…

微信开发者工具实现代码加固

一&#xff1a;下载安装node.js node.js下载地址&#xff1a;下载 | Node.js 二&#xff1a;微信开发者工具安装代码加固拓展 1&#xff1a;开发者工具选择设置-》拓展设置 2:安装代码加固拓展 三&#xff1a;使用代码加固拓展实现核心密码加密 1&#xff1a;安装devtool-cod…

makefile 变量赋值方式

文章目录 前言一、变量的定义和使用二、变量的赋值方式1&#xff0c;简单赋值 &#xff08; : &#xff09;2&#xff0c;递归赋值 &#xff08; &#xff09;3&#xff0c;条件赋值 &#xff08; ? &#xff09;4&#xff0c;追加赋值 &#xff08; &#xff09; 三、预定义…

Django框架003:orm与MySQL数据库的连接及踩坑记录

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人的博客_CSDN博客-微信小程序,前端,python领域博主lqj_本人擅长微信小程序,前端,python,等方面的知识https://blog.csdn.net/lbcyllqj?spm1011.2415.3001.5343哔哩哔哩欢迎关注…

FFCNet:基于傅立叶变换的频率学习和复杂卷积网络用于结肠疾病分类

文章目录 FFCNet: Fourier Transform-Based Frequency Learning and Complex Convolutional Network for Colon Disease Classification摘要方法Patch Shuffling Module (PSM)Frequency-Domain Complex NetworkComplex ConvolutionComplex ReLUComplex BN 实验结果 FFCNet: Fou…

1— .Net MVC之控制器

在上下文中使用的控制器 问题 答案 什么是控制器&#xff1f; 控制器包含用于接收请求、更新应用程序状态或模型以及选择将发送给客户端的响应的逻辑 控制器有什么用&#xff1f; 控制器是MVC项目的核心&#xff0c;并包含Web应用程序的逻辑 如何使用控制器&#xff1f; …

第五讲:设计库的管理和使用

第五讲&#xff1a;设计库的管理和使用 概述&#xff1a; 课程内容简介&#xff1a; 1、 下载Design kit 2、 安装 Design kit 3、 Design kit的使用 4、 如何进行优化设计 Design Kit – 由元件厂商所提供设计元件库&#xff0c; PDK – Process Design Kit &#xff08;IC的…

智能改写工具免费-智能改写工具

中文改写软件 您好&#xff0c;中文改写软件是使用人工智能技术改写中文文本的工具。它可以自动识别并改写文本中的某些词语、短语或句子&#xff0c;以使它们在语意上更为准确、清晰或通顺。 中文改写软件的工作原理基于自然语言处理技术。它使用预先训练好的模型&#xff0…

Windows Server 2016安装Mysql 5.6

&#x1f388; 作者&#xff1a;互联网-小啊宇 &#x1f388; 简介&#xff1a; CSDN 运维领域创作者、阿里云专家博主。目前从事 Kubernetes运维相关工作&#xff0c;擅长Linux系统运维、开源监控软件维护、Kubernetes容器技术、CI/CD持续集成、自动化运维、开源软件部署维护…

软考——软件工程,面向对象,数据流图,数据库设计,UML建模,数据结构及算法应用,面向对象程序设计

软件工程 开发模型 瀑布模型 其他模型 增量模型和螺旋模型 V模型 瀑布模型是结构化模型&#xff0c;喷泉模型是面向对象模型 构件组装模型&#xff08;CBSD&#xff09; 统一过程 敏捷开发方法 信息系统开发方法 需求 结构化设计 基本原则 内聚与耦合 软件测试 测试原则与类…

Nmap服务版本探测研究

文件nmap-services 作用&#xff1a;预定义服务和端口映射表&#xff0c;该文件原则上不允许修改 nmap-services未定义33890端口映射&#xff0c;扫描结果&#xff1a; PORT STATE SERVICE REASON 33890/tcp open unknown syn-ack nmap-services定义33890端口映射&#x…

从环形图出发,打造高效数据分析流程

什么是环形图&#xff1f; 环形图是一种数据可视化的图表类型&#xff0c;它通常用于显示数据的比例关系和占比情况。环形图与饼图类似&#xff0c;都是由一个圆形或圆环和若干个扇形组成&#xff0c;每个扇形的面积大小表示该数据所占比例的大小。与饼图不同的是&#xff0c;…

强化学习p4-Actor-Critic

策略网络和价值网络的架构 我们知道状态价值函数 V π ( s ) ∑ a π ( a ∣ s ) ⋅ Q π ( s , a ) V_\pi(s)\sum_a\pi(a|s)\cdot Q_\pi(s,a) Vπ​(s)∑a​π(a∣s)⋅Qπ​(s,a)&#xff0c;在策略学习中&#xff0c;我们用神经网络去近似 π \pi π函数&#xff0c;得到策…

模糊PID(重心法解模糊梯形图FC)

模糊PID的模糊化请参看下面的博客文章: 博途PLC模糊PID三角隶属度函数指令(含Matlab仿真)_plc 模糊pid_RXXW_Dor的博客-CSDN博客三角隶属度函数FC,我们采用兼容C99标准的函数返回值写法,在FB里调用会更加直观,下面给大家具体讲解代码。常规写法的隶属度函数FC可以参看下…

分享一个提高运维效率的 Python 脚本

哈喽大家好我是咸鱼&#xff0c;今天给大家分享一个能够提升运维效率的 python 脚本 咸鱼平常在工作当中通常会接触到下面类似的场景&#xff1a; 容灾切换的时候批量对机器上的配置文件内容进行修改替换对机器批量替换某个文件中的字段 对于 Linux 机器&#xff0c;咸鱼可以…

Fiddler 抓包工具使用 - 手摸手教你

Fiddler简介 Fiddler 是一款免费、灵活、操作简单、功能强大的 HTTP 代理工具&#xff0c;是目前最常用的 HTTP 抓包工具之一。可以抓取所有的 HTTP/HTTPS 包、过滤会话、分析请求详细内容、伪造客户端请求、篡改服务器响应、重定向、网络限速、断点调试等功能。 Fiddler工作…

2023什么蓝牙耳机好?经销商盘点新手必入蓝牙耳机品牌

蓝牙耳机是除手机外我们使用频率最高的数码产品&#xff0c;我做蓝牙耳机经销商五年来&#xff0c;对各个品牌都有深入了解。近期看到很多新手们咨询什么蓝牙耳机好&#xff0c;我给大家盘点一下新手必看的五大蓝牙耳机品牌。 1.JEET Air 2蓝牙耳机 推荐理由&#xff1a;专为舒…

和AI聊天

AI产品工具目录&#xff1a;AI产品目录 Prompt 在和AI聊天时&#xff0c;你得表明你需要啥&#xff0c;并且描述的越精确&#xff0c;AI回答的就越贴近你的预期&#xff0c;简单的对话&#xff0c;可以自己尝试&#xff0c;但是如果想做应用级别的就得学习这种聊天技术&#…

Guitar Pro8.1最新中文版自动扒谱编写吉他谱 新功能讲解

Guitar Pro8是一款非常受欢迎的音乐制作软件&#xff0c;它可以帮助用户创建和编辑各种音乐曲谱。从其诞生以来就送专门为了编写吉他谱而研发迭代的。 尽管这款软件可能已经成为全球最受欢迎的吉他打谱软件&#xff0c;在编写吉他六线谱和乐队总谱中始终处于行业领先地位&…