类和对象(下)

类和对象(下)

本节内容
  • 再谈构造函数
  • C++11的成员函数初始化新玩法
  • 友元
  • Static成员
  • 内部类
  • 再次理解封装
再谈构造函数
构造函数体赋值
  • 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
  • 构造函数最主要是用来完成对象的初始化的工作的,但是我们在构造函数函数体内进行的赋值操作虽然说也可以把给参数的值放到对象里面去,但是构造函数函数体里面不是初始化,因为是赋值操作,那么我们如何来判断构造函数函数体里面是赋值还是初始化呢?我们可以使用一个在定义的时候必须进行初始化的东西来判断构造函数函数体里面到底是赋值还是初始化。那么很容易想到的就是引用在定义的时候必须进行初始化操作,那么我们可以在private定义的变量中引入一个引用变量,给了引用变量之后是会报错的,那么就说明构造函数函数体里面的操作其实是赋值而不是初始化的操作
class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
  • 虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
  • 所以得出结论—构造函数函数体中是赋值不是初始化
初始化列表
  • 构造函数具有初始化列表,并且只有工造函数具有初始化列表,别的函数都没有初始化列表
  • 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year),
		_month(month),
		_day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
  • 那么如何验证初始化列表是初始化而不是赋值呢?
  • 因为我们是知道的,赋值是可以进行多次的,但是初始化只能初始化一次,那么我们就可以利用这个性质,在初始化列表中让一个变量出现多次,如果代码报错的话,那么就说明初始化列表是初始化而不是赋值了,如下所示,下面的代码时会报错的
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year),
		_month(month),
		_day(day),
		_day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
  • 报错内容是_day已经初始化了
初始化列表的功能
  • 初始化列表就是专门就是为了对类中各个成员变量进行初始化的操作,而初始化呢他不是可以初始化两次的,所以一个变量如果初始化两次的话就会出错。因为在对变量进行初始化的时候需要为变量开辟内存空间,如果已经为一个变量开辟了内存空间的话,就不需要为那个变量再次开辟内存空间了。 一个变量开辟两个空间肯定是有问题的
  • 初始化列表并没有规定各个变量出现的次序,就算初始化列表的次序和形参出现的次序是不一样的,代码也是不会报错的,所以说,初始化列表并没有强制规定参数的次序要和形参的次序是一样的。但是一般情况下不建议这么做,防止出现像这样的问题
    在这里插入图片描述
    在这里插入图片描述
    为什么month会是随机值呢,因为编译器先去初始化year,然后初始化year完成之后,编译器开始去初始化month,但是初始化列表给的是用day去初始化month,但是此时day并没有进行初始化的操作,所以最终看出,month为随机值。
注意事项
  • (1)初始化列表中成员的出现次序,不代表其真正的初始化次序
  • (2)成员变量在初始化列表中的初始化次序为其在类中的声明次序
  • 建议:最好不要使用成员初始化成员
  • 虽然说我们可以把_day写在_month的前面,但是我们在初始化的时候,还是先去初始化的_month然后再去初始化的_day
  • 编译器是不会按照我们所给出的初始化的顺序来进行初始化的,编译器是按照形参出现的顺序来进行初始化操作的
拷贝构造函数也可以有初始化列表
#include<iostream>
using namespace std;
class Date
{
public:
	Date(const Date& d)
		: _year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		// 		_year = d._year;
		// 		_month = d._month;
		// 		_day = d._day;
		cout << "Date(Date&):" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

引用变量以及const类型的变量的注意事项
  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 类中包含以下成员,必须放在初始化列表位置进行初始化:(在构造和拷贝构造的地方都需要进行初始化)
  • (1)引用成员变量
  • (2)const成员变量
#include<iostream>
using namespace std;
class Date
{
public:
#if 0
	// 初始化列表
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
		// 构造函数体中是赋初值不是初始化
// 		_year = year;
// 		_month = month;
// 		_day = day;
		//r = _day;
		cout << "Date(int,int,int):" << this << endl;
	}
#endif

	// 初始化列表
	// 1. 初始化列表中成员的出现次序,不代表其真正的初始化次序
	// 2. 成员变量在初始化列表中的初始化次序为其在类中的声明次序
	// 建议:最好不要使用成员初始化成员
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
		, _r(_day)
		, _a(_day)
	{
		// 构造函数体中是赋初值不是初始化
		// 		_year = year;
		// 		_month = month;
		// 		_day = day;
		//r = _day;
		cout << "Date(int,int,int):" << this << endl;
	}

	Date(const Date& d)
		: _year(d._year)
		, _month(d._month)
		, _day(d._day)
		, _r(d._r)
		, _a(d._a)
	{
		// 		_year = d._year;
		// 		_month = d._month;
		// 		_day = d._day;
		cout << "Date(Date&):" << this << endl;
	}

	// d1 = d2 = d3;
	Date& operator=(const Date& d)
	{
		cout << this << "=" << &d << endl;
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}

	~Date()
	{
		cout << "~Date():" << this << endl;
	}


	int _year;
	int _month;
	int _day;
	int& _r;
	const int _a;
};
  • (3)类类型成员(该类没有默认构造函数)
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour, int minute, int second)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}


private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
		, _t(0, 0, 0)
		//, _t()   //调用无参的默认的构造函数
	{
		cout << "Date(int,int,int):" << this << endl;
	}

private:
	int _year;
	int _month;
	int _day;
	Time _t;
	//如果没有上面的_t(0, 0, 0),直接给出Time_t是会报错的,原因在于
	//声明了一个time类的对象t,那么这个对象t一定是需要进行初始化的
	//那么编译器会默认去Time类中寻找没有参数的构造函数,但是Time类此时是没有
	//显示给出无参的构造函数的,所以就会出错
	//那么我们为了可以正确的创建出来这个Time类类型的对象的话,那么我们就需要给出来一个有参数的
	//就好比说_t(0, 0, 0),就可通过编译了
};

int main()
{
	//Date d(2019, 3, 24);
	Date d;
	return 0;
}
  • 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。(没写并不代表没有,还是会去调用相应的函数的)
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
		cout << this->_hour << endl;
	}

	void TestFunc()
	{}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		// 		: _year(year)
		// 		, _month(month)
		// 		, _day(day)
				//, _t()   //调用无参的默认的构造函数
	{
		cout << "Date(int,int,int):" << this << endl;
	}

private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,也就是说,不是按照我给的初始化列表的次序来进行初始化操作的,编译器是按照声明的次序来进行初始化的(上面已经提到过了)
提问,在初始化列表中如何知道对象已经构造好了?
  • 就是在构造函数的初始化列表完成之后,去打印一下this,如果代码可以正常的通过编译,那么就说明我的对象是构造好了的
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
		cout << this<< endl;    //我的意图是要去打印this,然后发现代码可以正常通过编译
		//那么,也就是说,此时,我的对象已经构造好了
	}
private:
	int _hour;
	int _minute;
	int _second;
};
两个问题
在构造函数有没有把对象的地址传递过去呢?
  • 对象的地址一定是传递过来了的,如果对象的地址没有传递过来的话,那儿我怎么知道当前构造的是那个对象呢?所以说对象的地址一定是已经传递过来了的
    在这里插入图片描述
  • 008560677 把t的首地址放到了exc寄存器里面,然后去调用Time类的函数
  • 在构造函数调用之前,对象是不存在的,因为构造函数的目的就是用来初始化对象的,虽然对象本身是并不存在的,但是对象的空间是存在的,所以也就是说,在我们没有调用构造函数之前,我们就已经有了一段空间了,因为编译器是要进行编译的,所以他必须提前计算出这个对象需要多大的栈空间
    在这里插入图片描述
  • 定义了两个对象,所以编译器必须要在编译期间计算出这两个对象的大小,因为不可能做到一边运行一边修改,所以说,必须要提前计算好,在编译的时候是不会调用构造函数的,是在运行的时候去调用的
在构造函数的初始化列表的位置可不可以使用this指针呢?

在这里插入图片描述

  • 在构造函数初始化列表的位置还不能使用构造函数,因为this是指向当前对象的,初始化列表的位置,当前对象还并没有构造好,所以还是不能使用的,还没有真正的区划分空间。但是在函数体里面是完全可以,因为对象已经完全的初始化完成了,是 可以正常去使用的
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
		cout << this->_hour << endl;
	}

	void TestFunc()
	{}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		// 		: _year(year)
		// 		, _month(month)
		// 		, _day(day)
				//, _t()   //调用无参的默认的构造函数
	{
		cout << "Date(int,int,int):" << this << endl;
	}

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

int main()
{
	// 在编译器编译期间,已经为main分配好了栈空间
	// 该空间中已经包含了函数体中的局部对象
	Date d;    // 在构造函数调用之前,对象是不存在的
	Time t;
	t.TestFunc();

	return 0;
}
结论:也就是说对象的空间早就已经给好了,只不过缺的是构造函数,构造函数的功能就是把对象中各个成员变量给其初始化好就可以了
构造函数的功能
  • 构造函数不仅就有初始化成员变量的功能,还具有类型转化的功能
explicit关键字
  • 构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year)
		: _year(year)
	{
		cout << "Date(int,int,int):" << this << endl;
	}

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

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


int main()
{
	Date d(2019);
	d = 2020;   
	//一个是日期类型的变量,一个是整形的变量,在我们之前想的是,这时不能通过编译的
	//但是没想到,这样的代码是可以通过编译的,原因在于
	// 2020---> 通过单参构造函数--->临时对象
	//也就是说构造函数具有类型转换的功能,本来是一个int类型,然后被转换了
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2020构造一个无名对象,最后用无名对象给d1对象进行赋值
	//但是一般情况下,会把这种类型转换禁止掉,那么如何来禁止呢?
	//禁止的方法就是在构造函数前面加上一个explicit关键字
	return 0;
}
  • 上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
#include<iostream>
using namespace std;
class Date
{
public:
	explicit Date(int year)
		: _year(year)
	{
		cout << "Date(int,int,int):" << this << endl;
	}

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

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


int main()
{
	Date d(2019);
	d = 2020;   // 2020---> 通过单参构造函数--->临时对象
	return 0;
}
之前说过的几个默认的构造函数,如果我么没有显示给出的话,编译器会生成默认的
#include<iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	return 0;
}
  • 问题来了,什么时候编译器会生成默认的?-------答案就是—>如果编译器感觉自己需要的时候就会生成,那么问题又来了,什么才是编译器需要的时候(一共有四种场景)
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
		cout << this->_hour << endl;
	}

	Time(Time&)
	{}

	Time& operator=(Time& t)
	{
		return *this;
	}

	~Time()
	{}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	// 	Date()
	// 	{}

		/*
		Date(Date& d)
		   : _t(d._t)
		{}

		*/

private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1; // 日期类的构造函数 // Time()
	Date d2(d1);  // Time(Time&)--->找个调用位置--->Date类的拷贝构造函数中
	Date d3;
	d3 = d1;
	return 0;
}
  • (1)现在有A类和B类两个类,B类中包含A类的对象,A类中有缺省的构造函数,B类中没有定义任何的构造函数,编译器此时就会生成默认的构造函数
  • (2)Time(Time&)—>找个调用位置—>Date类的拷贝构造函数中
  • (3)赋值也是一样的
那么我们该如何知道类到底创建了多少个变量呢?

在这里插入图片描述

  • 场景就是:提问:我现在有一个类,这个类在程序中很多位置都用到了,那么,我该如何知道这个类到现在位置总共创建了多少个对象,那么我们就会想了,我要知道创建了多少个对象,就必须给一个变量去记录对象创建的个数,这个变量一定是整形的,那么问题又来了,这个变量在哪创建?
#include<iostream>
using namespace std;
class Test
{
public:
	Test()
		:_count(0)   //把count的初始值给成0
	{
		_count++;
	}

	Test(Test& t)
	{
		_count++;
	}

	~Test()
	{
		_count--;
	}

private:
	int _b;
	int _count;   // 能否定义一个共享的成员变量
};

int main()
{
	Test t1, t2;
	return 0;
}

在这里插入图片描述
在这里插入图片描述

  • 当我们把变量给出来之后,因为知道,构造函数是用来初始化对象的,所以有几个对象,就会调用几次构造函数,那么我们只需要每次掉调用一次构造函数,就给count++就可以了,当然拷贝构造函数也要给出来,每次调用一次,count也要++,析构函数,每次调用一次count–
  • 但是,当我们将代码运行起来的时候,我们实际上是创建了两个对象的,但是目前还不知道是为什么,count的值却为1
    在这里插入图片描述
  • 两个count既然都为1的话,那么说明我们现在所给的count计数可能是不对的,现在的count是一个普通的整形变量,它既然是一个普通的整形变量,那么它一定是在每个对象里面都有的,但是我们现在所需要的计数应该是类里面所有对象所公共的一个变量,如果每一个对象里面都有一个count,那肯定是不行的,因为如果每一个对象里面都有count的话,那么这个count就记录的是每一个对象里面当前count的值的大小,而不是所有对象的个数。
    在这里插入图片描述
  • 那么如果要用来计数的话,计数的变量不能包含在每个对象中,应该是所有对象所共享的,那么其实很好想到的就是全局变量了
#include<iostream>
using namespace std;
int g_count = 0;
class Test
{
public:
	Test()
	{
		g_count++;
	}

	Test(Test& t)
	{
		g_count++;
	}

	~Test()
	{
		g_count--;
	}

private:
	int _b;
};

void TestCount()
{
	Test t1, t2;   //这个函数里面创建了两个对象,出了函数体之后销毁
	cout << g_count << endl;
}

int main()
{
	Test t1, t2;
	//这个函数里面也创建了两个对象,所以在上面的对象没有被销毁的时候,一共有四个对象
	//当上面的函数体中的对象被销毁了之后,就只剩了两个对象了
	TestCount();
	cout << g_count << endl;
	return 0;
}
  • 但是使用全局变量有一个不太好的地方就是,如果在代码中的某个位置处修改了全局变量的值,那么我们最后所得到的计数就是有问题的计数
    在这里插入图片描述
#include<iostream>
using namespace std;
int g_count = 0;
class Test
{
public:
	Test()
	{
		g_count++;
	}

	Test(Test& t)
	{
		g_count++;
	}

	~Test()
	{
		g_count--;
	}

private:
	int _b;
};

void TestCount()
{
	Test t1, t2;   //这个函数里面创建了两个对象,出了函数体之后销毁
	cout << g_count << endl;
}

int main()
{
	Test t1, t2;
	//这个函数里面也创建了两个对象,所以在上面的对象没有被销毁的时候,一共有四个对象
	//当上面的函数体中的对象被销毁了之后,就只剩了两个对象了
	g_count=0;  //如果进行了修改,那么就会得到不正确的计数了
	TestCount();
	cout << g_count << endl;
	return 0;
}
那么,现在的问题就在于能否在类中定义一个共享的成员?
static成员

在这里插入图片描述
在这里插入图片描述

  • 问题来了,在类中,有没有这种共享的变量?
  • 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;
  • 用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
    在这里插入图片描述
  • 如何判断多个对象使用的是不是同一个count对象?—可以通过打印多个对象中同一个变量的地址来查看他们到底是不是共用的同一个变量count
    在这里插入图片描述
  • 通过计算对象的大小可以直到,静态的成员变量没有存储在对象的空间中,对象中存储的是非静态成员变量(静态成员变量存储在数据段中)
    在这里插入图片描述
  • 静态变量可以通过类名去访问
    在这里插入图片描述
#include<iostream>
using namespace std;
// 类总共创建了多少个对象?
// 计数的变量----不能包含在每个对象中,应该是所有对象共享
// 1. 使用全局变量---->可以---缺陷:不安全
//int g_count = 0;

// 普通成员变量                static成员变量
// 可以在初始化列表中初始化         不行
// 每个对象中都包含             只有一份,没有包含在具体的对象中,是所有对象共享的
// 必须通过对象访问             可以通过对象直接访问 || 也可以通过类名加作用域直接访问
class Test
{
public:
	Test()
		: _b(0)   //普通类型的变量可以在成员初始化列表的位置进行初始化
		//但是,static成员变量不能在成员的初始化列表中进行初始化
	{
		_count++;
	}

	Test(Test& t)
	{
		_count++;
	}

	static int GetCount()
	{
		return _count;
	}

	~Test()
	{
		_count--;
	}

private:
	int _b;
	static int _count;   //只是一个声明,static类型的变量必须要在类外进行初始化操作
};

int Test::_count = 0;   //才算是真正开辟了内存空间
//要在类外进行初始化的操作

void TestCount()
{
	Test t1, t2;
	//cout << g_count << endl;
	cout << t1.GetCount() << endl;
}

int main()
{
    //把GetCount给成static类型的,就可以不通过对象去访问了
	Test::GetCount();
	Test t1, t2;
	cout << Test::GetCount() << endl;
	cout << sizeof(t1) << endl;
	//cout << &t1._count << "=" << &t2._count << endl;    
	//如何看出static成员变量是所有成员都共享的,而不是每个成员变量中都各自存在有一份
	//我门可以通过打印不同变量中static成员变量的地址,来看打印出来的地址的结果是不是一样的
	//就可以判断出来到底是每个变量存在有一份,还是所有变量共享一个static成员变量
	//static成员变量的大小是不计算在对象大小里面的,因为其是所有对象所共有的
	
	//cout << Test::_count << endl;
	//可以通过对象直接访问 || 也可以通过类名加作用域直接访问
	//因为static不是独享所独有的,所以可以通过对象去访问,当然也可以不通过对象去访问
	//都是可以的,那么这两中访问方式有什么区别呢

	//这两中访问方式有什么区别
	//int a = 10;
	//a = t1._count;
	//a = Test::_count; 


	TestCount();
	cout << Test::GetCount() << endl;
	return 0;
}
  • 两种访问方式没有什么区别
    在这里插入图片描述
    在这里插入图片描述
    5.static不能修饰构造和析构
静态成员函数与普通类型的成员函数又什么区别

在这里插入图片描述

#include<iostream>
using namespace std;
class Test
{
public:
	Test()
		: _b(0)
	{
		_count++;
	}

	Test(Test& t)
	{
		_count++;
	}

	// 普通的成员函数:有一个隐藏的this指针
	// 可以访问普通的成员变量
	int GetB()
	{
		GetCount();
		cout << this << endl;
		return this->_b;
	}

	// 静态成员函数: 没有this指针
	// 不能访问普通的成员变量
	static int GetCount()
	{
		//cout << this << endl;
		//cout << _b << endl;
		//GetB();  不能调用
		return _count;
	}

	~Test()
	{
		_count--;
	}

private:
	int _b;

	// 相当于在类中定义一个与对象无关的全局变量---所有对象共享
	static int _count;   // 声明
};


int Test::_count = 0;  // 开辟空间

int main()
{
	Test t;
	t.GetB();

	Test::GetCount();
	return 0;
}
  • 静态成员函数: 没有this指针,不能访问普通的成员变量
  • 普通的成员函数:有一个隐藏的this指针,可以访问普通的成员变量
    在这里插入图片描述
特性
  • 静态成员为所有类对象所共享,不属于某个具体的实例
  • 静态成员变量必须在类外定义,定义时不添加static关键字
  • 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别
问题
  • 静态成员函数可以调用非静态成员函数吗?
    不可以,因为调用普通的函数需要传递this过去,但是static类型的静态成员函数,是没有this的,所以传不过去,没有了this就不知道操作那个对象,既然不知道操作哪个对象的话,那么肯定是不能通过编译的,所以不可以
  • 非静态成员函数可以调用类的静态成员函数吗?
    可以,因为不需要什么别的条件,也不需要传递什么东西
C++11 的成员初始化新玩法
  • C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值
class B
{
public:
	B(int b = 0)
		:_b(b)
	{}
	int _b;
};
class A
{
public:
	void Print()
	{
		cout << a << endl;
		cout << b._b << endl;
		cout << p << endl;
	}
private:
	// 非静态成员变量,可以在成员声明时给缺省值。
	int a = 10;
	B b = 20;
	int* p = (int*)malloc(4);
	static int n;
};
int A::n = 10;
int main()
{
	A a;
	a.Print();
	return 0;
}

在这里插入图片描述
在这里插入图片描述

友元
  • 友元分为:友元函数和友元类
  • 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
  • 问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理
#include<iostream>
using namespace std;
class Time
{
	friend void TestFriend();
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
		cout << this->_hour << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
	// 友元函数
	friend ostream& operator<<(ostream& _cout, const Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}

	void PrintDate()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	/*
	// d << cout;
	void operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
	}
	*/

	friend void TestFriend();

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

void TestFriend()
{
	Time t;
	t._hour = 16;

	Date d;
	d._year = 2019;
}

// 4. 将该函数作为类的友元函数
ostream& operator<<(ostream& _cout, const Date& d)
{
	// _cout<<d.GetYear()<<"-"<<d.GetMonth()<<"-"<<d.GetDay();
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

// >> istream

int main()
{
	Date d(2019, 3, 24);
	d.PrintDate();
	cout << 10 << endl; // cout<<10  cout<<endl;

	cout << d << endl;
	//cout << 10;
	//cout << d;

	//d.operator<<(cout);
	//d << cout;

	return 0;
}
  • 有元函数还可以提高代码的运行效率,有元可以直接那变量,普通函数需要调用函数才能拿到变量的值
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
条件
  • 两个参数:参数一一定为ostream&, 参数2输出的内容
  • 必须要有返回值:ostream&, 支持连续输出
  • 少做格式化操作:比如换行
  • 将该函数作为类的友元函数
说明:
  • 友元函数可访问类的私有成员,但不是类的成员函数(不受访问限定符号的限制)
  • 友元函数不能用const修饰(有元函数不是类的成员函数,所以没有this指针,也就不能加const了)
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数(一个人可以是多个人的朋友)
  • 友元函数的调用与普通函数的调用和原理相同
友元类
  • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
  • 友元关系是单向的,不具有交换性。
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递
    如果B是A的友元,C是B的友元,则不能说明C时A的友元。
#include<iostream>
using namespace std;
class Time
{
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{
		cout << this->_hour << endl;
	}

	void TestFriendClass()
	{
		Date d;
		d._year = 2019;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
	friend class Time;
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}

	void PrintDate()
	{
		cout << _year << "-" << _month << "-" << _day << _t._hour << ":" << _t._minute << ":" << _t._second << endl;
	}

private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
内部类
概念及特性
  • 概念:如果一个类定义在另一个类的内部,这个定义在另一类内部的类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限
  • 注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
  • 特性:
  • 1.内部类可以定义在外部类的public、protected、private都是可以的。
  • 2.注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  • 3.sizeof(外部类)=外部类,和内部类没有任何关系。
再次理解封装
  • C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
  • C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
//类和对象下
/*
构造函数的作用:用来初始化对象空间中的变量的
那么构造函数内部的语句到底是初始化还是赋值呢?
其实很容易进行验证,从概念的角度出发就可以发现,初始化
只可以初始化一次,而赋值可以多次进行赋值
通过对代码进行验证,可以得出结论,在构造函数函数体内
所进行的语句其实是赋值语句,而不是进行初始化的语句
构造函数具有初始化列表
初始化列表的功能其实就是用来进行初始化的
初始化列表:以一个冒号作为开头,接着是一个以逗号
进行分割的数据成员列表,每个成员变量后面跟一个放在
括号中的初始值或者表达式,用来进行初始化的操作
*/

#if 0
#include<iostream>
using namespace std;
class Date
{
public:
	//下面是构造函数初始化列表的书写方式
	Date(int year, int month, int day)
		:_year(year),
		 _month(month),
		 _day(day)
	{
		/*_year = year;
		_month = month;
		_day = day;*/
		cout << "Date(int year, int month, int day)" << endl;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 3, 26);

	d1.Print();

	return 0;
}
#endif


#if 0
#include<iostream>
using namespace std;
class Date
{
public:
	/*
	构造函数都有初始化列表,即使没有显示写出构造函数的初始化列表
	构造函数的初始化列表也是存在的
	编译器仍然会执行构造函数初始化列表的部分
	*/
	Date(int year, int month, int day)
		:_year(year),
		_month(month),
		_day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 3, 26);

	d1.Print();

	return 0;
}
#endif


/*
尽量使用初始化列表进行初始化,因为不管你是否使用
初始化列表,对于自定义类型成员变量
一定会先使用初始化列表来进行初始化
成员变量在类中的声明次序就是其在初始化列表中的初始化次序
与其在初始化列表中的先后次序无关
所以不要使用一个成员变量给另一个成员变量进行初始化的操作
*/

#if 0
/*
explicit关键字
单个参数的构造函数具有类型转换的功能
下面的这个代码是可以通过编译的
下面的代码编译没有任何的问题,但是这个代码稍微有点奇怪
因为我定义d1为一个日期类的对象,那么日期类的对象其实包含有
年月日,三个属性,但是我在下面的代码中体现了d1=2023,相当于
是把一个整形赋值给了一个日期类的对象
按照理论上来讲,这样的赋值操作其实是有问题的
但是下面的代码编译没有任何的问题,所以代码d1=2023背后的本质
其实是:
整形和日期类类型的对象肯定是不可以直接进行赋值的操作的
但是恰巧的是,我们在类型中给了一个单参的构造函数
那么编译器,其实就把那个整形2023作为一个参数传递给了构造函数
构造了一个没有名字的对象,然后把对象的变量赋值给了d1
就不会有任何的问题产生了
所以说单参的构造函数具有类型转化的功能
但是,这样有一个不好的地方其实就是会导致代码的可读性降低
当然,这种功能也是可以被禁止掉的
那么如何禁止掉单参的构造函数具有类型转换的功能呢
使用explicit关键字就可以禁止掉这种功能
*/
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year)
		:_year(year),
		_month(1),
	    _day(1)
	{
		cout << "Date(int year)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022);
	d1 = 2023;
	return 0;
}
#endif

#if 0
/*
使用了explicit关键字之后
单参的构造函数的类型转换的功能其实就被禁止了
*/
#include<iostream>
using namespace std;
class Date
{
public:
	explicit Date(int year)
		:_year(year),
		_month(1),
		_day(1)
	{
		cout << "Date(int year)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022);
	d1 = 2023;
	return 0;
}
#endif


/*
static关键字在c++中的作用
在回答static关键字的时候最好分成c语言里面的作用和c++里面的作用分别去回答
在c++中static也可以修饰函数和变量作用和c语言里面是一样的
同时static关键字还可以修饰成员变量和成员函数
假如说现在有一个需求:
我想要知道一个类中创建了多少对象--很容易想到的一个方法其实就是使用计数的方法
如果想通过计数得出结论,切记不可以使用普通变量
必须要使用全局变量,但是全局变量在函数的任何位置都可以修改
所以全局变量不是那么的安全,一不小心修改的话,都可能影响最终的结果
用全局变量确实是可以实现相应的功能,就是不太安全
static可以修饰成员变量
static修饰的成员变量需要在类内声明,类外进行初始化的操作
把static修饰的成员称为静态成员,修饰成员函数
称为静态成员函数


普通成员变量和静态成员变量的区别!!!!!!
普通成员变量每个对象都有一份,静态成员变量是很多
对象共享一个静态成员变量,静态成员变量没有包含在对象中,是共享的
如何验证多个对象是否是共用同一个静态成员变量
可以通过打印静态成员变量的方法去验证静态成员变量是否是被共享的
通过代码,可以发现打印的地址是一样的,所以说静态成员变量只有一份
是类的属性,同时静态成员变量不会影响类的大小

静态成员变量需要在类内声明,类外进行初始化的操作,且
不能放在构造函数初始化列表的位置里进行初始化的操作
只能在类外单独进行初始化,普通成员变量只能在构造函数
初始化列表的位置进行初始化的操作

关于两者的使用方法,普通变量的访问方式是需要通过对象打点来进行访问
因为普通变量是存在在具体的对象中的,所以需要通过对象打点来进行访问
但是针对静态成员变量来说,也可以通过对象打点的方式来进行访问,对象.静态成员变量
同时也可以通过类名::来进行访问的操作  类名::静态成员变量来进行访问
而且两种方式只是看起来不一样,其实在底层都是同一种访问方式
静态成员函数没有this指针
*/

/*
静态成员函数不能访问非静态成员函数和非静态成员变量
因为静态成员函数没有this指针

普通成员函数可以访问静态成员变量
也可以访问静态成员函数

普通类型的成员函数可以被const修饰,一旦被const修饰之后
就变成了const类型的函数
但是静态成员函数不可以被const修饰
因为静态成员函数没有this指针
而const修饰函数就是在修饰成员函数的this指针
因为没有this指针,所以不可以被const关键字来进行修饰

构造函数能不能被static修饰---构造函数不能被static修饰
拷贝构造也不可以,析构函数,赋值运算符重载也不可以
因为构造函数需要进行对象种内容的初始化
那么既然你现在需要进行初始化的操作,那么其实就需要把对象的地址
传递过来,所以不可以

静态成员函数和普通成员函数还有一个区别就是
静态成员函数默认的函数调用约定是_cdecl
普通成员函数默认的函数调用约定是thiscall
*/

//针对<<和>>运算符的重载操作
/*
根据我们对日期类的了解,日期类里面其实也有一个对于
日期类对象的输出函数,那么,如果我们想要将那个日期类类型的
对象进行输出操作的话,我们只需要用对象调用相应的函数来完成输出的操作就可以了
那么,换一种想法,我们能不能直接使用<<运算符来进行输出的操作呢?
通过对代码的验证我们发现,其实是不可以的
cout不知道到底按照什么样的格式对类类型的变量进行打印的操作
当然,如果我们想用<<符号直接输出的话,我们可以对<<运算符
进行重载的操作
<<有两个操作数
*/

/*
有元:
有元分为有元函数和有元类
有元函数不属于类的成员函数,所以有元函数也没有this指针
有元函数不可以被const修饰
因为const其实是修饰的成员函数的this指针
但是有元不属于成员函数也没有this指针
所以不可以被const修饰
一个函数可以是多个类的有元函数
*/
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值