C++类和对象2

一.类的默认成员函数

类的默认成员函数是用户自己没有显示写出,而编译器会自动生成的成员函数。当需要用到这些函数时,编译器就会自动生成并调用。但是当用户显示写出时,以后再调用就会调用用户写的,编译器不再自动生成。下面就是C++类中的默认成员函数:

二.构造函数

每个类都分别定义了它的对象被初始化的方式,类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

而且,构造函数是在创建对象时自己调用,不需要用户人为调用构造函数。构造函数的功能只有对对象的初始化,没有其他功能。

构造函数的特点:

  • 构造函数的函数名类名相同
  • 构造函数没有返回值,也不返回void,不需要写返回类型
  • 对象实例化时会自动调用构造函数
  • 构造函数可以重载
  • 如果用户没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数,一旦显式定义了,编译器就不再生成
  • 无参构造函数全缺省构造函数,以及编译器生成的无参构造函数都是默认构造函数。默认构造函数在一个类中有且只能出现一个,不可以同时出现。默认构造函数就是不需要传参就可以调用的构造函数,无参显然可以,全缺省不传参就会使用缺省值,所以也可以。
  • 编译器自己生成的构造函数对内置类型的成员变量的初始化是不确定的,由编译器决定。而对自定义类型的成员变量则会调用该类型的默认构造函数。

2.1无参构造函数

无参构造函数是最简单的构造函数之一,它初始化的方式是直接在函数体中对类对象的数据成员进行初始化操作。而无参构造函数之所以可以这样直接引用私有变量是因为隐含的this指针。类的成员函数会隐式的传对象的地址,用this来接收。

因为无参构造函数可以不传参就调用,所以无参构造函数是默认构造函数

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

private:
	int _year;
	int _month;
	int _day;
};

2.2有参构造函数

有参构造函数和无参构造函数构成了函数重载,编译器会根据调用对象时参数的区别来调用不同的构造函数。

有参构造函数就可以自己设置对象的初始化值,不再像无参构造函数那样所有的对象都有相同的初值。

有参构造函数在调用时必须传参,否则就会报错,所以有参构造函数不是默认构造函数。

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

2.3全缺省构造函数

全缺省构造函数即给每个参数一个缺省值,当没有传参数时就会使用缺省值。所以全缺省构造函数也可以不传参使用,也属于默认构造函数

//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}

以上三种构造函数的调用方式:

//无参构造函数
Date d1;
d1.Show();

//带参构造函数
Date d2(2024, 8, 31);
d2.Show();
//全缺省构造函数
Date d3;
Date d4(2024);
Date d5(2024,8);
Date d6(2024,8,31);

无参构造函数和全缺省构造函数都是默认构造函数,一个类中有且只有一个默认构造函数,当两者同时存在时就会产生歧义: 

编译器不知道是该调用无参构造函数还是全缺省构造函数。 

三.析构函数

析构函数的作用和构造函数相反,析构函数是用于在对象销毁时执行清理操作的特殊成员函数。它的名字与类的名字相同前面加上一个波浪线(~)作为前缀并且没有返回值和参数。析构函数的主要作用是执行对象的清理工作,例如释放在对象生命周期中创建的资源,如动态分配的内存、打开的文件等。而一个类中全是内置类型时,就不需要析构函数进行清理工作。

析构函数的格式:

//析构函数
~Date()
{
	//清理操作
}

而我们的Date类中只有内置类型,所以不需要析构函数进行清理,当我们加上一个malloc的成员时,当该对象销毁时,就需要利用析构函数对malloc的资源进行释放操作: 

class Date
{
public:
	//全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//析构函数
	~Date()
	{
		//清理操作
		free(_a);
		_a = nullptr;
	}

	void Show()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
	int* _a = (int*)malloc(sizeof(int));//动态申请的资源
};

析构函数也是在对象销毁时自动调用的。

析构函数的特点: 

  • 析构函数的函数名类名相同,前面有一个~(波浪号)
  • 无参无返回值(与无参构造函数类似)
  • 一个类只能有一个析构函数,当没有显式实现析构函数时,编译器会自己生成一个析构函数。
  • 对象的生命周期结束时,自动调用析构函数
  • 析构函数不可以重载
  • 编译器自动生成的析构函数对内置类型成员变量不做处理,自定义类型的成员变量则调用该类型的析构函数
  • 当一个类没有申请资源时(无动态申请空间或者打开文件等),就不需要显式写析构函数,直接使用编译器自动生成的即可。如果有资源的申请,则必须显式实现析构函数,否则可能会造成内存泄漏。
  • 当一个作用域中定义了多个对象,C++定义后定义的先析构 

 当有资源申请时,通过析构函数就可以回收空间,方式内存泄漏。

 当我们没有显式定义析构函数,系统自动生成的析构函数对内置类型不会有任何操作。


当有多个对象在一个作用域时,它们的构造函数和析构函数的调用顺序遵循:先构造后析构

我们在构造函数和析构函数中加上打印语句来观察这一现象:

我们先后创建了d1,d2,d3,d4这四个对象,每次创建对象时会自动调用构造函数,所以前面四个分别就是d1,d2,d3,d4的构造函数,然后当程序结束时对象就要销毁了,此时调用析构函数,而析构函数的顺序刚好与构造函数相反。 

我们可以通过特殊的对象,以及在构造函数和析构函数打印对应数据来观察。可以很清楚的观察到先构造的后析构,后构造的先析构。

四.拷贝构造函数

拷贝构造函数的第一个参数是同类对象的引用,且任何额外的参数都有缺省值。拷贝构造函数的作用是借助已有的对象对新创建的对象进行初始化。

拷贝构造函数和带参构造函数构成函数重载。拷贝构造函数的格式为:

//拷贝构造函数
Date(const Date& d)
{
	//……
}

调用拷贝构造函数的格式为: 

Date d4(4,4,4);

Date d5(d4);//调用拷贝构造
Date d6 = d4;//调用拷贝构造

拷贝构造函数的特点: 

  • 拷贝构造函数是构造函数的函数重载(函数名相同,形参数量不同或者形参类型不同)
  • 拷贝构造函数的第一个参数必须是同类对象的引用,使用传值调用编译器会直接报错,因为逻辑上会导致无限递归。拷贝构造函数也可以有多个参数,但其他参数必须在右侧,且必须有缺省值
  • C++规定自定义类型对象进行拷贝时要调用拷贝构造,因为传值传参和传值返回都涉及到了对象的拷贝,所以必须调用拷贝构造如果拷贝构造采取传值传参,就会进行值拷贝,调用拷贝构造函数,调用拷贝构造函数时又会发生值拷贝,然后又调用拷贝构造,就会导致无限递归下去。所以拷贝构造必须采用传引用传参。
  • 若没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。该拷贝构造函数对内置类型成员会完成值拷贝/浅拷贝(一个一个字节的完成拷贝),对于自定义类型的成员则会调用其自己的拷贝构造函数。
  • 如果一个类中涉及到了资源的申请,那么就需要显式的定义拷贝构造函数,进行深拷贝,浅拷贝会导致多次析构,反之就不需要显式定义。

当前日期类中有资源申请所以显式定义了拷贝构造,进行深拷贝,不会有问题。但是如果没有显式定义的话就会导致问题: 

我们看到,当我们没有显式定义时,因为进行的是值拷贝,所以导致两个对象指向了同一块动态申请的空间 ,而当程序结束时调用这两个对象的析构函数,就会对同一块空间释放两次,导致出错。

五.运算符重载

5.1运算符重载

运算符重载和函数重载类似,函数重载是同一函数名有不同的功能,而运算符重载是同一个运算符有多种用途,即一物多用。

运算符重载的关键字是operator

例如:加法运算符+本来是用于整型或者浮点型数据的相加,我们可以对+进行重载,让其可以用作自定义类型的相加。还有就是<<和>>流插入和流提取运算符,它们在C++中与cout和cin一起使用代替了C语言中的输入输出语句,而它们也是C语言中位运算符。这也体现了运算符重载的意义。

运算符重载函数的一般格式为:

//运算符重载
//返回值 operator运算符(形参表)
Date operator+(const Date& d1, const Date& d2)
{
	//……
}

运算符重载的特点: 

  • 当运算符被用于类类型的对象时,C++规定必须转换成对应的运算符重载,若没有对应的运算符重载,则会编译报错对于定义的日期类来说,我们并没有对应的+运算符的重载,而直接使用的话就会导致报错。
  • 运算符重载是具有特殊名字的函数,它的函数名由operator和待重载的运算符组成,并且它和普通函数一样也具有返回值和形参表
    Date operator+(const Date& d1, const Date& d2)//加法运算符重载
  • 重载运算符的参数个数和该运算符的操作数个数相同。一元操作符就只有一个参数,二元运算符就有两个参数,且左侧的操作数传给第一个参数,右侧的操作数传给第二个参数
    //d1传给d1,d2传给d2
    Date operator+(const Date & d1, const Date & d2)
    d1 + d2;
    
    //d1传给this,d2传给d
    Date operator+(const Date & d)
    d1 + d2;
  • 如果一元运算符重载为类的成员函数,因为成员函数会有隐式的this指针,所以就不再需要显式传参数了。如果二元运算符重载为类的成员函数,也因为this指针的缘故,只需要传第二个操作数即可。
    //前置++
    //d1传给this
    Date operator++();
    ++d1;
  • 运算符重载之后的运算优先级以及其中内置类型的计算方式不变
  • 不能对原本就不是运算符的运算符进行重载。比如:operator@
  • (.*) (::) (sizeof) (?:) (.),这五个运算符不能重载
  • 重载运算符必须有一个参数是类类型的对象,且不能改变运算符原本对内置类型的操作,比如:将+运算符重载为两个整型相减
  • 一个类重载运算符时仅需重载那些对类有意义的
  • 重载++,--运算符时,前置和后置的函数名是相同的,编译器无法区分到底调用哪一个,所以C++规定,后置++或者--增加了一个int形参,该形参只起区分作用
    Date opertaor++(int);//后置++
    Date operator++();//前置++
  • 重载<<和>>运算符时不能重载为成员函数,否则会导致运算符的顺序改变
    ostream& operator<<(ostream& output, const Date& d)
    {
    	//……
    }
    
    int main()
    {
    	Date d(2024, 9, 2);
    
    	cout << d;
    
    	return 0;
    }

    当这样定义<<操作符时,cout传给第一个参数output,d传给第二个参数d。如果定义成成员函数:

    ostream& operator<<(ostream& output)
    {
    	//……
    }
    
    int main()
    {
    	Date d(2024, 9, 2);
    
    	//cout << d;//错误
    
    	d << cout;//正确
    
    	return 0;
    }

    当我们定义成成员函数时,类对象肯定要传给this,所以要将ostream& output显式写出来。第一个操作数传给this,第二个操作数传给output,为了类型的对应,所以只能d<<cout;虽然这样也可以实现,但是使用方式和正常操作有区别,况且这样不方便连续的输出

对于单目运算符重载时建议重载为成员函数,这样就不需再传参;双目运算符即可以重载为成员函数也可以重载为全局函数借助友元friend关键字来实现对私有成员变量的访问;流插入和流提取运算符只能重载为全局函数,借助友元friend来实现访问。

 对于日期类来说,我们重载加法运算符对两个日期相加是没有意义的,但是我们可以重载==,判断两个日期是否相等,这是有意义的。

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

因为该运算符是双目运算符,所以这里重载成了全局函数,借助friend声明在了类中。 

5.2流插入和流提取运算符重载

流插入(<<)和流提取(>>)运算符再重载时与别的运算符有区别,流插入运算符重载的第一个参数和返回值都是ostream类对象的引用,而流提取运算符重载的第一个参数和返回值都是istream类对象的引用。返回值是为了可以连续的输入和输出。

 类中声明:

ostream& operator<<(ostream& output, const Date& d)
{
	output << d._year << "/" << d._month << "/" << d._day << endl;

	return output;
}
istream& operator>>(istream& input, const Date& d)
{
	cout << "请依次输入年月日:->";
	cin >> d._year >> d._month >> d._day;

	return input;
}

流插入运算符和流提取运算符的第一个参数是其对应流的对象,这里传引用是为了减少拷贝,增加运行效率。返回值返回流对象的引用是为了实现连续的输入或者输出。

对于流提取运算符来说,他的第一个参数是cin,第二个参数是d1对象,然后调用流提取运算符重载,结束之后返回了流对象的引用也就是cin,又与d2结合构成另一个输入语句。 

cout语句与cin语句类似,每打印一个对象之后就会返回cout对象的引用,以此来实现连续输出。 

六.赋值运算符重载

赋值运算符重载是一个默认成员函数,用于两个已经存在的对象之间的直接赋值,这里要注意和拷贝构造区分,拷贝构造是在一个对象创建的时候进行的初始化操作

赋值运算符是二元运算符,但是C++规定必须重载为成员函数: 

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

	return *this;
}

调用方式:

Date d1(2024,9,3);
Date d2;

//d1和d2都已经存在,所以调用赋值运算符重载
d2 = d1;

Date d3;
Date d4;
Date d5;

d5 = d4 = d3 = d1;//可以连续赋值

Date d6 = d1;//不是赋值运算符重载,而是拷贝构造,d6创建时完成初始化操作

赋值运算符重载的特点:

  • 赋值运算符重载是一个运算符重载函数,归档必须重载为成员函数。赋值运算符重载的参数建议写成const当前类对象的引用,可以减少拷贝,否则传值传参会产生拷贝
  • 有返回值,建议写成当前类对象的引用,可以减少拷贝,提高效率,有返回值是为了连续赋值的场景(同流插入流提取)
  • 没有显式实现赋值运算符重载时,编译器会自动生成一个默认赋值运算符重载,该默认函数和默认拷贝构造类似,对内置类型数据成员进行值拷贝/浅拷贝 ,对自定义类型则调用其赋值运算符重载
  • 当一个类的数据成员不涉及资源的申请时,不需要显式写出该函数,默认生成的即可完成要求。但当类中含有资源的申请时,就需要显示写出赋值运算符重载,否则会发生析构函数对同一块空间的多次释放

七.const成员函数 

将类中的成员函数用关键字const修饰后,就称为const成员函数,const放在形参表的后面,格式为:返回类型 函数名(形参表)const。

const成员函数的特点是:

  1. const成员函数可以访问对象的成员变量,但不能修改它们的值。

  2. const成员函数可以调用其他的const成员函数,但不能调用非const成员函数。

  3. const成员函数可以被非const对象和const对象调用。

  4. const成员函数在编译时会进行一定的优化,提高程序的执行效率

当一些成员函数的功能只是打印数据等不会修改数据成员,则就可以将其定义为const类型的对象 :比如日期类中的show()函数,不涉及数据成员的修改,所以可以将其定义为const成员函数。

void Show() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

非成员函数不可以被const修饰。 

const对象和非const对象都可以调用const成员函数,其本质分别是权限平移和权限缩小。权限可以平移和缩小,但是不可以放大。比如, const对象不可以调用非const成员函数

Date d1(2024, 9, 3);
d1.Show();

const Date d2(2024, 9, 1);
d2.Show();

show1()是普通成员函数,const对象不可以调用普通成员函数。 

八.取地址运算符重载

取地址运算符(&)是C++中的一个重要运算符,用于获取变量的地址。在C++中,可以通过重载运算符来定义用户自定义类型的取地址运算符行为。

重载取地址运算符的格式如下:

返回类型 operator& ();

重载取地址运算符必须是类的成员函数,因为取地址运算符必须作用于对象上。

Date* operator&()
{
	return this;
}

const Date* operator&() const
{
	return this;
}

取地址运算符可以重载为普通取地址运算符重载和const取地址运算符重载,而这两个编译器自动生成的就已经够用了,我们一般不需要显式实现。

当我们不想被取出类对象的地址时,我们可以显式实现:

Date* operator&()
{
	//return this;
	return nullptr;
}

const Date* operator&() const
{
	//return this;
	return nullptr;
}

需要注意的是,在重载取地址运算符时,我们返回的是一个指向类对象的指针,而不是对象本身的地址。这是因为取地址运算符的返回值必须是指针类型。

此外,由于取地址运算符必须作用于对象上,所以重载取地址运算符的类成员函数不能是静态成员函数。

九.传值传参和传引用传参

传值传参和传引用传参的区别就在于传值传参会调用拷贝构造,创建一个临时变量,而传引用传参形参和实参占据同一块空间,不会生成临时变量,效率更高。

十.传值返回和传引用返回

传值返回和传引用返回的区别在于传值返回会发生拷贝,要调用拷贝构造,产生消耗,而传引用返回不需要拷贝,所以效率更高。但是需要注意的是,传引用返回,必须是返回的对象不会随着函数的结束而销毁,否则就会返回野引用(类似野指针)。所以当返回对象不会销毁时就可以采用传引用返回来提升效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值