c++类和对象 6个默认成员函数

在这里插入图片描述
默认成员函数
在c++中,类在创建对象的时候,默认加入的函数,也就是你即使在类中没有写这6个函数,编译器也会自动生成这些函数

1. 构造函数

1.1 构造函数的使用

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

先说说这个函数是为了解决什么问题而出现的:

  • 在我们实现栈的时候,可能忘记初始化,或者每次栈,队列,各种都需要在开头进行初始化,好麻烦,所以引入了构造函数的概念。
  • 构造函数的特点:
  1. 函数名和类名相同。
  2. 无返回值(函数定义前不需要写void,int,或者自定义类型)。
  3. 对象实例化时,编译器自动调用对应函数。
  4. 构造函数可以重载(可以写多个构造函数,提供多种初始化的方式)
#include<iostream>
using namespace std;
class Date
{
public:
        Date()
        {
                _year = 1;
                _month = 1;
                _day = 1;
        }
        Date(int x,int y,int z)
        {
                _year = x;
                _month = y;
                _day = z;
        }
        void Print()
        {
                cout << this->_year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year;
        int _month;
        int _day;
};

int main()
{
        Date a;
        a.Print();

        Date b(2023, 10, 11);
        b.Print();
        return 0;
}

在这里插入图片描述
我们上面提供了两种初始化的方式
第一种是无数据传入,默认全部成员变量初始化为 1 。
第二种是传入三个数据,成员变量被输入数据赋值。
这样,在定义好对象的时候,对象会根据输入的不同,而自动调用不同的构造函数。

  • 与前面的缺省参数结合起来,我们可以在输入的时候更加随意。
        Date(int x=1,int y=1,int z=1)
        {
                _year=x;
                _month=y;
                _day=z;
        }

这样的构造函数会更灵活。

1.2. 注意事项

还是上面Date类型
1 .传入需注意
如果定义的构造函数有参数传入需要写成

Date a(1,2,3);

但是下面这种情况要特别注意
在没有参数传入的情况下,后面不能加“()”。如:

Date a;//正确
Date a();//错误
Date Func();//函数声明

编译器不允许下面的写法----容易和声明函数冲突
可以看出,错误的和函数声明的写法一致,这么写会出现歧义。
传入参数的情况下:

Date b(2023, 10, 11);
Date f(int x,int y,int z);

函数声明和传参一眼就能分辨出来,不存在歧义。

  1. 全缺省和无参函数
        Date(int x=1,int y=1,int z=1)
        {
                _year=x;
                _month=y;
                _day=z;
        }
        Date()
        {
                _year=1;
                _month=1;
                _day=1;
        }

无参的函数和全缺省的函数语法上构成重载,但是实际上我们使用的时候无法调用无参数的函数。
所以我们一般写 全缺省的函数* 更加灵活

下面的是我们后面会经常出现的老朋友,栈

#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public:
        Stack()
        {
        	a=nullptr;
        	top=capacity=0;
        }

        void StackPush(int x)
        {
                if (top == capacity)
                {
                        int newcapacity = (capacity == 0 ? 4 : 2 * capacity);
                        int* tmp = (int*)realloc(a,sizeof(int) * newcapacity);
                        if (tmp == NULL)
                        {
                                perror("malloc failed");
                                exit(-1);
                        }
                        if (a == tmp)
                        {
                                cout <<capacity<<"  " << "same" << endl;
                        }
                        else
                        {
                                cout << capacity << "  " << "not same" << endl;
                        }
                        a = tmp;
                        capacity = newcapacity;
                }
                a[top++] = x;
        }
        
        void StackPop()
        {
                assert(top > 0);
                a[top--] = 0;
        }

        void StackDestory()
        {
                free(a);
                top = capacity = 0;
        }
        void StackPrint()
        {
                for (int i=0;i<top;i++)
                {
                        cout << a[i] << "  "  ;
                }
        }
        bool Empty()
        {
                return top == 0;
        }

        int Top()
        {
                assert(top > 0);
                return a[top - 1];
        }
private:
        int* a;
        int top = 1;//声明时的缺省值,补坑
        int capacity = 1;
}

如果我们知道我们要传入600个数据,Push里面的扩容的顺序 4 8 16,32 64,128,256,512,1024,最后会多出狠多空间,所以这里使用传入参数更好,直接在初始化中开辟好空间。
但是如果我们不知道需要传入多少参数时,不传入参数更好。
所以,这里我们就可以使用缺省参数解决上面的问题
初始化:

 Stack()
        {
                if (n == 0)
                {
                        a = nullptr;
                        capacity = top = 0;
                }
                else
                {
                        a = (int*)malloc(sizeof(int) * n);
                        if (a == nullptr)
                        {
                                perror("malloc failed");
                                exit(-1);
                        }
                        capacity = n;
                        top = 0;
                }
        }

如果不传入参数,默认初始化栈
传入参数,就会根据传入参数的大小开辟空间。

  • 有了上面的构造函数,我们想初始化什么样都可以。

1.3. 默认的含义

  • 构造函数,也是默认成员函数,我们不写,编译器会自动生成。

1. 没有,编译器会自动生成

不写才编译器会自动生成一个,写了编译器就不会自动生成。

class Date
{
public:
         Date(int year,int month=1,int day=1)
        {
                _year = year;
                _month = month;
                _day = day;
        }
private:
        int _year;
        int _month;
        int _day;
};
int main()
{
    Date d;
    d.Print();
    
    return 0;
}

在这里插入图片描述
这里由于我们写了构造函数,编译器不会自动生成构造函数。但是我们写的构造函数不满足我们定义对象的条件(必须至少传入一个值),它会没有构造函数可以自动调用,因此会显示“不存在默认构造函数”。
但是这里系统自动生成的构造函数,是,没有参数传入的,是为了让所有函数都可以调用该函数。

2. 不会处理内置类型

内置类型的成员不会处理,如编译器本来就含有的 int char 等类型(在c++11中,支持给缺省值----成员变量初始值)

class Date
{
public:
         Date(int year,int month=1,int day=1)
        {
                _year = year;
                _month = month;
                _day = day;
        }
private:
        int _year=1//声明给的缺省值,默认生成的构造函数就会使用缺省值初始化
        int _month=1int _day=1};

ps:这里的成员变量的缺省值只是补 不能给内置类型初始化的坑。

3. 自定义类型的成员,会调用成员的构造函数

如:
下面要实现栈实现队列:

class MyQueue
{
private:
        Stack _pushst;
        Stack _popst;
};
int main()
{
        MyQueue mq;

        return 0;
}

在这里插入图片描述
在 MyQueue 这个定义的类型中,我们声明的成员变量都是 Stack 类型,也就是我们上面栈的类型。
对内部成员是自定义类型的,在创建对象,系统自动创建的MyQueue构造函数,会自动调用成员的构造函数。

  • 一般情况下,都需要我们自己写构造函数,决定初始化成员变量的方式。
  • 成员变量都是自定义类型的,可以考虑不写其构造函数。

4. 不需要传值调用的–默认构造函数

无参,全缺省,系统自动生成,的构造函数,都可以认为是默认构造函数。
这三种默认构造函数只能存在其中一个,存在多个会存在歧义。


2. 析构函数

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

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型
  3. 一个类只能有一个析构函数。若我们没有定义,系统会自动生成一个默认的析构函数,注:析构函数不能重载。
  4. 对象生命周期结束时,c++编译系统会自动调用析构函数。
        ~Date()
        {
                _year = 0;
                _month = 0;
                _day = 0;
        }

对于上面 Date 类型的析构函数,我们并没有严格要求,因为内部 Date 内部成员变量都是局部变量,如果主函数结束,这些局部变量也就会自己销毁。
析构函数的主要价值在于 对栈,队列等这些结构上。
栈每次在程序结束之前,都需要我们手动销毁,但是有了析构函数,在每次对象销毁的时候,就会自动调用析构函数,帮我们进行销毁。

  • 我们在c++中一个功能比较完善的栈就差不多写好了:
class Stack
{
public:
        Stack(int n = 4)
        {
                if (n == 0)
                {
                        a = nullptr;
                        capacity = top = 0;
                }
                else
                {
                        a = (int*)malloc(sizeof(int) * n);
                        if (a == nullptr)
                        {
                                perror("malloc failed");
                                exit(-1);
                        }
                        capacity = n;
                        top = 0;
                }
        }

        void StackPush(int x)
        {
                if (top == capacity)
                {
                        int newcapacity = (capacity == 0 ? 4 : 2 * capacity);
                        int* tmp = (int*)realloc(a,sizeof(int) * newcapacity);
                        if (tmp == NULL)
                        {
                                perror("malloc failed");
                                exit(-1);
                        }
                        if (a == tmp)
                        {
                                cout <<capacity<<"  " << "原地扩容" << endl;
                        }
                        else
                        {
                                cout << capacity << "  " << "异地扩容" << endl;
                        }
                        a = tmp;
                        capacity = newcapacity;
                }
                a[top++] = x;
        }
        
        void StackPop()
        {
                assert(top > 0);
                a[top--] = 0;
        }
        void StackPrint()
        {
                for (int i=0;i<top;i++)
                {
                        cout << a[i] << "   "  ;
                }
        }
        bool Empty()
        {
                return top == 0;
        }

        int Top()
        {
                assert(top > 0);
                return a[top - 1];
        }
        ~ Stack()
        {
                cout << "~ Stack()" << endl;
                free(a);
                top = capacity = 0;
        }
private:
        int* a;
        int top = 1;//声明时的缺省值,补坑
        int capacity = 1;
};
class MyQueue
{
private:
        Stack _pushst;
        Stack _popst;
};

int main()
{
        Stack st;
        st.StackPush(1);
        st.StackPush(2);
        st.StackPush(3);
        st.StackPush(4);
        st.StackPush(5);

        return 0;
}

在这里插入图片描述

我们不需要自己手动初始化,不需要自己手动销毁,这些构造函数和析构函数都帮我们处理好了,我们只需要进行我们想进行的操作即可。

如果我们定义了许多对象

int main()
{
		Date d1;
		Date d2;
	
		Stack st1;
		Stack st2;

        return 0;
}

调用析构函数时,是按照函数栈帧的顺序调用,最开始d1,d2,st1,st2 分别入栈,然后销毁的时候,st2先调用析构,st1调用,d2调用,d1调用。
后定义的先析构。

默认的析构函数:

  • 内置类型成员不会处理
  • 自定义类型会调用这个成员的析构函数
    如:
class MyQueue
{
private:
        Stack _pushst;
        Stack _popst;
};
int main()
{
        MyQueue mq;

        return 0;
}

在这里插入图片描述只需要定义一个MyQueue对象,他就会自动生成构造和析构函数,自动帮我们初始化和销毁,这不爽歪歪。

构造和析构最大的特性:自动调用

3. 拷贝构造函数

3.1. 浅拷贝

拷贝,也就是复制一份新的内容
我们先认识一下浅拷贝
首先,还是使用上面 Date类

void func(Date d)
{
	
}
int main()
{
	Date d1(2013,10,20);
	func1(d1);

    return 0;
}

如果我们想要拷贝Date类,我们调用func函数,在函数内,局部变量 d 会复制一份传入参数 d1 的值。
在这两份空间,我们对拷贝的 d 进行修改,修改结束后,不会对原数据 d1产生影响。

我们对栈进行上面对Date类的操作,我们看看会发生什么。

void Func(Stack st2)
{
    st2.StackPrint();
}
int main()
{
    Stack st1;
    Func(st1);
    return 0;
}

在这里插入图片描述
我们发现它经历了两个析构函数,还崩了
在这里插入图片描述
这就是浅拷贝的问题,我们简单分析一下:
在这里插入图片描述
虽然我们对 st1 复制了一份给 st2 ,但是 st1的a 存储的是 动态数组的空间首地址,复制给 st2 之后,是将 st1.a保存的地址复制一份给了 st2 的 a,但是开辟的数组空间仍然只有一份。
所以在 st2 进行销毁的时候,析构函数会自动销毁开辟的空间,st1此时指向的是数组的地址,st2销毁后,st1会变为野指针,在后面 st2 销毁的后,会对已经销毁的空间再次进行销毁,因此发生错误。
st1,st2 指向空间相同,st1,st2是是两个变量,他们地址不同。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
如果想要解决两次析构的问题,很简单,传入引用即可

void Func(Stack& st2)
{
    st2.StackPrint();
}

在这里插入图片描述
在这里插入图片描述

传入引用的话,由于都是使用 st1 没有产生第二个变量,当函数结束时,不会对 st1 进行销毁, 只有当程序结束的时候 st1 才会销毁,只销毁一次。
但是我们的最终目的是拷贝这个 st1,但是引用传入的就是st1,如果我们在func函数内修改数据,主函数内的st1 数据也会改变,这不是我们想要的拷贝。

3.2. 深拷贝(拷贝构造函数)

深拷贝:也就是对需要拷贝的内容整体做一份拷贝,如栈,原栈开辟了多少空间,新拷贝的也要开辟多少空间,且和原空间不存在重合,这样我们对新拷贝空间的使用,就不会影响原空间。
拷贝构造函数:
拷贝构造函数也是特殊的成员函数,特征:

  1. 拷贝构造函数时构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式编译器直接报错,会引发无穷递归调用。
    Date(Date d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    int main()
    {
    Stack st1;
    Date a;
    Date a1(a);
    //Date (a);
    return 0;
    }    

注意:参考上面第一个函数,构造函数。
我们调用构造函数的方式

Date d;

如果我们按照上面 Date(Date d)的方式定义,我们看看会发生什么
在这里插入图片描述
在这里插入图片描述

会导致无穷递归,但是因为编译器检查严格,我们这里无法观察,我们可以推一下
在这里插入图片描述
因为每一次在传入参数位置,我们都用的是 Stack s,也就是在每次传入参数的位置,会将传入参数判断为新的拷贝构造,然后再次往下判断。,这样会导致无穷递归。

  • 所以这里我们就不能进行值传参,需要引用传参。
Date(Date& d)
{
		_year = d._year;
		_month = d._month;
		_day = d._day;
}
    Stack(Stack& s)
    {
        cout << "Stack(Stack& s)" << endl;
        a = (int*)malloc(sizeof(int) * s.capacity);
        if (NULL == a)
        {
            perror("malloc failed");
            exit(-1);
        }
        memcpy(a, s.a,(sizeof(int) * s.top));
        top = s. top;
        capacity = s.capacity;

    } 

引用是一种别名,传入后,传入参数为 Date& d ,传参的时候就不会进入下一层的拷贝构造函数,也就不会出现上面的错误。

怎么使用拷贝构造函数

void Func(Stack st2)
{
    st2.StackPrint();
    st2.StackPush(5);
    st2.StackPush(6);
    st2.StackPush(7);
    st2.StackPush(8);
    st2.StackPrint();
}
int main()
{
	Stack st1;
    st1.StackPush(1);
    st1.StackPush(2);
    st1.StackPush(3);
    st1.StackPush(4);
    st1.StackPrint();
    Func(st1);
    st1.StackPrint();
	return 0;
}

在这里插入图片描述
这里,我们传入的参数的时候,会先进入Func函数的参数传入,这里会被认为是调用拷贝构造函数,然后调用,再接收拷贝后的st1,然后用拷贝的部分进行函数内的操作。
在这里插入图片描述

指针也可以实现上面的拷贝构造函数,但是与引用还是存在差别。
使用指针的传参:

Date d2(&d1);
func1(&d1);

写起来没有引用的方便。
下面两种写法等价

Date d3=d1;
Date d2(d1);

拷贝构造函数特性

  • 拷贝构造和前面两张特性不太一样
  1. 内置类型,进行值拷贝。
  2. 自定义类型,调用他的构造函数。

日期类,不需要我们去实现拷贝构造,默认生成的就可以用。
但是 栈 这种,仅仅是对内部成员的值拷贝,不满足我们使用的需要(如:两个指针,复制完后都指向同一块空间,对一个指针指向的空间进行修改,会影响原指针指向的空间),对这种类型,我们不能使用浅拷贝,必须使用深拷贝。

和上面两个函数类似,内部函数为自定义类型的不要写默认拷贝构造函数

class MyQueue
{
public:
    Stack _pushst;
    Stack _popst;
};

在这里插入图片描述
这里,我们只是简单定义了qu,然后拷贝了一份qu,他就会自动调用内部成员的 默认构造函数,拷贝函数,析构函数。

拷贝构造函数的注意

const引用

Date( Date& d)
{
	d._year = _year;
	d.month = _month;
	d._day = _day;
}

如果我们拷贝构造函数内部写反了,系统不会报错,但是这样运行出来后,原来有的数据会被覆盖。
所以如果我们传入的引用,只是为了赋值,我们就可以使用const 对其修饰,这里属于权限的缩小(从Date类型变为const Date类型),不会产生问题,如果函数内我们写反了,就会报错,有效防止错误。

4. 赋值运算符重载

4.1. 运算符重载的认识

如果想要比较两个 Date 类的大小

int main()
{
	Date d1(2023,10,18);
	Date d2(2022,8,20);

	d1<d2;
	return 0;
}

如果是两个整形类型,我们可以这样比较,但是这是两个自定义类型,我们不能直接进行比较。
一般想要比较自定义类型大小的话,可以使用函数来操作

bool DateLess(const Date& x1,const Date& x2)
{
		if(x1._year<x2._year)
		{
				return true;
		}
		else if(x1._year == x2._year&&x1._month<x2._month)
		{
				return true;
		}
		else if(x1._year == x2._year&&x1._month==x2._month&&x1._day<x2._day)
    	{
    		return true;
   		 }
  	  	 else
    	 {
    		return false;
         }
}
int main()
{
	     Date d1(2023,10,18);
		 Date d2(2022,8,20);
		 cout<<DateLess(d1,d2)<<endl;
		 return 0;
}

上面的函数能输出 d1<d2 的结果,但是代码的可读性不是很高,每次需要看函数名来判断这个函数的意义。
在c++中,有一种新的方式,运算符重载。
在类外:

bool operator<const Date& x1,const Date& x1)
{
	if(x1._year<x2._year)
    {
        return true;
    }
    else if(x1._year == x2._year&&x1._month<x2._month)
    {
        return true;
    }
    else if(x1._year == x2._year&&x1._month==x2._month&&x1._day<x2._day)
    {
        return true;
    }
    else
    {
        return false;
    }

int main()
{
    Date d1(2023,10,18);
    Date d2(2022,8,20);
    
    cout<<(d1<d2)<<endl;
    return 0;
}

c++规定,operator+‘<’ <会被当做函数名
调用的时候

cout<<(d1<d2)<<endl;
cout<<(operator<(d1,d2)<<endl;

如果使用运算符重载,可读性更强,只需要看中间的符号就知道是什么含义,不再需要根据那些函数名来判断。
但是上面在类外学的函数,访问了成员变量,按照上面的写法,我们不得不将成员变量的访问权限改为公有。
所以我们可以想办法将该函数写进类内部

    bool operator<(const Date& d)
    {
        if (_year < d._year)
        {
            return true;
        }
        else if (_year == d._year && _month < d._month)
        {
            return true;
        }
        else if (_year == d._year && _month == d._month && _day < d._day)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

上面写入类中,我们只需要传入一个参数即可,因为另一个参数是this指针的形式传入。
我们想要调用可以按照下面的方式:

cout<<(d1<d2)<<endl;
cout<<d1.operator<(d2)<<endl;

ps:上面 (d1<d2) 的括号是必须加上的,因为(d1<d2) 在编译器看来就是第二种写法,加上括号表示调用

4.2. 运算符重载的规定

c++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数,也是具有返回值类型,函数名以及参数列表,其返回值类型与普通函数类似。
函数名:关键字operator后面需要重载的运算符符号
函数类型:返回值类型operator操作符。
注意:

  • 不能通过连接其他符号来创建新的操作符:比如:operator@,使用原有的操作符对自定义类型使用。
  • 重载运算符必须有一个类的类型参数(自定义类型)
  • 用于内置类型的运算符,其含义不变,如:内置的类型 +,不能改变其含义。
  • 作为类成员函数重载时,其形参看起来比操作数少1个,因为成员函数的第一个参数为隐藏的this。
  • .*(点星),:: ,sizeof , ?:(三目) ,.(点) 注意以上5个运算符不能重载。
    简单来说,我们重载后的操作符要配合我们的使用习惯,创建运算符重载的目的是为了增强程序的可读性,不是为了整花活。

4.3. 函数的复用

#include<iostream>
using namespace std;
class Date
{
public:
        Date()
        {
                _year = 1;
                _month = 1;
                _day = 1;
        }
        Date(int x,int y,int z)
        {
                _year = x;
                _month = y;
                _day = z;
        }
        bool operator<(const Date& d)
    	{
        	if (_year < d._year)
        	{
            	return true;
        	}
        	else if (_year == d._year && _month < d._month)
        	{
            	return true;
        	}
        	else if (_year == d._year && _month == d._month && _day < d._day)
        	{
            	return true;
        	}
        	else
       	 	{
           	 return false;
        	}
    	}
        void Print()
        {
                cout << this->_year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year;
        int _month;
        int _day;
};

上面我们简单的实现了 基础的日期类的小于
但是如果我们需要判断 日期相等,日期大于,日期大于等于,日期小于等于呢。
实现日期等于:

 	bool operator==(const Date& d)
 	{
 		 return _year == d._year && _month == d._month && _day == d._day;
 	}

有了上面的 < 和 == ,我们实现后面的,只需要对前面函数复用即可。

 bool operator<=(const Date& d)
    {
        return *this < d || *this == d;
    }
    bool operator>(const Date& d)
    {
        return  !(*this <= d);
    }
    bool operator>=(const Date& d)
    {
        return  !(*this < d);
    }
    bool operator!=(const Date& d)
    {
        return !(*this==d);
    }

扩充:日期计算的问题

对于日期类,如果我们给一个日期加上天数,它应该怎么变,这牵扯到日,月,年的变化,同时有些月的时间都不相同,所以,这种日期的加减法还是有必要的。

  1. 根据输入的日期,计算是这一年的第几天。
    保证年份为4位数且日期合法。
    进阶:时间复杂度:O(n)\O(n) ,空间复杂度:O(1)\O(1)
#include <iostream>
using namespace std;

class Date
{
public:
    Date(int year=1,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    int GetMonthDay(int year,int month)
    {
        static int arr[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
        if(month==2 && (year%4 == 0 && year%100 != 0 || year%400 == 0))
        {
            return 29;
        }
        return arr[month];
    }
    int dayofyear()
    {
        int sum=0;
        for(int i=1;i<_month;i++)
        {
            sum+=GetMonthDay(_year,i);
        }
        sum+=_day;
        return sum;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main() 
{
    int year=0,month=0,day=0;
    cin>>year>>month>>day;    
    Date d(year,month,day);

    int sum=d.dayofyear();
    cout<<sum;
    return 0;
}

4.4. const问题

const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员成员函数隐含的this指针,表明在该成员函数中,不能对类的任何成员进行修改。

  1. 后面加const
void testDate()
{
    const Date d1(2023,10,24);
    d1.Print();
}

上面这段代码,d1 是由const 修饰的,但是 Print 函数传递的 this指针 没有经过 const 修饰,如果传进去,会导致权限放大,所以这里直接调用Print函数会出现问题。
但是在定义 Print函数的时候,this 指针是默认 传进去的类型是自定义类型的地址,我们没办法直接修改。
为了避免上面的问题,c++中的新写法

void Print() const ;

这样能直接将默认的 this 指针定位为 const 类型,
这样写,在编译器看来:

void Print( const Date* this )

因为权限只能缩小和平移,所以后面传入的const 和非const 都没有影响。
2. 前面加const

const int func()
{
	int ret;
	return ret;
}

int func()
{
	int ret;
	return ret;
}

前面的const修饰返回值,后面的const修饰this指针。
两个函数,都返回的是ret,但是返回值的类型没有区别。
第二个函数返回值没有const,但是在返回ret的时候,会产生新的临时变量接收这个值,而新产生的临时变量具有常性,不可以修改,所以从返回值来说,上下没有区别。

  1. 后置const和没有const修饰的同时存在
void Print()const;
void Print();

看似没区别,实际上这两个函数构成重载,传入参数类型不同,上面传入的是 const Date* this,下面的传入的是Date* this。
这种情况下,如果我们传入const Date的类型,就是调用第一个函数,如果传入 Date 类型,就会调用第二个函数。
(如果没有第一个用const修饰的函数,编译器会进入最接近的第二个,也可以运行(权限缩小))

  • 上面的例子意义不大

4.5. [ ]运算符重载

#include<iostream>
using namespace std;
class Seqlist
{
public:
	void PushBack(int x)
	{
		_a[size++]=x;
	}
	size_t size()
	{
		return _size;
	}
	int operator[](size_t i)
	{
		assert(i<_size);
		return _a[i];
	}
private:
	int* _a = (int*)malloc(sizeof(int)*10);
	int _size =0;
	int _capacity = 10;
};
int main()
{
        Seqlist s;
        s.PushBack(1);
        s.PushBack(2);
        s.PushBack(3);
        s.PushBack(4);
        for (size_t i = 0; i < s.size(); i++)
        {
                cout << s[i] << "  ";
                cout << s.operator[](i) <<" ";
        }
        cout << endl;
        return 0;
} 

只要我们使用 s[i] ,他就会自动返回 s._a[i] 的数据,这样我们就能把对象当成数组一样使用,方便了很多。

4.6. const的意义

如果我们在类外面写 Print 函数

void Print(const Seqlist& sl)
{
	for(size_t i = 0 ;i < sl.size();i++)
	{
		cout<sl[i]<<" ";
	}
	cout<<endl;
}

在这里插入图片描述
这样写,编译器会报错,因为我们Print函数的参数是const 修饰的对象,但是 [] 运算符重载函数,接收的是非const修饰的对象,也就是说我们把const的对象,传入了非const的函数,所以会报错。
消除错误:定义的函数用const修饰

        size_t size()const
        {
                return _size;
        }
        int& operator[](size_t i)const
        {
                assert(i < _size);
                return _a[i];
        }

在这里插入图片描述
这样就没有问题了。

  • 但是如果我们在Print函数内进行下面操作
void Pirnt(const Seqlist& s1)
{
	for(size_t i =0; i <sl.size();i++)
	{
		sl[i]++;//对对数组内元素++
		//这里的sl[i]++在Print函数内,我们更希望这句代码会报错
		cout<<sl[i]<<" ";
	}
	cout<<endl;
}

在这里插入图片描述
我们会发现,数组内元素发生了改变,我们的数组全程是const保护过的,但是为什么上面的++操作依旧有效?
我们先分清楚const保护了什么,const保护的是 传入对象内容不被改变,也就是对象的成员变量,_a,_size,_capacity,const保护了这三个,但是_a是个数组,数组的内容不是传入对象的成员变量,不受const保护,所以会出现这种情况。

  • 解决方法:
    对 [] 运算符重载函数的返回值进行修改
const int& operator[](size_t i)const
{
	assert( i < _size);
	return _a[i];
}

这样写的话,我们返回的值会受到const的保护,这样在Print函数内我,我们就无法修改数字内的数据。
但是这样写,如果我们在其他地方想要改变数组中的值

int main()
{
        Seqlist s;
        s.PushBack(1);
        s.PushBack(2);
        s.PushBack(3);
        s.PushBack(4);
        for (size_t i = 0; i < s.size(); i++)
        {
               s[i]++;
        }
        cout << endl;
        return 0;
} 

在这里插入图片描述
这里会报错,因为上面函数返回来的是const保护过的数据,这样++是对const修饰的内容++,所以会报错。
因此这里需要一个没有const修饰返回值的函数。

  • 这个时候函数重载就用上了
    我们可以写两个函数。
int& operator[](size_t i)
{
	assert( i <_size)
	return _a[i];
}
const int& operator[](size_t i)const
{
	assert( i < _size);
	return _a[i];
}

上面的函数构成重载。
如果我们在Print函数内使用,传入的是const修饰的参数,会进入第二个函数,最后返回的是const保护的数据,s[i]++会报错。
如果我们在主函数想要修改数组的数据,传入的是没有const修饰的参数,会进入第一个函数,返回来的数据也是数组数据的引用,可以直接修改。
const修饰的,只支持读
非const修饰的,支持写入
完美的解决了上面的问题。

  • 这个方法在c++的库函数中就在使用
    在这里插入图片描述
    vector中的 [] 也是使用了重载的使用方法。
  • 这里要注意const的使用
    非const修饰的变量/对象传入const修饰过的变量/对象,权限都会缩小。
    const修饰过的变量/对象传入非const修饰过的变量/对象,权限会放大。
  1. 权限放大不可以,权限缩小可以。
  2. const函数,const函数可以调用(权限平移),非const函数也可以调用(权限缩小)
  3. 只读函数可以加const,内部不涉及修改生成。

5. 取地址运算符的重载

取地址运算符重载和const取地址运算符重载近似,而且因为编译器自动生成的够用,平时用的比较少,这里两个合起来说
本质还是运算符的重载

Date* operator&()
{
	return *this;
}

取地址运算符,作用还是取对象地址,但是平时编译器会自动生成,自动生成的足够使用。
如果我们的对象是const类型,或者只需要读取,不需要修改,和上面一样,用const修饰即可

const Date* operator&()const
{
	return this;
}

如果仅仅是传对象地址,就没什么说的,但是哦们可以自己写这两个函数,我们返回的就不一定只是对象地址了。

  • 如果不想让别人获取我这个类的对象的地址
    我们可以自己实现一个取地址运算符重载
Date* operator&()
{
	return nullptr;
}
const Date* operator&()const
{
	return this;
}

在这里插入图片描述
这样的话,最后返回来的地址,非const的不能取到地址,const修饰的可以取到地址。
如果想整活,也可以返回野指针,看个人

Date* operator&()
{
	return (Date*)0x01202;//强制将后面的数据转换为(Date*)类型,可以返回的类型
}

这种情况很危险,一般不要这样写。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值