类和对象(下篇)

目录

1.explicit关键字

1.1 强制类型转化和隐式类型转化

1.2 explicit应用

1.3 C++11多参数构造

2.Static成员

3.友元

3.1友元函数

3.2友元类

4.内部类

5.匿名对象

6.拷贝对象时编译器所做的优化

7.时间类的实现


1.explicit关键字

1.1 强制类型转化和隐式类型转化

在C语言中,其实我们早已经遇到过类似的概念

比如说下面的代码,就是隐式类型转化的典型例子.

j是浮点double类型,如果要赋值给i,编译器会创建一个临时的int类型变量,将j的值赋给该临时变

量,再赋值给i.

#include <iostream>
using namespace std;
int main()
{   
    //隐式类型转化
	int i = 1;
	double j = 2.0;
	i = j;
	cout << i << endl;
	return 0;
}

 又或者说我们当时C语言用malloc开辟空间,返回的是void*类型,此时一般需要强制类型转化.

#include <iostream>
using namespace std;
int main()
{   
    //强制类型转化
	int* tmp = (int*)malloc(sizeof(int) * 4);
	return 0;
}

总的来说,隐式类型转化和强制类型转化都有其运用的场景,合理使用,可以带来奇效,当然也可能带来巨大的隐患. 

1.2 explicit应用

构造函数不仅可以初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用

比如说下面的代码,我们先用2018作为单参数构造出d1对象.

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
class Date
{
public:
	Date(int year)
		:_year(year)
	{}
	void Print()
	{
		std::cout << _year <<std::endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2018);
    //隐式类型转化
    d1 = 2019;
    d1.Print();
	return 0;
}

对于2019,是一个int类型的变量,编译器会先构造出一个临时变量,是Date自定义类型的,然后

再用这个临时变量调用赋值函数,更新我们的d1对象.

还有下面这种情况的单参数构造函数也属于隐式类型转化

编译器会先用1构造出Date类型的临时对象(隐式类型转化),再拷贝构造给aa2对象.

// 单参数的构造函数
class A
{
public:
	A(int a)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a1;
};

int main()
{
	// 单参数构造函数 C++98
	A aa1(1);  // 构造函数
	// 隐式类型转换   构造+拷贝+优化->构造
	A aa2 = 1;

	return 0;
}

但是从结果上来看,并没有调用拷贝函数,这是为什么呢?

因为现在的编译器已经非常智能,能够识别出来,直接用1调用拷贝函数,构造对象aa2.

那如何验证的的确确是存在隐式类型转化这种现象呢?

我们可以用下面这段代码验证,由于是引用,就不需要再赋值,而是直接创建一个临时变量,ref

是它的别名.

而用10直接构造的这个临时变量,具有常性,权限可以缩小但不能扩大,如果要引用,就需要const修饰.

// 单参数的构造函数
class A
{
public:
	A(int a)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a1;
};

int main()
{    
    //不加const修饰,程序报错
    //A& ref = 10;	
    //加了const修饰后,程序正常运行
    const A& ref = 10;
	return 0;
}

上面这样的代码,在可读性上大大比较低,因此C++引入了explicit关键字,只要在函数前面加上,就可以禁止隐式转化这种方式.

// 单参数的构造函数
class A
{
public:
	explicit A(int a)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a2;
	int _a1;
};

int main()
{
	// 单参数构造函数 C++98
	A aa1(1);  // 构造函数
	// 隐式类型转换   构造+拷贝+优化->构造
	A aa2 = 1;

	return 0;
}

  

1.3 C++11多参数构造

C++11现在也支持多参数的隐式类型转化,编写的方式和结构体初始化的方式类似.

// 多参数的构造函数
class A
{
public:
	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{}

	A(const A& aa)
		:_a1(aa._a1)
		,_a2(aa._a2)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a2;
	int _a1;
};

int main()
{
	// 多参数构造函数 C++11
	//多参数构造
	A aa2(1, 1);
	//多参数隐式类型转化
	A aa3 = { 2, 2 };
	const A &ref = { 2, 2 };

	return 0;
}

2.Static成员

我们先来看一道题,实现一个类,计算程序中创建出了多少个类对象

一个简单的想法,就是创建一个全局变量count,在每个构造成员函数里面,每次调用成员函数的时候,就count++. 

PS:由于std类中含有count命名的函数,所以直接命名空间全部展开,会发生程序报错,这里采取部分展开.

int count = 0;
using std::cout;
using std::endl;
class A
{
public:
	A(int a = 0)
	{
		++count;
	}

	A(const A& aa)
	{
		++count;
	}
};

void func(A a)
{}

int main()
{
	A aa1;
	A aa2(aa1);
	func(aa1);
	A aa3 = 1;

	cout << count << endl;

	return 0;
}

构造对象aa1,拷贝构造aa2,调用func函数,拷贝构造临时变量a,优化构造对象aa3.

总计构造4个类对象.

但是这样的实现方式很挫,如果在函数外,又直接对count操作,那输出的结果可能就有问题.

这时候,C++就引入了一个叫做静态成员的概念. 

静态成员是属于整个类的,是属于所有的对象.

存放在静态区

就像小区里面的运动设施,是小区成员所共有的,能够一起使用的.

访问静态成员有两种方式,一种是类名::静态成员 或者 对象.静态成员

这个也很好理解,由于静态成员是属于类,属于所有对象,所以只要能够突破类域,告诉编译器去哪里找该成员,都可以实现访问.

换言之,用类A的指针,也是可以的.

代码可以修改为这样

using std::cout;
using std::endl;
class A
{
public:
	A(int a = 0)
	{
		count++;
	}

	A(const A& aa)
	{
		count++;
	}

	//类里面声明
	static int count;
};
//类外定义
int A::count = 0;

int main()
{
	A aa1;
	A aa2(aa1);
	A* ptr = nullptr;

	cout << A::count << endl;
	cout << aa1.count << endl;
	cout << ptr->count << endl;


	return 0;
}

但是静态成员并不是私有的,我们将其放开了,如果加上private进行修饰,那程序会发生报错.

换言之,静态成员也是类的成员,是受public、protected、private 访问限定符的限制

为了解决这个问题,配套给出了静态成员函数的概念.

在成员函数前面加上static修饰,就能够成为静态成员函数,它没有隐藏的this指针,不

能访问任何非静态成员,只为静态成员服务.

并且使用上,不需要创建对象,只需要类,就可以访问.

class A
{
public:
	A(int a = 0)
	{
		count++;
	}

	A(const A& aa)
	{
		count++;
	}

	static int GetCount()
	{
		return count;
	}
private:
	//类里面声明
	static int count;
};
//类外定义
int A::count = 0;
void f1()
{
	A aa1;
	A aa2(aa1);
	A aa3 = 1;
	A aa4[10];
}
int main()
{
	f1();
	cout << A::GetCount() << endl;
	return 0;
}

3.友元

在类外部编写的函数,是无法访问类里面私有的成员变量.

那是否有方法是可以解决这个问题的呢?

C++提供了友元的概念,它能够突破封装.(但友元不能多用,能少用就少用,毕竟它会增加耦合度,破坏封装)

3.1友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,并且声明时需要加friend关键字

PS:可以在类里面任何一个地方声明.

比如说我们需要实现Date类对象,也能调用流插入和流输出的函数.

假如在类内部直接编写的话,会出现一个问题,就是类内部成员函数默认第一个参数是this指针,并且我们无法对其直接进行修改.

我们对流插入和流输出进行重载的话,默认左边的参数就是*this,所以使用上会非常怪异.

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day;
		return _cout;
	}
	istream& operator>>(istream& _cin)
	{
		_cin >> _year;
		_cin >> _month;
		_cin >> _day;
		return _cin;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
    //d需要放置在左边
    d >> cin;
    d << cout << endl;
	return 0;
}

但我们实际上常规使用,并不是这样,我们想要的是cout << d,d >> cin这种形式.

所以我们想到把函数实现放在外面,但又需要访问类里面的私有成员,因此我们用友元函数进行实现.

即在函数前面加friend关键字修饰,就可以在类外部,访问类里的所有成员,包括私有和保护成员.

PS:友元函数参数中没有this指针,更谈不上this指针解引用等说法.

#include <iostream>
using namespace std;
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

补充:

1.友元函数的作用就是使其可以突破封装,在类外也能访问私有和保护成员,因此不可以用const进行修饰
2.一个函数可以是多个类的友元

3.2友元类

既然我们可以在类里面声明友元函数,自然而然的一个想法,就是可不可以在类里面声明友元类呢?

答案是可以的.

比如说下面这段程序,Date类在Time类里声明为其友元类,因此我们可以在Date类里面访问Time类内私有的成员,比如hour,minute,second.

class Time {
	//Date类是Time的友元,因此在Date类内部可以访问Time类的私有和保护成员
	friend class Date;
public:
    Time(int hour = 1,int minute = 1,int second = 1)
		:_hour(hour)
		,_minute(minute)
		,_second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date {
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

PS:

1.友元类不具有传递性

A是B的友元,也是C的友元,B和C不是友元关系

2.友元类是单向的关系

类似于我们关注明星,我们可以获取明星的相关信息,但他不能够获取我们的相关信息

同样地,友元类可以访问你的私有成员变量,但是反过来则不行. 

4.内部类

在类里面定义的类,我们称为内部类.

比如说我们在A类里面定义B类,B类就是内部类,A类就是外部类.

#include <iostream>
using namespace std;
class A
{
public:
	class B //B是A的内部类
	{
	public:
		void f1(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
private:
	static int k;
	int h;
};

int A::k = 1;

它主要带来下面几个特性:

1.B天生就是A的友元,即B可以访问A的私有和保护成员(还包括A类的static修饰的成员)

2.sizeof(A)计算的是类A的大小,和B无关

3.两者相互独立,只是内部类受外部类的类域限制,比如创建B类对象,需要A::B b1.

5.匿名对象

在构造对象的时候,我们曾经说过,下面这种构造方式是不被允许的.

因为我们无法区分这究竟是调用函数还是构造A类型的对象.

#include <iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main()
{   
	//error
	A a();
	return 0;
}

但是下面这种方式是可以的,也就是说编译器允许我们构造一个没有名字的对象.

int main()
{   
	//correct
	A();
	return 0;
}

它的特点就一个,它的生命周期只有这一行,一旦到下一行,这个对象就会调用析构函数被销毁.

看起来好像没什么用,实际上在某些场景上还是很实用的.

比如说下面的例子,我们调用Solution类里面的函数输出结果,其实不需要单独构造一个对象,直接用匿名对象输出就可以了.

class Solution {
public:
	int Sum_Solution(int n) {
		//...
		return n;
	}
};

int main()
{   
	//Solution s;
	//cout << s.Sum_Solution(10) << endl;
	cout << Solution().Sum_Solution(10) << endl;
	return 0;
}

6.拷贝对象时编译器所做的优化

现在的编译器比较智能,会对我们编写的程序做一定优化,我们可以用代码简单验证一下.

我们先构造一个A类,并自己实现它的构造,析构,拷贝构造等函数,并且在每次调用的时候,输出相应的提示,以便我们的观察.

class A
{
public:
	//构造函数
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	//拷贝构造
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
	//赋值重载
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;

		if (this != &aa)
		{
			_a = aa._a;
		}

		return *this;
	}
	//析构函数
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

我们先来看第一组测试案例

void func1(A a)
{
}
int main()
{
	A aa1 = 1;   //1
	func1(2);    //2
	func1(A(3)); //3
	return 0;
}

 按照我们预期的想法

1,2,3组都应该是用1先构造A类型的临时对象,再拷贝构造

但按照结果输出来看,全部都是直接调用了构造函数

1.隐式类型,连续构造+拷贝构造->优化为直接构造(A aa1 = 1;)

2.一个表达式中,连续构造+拷贝构造->优化为一个构造  (func1(A(3));)

我们再来看第二组案例

A func3()
{
	A aa;
    //构建对象返回
	return aa; 
}

A func4()
{
	//匿名对象返回
	return A();
}

int main()
{
	A aa1 = func3(); 
	cout << "---------------------------" << endl;
	A aa2 = func4(); 

	return 0;
}

按照我们预期的想法,两者都应该是先构造一个对象,然后再拷贝构造给一个临时变量,然后再通过这个临时变量,拷贝给对象aa1和aa2.

但按照结果输出来看,我们可以发现,它会被优化为先构造,然后一次拷贝构造.

对于匿名对象直接返回,它甚至直接调用构造函数.

3.一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造(A aa1 = func3();)
4.匿名对象返回,构造+连续拷贝构造->优化为直接构造(A aa2 = func4();)

对象返回总结:

1.接受返回值对象,尽量拷贝构造方式接收,不要赋值接受

2.函数中返回对象时,尽量返回匿名对象.

7.时间类的实现

在了解前面有关类和对象的知识后,我们可以简单实现日期类.

日期类可以支持日期与整数相加减,日期与日期相减等操作.

具体代码如下:

//Date.h
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
class Date {
public:
	//全缺省构造函数
	Date(int year = 2003, int month = 1, int day = 1);
	void Print()const;
	//根据年和月,得到对应的天数
	int GetMonth(int year, int month) const;

	//运算符重载
	bool operator==(const Date& d)const;
	bool operator!=(const Date& d)const;
	bool operator<(const Date& d)const;
	bool operator<=(const Date& d)const;
	bool operator>(const Date& d)const;
	bool operator>=(const Date& d)const;

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

	// d1 - 100
	Date operator-(int day)const;

	// d1 - d2;
	int operator-(const Date& d)const;

	// ++d1
	Date& operator++();
    // d1++
	// int参数 仅仅是为了占位,跟前置重载区分
	Date operator++(int);
	//--d1
	Date& operator--();
	//d1--
	Date operator--(int);
private:
	int _year;
	int _month;
	int _day;
};

看上去需要实现的代码非常多,但实际上,我们可以换一个思路

假如我们实现了==, <的运算符重载,实际上<=,>,>=,!=等运算符都可以赋用==,<的运算符重载实现.

//Date.c
#include "Date.h"

int Date::GetMonth(int year, int month) const
{   
	assert(month > 0 && month < 13);
	int MonthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	//是闰年的2月,返回29
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		return 29;
	else
		return MonthArray[month];
}

//声明时已经给了缺省参数,则定义不需要再给
Date::Date(int year, int month, int day)
{   
	//月份必须在1到12之间,天数必须符合1到该月的最大天数
	if ((month > 0 && month < 13) && (day > 0 && day <= GetMonth(year,month)))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		perror("Create fail.");
		exit(-1);
	}
}

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

//判断两个日期是否相同
bool Date::operator==(const Date& d)const
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

//判断两个日期谁比较大
//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;
//	}
//	else
//	{
//		return false;
//	}
//}

//判断两个日期谁比较大
bool Date::operator<(const Date& d)const
{
	return (_year < d._year) 
		|| (_year == d._year && _month < d._month)
		|| (_year == d._year && _month == d._month && _day < d._day);
}

//实现判断d1 <= d2
bool Date::operator<=(const Date& d)const
{
	return *this == d || *this < d;
}

//实现判断d1 > d2
bool Date::operator>(const Date& d)const
{
	return !(*this <= d);
}

//实现判断d1 >= d2
bool Date::operator>=(const Date& d)const
{
	return !(*this < d);
}

//实现判断d1 != d2
bool Date::operator!=(const Date& d)const
{
	return !(*this == d);
}

//实现原日期与天数相加
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		*this -= (-day);
		return *this;
	}

	_day += day;
	while (_day > GetMonth(_year, _month))
	{
		_day -= GetMonth(_year, _month);
		_month++;
		//判断是否需要跨年
		if (_month == 13)
		{
			_month = 1;
			++_year;
		}
	}

	return *this;
}

//实现新日期与天数相加
Date Date::operator+(int day)const
{
	//拷贝构造新日期
	Date tmp(*this);
	tmp += day;
	return tmp;
}

//++d1
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

// d1++
// int参数 仅仅是为了占位,跟前置重载区分
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

//实现原日期和天数相减
Date& Date::operator-=(int day)
{
	//如果天数是负数,本质上是日期和天数相加
	if (day < 0)
	{
		*this += (-day);
		return *this;
	}
	else
	{
		_day -= day;
		while (_day < 0)
		{   
			//要先判断是否需要跨年
			_month--;
			//等于0的时候,就需要调整
			if (_month == 0)
			{
				_month = 12;
				_year--;
			}
			//由于前面已经调整好相应的月份,所以这里的_month就不需要再--
			_day += GetMonth(_year, _month);
		}
	}
	return *this;
}

//实现新日期和天数相减
Date Date::operator-(int day)const
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}

//实现两个日期相差天数
int Date::operator-(const Date& d)const
{
	//常规套路,由于无法确定哪个天数更大,先假设
	Date max = *this;
	Date min = d;
	//设立哨兵位,初始值为1
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n * flag;
}

Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

// d1-- -> d1.operator--(1)
Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;

	return tmp;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值