C++——类与对象(下)

1.再谈构造函数

1.1引入

MyQueue的默认构造函数可以不写,因为它的自定义类型成员会去调用自己的默认构造函数。

//用两个栈实现队列
class MyQueue
{
private:
	Stack _st1;
	Stack _st2;
};

但如果Stack类不提供默认构造函数怎么办?

此时MyQueue的默认构造函数生不成,就会报错,所以此时就需要自己去书写构造函数,显式调用构造函数,但是在函数体中自定义类型调用不了函数。

此时就要引入初始化列表

构造函数初始化分为两种:

一、在构造函数体内去写初始化

二、初始化列表

1.2在构造函数体内赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date
{
public:
Date(int year, int month, int day)
{
     _year = year;
     _month = month;
     _day = day;
}
private:
     int _year;
     int _month;
     int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对 对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为 初始化只能初始化一次,而构造函数体 内可以多次赋值

1.3初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

class Date
{
public:
	Date(int year, int month, int day)
		:_year()//可以不传值
		,_month(month)
		,_day(day)	
	{

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

int main()
{
	Date d1(2024, 9, 4);

	return 0;
}

1.3.1注意:二者可以混着使用

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)	
	{
		_day = 1;
	}
//...
private:
	int _year;//每个成员声明
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 9, 4);

	return 0;
}

1.3.2初始化列表是每个成员定义的地方

对变量而言,声明和定义的区别在于:声明时不开空间,对象实例化、定义时才会去开空间。

三个成员在Date d1(2024, 9, 4);这里整体定义,整体定义时开空间。

那么,每个成员在什么时候定义呢?

C++认为,初始化列表就是每个成员定义的地方,定义时要给值。

问题又来了,为什么要这样设计这样一个看似多此一举的初始化列表?在Date d1(2024, 9, 4);这里对对象整体定义不好吗?

1.3.3原因

设计初始化列表是为了解决三种情况,或是说填三个大坑。

1.3.3.1

const修饰的成员在构造函数体内给值是不可行的,因为它必须在定义时初始化,const修饰说明它不能再次被赋值修改,要赋值只有一次机会,就是在定义时。

所以_x定义的地方要在初始化列表

class Date
{
public:
	//初始化列表是每个成员定义的地方
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_x(1)
	{
		//_year = 1;
		_day = 1;

	}

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

private:
	int _year;
	int _month;
	int _day;

	//必须定义时初始化
	const int _x;
};
1.3.3.2

在成员变量声明处声明引用

成员变量可以定义引用,因为在声明处,可以不给值,但是引用有个特点:必须在定义时初始化

class Date
{
public:
	Date(int year, int month, int day,int& i)
                    //Date d1(2024, 9, 4, n);
		:_year(year)
		,_month(month)
		,_x(1)
		,_refi(i)//把n传给i,i是n的别名,_refi又是i的别名
	{
		_day = 1;

	}
    
    void func()//调用这个函数,外面的a会随之改变
	{
		_refi++;
		_refi++;
	}

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

private:
	int _year;
	int _month;
	int _day;

	//必须定义时初始化
	const int _x;
	int& _refi;

};

int main()
{
	int a = 0;
	int& n = a;

	Date d1(2024, 9, 4,a);
	
    d1.func();
	
    return 0;
}

但如果是在构造函数体内

class Date
{
public:
	Date(int year, int month, int day,int& i)
		:_year(year)
		,_month(month)
		,_x(1)
	  //,_refi(i)
	{
		_refi=i;//明显就是赋值了
	}
//...
};
1.3.3.3

成员中有自定义类型,但是,

1.自定义类型成员没有默认构造函数

2.自定义类型成员有默认构造函数,但是不想去调用它的默认构造函数

就比如上面提到的MyQueue,此时就要去调用带参数的构造函数,在定义初始化时去调用

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

class Date
{
public:
	//初始化列表是每个成员定义的地方
	Date(int year, int month, int day,int& i)
		:_year(year)
		,_month(month)
		,_x(1)
		,_refi(i)
		,_a(1)
    //定义初始化的地方
	{
		_day = 1;

	}

	void func()
	{
		_refi++;
		_refi++;

	}

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

private:
	int _year;
	int _month;
	int _day;

	//必须定义时初始化
	const int _x;
	int& _refi;
	A _a;

};

int main()
{
	int a = 0;
	int& n = a;
	Date d1(2024, 9, 4, n);

	return 0;
}

1.3.4补充

调用构造函数时,先执行初始化列表,再执行函数体

这里代码执行到489行时,其它成员已经初始化,而_day却是随机值也可以证明这个。

实际上在初始化列表阶段,_day也定义了,但是对于内置类型,编译器是不处理的,所以这里给了随机值,而自定义类型就算不写在初始化列表处,编译器也会作出处理,会去调用它的默认构造函数,当然前提它要有默认构造函数

总结:在构造函数的初始化列表阶段,对内置类型编译器是不做处理的,是用随机值初始化(有些编译器可能会初始化为0),对自定义类型,会去调用它的默认构造函数

初始化列表是每个成员定义的地方!!!不管写不写初始化列表,调用构造函数时都会去执行这一部分,初始化列表就是构造函数的一部分

如果自定义类型既有默认构造函数,又显式写了比如_a(1);  此时会使用显式写的,只有不传值时才会去调用它的默认构造函数

1.3.5声明时给缺省值

class Date
{
public:
	//初始化列表是每个成员定义的地方
	Date(int year, int month, int day,int& i)
		:_year(year)
		,_month(month)
	  //,_day(day)
		,_x(1)
		,_refi(i)
		,_a(1)
	{
		_day = 1;//赋值
	}

	void func()
	{
		_refi++;
		_refi++;

	}

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

private:
	int _year;
	int _month=12;
	int _day=20;//给值不是初始化,给的是缺省值
//C++11支持给缺省值,但这个缺省值其实是给初始化列表的
//如果初始化列表没有显式给值,就使用这个缺省值
//如果显式给值了,就不使用这个缺省值

	//必须定义时初始化
	const int _x;
	int& _refi;
	A _a;

};

C++11支持给缺省值,但这个缺省值是给初始化列表的,比如上面的代码,_day没有在初始化列表处显式给值,此时_day会使用缺省值初始化,而_month在初始化时显式给值了,就不会使用缺省值了。

缺省值在参数处、成员声明处可以存在,可以看作备胎。

初始化列表是按照声明的顺序执行的,和初始化列表中的书写定义顺序无关

注意:const修饰的成员也可以只在声明处给缺省值,它有了缺省值后,在初始化列表不进行书写也是可以的

初始化列表的执行顺序只是说祖师爷选择了这种方式,如果按照定义顺序去执行,那么如果有些成员不在初始化列表中显式书写,就不好去确认它们的先后顺序了。

1.3.6总结

1. 每个成员变量在初始化列表中 只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员时,必须放在初始化列表位置进行初始化:
引用成员变量 、const成员变量 、自定义类型成员(且该类没有默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
4. 成员变量在类中 声明次序就是其在初始化列表中的 初始化顺序,与其在初始化列表中的先后次序无关
//我们来看一道题
//    A.输出1 1
//    B.程序崩溃
//    C.编译不通过
//    D.输出1 随机值

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)//这里会先使用_a1来初始化_a2,但是这里_a1是随机值
	{}

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

//答案是D

5.总之,所有的成员能使用初始化列表初始化就尽量使用初始化列,当然,有些场景还是需要初始化列表和函数体混着使用,比如下图

//书写Stack类的构造函数时
class Stack
{
	Stack(int n)
		:a((int*)malloc(sizeof(int) * n))//这里不混着写就不方便检查失败了
		,top(0)
		,capacity(n)
	{
		cout << "Stack(size_t n = 4)" << endl;
		//检查是否失败还是要放在这里
		if (n == 0)
		{
			a = nullptr;
			top = capacity = 0;
		}
		else
		{	
			if (a == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
		}
		//同时初始化列表无法解决初始化问题
		//那么这里想给数组初始化,还需要自己写一段代码
		memset(a, 0, sizeof(int) * n);
		//不能做到不写上述代码,只在初始化列表中操作,有时候还需要调用一些函数
	}
//...
}

1.4explicit关键字

1.4.1引入

class A
{
public:
	A(int i)
		:_a(i)
	{
		cout << "A(int i)" << endl;
	}

	A(const A& aa)//用来验证第二个会不会先构造再拷贝构造
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
private:
	int _a;
};

int main()
{
	A aa1(1);//这里是直接构造

	A aa2 = 2;//这里为什么也能通过?
//这里是先构造,再拷贝构造,再优化

	//这里本质是隐式类型转换
	//C++支持单参数构造函数的隐式类型转换
	//隐式类型转换中间要生成一个临时对象

	//这里应该是用2去调用A的构造函数生成一个临时对象,再用这个对象去拷贝构造aa2
	//但是这里编译器又有优化,优化为使用2直接构造
	//这里的直接构造是优化的结果
	//所以它不会先构造再拷贝构造

	//所以这两段代码的过程不一样,但结果是一样的

	return 0;
}

注意:拷贝构造也可以使用初始化列表,因为它也是一个构造,它是一个特殊的构造

class A
{
public:
	A(int i)
		:_a(i)
	{
		cout << "A(int i)" << endl;
	}

private:
	int _a;
};

int main()
{
	A aa1(1);//这里是直接构造

	A aa2 = 2;

	//A& ref = 2;//会报错

	//原因不是int类型不能转换为A类型
	//是因为这里支持隐式类型转换,中间会生成临时对象,临时对象具有常性
                    //(与传值返回会生成临时对象类似)
	//所以ref不能引用这个临时对象,发生了权限的放大

	const A& ref = 2;//加上const就可以了
//这里可以引用是因为单参数的构造函数支持隐式类型转换,参数的整型值能够转换为一个A的对象

	return 0;
}
1.4.1.1适用场景
class A
{
public:
	A(int i)
		:_a(i)
	{
		cout << "A(int i)" << endl;
	}

private:
	int _a;
};

struct SeqList
{
public:
	void PushBack(const A& x)//假设A对象比较大?

	{
		// ... 扩容
		_a[_size++] = x;
	}
//...
private:
	A* _a = (A*)malloc(sizeof(A) * 10);
	size_t _size = 0;
	size_t _capacity = 0;
};

int main()
{
	SeqList s;

	//此时想要插入数据会很不方便,需要自己先定义
	A aa3(1);
	s.PushBack(aa3);

	//但使用上面的语法,就会方便许多
	s.PushBack(4);
	//可以这样写的原因是隐式类型的转换

	return 0;
}

1.4.2explicit的一种用法

构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现

1. 构造函数只有一个参数

2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值

3. 全缺省构造函数
class A
{
public://如果不想让隐式类型转换发生
//在构造函数这个位置加上explicit就可以让隐式类型转换不去发生,就不支持这个转换了
//禁止隐式类型转换后还可以提高代码可读性
	explicit A(int i)
		:_a(i)
	{
		cout << "A(int i)" << endl;
	}

private:
	int _a;
};

1.4.3补充:隐式类型转换

int main()
{
	int i = 0;
	double d = 1;
	i = d;
    //这里d可以赋给i
	//类型转换,中间都会生成一个临时变量

	return 0;
}

如何证明呢?

int main()
{
	//如何证明?
	double d = 1;
	int i = d;

	//int& j = d;
	//这里j不能引用d
	//但不能引用的原因不是类型不同
	//是因为中间生成了临时变量,临时变量具有常性,发生了权限的放大
	
    const int& j = d;
	//所以加上const就可以引用了

	return 0;
}

上述代码都是同理

1.4.4多参数构造函数

class B
{
public:
    //explicit B(int b1,int b2)//同理,这样就不会发生类型转换了
	B(int b1,int b2)
		:_b1(b1)
		,_b2(b2)
	{
		cout << "B(int b1,int b2)" << endl;
	}

private:
	int _b1;
	int _b2;
};

int main()
{
	//多参数的也无法给值
	//B jx = 1, 2;
	//难道这样给吗?当然是不行的
	
	//C++98不支持多参数隐式类型转换

	//但是C++11是支持多参数的隐式类型转换的
	B bb1(1, 2);

	B bb2 = { 1,2 };//与上述语法原理相同
	
	//B& ref = { 1,2 };//这里不支持
	const B& ref2 = { 1,2 };//这里就支持了

	return 0;
}

//	s.PushBack{1,2};//刚刚那里如果是B可以这样传值

1.5匿名对象

C++中还允许定义一个东西,叫做匿名对象。

int main()
{
	//有名对象	特点:生命周期在当前局部域
	A aa6(1);//可以这样定义对象

	//匿名对象	特点:生命周期只在这一行    匿名对象别人使用不了
	A(10);//还可以这样定义对象
	//这里先去调用构造,执行完这一行后就直接去调用析构了
	
	A aa7(2);//然后这里再次调用构造
}

注意:匿名对象、临时对象都具有常性

1.5.1适用场景

1.5.1.1
int main()
{
    SeqList s1;
    s1.PushBack(aa6);
    s1.PushBack(aa7);
    //平时传参,都需要先去定义一个有名对象,再去传参
    //先定义对象,再传参是很麻烦的

    //又恰巧此时隐式类型转换被禁用了 explicit A(int i)
    //那么这个语法就也没办法使用了  s1.PushBack(8);

    //这时,使用匿名对象就会很方便了
    s1.PushBack(A(10));
    //匿名对象的用途也没有那么核心,但是它可以简化代码
}
1.5.1.2在OJ题中
//C++的OJ题通常类似这样

class Solution {
public:
//io型、接口型的,都把函数放在一个类中
	int Sum_Solution(int n) {
		// ...
		return n;
	}
private:
};

int main()
{
	//正常情况想要去调用函数
	//需要先定义一个有名对象,再使用有名对象去调用函数

	Solution sl;
	sl.Sum_Solution(10);

	//然而定义对象就是为了调用这个函数,还要写两行代码,有些麻烦
	//这时使用匿名对象也十分方便

	Solution().Sum_Solution(100);//直接定义一个匿名对象去调用函数
//Solution()这里定义了一个对象,只是这个对象没有取名字
//会去调用它的构造函
//调用它的构造函数是为了调用int Sum_Solution(int n)这个函数

	//又比如要把今天的日期打印一下
	Date d(2024, 9, 4);
	cout << d;
	//假设只是要求打印日期,却还要去取个名字,有些麻烦

	cout << Date(2024, 9, 4);
	//如果使用这个对象只使用一次,匿名对象就会十分方便

    return 0;
}

2.static(静态)成员

声明为 static的类成员称为 类的静态成员,用 static修饰的 成员变量,称之为 静态成员变量;用 static修饰成员函数,称之为 静态成员函数静态成员变量一定要在类外进行初始化。

2.1引入

class A
{
public:
	A()
	{
	}
	A(const A& t)
	{
	}
	~A()
	{
	}

private:
};

假如有些需求
1.想要统计下A这样的一个类创建了多少个对象,累计创建了多少个对象?
2.统计下正在使用的还有多少个对象?

如何达成目的?

2.1.1方法一:定义全局变量

int n=0;//代表累计创建的对象数

int m=0;//代表正在使用的对象数

//A的所有对象都是构造出来或是拷贝构造出来的
class A
{
public:
	A()
	{
		++n;
		++m;
	}
	A(const A& t)
	{
		++n;
		++m;
	}
	~A()
	{
		--m;
	}

private:
};

//A& func(A& aa) //所以可以使用引用就最好去使用引用
A func(A aa)
{
	return aa;
}

int main()
{
	A aa1;
	A aa2;
	cout << n << " " << m << endl;    //2	2

	A();//定义匿名对象,它的生命周期只在这一行
	cout << n << " " << m << endl;    //3	2
	
	func(aa2);//如果使用引用,则不会去创建新的对象
	cout << n << " " << m << endl;    //5	2

	return 0;
}
2.1.1.1问题

但是这种写法存在很大的缺陷,C++讲究封装,定义全局变量虽然可以解决问题,但是n和m可能会在外面被人随意修改,比如当有人书写了 n--;  m++; 这样的代码时。

那么把m和n定义到函数的私有成员处能不能解决这个问题

class A
{
public:
	A()
	{
		++n;
		++m;
	}
	A(const A& t)
	{
		++n;
		++m;
	}
	~A()
	{
		--m;
	}
private:
	int n = 0;
	int m = 0;

//显然是不行的,这样的话,每个对象就都有m和n了
//我们不期望这个变量属于某一个对象
//而是期望它们属于整个类,是全局的,属于所有对象
};

2.1.2方法二:定义静态成员变量

class A
{
public:
	A()
	{
		++n;
		++m;
	}
	A(const A& t)
	{
		++n;
		++m;
	}
	~A()
	{
		--m;
	}
private:
	static int n;//加上static,成员就属于所有对象了,它是全局的
	static int m;
};
2.1.2.1注意

一、静态变量不能在这里给缺省值了,缺省值是在初始化列表阶段使用的,而静态变量不会去执行初始化列表阶段,初始化列表是某个对象的成员的初始化,而静态变量不是属于某个对象,它属于所有对象

二、此时sizeof(A)应该是1,这两个成员不会存储在对象中,因为它们不是属于某个对象,而是属于所有对象,它们存储在静态区

三、私有成员这里是声明不会去开空间

//定义时要去说明它是属于A这个类域的

int A::n = 0;
int A::m = 0; 

//它们是私有的,不能在类外面访问
//这里只是相当于声明与定义分离,而不是在外访问
//如果有.h和.cpp的话,这个要写在.cpp中,否则会出问题
//成员变量不能声明定义放在一起,所以必须声明和定义分离

//此时就无法随意修改n和m了
//多次调用构造函数访问到的就是同一个m和n

四、如果m和n是公有的,还可以使用别的访问方式。

突破类域有三种方式

int main()
{
	//如果m和n是公有的,还可以这样访问

	cout << A::n << " " << A::m << endl;//突破类域的一种方式
	// 属于类域

	cout << aa1.n << " " << aa1.m << endl;
	// 属于所有对象

	//直接写m或者n是不行的,因为它们二者属于类域、命名空间域

	A* ptr = nullptr;
	cout << ptr->n << " " << ptr->m << endl;

//注意,可以这样访问不代表n、m就在aa1或ptr指向的对象里面
//从底层来说,n、m是不在aa1或ptr指向的对象里面的
//上面两种方法仅仅只是帮助它突破类域

	return 0;
}

五、如果m和n是私有的,此时想要访问,就要写一个公有的成员函数

class A
{
public:
//...
	int GetM()
	{
		return m;
	}

	void Print()//在类域中可以访问n和m
	{
		cout << n << " " << m << endl;
	}

private:
	static int n;
	static int m;
};

2.1.3新的场景

int main()
{
	A();
	A();

	//上面定义了两个匿名对象
	//那么这里如何访问呢?

	A().Print();//使用匿名对象调用函数是可以的
	//但是这里会再创建一个对象,就干扰了我们的逻辑

	//A::Print();//这样访问也是不行的
	//报错信息:“A::Print”: 调用非静态成员函数需要一个对象

	A aa1;
	func(aa1);
	aa1.Print();//这里可以访问

	return 0;
}

2.2静态成员函数

class A
{
public:
//...
	int GetM()
	{
		return m;
	}

	//这里static与返回值无关,它表明这是一个静态成员函数
	static void Print()//在类域中可以访问n和m
	{
		cout << n << " " << m << endl;
	}
	//如果有静态成员变量,一般就会提供静态成员函数来访问静态成员变量

private:
	static int n;
	static int m;
};

int main()
{
	A();
	A();

	//上面定义了两个匿名对象
	//那么这里如何访问呢?

	//这时就需要使用静态成员函数
	A::Print();

	//这里就不需要传this指针了,只需要突破类域即可
	//不使用对象去调用,而是使用类域去调用

	A aa1;
	func(aa1);
	aa1.Print();
	//三种突破类域的方式依旧可以用来访问静态成员函数

	return 0;
}

静态成员函数的特点没有this指针

之所以调用普通成员函数时要使用对象去调用,是因为要去对象里面找,要使用this指针
比如 aa1.GetM();

静态成员函数的限制

class A
{
public:
//...
	int GetM()
	{
		return m;
	}

	//这里static与返回值无关,它表明这是一个静态成员函数
	static void Print()//在类域中可以访问n和m
	{
        //_x++;
//静态成员函数不能访问非静态成员,因为没有this指针
//普通成员函数可以访问都是通过this指针来访问的

		cout << n << " " << m << endl;
	}
	//如果有静态成员变量,一般就会提供静态成员函数来访问静态成员变量

private:
	static int n;
	static int m;

    int _x=0;
};

2.2.1练习题

求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)

//此题从教学角度完美体现了静态成员的价值

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;


//C++的题都会使用Solution包起来
//为了防止冲突
class Solution {
public:
    int Sum_Solution(int n) {
        Sum a[n];//牛客使用g++,支持一个 变常数组(C99支持)
        //VS编译器不支持
        //利用这种方法就把n次构造函数给调用了
        return a[0].GetRet();

    }
};

//比如想调用10次构造函数
//需要定义10个对象
//如何定义10个对象? 可以定义一个数组
//比如 A arr[10];

//私有和保护限制的是能否在类外直接访问,而不是说不能被修改

2.3总结特性

1. 静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在 类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数 没有隐藏的 this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
注意:
1. 静态成员函数不可以调用非静态成员函数
2. 非静态成员函数可以调用类的静态成员函数

3.友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会 增加耦合度破坏了封装,所以友元要谨慎地使用, 不宜多用
友元分为: 友元函数友元类

3.1友元函数

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

3.1.1注意

1.友元函数可以访问类的 私有和保护成员,但它 不是类的成员函数,所以友元函数没有 this指针,它只是一个 友元声明
2.友元函数 不能用const修饰,它没有 this指针静态成员同理
3.友元函数可以在类定义的任何地方声明, 不受类访问限定符限制,它可以在类中可以随意放置,它只是一个 声明
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同,只是说语法检查时会把访问私有成员的地方执行通过

3.2友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time
{
 friend class Date; 
//声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量

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

class Date
{
 
    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;//这里定义了一个Time类的对象
};

3.2.1注意

1.友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想 在Time类中访问Date类中私有的成员变量则不行。
2.友元关系不能传递,比如B是A的友元,C是B的友元,但这不能说明C是A的友元。
3.友元关系不能继承

3.3总结

友元的优势就是 方便,但是尽量还是 能不使用友元 就不要使用友元。

4.内部类

4.1概念

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更 不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

4.2注意

内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

4.3特性

1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。
class D
{
private:
	int _d;
};

class C
{
public:

private:
    int _c;
	B _bb; //这样写sizeof(C)才会是8
};
//类比上面,上面也是两个独立的类

//1.这里D类受C类域和访问限定符的限制,实际上它们也是两个独立的类
//2、内部类默认就是外部类的友元类

//所以C对象里没有D
class C
{
public:
	class D
	{
	private:
		int _d;
	};

private:
	int _c;
};

int main()
{
	cout << sizeof(C) << endl;//这里是4

	return 0;
}

//分开写和放在一起写是几乎一样的

class C
{
//public:
	class D
	{
	private:
		int _d;
	};

	void func()
	{
		D dd;
		//此时只有类里面才能使用D定义对象
	}
private:
	int _c;
};

int main()
{
	cout << sizeof(A) << endl;//这里是4

	C cc;
	//C::D dd;
	//前提D是公有的,才可以这样写

	//内部类不想被别人使用,就可以定义为私有
	//此时只有类里面才能使用它定义对象

	return 0;
}

4.4改进练习题

求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)

//改进1
class Solution {
	class Sum//外部无法使用Sum类定义对象
	{
	public:

		Sum()
		{
			ret += _i;
			_i++;
		}

		static int GetRet()
		{
			return ret;
		}
	private:
		static int _i;
		static int ret;
	};

public:
	int Sum_Solution(int n) {
		Sum a[n];
		return Sum::GetRet();

	}
};

int Solution::Sum::_i = 1;
int Solution::Sum::ret = 0;
//改进2
class Solution {
    class Sum//外部无法使用Sum类定义对象
    {
    public:

        Sum()
        {
            ret+=_i;
            _i++;
        }

};

public:
    int Sum_Solution(int n) {
        Sum a[n];
        return ret;

    }
private:
    static int _i;
    static int ret;
};

int Solution::_i=1;
int Solution::ret=0;

5.拷贝对象时编译器做出的一些优化

5.1回顾

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

	A(const A& aa)
	{
	}

	void Print() const
	{
		cout <<"Print->" << _a << endl;
	}
private:
	int _a = 0;
};

void f1(A aa)//这里传参调用拷贝构造函数是很有意义的
{
	aa.Print();
}

int main()
{
	A aa1;
	f1(aa1);

	return 0;
}

void f1(const A& aa)//使用引用可以减少一次拷贝构造
  //而且这里最好加上const
{
	aa.Print();
//但是如果只是调用了一个简单的函数
//那这里就没有必要去拷贝
}

int main()
{
	A aa1;
	f1(aa1);

	f1(A());//匿名对象,具有常性,Print函数不加const就会报错
	f1(2);//隐式类型转换,生成临时对象,具有常性,容易报错
//上面加上const可以避免报错

	return 0;
}

甚至还可以这样调用函数

void f1(const A& aa=A())
//这里匿名对象的生命周期不只在这一行
{
	aa.Print();
}

int main()
{
	A aa1;
	f1(aa1);

	f1(A());
	f1(2);  
    f1(); //甚至还可以这样调用函数


	const A& ref = A();   
	// const引用会延长匿名对象的生命周期
	// 此时ref出了作用域,匿名对象才会销毁
    //本质是引用让匿名对象变成了有名对象
    //匿名对象的生命周期就跟着ref引用走了

	return 0;
}

5.2编译器会做出的优化

5.2.1

void f1(A aa)
{
	aa.Print();
}

int main()
{
	A aa1;
	f1(aa1);

	f1(A());
//这里看似和上面调用函数的过程是一样的,其实不然

	return 0;
}

在上面的隐式类型转换时提到过,编译器在同一个表达式同一行同一个调用一个连续的步骤里面,比如这里就是连续的构造、拷贝构造,它就会被优化,编译器认为连续的构造、拷贝构造太过浪费,于是会将二者合二为一,将其优化为直接构造

注意:C++标准并未规定要进行优化,这只是目前大多数主流编译器所达成的共识。

void f1(A aa)
{
	aa.Print();
}

int main()
{
	A aa1;
	f1(aa1);//这里是两个表达式,所以不会合二为一

	cout << "--------------------------------" << endl;
	// 一个表达式,连续的步骤里面,连续的构造可能会被合并
	f1(A());

	cout << "--------------------------------" << endl;
	f1(1);
	
	cout << "--------------------------------" << endl;
	A aa2 = 1;//和上面同理

	cout << "--------------------------------" << endl;
	A aa3 = A(12);

	return 0;
}

证明

5.2.2

A func()
//传引用返回就不会拷贝了,但这里不能,如果对象中带析构,那会出更多的问题
{
	A aa;//一次构造
	return aa;//一次拷贝构造
}

int main()
{
	A ret1=func();//这里本来应该是2次拷贝构造
	cout << "--------------------------------" << endl;

	A ret2;
	ret2 = func();//这种情况编译器就优化不了了

	//1.同类型才能优化,拷贝构造和赋值不能合并
	//2.在同一个步骤中才能优化

	return 0;
}

5.2.3

A func()
{
	//	A aa;
 	//return aa;
	
	//return A(1);//写法一

	return 2;//写法二
	//隐式类型转换

	//能这样写最好这样写,首先这种函数不能使用引用返回
	//这样写的话,它会触发编译器的优化,减少拷贝,提高效率
}

int main()
{
	A ret1 = func();//构造+拷贝构造+拷贝构造 优化为了 直接构造

	cout << "--------------------------------" << endl;
	A aa2 = 1;

	cout << "--------------------------------" << endl;
	A aa3 = A(12);
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值