C++初阶教程之类和对象

目录
  • 类和对象<上>
    • 1. 类的定义
    • 2. 类的封装
      • 2.1 访问限定修饰符
      • 2.2 类的封装
    • 3. 类的使用
      • 3.1 类的作用域
      • 3.2 类的实例化
    • 4. 类对象的存储
    • 5. this 指针
      • 5.1 this 指针的定义
      • 5.2 this 指针的特性
  • 类和对象<中>
    • 1. 构造函数
      • 1.2 构造函数的定义
      • 2.2 构造函数的特性
    • 2. 析构函数
      • 2.1 析构函数的定义
    • 3. 拷贝构函数
      • 3.1 拷贝构造函数的定义
      • 3.2 拷贝构造函数的特性
    • 4. 运算符重载
      • 4.1 运算符重载的定义
      • 4.2 运算符重载的特性
      • 4.3 赋值运算符重载
    • 5. 日期类的实现
      • 5.1 日期类的定义
      • 5.2 日期类的接口实现
    • 6. const 类
      • 6.1 const 类的成员函数
      • 6.2 取地址操作符重载
  • 总结

类和对象<上>

面向对象

一直以来都是面向过程编程比如C语言,直到七十年代面向过程编程在开发大型程序时表现出不足,计算机界提出了面向对象思想(Object Oriented Programming),其中核心概念是类和对象,面向对象三大特性是封装、继承和多态。

面向过程和面向对象只是计算机编程中两种侧重点不同的思想,面向过程算是一种最为实际的思考方式,其中重要的是模块化的思想,面向过程更注重过程、动作或者说事件的步骤。就算是面向对象也是含有面向过程的思想,对比面向过程,面向对象的方法主要是把事物给对象化,认为事物都可以转化为一系列对象和它们之间的关系,更符合人对事物的认知方式。

用外卖系统举例,面向过程思想就会将订餐、取餐、送餐、接单等等步骤模块化再一个一个实现,体现到程序中就是一个个的函数。面向对象思想会将整个流程归结为对象和对象间的关系,也就是商家、骑手和用户三者和他们的关系,体现到程序中就是类的设计。

面向对象是一个广泛而深刻的思想,不可能一时半会就理解透彻,需要再学习和工作中慢慢体会。

C++不像Java是纯面向对象语言,C++基于面向对象但又兼容C所以也可以面向过程。

1. 类的定义

//C
struct Student {
	char name[20];
	int age;
	int id;
};
struct Student s;
strcpy(s.name, "yyo");
s.age = 18;
s.id = 11;
//C++
struct Student {
	//成员变量
	char _name[20];
	int _age;
	int _id;
	//成员方法
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}
	void Print() {
		cout << _name << endl;
		cout << _age << endl;
		cout << _id << endl;
	}
};
s1.Init("yyo", 19, 1);
s1.Print();
s2.Init("yyx", 18, 2);
s2.Print();

从上述代码可以看出,在C语言中,结构体中只能定义变量,就相当于是个多个变量的集合,而且操作成员变量的方式相较于C++更加繁琐且容易出现错误。

由于C++兼容C,故C++中定义类有两个关键字分别是struct和class,结构体在C++中也升级成了类,类名可以直接作类型使用。类与结构体不同的地方在于,类中不仅可以定义变量,还可以定义方法或称函数。

C++中更多用class定义类,用class定义的类和struct定义的类在访问限定权限上稍有不同。

class className {
    // ...
};

class是类的关键字,className是类的名字,{}中的内容是类体。类中的元素即变量和函数都叫类的成员,其中类的成员变量称为类的属性或是类的数据,类的函数成为类的方法或成员函数。

2. 类的封装

面向对象编程讲究个“封装”二字,封装体现在两方面,一是将数据和方法都放到类中封装起来,二是给成员增加访问权限的限制。

2.1 访问限定修饰符

C++共有三个访问限定符,分别为公有public,保护protect,私有private。

  • public修饰的成员可以在类外直接访问,private和protect修饰的成员在类外不能直接访问。
  • class类中成员默认访问权限为private,struct类中默认为public。
  • 从访问限定符出现的位置到下一个访问限定符出现的位置之间都是该访问限定符的作用域。

和public相比,private和protect在这里是类似的,它二者具体区别会在之后的继承中谈到。封装的意义就在于规范成员的访问权限,放开struct类的权限是因为要兼容C。

封装的意义就在于规范成员的访问权限,更好的管理类的成员,一般建议是将成员的访问权限标清楚,不要用类的默认规则。

class Student {
private:
	//成员变量
	char _name[20];
	int _age;
	int _id;
public:
	//成员方法
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}
	void Print() {
		cout << _name << endl;
		cout << _age << endl;
		cout << _id << endl;
	}
};

注意,访问限定修饰符只在编译阶段起作用,之后不会对变量和函数造成任何影响。

2.2 类的封装

面向对象三大特性是封装、继承和多态。类和对象的学习阶段,只强调类和对象的封装机制。封装的定义是:将数据和操作数据的方法放到类中有机结合,对外隐藏对象的属性和实现细节,仅公开交互的接口。

封装的本质是一种管理机制。对比C语言版的数据结构实现可以看到,没有封装并将结构的成员全部暴露出来是危险的且容易出错,但调用结构提供的接口却不易出错。一般不允许轻易的操作在函数外操作和改变结构,这便是封装的好处。面向过程只有针对函数的封装,而面向对象编程提出了更加全面的封装机制,使得代码更加安全且易于操作。

class Stack {
public:
	void Init();
	void Push(STDataType x);
	void Pop();
	STDataType Top();
	int Size();
	bool Empty();
	void Destroy();
private:
	STDataType* _a;
	int _top;
	int _capacity;
};

3. 类的使用

3.1 类的作用域

类定义了一个新的作用域,类中所有成员都在类的作用域中。

  • 若直接在类内定义函数体,编译器默认将类内定义的函数当作内联函数处理,在满足内联函数的要求的情况下。
  • 在类外定义成员函数时,需要使用域作用限定符::指明该成员归属的类域。如图所示:

一般情况下,更多是采用像数据结构时期那样,声明和定义分离的方式。

3.2 类的实例化

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

  1. 类只是一个“模型”,限定了类的性质,但并没有为其分配空间。
  2. 由类可以实例化得多个对象,对象在内存中占据实际的空间,用于存储类成员变量。

类和对象的关系,就与类型和变量的关系一样,可以理解为图纸和房子的关系。

4. 类对象的存储

既然类中既有成员变量又有成员函数,那么一个类的对象中包含了什么?类对象如何存储?

class Stack {
public:
	void Init();
	void Push(int x);
	// ...
private:
	int* _a;
	int _top;
	int _capacity;
};
Stack st;
cout << sizeof(Stack) << endl;
cout << sizeof(st) << endl;

如果类成员函数也存放在对象中,实例化多个对象时,各个对象的成员变量相互独立,但成员函数是相同的,相同的代码存储多份浪费空间。因此,C++对象中仅存储类变量,成员函数存放在公共代码段

类的大小就是该类中成员变量之和,要求内存对齐,和结构体一样。注意,空类的大小为1个字节,用来标识这个对象的存在。

空类的大小若为0,相当于内存中没有为该类所创对象分配空间,等价于对象不存在,所以是不可能的。

接下来都使用栈和日期类来理解类和对象中的知识。

5. this 指针

class Date {
public:
	void Init(int year, int month, int day) {
		//year = year;//Err
		//1.
        _year = year;
        //2.
        Date::month = month;
        //3.
		this->day = day;
	}
private:
	int _year;
	int month;
	int day;
};

如果成员变量和形参重名的话,在Init函数中赋值就会优先使用形参导致成员变量没有被初始化,这种问题有三种解决方案:

  1. 在成员变量名前加_,以区分成员和形参。
  2. 使用域访问修饰符::,指定前面的变量是成员变量。
  3. 使用 this 指针。

5.1 this 指针的定义

d1._year;的意义是告诉编译器到d1这个对象中查找变量_year的地址。但函数并不存放在类对象中,那d1.Print();的意义是什么?

如图所示,d1,d2两个对象调用存储在公共代码区的Print函数,函数体中并没有区分不同对象,如何做到区分不同对象的调用呢?

C++中通过引入 this 指针解决该问题,C++编译器给每个非静态的成员函数增加了一个隐藏的参数叫 this 指针。this 指针指向当前调用对象,函数体中所有对成员变量的操作都通过该指针访问,但这些操作由编译器自动完成,不需要主动传递。

如图所示,在传参时隐藏地传入了对象的指针,形参列表中也对应隐藏增加了对象指针,函数体中的成员变量前也隐藏了 this 指针。

5.2 this 指针的特性

this是C++的一个关键字,代表当前对象的指针。this 指针是成员函数第一个隐含的指针形参,一般由寄存器传递不需要主动传参。

  1. 调用成员函数时,不可以显式传入 this 指针,成员函数参数列表也不可显示声明 this 指针。
  2. 但成员函数中可以显式使用 this 指针。
  3. this 的类型为classType* const,加const是为了防止 this 指针被改变。
  4. this 指针本质上是成员函数的形参,函数被调用时对象地址传入该指针,所以 this 指针是形参存储在函数栈帧中,对象中不存储this指针。

Example 1和2哪个会出现问题,出什么问题?

class A {
public:
	void Printa() {
		cout <<  _a << endl;
	}
	void Show() {
		cout << "Show()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* a = nullptr;
	//1.
	a->Show();
	//2.
	a->Printa();
	return 0;
}

函数没有存储在对象中,所以调用函数并不会访问空指针a,仅是空指针作参数传入成员函数而已。二者没有程序语法错误,所以编译一定通过。

调用Show()函数没有访问对象中的内容,不存在访问空指针的问题。调用Print()函数需到a指针所指对象中访问成员_a,所以访问空指针程序崩溃。

类和对象<中>

默认成员函数

一个对象都要要对其进行初始化,释放空间,拷贝复制等等操作,像栈结构不初始化直接压栈就会报错。由于这些操作经常使用或是必不可少,在设计之初就被放到类中作为默认生成的成员函数使用,解决了C语言的一些不足之处。

C++在设计类的默认成员函数的机制较为复杂,一个类有6个默认的成员函数,分别为构造函数、析构函数、拷贝构造函数、赋值运算符重载、T* operator&()const T* operator&()const。他们都是特殊的成员函数,这些特殊函数不能被当作常规函数调用。

默认的意思是我们不写编译器也会自动生成一份在类里,如果我们写了编译器就不生成了。自动生成默认函数有的时候功能不够全面,还是得自己写。

1. 构造函数

1.2 构造函数的定义

构造函数和析构函数分别是完成初始化和清理资源的工作。构造函数就相当于数据结构时期我们写的初始化Init函数。

构造函数是一个特殊的函数,名字与类名相同,创建类对象时被编译器自动调用用于初始化每个成员变量,并且在对象的生命周期中只调用一次。

2.2 构造函数的特性

构造函数虽然叫构造函数,但构造函数的工作并不是开辟空间创建对象,而初始化对象中的成员变量。

  • 函数名和类名相同,且无返回类型。
  • 对象实例化时由编译器自动调用其对应的构造函数。
  • 构造函数支持函数重载。
//调用无参的构造函数
Date d1;
Date d2(); //Err - 函数声明
//调用带参的构造函数
Date d2(2020,1,18);

注意,调用构造函数只能在对象实例化的时候,且调用无参的构造函数不能带括号,否则会当成函数声明。

  • 若类中没有显式定义构造函数,程序默认创建的构造函数是无参无返回类型的。一旦显式定义了编译器则不会生成。
  • 无参的构造函数、全缺省的构造函数和默认生成的构造函数都可以是默认构造函数(不传参也可以调用的构造函数),且防止冲突默认构造函数只能有一个。

默认构造函数初始化规则

从上图可以看出,默认生成的构造函数对内置类型的成员变量不进行有效初始化。其实,编译器默认生成的构造函数仅对自定义类型进行初始化,初始化的方式是在创建该自定义类型的成员变量后调用它的构造函数。倘若该自定义类型的类也是默认生成的构造函数,那结果自然也没有被有效初始化。

默认生成的构造函数对内置类型的成员变量不作处理,对自定义类型成员会调用它们的构造函数来初始化自定义类型成员变量。

一个类中最好要一个默认构造函数,因为当该类对象被当作其他类的成员时,系统只会调用默认的构造函数。

目前还只是了解掌握基本的用法,对构造函数在之后还会再谈。

2. 析构函数

析构函数同样是个特殊的函数,负责清理和销毁一些类中的资源。

2.1 析构函数的定义

与构造函数的功能相反,析构函数负责销毁和清理资源。但析构函数不是完成对象的销毁,对象是main函数栈帧中的局部变量,所以是随 main 函数栈帧创建和销毁的。析构函数会在对象销毁时自动调用,主要清理的是对象中创建的一些成员变量比如动态开辟的空间等。

2.2 析构函数的特性

  • 析构函数的名字是~加类名,同样是无参无返回类型,故不支持重载。
  • 一个类中有且仅有一个析构函数,同样若未显式定义,编译器自动生成默认的析构函数。
  • 对象生命周期结束时,系统自动调用析构函数完成清理工作。
  • 多个对象调用析构函数的顺序和创建对象的顺序是相反的,因为哪个对象先压栈哪个对象就后销毁。

调用对象后自动调用析构函数,这样的机制可以避免忘记释放空间以免内存泄漏的问题。不一定所有类都需要析构函数,但对于有些类如栈就很方便。

默认析构函数清理规则

和默认生成的构造函数类似,默认生成的析构函数同样对内置类型的成员变量不作处理,只在对象销毁时对自定义类型的成员会调用它们的析构函数来清理该自定义类型的成员变量。

倘若该自定义类型成员同样只有系统默认生成的的析构函数,那么结果就相当于该自定义类型成员也没有被销毁。

不释放内置类型的成员也是有一定道理的,防止释放一些文件指针等等可能导致程序崩溃。

3. 拷贝构函数

除了初始化和销毁工作以外,最常见的就是将一个对象赋值、传参等就必须要拷贝对象。而类这种复杂类型直接赋值是不起作用的,拷贝对象的操作要由拷贝构造函数实现,每次复制对象都要调用拷贝构造函数。

3.1 拷贝构造函数的定义

根据需求我们也可以猜测出C++中的拷贝构造函数的设计。

拷贝构造函数也是特殊的成员函数,负责对象的拷贝赋值工作,这个操作只能发生在对象实例化的时候,拷贝构造的本质就是用同类型的对象初始化新对象,所以也算是一种不同形式的构造函数满足重载的要求,也可叫复制构造函数。

拷贝构造函数仅有一个参数,就是同类型的对象的引用,在用同类型的对象初始化新对象时由编译器自动调用。拷贝构造函数也是构造函数,所以拷贝也是构造的一个重载。

3.2 拷贝构造函数的特性

  • 拷贝构造函数是构造函数的一个重载形式。
  • 拷贝构造函数只有一个参数,且必须是同类型的对象的引用,否则会引发无穷递归。

因为传值调用就要复制一份对象的临时拷贝,而要想拷贝对象就必须要调用拷贝构造函数,而调用拷贝构造函数又要传值调用,这样就会在调用参数列表中“逻辑死循环”出不来了。

设计拷贝构造函数时就已经修改了系统默认生成的拷贝构造函数,所以在此过程不可以再发生拷贝操作。而传引用不会涉及到拷贝操作所以没问题。

另外,有趣的是设计者规定拷贝构造函数的参数必须是同类型的引用,如果设计成指针,系统就当作没有显式定义拷贝构造函数了。

一般拷贝构造另一个对象时,都不希望原对象发生改变,所以形参引用用const修饰。

只显式定义拷贝构造函数,系统不会生成默认的构造函数,只定义构造函数,系统会默认生成拷贝构造。

默认拷贝构造拷贝规则

若未显式定义拷贝构造,和构造函数类似,默认生成的拷贝构造函数对成员的拷贝分两种:

  • 对于内置类型的成员变量,默认生成的拷贝构造是把该成员的存储内容按字节序的顺序逐字节拷贝至新对象中的。这样的拷贝被称为浅拷贝或称值拷贝。类似与memcopy函数。
  • 对于自定义类型的成员,默认生成的拷贝构造函数是调用该自定义类型成员的拷贝构造函数进行拷贝的。

默认生成的拷贝函数也不是万能的,比如栈这个结构。用st1初始化st2时,会导致二者的成员_a指向相同的一块空间。

4. 运算符重载

运算符重载是C++的一大利器,使得对象也可以用加减乘除等各种运算符来进行相加相减比较大小等有意义的运算。默认情况下C++不支持自定义类型像内置类型变量一样使用运算符的,这里的规则需要开发者通过运算符重载函数来定义。

4.1 运算符重载的定义

运算符重载增强了代码的可读性也更方便,但为此我们必须要为类对象编写运算符重载函数以实现这样操作。运算符重载是具有特殊函数名的函数,也具有返回类型、函数名和参数列表。重载函数实现后由编译器自动识别和调用。

  1. 函数名是关键字operator加需要重载的运算符符号,如operator+,operator=等。
  2. 返回类型和参数都要根据运算符的规则和含义的实际情况来定。
bool operator>(const Date& d1, const Date& d2) {
	if (d1._year > d2._year) {
		return true;
	}
	else if (d1._year == d2._year && d1._month > d2._month) {
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day) {
		return true;
	}
	return false;
}
d1 > d2;
operator>(d1, d2);

两个日期类进行比较大小,传参采用对象的常引用形式,避免调用拷贝构造函数和改变实参,返回类型为布尔值,同样都是符合实际的。编译器把operator>(d1,d2)转换成d1>d2,大大提高了代码的可读性。

4.2 运算符重载的特性

  • 只能重载已有的运算符,不能通过连接其他符号来定义新的运算,如operator@。
  • 重载操作符函数只能作用于自定义类型对象,且最多有两个参数,自定义类型最好采用常引用传参。
  • 重载内置类型的操作符,建议不改变该操作符本身含义。
  • 共有5个运算符不可被重载,分别是:.*,域访问操作符::,sizeof,三目运算符?:,结构成员访问符.。

运算符重载不像构造函数是固定在类中的特殊的成员函数,运算符重载适用于所有自定义类型对象,并不单独局限于某个类。但由于类中的成员变量是私有的,运算符重载想使其作用于某个类时,解决方法有三:

  1. 修改成员变量的访问权限变成公有,但破坏了类的封装性,是最不可取的。使用友元函数,但性质与修改访问权限类似,同样不可取的。
  2. 使用Getter Setter方法提供成员变量的接口,保留封装性但较为麻烦。
  3. 将运算符重载函数放到类中变成成员函数,但需要注意修改一些细节。作为类成员的重载函数,形参列表默认隐藏 this 指针,所以必须去掉一个引用参数
class Date {
public:
	Date(int year = 0, int month = 1, int day = 1);
	bool operator>(const Date& d);
private:
	int _year;
	int _month;
	int _day;
};
//bool Date::operator>(Date* this, const Date& d) {...}
bool Date::operator>(const Date& d) {
	// ...
}
d1 > d2;
d1.operator>(d2); //成员函数只能这样调用

4.3 赋值运算符重载

赋值运算符重载实现的是两个自定义类型的对象的赋值,和拷贝构造函数不同拷贝构造是用一个已存在的对象去初始化一个对象,赋值运算符重载是两个已存在的对象进行赋值操作。和两个整形数据的赋值意义相同,所以定义时也是参考内置类型的赋值操作来的。

  • 参数列表 —— 两个对象进行赋值操作,由于放在类中作成员函数,参数列表仅显式定义一个对象的引用。
  • 返回类型 —— 赋值表达式的返回值也是操作数的值,返回对象的引用即可。
// i = j = k = 1;
Date& Date::operator=(const Date& d) {
	if (this != &d) { //优化自己给自己赋值
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}

不传参对象的引用或者不返回对象的引用都会调用拷贝构造函数,为使减少拷贝和避免修改原对象,最好使用常引用。

默认赋值重载赋值规则

类中如果没有显式的定义赋值重载函数,编译器会在类中默认生成一个赋值重载函数的成员函数。默认赋值重载对于内置类型的成员采用浅拷贝的方式拷贝,对于自定义类型的成员会调用它内部的赋值重载函数进行赋值。

所以写不写赋值重载仍然要视情况而定。

Date d5 = d1;
// 用已存在的对象初始化新对象,则是拷贝构造而非赋值重载

掌握以上四种C++中默认的函数,就可以实现完整的日期类了。

5. 日期类的实现

5.1 日期类的定义

class Date {
public:
	Date(int year = 0, int month = 1, int day = 1);
	Date(const Date& d);
	~Date();
    void Print();
	int GetMonthDay();

    bool operator>(const Date& d);
	bool operator<(const Date& d);
	bool operator>=(const Date& d);
	bool operator<=(const Date& d);
	bool operator==(const Date& d);
	bool operator!=(const Date& d);

	Date& operator=(const Date& d);

	Date& operator+=(int day);
	Date operator+(int day);
	Date& operator-=(int day);
	int operator-(const Date& d);
	Date operator-(int day);

	Date& operator++();
	Date operator++(int);
	Date& operator--();
	Date operator--(int);
private:
	int _year;
	int _month;
	int _day;
};

日期类很简单,一样的函数一样的变量再封装起来,把之前联系的代码放到一起。接下来就是函数接口的具体实现细节了。

5.2 日期类的接口实现

//构造函数
Date(int year = 0, int month = 1, int day = 1);
//打印
void Print();
//拷贝构造
Date(const Date& d);
//析构函数
~Date();
//获取当月天数
int GetMonthDay();
// >运算符重载
bool operator>(const Date& d);
// >=运算符重载
bool operator>=(const Date& d);
// <运算符重载
bool operator<(const Date& d);
// <=运算符重载
bool operator<=(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// !=运算符重载
bool operator!=(const Date& d);
// =运算符重载
Date& operator=(const Date& d);
//日期+天数=日期
Date& operator+=(int day);
//日期+天数=日期
Date operator+(int day);
//日期-天数=日期
Date& operator-=(int day);
//日期-日期=天数
int operator-(const Date& d);
//日期-天数=日期
Date operator-(int day);
//前置++
Date& operator++();
//后置++
Date operator++(int);
//前置--
Date& operator--();
//后置--
Date operator--(int);

从上述函数声明的列表也可以看出,构造函数、析构函数等都是相对简单的,实现类的重点同样也是难点是定义各种运算符的重载。

日期类的构造函数

日期类的构造函数之前实现过,但仍需注意一些细节,比如过滤掉一些不合法的日期。要想实现这个功能就要定好每年每月的最大合法天数,可以将其存储在数组MonthDayArray,并封装在函数GetMonthDay中以便在判断的时候调用。

//获取合法天数的最大值
int Date::GetMonthDay() {
	static int MonthDayArray[13] = { 0, 31 ,28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int day = MonthDayArray[_month];
	//判断闰年
	if (_month == 2 && ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0))) {
		day += 1;
	}
	return day;
}
//构造函数
Date::Date(int year, int month, int day) {
	_year = year;
	_month = month;
	_day = day;
	//判断日期是否合法
	if (month > 12 || day > GetMonthDay()) {
		cout << "请检查日期是否合法:";
		Print();
	}
}

数组MonthDayArray的定义也有讲究,定义13个数组元素,第一个元素就放0,这样让数组下标和月份对应起来,使用更加方便。确定每月的天数还要看年份是否是闰年,所以还要判断是否是闰年,因为闰年的二月都要多一天。这些封装在函数GetMonthDay中,调用时返回当月具体天数,放在构造函数中判断是否日期是否合法。

由于两个函数都是定义在类中的,默认将类对象的指针作函数参数,调用时更加方便。

析构函数、打印函数和拷贝构造函数都很简单和之前一样,这里就不写了。接下来就是实现的重点运算符重载。

比较运算符的重载

//运算符重载 >
bool Date::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;
	}
	return false;
}
//运算符重载 >=
bool Date::operator>=(const Date & d) {
	return (*this > d) || (*this == d);
}
//运算符重载 <
bool Date::operator<(const Date& d) {
	return !(*this >= d);
}
//运算符重载 <=
bool Date::operator<=(const Date& d) {
	return !(*this > d);
}
//运算符重载 ==
bool Date::operator==(const Date& d) {
	return (_year == d._year) && (_month == d._month) && (_day == d._day);
}
//运算符重载 !=
bool Date::operator!=(const Date& d) {
	return !(*this == d);
}

比较运算符的重载不难实现,注意代码的逻辑即可。主要实现>和==的重载,其他的都调用这两个函数就行。这样的实现方法基本适用所有的类。

加法运算符的重载

加法实现的意义在于实现日期+天数=日期的运算,可以现在稿纸上演算一下探寻一下规律。

可以看出加法的规律是,先将天数加到天数位上,然后判断天数是否合法。

  1. 如果不合法则要减去当月的最大合法天数值,相当于进到下一月,即先减值再进位
  2. 若天数合法,则进位运算结束。
  3. 在天数进位的同时,月数如果等于13则赋值为1,再年份加1,可将剩余天数同步到明年。

先减值再进位的原因是,减值所减的是当月的最大合法天数,若先进位的话,修改了月份则会减成下个月的天数。

//运算符重载 +=
//日期 + 天数 = 日期
Date& Date::operator+=(int day) {
	_day += day;
	//检查天数是否合法
	while (_day > GetMonthDay()) {
		_day -= GetMonthDay();//天数减合法最大值 --- 先减值,再进位
		_month++;//月份进位
		//检查月数是否合法
        if (_month == 13) {
			_month = 1;
			_year += 1;//年份进位
		}
	}
	return *this;
}

这样的实现方法会改变对象的值,不如直接将其实现为+=,并返回对象的引用还可以避免调用拷贝构造。

实现+重载再去复用+=即可。

//运算符重载 +
Date Date::operator+(int day) {//临时变量会销毁,不可传引用
	Date ret(*this);
	ret += day; // ret.operator+=(day);
	return ret;
}

创建临时变量并用*this初始化,再使用临时变量进行+=运算,返回临时变量即可。注意临时变量随栈帧销毁,不可返回它的引用。

减法运算符的重载

//运算符重载 -=
//日期 - 天数 = 日期
Date& Date::operator-=(int day) {
    //防止天数是负数
	if (_day < 0) {
		return *this += -day;
	}
	_day -= day;
	//检查天数是否合法
	while (_day <= 0) {
		_month--;//月份借位
		//检查月份是否合法
		if (_month == 0) {
			_month = 12;
			_year--;//年份借位
		}
		_day += GetMonthDay();//天数加上合法最大值 --- 先借位,再加值
	}
	return *this;
}

实现减法逻辑和加法类似,先将天数减到天数位上,再检查天数是否合法:

  1. 如果天数不合法,向月份借位,再加上上月的最大合法天数,即先借位再加值。并检查月份是否合法,月份若为0则置为12年份再借位。
  2. 如果天数合法,则停止借位。

先借位再加值是因为加值相当于去掉上个月的过的天数,所以应加上的是上月的天数。

值得注意的是,修正月数的操作必须放在加值的前面,因为当月数借位到0时,必须要修正才能正常加值。

//运算符重载 -
//日期 - 天数 = 日期
Date Date::operator-(int day) {
	Date ret(*this);
	ret -= day;
	return ret;
}
//日期 - 日期 = 天数
int Date::operator-(const Date& d) {
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (max < min) {
		max = d;
		min = *this;
		flag = -1;
	}
	int gap = 0;
	while ((min + gap) != max) {
		gap++;
	}
	return gap * flag;
}

日期-日期=天数的计算可以稍微转化一下变成日期+天数=日期,让小的日期加上一个逐次增加的值所得结果和大的日期相等,那么这个值就是二者所差的天数。

加的时候,日期不合法是因为天数已经超出了当月的最大合法天数,既然超出了,就将多余的部分留下,把当月最大合法天数减去以增加月数。减的时候同理,日期不合法是因为天数已经低于了0,回到了上一个月,那就补全上一个月的最大合法数值用此去加上这个负数,这个负数就相当于此月没有过完的剩余的天数。

自增自减的重载

C++为区分前置和后置,规定后置自增自减的重载函数参数要显式传一个int参数占位,可以和前置构成重载。

//前置++
Date& Date::operator++() {
	return *this += 1;
}
//后置++
Date Date::operator++(int) {
	return (*this += 1) - 1;
}
//前置--
Date& Date::operator--() {
	return *this -= 1;
}
//后置--
Date Date::operator--(int) {
	return (*this -= 1) + 1;
}
// 实现方式2
Date ret = *this;
*this + 1;
return ret;

实现对象的前置后置的自增和自减,要满足前置先运算再使用和后置先使用再运算的特性。也用上面实现好的重载复用即可。或者也可以直接利用临时变量保存*this,改变*this之后返回临时变量即可。

++d2;
d2.operator();
d1++;
d1.operator(0);

可以看出,对于类对象来说,前置++比后置++快不少,只调用了一次析构函数,而后置++ 调用了两次拷贝构造和三次析构。

6. const 类

被const修饰的类即为 const 类,const 类调用成员函数时出错,因为参数this指针从const Date*到Date*涉及权限放大的问题。如图所示:

6.1 const 类的成员函数

想要避免这样的问题,就必须修改成员函数的形参this,但 this 指针不能被显式作参数自然不可被修改。为解决这样的问题,C++规定在函数声明后面加上 const ,就相当于给形参 this 指针添加 const 修饰。

//运算符重载 !=
//声明
bool Date::operator!=(const Date& d) const;
//定义
bool Date::operator!=(const Date& d) const {
	return !(*this == d);
}

像上述代码这样,由 const 修饰的类成员函数称之为 const 成员函数,const 修饰类成员函数,实际修饰函数的隐含形参 this 指针,这样该函数就不可修改对象的成员变量。

6.2 取地址操作符重载

还有两个类的默认成员函数,取地址操作符重载和 const 取地址操作符重载,这两个默认成员函数一般不用定义,编译器默认生成的就够用了。

Date* operator&() {
    return this;
    //return NULL; //不允许获取对象的地址
}
const Date* operator&() const {
    return this;
}

当不允许获取对象的地址时,就可以将取地址重载成空即可。

总结

到此这篇关于C++初阶教程之类和对象的文章就介绍到这了,更多相关C++类和对象内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++ 类和对象基础篇

    类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量:创建对象的过程也叫类的实例化.每个对象都是类的一个具体实例(Instance),拥有类的成员变量和成员函数. 一.类的定义 一个简单的类的定义: class Student{ public: //成员变量 char *name; int age; float score; //成员函数 void say(){ cout<<name<<"的年龄是"<<age<<&qu

  • C++之类和对象课后习题简单实例

    建立一个对象数组,内放5个学生的(学号,成绩),设立一个函数max,用指向对象的指针作函数参数,在max函数中找出5个学生的最高成绩者,并输出其学号. #include<iostream> using namespace std; class Student {public: Student(int=10,int=0); int number; int score; void display(); }; Student::Student(int num,int sco):number(num)

  • 详解C++编程中类的声明和对象成员的引用

    C++类的声明和对象的创建 类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量:创建对象的过程也叫类的实例化.每个对象都是类的一个具体实例(Instance),拥有类的成员变量和成员函数. 与结构体一样,类只是一种复杂数据类型的声明,不占用内存空间.而对象是类这种数据类型的一个变量,占用内存空间. 类的声明 类是用户自定义的类型,如果程序中要用到类,必须先进行声明,或者使用已存在的类(别人写好的类.标准库中的类等),C++语法本身并不提供现成的类的名称.结构和内容. 一个简

  • C++中对象与类的详解及其作用介绍

    目录 什么是对象 面向过程 vs 面向对象 面向过程 面向对象 什么是类 类的格式 类的成员函数 函数访问权限 方法一 方法二 方法三 inline 成员函数 什么是对象 任何事物都是一个对象, 也就是传说中的万物皆为对象. 对象的组成: 数据: 描述对象的属性 函数: 描述对象的行为, 根据外界的信息进行相应操作的代码 具有相同的属性和行为的对象抽象为类 (class) 类是对象的抽象 对象则是类的特例 面向过程 vs 面向对象 面向过程 面向过程的设计: 围绕功能, 用一个函数实现一个功能

  • c++ 类和对象总结

    话不多说,我们直接进入主题: 对象:客观世界里的一切事物都可以看作是一个对象,每一个对象应当具有属性(静态特征,比如一个班级,一个专业,一个教室)和行为(动态特征,例如:学习,开会,体育比赛等)两个要素. 对象是由一组属性和一组行为构成的. 类(class):就是对象的类型,代表了某一批对象的共同特性和特征.类是对象的抽象,而对象是类的具体实例. 2.1 类的引入 在C语言中我们定义一个结构体是这样定义的: struct Student { int _age; char* _Gender; ch

  • C++类和对象实例解析(二)

    C++既是面向对象也是面向过程的语言,在这里就有一个重要的概念--类.         何谓类?类是对对象的一种抽象,举例来讲:每一个实实在在存在的人就是一个对象,人有很多共同的特征(一个头,两条腿,能走,能跑),这具有共同特征的人就成为一个类.类是一个抽象的名词,每一个人(即对象)是这个类的实例.         对象间具有的共同特征是对象的属性和行为.录像机是一个对象,它的属性是生产厂家.牌子.重量.颜色等等,它的行为就是它的功能,如录像.放像.快进.倒退等操作. C++程序中,需要先定义一

  • C++初阶教程之类和对象

    目录 类和对象<上> 1. 类的定义 2. 类的封装 2.1 访问限定修饰符 2.2 类的封装 3. 类的使用 3.1 类的作用域 3.2 类的实例化 4. 类对象的存储 5. this 指针 5.1 this 指针的定义 5.2 this 指针的特性 类和对象<中> 1. 构造函数 1.2 构造函数的定义 2.2 构造函数的特性 2. 析构函数 2.1 析构函数的定义 3. 拷贝构函数 3.1 拷贝构造函数的定义 3.2 拷贝构造函数的特性 4. 运算符重载 4.1 运算符重载的

  • C语言新手初阶教程之三子棋实现

    目录 三子棋 创建项目环境 头文件内容 编写main函数(test.c) 实现每一个接口函数 1.board 2.printfboard 3.play 4.computerplay 5.win 总结 三子棋 大家小时候应该都玩过三子棋吧,学习了这么久的C语言,我们其实已经具备了做三子棋这种小型项目的能力,今天我会尽量沉浸式的教大家实现三子棋,如果看完这篇博客还是不会的可以去我最后附上的gitee仓库链接中寻找.但我还是希望大家能自己完成,在三子棋中体现自己的风格. 创建项目环境 首先,第一步,打

  • Python Matplotlib初阶使用入门教程

    目录 0. 前言 1. 创建Figure的两种基本方法 1.1 第1种方法 1.2 第2种方法 2. Figure的解剖图及各种基本概念 2.1 Figure 2.2 Axes 2.3 Axis 2.4 Artist 3. 绘图函数的输入 4. 面向对象接口与pyplot接口 5. 绘图复用实用函数例 0. 前言 本文介绍Python Matplotlib库的入门求生级使用方法. 为了方便以下举例说明,我们先导入需要的几个库.以下代码在Jupyter Notebook中运行. %matplotl

  • C++初阶之list的模拟实现过程详解

    list的介绍 list的优点: list头部.中间插入不再需要挪动数据,O(1)效率高 list插入数据是新增节点,不需要增容 list的缺点: 不支持随机访问,访问某个元素效率O(N) 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低. 今天来模拟实现list 我们先来看看官方文档中对于list的描述 我们先大致了解一下list的遍历 迭代器 对于迭代器我们可以用while循环+begin()end().同时还可以用迭代器区间. 当然迭代器区间的方式只适用于内存连续的结构

  • BootStrapValidator初使用教程详解

    bootstrap:能够增加兼容性的强大框架. 因为项目需要数据验证,看bootstrapValidator 还不错,就上手一直,完美兼容,话不多说. bootstrapValidator的github地址 需要引用css: bootstrap.min.css bootstrapValidator.min.css js: jQuery-1.10.2.min.js bootstrap.min.js bootstrapValidator.min.js 以上这些都是必须的. 先上个简单的例子,只要导入

  • Node.js 基础教程之全局对象

    Node.js 基础教程之全局对象 在浏览器 JavaScript 中,通常 window 是全局对象. Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性. global 最根本的作用是作为全局变量的宿主. 注意: 永远使用 var 定义变量以避免引入全局变量,因为全局变量会污染 命名空间,提高代码的耦合风险. __filename 脚本绝对路径 表示当前正在执行的脚本的文件名.它将输出文件所在位置的绝对路径,且和命令行参数

  • Zend Framework教程之响应对象的封装Zend_Controller_Response实例详解

    本文实例讲述了Zend Framework教程之响应对象的封装Zend_Controller_Response用法.分享给大家供大家参考,具体如下: 概述 响应对象逻辑上是请求对象的搭档.目的在于收集消息体和/或消息头,因而可能返回大批的结果. Zend_Controller_Response响应对象的基本实现 ├── Response │   ├── Abstract.php │   ├── Cli.php │   ├── Exception.php │   ├── Http.php │  

  • Kotlin 基础教程之类、对象、接口

    Kotlin 基础教程之类.对象.接口 Kotlin中类.接口相关概念与Java一样,包括类名.属性.方法.继承等,如下示例: interface A { fun bar() fun foo() { // 可选方法体 } } class Child: A { override fun bar() { // todo } override fun foo() { super.foo() } } class 构造器 Kotlin 中的类可以有一个 主构造器, 以及一个或多个次构造器, 主构造器是类头

  • Zend Framework教程之请求对象的封装Zend_Controller_Request实例详解

    本文实例讲述了Zend Framework教程之请求对象的封装Zend_Controller_Request方法.分享给大家供大家参考,具体如下: 概述 请求对象是在前端控制器,路由器,分发器,以及控制类间传递的简单值对象.请求对象封装了请求的模块,控制器,动作以及可选的参数,还包括其他的请求环境,如HTTP,CLI,PHP-GTK. 请求对象的基本实现 ├── Request │   ├── Abstract.php │   ├── Apache404.php │   ├── Exceptio

  • C语言中的指针 初阶

    目录 1.指针是什么 2.指针和指针类型 3.野指针 3.1野指针成因 3.2如何规避野指针 4.指针的运算 4.1指针±整数 4.2指针-指针 4.3指针的关系运算 5.指针和数组 6.二级指针 7.指针数组 1.指针是什么 初学者都有一个疑问,那就是指针是什么?简单的说,就是通过它能找到以它为地址的内存单元. 地址指向了一个确定的内存空间,所以地址形象的被称为指针. int main() { int a = 10; int* pa = &a; return 0; } //pa是用来存放地址(

随机推荐