类与对象

1. 类简介

在 C++ 中,我们通过定义一个(class)来定义自己的数据结构
一个类定义了一个类型,以及与其相关联的一组操作
类机制是 C++ 最重要的特性之一。
为了使用类,我们需要了解三件事:

  • 类名是什么?
  • 它在哪里定义的?
  • 它支持什么操作?
1.1 类与对象的初步认识

:某类事物的抽象(具有相同的特征,例如一个学生类:有姓名,有性别,有年龄…)
对象:一个具体的事物(具体的一个学生)

1.2 类的引入

我们说 :
C 语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++ 是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
于是,在 C++ 语言中,引入了,类中可以定义变量,也可以定义函数。这也是C++ 封装 特性的体现!!!

1.3 类的定义

定义一个类的关键字是 structclass
在 C++ 中,struct 可以当成结构体使用,另外,还可以用来定义类

class class_name
{
	成员变量;
	成员函数;
}; // 一定要注意后面的分号

类的两种定义形式:
1.声明和定义全部放在类体内
2. 声明放在 .h 文件中,类的定义放在 .cpp 中
对第 2 种方法,每个函数名前面需要加类名和作用域运算符(::)

通常我们在头文件 class_name.h 中定义这个类;
支持什么操作全看我们在类中实现了什么。
类定义了类对象可以执行的所有动作。

1.4 类的访问限定符及封装

C++ 实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部用户使用
访问限定符说明:

访问限定符说明
public公有,在类外可以直接被访问
protected保护,在类外不能被直接访问
private私有, 在类外不能被直接访问

struct 是从 C 语言中引入的,并且为了兼容 C 语言,struct 和 class 定义一个类是由区别的:

structclass
struct 定义的类成员默认访问方式是 publicclass 定义的类成员默认访问方式是 privavte

面向对象的三大特性:封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象属性和实现细节,仅对外公开接口来和对象进行交互。
剩下两个特性这里没体现到!!!后面再说。

1.5 类的作用域

类指定了一个新的作用域,类的所有成员都在类的作用域中。
在类外定义成员,需要使用 作用域运算符(::) 指明成员属于哪个类域。

class Person
{
public:
	void printPersonInfo();
private:
	char _name[20];
	char _gender[4];
	int _age;
};
Person::printPersonInfo()
{
	std::cout << _name << " " << _gender << " " << _age << std::endl;
}
1.6 初识成员函数

成员函数(member function)是定义为类的一部分的函数,有时也被称为方法(method)。
我们通常以一个类对象的名义来调用成员函数。
类对象名.相应的成员函数
比如现在有一个 Student 类:

// 定义在头文件 Student.h 中

class Student
{
public:
	void setStudentInfo(const char * name, const char * gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}
	void printStudentInfo()
	{
		std::cout << _name << " " << _gender << " " << _age << std::endl;
	}
private:
	char _name[20];
	char _gender[4];
	int _age;
};

我们可以这样来调用成员函数:

#include "Student.h"

int main(void)
{
    Student stu1;

    stu1.setStudentInfo("张三", "男", 20);
    stu1.printStudentInfo();

    return 0;
}

编译(编译环境:CentOS 7)运行:
student 类调用成员函数
我们成功调用打印出来了结果。
点运算符(.)只能用于类类型(class type)的对象。其左侧运算对象必须是一个类类型的对象,右侧运算对象必须是该类型的一个成员名,运算结果为右侧运算对象指定的成员。
当用点运算访问一个成员函数时,通常我们是想调用该函数。我们使用调用运算符(())来调用一个函数。调用运算符是一对圆括号,里面放置 实参 (argument)列表(可能为空)。

1. 7 类的实例化

用类类型创建对象的过程,称为类的实例化
1.类只是一个模型,限定了类有哪些成员,定义出一个类并没有分配实际内存空间来存储它(不要钻牛角,程序运行起来。代码肯定加载到内存并放在代码段,这里说的是在栈上分配空间)。
2.一个类可以实例化出很多对象,实例化出的对象占用实际的物理空间,存储类成员变量

1.8 类对象模型

如何计算类对象的大小

// test.h

#include <iostream>

class test1 
{
public:
    void f(){}
private:
    int _a;
};

class test2
{
public:
    void f2(){}
};

class test3
{};
// test.cpp

#include "test.h"

int main(void)
{
    std::cout << "sizeof(test1) = " << sizeof(test1) << std::endl;
    std::cout << "sizeof(test2) = " << sizeof(test2) << std::endl;
    std::cout << "sizeof(test3) = " << sizeof(test3) << std::endl;

    return 0;
}

编译(编译环境: CentOS 7)运行之:
计算类对象的大小
结果貌似出乎意料,其实是:
一个类的大小,实际就是该类中“成员变量”之和,当然也要进行内存对齐
注意空类,g++ 编译器给了空类一个字节来唯一标识这个类,其实就是个占位的作用,你想想,如果空类实例化了个对象,实例化就要开辟内存,要是没有大小,就不叫初始化了,g++ 编译器对空类的大小处理是1个字节,我在 vs 2013 上也测试了,也是 1 个字节。

  • 结构体内存对齐规则
  • 第一个成员在与结构体偏移量为 0 的地址处
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍地址处
    这里的对齐数是:编译器默认的对齐数和该数据类型实际大小(例如 32 位系统 int 类型占 4 个字节)中的较小值
    vs 默认对齐数是8,g++/gcc 默认对齐数是 4
  • 结构体的总大小为:最大对齐数的整数倍
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最对齐数(含嵌套结构体的对齐数)的整数倍。
2. this 指针
2.1 引入 this

让我们再次观察对成员函数的调用:

#include <iostream>
#include <cstring>

class Student
{
public:
	void setStudentInfo(const char * name, const char * gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}
	void printStudentInfo()
	{
		std::cout << _name << " " << _gender << " " << _age << std::endl;
	}
private:
	char _name[20];
	char _gender[4];
	int _age;
};

int main(void)
{
	Student stu1;
	stu1.setStudentInfo("张三", "男", 18);
	stu1.printStudentInfo();
	
	return 0;
}

在这里,我们使用了点运算符来访问 stu1 对象的 setStudentInfo 和 printStudentInfo 成员,然后调用它。
当我们调用成员函数时,实际上是在替某个对象调用它C++编译器给每个“成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象,在函数体内,所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
所以对上面的例子,对象 stu1 调用 setStudentInfo 和 printStudentInfo 相当于:

stu1.setStudentInfo(/* &stu1, */ "张三", "男", 18);
stu1.printStudentInfo(/* &stu1 */);
void setStudentInfo( /* Student * const this, */ const char * name, const char * gender, int age)
	{
		strcpy(this->_name, name);
		strcpy(this->_gender, gender);
		this->_age = age;
	}
	void printStudentInfo(/* Student * const this */)
	{
		std::cout << this->_name << " " << this->_gender << " " << this->_age << std::endl;
	}
2.2 this 指针特性
  • this 指针的类型: 类类型 * const
  • 只能在成员函数内部使用
  • this 指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给 this 形参(编译器帮我们做了,不需要我们手动取地址传参)。所以对象中不存储 this 指针
  • this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。
2.3 this 指针存在哪?

我们分别在Linux(CentOS 7) 的 g++ 和 Windows 的 VS2013 上测试一下

Linux(CentOS 7) g++ 测试:
g++测试
我们看到,g++ 是采用参数压栈的方式保存 this 指针

VS 2013 测试:
vs 2013 编译器
我们看到,vs 2013 环境下,this 指针存放在 ecx 寄存器上。

我们来看一个例子:

// 下面的程序能编译通过吗?
// 下面的程序会崩溃吗?
// 在哪里崩溃?为什么崩溃?
#include <iostream>

class A
{
public:
    void printA()
    {
        std::cout << _a << std::endl;
    }

    void show()
    {
        std::cout << "show()" << std::endl;
    }
private:
    int _a;
};

int main(void)
{
    A * p = nullptr;
    p->printA();
    p->show();

    return 0;
}

我们通过C++11标准编译(编译环境: CentOS 7 g++)一下:
编译
编译通过,没有问题
之后我们运行之:
运行
发现 段错误(Segmentation fault (core dumped))
通常这种情况都是非法访问了内存
为什么呢?
其实还是 this 指针

这行代码屏蔽掉,再编译运行之

编译运行
我们发现,编译通过,程序运行没有崩溃,输出 show()

为什么呢?理论上,
p 是一个 A 类的空指针,第一种情况报错,段错误是意料之中的事,我们想的是,它用空指针去调用类的成员函数,不出错才怪呢
其实啊,并不是这样的接下来我们细说:
由于 p 去调用成员函数 printA ,要打印成员变量 _a 的值, 那必然有 this->_a 的操作,这肯定就是非法访问内存了啊,所以程序崩溃在这里!
第二次,我们把
p->printA();
这行代码屏蔽掉,我们发现,不仅编译没有问题,运行也没有问题,那这又是为什呢?
其实很好想,在成员函数 show() 里面,我们并没有要对 this 解引用,所以成功打印 show 一点问题都没有!!!

相信经过这个例子,你对 this 的理解会更加深刻!

3. 类的默认成员函数

啥叫默认,就是你不给出,编译器会自动为这个类生成,兄弟,别想太多啊,编译器还没那么聪明,不会自动按照你的想法写代码的!
默认有 6 个成员函数:
构造函数、拷贝构造函数、析构函数、赋值运算符重载、取地址操作符重载、 const 修饰的取地址操作符重载

3.1 构造函数
3.1.1 概念(任务)

初始化类对象的数据成员,无论何时只要类的对象被创建,就会调用构造函数。

3.1.2 构造函数特性
  • 函数名与类名相同
  • 无返回值
  • 构造函数也有一个参数列表(可能是空的)和一个函数体(也可能是空的)
    构造函数可以重载
  • 构造函数在对象创建时由编译器自动调用
    用户自己不能调用,一调用就出错
// Date.h
#pragma once
#include <iostream>

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

    void show()
    {
        std::cout << _year << "-" << _month << "-"  << _day << std::endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
// test.cpp

#include "Date.h"

int main(void)
{
    Date d1;
    d1.Date(2018, 11,19);

    return 0;
}

我们编译(编译环境:CentOS 7 g++):
用户调用类的构造函数
我们发现,用户调用类的构造函数不能通过编译。
我们再测一下:

#include "Date.h"

int main(void)
{
    Date d1; // 不传递参数调用参构造函数
    Date d2(2018, 11, 19); // 传递参数 

    d1.show();
    d2.show();
    return 0;
}

编译(编译环境:CentOS 7 g++):
调用构造函数
编译通过,并且成功打印出期望的日期
构造函数是一个特殊的成员函数,但是需要注意,构造函数的主要任务是初始化对象,而不是开辟空间创建对象

  • 如果类中没有显示定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成
  • 无参的构造函数和全缺省的构造函数都被称为默认构造函数,并且默认构造函数只能有一个
    这是为什么呢,很好想,一个无参的,一个全缺省的,好像调用哪一个都可以吧,那编译器傻乎乎的,他哪知道掉哪一个?于是编译器把这个问题抛给用户自己处理,自然就报错了。
  • 编译器生成的默认构造函数,对一个类对象的成员变量进行初始化操作,对内置不初始化,对自定义类型(比如另一个类类型)按其方法初始化
// 比如现在有两个类 Date 和 Time
// Date 没有显示定义构造函数
// Time 显示定义自己的构造函数
// 在 Date 类中,有一个 Time 类型的成员变量
#include <iostream>

class Time
{
public:
    Time(int hour = 12, int minute = 0, int second = 0) 
    {
        _hour = hour;
        _minute = minute;
        _second = second;
    }

    void showTime()
    {
        std::cout << _hour << ":" << _minute << ":" << _second;
        std::cout << std::endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
public:

    void show()
    {
        std::cout << _year << "-" << _month << "-"  << _day << " ";
        _t.showTime();
    }
private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

int main(void)
{
    Date d1; // 不传递参数调用参构造函数

    d1.show();
    return 0;
}

我们编译(编译环境:CentOS 7 g++)运行之:
编译运行
果然和我们预期的一样啊。
我们再来看看汇编代码:
汇编
在创建变量 d1 时,没有对它的内置类型成员变量(_yeay、_month、_day)初始化,而对Time类类型的成员变量 _t 调用它对应的构造函数。

3.1.3 某些类不能依赖于合成的默认构造函数
  • 编译器只有发现类不包含任何构造函数,才会替我们生成一个默认的构造函数
  • 合成的默认构造函数可能执行错误的操作
    比如指针,默认初始化的值是未定义的
  • 有的时候编译器不能为某些类合成默认的构造函数
    例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
    对于这样的类,我们必须自定义默认构造函数。
3.1.4 构造函数初始化列表

构造函数初始化列表负责为新创建的对象的一个或几个数据成员进行初始化
到此,我们就得说一下初始化赋值的区别了
初始化就是创建一个变量的同时给他赋一个初始值
而赋值是,改变一个已存在变量的值
初始化只能初始化一次,但可以多次赋值。
这里的构造函数初始化列表就是初始化,而不是赋值
初始化列表与别的函数有很大区别,我们先看个实例:

#pragma once
#include <iostream>

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

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

初始化列表:以一个冒号开始,接着是一个以逗号分隔的初始化列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式
注意

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 如果类中包含引用成员变量const 成员变量没有默认构造函数的类类型成员,必须放在初始化列表位置进行初始化。
    为什么?很好想,引用在定义时必须初始化,并且是绑定关系,改变不了,const 修饰的变量也一样,至于没有默认构造函数的类类型成员,你不在这初始化,谁给你初始化?
  • 尽量使用初始化列表初始化
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序。与成员变量在初始化列表的先后次序无关。
3.2 析构函数
3.2.1 概念(功能)

与构造函数相反,析构函数不能完成对象的销毁,它只是在对象销毁之前做一些清理工作
局部对象(在栈上的对象)的销毁工作是由编译器完成的,其实所有在函数内定义的对象都是在栈上,但是这个对象可能持有堆上动态申请的内存,在销毁之前,编译器调用析构函数来释放堆上的空间(做一些清理工作)。

3.2.2 析构函数特性
  • 析构函数函数名是在类名前面加上字符 ‘~’
  • 无参数无返回值
  • 一个类只有一个析构函数,若未显示定义,系统会生成默认的构造函数
  • 对象生命周期结束时,C++编译器自动调用析构函数。
  • 编译器生成的默认析构函数,对自定义类型(类类型)成员变量调用该类的析构函数。

例如,我们熟悉的顺序表:

// seqlist.h
#pragma  once
#include <iostream>
#include <cstdlib>
#include <cassert>

typedef  int DataType;

class seqlist
{
public:
    seqlist(int capacity = 10);
    ~seqlist();

private:
    DataType * _array;
    int _size;
    int _capacity;
};
// seqlist.cpp

#include "seqlist.h"

seqlist::seqlist(int capacity)
{

    std::cout << "seqlist::seqlist(int)" << std::endl;
     _array = (DataType *)malloc(capacity * sizeof(DataType));
    if(_array == nullptr)
    {
        assert(0);
    }
    _capacity = capacity;
    _size = 0;
}

seqlist::~seqlist()
{
    std::cout << "seqlist::~seqlist()" << std::endl;
    if(_array != nullptr)
    {
        free(_array);
        _array = nullptr;
        _capacity = 0;
        _size = 0;
    }
}
// seqlist_test.cpp

#include "seqlist.h"

int main(void)
{
    seqlist s;

    return 0;
}

我们编译(编译环境:CentOS 7 )运行之:
顺序表编译运行
操作很简单,只是定义了一个 seqlist 类型的变量 s
它在定义时调用构造函数,销毁之前调用析构函数。

我们再看一个例子,定义两个类:String Person
String 类就是一个字符串的管理,包含构造函数和析构函数
Person 是对一个人的信息的封装,包含,姓名(String类型),性别(String类型),年龄,Person 类没有析构函数

// String.h
#pragma once
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cassert>

class String
{
public:
    String(const char* str) 
    {
        std::cout << "String::String(char * )" << std::endl;
        _str = (char * )malloc(strlen(str) + 1);
        if(_str == NULL)
        {
            assert(0);
        }
        strcpy(_str, str);
    }
    ~String() 
    {
        std::cout << "String::~String()" << std::endl;
        if(_str != nullptr)
        {
            free(_str);
            _str = nullptr;
        }
    }

private:
    char* _str;
};
// Person.h
#pragma once

#include "String.h"

class Person
{
public:
    Person(const char * name, const char * gender, int age)
        :_name(name)
         ,_gender(gender)
         ,_age(age)
    {}

private:
    String _name;
    String _gender;
    int _age;
};
// test.cpp
#include "Person.h"

int main(void)
{
    Person p1("张三", "男", 18);

    return 0;
}

我们编译(编译环境:CentOS 7 g++)运行之:
编译运行
我们并没有定义 person 类的析构函数,但是结果确实调用了 String 类的析构函数。

3.3 拷贝构造函数
3.3.1 概念(功能)

由编译器调用来完成一些基于同一类的其他对象的构建及初始化,用已存在的类类型对象创建新对象时由编译器自动调用。

3.3.2 拷贝构造函数特性
  • 特殊的成员函数,是构造函数的一个重载形式
  • 只有单个形参
    该形参是对本类类型对象的引用(一般用 const 修饰)
    如果不是引用传参(传值)会引发无穷递归(形参也是对象嘛,你传值的话我就得创建新对象接收啊,还是以拷贝构造的方式进行创建新对象)

这个可能有点不好想,特别是学过C语言的同学,形参就形参么,传过来就创建了啊,怎么会无穷递归呢,存在这个想法的同学是对 对象 的理解有点偏差,在创建对象时,必定要调用构造函数,那调哪个构造函数,自然是拷贝构造函数了,为什么呢?因为拷贝构造函数的概念,它就是用一个已经存在的对象创建一个新对象,所以除了本身的 this 指针 不用穿,还得传一个参数,但是这个形参偏偏是以传值的形式传过来的,那你传过来了,我就要创建临时变量啊,然后又调用拷贝构造函数,…,就这样,无穷递归喽!!!
可能我说的还是不清楚,我们来看个例子:

// Date.h
#pragma once
#include <iostream>

class Date
{
public:
    Date(int year = 1977, int month = 1, int day = 1)
        :_year(year)
         ,_month(month)
         ,_day(day)
    {
        std::cout << "Date::Date(int, int , int)" << std::endl;
    }

    Date(const Date & d)
    {
        std::cout << "Date::Date(const Date&)" << std::endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    void show()
    {
        std::cout << _year << "-" << _month << "-"  << _day << " ";
        std::cout << std::endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
// test.cpp
#include "Date.h"

int main(void)
{
    Date d1(2018, 11, 19);
    Date d2(d1);

    d1.show();
    d2.show();

    return 0;
}

我们编译运行之:
拷贝构造函数
我们看到,创建 Date 类对象 d2 时调用了拷贝构造函数
如果我们的拷贝构造函数是这样:

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

我们来分析一波:
拷贝构造函数传值
就是这幅图了,无穷递归!!!

  • 如果没有显式定义,系统生成默认的拷贝构造函数
    默认的拷贝构造函数对象按内存字节序完成拷贝,这种拷贝我们叫做浅拷贝或者值拷贝
3.3.3 某些类不能依赖于合成版本

当类需要分配类对象之外的资源时,合成的版本常常会失效。
建议:很多需要动态内存的类,使用 vector 对象或者 string 对象能避免分配和释放内存带来的复杂性。
简单的说,没有管理对象之外的资源的类,可以使用合成的默认构造函数,但是管理了其他资源的话,就需要自己显式定义拷贝构造函数

3.4 赋值运算符重载
3.4.1 运算符重载

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

  • 不能通过连接其他符号来创建新的操作符,比如 operator$
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变
  • 操作符有一个默认的形参 this ,默认为第一个形参
    所以形参看起来少了一个
  • 有五个运算符不能重载
    .* 不知道为啥,这个运算符我没见过
    :: 作用域运算符
    sizeof 求大小的
    ? : 三目运算符
    . 访问类成员的

对 Date 类,我们试着重载几个运算符

// Date.h
#pragma once
#include <iostream>

class Date
{
public:
    Date(int year = 1977, 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;
    }

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

    bool operator!=(const Date &d)
    {
        if(*this == d)
        {
            return false;
        }
        return true;
    }

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

    void show()
    {
        std::cout << _year << "-" << _month << "-"  << _day << " ";
        std::cout << std::endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
#include "Date.h"

int main(void)
{
    Date d1(2018, 11, 19);
    Date d2(2018, 11, 20);

    if(d1 == d2)
    {
        std::cout << "d1 == d2" << std::endl;
    }
    else if(d1 < d2)
    {
        std::cout << "d1 < d2" << std::endl;
    }
    else
    {
        std::cout << "d1 > d2" << std::endl;
    }

    return 0;
}

编译(编译环境:CentOS 7 g++)运行之:
运算符重载
我们重载了 ==、!=、< 这三个运算符。

3.4.2 赋值运算符重载

赋值运算符主要有四点:

  • 参数类型
  • 返回值
  • 检测是否是自己给自己赋值
  • 返回 *this
    一个类如果没有显式的重载赋值运算符,编译器也会生成一个,完成对象按字节序的浅拷贝

这里和构造函数一样,如果管理对象之外的资源了,就需要显式定义,如果没有管理对象之外的资源,用默认的也可以。
我们对 Date 类重载赋值运算符

// Date.h
#pragma once
#include <iostream>

class Date
{
public:
    Date(int year = 1977, 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;
    }

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

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

    bool operator!=(const Date &d)
    {
        if(*this == d)
        {
            return false;
        }
        return true;
    }

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

    void show()
    {
        std::cout << _year << "-" << _month << "-"  << _day << " ";
        std::cout << std::endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
// test.cpp
#include "Date.h"

int main(void)
{
    Date d1(2018, 11, 19);
    Date d2;

    d1.show();
    d2.show();
    d2 = d1;
    d2.show();

    return 0;
}

编译运行之:
重载赋值运算符

3.4 const 成员

其实我们已经接触了 const 关键字,比如它修饰一个变量,这个变量具有常属性,不能被修改并且具有宏的特性。

3.4.1 const 修饰类的成员函数

将 const 修饰的类成员函数称之为 const 成员函数,const 修饰类成员函数,实际上修饰的是该成员函数的 this 指针表明在该成员函数中不能修改类的成员变量
如果想要修改,需要在类的成员变量前加上关键字 mutable
我们来看 Date 类

// Date.h

#pragma once
#include <iostream>

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

    void show()
    {
        std::cout << "void show(): " << std::endl;
        _month += 1;
        std::cout << _year << "-" << _month  << "-"  << _day << " ";
        std::cout << std::endl;
    }
    void show()const
    {
        std::cout << "void show()const: " << std::endl;
        _day += 1;
        std::cout << _year << "-" << _month << "-"  << _day << " ";
        std::cout << std::endl;
    }
private:
    int _year;
    int _month;
    mutable int _day;
};
// test.cpp

#include "Date.h"

int main(void)
{
    Date d1(2018, 11, 19);
    const Date d2;

    d1.show();
    d2.show();

    return 0;
}

编译(编译环境)运行之:
const 修饰类的成员函数
对没加 const 的成员函数 show ,本来 _month 的值是 11,我们加1之后在打印,没有一点问题;对加 const 修饰的成员函数 show 来说,本来 _day 不能被修改,但是加上关键字 mutable ,它也可以修改了

3.4.2 const 修饰与非 const 修饰的区别
  • const 对象只能调用 const 成员函数
    如果调用普通成员函数,可能会修改 const 对象的内容
  • 非 const 对象可以调用 const 成语函数
  • const 成员函数内不能调用非 const 成员函数
  • 非 const 成员函数内可以调用 const 成员函数
3.5 取地址及 const 取地址操作符重载

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

    Date* operator&()
    {
        return this;
    }

    const Date* operator&()const
    {
        return this;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值