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

news2024/12/24 2:21:29

上一次直播好像过去很久了,中间有加班,有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;
		}
	}
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/500084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Solr(2):Solr的安装

1 安装前的概述 &#xff08;1&#xff09;solr是基于lucene而lucene是java写的&#xff0c;所以solr需要jdk----当前安装的solr-7.5需要jdk-1.8及以上版本&#xff0c;下载安装jdk并设置JAVA_HOME即可。 &#xff08;2&#xff09;下载solr&#xff0c;然后解压即可&#xf…

Map对象的用法(JS)

&#x1f4dd;个人主页&#xff1a;爱吃炫迈 &#x1f48c;系列专栏&#xff1a;数据结构与算法 &#x1f9d1;‍&#x1f4bb;座右铭&#xff1a;道阻且长&#xff0c;行则将至&#x1f497; 文章目录 MapkeyMap常用语法Map其他语法创建map的其他方式 Map Map是一个键值对形式…

FPGA设计之控制集优化详解

目录 一、前言 二、Control Set控制集 三、Control Set控制集优化 四、优劣 一、前言 在工程设计优化中&#xff0c;综合阶段优化中有一项常见的优化&#xff0c;控制集&#xff08;control set&#xff09;优化&#xff0c;在vivado的Synthesis中有对该配置项的解释&#x…

Android Jetpack组件化之ORM 数据库访问框架详解

一、对象关系映射 - ORM / Object Relational Mapping Android 系统中使用的数据库是 SQLite 关系型数据库 , 使用 Android 提供的 api 访问 SQLite 数据库非常繁琐 , 由此出现了很多 ORM 框架 ; ORM 英文全称 Object Relational Mapping , 对象关系映射 ; 对象关系映射 ORM …

“智能指针:C++中优雅的内存管理解决方案“

前言 欢迎来到&#x1f496;小K&#x1f496;的&#x1f49e;C专栏&#x1f49e;&#xff0c;内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况&#xff0c;这是C和C程序员的噩梦之一。本节将为大家带来解决办法—>智能指针 文章目录 前言1、简介2、为什么…

【移动端网页布局】flex 弹性布局 ⑤ ( 设置侧轴单行子元素排列方式 | align-items 样式说明 | 代码示例 )

文章目录 一、设置子元素是否换行 : align-items 样式说明1、 align-items 样式引入2、 align-items 样式属性值 二、代码示例1、 代码示例 - 默认样式2、 代码示例 - 设置主轴水平居中3、 代码示例 - 设置侧轴垂直居中4、 代码示例 - 设置侧轴从下到上排列5、 代码示例 - 设置…

C++之类和对象(二)

目录 前言 类的6个默认成员函数 1.构造函数 1.1 概念 1.2 特性 2. 析构函数 2.1 概念 2.2 特性 3. 拷贝构造函数 3.1 概念 3.2 特征 4.赋值运算符重载 4.1 运算符重载 4.2.赋值运算符重载 4.3 前置和后置重载 5. .const成员 6.取地址及const取地址操作符重载 前…

【二叉搜索树】

1 二叉搜索树概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树 &#xff0c;或者是具有以下性质的二叉树 : 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值 它的左…

Android“真正的”模块化

作者&#xff1a;bytebeats 模块化背后的原则概述 “如果说SOLID原则告诉我们如何将砖块排列成墙和房间, 那么组件原则则告诉我们如何将房间排列成建筑.” ~ Robert C. Martin, Clean Architecture 你应该分层打包还是分特性打包?还有其他方法吗? 如何提高项目的编译时间? 你…

将Python环境迁移到另一台设备上

本方法可以将一台电脑上的python环境迁移到另一台电脑上&#xff0c;可以省去一个一个包pip的麻烦。本文以pytorch的迁移为例。 一、从源环境备份安装包 在原来的电脑的Conda控制台中使用语句 pip freeze > c:\myrequirement.txt 后面跟的参数是文件的路径和文件名&#x…

Spring MVC自定义拦截器--Spring MVC异常处理

目录 自定义拦截器 什么是拦截器 ● 说明 自定义拦截器执行流程分析图 ● 自定义拦截器执行流程说明 自定义拦截器应用实例 ● 应用实例需求 创建MyInterceptor01 创建FurnHandler类 在 springDispatcherServlet-servlet.xml 配置拦截器 第一种配置方式 第二种配置方…

linux 互斥量pthread_mutex

专栏内容&#xff1a;linux下并发编程个人主页&#xff1a;我的主页座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物&#xff0e; 目录 前言 概述 原理 初始化 进程和线程使用的不同点 死锁 接口 基本API 属性设置 …

探索机器翻译:从统计机器翻译到神经机器翻译

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

Osek网络管理及ETAS实现

OSEK/VDX&#xff08;Offene Systeme und deren Schnittstellen fr die Elektronik in Kraftfahrzeugen / Vehicle Distributed eXecutive&#xff09;是一种用于嵌入式系统&#xff08;尤其是汽车电子控制单元&#xff09;的开放标准。它旨在提供一种统一、可互操作的软件架构…

关于 《python 从入门到实践》的 matplotlib 随机漫步小项目

使用 python 生成随机漫步数据&#xff0c;再使用 matplotlib 将数据呈现。 所谓随机漫步&#xff1a; 每次行走的路径都是完全随机的&#xff0c;就像蚂蚁在晕头转向的情况下&#xff0c;每次都沿随机方向前行路径。 在自然界&#xff0c;物理学&#xff0c;生物学&#xff0…

【Linux】Job for network.service failed(网卡启动报错)

上图是Linux网卡启动报错的情况 这是由于cat/etc/sysconfig/network-scripts/ifcfg-xxx 中HWADDR的MAC地址和ifconfig中的MAC地址不一样&#xff0c;或者缺少cat/etc/sysconfig/network-scripts/ifcfg-xxx 中HWADDR的MAC地址 1.查看ifconfig中的MAC地址 图中00&#xff1a;0c…

【新星计划-2023】IP地址是什么?IP地址的主要功能是什么?

IP地址在生活中是很常见的&#xff0c;我们所使用的手机、电脑等等&#xff0c;都有一个IP地址&#xff0c;那么IP地址是什么&#xff1f;通过IP地址又能干什么&#xff1f;下文就来给大家详细的讲解一下。 一、什么是IP地址 通常我们说的IP地址多数是指互联网中联网的IP地址…

Java 基础进阶篇(十一)—— Arrays 与 Collections 工具类

文章目录 一、Arrays工具类1.1 Arrays 类常用方法1.2 对于 Comparator 比较器的支持1.3 Arrays 的综合应用1.3.1 应用一&#xff1a;数组的降序排序1.3.2 应用二&#xff1a;根据学生年龄进行排序 二、Collections工具类2.1 Collections 类常用方法2.2 Collections 排序相关 AP…

神经网络实验---梯度下降法

本次实验主要目的是掌握梯度下降法的基本原理&#xff0c;能够使用梯度下降法求解一元和多元线性回归问题。 文章目录 目录 文章目录 1. 实验目的 2. 实验内容 3. 实验过程 题目一&#xff1a; 题目二&#xff1a; 题目三&#xff1a; 实验小结&讨论题 1. 实验目的 ① 掌握…

〖Python网络爬虫实战㉓〗- Ajax数据爬取之什么是Ajax

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000 python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;目前专栏免费订阅&#xff0c;在转为付费专栏前订阅本专栏的&#xff0c;可以免费订阅付…