三、c++学习(指针&引用详解)

上一次直播好像过去很久了,中间有加班,有5 1假期等,现在5 1放假完了,所以继续卷。

C++学习,b站直播视频

3.1 指针

这个指针,考虑了很久,一直不知道在哪个地方介绍,为啥纠结,是因为c++有智能指针,智能指针有涉及了很多以后的知识。所以就一直在纠结,但是后来想想还是把智能指针和指针拆开,这样会更好,拆都拆开了,那肯定把指针提前了,所以就放在了这个位置。

3.1.1 变量是什么

在我刚学c语言的时候,一直对指针处于懵逼状态,是因为当初没有好好听课,哈哈哈。以为读了大学就可以放飞自我,结果最后才发现,这是高中老师撒的弥天大谎。

直到后来学了linux课程之后,才发现指针其实很简单的。

想理解指针先来看看变量是什么??

我们从第一节课开始就写了这种代码,申请一个变量:

int a = 1;

我们反汇编看看,这个变量是什么?

我们看了汇编代码:(windows会自动显示符号名,我们需要不显示符号名)

    91: 	int a = 1;		
00007FF7228E2CDE C7 45 04 01 00 00 00 mov         dword ptr [rbp+4],1  
    92: 	int b = 2;
00007FF7228E2CE5 C7 45 24 02 00 00 00 mov         dword ptr [rbp+24h],2  

// 好像目前还没学习汇编知识,不过没关系,今天我们只知道rbp寄存器存储的是栈基地址,看着b申请的内存,栈是往上加的,但是linux的栈是满减栈。
// rbp+4,也就是在栈基地址偏移0x4字节的位置上,开辟一个变量a的内存,我们现在都是高级语言,直接申请一个变量,通过汇编给我们指定地址,如果还是按之前写汇编代码的话,就需要直接rbp+4了,想想就头大。
// 当然如果大家不信,可以取a的地址&a,这个值就是rbp+4的值。
// mov是一条指令,一个很常见的汇编指令,其实这个指令就是把数字1赋值给rbp+4,从上面我们知道rbp+4就是a,所以其实就是给a赋值1.

// 但是我万万没想到,紧接着申请b,b的栈偏移竟然是+24h,中间的20h的字节去哪了?其实我也不知道

不过,我持怀疑的态度,监控了一个内存,然后gdb执行了两个赋值语句,发现确实如此,我也不知道中间这些干了啥。

在这里插入图片描述

不了解这些内存是干啥之前,还是不发表意见,哈哈哈。

变量其实就是我们高级语言中,给一块内存起的名字,上面我们介绍的是在栈内存,其实变量也是存储在堆中,或者全局区,不管存储在哪,变量都是一块内存的别名。

前面也介绍了不同类型的变量占的内存大小不一样了,这里就不描述了。

3.1.2 指针是什么

上面介绍了变量是啥,接下来我们看看指针是啥。

指针的定义:

int* pa = &a;		// &a是一个地址 int* 是int型的指针
// int型的指针指向a的地址

我们继续反汇编查看:

    94: 	int* pa = &a;
00007FF7D8E82CEC 48 8D 45 04          lea         rax,[rbp+4]  
00007FF7D8E82CF0 48 89 45 48          mov         qword ptr [rbp+48h],rax  

这windows编译器搞的有点奢侈啊,第三个变量给我存储的偏移是+48h。

我们定义一个指针,windows编译器给我们汇编成了两步,lea指令其实就是把地址加载到目标寄存器中,这样用的是rax,现在寄存器rax的值就是rbp+4的地址。

第二步就是,把rax的值赋值给rbp+48h了,真的是太奢侈了。

我们来看看gdb的图

在这里插入图片描述

所以pa的值就是0x000000d25c10ef74,这个就是一个内存地址,其实这个值就是我们a的地址,我们来可以来看看这个地址指向的值。

在这里插入图片描述

介绍了那么多,现在知道指针是什么了吧,指针其实也是一个变量,相当于a一样,只不过指针变量存的是地址,那存谁的地址,存的是变量的地址,比如a的地址。我们上面分析了a其实是跟一个地址绑定的,我们存的就是a的这个地址。

这里画一个图:

在这里插入图片描述

大概就是这个意思。

另外:

int *p = &a;		// int *定义的就是指针,指针变量虽然存的都是地址,都是地址里的内容可能不一样,有可能是int、float、long之类,我们a是int类型,所以定义的是int *
// p 的值其实就是 0x000000d25c10ef74
// 那存这个值有啥用,其实是可以间接访问。
*p;  // 这个就是可以间隔访问a的值
// 因为p是int *,所以*p就是把 0x000000d25c10ef74 0x000000d25c10ef75 0x000000d25c10ef76 0x000000d25c10ef77  这四个字节的地址看成一个int,所以这个值刚好是1
// 注意 p这个指针,其实也占有内存,因为p也是一个变量,64位系统,占的是8字节
// 这个可以自己反汇编分析分析

3.1.3 指针的应用

通过上面介绍,我们认识到了指针是什么了,那指针有什么用处呢?

这里就有一个经典的例子了,用一个函数交换两个值。

// 这个是不能交换a b的值
void swap(int a, int b)
{
	int temp;

	temp = a;
	a = b;
	b = temp;
}

// 所以需要使用指针,指针传的就是两个地址,就可以交换
// 当然c++还可以传引用,引用后面介绍
void swap(int* a, int* b)
{
	int temp;

	temp = *a;
	*a = *b;
	*b = temp;
}

int main()
{
	int a = 1;
	int b = 2;

	cout << "a = " << a << " b = " << b << endl;
	swap(a, b);
	cout << "a = " << a << " b = " << b << endl;
}

3.1.4 数组 & 指针

我们在上面学习过数组,那指针跟数组的结合是最有意思的了。

3.1.4.1 指向数组的指针
// 先定义一个数组
int a2[5];
a2[0] = 1;
a2[1] = 2;
a2[2] = 3;
a2[3] = 4;
a2[4] = 5;

int* p = a2;		// 数组名其实就是地址
int* p2 = &a2[0];	 // 或者也可以取数组首元素的地址

cout << "*p = " << *p << " *p2 = " << *p2 << endl;
3.1.4.2 指针的运算
// 指针的运算,这个是一个难点
	// 所以后面的go语言的指针,只是指向,并不支持运算,其实指针运算也不难,理解成数组那样就可以了。
	cout << *(++p) << endl;		// 指针++,相当于跳过一个指向数据的大小,比如没有++之前,指向的是a[0],++操作之后,就指向了a[1],移动了4个字节
	// (这个要理解好,是指针运算的核心)
	cout << *(--p) << endl;	// 同理,现在指针指向了a[1],减减就移动回到a[0]

	// 上面的理解好,我们来个难度高的
	cout << *(p + 3) << endl;		// 分析这种,一定要记住当前p指向谁,当前p指向a[0],+3就想当移动3个数组元素,a[3]

	cout << *(p - 2) << endl;	// 同理了,这个差点就错了,因为p+3没有赋值给p,所以这里的-2,是有问题的

	// 指针不支持 乘除
3.1.4.3 数组作为函数参数
void chageArr(int a[5])		// 这个带这个数字是没意义的
{
	a[1] = 11;
	a[3] = 33;

}

// 这个确实跟上面是一样的,编译报错了
void chageArr1(int a[])		// 这个带这个数字是没意义的
{
	a[1] = 11;
	a[3] = 33;
}

// 数组做为函数参数
chageArr(a2);

for (int i = 0; i < 5; i++)
{
    cout << a2[i] << " ";
}
cout << endl;

int a3[4];
chageArr(a3);
for (int i = 0; i < 4; i++)
{
    cout << a3[i] << " ";
}
cout << endl;

看着函数参数中的数组不带下标和带下标都一样,不得不怀疑传输的是啥?

void chageArr(int a[5])		// 这个带这个数字是没意义的
{
	cout << "sizeof(a) = " << sizeof(a) << endl;
	a[1] = 11;
	a[3] = 33;
}

经过打印,sizeof(a) = 8,所以这个数组其实传的是地址。

所以大胆假设,以指针的方式,传参也是对,但是因为用指针的方式,所以需要在写一个参数,表示的长度。

void chageArr(int* a)		// 这个带这个数字是没意义的
{
	cout << "sizeof(a) = " << sizeof(a) << endl;
	a[1] = 11;
	a[3] = 33;
}
3.1.4.4 二维数组
// 介绍完了一维数组,一维数组的升级版就是二维数组
	// 上面介绍过了,就是定义一个页面,都是用一个二维数组
	int a4[4][5];
	// 怎么定义一个指针,才是指向二维数组呢?
	int(*p3)[5] = a4;	// 这个可以这么理解 *p3 是指向a4[4]的一维数组,其中a4[4]里面每个都是一个一维数组,长度是5,所以是(*p3)[5]
	// 知道为啥加括号不?
	// 是因为[]的优先级比*号高,所以需要再括号

	int sum = 0;
	for (int i = 0; i < 4; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			a4[i][j] = ++sum;
		}
	}

	// 那如果p3++;  // 会是啥?  大家想象一下
	cout << hex << "p3 = " << p3 << endl;
	//cout << hex << "++p3 = " << ++p3 << endl;

	cout << dec << sizeof(a4) << endl;

	// p3 = 00000062AFDFF4F0
	// ++p3 = 00000062AFDFF504

	// 这相差是是20字节,为啥这么大,这是因为这个p3指向的是 a[0]-a[3]的一维数组,一维数组中的元素都是5个int的元素,所以都是20字节

	// 那我需要取 a[0][1]
	cout << *(*(p3 + 0) + 1) << endl;		// 这样就对了,p3+i 就是a[0]-a[3]的一维数组,*(p3+1) + 1 才对应a[1][1]的地址
	// 那我需要取 a[1][1]
	cout << *(*(p3 + 1) + 1) << endl;


	// 也可以把第一个元素的首地址赋值给指针
	int* p4 = &a4[0][0];
	// 这个如果要取到a[1][1]
	cout << *(p4 + (5 * 1) + 1) << endl;	// 需要自己算偏移

	// 当然也可以把一维数组赋值给指针
	int* p5 = a4[1];		// 这个跟上面那个是一样的道理
	// 如果要取a[1][1]
	cout << *(p5 + 1) << endl;

	// 如果是二维数组的地址呢
	int(*p6)[4][5] = &a4;
	// 这个就有意思了
	// 取a[1][1]
	cout << *((*((*p6) + 1)) + 1) << endl;		// 这个比int(*p3)[5] = a4; 比这个多了一层,比较少用

	++p6;		// 这个可以自行思考一下
3.1.4.5 二维数组做为函数参数

这个为啥要拿出来再介绍一次呢?因为我之前就不会,哈哈哈

void chageArr1(int(*a)[5])		// 这个带这个数字是没意义的
{
	cout << "sizeof(a) = " << sizeof(a) << endl;

	a[1][1] = 111;
	a[3][3] = 333;

	// 这个5只能强制
}

这个参数是要跟指针的一样,5也是要一致。如果要把5作为变量,就需要用到后面的函数模板了。

二维数组其实还有其他几种方式:

void chageArr1(int** a)
// 这个理解起来有点难度,就是就是把二维数组给强制转换成一个int型的双重指针,
// 所以这个指针++,就是只是移动了4个字节(int的长度),如果要移动到a[1][x]就需要计算了
// 这个肯定不用,只是这里做了个提示
{
	cout << "sizeof(a) = " << sizeof(a) << endl;
	//a[1][1]
	printf("win 0x%x 0x%x\n", (char*)a + 1, ((char*)a + (1 * 5 + 1)));
	printf("win %c\n", *((char*)a + (1 * 5 + 1)));
}

//void chageArr1(int a[2][5])		// 这个中间的2其实不写是一个道理
//{
//	cout << "sizeof(a) = " << sizeof(a) << endl;
//
//	a[1][1] = 111;
//	a[3][3] = 333;
//
//}

//void chageArr1(int a[][5])		// 这个其实跟后面的(*a)指针也是同样的,编译报错
//{
//	cout << "sizeof(a) = " << sizeof(a) << endl;
//
//	a[1][1] = 111;
//	a[3][3] = 333;
//
//}
3.1.4.6 指针数组 & 数组指针

一个面试比较喜欢问的问题,指针数组和数组指针,该说是中国文字比较牛,还是说代码本来都不打算设计这么复杂。

  1. 指针数组

    首先说说啥是指针数组,首先从中文出发,指针做为修饰词,数组是名字,所以这个次的意思是,一个数组,里面存的是指针。我们回归代码:

    // 指针数组
    	int* p7[5];  // 由于优先级p7先跟[]组合,形成一个数组,int* 是数组的内容,表示数组存储的是int*指针
    
    	int c = 1;
    	int d = 2;
    	int e = 3;
    	int f = 4;
    	int g = 5;
    
    	p7[0] = &c;		// p7每个元素都是int*
    	p7[1] = &d;
    	p7[2] = &e;
    	p7[3] = &f;
    	p7[4] = &g;
    
    	// 我们可以用for循环来打印
    	for (int i = 0; i < 5; i++)
    	{
    		cout << *p7[i] << endl;
    	}
    
  2. 数组指针

    数组指针,其实是一个指针,这个指针指向数组,比如我举例写一个指向10个元素的数组的指针。

    // 数组指针
    	int a5[10];
    	int(*p8)[10];		// 是不是有点像二维数组
    
    	p8 = &a5;	// 就是一个二维数组,只不多这个二维数组的第一维是1
    

3.1.5 字符 & 数组

本来没计划讲这个,第一是因为前面已经讲了,第二是c++已经使用string来替代字符数组了。

就简单讲一下。c语言存储字符串,是使用字符数组。

	// 字符数组
	char a6[20] = { "I Love China" };
    char *pa6 = a6;

每一个char都存储一个字符,可以使用for循环打印就知道了。

3.1.5.1 字符串数组

为啥说要介绍这个,因为这个难点就是字符串数组。

// 字符串数组
	char a7[3][10] = { "I ", "Love", "china" };

有点像二维数组,每一个一维数组存储的都是一个字符串。

// 字符数组
	char a6[20] = { "I Love China" };

	// 字符串数组
	char a7[3][10] = { "I ", "Love", "china" };

	// 还有另一种写法
	const char* p10[] = { "I", "Love", "china" };  // linux

	// 这两种写法有啥不一样么?哈哈哈
	printf("a7 %p\n", a7);
	for (int i = 0; i < 3; i++)
	{
		printf("a7 %p\n", &a7[i][0]);
	}

	printf("p10 %p\n", p10);
	for (int i = 0; i < 3; i++)
	{
		printf("p10 %p\n", &p10[i][0]);
	}

我们来看看打印

a7 00000071BD9FF588
a7 00000071BD9FF588
a7 00000071BD9FF592
a7 00000071BD9FF59C
p10 00000071BD9FF5C8
p10 00007FF670CBC0DC
p10 00007FF670CBC0CC
p10 00007FF670CBC0D4

自行猜测一下,哈哈哈

3.1.6 常量 & 指针

常量和指针这个话题,也是面试的时候会问,平时一般不会写这么多的骚操作。

3.1.6.1 常量指针

又是玩中文的时候了,常量指针,其实这是一个指针,指向的是一个常量。

代码表示:

	// 常量指针
	const int a8 = 10;
	const int a9 = 20;

	// 下面两种是都是常量指针,指向常量的指针,技巧:看const后面 如果是*p  就说明是修饰这个指针,所以是常量指针
	const int* p9 = &a8;
	int const* p10 = &a9;

	// 常量指针是不可以修改指向的内容,但是可以修改指针
	//*p9 = 21;  // 报错
	p9 = &a9;	// 完全没问题
3.1.6.2 指针常量
	// 指针常量
	// 指针常量,说明这是一个常量,只不过值指针
	int* const p13 = &a9;

	//p13 = &a8;		// 是一个常量,所以不能换
	*p13 = 11;		// 指向的值就无所谓了
3.1.6.3 指向常量的常量指针

最后来个大boss,结果是最简单的。

	// 指向常量的常量指针
	const int* const p14 = &a9;
	const int const* p15 = &a9;
	// 反正这两个都不可以变

3.1.7 函数 & 指针

终于来了点实用的,不过函数指针在c语言是很重用,不过在c++11之后,可能用的也比较少了,因为c++11推出了新的函数绑定机制,这个后面再说。

3.1.7.1 函数指针

还是这难受的中文,函数指针,说明这是一个指针,指向一个函数。

函数是什么?函数其实是一段代码。这段代码怎么调用?其实是使用了函数名。

这个函数名其实就是一个地址,所以指针是可以指向这个地址的。

// 函数指针
	// void swap(int* a, int* b)
	// 这个函数的函数指针这么写?
	// 有技巧
	void (*pswap)(int* a, int* b);	// 定义了一个函数指针,把函数名用括号括起来,然后写一个*,参数照抄。
	// 就可以写出一个函数指针了,可以省略参数名字:void (*swap)(int*, int*);
	// 看到这个为啥加括号了吧?其实跟数组指针一样,先跟指针结合才是指针

	// 怎么调用呢?
	// 这样只是定义了一个函数指针,需要赋值,如果没赋值直接调用,就马上寄了
	pswap = swap;

	// 调用
	(*pswap)(&a, &b);	 // 这样就调用了,是不是很方便
	// 其实也可以省略*
	pswap(&a, &b);
3.1.7.2 函数指针汇编分析

我有一个不成熟的想法,如果编译器怎么绑定的函数地址,是不是可以优化成直接调用函数地址。

我们反汇编看看。

	// 这样只是定义了一个函数指针,需要赋值,如果没赋值直接调用,就马上寄了
	pswap = swap;
00007FF7A8853257 48 8D 05 5D E1 FF FF lea         rax,[swap (07FF7A88513BBh)]  
00007FF7A885325E 48 89 85 98 05 00 00 mov         qword ptr [pswap],rax  

	// 调用
	(*pswap)(&a, &b);	 // 这样就调用了,是不是很方便
00007FF7A8853265 48 8D 55 24          lea         rdx,[b]  
00007FF7A8853269 48 8D 4D 04          lea         rcx,[a]  
00007FF7A885326D FF 95 98 05 00 00    call        qword ptr [pswap]  
	// 其实也可以省略*

反汇编看了一下,是动态绑定的,哈哈哈,没有说把这个指针调用直接用函数地址替换。

3.1.7.3 指针函数
int* add(int a, int b)
{
	int sum = a + b;
	return &sum;
}

// 指针函数
// 现在都是老司机了,首先是一个函数,返回值是一个指针。
int* ret = add(1, 2);
// 这种写法不可取,我只是简单写一个例子,哈哈哈

3.1.8 二重指针

二重指针就是指针的指针。

可以用在函数参数,我要修改这个函数指针的时候,可以用二重指针。

int chagep(int** a)
{
	*a = new int(1);
	return 0;
}

// 二重指针
int* p16;		
chagep(&p16);	// 在函数内部申请内存,可以把指针的指针传入

cout << *p16 << endl;

// 还有另一个用法,就是字符串数组
const char* p17[] = { "I", "Love", "china" };
const char** p18 = p17;		// 这种也是

3.1.9 void *指针

还剩最后一个知识点了。

void*指针,又叫万能指针,可以随意转换成其他指针。

知道为啥void*指针可以转换成其他指针么? 都发表一下意见。

因为指针的大小是根据系统位数决定的,我们现在都是64位,所有的指针都是8字节,所以申请一个void*指针也是8字节,到时候用的时候,就可以转成其他指针了。

为啥没有void a = 1;

这是因为不知道1是什么类型,类型不一样大小不一样,但是c++11有了一个auto,自动推导,自动推导也是有值才能推,如果没有赋值也会出错。

3.2 引用

c++11中觉得指针比较容易出错,所以引入了一个引用的概念。

3.2.1 左值 & 右值

// 左值和右值的概念
// 这个概念很抽象,不能听懂就算了,以后慢慢再学,我这里也简单提及一下
// 左值:能在等号左边的值,叫左值
// 右值:不是左值的值,叫右值

// 网上有很多这种介绍啥是右值的,分字面量,返回值非引用的,逻辑表达式,啥鬼的,反正没听过

// 我给的一个最简单的方法,就是让编译器来帮我们是是左值还是右值,直接来来怼在等号左边,如果编译器不报错就是左值,报错了,就是右值

int a = 0;
//10 = 10;   // 说明字面量是右值
//a++ = 10;  // 说明这个也是右值
++a = 10;	// 这尼玛是左值,
//a + 10 = 20;	// 这是右值

/*[=] {
return a;
}() = 10;*/		// 返回值是右值,这个以后可能会用,如果加了引用就是左值了

[]()->int& {		// 返回值是引用的值左值
int a = 0;
int& aa = a;
return a;
}() = 10;

3.2.2 左值引用

// 其实这个左值引用,就是我们常说的引用,只不过后面c++11加了右值引用,所以这个引用才改成左值引用
// 现在对左值和右值有点熟悉了吧
// 对左值的引用,就是左值引用
// 引用就是给变量取了别名,但是这个别名不能被修改了,一辈子就跟这个变量好了(指针为啥难,就是因为指针可以变了指向,而且还可以加减,c++吸取了指针的教训,引用只能在初始化绑定,其他地方都不能改了,更不能加减操作了)
int aa = 11;
int& aaa = aa;		// 这个就是aa的引用,因为是左值的引用,其实也是左值引用
int bb = 12;
aaa = bb;		// 这样赋值是修改引用里面的值,而不是修改引用本身

std::cout << "aa: " << aa << " bb:" << bb << std::endl;		// 打印结果被改了

	// 那引用有啥用,其实引用就是替代指针的,因为指针的骚操作太多了,怕指针指向的地址为问题,所以c++才引用了引用,
	// 其实使用引用做为函数参数,在函数内部也是可以修改外部的值的
	swap1(aa, bb);	// 这就交换了
	std::cout << "aa: " << aa << " bb:" << bb << std::endl;		// 引用版的交换,所以引用确实也可以修改外部的值,还不用担心空指针问题


// 引用版的交换
void swap1(int& a, int& b)
{
	int temp;

	temp = a;
	a = b;
	b = temp;
}

3.2.3 右值引用

// 那右值的引用就叫右值引用了
int&& cc = 10;		// 右值引用就是&& (c++语法就是多的恶心)
// 这个右值引用有啥用呢?我目前接触的就是我们要说的移动构造函数,
// std::move  就是库函数中,让我们强制把一个左值引用转成右值引用
Snake s5 = std::move(s4);		// 这样子就可以触发类中的移动构造函数

// 右值引用以后再说用法

3.2.4 引用存在哪

大家有没有好奇,引用存在哪?

我们还是反汇编查看:

   341: 	int& aaa = aa;		// 这个就是aa的引用,因为是左值的引用,其实也是左值引用
00007FF64C018354 48 8D 45 04          lea         rax,[rbp+4]  
00007FF64C018358 48 89 45 28          mov         qword ptr [rbp+28h],rax  

看着这个反汇编代码,就知道是在栈中分配,这时候就有一个邪恶的想法,如果我们用指针改掉这个rbp+28h的值,会不会就改变了引用。

	// 引用存在那
	std::cout << "&aaa: " << aaa << std::endl;

	// 竟然是存在栈里,那我们尝试改一个引用的指向
	int cc = 55;
	int** paa_36 = (int**)((char*)&aa + 36);  // 不知道为啥windows系统两个变量位置差别这么远
	*paa_36 = &cc;

	std::cout << "&aaa: " << aaa << std::endl;

	aaa = 44;
	std::cout << "cc: " << cc << " aa: " << aa << std::endl;

我把cc的地址赋值到rbp+28h的位置了,后来发现引用是真的被改了。

3.3 枚举

3.3.1 c++98时代

我们设计的这个游戏,需要这么多个页面,那到时候怎么切换呢?不知道大家在写单片机的时候,有没有写过状态机代码,就是由一个状态切换到另一个状态,我们这个就是由一个页面切换到另一个页面,我们也把页面当做状态。

enum Page {
	PageWelcome = 0, // 欢迎页面
	PageMenus,		 // 菜单页面
	PageContinue,	 // 继续游戏页面
	PageNew,		 // 新游戏页面
	PageSelected,	 // 选项页面
	PageExplain,	// 说明页面
	PageScore,		// 分数页面
	PageFriend,		// 好友页面
    PageChat,		// 聊天页面
	PageShop,		// 商店页面
    PageLogin,		// 登录页面
};

目前我们定义了这么多页面,枚举的话,主要我们第一项给了初值,其他的都会自动加1。

c++对枚举做了加强,不能用整形变量给枚举赋值,c语言就可以,导致容易超出枚举的范围。

所以c++引入了类型转换,后面介绍。

3.3.2 c++11枚举加强

有时候我使用枚举的时候,也是发现枚举定义的变量,可能会重复,导致每个枚举的变量,都要加上自己枚举的前缀:

enum Page {
	PageWelcome = 0, // 欢迎页面
	PageMenus,		 // 菜单页面
	PageContinue,	 // 继续游戏页面
	PageNew,		 // 新游戏页面
	PageSelected,	 // 选项页面
	PageExplain,	// 说明页面
	PageScore,		// 分数页面
	PageFriend,		// 好友页面
    PageChat,		// 聊天页面
	PageShop,		// 商店页面
    PageLogin,		// 登录页面
};

都加上了Page的前缀,所以c++11对这个枚举加强了:

enum class Page {
	Welcome = 0, // 欢迎页面
	Menus,		 // 菜单页面
	Continue,	 // 继续游戏页面
	New,		 // 新游戏页面
	Selected,	 // 选项页面
	Explain,	// 说明页面
	Score,		// 分数页面
	Friend,		// 好友页面
	Shop,		// 商店页面
};

enum class Test {
	Welcome = 0, // 欢迎页面
	Menus,		 // 菜单页面
	Continue,	 // 继续游戏页面
};

在enum后面加上class,就是强枚举,如我们上面定义的,是不会冲突的,所以使用的时候,也需要加上类名:

Page page = Page::Menus;

3.3.3 c++11可以显示指定枚举底层类型

c++11除了对对枚举加强外,还可以显示指定枚举底层类型。如果不指定,交给编译器自己选择。

enum class Page : char {    // 这样指定的
	Welcome = 0, // 欢迎页面
	Menus,		 // 菜单页面
	Continue,	 // 继续游戏页面
	New,		 // 新游戏页面
	Selected,	 // 选项页面
	Explain,	// 说明页面
	Score,		// 分数页面
	Friend,		// 好友页面
	Shop,		// 商店页面
};

3.4 类型转换

在c语言中,有两种类型转换,一种是隐形的,一种就是强制类型转换,就是上面介绍的修改引用指向的值。

但是在c++11中,为了安全,把类型转换改成了几种。

3.4.1 static_cast

静态转换,一些比较正常的转换,就是一些c语言中隐形类型转换差不多。

比如我们上面的枚举或整形之间的转换:

ePage = static_cast<Page>(index + PageContinue);	// int index不能直接等于枚举类型
double f = 100.34f;
int static_cast<int>f();

子类到父类,或这void*之间转换。

3.4.2 dynamic_cast

动态转换,这个主要用于父类子类之间的,类下一节课就会讲了,用于父类转子类等。

3.4.3 const_cast

这个更简单了,去除指针或引用的const属性,该转换能够将const性质转换掉。

3.4.4 reinterpret_cast

这个就是比较危险的转换,比如我们引用的强转,从一个地址转换成一个指针。

int** paa_36 = (int**)((char*)&aa + 36);
int** paa_36 = reinterpret_cast<int**>((char*)&aa + 36);

3.4.5 总结

来一个总结的图

在这里插入图片描述

3.5 实现页面切换

再我们点击回车键的时候,我们需要把我们选择的index转换成这个枚举:

// 02.3  优化贪吃蛇.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <conio.h>
#include<windows.h>

using namespace std;

constexpr int MENUS_MAX = 9;    // 定义9个菜单

// 我们知道了有字符串数组,我们这词就存储起来
string menus[MENUS_MAX] = { "继  续" , "新游戏" , "选  项" , "说  明", "最高分", "好  友", "聊  天", "商  店", "登  录" };

void hiddenCursor() {
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态
}

void InitWin(char win[22][44])
{
	int height = 22;
	int width = 44;

	// 因为是临时变量,需要初始化,不过我们就一起填充好了。

	// 3.先填充中间的空白
	for (int i = 1; i <= height - 2; i++)
	{
		for (int j = 1; j <= width - 2; j++)
		{
			win[i][j] = ' ';		// 这就是设置为空
		}
	}

	// 4.画四周
	// 4.1 画上边
	for (int i = 0; i <= width - 1; i++)
	{
		win[0][i] = '#';
	}

	// 4.2 画右边
	for (int i = 1; i <= height - 1; i++)
	{
		win[i][width - 1] = '#';
	}

	// 4.3 画下边
	for (int i = 0; i <= width - 1; i++)		// 整个0要注意
	{
		win[height - 1][i] = '#';
	}

	// 4.4 画左边
	for (int i = 1; i <= height - 2; i++)
	{
		win[i][0] = '#';
	}
}

void showText(char win[22][44], int x, int y, string& str)
{
	for (int i = 0; i < str.size(); i++)
	{
		win[y][x] = str[i];		// 第一个是高,习惯使用y作为高
		x++;
	}
}

void showText(char win[22][44], int y, string& str, string prex = " ", bool juzhong = true) // 重载+默认参数
{
	int width = 44;
	int x = (width - str.size()) / 2;		// 取出中间的位置

	int temp = x;
	for (int i = prex.size() - 1; i >= 0; i--)	// 因为选中,需要前面加*
	{
		temp--;
		win[y][temp] = prex[i];			// 这个是从中间开始,往左推着写
	}

	for (int i = 0; i < str.size(); i++)
	{
		win[y][x] = str[i];
		x++;
	}
}


void PrintWin(char win[22][44])
{
	int height = 22;
	int width = 44;

	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width; j++)
		{
			cout << win[i][j];
		}
		// 这一条边打完了,需要回车
		cout << endl;
	}
	cout << endl;		// 整个打完了也需要回车
}

enum Page {
	PageWelcome = 0, // 欢迎页面
	PageMenus,		 // 菜单页面
	PageContinue,	 // 继续游戏页面
	PageNew,		 // 新游戏页面
	PageSelected,	 // 选项页面
	PageExplain,	// 说明页面
	PageScore,		// 分数页面
	PageFriend,		// 好友页面
	PageChat,		// 聊天页面
	PageShop,		// 商店页面
	PageLogin,		// 登录页面
};

int main()
{
	// std::cout << "Hello World!\n";

	// 我们继续学习二维数组,我们就直接写。
	// 问题?一个高是22,宽度是44的二维数组,需要在四条边上画上#

	// 1.先定义 宽  高
	const int height = 22;
	const int width = 44;

	// 2.定义个二维数组
	char win[height][width];

	// 我们把初始化页面的函数提出出来

	InitWin(win);

	// 写字符串我们也可以写单独函数
	// 写入字符串
	// 
	// y也要中间
	int hm = (height - MENUS_MAX) / 2;		// 22 - 9 = 11 /2 = 5 从第5行开始写

	for (int i = 0; i < MENUS_MAX; i++)		// 有九个菜单
	{
		//showText(win, 2, i + 1, menus[i]);

		showText(win, hm + i, menus[i]);		// y需要改
	}


	// 打印也写成一个函数
	PrintWin(win);

	char ch;
	int index = 0;

	Page ePage = PageMenus;		// 默认初始化是菜单栏页面

	while (1)
	{
		hiddenCursor();				// 引入一个去掉光标的函数
		system("cls");

		switch (ePage)
		{
		case PageMenus:
			// 我们需要把之前的写的代码改了。
			for (int i = 0; i < MENUS_MAX; i++)
			{
				if (i == index)
				{
					//prex = " *";
					//showText(win, hm + i, menus[i], "%¥%");		// 这个是选中的
					showText(win, hm + i, menus[i], "*");		// 这个是选中的
				}
				else
				{
					//prex = "  ";
					//showText(win, hm + i, menus[i], "    ");		// 没有选择就使用默认参数
					showText(win, hm + i, menus[i]);
				}

				//string show = prex + menus[i];    // 我们可以直接用+号拼接

				//cout << show << i << index << endl;
			}

			break;
		case PageNew:	  // 新游戏,等待下一节实现
			InitWin(win);

			// 下一节课会将类  Snake直接写成类

			break;
		default:
			InitWin(win);

			break;
		}

		PrintWin(win);		// 选完之后,还要打印一下
		// 这里为啥不用cin,是因为cin需要回车,这个函数是conio.h的函数,不需要使用回车就可以获取到按键的值
		ch = _getch();

		switch (ch)
		{
		case 'H':
			if (index > 0)
			{
				index--;
			}


			break;

		case 'P':
			if (index < MENUS_MAX - 1)
			{
				index++;
			}
			break;

		case 13:	// 点击了回车
			// ePage = index;  // c++做了保护,不能把整形直接赋值给枚举
			ePage = static_cast<Page>(index + PageContinue);  // 类型转换,index是从继续游戏算做0,所以需要加个偏移
			break;
		}
	}
}

目前整个简单的页面选择我们已经做完了。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值