c++的类和对象(中):默认成员函数与运算符重载(重难点!!)

近听水无声477 2024-08-12 08:05:02 阅读 92

前言 

Hello, 小伙伴们,我们今天继续c++的学习,我们上期有介绍到c++的部分特性,以及一些区别于c语言的地方,今天我们将继续深入了解c++的类和对象,探索c++的奥秘。

好,废话不多说,开始我们今天的学习。 

1.类默认成员函数

默认成员函数就是用户没有显示实现,编译器会自动生成的函数称为默认成员函数。一个类,我们不写的情况下,编译器会默认生成6个默认函数,需要注意的是这六个函数中重要的是前4个,最后两个取地址重载不重要,我们稍微了解一下就好。其次,c++11以后还会增加两个默认的函数,移动构造和移动赋值,这个我们后面再讲解。默认成员函数十分的重要,也比较复杂,我们要从两个方面去学习:

第一:我们不写默认函数时, 编译器默认的函数行为是什么,是否能满足我们的要求。

第二:编译器默认生成的函数不满足我们的要求时,我们要自己去实现符合要求的函数。

 

1.1构造函数 

构造函数就是特别的成员函数,需要注意的是,构造函数虽然名为构造,但是构造函数的主要任务不是开辟空间创建对象(我们之前使用的局部对象是栈帧创建时,就开辟好的),而对象的实例化时初始化对象。构造函数的本质就是要替代我们之前在实现Stack 和 Queue类中的Init函数功能,构造函数自动调用的特点就完美的替代了Init函数。

构造函数的特点:

1.函数名与类名相同。会自动生成一个无参数的默认构造函数,一旦用户显示定义,编译器就不会再生成默认的

2.无返回值(返回值啥的都不需要给,也不需要void, 也不需要纠结, c++的一定就是如此)。

3.对象实例化时西永会和全缺省函数也是默认构造函数,总结一下就是不传实参就可

自动调用构造函数。

4.构造函数可以重载。

5.如果类中没有显示定义的构造函数,则c++编译器

构造函数。

 6.无参的构造函数、全缺省构造函数、我们不写构造函数时编译器自己默认生成的构造函数,都叫做默认构造函数。但着三个函数有且只有一个存在于一个类中,不能同时存在。无参构造函数和全缺省函数虽然构成函数重载,但是调用时产生歧义。要注意的是很多的同学会认为默认构造函数是编译器默认生成的,但实际上无参构造函数

以调用的构造函数就叫默认构造。

7.我们不写,编译器默认生成的构造,对内置类型的成员变量初始化没有要求,也就是说是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员便变量没有默认构造函数,那么就会报错,我们要初始化这个变量,需要用初始化列表才能解决,初始化列表我们到了后面会详细介绍!!

说明: c++把类型分为内置类型(基本类型)和自定义类型。内置类型就是语言原生数据类型,如:int/char/double/指针等类型,自定义类型就是我们使用class和struct等关键字自己定义的类型。

我们来看看下面的代码实例:

<code>#include<iostream>

using namespace std;

class Date

{

public :

// 1.⽆参构造函数

Date()

{

_year = 1;

_month = 1;

_day = 1;

}

// 2.带参构造函数

Date(int year, int month, int day)

{

_year = year;

_month = month;

_day = day;

}

// 3.全缺省构造函数

/*Date(int year = 1, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}*/

void Print()

{

cout << _year << "/" << _month << "/" << _day << endl;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

// 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉

// 编译报错:error C2512: “Date”: 没有合适的默认构造函数可⽤

Date d1; // 调⽤默认构造函数

Date d2(2025, 1, 1); // 调⽤带参的构造函数

// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法

// 区分这⾥是函数声明还是实例化对象

// warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?)

Date d3();

d1.Print();

d2.Print();

return 0;

}

1.2析构函数

析构函数与构造函数的功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他释放了,我们就不需要管,c++规定对象在销毁前会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类好比我们之前的Stack 实现的Destroy功能,反而Date没有Destroy,其实就是没有资源的释放(没有动态的申请内存空间),所以严格上说Date是不需要析构函数的!!

析构函数的特点

析构函数名就是类名前面加上字符~。无参数返回值。(这里和构造函数十分的相似,也不需要加 void).一个类只能有一个析构函数。若没有显示定义,系统就会自动生成默认的析构函数。对象的生命周期结束时,系统会自动调用析构函数。跟构造函数类似,我们不写编译器就会自动生成析构函数对内置类型成员不做处理,自定义类型成员就会调用他的析构函数。还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说,自定义类型成员无论在什么样的情况下都会自动调用自己的析构函数!!如果类中没有申请资源,析构函数就可以不写,直接使用编译器生产的默认函数,如Date;如果默认生成的析构函数就够用,我们就不需要再显示其结构, 比如:MyQueue;但是有资源的申请时,一定要自己写析构函数,否则会造成资源泄露,如:Stack。一个局部域的多个对象,c++规定后定义的先析构。

我们来看下面的用例:

#include<iostream>

using namespace std;

typedef int STDataType;

class Stack

{

public :

Stack(int n = 4)

{

_a = (STDataType*)malloc(sizeof(STDataType) * n);

if (nullptr == _a)

{

perror("malloc申请空间失败");

return;

}

_capacity = n;

_top = 0;

}

~Stack()

{

cout << "~Stack()" << endl;

free(_a);

_a = nullptr;

_top = _capacity = 0;

}

private:

STDataType* _a;

size_t _capacity;

size_t _top;

};

// 两个Stack实现队列

class MyQueue

{

public :

//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源

// 显⽰写析构,也会⾃动调⽤Stack的析构

/*~MyQueue()

{}*/

private:

Stack pushst;

Stack popst;

};

int main()

{

Stack st;

MyQueue mq;

return 0;

}

 看一下用c++实现栈的结构,再对比一下我们自己之前用C语言实现的栈结构,我们可以明显的观察到c++相较于C语言明显的优势。

我们发现有了析构函数和构造函数确实方便了很多,也不会在担心忘记写Init函数和Destroy函数了,我们写代时也方便了不少!!

1.3拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都是默认值,则此构造函数也叫拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。

拷贝构造的特点:

拷贝构造函数是构造函数的一个重载。拷贝构造函数的第一个函数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引起无穷递归调用、拷贝构造函数也可以有多个参数,但是第一个参数必须是类类型对象的应用, 后面的参数必须有缺省值。c++规定的自定义类型对象进行拷贝行为,必须调用靠别构造函数,所以这里自定义类型传值传参和传值返回都会调用拷贝调用完成。若显示未定义拷贝构造,编译器会生成自动的拷贝构造函数,自动生成的拷贝构造对内置类型成员变量完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的拷贝构造。像Date这样的类成员变量全是内置类型且没有指向那个资源,编译器自动生成的拷贝构造就可以完成值的拷贝,所以不需要我们显示拷贝构造。但是像Stack这样的类,虽然也是内置类型,但是_a指向的资源,编译器自动生成拷贝构造就不符合我们现在的要求了,所以需要我们自己来实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造。这里有一个小技巧,如果一个类显示实现了析构并释放资源那么他就需要显示拷贝构造,否则就不需要。传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回就有问题,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

我们来看下面的例子:

#define _CRT_SECURE_NO_WARNINGS 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;

}

// 编译报错:error C2652 : “Date”: ⾮法的复制构造函数: 第⼀个参数不应是“Date”

//Date(Date d)

Date(const Date & d)

{

_year = d._year;

_month = d._month;

_day = d._day;

}

Date(Date * d)

{

_year = d->_year;

_month = d->_month;

_day = d->_day;

}

void Print();

private:

int _year;

int _month;

int _day;

};

void Date:: Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

void Func1(Date d)

{

cout << &d << endl;

d.Print();

}

// Date Func2()

Date & Func2()

{

Date tmp(2024, 7, 5);

tmp.Print();

return tmp;

}

int main()

{

Date d1(2024, 7, 5);

// C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥传值传参要调⽤拷⻉构造

// 所以这⾥的d1传值传参给d要调⽤拷⻉构造完成拷⻉,传引⽤传参可以较少这⾥的拷⻉

Func1(d1);

cout << &d1 << endl;

// 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造

Date d2(&d1);

d1.Print();

d2.Print();

//这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针

Date d3(d1);

d2.Print();

// 也可以这样写,这⾥也是拷⻉构造

Date d4 = d1;

d2.Print();

// Func2返回了⼀个局部对象tmp的引⽤作为返回值

// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤

Date ret = Func2();

ret.Print();

return 0;

}

 2.赋值运算符的重载

2.1运算符的重载

当运算符被用于类类型的对象时,c++语言允许我们通过对运算符重载的形式指定其新的含义。c++规定类类型对象使用运算符时,必须转换调用对应运算符的重载,若没有对饮的运算符重载,则会编译报错。 运算符重载是具有特殊名字的函数,他的名字是有operaor和后面的要定义的运算符共同构成的。和其他函数一样,它具有其返回类型和参数列表以及函数体。重载运算符的参数个数和该运算对象的数量一致。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐士指针---this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。运算符重载后,其优先性和结核性与对应的内置类型运算符保持一致。不能通过连接语法中没有的符号来创建新的操作符,比如:operator@。.*   ::  sizeof  ?:     .  注意前面的操作符不能重载。重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义!!一个类需要重载那些运算符,是看运动算符在重载后承载了什么样的意义。重载++运算符时,有前置++,和后置++,而运算重载函数都是通过operator++来实现的,没有办法很好的来区分,c++规定,,后置的++重载时增加一个int形参,跟前置++构成函数重载,方便区分。重载<<和>>时,需要重载为全局函数,因为重载成员函数时,this指针默认会抢占形参的第一个位置,第一个形参位置是左侧运算对象,调用时变成了<<cout,不符合使用习惯和可读性。重载为全局函数把ostram和istream1放到第一个形参位置就i行了,第二个形参位置当类类型对象。

比如,我们来看下面的代码:

#include<iostream>

using namespace std;

// 编译报错:“operator +”必须⾄少有⼀个类类型的形参

int operator+(int x, int y)

{

return x - y;

}

class A

{

public :

void func()

{

cout << "A::func()" << endl;

}

};

typedef void(A::* PF)(); //成员函数指针类型

int main()

{

// C++规定成员函数要加&才能取到函数指针

PF pf = &A::func;

A obj;//定义ob类对象temp

// 对象调⽤成员函数指针时,使⽤.*运算符

(obj.*pf)();

return 0;

}

3.日期类的实现

接下来我们实现一个日期类应用的底层逻辑,结合上面的知识,我们来试着实现一下把 :

首先我们还是要创建三个文件:

接下来我们展示代码:

Date.h

 

<code>#pragma once

#include<iostream>

#include<assert.h>

using namespace std;

class Date

{

public:

Date(int year = 2023, int month = 8, int day = 10);

//~Date();

void Print();

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+=(int x);

Date& operator+(int x);

Date& operator-=(int x);

Date& operator-(int x);

//++d

Date& operator++();

//d++

Date operator++(int);

//两个日期相减隔开的天数

int operator-(const Date& d);

int GetMonthDay(int month, int year)

{

assert(month > 0 && month <= 12);

static int monthday[13] = { -1, 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 monthday[month];

}

private:

int _year;

int _month;

int _day;

};

 Date.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include"Date.h"

Date::Date(int year, int month, int day)

{

_year = year;

_month = month;

_day = day;

}

void Date:: Print()

{

cout << _year << "/" << _month << "/" << _day << endl;

}

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);

}

bool Date::operator<=(const Date& d)

{

return *this < d || *this == d;

}

bool Date::operator>=(const Date& d)

{

return *this > d || *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);

}

Date& Date::operator+=(int x)

{

if (x < 0)

{

return *this -= -x;

}

_day += x;

while (_day > GetMonthDay( _month,_year))

{

_day -= GetMonthDay(_month, _year);

_month++;

if (_month > 12)

{

_year++;

_month = 1;

}

}

return *this;

}

Date& Date::operator+(int x)

{

Date tmp = *this;

tmp += x;

return tmp;

}

Date& Date::operator -= (int x)

{

if (x < 0)

{

return(*this) += -x;

}

_day -= x;

while (_day <= 0)

{

_day += GetMonthDay(_month, _year);

_month--;

if (_month == 0)

{

_year--;

_month = 12;

}

}

return *this;

}

Date& Date::operator-(int x)

{

Date tmp = *this;

tmp -= x;

return tmp;

}

//++d

Date& Date::operator++()

{

*this += 1;

return *this;

}

//d++

Date Date::operator++(int)

{

Date tmp = *this;

*this += 1;

return tmp;

}

//使用逐一++的方法来判断相差的天数

int Date::operator-(const Date& d)

{

Date big = *this;

Date small = d;

int flag = 1;

if (*this < d)

{

big = d;

small = *this;

flag = -1;

}

int count = 0;

while (big != small)

{

small++;

count++;

}

return count * flag;

}

Test.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include"Date.h"

int main()

{

Date d1(2023, 4, 5);

Date d2 = d1 + 100;

d2.Print();

d1.Print();

//d1 += 100;

d1.Print();

d1 -= 5;

d1.Print();

(d1 += (-100)).Print();

(d1 -= (-100)).Print();

//cout << (d3 ? "true" : "false") << endl;

cout << endl;

d1.Print();

d2.Print();

cout << d1 - d2 << endl;

return 0;

}

好,今天的学习就到这里,我们下期再见!!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。