上一次直播好像过去很久了,中间有加班,有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 指针数组 & 数组指针
一个面试比较喜欢问的问题,指针数组和数组指针,该说是中国文字比较牛,还是说代码本来都不打算设计这么复杂。
-
指针数组
首先说说啥是指针数组,首先从中文出发,指针做为修饰词,数组是名字,所以这个次的意思是,一个数组,里面存的是指针。我们回归代码:
// 指针数组 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; }
-
数组指针
数组指针,其实是一个指针,这个指针指向数组,比如我举例写一个指向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 ∑
}
// 指针函数
// 现在都是老司机了,首先是一个函数,返回值是一个指针。
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;
}
}
}
目前整个简单的页面选择我们已经做完了。