二、c++学习(函数详解)
第一节课,讲的不是太好,没事,我们第二节课及时改正。修改成上下课分两节,每节控制一个小时。虽然刚开始不好控制,但是还是要多尝试。
C++学习,b站直播视频
2.0 课程目标
我们上节课写了一个简单的菜单栏,是真的很简单,不过没关系,我们这节课打算引入界面,二维数组做一个页面。
2.1 二维数组
我们在上一节课已经介绍过了数组,为啥这里会继续介绍二维数组,其实主要是我们需要一个二维数组做为页面,经过上一节的菜单栏,是不是感觉页面也太简单了,不适合我们的审美。(虽然小黑框并没有审美),但是为了体现我们的追求美的眼光,还是需要把页面抽象成一个二维数组。
#include <iostream>using namespace std;int main()
{std::cout << "Hello World!\\n";// 我们继续学习二维数组,我们就直接写。// 问题?一个高是22,宽度是44的二维数组,需要在四条边上画上#// 1.先定义 宽 高const int height = 22;const int width = 44;// 2.定义个二维数组char win[height][width];// 因为是临时变量,需要初始化,不过我们就一起填充好了。// 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] = '#';}// 打印一下for (int i = 0; i < height; i++){for (int j = 0; j < width; j++){cout << win[i][j];}// 这一条边打完了,需要回车cout << endl;}cout << endl; // 整个打完了也需要回车
}
上面是我们打印整个页面的代码,编译执行:
我们的页面就做了出来。
2.2 显示菜单栏
离不开的菜单栏,上节课我们菜单栏的显示比较随意。这次我们就把菜单栏显示在页面中。
// 写入字符串
for (int i = 0; i < MENUS_MAX; i++) // 有九个菜单
{for (int j = 0; j < menus[i].size(); j++) // 字符串是由字符组成的{win[i + 1][j + 2] = menus[i][j]; // 这个字符串也是二维数组}
}
直接在main函数中加入代码,就可以显示菜单栏了。
看看效果:
完成,难道这节课就结束了?页面也实现了,菜单栏也在页面里了?
如果这么问的话,肯定不会结束的了。
2.3 函数
就我们这个例子,我们已经把main函数写的有点长了,当我们代码越写越多的时候,肯定不能都写在main函数中,所以引入了函数的概念。
写函数的注意事项就是功能要单一,返回值要注意。
2.3.1 函数介绍
int add(int x, int y)
{return x + y;
}
函数调用,和函数的形参实参,返回值,这些都知道吧。都是老司机了。
老司机了就忽略,但是我们遇到的第一个问题是:我们的页面是二维数组,那二维数组怎么做参数?
// 二维数组可以这样接受参数,具体关于指针和二维数组,我们下节课讲
void printWin(char pwin[][44], int height)
{cout << pwin[1][1];
}// 1.先定义 宽 高const int height = 22;const int width = 44;// 2.定义个二维数组char win[height][width];win[1][1] = 'x';printWin(win, height);
如果想知道二维数组,要使用二维指针能不能接,我们下课可讲。
2.3.2 函数重载
函数的其他功能就不说了,就说几个c++跟c语言不一样的。
在c语言中我们两个函数名是不能一样的,但是c++可以两个名字一样,但是参数需要不一样,这种函数叫函数重载。
int add(int x, int y)
{return x + y;
}double add(double x, double y)
{return x + y;
}
其实这种就是一个语法糖,编译器最终会根据函数名和参数自动生成一个新的函数名,其实编译器层面其实两个函数名还是不一样的。
百度了一下vs好像不能看到,那只能用g++来看看了:
g++ -S "02.2 页面(函数).cpp" -o test
-S是指定编译成汇编代码
通过分析汇编文件:
是不是不一样。
2.3.3 默认参数
c++函数除了支持重载,还支持默认参数。
如果函数有默认参数,我们不传实参的话,这个参数的形参就是默认参数,如果传了实参,那当时是实参了。
默认参数一定要放在最后面,并且写多个默认参数一定要记好顺序。
int add(int x = 1, int y = 1) // 默认参数
{return x + y;
}
这样就是默认参数,我们调用看看:
// 默认参数int sum3 = add(1, 2);cout << "sum3: " << sum << endl;int sum4 = add();cout << "sum4: " << sum << endl;
顺便看看汇编,我猜测是编译器给我们填值的。
// 默认参数int sum3 = add(1, 2);
00007FF603D96CB9 BA 02 00 00 00 mov edx,2
00007FF603D96CBE B9 01 00 00 00 mov ecx,1
00007FF603D96CC3 E8 D0 A6 FF FF call add (07FF603D91398h)
00007FF603D96CC8 89 85 74 04 00 00 mov dword ptr [sum3],eax int sum4 = add();
00007FF603D96CFD BA 01 00 00 00 mov edx,1
00007FF603D96D02 B9 01 00 00 00 mov ecx,1
00007FF603D96D07 E8 8C A6 FF FF call add (07FF603D91398h)
00007FF603D96D0C 89 85 94 04 00 00 mov dword ptr [sum4],eax
2.3.4 内联函数
本来之前安排是没有内联函数,现在想想还是加上吧。
int inline add1(int x, int y)
{return x + y;
}
我们调用一下:
// 内联函数
int sum5 = add1(1, 2); // 内联函数看着跟普通函数是不是没多大差别
cout << "sum4: " << sum5 << endl;
是不是看着跟普通函数一样,其实内联函数,编译器会把这段代码直接复制到这一部分,是不会进行函数调用的,函数怎么调用的?我们在linux课程讲,函数调用是需要创建资源,需要保存一些现场,内联函数是提高了性能。
我们反汇编看看:
// 内联函数int sum5 = add1(1, 2); // 内联函数看着跟普通函数是不是没多大差别
00007FF61C7F6D51 BA 02 00 00 00 mov edx,2
00007FF61C7F6D56 B9 01 00 00 00 mov ecx,1
00007FF61C7F6D5B E8 0A A7 FF FF call add1 (07FF61C7F146Ah)
00007FF61C7F6D60 89 85 B4 04 00 00 mov dword ptr [sum5],eax
结果一看吓一跳,这不是说好的内联函数不会有函数调用么?这call这么明显。
不要急,这是vs在debug模式下,内联函数,默认就按照普通函数调用,方便调试。所以我们切换到release版本。
int sum5 = add1(1, 2); // 内联函数看着跟普通函数是不是没多大差别cout << "sum4: " << sum5 << endl;
00007FF7EC4610EC 48 8B 0D 95 1F 00 00 mov rcx,qword ptr [__imp_std::cout (07FF7EC463088h)]
00007FF7EC4610F3 48 8D 15 EA 21 00 00 lea rdx,[string "sum4: " (07FF7EC4632E4h)]
00007FF7EC4610FA E8 61 02 00 00 call std::operator<<<std::char_traits<char> > (07FF7EC461360h)
00007FF7EC4610FF 48 8B C8 mov rcx,rax
00007FF7EC461102 BA 03 00 00 00 mov edx,3 // 这个就牛逼,编译器还帮你生成了结果,哈哈哈
00007FF7EC461107 FF 15 83 1F 00 00 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7EC463090h)]
00007FF7EC46110D 48 8B C8 mov rcx,rax
00007FF7EC461110 48 8D 15 19 04 00 00 lea rdx,[std::endl<char,std::char_traits<char> > (07FF7EC461530h)]
00007FF7EC461117 FF 15 7B 1F 00 00 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7EC463098h)]
这样是不是看出内联函数的区别了。
2.3.5 auto & decltype 与函数
上一节,我们匆匆忙忙介绍了一下auto和decltype,基本没领略到这两个关键字的厉害。这次我们就跟函数搞一起,看看会起什么反应。
auto Add(int x, int y) // auto可以做函数返回值
{return x + y; // 通过这个推导
}// 更狠一点的,函数参数也来推导,不过这个是c++20的语法
auto Add(auto x, auto y) // c++20的语法
{return x + y;
}// 其实在c++11的时候,并不直接支持auto做为返回值,需要一个后置语法
auto Sub(float x, int y) -> decltype(x - y) // auto只是占位,返回值类型会根据x-y推导
{return x - y;
}// 然后c++14,又发展回来一个decltype(auto)
decltype(auto) Sub2(float x, int y) // 这玩意看着好像跟直接使用auto是不是没有差别
{return x - y;
}// 带着这个疑问,我们就试试这两个auto 和 decltype(auto)
auto Multiple(int& x) // 这个返回的是int 会把引用丢掉
{x *= 2;return x;
}decltype(auto) Multiple2(int& x) // 这个返回的是int & 不会丢引用
{x *= 2;return x;
}
关于auto会丢弃属性,decltype(auto)都会补回来,具体的auto会丢弃啥属性,这个需要大家以后回去多练习,多看。我这里就不详细介绍了。
2.4 lambda
2.4.1 lambda表达式介绍
c++11给我的感觉就是吸取了别的语言的一些语法,比如很多语言都是匿名函数,所谓的匿名函数,其实就是没写名字,c++11也引入这种,就是我们现在讲的lambda表达式。
有时候我们写代码的时候,比如写一些回调函数,是不是要重新定义一个函数,然后把函数名作为参数传入,如果有lambda就可以就地定义。
// lambda// 就地定义一个lambdaauto func = [](int x, int y) -> int {return x + y;};cout << func(1, 2) << endl;// []里面是捕获的意思,啥是捕获等会介绍// ()里面就是参数列表// -> int 这个就是我们刚刚介绍的后置返回// 其实() 如果没有参数的时候也是可以省略,如果返回值编译器可以自动推导返回值也可以省略// 最后可以省略成这样auto func1 = [] {return 1; };// lambda表达式是否可以直接运行,不需要用变量接受呢?int sum6 = [](auto x, auto y) {return x + y;}(1, 2);// 我们来反汇编看看这个匿名函数在编译期眼中是什么样的cout << "sum6: " << sum6 << endl;
我们来看看汇编中的lambda是啥?
int sum6 = [](auto x, auto y) {
00007FF6446674C1 41 B8 02 00 00 00 mov r8d,2
00007FF6446674C7 BA 01 00 00 00 mov edx,1
00007FF6446674CC 48 8D 8D 74 07 00 00 lea rcx,[rbp+774h]
00007FF6446674D3 E8 E8 AD FF FF call `main'::`2'::<lambda_3>::operator()<int,int> (07FF6446622C0h)
00007FF6446674D8 89 85 14 05 00 00 mov dword ptr [sum6],eax
这样看编译器其实也是给匿名函数起了一个名,看看函数调用,其实内部都是一样的道理。
2.4.2 闭包
// 闭包// 闭包的概念,反正听别人说就很复杂,但按我自己的理解就是// 一个函数可以读到另一个函数的内部变量。// 那我们怎么访问到另一个函数的内部变量?// 其实很简单,主要我们在一个函数内部定义一个匿名函数就可以了。
auto bibao()
{int waibu = 1;//return []() {// return waibu * 2; // 这样直接写是报错的,c++的lambda函数如果需要外部变量,就需要捕捉//};return [=]() { // = 就是捕获外部变量的值,&就是捕获引用 如果是类可以捕获this或者*thisreturn waibu * 2;};
}auto fun1 = bibao();
cout << fun1() << endl; // 这样调用就是调用到了,函数的内部变量
关于闭包的临时变量的存储,我们暂时不介绍,这个到linux中再介绍。
2.4.3 捕获参数
我们上面也介绍了,如果lambda表达式,需要使用外部变量,就需要捕获,捕获有几种。
2.4.3.1 []
为空就是不捕获任何变量,但不包括静态局部变量,lambda可以直接使用局部静态变量。
2.4.3.2 [=]
捕获外部作用域中所有变量,并作为副本(按值)在函数中使用,可以使用它的值,但不许给它赋值。
// [=]int waibu = 0;auto func2 = [=]() { // 使用值捕获return waibu * 2; // 因为会创建一个副本,存储这个返回值};waibu = 4;int iRet = func2();cout << "func2():" << iRet << endl; // 结果是0,说明这是值捕获,如果是=号,就会把值存一份,
想不想看汇编代码是怎么处理的?
int waibu = 0;
00007FF7BD8734A2 C7 85 54 05 00 00 00 00 00 00 mov dword ptr [waibu],0 auto func2 = [=]() { // 使用值捕获return waibu * 2; // 因为会创建一个副本};
00007FF7BD8734AC 48 8D 95 54 05 00 00 lea rdx,[waibu] // 第二个参数是waibu
00007FF7BD8734B3 48 8D 8D 74 05 00 00 lea rcx,[func2] // func2就是编译器给我们捕获的变量的地址,linux系统是在栈上,windows系统不清楚具体在哪个位置
00007FF7BD8734BA E8 A1 F0 FF FF call `main'::`2'::<lambda_4>::<lambda_4> (07FF7BD872560h) // 这个就是把waibu的值,存到[func2]这个地址上,已备后面使用。waibu = 4;
00007FF7BD8734BF C7 85 54 05 00 00 04 00 00 00 mov dword ptr [waibu],4 int iRet = func2();
00007FF7BD8734C9 48 8D 8D 74 05 00 00 lea rcx,[func2] // 调用的时候到了,还是把这个[func2]的地址传给这个函数
00007FF7BD8734D0 E8 8B F5 FF FF call `main'::`2'::<lambda_4>::operator() (07FF7BD872A60h) // 函数中就是使用[fun2]中的waibu值,计算的结果
00007FF7BD8734D5 89 85 94 05 00 00 mov dword ptr [iRet],eax
main'::
2’::<lambda_4>::<lambda_4>:
00007FF6A3192584 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6A319258B 48 8B 8D E8 00 00 00 mov rcx,qword ptr [__waibu]
00007FF6A3192592 8B 09 mov ecx,dword ptr [rcx]
00007FF6A3192594 89 08 mov dword ptr [rax],ecx
00007FF6A3192596 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6A319259D 48 8D A5 C8 00 00 00 lea rsp,[rbp+0C8h]
这就是把waibu的值存入的汇编代码。
main’::`2’::<lambda_4>::operator():
00007FF6A3192A7F 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6A3192A86 8B 00 mov eax,dword ptr [rax]
00007FF6A3192A88 D1 E0 shl eax,1
这个就是取出来,然后计算。
2.4.3.3 [&]
捕获外部作用域中所有变量,并作为引用在函数体内使用。
// [&]
auto func3 = [&]() { // 引用捕获return waibu * 2;
};
waibu = 8; // 我在这里再改这个值int iRet1 = func3();
cout << "func3():" << iRet1 << endl; // 大家猜猜,这个值是多少?输出16,这里大家都明白了,是捕获了地址
我们在分析一下引用是怎么捕获的,后面的捕获就不分析了。
// [&]auto func3 = [&]() { // 引用捕获return waibu * 2;};
00007FF6A3193523 48 8D 95 54 05 00 00 lea rdx,[waibu]
00007FF6A319352A 48 8D 8D B8 05 00 00 lea rcx,[func3]
00007FF6A3193531 E8 8A F0 FF FF call `main'::`2'::<lambda_5>::<lambda_5> (07FF6A31925C0h) // 分析过了是存了一个引用waibu = 8; // 我在这里再改这个值
00007FF6A3193536 C7 85 54 05 00 00 08 00 00 00 mov dword ptr [waibu],8 int iRet1 = func3();
00007FF6A3193540 48 8D 8D B8 05 00 00 lea rcx,[func3]
00007FF6A3193547 E8 64 F5 FF FF call `main'::`2'::<lambda_5>::operator() (07FF6A3192AB0h) // 这里也是存了一个引用
00007FF6A319354C 89 85 D4 05 00 00 mov dword ptr [iRet1],eax
这个汇编代码就不说了。
2.4.3.4 [this]
这个是在类中,捕获this指针的,目前还没讲类,先不说,不过效果也是一样,捕获到this就可以使用该类的成员了。
2.4.3.5 [变量名]
如果是多个变量,使用,分割。表示按值捕获变量名中的变量,同时不捕获其他变量。
[&变量名] 按引用捕获变量名中的变量,同时不捕获其他变量
// [变量名]int waibu3 = 0;int waibu4 = 1;auto func4 = [waibu3, waibu4]() { // 使用值捕获return waibu3 + waibu4 * 2; // 因为会创建一个副本,存储这个返回值(具体我们后面分析)};auto func5 = [&waibu3, &waibu4]() { // 使用值捕获return waibu3 + waibu4 * 2; // 因为会创建一个副本,存储这个返回值(具体我们后面分析)};
2.4.3.6 [=, &变量名]
按值捕获所有外部变量,但按引用捕获&中的变量,=必须写在开头
2.4.3.7 [&, 变量名]
按引用捕获所有外部变量,但按值来捕获变量名中的变量。
2.4.4 易错点
2.4.4.1 引用捕获临时变量
auto testfunc()
{int x = 11; // lambda表达式引用了临时变量// 我们知道在函数执行完之后,x的值就会被回收,如果这样我们还调用这个函数的话,这个x的值是不可预料的// 如果有写过go,js等语言的闭包,他们明显是可以这么用,c++不行return [&] {return x;};
}// 既然上面说可以捕获&,这样就会引出另一个问题
auto func4 = testfunc();
// 调用到这里后,x的值已经凉凉了,(最近新冠比较严重,习惯用凉凉说话了),
// 我们打印一下x的值
cout << "func4:" << func4() << endl;
2.4.4.2 捕获临时变量的指针
如果是指针同理,如果是指针的话可以使用初始化捕获:
auto testfunc2()
{int x = 10;int* px = &x; // 这种取地址应该没人不会吧,那就不说了return [=] { // 这个=就是把指针的值,捕获到lambda中,如果这个指针释放了,就会凉return *px;};
}auto func5 = testfunc2();
cout << "func5:" << func5() << endl; // 这样基本凉凉
2.4.4.3 c++14引入初始化捕获
auto testfunc3()
{int x = 10;int* px = &x; // 这种取地址应该没人不会吧,那就不说了return [lx = *px] { // 这个是c++14的语法,初始化捕获,把*px的值赋值给一个新的变量lxreturn lx; // 这个返回lx就是安全的};
}auto func6 = testfunc3();
cout << "func6:" << func6() << endl; // 输出10,这样就对了
为了解决捕获之后,引用地址,或指针就没了,就可以直接这样捕获。
2.4.5 新语法
新语法的关键内存已经写在思维导图里了,这里就不介绍了。
最后,还画了一个捕获参数的图。
2.5 函数模板
2.5.1 为啥要有函数模板
这里举一个例子,大家就明白了, 一个公认的例子。
int add(int a, int b)
{return a + b;
}float add(float a, float b)
{return a + b;
}// 如果还需要其他类型,是不是还需要继续写。
// 所以c++提供了一种叫函数模板的泛型编程方式
2.5.2 函数模板
template<typename T>
T add(T a, T b)
{return a + b;
}
这个就是函数模板,为啥叫函数模板,就是我们可以通过这个模板去推断具体的函数,然后再调用具体的函数去执行。
这样说是不是也难理解,其实不难,主要是理解好怎么去推断成具体函数的,这个函数模板如果只是定义在这里,是没什么用的。那什么时候推断成具体函数呢?是在我们调用的地方:
// main函数片段
cout << Add2(1, 2) << endl;
这个代码就是调用的地方,我们通过传递的参数,编译器就会给我们自动推动出具体的函数,比如我们现在传的是1 ,2 编译器给两个参数推导的是int int。
我们直接反汇编查看编译器推导的结果:
00007FF6841A3DA5 BA 02 00 00 00 mov edx,2
00007FF6841A3DAA B9 01 00 00 00 mov ecx,1
00007FF6841A3DAF E8 08 D5 FF FF call Add2<int,int> (07FF6841A12BCh)
00007FF6841A3DB4 89 85 E4 06 00 00 mov dword ptr [iRet6],eax
那如果传的是double?
cout << Add2(1.9, 2.4) << endl;
00007FF6841A3DE8 F2 0F 10 0D 68 96 00 00 movsd xmm1,mmword ptr [__real@4003333333333333 (07FF6841AD458h)]
00007FF6841A3DF0 F2 0F 10 05 40 96 00 00 movsd xmm0,mmword ptr [__real@3ffe666666666666 (07FF6841AD438h)]
00007FF6841A3DF8 E8 25 D3 FF FF call Add2<double,double> (07FF6841A1122h)
编译器会推导成double。
其实我们也可以指定函数参数的:
// 本来传参是double,我硬是传int,编译器心里肯定说:这人很狗cout << Add2<int, int>(1.9, 2.4) << endl;
00007FF6841A3E35 BA 02 00 00 00 mov edx,2
00007FF6841A3E3A B9 01 00 00 00 mov ecx,1
00007FF6841A3E3F E8 78 D4 FF FF call Add2<int,int> (07FF6841A12BCh)
还是int。
2.5.3 c++20更灵活的写法
其实我们在函数的时候,已经讲过了,就是都是使用auto。
auto Add(auto x, auto y) // c++20的语法
{return x + y;
}
这个语法是c++20才被支持,但其实这也是一个编译器的语法糖,其实底层实现就是一个函数模板:
具体实现是这样的:
template<typename T, typename U>
auto Add2(T x, U y) -> decltype(x + y)
{return x + y;
}
这种写法就是x 和 y都不一样,也可以计算出来。
2.5.4 函数模板遇到函数重载
template<typename T>
T add(T a, T b)
{return a + b;
}int add(int a, int b)
{cout << "普通函数" << endl;
}// 调用函数
add(1,2); // 会优先调用普通函数// 如果需要调用函数模板,可以显示调用
add<>(a, b); // 这样也可以
add<int>(a,b);
2.6 变参函数
2.6.1 initializer_list
在c++11中推出了这么一个初始化列表,让我们实现变参函数有了另外一种实现。(如果没必要,不要写变参函数)
这个类型需要每个元素类型一致,可以理解成数组,但是这个类型又是只读的,不能修改。并且赋值或者拷贝只能共享这份数据。
// initializer_list
auto yy = { 1,2,3 }; // 这个会推导出 initializer_list<int>类型initializer_list<int> arry = { 2, 4, 5 };// 怎么看拷贝也是同一个副本的话,gdb来看就明白了:
auto arry2 = arry;printvalue({ 3,6,8 });void printvalue(initializer_list<int> temp)
{for (auto beg = temp.begin(); beg != temp.end(); beg++) // 这是一个容器的遍历{cout << *beg;}for (auto a : temp){cout << a;}cout << endl << temp.size() << endl;
}
第二种实现变参函数的方式,就是使用栈,知名的printf函数就是这样实现.
2.6.2 …
// ...
#include "stdarg.h"
double average(int num, ...)
{va_list valist; // 创建一个vs_list类型变量double sum = 0;va_start(valist, num); // 使valist指向起始参数for (int i = 0; i < num; i++){// 遍历参数sum = sum + va_arg(valist, int); // 参数2说明可变参数类型是整形}va_end(valist); // 释放va_listreturn sum;
}// ...
int iRet8 = average(3, 10, 20, 30); // 60 第一个参数不计算
cout << iRet8 << endl;
实现一个比较简单的…,本来之前先分析va_start这几个宏的,发现是编译器处理的,立马不分析了,哈哈哈。
我这里只是实现了一个简单的int,如果是printf,大概思路就是根据前面的%d来推断这个值是那个类型,然后在栈中,取出对应大小的值。
这种变参函数在c++中要少用,因为是使用栈存储的,能正确处理的都是普通的类型,类类型可能处理的不好。
va_start必须要有一个开始的参数,指向了这个参数,才取到后面的值。
2.6.3 有空分析一下printf实现
感觉这个分析,性价比不高,我就不分析了,留着大家自己去分析,并写一个博客把。
2.7 优化贪吃蛇
吹了一大波水了之后,我们继续在刚开始写代码的基础上,优化一下,我们的贪吃蛇。
// 02.3 优化贪吃蛇.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//#include <iostream>using namespace std;constexpr int MENUS_MAX = 9; // 定义9个菜单// 我们知道了有字符串数组,我们这词就存储起来
string menus[MENUS_MAX] = { "继 续" , "新游戏" , "选 项" , "说 明", "最高分", "好 友", "聊 天", "商 店", "登 录" };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 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; // 整个打完了也需要回车
}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);// 写字符串我们也可以写单独函数// 写入字符串for (int i = 0; i < MENUS_MAX; i++) // 有九个菜单{showText(win, 2, i + 1, menus[i]);}// 打印也写成一个函数PrintWin(win);
}
第一版代码写完了。
这个看着显示明显不对,有点偏,我们在改一下显示。
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++;}
}// 调用的地方也要改
// 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需要改
}
这个差不多吧,然后把按键功能引入,就可以了。
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;while (1){hiddenCursor(); // 引入一个去掉光标的函数system("cls");// 我们需要在菜单栏前面+* 表示我选中了这个//string prex = " ";// 我们需要把之前的写的代码改了。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;}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;}}
}
完整版代码路径
2.8 作业
实现一个闪瞎眼睛的菜单栏。