类和对象(二)

类的6个默认成员函数

如果一个类中声明成员都没有,我们简称为空类。但是空类中并不是真的什么都没有,即使我们什么都不写,类中也会自动生成6个默认成员函数。

class Date();//空类

注意:默认成员函数其实就相当于缺省成员函数。如果不写就会编译器默认生成的,写了编译器就不会生成。

一、构造函数

1.构造函数的概念

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

例.一个Date日期类,当创建Date日期类对象时,编译器就会自动调用其对应的构造函数(自己写的或编译器默认生成的)初始化新创建的类对象,初始化类内的变量。

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

注:构造函数是初始化对象,而不是开辟空间。

2.构造函数的特性

一、构造函数函数名与类名相同

二、构造函数无返回值

区别于void无返回值,构造函数无返回值是真正的无返回值。

三、对象实例化时编译器会自动调用其对于的构造函数

当创建类对象时,编译器会自动调用该类的构造函数对新创建的类的变量进行初始化。

四、构造函数支持重载

意味着初始化对象有多种方式,和普通的函数重载一样,编译器会根据你传递的参数去调用对应的构造函数。

五、无参的构造函数、全缺省的构造函数 以及 我们不编写,编译器自动生成的构造函数 都称为默认构造函数,并且默认构造函数只能有一个

以下三种都叫做默认构造函数:

  1. 我们不写,编译器自动生成的构造函数。
  2. 我们自己写的无参构造函数
  3. 我们自己写的全缺省构造函数

总之,无需传参就可调用的构造函数叫做默认构造函数

六、如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数。若用户定义了,则编译器不再生成。

编译器默认生成的无参构造函数是有缺陷的,该构造函数只会去自动调用类里变量中的其他类对象的默认构造函数初始化,而不会对内置类型变量进行初始化。所以一般还是需要我们自己写构造函数的。

例.如下Date日期类,没有自己写构造函数,而是由编译器生成的默认构造函数来初始化类对象。

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


class Date2
{
public:
	void Print_built_in_type()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
    
    void Print_class_type()
    {
        d1.Print();
	}
private:
	int _year;
	int _month;
	int _day;
    Date1 d1;
};
void test_class_character()
{
	Date2 d2; // 编译器将调用自动生成的默认构造函数对d1进行初始化
	d1.Print_neizhi();
}

结果是Date2类对象 d2中的内置变量都被初始化成了随机值。而Date2类对象 d2中的其他类的变量却调用了自己的默认构造函数初始化了自己的内置变量。

编译器自动生成的构造函数的机制:

  1. 编译器自动生成的构造函数对内置类型不做处理。
  2. 对于自定义类型,编译器会再去调用它们自己的默认构造函数。若没有默认构造函数,那也不会去初始化了。

我们需要的构造函数的机制:

1.需要自行对内置类型初始化。(要我们自己写)

2.对于自定义类型,会去调用它们自己的默认构造函数。(自带)

总结:我们不写构造函数的情况下,编译器会自动生成默认构造函数,但编译器生成的默认构造函数可能不会达到我们想要的效果,所以大多数情况下是需要我们自己写构造函数的。

二、析构函数

1.析构函数的概念

**析构函数:**与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。


类对象内部的变量会随着其销毁而被编译器自动销毁

在一个类对象生命周期结束后或出了作用域后,类对象会销毁,其中的类内部的非动态开辟的变量(非动态开辟的局部变量)会随着该对象的销毁而自动销毁。

例如,用日期类创建一个d1,在d1被销毁的时候,其内部的_day, _month, _year这些变量也会被编译器销毁。

那为什么还需要析构函数呢?

在只有内置变量的类对象中,确实可以不需要析构函数,在该对象被销毁时,编译器会自动销毁其内部的非动态开辟的内置变量。

但如果该类的内部还有动态开辟的变量的话,在该类对象销毁的时候,编译器就不会自动帮我们释放了,可能就会导致内存泄露了。

这时就需要我们自己对其空间进行释放了,此时就体现析构函数的意义了。——释放动态开辟的变量的空间。


2.析构函数的特性

一、析构函数的函数名是在类名前加上字符‘~’

class Date
{
public:
	Date()// 构造函数
	{}
	~Date()// 析构函数
	{}
private:
	int _year;
	int _month;
	int _day;
};

二、析构函数无参数,无返回值

析构同构造函数一样,都是区别于void的的真正无返回值。

三、栈上的类对象生命周期结束时,C++编译器会自动调用析构函数

这样可以大大降低在C语言中的忘记释放动态开辟的内存时,而导致的内存泄露问题。当栈对象声明周期结束时(如果是在堆上动态开辟的对象的话,对象销毁时并不会调用析构函数,需要自己手动调用),C++编译器会自动调用析构函数对其栈空间进行释放。

如下:

void test_Destructor()
{
	Date* d1 = new Date;//堆上动态开辟的对象销毁时不会自动调用析构函数
	delete d1;

	Date d2;//栈上开辟的对象销毁时会自动调用析构函数
}

四、一个类有且只有一个析构函数。若未显式定义析构函数,系统会自动生成一个默认析构函数。

编译器自动生成的析构函数的机制:

  1. 编译器自动生成的析构函数对内置类型不做处理。而是在对象销毁时,编译器会自动销毁其内置的非动态开辟的变量,动态开辟的不会释放。
  2. 对于自定义类型,编译器会再去调用它们自己的默认析构函数。

我们需要的析构函数的机制:

  1. 不需要对非动态开辟的内置类型处理,在类对象销毁时,编译器会自动销毁非动态开辟的对象。(自带)
  2. 需要我们自行释放动态开辟的内置变量。(要我们自己写)
  3. 对于自定义类型,会去调用它们自己的默认析构函数。(自带)

五、先构造的后析构,后构造的先析构

和入栈出栈类似,先进后出,后进先出。

void test_Destructor_order()
{
	Date d1(1,2,3);//先构造,先入栈
	Date d2(3,4,5);//后构造,后入栈
}

三、拷贝构造函数

1.拷贝构造函数的概念

**拷贝构造函数:**只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已经存在的类对象去 创建/构造 新的对象时,由编译器自动调用的构造函数,叫做拷贝构造函数。

class Date
{
public:
	Date(int year = 0, 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;
};
void test_copy()
{
	Date d1(2021, 5, 31);
	Date d2(d1); // 用已存在的对象d1创建对象d2
}

2.拷贝构造函数的特性

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

拷贝构造函数的函数名和构造函数的函数名是一样的,只有参数不一样。

二、拷贝构造函数的参数只有一个,且必须使用引用传参,用传值传参的话会引发无穷的递归调用

要拷贝构造就需要先传参,若传参使用传值传参的,那么在传参的过程中又需要进行对象的拷贝构造,在要实现的拷贝构造里面使用了拷贝构造,这样就引发无穷的递归调用,就会陷入死循环。

ps:在其他函数对自定义类型对象进行传参时,推荐使用引用传参。使用传值传参也是可以的,不过会调用拷贝构造函数。

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

void test_copy_character()
{
	Date3 d1(2021, 5, 30);
	Date3 d2(d1); // 用已存在的对象d1创建对象d2
	d1.Print();
	d2.Print();
}

三、代码中,没有自己写拷贝构造函数,编译器自动生成默认的拷贝构造函数。

四、自动生成的拷贝构造与自己写的拷贝构造函数的区别:

  • 编译器自动生成的拷贝构造函数机制:
  1. 编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)。
  2. 对于自定义类型,编译器会自动去调用它们自己默认的构造函数。
  • 我们需要的拷贝构造函数的机制:
  1. 根据使用场景,自己规定拷贝构造函数是浅拷贝(值拷贝),还是深拷贝。(要我们自己写)
  2. 对自定义类型,自动去调用它们的自己的默认构造函数。(自带)

五、编译器自动生成的拷贝构造函数不能实现深拷贝

编译器自动生成的默认构造函数不能实现深拷贝,只能实现浅拷贝。如下

  • 内置类型的浅拷贝
Date d2(d1);//用已存在的对象d1去 创建/构造 d2

内置类型的浅拷贝是可行的。

  • 自定义类型的浅拷贝

自定义类型如下:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_ps = (int*)malloc(sizeof(int)* capacity);
		_size = 0;
		_capacity = capacity;
	}
	void Print()
	{
		cout << _ps << endl;// 打印栈空间地址
	}
private:
	int* _ps;
	int _size;
	int _capacity;
};

自定义类型浅拷贝如下:

Stack s1;
Stack s2(s1);//用已存在的s1去 创建/构造 s2

自定义类型的浅拷贝是不可行的。

**不可行之一:**代码中,本意是用已经存在的对象s1去 创建/构造 s2,但编译器自动生成的默认拷贝构造函数,完成是浅拷贝,拷贝出来的对象s2的内部的动态开辟的空间的变量和s1的内部的动态开辟的空间的变量是共用一块内存空间的。修改s1或s2任一方的该动态开辟内存的变量,都会影响另一方的变量,因为他们共用同一块位置的内存,则自定义类型的浅拷贝是不可行的。

**不可行之二:**由于对象s1和对象s2用的栈空间是同一块,所以若其析构函数会释放内存空间的话,那么将会对同一块内存空间释放两次,这样就会报错。

例.如下

void test_copy_error()
{
    Stack s1;
	s1.Print();// 打印s1栈空间的地址
	Stack s2(s1);// 用已存在的对象s1创建对象s2
	s2.Print();// 打印s2栈空间的地址
}

结果打印s1和s2栈空间的地址相同,这就意味着,在创建完s2栈后,我们对s1做的任何操作都会直接影响到s2栈。并且s1和s2的析构函数若释放动态开辟的空间资源的话,会对同一块内存空间释放两次。

在这里插入图片描述

这样是不可取的。所以就要我们自己实现深拷贝的栈。

总结:

  1. 像Date这样的类,需要的就是浅拷贝,此时编译器生成的默认拷贝构造函数就够用了。
  2. 像Stack这样的类,需要的是深拷贝,浅拷贝会导致两次析构时对同一块内存释放两次,导致程序崩溃问题,此时就需要我们自己写深拷贝的拷贝构造函数。

四、赋值运算符重载

1.运算符重载

C++为加强代码的可读性,引入了赋值运算符重载,运算符重载是特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样,可以直接使用运算符进行操作。

  • 让自定义类型和内置类型一样:
d1 == d2;// 可读性高(书写简单)
IsSame(d1, d2);// 可读性差(书写麻烦)

运算符重载函数也具有自己的返回值类型,函数名以及参数列表。其返回值类型和参数列表与普通函数类似。

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

  • 运算符重载函数原型如下:
返回值 operator 运算符(参数列表);

注意:

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

2.重载操作符必须有一个类类型或枚举类型的操作数。

3.用于内置类型的操作符,重载后其含义不能改变。

4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。

5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载。

  • 例.重载==运算符

实现在内部:

将重载==运算符函数作为类的一个内部成员函数,就可以限定第一个形参为this指针了。

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
	bool operator==(const Date& d)// 运算符重载函数
	{
		return _year == d._year
			&&_month == d._month
			&&_day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void test_calculate_charater_overloading()
{
	Date5 d1(2002, 6, 16);
	Date5 d2(2002, 6, 16);
	Date5 d3(2003, 6, 16);
	cout << (d1 == d2) << endl;
	cout << (d1 == d3) << endl;
}

实现在外部:

也可以将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public)或者将运算符重载函数设置成友元函数,这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
	int _year;
	int _month;
	int _day;
};
bool operator==(const Date& d1, const Date& d2)// 运算符重载函数。内置类型调用默认的==,Date类型调用该重载的==。
{
	return d1._year == d2._year
		&&d1._month == d2._month
		&&d1._day == d2._day;
}

void test_calculate_charater_overloading()
{
	Date d1(2002, 6, 16);
	Date d2(2002, 6, 16);
	Date d3(2003, 6, 16);
	cout << (d1 == d2) << endl;
	cout << (d1 == d3) << endl;
}

2.赋值运算符重载

  • 例.=运算符重载
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)// 构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator=(const Date& d)// 赋值运算符重载函数
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	void Print()// 打印函数
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

重载赋值运算符需要注意以下几点:

一、参数类型设置为引用,并用const进行修饰

保证传进来赋值的变量,不能被修改,我们也不想修改。所以要用const修饰

二、函数的返回值使用引用返回
保证了可以连续赋值,d1=d2=d3;从右向左赋值。d2=d3赋值完后,返回了d2,所以d1=d2又可以继续执行。

三、赋值前检查是否是给自己赋值
 若是出现d1 = d1,我们不必进行赋值操作,因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值,避免不必要的赋值操作。

四、引用返回的是*this
 赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。

五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值(浅)拷贝
 没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,当遇到一些特殊的类,我们还是得自己动手写赋值运算符函数的。

Date6 d1(2021, 6, 1);//直接构造
Date6 d2(d1);//拷贝构造
Date6 d3 = d1;//拷贝构造,相当于Date6 d3(d1)

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?

​ 其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:

​ **拷贝构造函数:**用一个已经存在的对象去构造初始化另一个即将创建的对象。

​ **赋值运算符重载函数:**在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

五、const成员

1.const修饰类的成员函数

const修饰类的成员函数
 我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改,即不可以对其成员变量进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:

void Print()const// cosnt修饰this指针的打印函数
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

思考下面几个问题(经典面试题):

  1. const对象可以调用非const成员函数吗?(const对象有被修改的风险)
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其他的非const成员函数吗?(const对象有被修改的风险)
  4. 非cosnt成员函数内可以调用其他的cosnt成员函数吗?

答案是:不可以、可以、不可以、可以
解释如下:
 1.非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,const对象有被修改的风险。函数调用失败。

2.const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功。

3.在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败。

4.在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功。

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

自己不写编译器会自动生成

class Date
{
public:
	Date* operator&()// 取地址操作符重载
	{
		return this;
	}
	const Date* operator&()const// const取地址操作符重载
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值