类和对象(末)

目录

·初始化列表:

 explicit

static修饰的静态成员变量

 友元:

 友元类:

内部类

匿名对象

 拷贝对象时的一些优化:


·初始化列表:

class Date
{
public:
	Date(int year, int month, int day)

		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

如代码所示:

类和对象(末)_第1张图片

代码中的这部分就是初始化列表,初始化列表也可以完成构造函数的任务。

 并且构造函数函数体内的初始化列表可以混合着一起使用。

我们写一个栈的构造函数:

Stack(int capacity = 4)
		: _a((int*)malloc(sizeof(int)*capacity))
		, _top(0)
		, _capacity(capacity)
	{
		if(_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
	}

同样,我们也可以这样写:

我们可以把_a的初始化放到函数体内:

Stack(int capacity = 4)
		: _top(0)
		, _capacity(capacity)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		if(_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
	}

当然,初始化列表并不能够解决全部的初始化问题。

假如我们想要把我们动态申请的_a的这部分空间全部置为空,我们的初始化列表就完成不了。

例如:

Stack(int capacity = 4)
		: _top(0)
		, _capacity(capacity)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		if(_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memset(_a, 0, sizeof(int)*capacity);
	}

总结:初始化列表是在构造函数体之外写的一种初始化方法,但是初始化列表并不能够解决全部的初始化问题,所以我们需要和构造函数的函数体内容一起配合使用。

 问题:初始化列表的一个变量可以被重复初始化吗?

答:不能,例如:

类和对象(末)_第2张图片

 _top被初始化了两次。

这里就会报错。

问题:既然构造函数体能够解决全部的问题,而初始化列表只能解决一部分问题,那初始化还起到什么作用呢?什么场景下需要使用初始化列表呢?

答:例如const修饰的成员变量

class B
{
public:
	B()
	{
		_n = 10;
	}
private:
	const int _n;
};
int main()
{
	B b;
	return 0;
}

对于这样的一串代码,我们进行编译:

 报错的原因是这样:

int main()
{
	const int a;
	return 0;
}

例如这样的一串代码,我们进行编译:

 报错的原因是const修饰的变量是静态变量是不可以修改的,所以我们必须在其定义时进行赋值:

类和对象(末)_第3张图片

同理:

 类和对象(末)_第4张图片

 我们的_n定义发生在我们创建对象的时候

类和对象(末)_第5张图片

_n的定义再细节一些:定义是在构造函数体内还是初始化列表呢? 

答:定义是在初始化列表中进行的,我们的初始化列表并没有写这个静态变量的初始化,所以就会报错。

如何解决这个问题呢?

答:我们可以在初始化列表中进行初始化:

类和对象(末)_第6张图片

 总结:所有的成员变量进行初始化时都要经过初始化列表,并且成员变量的定义发生在初始化列表中。

我们再写一个内置类型的成员变量,不显示写初始化列表,看初始化列表对内置类型如何处理:

例如:

类和对象(末)_第7张图片

_m内置类型被初始化成了随机值。

得出结论:对于内置类型,假如我们不显示写初始化列表时,初始化列表会把内置类型初始化成为随机值。 

假如我们在声明位置给_m一个缺省值,_m的值会变成什么?

答:类和对象(末)_第8张图片

 所以,缺省值对于初始化列表也是同样适用的。

但是,假如我们对内置类型进行显示初始化列表呢?

答:类和对象(末)_第9张图片

 假如我们显示初始化列表了,那我们给的缺省值就不起任何作用了。

总结:如果没有在初始化列表中显示初始化,对于内置类型,假如有缺省值就用缺省值,没有缺省值就用随机值。

 那对于自定义类型呢?

例:

类和对象(末)_第10张图片

 我们在类B中的成员变量中添加一个内置类型的A,A是这样的一个类:

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};

我们可以发现,A没有默认构造函数。

默认构造函数的定义:我们不写参数也可以调用的函数叫做默认构造函数,例如无参,全缺省,或者不写编译器默认给的。

我们进行编译:

 会报错。

但是假如我们在类A中写一个默认构造函数呢?

类和对象(末)_第11张图片

 我们进行调试:

类和对象(末)_第12张图片

 成功调用了默认构造函数。

总结:假如我们没有在初始化列表中显示初始化时,对于自定义类型,假如我们的自定义类型有默认构造函数,调用默认构造函数,假如没有默认构造函数,就报错。

 那我们该如何显示的在初始化列表中初始化自定义类型呢?

例如:

类和对象(末)_第13张图片

 和内置类型的初始化是相同的

类和对象(末)_第14张图片

 这里就相当于显示调用类A的构造函数。

我们写一个之前熟知的用两个栈来实现队列:

class MyQueue {
public:
	MyQueue()
	{}
	void push(int x)
	{
		_pushST.Push(x);
	}
private:
	Stack _pushST;
	Stack _popST;
	size_t _size = 0;
};

我们的构造函数和初始化列表就这样写:

类和对象(末)_第15张图片

 因为我们没有在初始化列表中显示初始化,所以对于自定义类型,默认调用其构造函数,假如没有构造函数的话就报错。对于内置类型,有缺省值的用缺省值,没有缺省值的用随机值。

假如我们删除栈的默认构造函数:

例如:

类和对象(末)_第16张图片

我们进行调用:

 

 这个时候就会显示没有默认构造函数可用。

尽管我们没有创建对象,但是只要我们写了初始化列表,我们就会检查对应的自定义类型是否会有构造函数,没有的话,就会报错。

所以什么情况下需要使用初始化列表:当我们是自定义类型,并且我们没有写默认构造函数时,这时候,我们需要自己写初始化列表。

我们先恢复栈的默认构造:

类和对象(末)_第17张图片

 定义一个队列的对象:

类和对象(末)_第18张图片

我们看是否可以实现初始化:

 类和对象(末)_第19张图片

 可以完成初始化。

假如我们要求MyQueue需要输入一个参数:

类和对象(末)_第20张图片

 我们输入一个参数看是否能完成初始化:

类和对象(末)_第21张图片

 我们进行调试:

类和对象(末)_第22张图片

 我们发现,并没有完成我们期望的初始化。原因是什么?

答:无论我们显示写不写初始化列表,我们的全部成员变量都会走初始化列表,对于自定义类型的化,调用其默认构造,这里的默认构造就是栈的默认构造,我们的栈的默认构造的capacity,对于内置类型我们有缺省值就用缺省值,没有缺省值的话,就用默认构造。

那我们该如何显示初始化列表呢?

答: 

类和对象(末)_第23张图片

 总结:有哪些成员必须要显示初始化?

答:1:const修饰的静态成员变量

2:引用成员变量

3:没有写默认构造函数的自定义类型

注意:尽量去使用初始化列表去初始化,因为无论你是否使用初始化列表,对于自定义类型,一定会使用初始化列表去初始化。

我们来写一道题目:

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print(){
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main()
{
	A aa(1);
	aa.Print();
}

程序输出的结果是多少?

答:1   随机值

类和对象(末)_第24张图片

 原因如下:我们的初始化列表的初始化顺序是根据成员变量的书写顺序进行的,所以我们先初始化的是_a2,_a2的结果是_a1,但是_a1这时候还没有被初始化,_a1这时候的结果是随机值。

所以_a2的结果就是随机值。

我们再初始化_a1,_a1是_a的拷贝,所以_a1的结果为1.

我们打印的顺序是先打印_a1,再打印_a2,所以我们的结果就是1和随机值。

判断题

一个类如果不提供默认构造的话就会报错?

答:错误,例如:

class A
{
public:
	A(int a)
		/*:_a1(a)
		, _a2(_a1)*/
	{}
	void Print(){
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};

我们进行编译:

 原因:我们并通过该类来创建对象

当我们使用该类来创建对象,并且不输入参数时,就会报错:

类和对象(末)_第25张图片

 这个时候进行运行就会报错。

当我们注释掉自己写的构造函数时,就不会报错:

class A
{
public:
	//A(int a)
	//	/*:_a1(a)
	//	, _a2(_a1)*/
	//{}
	void Print(){
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};

int main()
{
	A aa;
	aa.Print();
	return 0;
}

因为默认构造函数分为三种:

1:全缺省

2:无参

3:我们不写,编译器默认的。

这里我们不写,就会执行编译器默认的构造函数。

我们再写一个例子来加深印象:

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print(){
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};



class B
{
private:
	A _aa;
};
int main()
{
	B bb;
	return 0;
}

我们这里没有创建对象,就不会报错

但是我们假如创建了对象就会报错:

 类和对象(末)_第26张图片

 

 原因是什么,我们进行分析:

答:首先,我们先创建一个对象bb,对象bb中有一个成员变量_aa,我们没有显示写初始化,对于自定义类型会调用其默认构造:

类和对象(末)_第27张图片

 而这里又没有默认构造,就会报错。

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print(){
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};



class B
{
public:
	B()
	{}
private:
	A _aa;
};
int main()
{
	/*B bb;*/
	return 0;
}

但是我们这里没有创建对象也会报错。

原因是什么?

答:原因是只要我们这里写了

类和对象(末)_第28张图片

只要我们显示写了这部分,我们就会首先进行初始化列表(无论我们是否创建对象),对于自定义类型会调用其默认构造。

类和对象(末)_第29张图片

 但是我们并没有默认构造,所以就会报错。

总结:情况1:没有显示写初始化列表,对于自定义类型,会调用其默认构造,假如没有默认构造,就会报错(创建过对象)

情况2:只显示了初始化列表的框架,对于自定义类型,会调用其默认构造,假如没有默认构造,就会报错(无论是否创建对象)

注意:能用初始化列表就用初始化列表,因为初始化列表一定不会错。

2:尽量给一个函数提供默认构造。

 explicit

例:

类和对象(末)_第30张图片

 对于这个日期类,我们一般是这样创建对象的:

类和对象(末)_第31张图片

 但是现在我们c++98中又添加了一种写法:

类和对象(末)_第32张图片

 我们可以这样写。

这种写法如何理解呢?

答:我们可以类比整型和浮点型之间的赋值:

例如:类和对象(末)_第33张图片

 我们可以对这里这样理解:首先把浮点型强制类型转换为整型,赋给一个临时变量tmp,这个临时变量的类型是整型,然后再把该临时变量赋给d。

所以,对于

我们可以这样理解成这样的代码:

 类和对象(末)_第34张图片

 这里的意思是我们首先把2022当作参数创建一个临时日期类对象tmp,然后再进行拷贝构造,把tmp拷贝给d2.

所以

这里相当于把构造和拷贝构造隐式类型转换成为了一个构造

我们再举一个例子:

类和对象(末)_第35张图片

 类和对象(末)_第36张图片

 为什么加上了const就没有错误提示符,而没有加const就有错误提示符呢?

答:因为我们首先通过参数2022创建一个临时日期类对象,该日期类对象不可修改,具有常性,我们的引用的类型不对等,所以我们要加上const把类型也转换成具有常性的日期类对象来接收。

我们用内置类型也可以解释:

 

 这类相对简便的写法单参数的对于单参数的构造函数是支持的。

 

 假如我们不想让这类隐式类型转换发生,我们可以加一个explicit

类和对象(末)_第37张图片

这时候,我们的这种写法就会报错了。

我们就不能再使用这种隐式类型转换了。

是不是仅仅只有一个参数的构造函数才能使用这种隐式类型转换呢?

答:并不是,例如,我们可以这样写:

class Date
{
public:
	/*explicit Date(int year)
		:_year(year)*/
	Date(int year, int month = 1,int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

虽然我们有3个参数,但是因为我们的构造函数的三个参数有两个参数是缺省值,所以我们依然可以只输入一个参数完成构造。

我们进行实验:

类和对象(末)_第38张图片

 

 没有报错,我们的假设成功。

当然全缺省的也是可以的:

类和对象(末)_第39张图片

 类和对象(末)_第40张图片

 

 也是可以运行成功的。

如果是多参数呢?

例如:

类和对象(末)_第41张图片

类和对象(末)_第42张图片

 我们这样写明显不可以。

那这样呢?

类和对象(末)_第43张图片

 这样写也是不可以的。

这样写可以吗?

 这样写是可以的。

这两种写法对应的结果其实是一样的,只是过程不同。

 总结:c++98支持单参数的隐式类型转换,而c++11才支持了多参数的隐式类型转换

隐式类型转换途中生成的临时变量是具有常性的。

explicit修饰的构造函数不能使用隐式类型转换

static修饰的静态成员变量

面试题:实现一个类,计算类中一共创建了多少个对象。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2 = 2;
	return 0;
}

统计以下A类型的对象创建了多少个。

我们可以分析出一个结论:以A类型创建的对象不是构造出来的就是拷贝构造出来的。

所以我们可以这样写:

int N = 0;
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		++N;
	}
	A(const A&aa)
		:_a(aa._a)
	{
		++N;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2 = 2;
	A aa3 = aa1;
	cout << N << endl;
	return 0;
}

我们的思路是这样的:我们先创建一个全局变量N,当调用构造函数或者调用拷贝构造函数时,我们的N++,我们就可以检查出一个创建了多少个对象了。

我们构造了2个,拷贝构造了1个,所以打印的结果应该为3

 说明我们的编译器经过了优化,假如我们的编译器不进行优化,

这里就是一个构造和一个拷贝构造,那我们编译的结果就是4了。

 但是这里有一些问题需要注意以下:

例如:我们多写一个函数:

 这个函数的参数是一个类对象。

 我们在main函数中进行调用:

进行运行:

类和对象(末)_第44张图片

我们进行运行:

 类和对象(末)_第45张图片

 我们可以发现 ,当我们调用了F1函数,N的值就加了1,原因是什么呢?

答:

 因为我们调用函数需要传递参数,我们需要传递的是一个类对象的参数,传递参数的过程本身就是一种拷贝,所以就会调用拷贝构造,调用拷贝构造的话,对应的N就加1.

传值传参会调用拷贝构造,那传值返回会吗?

答:会,例如:

我们先写一个传值返回的函数:

A F2()
{
	A aa;
	return aa;
}

我们在main函数的函数体内对F2进行调用:

类和对象(末)_第46张图片

我们进行编译:

 类和对象(末)_第47张图片

 3,4我们已经解释过了,那这里的6的原因是什么呢?

答:

类和对象(末)_第48张图片

 因为我们在函数体内创建了一个对象,会调用构造函数,然后再对对象进行返回,返回给返回值本身,这里本质上也发生了拷贝,所以会调用拷贝构造,所以这个函数既调用了构造函数又调用了拷贝构造,所以N+2.

我们使用引用传参:

 这个时候,我们继续运行:

类和对象(末)_第49张图片

 类和对象(末)_第50张图片

 我们可以发现,传引用传参相对于传值传参的作用在于减少了拷贝构造。

假设我们传引用返回呢?

类和对象(末)_第51张图片

 但是本质上,这里传引用是错误的,因为函数调用完毕时,aa就会被释放,对于释放的空间我们再引用返回,返回的值就是被释放的空间。

不过,我们这里强制的传引用返回:

 传引用返回也可以减少拷贝构造。

总结:传引用传参和传引用返回都会减少拷贝构造函数的产生。

 但是,这里的全局变量N其实存在有一些问题的。

首先,这里的全局变量N什么时候在什么地方都可以修改:

例如:类和对象(末)_第52张图片

 我们进行编译:

 我们总结以下影响变量的生命周期的都有哪些?

答:1:局部变量在栈里面

2:静态变量在静态区,既会影响生命周期又会影响链接属性

链接属性指的是链接过程中是否会进入符号表。

3:malloc申请的空间在堆上

4:常量:存储在常量区,代码段。

局部的静态变量和全局的静态变量的区别是什么?

答:生命周期都是全局,局部的静态变量的作用域是局部,全局的静态变量的作用域是全局,局部的静态变量的生命周期是局部。

我们可以在类里面设置一个静态变量:

类和对象(末)_第53张图片

 类里面的静态变量有什么特点:

1:收到类域的限制

2:生命周期是全局的。

我们提出一个问题:类里面的静态变量存在于对象里吗?

类和对象(末)_第54张图片

 aa1和aa2都有_a,那么aa1和aa2有没有N?

答:没有,因为对象aa1和aa2都是局部变量,局部变量在栈上面,而静态变量在静态区,两者肯定不能相互包含

两者的关系是类对象共享一个N,我们对一个类对象中的N进行修改,所有对象中的N都发生了改变。

这里的N可以给缺省值吗?

答:不可以,因为这里的N只是一个声明,静态变量是在定义时进行初始化的,所以我们这里不可以给缺省值。

我们在哪里对静态变量进行初始化呢?

答:我们可以在类外面对类里面的静态变量进行初始化

 类里面的静态变量什么特点

答:生命周期是全局的,因为类是不会销毁的。

作用域受类域限制。

 我们先把类里面的静态变量设置为public的:

对于公开的类的静态变量,我们有几种访问方法?

答:两种,1:我们可以用域操作限定符来进行访问:

 2:我们也可以用对象来进行访问:

 这里的意思并不是说对象aa1中含有静态变量N,而是说aa1属于类A,类A中包含了静态变量N,然后我们可以对N进行访问。

我们也可以用指针来访问N

例如:

 当然对于空指针也是可以访问的:

 原因是只要表示我们这里的ptr的类型是A*,就可以对N进行访问。

那假设我们的静态变量是私有的,我们怎么进行处理?

类和对象(末)_第55张图片

 答:我们可以用java的写法,在类里面写一个成员函数来取出类里面的成员变量。

类和对象(末)_第56张图片

 假设我们要访问N时,我们需要这样写:

 但是有对象的时候,我们可以顺便调用,但是没对象的时候,假如我们要调用函数的话,还需要额外创建一个对象,并且只要我们创建了一个对象,我们的N值就会++,怎么处理这种情况?

答:这个时候,我们可以写一个静态成员函数。

类和对象(末)_第57张图片

 写一个静态成员函数就能解决我们的问题,因为当我们是静态成员函数,静态成员函数没有this指针,所以我们不需要创建对象也可以调用函数

我们可以这样写:

 当然,即使我们有对象时,我们也可以用普通的方法:

类和对象(末)_第58张图片

 这种写法也是正确的。

这种方法叫做突破类域的方法。

静态成员函数能够访问其他的成员变量吗?

答:不可以,例如:

类和对象(末)_第59张图片

 因为没有this指针,所以我们无法访问其他的成员变量。

总结:在类里面我们设置静态变量,这个静态变量有以下特点:

1:生命周期是全局的。

2:受类域限制。

3:不在对象里,而被所有对象所共享

我们也可以设置静态成员函数来取出受private限制的静态变量

特点

1:静态成员函数没有this指针,所以不需要对象也可以调用

2:有对象情况下依然可以调用。

3:因为没有this指针,只能访问静态成员变量,不能访问其他的变量。

我们做一道算法题目:

类和对象(末)_第60张图片

 正常,我们用循环就可以求出n的阶乘,但是这里并没有求出,我们该怎么处理呢?

答:我们可以采用刚刚的思路,写一个类,写一个静态成员变量,调用n次构造函数即可。

class Sum {
  public:
    Sum() {
        _ret += _i;
        ++_i;
    }
    static int GetRet() {
        return _ret;
    }
  private:
    static int _i;
    static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
  public:
    int Sum_Solution(int n) {
        Sum arr[n];
        return Sum::GetRet();
    }
};

我们进行测试。

 我们对代码进行分析:

我们要求n的阶乘,我们的思想是创建一个类,在类里面写一个构造函数,在构造函数中写两个静态变量,用这两个静态变量来实现阶乘,既然用了类的静态变量,我们就需要写一个静态成员函数来取出静态变量,然后我们进行输出,我们可以用变长数组的方法,这里的变长数组的意思就是创建n个对象,每创建一个对象,我们就求出对应的阶乘,然后我们求出的就是n的阶乘。

问题:

为什么这里需要使用类的静态变量,为什么不能用类的普通变量

答:因为类的静态变量具有共享的性质,也就是任何一个对象使类的静态变量改变时,这个改变对于所有其他对象的静态变量都适用

 类的静态变量怎么使用?

答:静态变量的声明放到类里面,静态变量的定义放到类外面,注意静态变量的定义不需要再加static,但是需要我们的域操作限定符。然后我们在类里面写一个静态成员函数,作用是取出我们要求的静态变量。

题目:

要求我们创建的对象只能在栈上面:

答:我们先写一个简单的类。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
private:
	int _a;
};

要求我们用这个类创建的对象只能在栈上面。

我们思考:我们创建的对象都能在什么地方?

类和对象(末)_第61张图片

 大概分为这三部分,假如我们要求只能在栈上创建空间,我们就需要限制静态区和堆创建的对象。

我们可以从他们三个的共同点做起:他们三个都需要调用构造函数。

我们可以把构造函数设为私有的:

类和对象(末)_第62张图片

 那假设我们要创建对象,我们只有一种方法了,那就是在类中创建对象,也就是设置一个成员函数,在该成员函数中创建对象,然后然后该对象即可。

类和对象(末)_第63张图片

 现在,假如我们要创建对象,我们需要这样写:

类和对象(末)_第64张图片

 但是我们发现了一处错误:那就是假如我们需要调用GetObj函数时,我们需要创建一个对象才可以调用,但是我们又只有调用函数才能创建对象,所以就产生了矛盾。

这时候,我们想起了static,静态成员函数没有this指针,调用的时候不需要对象。

 这个时候,我们就不需要创建对象既可以调用函数了:

类和对象(末)_第65张图片

 这个时候,我们就实现了只能在栈上创建对象:

因为假如我们要创建对象,只能调用静态成员函数,通过接收静态成员函数的返回值来创建对象,但是我们的静态成员函数内部是创建的对象是在栈上面的,所以我们保证了创建的对象都在栈上面。

总结:我们创建的对象可以在栈,堆,静态区。

我们了解了一种题型:限制创建的对象的位置。

我们的大体思路分为以下即可步骤

1:限制构造函数的使用

2:创建成员函数来调用构造函数

3:使用静态成员函数,不需要创建对象

4:实现限制创建对象的位置。

 友元:

类和对象(末)_第66张图片

 友元类:

class Date
{
public:
	Date(int year, int month ,int day)
		:_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;
};
class Time
{
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};

如代码所示:Time类就是Date类的友元类。

类和对象(末)_第67张图片

假如一个函数是一个类的友元函数,那么这个函数就可以访问这个类里面的所有成员变量,友元类的所有成员函数都可以是另一个类的友元函数,也就是说Date类的所有成员函数都可以访问Time类的所有成员变量。

例如这里:

类和对象(末)_第68张图片

 友元函数是单向的,不具有交换性:

我们的Date是Time的友元类

所以

类和对象(末)_第69张图片

 类和对象(末)_第70张图片

 所以,Date的成员函数可以访问Time的成员变量,而Time的成员函数不能访问Date的成员变量。

内部类

类和对象(末)_第71张图片

 我们举一个例子:

class A
{
private:
	int _a;
public:
	class B
	{
		int _b;
	};
};
int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

 打印的结果是4还是8?

 原因如下:我们的类A中只有一个成员变量,是一个整型,占4个字节。

而我们的类B虽然是类A的内部类,但是类B和类A本质上没有所属关系。

那么我们创建一个类A对象中有没有类B的成员?

答:没有,例如:

类和对象(末)_第72张图片

 对象aa中并没有类B的成员变量。

这两个类就相当于两个独立的类。

B类受A的类域和访问限定符的限制。

例如:

类和对象(末)_第73张图片

 会报错

 我们要创建B类对象需要这样创建:

类和对象(末)_第74张图片

 内部类是外部类的友元函数:

例如: 

类和对象(末)_第75张图片

内部类可以访问外部类的成员变量。

 类和对象(末)_第76张图片

 而外部类不能访问内部类的成员函数。

内部类可以直接访问外部类的静态成员变量并且不需要创建对象。

例如:

类和对象(末)_第77张图片

 类和对象(末)_第78张图片

 原因是静态成员变量可以是类所共享的,可以突破类域。因为类B是类A的友元类,所以B可以直接访问类A的静态成员变量。

我们之前写的那道面试题,我们可以用内部类的方式解决:

我们可以把Sum类写成Solution的内部类

int Sum::_ret = 0;
int Sum::_i = 1;
class Solution {
  public:
    int Sum_Solution(int n) {
        Sum arr[n];
        return Sum::GetRet();
    }
  private:
    class Sum {
      public:
        Sum() {
            _ret += _i;
            _i++;
        }
        static int GetRet() {
            return _ret;
        }
      private:
        static int _i;
        static int _ret;
    };
};

但是我们的外部类是无法访问内部类的静态成员变量的。

所以我们可以把静态成员变量写道外面的类里面。

类和对象(末)_第79张图片

因为内部类是外部类的友元函数,所以内部类可以访问这些静态成员变量。

 

 然后我们把类的初始化的域作用限定符改以下即可。

类和对象(末)_第80张图片

总结:内部类

1:内部类是外部类的友元函数

2:外部类无法访问内部类的成员变量

3:外部类所占空间的大小不包含内部类。

4:内部类可以直接访问外部类的静态成员变量,无需船舰对象。 

匿名对象

我们先写一个类

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

对于这个类,创建对象的方法有哪些?

类和对象(末)_第81张图片

第一个就是普通的构造,第二个是隐式类型转换,本质上是构造和拷贝构造的集合。

 我们可以写两个匿名对象:

类和对象(末)_第82张图片

 匿名对象只存在于创建它的那一行。

而有名对象的生命周期在所在的局部域,这里就在main函数中。

例如,我们进行调试:

类和对象(末)_第83张图片

接下俩,我们来创建第一个匿名对象。

类和对象(末)_第84张图片

 我们可以发现,刚过了创建匿名对象这一行,匿名对象的构造和析构函数就都进行完毕了,所以匿名对象的生命周期在创建它的那一行。

匿名对象在一些情况下是有意义的:

例如:

class Solution{
public:
	int Sum_Solution(int n){
		return n;
	}
};
int main()
{
	Solution so;
	so.Sum_Solution(10);
	return 0;
}

对于这个类,我们本来的目的只是为了调用函数,返回n值,但是假如我们需要调用函数的话,还需要创建一个又名对象。

这个时候,匿名对象就派上了用场:

类和对象(末)_第85张图片

 我们可以这样写:我们不需要对对象起名字了,并且1行就解决了我们的问题。

我们再举一个例子:

class A
{
public:
	A(int a=1)
	{}
private:
	int _a = 0;
};
A F()
{
	A ret(10);
	return ret;
}

假如我们要传对象返回时,我们需要创建一个对象,然后返回。

有了匿名对象,我们可以这样写:

类和对象(末)_第86张图片

这样就方便多了。

总结:匿名对象的特性

1:写法:类名(+参数)

2:生命周期:创建对象的那一行

3:匿名对象用于调用类中函数和传值返回类中。

 拷贝对象时的一些优化:

例如,我们写一个类

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;
};

我们这样创建参数就是进行优化:

int main()
{
	A aa1 = 1;
	return 0;
}

我们知道,这样写的本质是构造加上拷贝构造,假如编译器进行了优化,那我们就只进行构造:

类和对象(末)_第87张图片

 我们没有进行拷贝构造,编译器执行了优化。

传参也可以优化

正常情况下的传参:

void f1(A aa)
{}
int main()
{
	/*A aa1 = 1;
	return 0;*/
	A aa1;
	f1(aa1);
	return 0;
}

我们进行运行的结果应该是一个构造加上一个拷贝构造。

类和对象(末)_第88张图片

 如图所示。

我们可以这样写来进行优化:

类和对象(末)_第89张图片

 这时候,我们进行编译:

我们只调用了一个构造函数。

假如我们在这里传一个引用呢?

 然后我们进行调用:

类和对象(末)_第90张图片

 我们进行编译:

类和对象(末)_第91张图片

 和上次的结果相同,但是

注意:这个写法在有些编译器下是不支持的,因为有些编译器会把匿名对象看作常性,我们不能用引用来接收常数,所以会报错。

 最后一个优化:

A f2()
{
	A aa;
	return aa;
}
int main()
{
	f2();
}

我们进行编译:正常的情况下应该是构造加上拷贝构造。

类和对象(末)_第92张图片

 这样不可以优化,但是这种写法呢?

A f2()
{
	A aa;
	return aa;
}
int main()
{
	A ret=f2();
}

这种写法实际上经历了三次构造

1:创建对象aa的构造

2:返回aa时会调用拷贝构造

3:A ret=f2()这里也会调用拷贝构造。

所以这里应该是构造 拷贝构造 拷贝构造。

我们进行编译:

类和对象(末)_第93张图片

我们发现,编译器优化掉了一个拷贝构造函数。

那我们看这样写可以优化吗?

A f2()
{
	A aa;
	return aa;
}
int main()
{
	A ret;
	ret = f2();
}

我们进行分析:

首先,我们先创建一个对象ret,调用构造函数。

然后调用f2函数,先创建一个对象aa,调用构造函数

然后返回aa,调用拷贝构造,然后释放掉aa。

然后调用赋值重载:

类和对象(末)_第94张图片

 可以发现,这种写法不能优化。

假如我们不用对对象的数据进行修改时,我们可以这样写:

A f3()
{
	return A(10);
}
int main()
{
	A ret = f3();
	return 0;
}

这里实际上发生了构造,拷贝构造,拷贝构造。

但是我们这里进行了优化:

类和对象(末)_第95张图片

编译器只执行了一个构造。

总结:创建对象时,如何进行优化

1:能不创建“中间商”就不创建“中间商”

2:能直接返回匿名对象就直接返回匿名对象。 

你可能感兴趣的:(c++,数据结构,开发语言)