类与对象(二)

类的六个默认构造函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

在这里插入图片描述

构造函数

之前我们创建过Date类,通过Init函数初始化Date类创建的对象,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

特性

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

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。

给大家举个例子:

#include<iostream>
using namespace std;

class Date {
private:
	int _year;
	int _month;
	int _day;

public:
	//无参构造函数
	Date() {
		_year = _month = _day = 1;
	}

	//带参构造函数
	Date(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}

	void Print() {
		cout << _year << '/' << _month << '/' << _day << endl;
	}
};

int main()
{
	Date d1;
	Date d2(2, 2, 2);

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

 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
 // Date d3():声明了d3函数,该函数无参,返回一个日期类型的对象
 // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
 int main()
 {
 // 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
 // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
 Date d1;
 return 0;
 }
  1. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
    解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

总结就是,构造函数会对自定义类型调用它的默认成员函数,而对内置类型不做处理
而在C++11的规定中,我们实现了可以在声明处写缺省值的功能,如下所示:

class Date{
private:
	int _year=1;//缺省值
	int _month=1;
	int _day=1;
	//这里的缺省值实际上是传递向某处的,只是我们现在还没有学到,后面将为大家讲解为什么这里传缺省值可以初始化
};

总而言之,经过分析,我们发现,绝大多数情况下我们都需要自己写构造函数,且自定义函数如果没有默认构造函数就会报错。因为自定义类型里也是内置类型,即使几个自定义类型套娃,到最后的一个类的成员也会是只有内置类型

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
    这里我们有三个参数,如果我们只传递其中一个参数或者两个参数,那这里就不是默认构造函数

析构函数

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

特性

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
//析构函数
	~Date() {
		//这里没有开空间,所以这里的析构函数可以什么都不写,或者使用系统默认生成的析构函数
	}

如果大家想看一下标准的析构函数,可以给大家看一下栈的析构函数

~Stack()
 {
 	if (_array)
 	{
 		free(_array);
 		_array = NULL;
 		_capacity = 0;
 		_size = 0;
 	}
 }
  1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
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对象销毁时,要保证其内部每个自定义对象都可以正确销毁
// main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

需要注意的是,destroy比Init更容易忘记,所以当我们在类里面自动调用析构函数,不容易出错,就比如我们之前在使用栈等需要自己开空间的类型,我们都有可能忘记free掉我们开辟的内存,就拿两个栈实现一个队列来说,我们给大家看一下用C++写和C写的区别

C

typedef int STDataType;
typedef struct Stack {
	STDataType* a;
	int top;
	int capacity;
}Stack;

void StackInit(Stack* ps) {
	assert(ps);
	ps->a = NULL;
	ps->capacity = 0;
	ps->top = -1;
}

// 入栈 
void StackPush(Stack* ps, STDataType x) {
	assert(ps);
	if (ps->capacity == ps->top + 1)
	{
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* node = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));

		if (node == NULL)
		{
			perror("node realloc");
			return;
		}
		ps->a = node;
		ps->capacity = newcapacity;
	}
	ps->top++;
	ps->a[ps->top] = x;
}
// 出栈 
void StackPop(Stack* ps) {
	assert(ps);
	assert(ps->top >= 0);
	ps->top--;
}
// 获取栈顶元素 
STDataType StackTop(Stack* ps) {
	assert(ps);
	assert(ps->top >= 0);
	return ps->a[ps->top];
}
// 获取栈中有效元素个数 
int StackSize(Stack* ps) {
	assert(ps);
	assert(ps->top >= 0);
	return ps->top + 1;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
bool StackEmpty(Stack* ps) {
	assert(ps);
	return ps->top == -1;
}
// 销毁栈 
void StackDestroy(Stack* ps) {
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = 0;
	ps->top = -1;
}

void StackPrint(Stack* ps) {
	assert(ps);

	while (ps->top>=0)
	{
		printf("%d ", ps->a[ps->top]);
		StackPop(ps);
	}
	printf("\n");
}

typedef struct {
    Stack pushst;
    Stack popst;
} MyQueue;


MyQueue* myQueueCreate() {
    MyQueue* obj=(MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&obj->pushst);
    StackInit(&obj->popst);
    return obj;
}

void myQueuePush(MyQueue* obj, int x) {
    StackPush(&obj->pushst,x);
}

int myQueuePop(MyQueue* obj) {
    int front=myQueuePeek(obj);
    StackPop(&obj->popst);
    return front;
}

int myQueuePeek(MyQueue* obj) {
    if(StackEmpty(&obj->popst))
    {
        while(!StackEmpty(&obj->pushst))
        {
            StackPush(&obj->popst,StackTop(&obj->pushst));
            StackPop(&obj->pushst);
        }
		}
		return StackTop(&obj->popst);
}

bool myQueueEmpty(MyQueue* obj) {
    return StackEmpty(&obj->popst)&&StackEmpty(&obj->pushst);
}

void myQueueFree(MyQueue* obj) {
    StackDestroy(&obj->pushst);
    StackDestroy(&obj->popst);
    free(obj);
}

C++

class MyQueue {
public:
    MyQueue() {
        // C++里面的栈,其内部容器会自动初始化
    }

    void push(int x) {
        in_Stack.push(x); // 进队列就从in_Stack进
    }

    int pop() {
        if (out_Stack.empty()) {
            while (!in_Stack.empty()) {
                out_Stack.push(in_Stack.top());
                in_Stack.pop();
                // 如果out_Stack是空的,那么就会进入循环,把in_Stack里面的元素全部导入out_Stack
            }
        }

        int x = out_Stack.top(); // 第一个元素就是队头元素
        out_Stack.pop();
        return x;
    }

    int peek() {
        if (out_Stack.empty()) {
            while (!in_Stack.empty()) {
                out_Stack.push(in_Stack.top());
                in_Stack.pop();
            }
        }

        return out_Stack.top(); // 这个跟pop类似
    }

    bool empty() {
        return in_Stack.empty() && out_Stack.empty(); // 两个都不为空就ok
    }

private:
    stack<int> in_Stack;
    stack<int> out_Stack;
};

可以看出,不仅不需要我们来创建Stack,还免去了一堆销毁函数的代码量,谁更简洁一目了然

最后总结一下:
①析构函数并不是所有类都需要,比如Date类就并不需要自己写
②析构函数的调用顺序:局部对象(后定义先析构)-》局部的静态(静态变量生命周期是全局的,所以再main函数结束后才销毁)-》全局对象(后定义先析构)

拷贝构造函数

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

特征

拷贝构造函数也是特殊的成员函数,其特征如下:

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

//使用样例
int main()
{
	Date d1(2024, 4, 24);
	Date d2(d1);

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

上面我们提到了,如果我们直接传值调用,就会引发无限递归,给大家看看:

在这里插入图片描述

这样调用拷贝构造,要先传参,这里传值传参,就会形成一个新的拷贝构造,这样下去,就会无限循环下去,代码崩溃

传值调用和传引用调用
C++规定自定义类型都会调用拷贝构造函数,只有调用完了之后才会再次进入函数体。传参这个过程调用的是拷贝构造,传值传参要调用拷贝构造;引用就不会调用拷贝构造,因为它是变量的别名,直接完成调用

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按(内置类型成员)内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
    给大家看一下:

在这里插入图片描述

我们注释掉了拷贝构造函数,但是依然成功了,这就是编译器自己生成的拷贝构造函数的作用。
但是浅拷贝会有问题,给大家看一个例子:

在这里插入图片描述

可以看出来,编译器自己生成的拷贝构造函数并不完全是拷贝,而是直接把d2变成了d1,而这并不是我们想看到的,我们只是想要把d1的成员变量都拷贝过来,却直接变成了d1,很明显这是十分错误的。

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。系统自己生成的像memcpy一样的,就是把每个字节的数据都拷过去,内置类型完成值拷贝

疑难

1.我们假使删除掉之前写的Date的构造函数,而只保留拷贝构造,那么d1就无法初始化了,因为拷贝构造也是构造,系统不会默认生成。但是我们也可以强制生成,使用Date()=default;(注意要写在类里面)
2.我们刚才用的是浅拷贝,它把指针的值也拷贝过来了,两个对象指向同一个空间,析构的时候对谁置空(地址),那两个都会被置空,同一个空间不能被释放两次。浅拷贝ok,但是需要动态开辟资源的,比如指针这种的都不行。深拷贝:比如这里,就是开一块相同大小的空间,然后让tmp指向自己独立的空间
3.再说用两个栈做一个队列,也就是我们上面写的,构造不用其自己写,析构也不需要,连拷贝构造也不需要,因为Stack就已经把它们全部做好

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

  1. 拷贝构造函数典型调用场景:
    ①使用已存在对象创建新对象
    ②函数参数类型为类类型对象
    ③函数返回值类型为类类型对象
    为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
    尽量使用引用。

运算符重载

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

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置- 的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出
    现。

我们写一个公有的operator==出来

在这里插入图片描述

再写一个私有的:
在这里插入图片描述

我们在这里没有给大家展示成员变量的情况,实际上,第一个我们是取消了成员变量的private限制,而改成了public,不然我们无法在其他地方使用d1和d2的成员变量;而第二个,由于我们是在类内部写的,所以可以改成private,这样封装性也更好,不会使权限出问题。并且,这个还有两种写法,大家可以多看看。

大家比较疑问的一个问题应该是为什么传递参数变成了一个,在类外的时候命名参数是两个,实际上,我们在类里面,函数是这个样子的:bool operator==(Date* this, const Date& d2),左操作数是隐含的this指针,指向调用函数的对象

赋值运算符重载

  1. 赋值运算符重载格式
    参数类型:const T&,传递引用可以提高传参效率
    返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
    检测是否自己给自己赋值
    返回*this :要复合连续赋值的含义

首先,我们知道,赋值和拷贝构造是两个意思,给大家写一个例子看看:

Date& operator=(const Date& d) {
	if (this != &d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;//防止自己给自己赋值
	}

	return *this;
}

在这里插入图片描述

d2=d1是赋值吗?不是,因为d2原本是未实例化的对象,而d1则是已经创建过的对象,所以这应该是拷贝构造;而d3=d1是赋值吗?答案很明显是的,两个都是已经创建过的对象,所以是把d1的值赋给d3
而返回*this的原因也很简单,因为我们如果使用void,那么就没有返回值,也就无法做到从右到左的连续赋值;而返回引用的原因,依旧是为了不调用拷贝构造,提高程序效率

给大家看一个表达式,大家可以理解一下是怎么做的:(i=j)=10
意思是j先赋值给i,表达式的返回值是i,所以10再赋值给i

在这里插入图片描述
另外大家需要记住,在类里面声明定义的函数会被认定为内联函数,不需要写inline

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

  1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。 注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
    注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现

前置++和后置++重载

// 前置++:返回+1之后的结果
 // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++() {
	(*this) += 1;
	return *this;
}

// 后置++:
 // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
 // C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
自动传递
 // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1
 // 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int) {
	Date tmp(*this);
	*this += 1;
	return tmp;
}

后面我们将从日期类的构建开始讲起,希望大家在看懂这篇文章之后可以继续加油!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值