1.0 课程目标(画饼)
我们的课程目标是做一个联机版的贪吃蛇,实现这个贪吃蛇的功能也跟课程分了3个部分。
第一部分:c++,主要实现单机版贪吃蛇,还有选项(我们玩贪吃蛇都有没墙模式,和墙模式),说明(其实就是个显示)
第二部分:linux系统编程,主要实现继续游戏(游戏玩到一半我退出,然后重来进来,可以继续游戏),登录(网络时代了,肯定需要联网),聊天(实现一个简单的IM)。(这一部分还没准备,不知道是不是画饼,哈哈哈)
第三部分:数据库,不能再想之前一样使用文件存储了,大数据时代,还是需要数据库,虽然我们数据不大。这一部分主要实现好友,(也是简单的IM),还有最高分,(游戏没有排行榜,是没有灵魂的,参考羊了个羊),日志模块,(这个没有在页面中体现,但是作为成熟的后端,如果有bug肯定需要查问题的)
好了课程目标,画饼到此结束,我们进入正题。
接下来我们第一部c++正式开始。
1.1 熟悉c++
我们先用vs2022来创建一个c++的工程,这个工程已经附带了一个hello world输出信息。
我们学习一门语言,都是比较喜欢使用hello world。
我们来看一下c++有什么不一样
// zero.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream> // 头文件不一样了,c语言的头文件是#include <stdio.h>
// 注意c语言的头文件是有.h的,c++是没有.h后缀,当然如果自己写的头文件,加上.h后缀也是可以的
int main() // main函数还是一样的配方
{
std::cout << "Hello World!\n"; // c++的输出方式和c的输出不一样,
// c的风格是:printf("Hello World\n");
// 虽然在c++中我们一样也可以使用c,但是我们要做好入乡随俗的准备。
}
// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单 // 运行程序的快捷键,也可以直接点击按钮
// 调试程序: F5 或调试 >“开始调试”菜单
1.0.0 命名空间
我们在输出语句中看到了std::cout 这个std是何方神圣。
其实std就是c++的一个命名空间,这个命名空间名字叫std,只不过这个std有点特殊,c++很多标准库函数都在这个命名空间中,包括了cout输出,cin输入,另外还有很多很多。std是standard 的缩写,意思是“标准命名空间”。
c++为啥定义一个命名空间?
我们之前在写c语言的时候,如果是多人开发一个项目,每一个人写一个.c文件,怕函数名会冲突,所以一般不需要暴露在外面的函数名前面都加一个static,表示这个函数作用域是在该文件内。
c++当然可以继续用static,毕竟c++是c的超集,但是c++有自己的解决方案,就是命名空间,我们来举个栗子:
// zero.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
/*
1.0.0 命名空间代码
*/
namespace jiangyou
{
void peixun()
{
std::cout << "酱油师兄培训c++" << std::endl;
}
}
namespace yangshijie
{
void peixun()
{
std::cout << "杨师姐培训前端js" << std::endl;
}
}
// 如果按道理来说,这两个函数的名字是一样的,所以会编译不过
// 但是因为我们加了命名空间,所以并不冲突,这里编译一下看看
// 如果我们觉得不想写那么多命名空间的名字,我们可以直接引入这个命名空间
using namespace yangshijie;
// 那如果我继续引入jiangyou的命名空间,两个函数就会二义性
// using namespace jiangyou; // 这样这个函数就会报错
// 最后一个,可以直接引入这个命名空间的函数,而不用引入整个命名空间
using yangshijie::peixun;
int main()
{
/*
1.0 代码
std::cout << "Hello World!\n";
*/
/* 1.0.0 命名空间代码 */
// 那我们添加了命名空间,那怎么调用呢?
// peixun(); // 如果直接调用是报错的
//jiangyou::peixun(); // 我们需要把命名空间带上,这样调用
//yangshijie::peixun();
peixun(); // 如果我们引入了命名空间,直接调用这个函数,就是相当是这个函数的命名空间中的函数。
}
1.2 功能菜单
我们这次培训,不会一股脑讲语法,因为市面上讲语法的视频很多,比如:王建伟老师的c++视频就不错,需要的话,百度一下就知道了。好了废话不多说,我们来看看我们这一节课的需求,
菜单栏有下面菜单
(1) 继续
(2) 新游戏
(3) 游戏选项(级别/游戏类型)
(4) 游戏规则说明
(5) 最高分
(6) 商店
(7) 好友
我们这节课的任务就是写出一个简单的功能菜单。
写个菜单嘛,那不是很简单,我们一样都输出就可以了。
// 1.1功能菜单.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std; // 觉得写这个玩意比较累
int main()
{
cout << "继续" << endl; // 当然我们也可以使用printf来实现,不过既然是写c++了,就尽量少用printf
cout << "新游戏" << endl;
cout << "游戏选项" << endl;
cout << "游戏规则说明" << endl;
cout << "最高分" << endl;
cout << "商店" << endl;
cout << "好友" << endl;
}
这个不是简简单单么,哈哈哈哈。
这样就结束了么,我们第一节课讲的真快,哈哈哈。
1.3 按键输入
虽然我们在上一节中把功能菜单实现了出来,但我们菜单最重要的是交互,而不是显示,显示出来我们操作不了,也没啥用。
在c语言中,我们一般都是使用scanf或者getchar等接受键盘数据,c++的话,有自己封装的>>。
char ch; // char 是一个类型
cin >> ch; // 这个时候c++ 的输入,后面的可以接受任意变量,这里我就不尝试了
// c语言的输入有scanf这个函数,我们可以忘记了
cout << ch;
float ff;
cin >> ff;
cout << ff;
1.4 变量
变量在程序中,是比较重要的,我们不同的变量长度,会对应这不同的大小,比如我们上面用cin接受输入的值的时候,有字符,有小数,有整数,我们代码也是有区分。
下面我们就来看看c++的一下变量与大小:
// one.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <climits>
using namespace std;
int main()
{
cout << "最大值:" << endl;
cout << "int类型:" << INT_MAX << " int字节:" << sizeof(int) << endl;
cout << "char类型:" << CHAR_MAX << " char字节:" << sizeof(char) << endl;
cout << "short类型:" << SHRT_MAX << " short字节:" << sizeof(short) << endl;
cout << "long类型:" << LONG_MAX << " long字节:" << sizeof(long) << endl;
cout << "long long类型:" << LLONG_MAX << " long long字节:" << sizeof(long long) << endl;
cout << "signed char类型:" << SCHAR_MAX << " signed char字节:" << sizeof(signed char) << endl;
cout << "unsigned char类型:" << UCHAR_MAX << " unsigned char字节:" << sizeof(unsigned char) << endl;
cout << "unsigned short类型:" << USHRT_MAX << " unsigned short字节:" << sizeof(unsigned short) << endl;
cout << "unsigned int类型:" << UINT_MAX << " unsigned int字节:" << sizeof(unsigned int) << endl;
cout << "unsigned long类型:" << ULONG_MAX << " unsigned long字节:" << sizeof(unsigned long) << endl;
cout << "unsigned long long类型:" << ULLONG_MAX << " unsigned long long字节:" << sizeof(unsigned long long) << endl;
cout << endl << endl << "最小值:" << endl;
cout << "char类型:" << CHAR_MIN << " char字节:" << sizeof(char) << endl;
cout << "short类型:" << SHRT_MIN << " short字节:" << sizeof(short) << endl;
cout << "int类型:" << INT_MIN << " int字节:" << sizeof(int) << endl;
cout << "long类型:" << LONG_MIN << " long字节:" << sizeof(long) << endl;
cout << "long long类型:" << LLONG_MIN << " long long字节:" << sizeof(long long) << endl;
cout << endl << endl << "浮点数:" << endl;
cout << "float类型:" << " float字节:" << sizeof(float) << endl;
cout << "double类型:" << " double字节:" << sizeof(double) << endl;
cout << "long double类型:" << " long double字节:" << sizeof(long double) << endl;
cout << "wchar_t类型:" << " wchar_t字节:" << sizeof(wchar_t) << endl;
cout << "char8_t类型:" << " char8_t字节:" << sizeof(char8_t) << endl;
cout << "min(bool) : " << numeric_limits<bool>::min() << endl;
cout << "min(int) : " << numeric_limits<int>::min() << endl;
cout << "min(unsigned int) : " << numeric_limits<unsigned int>::min() << endl;
cout << "min(short int) : " << numeric_limits<short int>::min() << endl;
cout << "min(long int) : " << numeric_limits<long int>::min() << endl;
cout << "min(float) : " << numeric_limits<float>::min() << endl;
cout << "min(double) : " << numeric_limits<double>::min() << endl;
cout << "min(long double) : " << numeric_limits<long double>::min() << endl;
cout << "max(bool) : " << numeric_limits<bool>::max() << endl;
cout << "max(int) : " << numeric_limits<int>::max() << endl;
cout << "max(unsigned int) : " << numeric_limits<unsigned int>::max() << endl;
cout << "max(short int) : " << numeric_limits<short int>::max() << endl;
cout << "max(long int) : " << numeric_limits<long int>::max() << endl;
cout << "max(float) : " << numeric_limits<float>::max() << endl;
cout << "max(double) : " << numeric_limits<double>::max() << endl;
cout << "max(long double) : " << numeric_limits<long double>::max() << endl;
}
1.5 常量
如果有变量,那就有常量,常量表示该值不可修改,比如PI的值,这个就是我们计算圆面积的一个常量。再说,就我们目前的小黑框显示页面,如果我们想定义一个高640,宽320的一个界面(这个界面我们下节课会介绍怎么写),因为我们一个程序这个长宽的页面是固定的,所以我们这里可以用常量表示。
const int Height = 640;
const int Width = 320; // 定义常量的关键字就是const
在c++中,就对const做了严格的约束,会把const int a=10;存储到一个符号表中,键值是a,值是10,这样通过a就能找到10。
如果int *p = &a; 这时候,c++会临时申请一个变量,并且把地址返回给p,这样操作之后,常量a是没有被改变的。
1.5.1 c语言的常量
在c语言中,其实我们是可以修改常量的,比如:const int a = 10;这个变量a虽然是常量,但是编译器并没有严格约束,如果用指针操作a的地址还是可以修改的。(这个可以自行测试)
1.5.2 const的局限
虽然const可以定义常量,但是有一个很大的缺陷,就是不能在编译时期使用,比如,就是不能做数组的长度。
const int len = 5;
int a[len]; // 这样是编译失败的
那如果这样子,我们怎么解决?如果是c语言的话,这里我们就会使用到宏定义:
#define LEN 5
int a[LEN];
这样子编译是通过的,总感觉这样比较low,哈哈哈。所以c++11引入了一个新的关键字constepr。
1.5.3 constexpr引入
constexpr这个也是常量的意思,我们也可以直接拿来定义常量。
constexpr int len = 5;
int a[len];
这样编译是没问题的,其实这个关键字还可以修饰函数,这个函数会在编译时期,会调用这个函数,并计算出返回值,如果有返回值的话。
constexpr int Getlen()
{
return 5;
}
int a2[Getlen()];
这样是可以编译通过的,如果我们在constexpr中使用了运行时变量,这个是肯定编译不过的。
static int dd = 10;
constexpr int Getlen1()
{
return dd;
}
int a2[Getlen1()]; // 直接编译失败
关于这个还有一个高级玩法,模板元编程(这些是c++高级玩法,目前就不要学习先)
constexpr unsigned long long Fibonacci2(unsigned long long n)
{
return n >= 2 ? Fibonacci2(n - 1) + Fibonacci2(n - 2) : n;
}
在编译时期,就可以把这个递归问题给计算出来,之后的脑洞有多大,就看大家的想象了。
关于这个其他用法,大家可以自行学习。
1.6 运算符
大家都是老司机了,这个运算符就不说了。
1.7 auto & decltype
我们开始上硬菜了。
总感觉是c++怕别人说自己的变量类型不能推导,是静态语法,而py,js,php这些是动态语言,他们那些动态语言,就是变量可以随时拿来用,都不用定义变量类型,由编译器推断,如果学了js就会明白,代码量一大,确实比较懵逼,所以后来出现了一个ts。
好了,话又说回到c++,c++为了紧跟时代潮流,c++11引入了两个自动推导类型的关键字,而且c++为了追求性能,推导过程肯定是放在编译阶段的。
我们来简单使用一下:
auto x = 10; // x -> int
auto f = 1.1f; // f -> float
decltype(x) xx; // xx -> int
decltype(f) ff;
decltype(1.4) d;
虽然现在举的例子,是比较简单,那时候因为我们目前学习的都是比较简单的数据类型,以后我们学习到类模板的时候,就会明天写这个auto的用处了。
目前我们就先介绍到这里,以后会更加说明的。
1.8 基本语句
这个基本语句,if while for,就不说了吧,老司机了大家。
c++11不甘寂寞,引入了一个范围for,可能看到其他语言都是这种写法吧,c++11也就引进了。
// 1.8 范围for
// c++11 在原来的for循环的基础上加了一个范围for,范围for也是有限制的,其实要是一个序列,
// 1.数组
int att[10] = { 1,2,3,4,5 };
for (auto a : att)
{
cout << a << endl;
}
// 但是new出来的数组不行
//int* patt = new int[10];
//for (auto a : patt) // 不行
//{
// cout << a << endl;
//}
// 2.还是就是我们后面学习的string
string sss = "I Love Wuxeu\n";
for (auto c : sss)
{
cout << c << endl;
}
// 3.还有后面学习的容器,这个我先写,后面学习到了就知道了。
vector<int> v = { 1,2,3,4 };
for (auto vv : v)
{
cout << vv << endl;
}
1.9 数组
大家都会数组没有?数组也比较简单,就是类型相同的一个顺序集合。
// 1.9 数组
int att2[10] = { 1,2,3,4,5 }; // 这个就是数组,类型都是int
// 数组位置存储是顺序的,可以打印出来看看
for (int i = 0; i < 10; i++)
{
printf("i = %d %p\n", i, &att2[i]); // 是不是地址连续
}
1.9.1 下标 & 指针 那个更快?
// 因为数组的地址是连续的,我们才能使用下标获取
int tu = att2[4]; // 可以这么取数据
// 想不想看编译器对这句话是怎么转换的?
/* // 因为数组的地址是连续的,我们才能使用下标获取
int tu = att2[4]; // 可以这么取数据
00007FF6C62274CE B8 04 00 00 00 mov eax, 4
00007FF6C62274D3 48 6B C0 04 imul rax, rax, 4 // rax存的就是int的大小,rax*4 就是att2的偏移
00007FF6C62274D7 8B 84 05 68 03 00 00 mov eax, dword ptr att2[rax]
00007FF6C62274DE 89 85 C4 03 00 00 mov dword ptr[tu], eax
*/
// 这时候我想到,当初老师说用指针操作数组,效率更高,那我们来试试?
int* ptu = att2;
int tu2 = *(ptu + 8);
/* int* ptu = att2;
00007FF6F4FBBD24 48 8D 85 68 03 00 00 lea rax, [att2]
00007FF6F4FBBD2B 48 89 85 E8 03 00 00 mov qword ptr[ptu], rax
int tu2 = *(ptu + 4);
00007FF6F4FBBD32 48 8B 85 E8 03 00 00 mov rax, qword ptr[ptu]
00007FF6F4FBBD39 8B 40 10 mov eax, dword ptr[rax + 10h] // 10h=16 然后编译器直接帮我们算好了
00007FF6F4FBBD3C 89 85 04 04 00 00 mov dword ptr[tu2], eax
*/
// 结论,老师没有骗我们,哈哈哈哈
1.9.2 练手1:提取图像的色调
本来数组写到这里就结束了,后面突然有一个需求,需要提取图像色调,也刚好符合数组练手的要求,就加上来一起讲了。
1.9.3 练手2:图像中占比TOP N的颜色
如果只是提出最大值,那是很简单的,我们提高一下需求难度,提取TOP 10种颜色。
这个不知道放这里合适不合适,反正这题是面试高频题,面试题是,在一篇文章中,取出使用最多的10个字母。
1.10 字符串
我们在生活中随时可见的都是字符串,比如名字,地址,甚至电话号码都是用字符串存储的。在我们游戏中,使用字符串的地方更多,玩家的名字,还有我们要写的菜单栏,也是需要字符串的。我们现在就学习字符串,然后立马就可以使用到我们的菜单栏上了。
1.10.1 C类型字符串
我们先复习一下c语言的字符串。
c语言的字符串定义有两种:
- char * (字符指针)
- char [] (字符数组)
这两种我们来试试:
// 这竟然要加const少见
const char* str = "I Love wuxie";
char str2[] = "I Love wuxie2";
cout << str << " " << str2 << endl;
// 第一种 字符指针的方式,我们不能修改字符串的内容,这是因为char *指向的字符串,存储到只读区(这个我们分析内存分布的时候才讲)
//str[1] = 'd';
// 第二种 字符数组,是可以修改内容的,因为这个是存储在我们栈里面的
str2[2] = 'd';
cout << str2 << endl;
// 这里是不是有疑问了,只有一个指针,怎么知道这个字符串的长度呢?
// c类型的字符串会在结尾的地方有一个字符'0' ,这个0不是数字0,是字符0
// 所以字符串的长度都是遍历到结尾才知道的
char* pStr3 = str2;
while (*pStr3)
{
cout << *pStr3 << " ";
pStr3++; // 指针不知道的我们等下分析
}
cout << endl << "结束了";
也正因为c语言的字符串需要最后的\0来做结尾,导致很多新手程序员经常忘记这个,所以c++才做了升级。
关于c语言的字符串处理这个链接讲的很仔细:
C语言超全面讲解字符串函数
1.10.2 string
c++作为c语言的进化版,引入了string类,类是什么,我们之后再说,我们先来看看string怎么使用。
string s1 = "I Love China";
string s2(s1);
string s3("哈哈哈");
cout << s1 << s2 << s3 << endl;
是不是很简单,并且不用操作长度问题,string类可以简单理解为,里面有一个字符数组,还有一个长度,就是这两个值来维护这我们字符串的存储。
可以看看这个string的常见函数
String的常见函数
不过使用最多的,就是string拼接,string拼接是怎么直接相加的,很方便,并且还有一个转换成c的字符串函数:s1.c_str() 这样就能转成c类型的字符串了
1.10.3 wchar_t与wstring
其实在c++98标准中,除了char表示一个字节的字符外,还定义了宽字符wchar_t。
wchar_t d = '中'; // 支持的
这个类型是支持中文的,不过wchar_t的宽度是由编译器决定的,windows上,多数是16位,而在linux上就会被实现为32位。
这样导致跨平台不兼容,后面c++标准会引进其他的字符串类型。
不过除了wchat_t之后,c++11其实也支持wstring,这是一个宽字符串:
wstring wgg = L"中国人";
wcout.imbue(std::locale("chs"));
wcout << wgg << " " << wgg.size() << endl; // 这个也需要专门wcout 来输出,很少见到有使用
不过前面需要加L前缀,感觉比较少用吧,我们这里就知道一下把。
1.10.4 c++11对字符串扩展
- char16_t:用于存储UTF-16编码的Unicode数据
- char32_t:用于存储UTF-32编码的Unicode数据
- chat8_t:c++20用于存储UTF-8编码的Unicode数据
由于这几个类型,所以添加了几个前缀:
- u8表示为UTF-8编码
- u表示为UTF-16编码
- U表示为UTF-32编码
// c++11对字符串的扩展
char16_t c16 = u8'a';
cout << "c16: " << sizeof(c16) << endl; // 用的比较少
除了对字符扩展之外,还对字符串也做了扩展
Strings library
std::string std::basic_string
std::wstring std::basic_string<wchar_t>
std::u8string (since C++20) std::basic_string<char8_t>
std::u16string (since C++11) std::basic_string<char16_t>
std::u32string (since C++11)std::basic_string<char32_t>
// c++11对字符串的扩展
char16_t c16 = u8'a';
cout << "c16: " << sizeof(c16) << endl; // 用的比较少
std::u16string u16 = u"中国人不骗中国人";
cout.widen(2);
// cout << "u16: " << u16 << endl; // 这个好像也输出不了 具体就不研究了,目前来看用的地方不多
1.10.5 c++17字符串视图
在c++17中,引入了一个字符串视图:std::string_view。它可以让我们向处理字符串一样处理字符序列,而不需要为它分配空间。
这样看着是不是很完美,其实不是的,std::string_view只是引用一个外部字符串序列,也就是一个指向真正字符串的指针。
借用了网络上的图,还不错,就是这个意思,总感觉怪怪的。
string_view hello{ "hello world" };
cout << hello << endl;
// 并且也定义这些
/*using u8string_view = basic_string_view<char8_t>;
using u16string_view = basic_string_view<char16_t>;
using u32string_view = basic_string_view<char32_t>;
using wstring_view = basic_string_view<wchar_t>;*/
这个我们记住一下,需要用的时候再用。用的时候注意这个string_view指向的string对象不能释放,这就是由自己控制的了。
string* sstr = new string("hello world");
string_view hello = *sstr;
cout << hello << endl;
delete sstr;
cout << hello << endl;
// 演示不能更改
string* sstr2 = new string("hello world");
string_view hello2 = *sstr2;
// hello2[2] = '3'; // 字符串视图不能修改,只能缩写视图,或者读取元素
错误示范,用这个一定要注意指向的问题。
视图可以用做函数参数,接受的实参类型,比较多:
void show(string_view view)
{
cout << "view: " << view << endl;
}
// 视图可以用作函数参数,后面写了之后发现,不管用什么参数,都可以接受,视图牛逼
string* sstr2 = new string("hello world");
string_view hello2 = *sstr2;
show(hello2);
show(*sstr2);
string s = "hahah";
show(s);
const char* pss = "你好";
show(pss);
char str3[] = "I Love wuxie2";
show(str3);
1.11 菜单栏实现
// 01.4 菜单栏实现.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include<conio.h>
using namespace std;
constexpr int MENUS_MAX = 9; // 定义9个菜单
// 我们知道了有字符串数组,我们这词就存储起来
string menus[MENUS_MAX] = { "继 续" , "新游戏" , "选 项" , "说 明", "最高分", "好 友", "聊 天", "商 店", "登 录" };
enum class Page : char {
Welcome = 0, // 欢迎页面
Menus, // 菜单页面
Continue, // 继续游戏页面
New, // 新游戏页面
Selected, // 选项页面
Explain, // 说明页面
Score, // 分数页面
Friend, // 好友页面
Chat, // 聊天页面
Shop, // 商店页面
Login, // 登录页面
};
int main()
{
//std::cout << "Hello World!\n";
char ch;
int index = 0;
Page page = Page::Menus;
while (1)
{
system("cls"); // 这个是系统的小黑框 清屏函数
// 这里可以使用for来替换
/*cout << "继续" << endl;
cout << "新游戏" << endl;
cout << "游戏选项" << endl;
cout << "游戏规则说明" << endl;
cout << "最高分" << endl;
cout << "商店" << endl;
cout << "好友" << endl;*/
//cout << index << endl;
// 我们需要在菜单栏前面+* 表示我选中了这个
string prex = " ";
for (int i = 0; i < MENUS_MAX; i++)
{
if (i == index)
{
prex = " *";
}
else
{
prex = " ";
}
string show = prex + menus[i]; // 我们可以直接用+号拼接
cout << show << i << index << endl;
}
// 这里为啥不用cin,是因为cin需要回车,这个函数是conio.h的函数,不需要使用回车就可以获取到按键的值
ch = _getch();
switch (ch)
{
case 'H':
if (index > 0)
{
index--;
}
break;
case 'P':
if (index < MENUS_MAX - 1)
{
index++;
}
break;
}
}
}
1.12 作业
面试题 17.14. 最小K个数
https://leetcode.cn/problems/smallest-k-lcci/