今天我们将学习字符串操作,为什么要着重来说这个呢?因为这是为我们之后window开发和api做准备。好的,我们现在正式开始:
字符串
字符串就是一串文字。
比如:"好好学习,天天向上"就是一个字符串
比如:"good good study,day day up"也是字符串
这俩都是字符串,而区别就是一个是英文,一个是中文。
大家都是来学编程的,相信大家一定遇到过有部分软件会出现中文乱码。或者是目录安装中路径不能含有中文的情况,而引起这个问题的原因就是编码问题。
在C语言中,字符串实际上是使用空字符\0结尾的一维字符数组。因此,\0 是用于标记字符串的结束。
比如1:char str1={'a','b','c'};
比如2:char str2="abc";
它们之间 比如1 不是字符串。
我们知道双引号中都是字符串,而单引号之间是字符。
代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>//函数,代码的入口函数
int main()
{char str1[] = "abd";char str2[] = "abc";getchar();return 0;
};
接下来我们看看内存:
可以发现str1的地址存放的数据为61、62、63 以及00。
说明了什么?
说明字符串的结尾真的有0。
这样也就证实了 \0 是用于标记字符串的结束。
给大家一串代码:
char* str = "我爱C语言";printf("%s", str);
请思考,输出的会是地址?还是字符?还是其他的值?
答案会是 字符!
那么这是为什么,明明赋予指针的值应该是地址,而且输出时并没有用 *(解引用),但是输出的还会是地址中的值呢?
很简单:这里设计了三个知识点
1、字符串常量:在C语言中,字符串常量(例如"我爱C语言"
)是一个以空字符('\0'
)结尾的字符数组。在我赋值给char* str
时,str
保存了这个字符数组的地址。
也就是说 我们可以把字符串看成数组,这证明了我们开头所说的理论。
2、printf的处理:当你用printf("%s", str);
来打印str
时,printf
会开始从str
所指向的地址读取字符,直到遇到空字符('\0'
)。在这个过程中,不需要手动解引用,因为printf
函数本身就可以处理指针,按照字符串格式输出。
也就是说:面对字符数组时,我们不用手动解引用,printf本身会处理指针。
3、为何不需要解引用:在C语言中,数组名(或字符串的首地址)在许多上下文中可以隐式转换为指针。因此,当你将指针传递给printf
时,%s
自动处理指针并开始输出从该地址开始的字符。
多字节/ANSI编码
接下来我们看看一段代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<string.h>//函数,代码的入口函数
int main()
{char str1[] = "abc";char str2[] = "abc";char str3[] = "我爱C语言";char* str = "我爱C语言";printf("%s", str1); getchar();return 0;
};
会输出abc,好的这不是重点,重点是我们调式一下,
看看其中str3的内存数值:
我们已经知道,字符串是以0结尾,所以说明
”我爱C语言“占了9个字节!
也就是说
在这个数值中,中文占了2个字节,而字母C占了一个字节,而这些一会占2字节,一会占1字节,这就叫多字节!而多字节其实也就是ANSI编码。
ANSI编码表示英文字符时占用1个字节,而中文时占用2-4个字节。
宽字节
除了多字节,我们还有宽字节。
我们先说说宽字节的概念:
使用2个字节(16位)或更多字节来表示一个字符。
也就是说宽字节字符能够表示更多的字符,包括多种国际字符集中的字符,如中文、日文、韩文等。
接下来我们说说语法:
wchar_t ch = L'A'; // 声明一个宽字符变量,值为'A'
wchar_t str[] = L"Hello, 世界"; // 声明宽字符数组并初始化
wchar_t *str = L"Hello, 世界"; // 声明一个指向宽字符字符串的指针
wprintf(L"%ls\n", str); // 打印宽字符字符串
以上就是宽字节常用的语法。
接下来我们说说用法:
宽字节字符的主要用途是支持多字节字符集,如Unicode字符集。使用宽字符时,需要使用专门的函数,如wprintf
和wscanf
,来处理wchar_t
类型的数据。
总结:
wchar_t
是C语言中的宽字符类型,用来表示支持多字节字符集的字符。- 宽字符用于表示多种语言(如中文、日文等)中的字符,能够存储更多的信息。
- 使用宽字符时,打印时需要用到
wprintf
函数,而不是printf
。 - 在Windows上,
wchar_t
通常占用2字节,在Linux等系统上占用4字节。
现在我们了解了宽字节,接下来看看一串代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>// 函数,代码的入口函数
int main()
{// 字符串常量char* str1 = "abc"; // 正确:str1应该是字符指针// 宽字符字符串wchar_t str2[] = L"abc"; // 宽字符数组wchar_t str3[] = L"我爱"; // 宽字符数组wchar_t* str = L"我爱C语言"; // 宽字符指针getchar();return 0;
}
我们先不用输出,我们先看看内存数值,看看str1和str2之间的区别:
这是str1的内存数值:
这是str2的内存数值:
我们比较一下二者之间的区别,发现str1也就是字符串,它是1字节占位,而str2(宽字节)则是2字节占位,包括最后的\0结尾。
接下来看看str3的内存数值:
没错也是一个中文占了2字节,也就是说
无论是中文还是英文,它都会占用2字节。
接下来我们试试打印宽字节:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>// 函数,代码的入口函数
int main()
{// 字符串常量char* str1 = "abc"; // 正确:str1应该是字符指针// 宽字符字符串wchar_t str2[] = L"abc"; // 宽字符数组wchar_t str3[] = L"我爱"; // 宽字符数组wchar_t* str = L"我爱C语言"; // 宽字符指针wprintf(L"%ls\n",str);getchar();return 0;
}
结果会是什么?
没错乱码:
那我们要怎么办呢?
请看代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <wchar.h> // 需要引入宽字符库
#include <locale.h> // 需要包含 locale.h 头文件// 函数,代码的入口函数
int main()
{setlocale(LC_ALL, "Chinese_China");// 字符串常量char* str1 = "abc"; // 正确:str1应该是字符指针// 宽字符字符串wchar_t str2[] = L"abc"; // 宽字符数组wchar_t str3[] = L"我爱"; // 宽字符数组wchar_t* str = L"我爱C语言"; // 宽字符指针wprintf(L"%ls\n",str);getchar();return 0;
}
接下来我们的输出结果会是:
为什么呢?因为我们添加了setlocale
()函数:
在 C 语言中,setlocale
用于设置程序的语言环境(locale)。常见的用法是在打印宽字符之前设置语言环境,确保正确处理 Unicode 字符。例如,中文的区域设置通常为 "zh_CN.UTF-8"
(Linux/Unix 系统)或 "Chinese_China"
(Windows 系统)。
给大家一个网站,微软的语言字符串,这是应用于setlocale()函数的值。
语言字符串 |Microsoft 学习https://learn.microsoft.com/en-us/cpp/c-runtime-library/language-strings?view=msvc-170
接下来我们拓展一下:
MessageBoxA
我们在以后会经常看到W的出现,也就是宽字节的出现:
比如说:MessageBoxA()消息框函数,它是多字节,A代表:ANSI
想要用到这类函数,我们需要加头文件导入库#include <windows.h>。
函数原型:
int MessageBoxA(HWND hWnd, 父窗口句柄,可以为 NULL 表示无父窗口LPCSTR lpText, 消息框的内容(ANSI 字符串)LPCSTR lpCaption, 消息框的标题(ANSI 字符串)UINT uType 消息框的类型(按钮和图标的组合)
);
参数说明
-
hWnd
:- 指定消息框的父窗口句柄。
- 如果设置为
NULL
,消息框会作为顶层窗口显示。
-
lpText
:- 消息框中显示的文本内容,必须是 ANSI 编码的字符串。
-
lpCaption
:- 消息框的标题,必须是 ANSI 编码的字符串。
-
uType
:- 指定消息框的样式,包括按钮、图标和默认按钮等。
- 由一组预定义的常量组合而成,例如:
- 按钮类型:
MB_OK
:显示一个“确定”按钮。MB_OKCANCEL
:显示“确定”和“取消”按钮。MB_YESNO
:显示“是”和“否”按钮。
- 图标类型:
MB_ICONINFORMATION
:信息图标。MB_ICONERROR
:错误图标。MB_ICONWARNING
:警告图标。
- 示例组合:
MB_OK | MB_ICONINFORMATION
。
- 按钮类型:
返回值
- 返回值是用户点击的按钮对应的常量值,例如:
IDOK
:用户点击了“确定”。IDCANCEL
:用户点击了“取消”。IDYES
:用户点击了“是”。IDNO
:用户点击了“否”。
接下来大家看一段代码,来试着理解一下messageBoxA()函数:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <wchar.h> // 需要引入宽字符库
#include <locale.h> // 需要包含 locale.h 头文件
#include <windows.h> // 包含 Windows API 函数定义int main() {// 弹出一个消息框int result = MessageBoxA(NULL, // 无父窗口"Hello, World!", // 消息框的内容"Greeting", // 消息框的标题MB_OK | MB_ICONINFORMATION // 消息框样式:确定按钮 + 信息图标);// 根据用户的点击进行判断if (result == IDOK) {MessageBoxA(NULL, "You clicked OK!", "Info", MB_OK | MB_ICONINFORMATION);}return 0;
}
根据上述概念,想想它的输出会是什么?
我先把结果放下方:
MessageBoxW:
与MessageBoxA一样,只不过,MessageBoxW代表的是宽字符的一个字符串。
字符串的操作:
strlen
:计算字符串的长度
返回字符串中字符的数量(不包括结束符 '\0'
)。
#include <string.h>
#include <stdio.h>int main() {char str[] = "Hello, World!";printf("Length of str: %zu\n", strlen(str)); // 输出:14return 0;
}
strcmp
:比较两个字符串
返回值:
0
:两个字符串相等。- 正数:第一个字符串大于第二个字符串。
- 负数:第一个字符串小于第二个字符串。
#include <string.h>
#include <stdio.h>int main() {char str1[] = "apple";char str2[] = "banana";int result = strcmp(str1, str2);if (result == 0) {printf("Strings are equal.\n");} else if (result > 0) {printf("str1 is greater than str2.\n");} else {printf("str1 is less than str2.\n");}return 0;
}
strcpy
:复制字符串
将一个字符串复制到另一个字符串中。
#include <string.h>
#include <stdio.h>int main() {char str1[] = "Hello, World!";char str2[50]; // 必须有足够的空间存放复制的内容strcpy(str2, str1);printf("str2: %s\n", str2); // 输出:Hello, World!return 0;
}
strcat
:连接两个字符串
将第二个字符串追加到第一个字符串的末尾。
#include <string.h>
#include <stdio.h>int main() {char str1[50] = "Hello";char str2[] = ", World!";strcat(str1, str2);printf("str1: %s\n", str1); // 输出:Hello, World!return 0;
}
strchr
:查找字符
返回字符串中首次出现指定字符的位置,如果没有找到,返回 NULL
。
#include <string.h>
#include <stdio.h>int main() {char str[] = "Hello, World!";char *ptr = strchr(str, 'o');if (ptr != NULL) {printf("Found 'o' at position: %ld\n", ptr - str); // 输出:4} else {printf("'o' not found.\n");}return 0;
}
strstr
:查找子字符串
返回字符串中首次出现指定子字符串的位置,如果没有找到,返回 NULL
。
#include <string.h>
#include <stdio.h>int main() {char str[] = "Hello, World!";char *ptr = strstr(str, "World");if (ptr != NULL) {printf("Found substring at: %s\n", ptr); // 输出:World!} else {printf("Substring not found.\n");}return 0;
}
strncpy
:安全复制指定长度的字符串
strncpy
将源字符串的前 n
个字符复制到目标字符串,且不会越界。如果源字符串长度小于 n
,则目标字符串会自动填充空字符。
#include <string.h>
#include <stdio.h>int main() {char str1[] = "Hello, World!";char str2[10];strncpy(str2, str1, 9);str2[9] = '\0'; // 确保字符串以'\0'结尾printf("str2: %s\n", str2); // 输出:Hello, Woreturn 0;
}
sprintf
:格式化输出到字符串
sprintf
可以像 printf
一样格式化数据,但将输出结果存储到字符串中。
#include <stdio.h>int main() {char str[50];int a = 10, b = 20;sprintf(str, "The sum of %d and %d is %d", a, b, a + b);printf("%s\n", str); // 输出:The sum of 10 and 20 is 30return 0;
}
memcpy
:复制内存块
将源内存块的内容复制到目标内存块,适用于任意类型的数据,不限于字符串。
#include <string.h>
#include <stdio.h>int main() {char src[] = "Hello, World!";char dest[50];memcpy(dest, src, strlen(src) + 1); // 包括终止符'\0'printf("dest: %s\n", dest); // 输出:Hello, World!return 0;
}
memset
:填充内存块
将内存块中的每个字节设置为指定的值,常用于初始化数组。
#include <string.h>
#include <stdio.h>int main() {char str[50];memset(str, '*', sizeof(str) - 1); // 填充字符‘*’str[49] = '\0'; // 确保字符串以'\0'结尾printf("str: %s\n", str); // 输出:**************************************************return 0;
}
strlwr:返回原字符串的小写形式
strupr:返回原字符串的小写形式
宽字节字符的操作:
strcpy、wcscpy、_mbscpy | Microsoft Learnhttps://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/strcpy-wcscpy-mbscpy?view=msvc-170
C 标准库提供了一些专门操作宽字符的函数,定义在 <wchar.h>
头文件中。常见的宽字符操作包括:
wprintf
:用于输出宽字符字符串。wcslen
:获取宽字符字符串的长度。wcscpy
:复制宽字符字符串。wcscmp
:比较两个宽字符字符串。
#include <stdio.h>
#include <wchar.h>
#include <locale.h>int main() {// 设置程序的区域设置为中文,以便正确处理中文字符setlocale(LC_ALL, "zh_CN.UTF-8");// 定义宽字符字符串wchar_t str[] = L"你好,世界";// 输出宽字符字符串wprintf(L"宽字符字符串:%ls\n", str);// 获取字符串的长度wprintf(L"字符串的长度:%zu\n", wcslen(str));// 复制宽字符字符串wchar_t copy[20];wcscpy(copy, str);wprintf(L"复制的字符串:%ls\n", copy);// 比较宽字符字符串int result = wcscmp(str, copy);if (result == 0) {wprintf(L"两个字符串相等\n");} else {wprintf(L"两个字符串不相等\n");}return 0;
}
注意:
-
区域设置(locale):
wchar_t
字符串的显示与操作通常依赖于系统的区域设置。在 Windows 系统上,你需要设置正确的区域,以确保宽字符能够正确处理。例如,在中文环境下,可以使用"zh_CN.UTF-8"
来设置中文区域。 -
字符集支持:宽字符字符串(如 UTF-16 或 UTF-32 编码)在不同的操作系统和编译器下可能有不同的实现。比如,Windows 通常使用 UTF-16 编码,而一些 Unix-like 系统(如 Linux)可能使用 UTF-32。
-
内存管理:宽字符数组或指针需要足够的内存空间来存储宽字符及其终止符。宽字符通常占用 2 或 4 字节,因此需要根据字符数和编码方式来分配适当的内存。
总结
宽字节操作主要使用 wchar_t
类型及其相关函数来进行。你可以使用 wprintf
、wcscpy
、wcslen
等函数来处理宽字符字符串。同时,确保使用正确的区域设置来支持多字节字符集的显示和处理。
游戏逆向分析中的编码
最后就是我们的逆向分析啦!!
首先要明确一点,这个游戏的语言是什么?
日语、汉语、繁体字他们的编码都是不一样的
所以,分析一个日本游戏,乱码应该如何解码?Shift_JlS
分析一个繁体字游戏,乱码应该如何解码?Big5
分析一个正常的汉字游戏,乱码应该如何解码?GBK
实践1:在线转换utf-8编码转big5编码、Unicode (UTF-8)编码转成繁体中文(Big5)编码、utf-8编码转换big5编码https://www.haomeili.net/HanZi/BianMaZhuanHuan?ToCode=Big5
编码过来之后,它的内存存储的值都是不一样的
实践2:带大家实践一下CE编码转换,与在线转换对比
我们首先打开ce,随便找个软件附加进去
我用的计算器, 进去之后点击内存,随便找个地址,添加进去。
将它地址的类型改为字符串类型:
Codepage是代码页
双击数值,将它的数值改为”你好“
修改完成后再将类型改为字节数组,并将十六进制勾选上(因为它本身就是十六进制)
更改完成后,我们再看数值
接下来找到在线转换,也就是在这一目录下的在线网址查看,简体中文的’你好‘ 的编码,是否能与这里的数值对上。
OK,很明显能对上。
为什么我们要来证明它是否能对上呢,很简单。
如果计算器真的有一段数值是”你好“,那么我们便可以在CE中搜索数值一栏,将编码输入进去,这样便可以找到相关的地址。举个例子:
如果真有”你好“我们来这里搜索数值,数值类型=字节数组。有该数值,我们便能将其搜索出来。
今天的代码就学在这,大家拜拜,我们争取明天见!
如果有疑问可以发在评论下方,我们共同进步哦。