类和对象基础知识

类和对象

类和对象基础知识

面向过程和面向对象

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成

类的引入

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数

C++虽然可以用struct来定义类,不过相比之下,C++更喜欢用关键字class

类的定义

语法:

class 类名
{
  成员变量
  成员函数
};

类体中内容称为类的成员:类中的变量称为类的属性成员变量; 类中的函数称为类的方法或者成员函数

类中成员变量的定义写在类的前面还是后面都无所谓。因为类是一个整体,既会向上搜索也会向下

对于成员变量

小建议:对于成员变量的命名可以在最前面加个_来区分于普通变量

对于成员函数

成员函数的定义有两种方式:

  1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中。注意如果这样的话,在对.cpp文件中的成员函数定义时,需要在成员函数名前加上类名::

类的访问限定符

访问限定符共三种:public(公有)、protected(保护)、private(私有)

说明:

  • public修饰的成员在类外可以直接被访问

  • protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

    protected和private的区别将在继承时体现

  • class的默认访问权限为private,struct为public(因为struct要兼容C)

注意:

  • 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
  • 访问限定符是限制类外不能随意访问类内的成员;而在类中可以随便访问成员函数或成员变量

类的作用域

类定义了一个新的作用域–类域,类的所有成员都在类的作用域中**。**在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域

类的实例化

用类类型创建对象的过程,称为类的实例化

对于变量来说,声明和定义的区别在于,定义会给变量开辟一块属于它的内存空间

所以,在类中的成员变量是声明

只有当类对象实例化后,成员变量才被定义。即也可以将类对象的实例化看作是在开辟空间

一个类,可以实例化出多个对象

打个比方:类就是别墅设计图,实例化就是依据设计图建造一栋栋别墅

image-20230315165827530

image-20230315120254637

类对象的大小

类对象的大小=类中成员变量的大小

类对象内存大小依据内存对齐的规则

也就是说类对象中只存储了成员变量,成员函数不存储在类对象中

为什么会这样设计呢?

我们看如下代码:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2;
	d1.Init(2022,2,2);
	d1._year++;
	d2.Init(2023,2,2);
	d2._year++;

	return 0;
}

这里的d1._yeard2._year是肯定不一样的,这两个成员变量是存储在不同的类中的。但d1.Initd2.Init调用的成员函数是一样的。

正是因为如此,我们完全可以将成员函数放在一个单独的共享空间(叫代码段),等用的时候直接去那里调用就行,而不用在类中去找了

为什么不通过在类中存储成员函数的地址这种方式呢?因为一个地址4个字节,10个函数就40个字节,10个对象就400个字节,所占用的空间是很大的。

不是说函数是建立在栈上的吗?为什么这里又说成员函数在代码段中?

这两者并不矛盾

参数和局部变量是函数运行起来存在函数栈帧中的;

成员函数存在代码段指的是成员函数被编译出来的指令(比如说压参数的指令)存在代码段

这两个是完全不同的东西。一个是指令,一个指令运行过程中相关的数据

调用函数即去运行代码段中的指令,运行指令期间,会有一些指令比如建立函数栈帧、开辟局部变量的空间,那么这里的函数栈帧、局部变量空间就是存放在栈中的。

对于没有成员变量的类,其实例化对象的大小为1个字节。这个1字节不是用来存储数据的,而是用来占位的,标识对象被实例化定义出来了

this指针

this指针的引出

在上面的Date类中的Init成员函数,函数体内并没有关于不同对象的区分。那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数增加了一个隐式的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

编译器会自己在成员函数的参数列表中增加一个参数this指针,并在调用处增加一个&对象

image-20230315142053447

在实参和形参处的添加参数我们是无法处理的,但我们可以在函数体内使用this

这一过程是在预处理之前就完成的。我们的代码都会转换成指令,只要转换成指令的时候多压一个参数就行了

this指针的特性
  1. this指针的类型是指向常量的指针。即成员函数中,不能给this指针赋值

  2. this指针的本质是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。

  3. this作为形参是存储在栈上的。但在VS下,考虑到this指针的频繁使用,而将其存到ecx寄存器中

    image-20230315143603753

this指针的例题
class A
{
  public:
    void Print()
    {
        cout << "Print()" << endl;
    }
    void PrintA() 
    {
        cout<<_a<<endl;
    }
  private:
    int _a;
};


int main()
{
 	A* p = nullptr;
 	p->Print();		//正常运行
    p->PrintA();	//运行崩溃
 	return 0;
}

解释为什么一个正常运行,一个运行崩溃

  1. 首先这里的p是一个空指针,那为什么空指针能够调用成员函数?这是因为成员函数并不在对象中,当调用函数的时候会到代码段中去找这个成员函数

    不是说有箭头、有*号就一定会对指针解引用

    解不解引用取决于到底需不需要到对象中去找

    p->Print();
    p->_a;
    

    Print( )不需要到对象中找;_year则需要到对象中找

    注意:注意p->(*p).是一样的,解引用的两种等价写法

    那我们调用成员函数能不能不用对象去调用?不是说成员函数不在对象中吗?

    不能。因为受到类域的限制,你不写对象的话,仅仅只会到全局区找这个函数,但这样是找不到的;并且调用成员函数需要传递this指针,没对象,也就没this指针

  2. 再者,调用成员函数会传递this指针,这里的this指针就是p,但要注意的是它是一个空指针,也就是说如果成员函数中有对他解引用,那么就会报错。这也就是p->PrintA()报错的原因

通过this指针看C和C++的区别

image-20230315154449434

image-20230315154500954

上面是C++类对象调用成员函数,下面是C语言结构体对象调用函数。

可以看到C++中是隐式传递对象,而C语言中是显式的

也可以看作是在C++中对函数的调用做了简化

封装

面向对象的三大特性:封装、继承、多态

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用

构造函数

C++用构造函数来替代C中类似于Init的初始化函数

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

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

构造函数的特性

  1. 函数名与类名相同

  2. 无返回值(即函数名前面啥都没有)

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

    这里对应的意思就是构造函数有多个,有有参数的,有无参数的

  4. 构造函数可以重载

    意味着一个类可以有多个构造函数,也就是说可以有多种初始化方式

构造函数的示例及注意点

class Date
{
public:
    // 1.无参构造函数
    Date()
    {}
    // 2.带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;          '
        _month = month;
        _day = day;
    }
    //3.带参全缺省构造函数(当然也可以半缺省)
    Date(int year = 0, int month = 0, int day = 0)
    {
        _year = year;          '
        _month = month;
        _day = day;
    }
    
private:
    int _year;
    int _month;
    int _day;
};

void TestDate()
{
    Date d1; 			// 调用无参构造函数或带参全缺省构造函数
    Date d2(2015, 1, 1);// 调用带参构造函数
}

注意点:

  • 如果通过无参构造函数或全缺省构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

    错误示范:Date d3();

  • 无参构造函数与全缺省构造函数不能同时存在。尽管语法上它,他们两个构成函数重载,但调用时会报错:函数重载调用不明确,因为在调用时,无参构造和全缺省构造的语法都是类 对象名;

默认构造函数

对象实例化时必须调用构造函数,否则会报错

如果类中没有显式定义构造函数,那么编译器会自动生成一个默认构造函数。但一旦我们自己生成了一个构造函数(不管是有参数的还是没参数的),编译器都不再生成构造函数。

因此假如,我们自己生成了一个带参数的构造函数,但我们创建了一个无参数的对象,那么就会报错

默认构造函数就是不传参就可以调用的构造函数

包括:无参构造函数、全缺省构造函数、我们没写时编译器默认生成的构造函数

关于编译器默认生成的构造函数:

在初始化的时候,对内置类型成员(如int、char、指针等)不做处理,即内置类型成员仍是随机值;对自定义类型成员(如class、struct、union等)会去调用它们的默认构造函数

因此默认生成的构造函数对于像时间类这种成员变量都是内置类型的,没啥用。

但对于像用两个栈实现队列时,在Queue中使用默认构造函数就很有用。因为Queue类的成员变量是栈,栈是一个class,class是自定义类型的

在C++11中对“内置类型成员不做处理”做出了补丁,可以对内置类型成员进行初值声明

class Date
{
  //……
private:
 // 基本类型(内置类型)
 int _year = 1970;
 int _month = 1;
 int _day = 1;
}

**注意这不是初始化!!!**这个缺省值的作用就是如果你不写构造函数,那么默认生成的构造函数就会用这个缺省值初始化

初始化列表

语法:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式

class Date
{
public:
    Date(int year, int month, int day)
        	: _year(year)
            , _month(month)
            , _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};
初始化列表存在意义

在实例化对象时,是对对象整体的定义。而当对象调用构造函数时,走到初始化列表处,则是对该对象所有的成员变量进行定义及初始化

不论某个成员变量,你是否在初始化列表处显式初始化,初始化都会对其初始化。哪怕冒号都没写,还是会走初始化列表。因为那是成员变量定义的地方。只不过不一定会去进行初始化

成员变量的初始化值:

  1. 假如该成员变量显式初始化了,那么其值就是显式初始化的值
  2. 假如没有显式初始化,但其有缺省值,那么就会用缺省值为其初始化
  3. 如果该成员变量既没有显式初始化,也没有缺省值,那么它的值就是随机值

由上可知:当类中存在const修饰的成员变量时,假如既不对其设置缺省值,也不将其在初始化列表初始化。那么对该类实例化对象时,就会报错。因为const修饰的变量必须在定义时就初始化

有了初始化列表后,我们再来看在构造函数内对成员变量初始化,其实这已经不叫初始化了,而是叫赋值了。因为初始化只能初始化一次,而构造函数体内可以多次赋值

注意点
  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

    • const修饰的成员变量
    • 引用成员变量
    • 自定义类型成员(且该自定义类没有默认构造函数)
  3. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

    class A
    {
    public:
        A(int a)
           :_a1(a)
           ,_a2(_a1)
       {}
        
        void Print() 
        {
            cout<<_a1<<" "<<_a2<<endl;
       }
    private:
        int _a2;
        int _a1;
    };
    
    int main() 
    {
        A aa(1);
        aa.Print();
    }
    A. 输出1  1
    B.程序崩溃
    C.编译不通过
    D.输出1  随机值
    

    答案:D

    因为在类中先声明的_a2后声明的_a1,所以在初始化列表中先初始化_a2,后初始化_a1

  4. 尽量使用初始化列表进行初始化

    因为,初始化列表是怎么样都会走的;况且有些变量只能在初始化列表进行初始化

    但不是说不在函数体内初始化。比如说,数组啥的,需要for循环进行初始化,那么还是要在函数体内进行的

析构函数

C++中用析构函数来替代类似于C中Destroy的销毁函数

析构函数是特殊的成员函数,与构造函数功能刚好相反。析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,比如说在堆上开辟的空间的清除

析构函数的特性

  1. 函数名是在类名前加上字符 ~
  2. 无参数、无返回值类型
  3. 对象生命周期结束时,编译系统自动调用析构函数
  4. 析构函数不能重载。即一个类只能有一个析构函数

析构函数的示例

class Stack
{
 public:
    Stack(int capacity = 3)
    {
        _array = (int*)malloc(sizeof(int) * capacity);
        _capacity = capacity;
        _size = 0;
    }
	
    //析构函数
    ~Stack()
    {
        free(_array);
        _array = NULL;
        _capacity = 0;
        _size = 0;
    }
private:
    DataType* _array;
    int _capacity;
    int _size;
};

void TestStack()
{
    Stack s;
}

默认析构函数

假如我们不写析构函数,编译器自己生成一个默认析构函数。这个编译器自动生成的默认析构函数,对内置类型成员不处理(也不需要处理,出了作用域就会自动销毁了);对自定义类型成员,则会调用它们的析构函数

看下例代码:

class Time
{
public:
    ~Time()
    {
        cout << "~Time()" << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;
};

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

程序运行结束后输出:~Time()

在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?

因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而 _t是Time类对象,所以在d销毁时,要将其内部包含的Time类的 _t对象销毁,所以要调用Time类的析构函数。

但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,**则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数。**即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。

注意:创建哪个类的对象则调用该类的构造函数,销毁那个类的对象则调用该类的析构函数

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

构造函数和析构函数的调用顺序

  • 类的析构函数调用一般按照构造函数调用的相反顺序进行调用

    但是要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象

  • 全局对象先于局部对象进行构造

  • 局部对象按照出现的顺序进行构造,无论是否为static

示例:

C c;
int main()
{
    A a;
    B b;
    static D d;
    return 0;
}

易得构造函数的调用顺序为:c、a、b、d

析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部对象之后进行析构

因此析构函数的调用顺序为:b、a、d、c

拷贝构造函数

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

拷贝构造函数的特性

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

  2. 拷贝构造函数的参数只有一个且必须是自身类类型对象的引用

    如果不用引用传参,而是传值传参则会发生无穷递归调用,编译器会直接报错

这里解释一下为什么要传引用传参,而传值传参就会发生无穷递归:

  • 首先C++规定:对于内置类型成员,传值传参直接拷贝即可;对于自定义类型成员,传值传参则需要调用拷贝构造函数

    这是因为:内置类型成员的大小是确定的,而自定义类型成员的大小是不确定的,所以编译器自己不敢直接拷贝

    在C语言中对于结构体等类型的拷贝,其实是有问题的

  • 比如说看下面的函数:

    void Func(Date d)
    {
        //……
    }
    
    

    当我Func(d1)调用这个函数时,编译器会先调用Date类的拷贝构造函数,先来生成形参d,然后将d1拷贝给d

  • 由上得知:如果拷贝构造函数用传值传参的话,如下:

    Date(Date d)
    {
        //……
    }
    

    我想用已经实例化的d1来实例化d2,即:Date d2(d1)。d2对象实例化时会去调用对应的构造函数,这里对应的构造函数是拷贝构造函数。而调用拷贝构造函数前需要先传参,传值传参又要调用一个拷贝构造函数;如此循环,即是无穷递归

  1. 拷贝构造函数还分为浅拷贝深拷贝

    如果自定义类型涉及到了空间的开辟就需要深拷贝的拷贝构造;如果自定义类型没有涉及空间的开辟那么浅拷贝的拷贝构造就足够了

拷贝构造函数的示例及注意点

class Date
{
public:
    //全缺省构造函数
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //拷贝构造函数
    Date(const Date& d)  
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    Date d2(d1);
    return 0;
}

注意点:

  • 拷贝构造函数有两种写法。除上述写法外,还可以写成:Date d2(d1)
  • Date(const Date* d)这并不是拷贝构造函数,拷贝构造函数必须用引用传参。这就是普通的构造函数,平时很少使用
  • 习惯上在拷贝构造函数的参数最前面加个const,防止改变源对象

默认拷贝构造函数

如果没有显式定义拷贝构造函数,则编译器会生成默认的拷贝构造函数。

默认拷贝构造函数对于内置类型成员会按内存存储一个一个字节拷贝,这种拷贝叫做浅拷贝或者值拷贝;对于自定义类型成员,则会去调用它们的拷贝构造函数

深拷贝

如果自定义类型成员中存在开辟空间的情况,此时如果还是用浅拷贝的话,则会出现原对象与拷贝对象指向同一块空间的问题。这会导致:

  • 插入、删除数据会相互影响
  • 对这块空间会析构两次,导致程序崩溃

我们看如下代码:

class Stack
{
public:
	//全缺省构造函数
	Stack(int capacity = 10)
	{
		_array = (int*)malloc(capacity * sizeof(int));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	//压栈
	void Push(const int& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	//析构函数
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _array;
	int _size;
	int _capacity;
};

int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

运行代码后,发现程序崩溃了,这是为什么呢?

经过调试发现,原本我们想用s1实例化一个新对象s2,可是s2中的数组地址指向了s1的数组地址,也就是两个不同类中的两个数组却指向了同一块空间。image-20230318092748454

发生上述问题的原因是,用s1实例化s2时,调用的是默认拷贝构造函数,是浅拷贝。而浅拷贝拷贝有空间开辟的自定义类型成员时,就会出现上述问题。

image-20230318093445496

image-20230318093506968

为了解决上述情况,有了深拷贝。深拷贝需要我们自己去实现,即手动开辟一个新空间,使拷贝对象指向新空间,再将原空间中的值拷贝进新空间即可。

image-20230317231956012

注意:类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

判断是否要写拷贝构造函数的小tips:如果自己实现了析构函数释放空间,就要实现拷贝构造。因为这里就一定涉及了资源申请

经典实例

image-20230317233550856

对于MyQueue这个类来说,构造函数、析构函数、拷贝构造函数都用默认的即可。反正都会去调用Stack类的构造函数、析构函数、拷贝构造函数

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

  1. 使用已存在对象创建新对象
  2. 函数参数类型为类类型对象
  3. 函数返回值类型为类类型对象
class Date
{
public:
    //构造函数
    Date(int year, int minute, int day)
    {
        cout << "Date(int,int,int):" << 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;		//场景3
}

int main()
{
    Date d1(2022,1,13);	//场景1
    Test(d1);			//场景2
    return 0;
}

image-20230318000451791

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用

赋值运算符重载

运算符重载

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

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

注意:

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

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

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

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

  • 运算符重载和函数重载之间没有关联。运算符重载是为了自定义类型成员也可以使用运算符

  • 不能重载的五个运算符:.*::sizeof?:.

    这里的.*很少用,记住就好

运算符重载写的位置以及调用写法

运算符重载可以写在全局,也可以写在类中

不过,不是所有的运算符都可以既写在全局,又写在类中的

一般建议写在类中。因为写在全局,会存在类中私有成员变量,无法在类外使用的问题

尽管后面的友元函数能解决上述问题,但我们不太推荐友元函数

以判断相等运算符重载为例:

  • 写在全局
bool operator==(const Date& x1, const Date& x2)
{
    return x1._year == x2._year
        && x1._month == x2._month
        && x1._day == x2._day;
}

这样写必须要把_year、_month、_day设置为公有成员

调用写法:operator==(d1,d2)d1 == d2

需要注意d1 == d2时,第一个形参就对应左操作数,第二个形参就对应右操作数

后者本质上就是前者。

编译器会在编译时看有没有判断自定义类型成员的相等的运算符,如果有就会直接指执行指令:call Date::operator==(函数地址)

  • 写在类中
class Date
{
public:
    bool operator==(const Date& d)
    {
        return _year == d._year
        && _month == d._month
        && _day == d._day;
    }
};

调用写法:d1.operator==(d2)d1 == d2

需要注意的时,在类中只有一个形参是因为,还隐藏了一个形参:this指针。左操作数是this,指向调用函数的对象

我们常用的调用写法为:d1 == d2

赋值运算符重载

Date& operator=(const Date& d)
{
    if(this != &d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    return *this;
}

//调用:d1 = d2

这里是以Date类的赋值运算符重载的实现为例。不同类的赋值运算符重载是不同的

注意点:

  • 可以选择传值传参,这里并不会引发无穷递归。但显然传引用效率更高
  • 返回值为Date&是为了实现连续赋值,即d1=d2=d3
  • if语句为了防止d1=d1自己给自己赋值的情况出现(这对于深拷贝来说,很有用)

这里解释一下为什么拷贝构造函数传值传参会引发无穷递归,而这里就不会:

默认赋值运算符重载

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

对于内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值

假如Stack类没有显式实现赋值运算符重载:

image-20230318154253195

image-20230318154315031

赋值运算符写的位置

赋值运算符只能重载成类的成员函数,而不能重载成全局函数

因为:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数

总结:类中六个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

image-20230318154455362

这里最主要的是前4个:默认构造函数、默认析构函数、默认拷贝构造函数、默认赋值运算符重载

  • 对于默认构造函数和析构函数
    1. 内置类型成员不做处理
    2. 自定义类型成员会调用这个成员的构造或析构
  • 对于默认拷贝构造函数和赋值运算符重载
    1. 内置类型成员完成浅拷贝
    2. 自定义类型成员会调用这个成员的拷贝构造或赋值重载

一旦某个类中的成员变量涉及了资源管理(空间开辟),那么该类的析构函数、拷贝构造函数、赋值运算符重载,都必须自己去实现。析构函数是为了释放空间,其余两个是为了防止出现浅拷贝的问题

const成员

将const修饰的成员函数称为const成员函数

说是说const修饰成员函数,实际上修饰的是该成员函数隐含的this指针指向的对象。此举表明:在该成员函数中不能对类的任何成员进行修改

即:const 类名* this

原因分析

为什么会有这个呢?

class A
{
public:
	void Print()
    {
        cout<<_a<<endl;
    }
private:
    int _a=10;
}

int main()
{
    const A aa;
    aa.Print();		//error
    return 0;
}

这里为什么会报错呢?aa是一个常对象(即它的任何成员都不能被改变),当他去调用Print()时,相当于将aa传给了Print()函数中的隐式参数:*this,而*this并没有被const修饰,这里就产生了权限的放大:原本的只读,现在扩大为了可读可写,就发生了报错

那么我们修改的措施也很简单,就是将*this也变成只读的。但这里有个困难就是,*this这个参数是隐式的,我们无法直接在其前面直接加const,因此有了const成员函数

修改如下:

class A
{
public:
	void Print() const	//修改
    {
        cout<<_a<<endl;
    }
private:
    int _a=10;
}

int main()
{
    const A aa;
    aa.Print();		
    return 0;
}

常用情景

class A
{
public:
	void Print() const
    {
        cout<<_a<<endl;
    }
private:
    int _a=10;
}

void Func(const A& x)
{
    x.Print();
}

int main()
{
 	A aa;
    aa.Print();		
    
    Func(aa);
    
    return 0;
}

对于aa.Print()来说是普通对象调用,是一个权限缩小,所以没有问题

对于x.Print()来说是const对象调用,倘若Print()函数不加const,就出问题了

上述x的调用很常见

对于一个成员函数,我们不想在函数内部改变对象的成员变量,所以我们通常会在形参处,给传递来的对象加上一个const,使对象变成常对象。

而当常对象调用其他成员函数时,被调用成员函数就必须用const修饰

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

这是类中六个默认成员函数的另外两个

这两个默认成员函数一般不用重新定义 ,编译器默认会生成

class Date
{ 
public :
    Date* operator&()	
    {
        return this ;
    }
    const Date* operator&()const
    {
        return this ;
    }
private :
    int _year ; 
    int _month ; 
    int _day ; 
};

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

这里的两个成员函数是构成函数重载的。

上面的函数的参数是Date* this;下面的函数的参数是const Date* this

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值