首页
前端面试题
前端报错总结
电子书
更多
插件下载
Search
1
JavaScript基础(二)操作符 流程控制
42 阅读
2
HTML基础
20 阅读
3
Vue基础
17 阅读
4
wctype.h
14 阅读
5
Vue2(知识点)
13 阅读
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
登录
Search
liuxiaobai
累计撰写
108
篇文章
累计收到
12
条评论
首页
栏目
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
104
篇与
的结果
2023-09-20
多字节字符
多字节字符本章介绍 C 语言如何处理非英语字符。Unicode 简介C 语言诞生时,只考虑了英语字符,使用7位的 ASCII 码表示所有字符。ASCII 码的范围是0到127,也就是最多只能表示100多个字符,用一个字节就可以表示,所以char类型只占用一个字节。但是,如果处理非英语字符,一个字节就不够了,单单是中文,就至少有几万个字符,字符集就势必使用多个字节表示。最初,不同国家有自己的字符编码方式,这样不便于多种字符的混用。因此,后来就逐渐统一到 Unicode 编码,将所有字符放入一个字符集。Unicode 为每个字符提供一个号码,称为码点(code point),其中0到127的部分,跟 ASCII 码是重合的。通常使用“U+十六进制码点”表示一个字符,比如U+0041表示字母A。Unicode 编码目前一共包含了100多万个字符,码点范围是 U+0000 到 U+10FFFF。完整表达整个 Unicode 字符集,至少需要三个字节。但是,并不是所有文档都需要那么多字符,比如对于 ASCII 码就够用的英语文档,如果每个字符使用三个字节表示,就会比单字节表示的文件体积大出三倍。为了适应不同的使用需求,Unicode 标准委员会提供了三种不同的表示方法,表示 Unicode 码点。UTF-8:使用1个到4个字节,表示一个码点。不同的字符占用的字节数不一样。UTF-16:对于U+0000 到 U+FFFF 的字符(称为基本平面),使用2个字节表示一个码点。其他字符使用4个字节。UTF-32:统一使用4个字节,表示一个码点。其中,UTF-8 的使用最为广泛,因为对于 ASCII 字符(U+0000 到 U+007F),它只使用一个字节表示,这就跟 ASCII 的编码方式完全一样。C 语言提供了两个宏,表示当前系统支持的编码字节长度。这两个宏都定义在头文件limits.h。MB_LEN_MAX:任意支持地区的最大字节长度,定义在limits.h。MB_CUR_MAX:当前语言的最大字节长度,总是小于或等于MB_LEN_MAX,定义在stdlib.h。字符的表示方法字符表示法的本质,是将每个字符映射为一个整数,然后从编码表获得该整数对应的字符。C 语言提供了不同的写法,用来表示字符的整数号码。\123:以八进制值表示一个字符,斜杠后面需要三个数字。\x4D:以十六进制表示一个字符,\x后面是十六进制整数。\u2620:以 Unicode 码点表示一个字符(不适用于 ASCII 字符),码点以十六进制表示,\u后面需要4个字符。\U0001243F:以 Unicode 码点表示一个字符(不适用于 ASCII 字符),码点以十六进制表示,\U后面需要8个字符。printf("ABC\n"); printf("\101\102\103\n"); printf("\x41\x42\x43\n");上面三行都会输出“ABC”。printf("\u2022 Bullet 1\n"); printf("\U00002022 Bullet 1\n");上面两行都会输出“• Bullet 1”。多字节字符的表示C 语言预设只有基本字符,才能使用字面量表示,其它字符都应该使用码点表示,并且当前系统还必须支持该码点的编码方法。所谓基本字符,指的是所有可打印的 ASCII 字符,但是有三个字符除外:@、$、 ` 。因此,遇到非英语字符,应该将其写成 Unicode 码点形式。char* s = "\u6625\u5929"; printf("%s\n", s); // 春天上面代码会输出中文“春天”。如果当前系统是 UTF-8 编码,可以直接用字面量表示多字节字符。char* s = "春天"; printf("%s\n", s);注意,\u + 码点和\U + 码点的写法,不能用来表示 ASCII 码字符(码点小于0xA0的字符),只有三个字符除外:0x24($),0x40(@)和0x60( ` )。char* s = "\u0024\u0040\u0060"; printf("%s\n", s); // @$`上面代码会输出三个 Unicode 字符“@$`”,但是其它 ASCII 字符都不能用这种表示法表示。为了保证程序执行时,字符能够正确解读,最好将程序环境切换到本地化环境。setlocale(LC_ALL, "");上面代码中,使用setlocale()切换执行环境到系统的本地化语言。setlocale()的原型定义在头文件locale.h,详见标准库部分的《locale.h》章节。像下面这样,指定编码语言也可以。setlocale(LC_ALL, "zh_CN.UTF-8");上面代码将程序执行环境,切换到中文环境的 UTF-8 编码。C 语言允许使用u8前缀,对多字节字符串指定编码方式为 UTF-8。char* s = u8"春天"; printf("%s\n", s);一旦字符串里面包含多字节字符,就意味着字符串的字节数与字符数不再一一对应了。比如,字符串的长度为10字节,就不再是包含10个字符,而可能只包含7个字符、5个字符等等。setlocale(LC_ALL, ""); char* s = "春天"; printf("%d\n", strlen(s)); // 6上面示例中,字符串s只包含两个字符,但是strlen()返回的结果却是6,表示这两个字符一共占据了6个字节。C 语言的字符串函数只针对单字节字符有效,对于多字节字符都会失效,比如strtok()、strchr()、strspn()、toupper()、tolower()、isalpha()等不会得到正确结果。宽字符上一小节的多字节字符串,每个字符的字节宽度是可变的。这种编码方式虽然使用起来方便,但是很不利于字符串处理,因此必须逐一检查每个字符占用的字节数。所以除了这种方式,C 语言还提供了确定宽度的多字节字符存储方式,称为宽字符(wide character)。所谓“宽字符”,就是每个字符占用的字节数是固定的,要么是2个字节,要么是4个字节。这样的话,就很容易快速处理。宽字符有一个单独的数据类型 wchar_t,每个宽字符都是这个类型。它属于整数类型的别名,可能是有符号的,也可能是无符号的,由当前实现决定。该类型的长度为16位(2个字节)或32位(4个字节),足以容纳当前系统的所有字符。它定义在头文件wchar.h里面。宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。setlocale(LC_ALL, ""); wchar_t c = L'牛'; printf("%lc\n", c); wchar_t* s = L"春天"; printf("%ls\n", s);上面示例中,前缀“L”在单引号前面,表示宽字符,对应printf()的占位符为%lc;在双引号前面,表示宽字符串,对应printf()的占位符为%ls。宽字符串的结尾也有一个空字符,不过是宽空字符,占用多个字节。处理宽字符,需要使用宽字符专用的函数,绝大部分都定义在头文件wchar.h。多字节字符处理函数mblen()mblen()函数返回一个多字节字符占用的字节数。它的原型定义在头文件stdlib.h。int mblen(const char* mbstr, size_t n);它接受两个参数,第一个参数是多字节字符串指针,一般会检查该字符串的第一个字符;第二个参数是需要检查的字节数,这个数字不能大于当前系统单个字符占用的最大字节,一般使用MB_CUR_MAX。它的返回值是该字符占用的字节数。如果当前字符是空的宽字符,则返回0;如果当前字符不是有效的多字节字符,则返回-1。setlocale(LC_ALL, ""); char* mbs1 = "春天"; printf("%d\n", mblen(mbs1, MB_CUR_MAX)); // 3 char* mbs2 = "abc"; printf("%d\n", mblen(mbs2, MB_CUR_MAX)); // 1上面示例中,字符串“春天”的第一个字符“春”,占用3个字节;字符串“abc”的第一个字符“a”,占用1个字节。wctomb()wctomb()函数(wide character to multibyte)用于将宽字符转为多字节字符。它的原型定义在头文件stdlib.h。int wctomb(char* s, wchar_t wc);wctomb()接受两个参数,第一个参数是作为目标的多字节字符数组,第二个参数是需要转换的一个宽字符。它的返回值是多字节字符存储占用的字节数量,如果无法转换,则返回-1。setlocale(LC_ALL, ""); wchar_t wc = L'牛'; char mbStr[10] = ""; int nBytes = 0; nBytes = wctomb(mbStr, wc); printf("%s\n", mbStr); // 牛 printf("%d\n", nBytes); // 3上面示例中,wctomb()将宽字符“牛”转为多字节字符,wctomb()的返回值表示转换后的多字节字符占用3个字节。mbtowc()mbtowc()用于将多字节字符转为宽字符。它的原型定义在头文件stdlib.h。int mbtowc( wchar_t* wchar, const char* mbchar, size_t count );它接受3个参数,第一个参数是作为目标的宽字符指针,第二个参数是待转换的多字节字符指针,第三个参数是多字节字符的字节数。它的返回值是多字节字符的字节数,如果转换失败,则返回-1。setlocale(LC_ALL, ""); char* mbchar = "牛"; wchar_t wc; wchar_t* pwc = &wc; int nBytes = 0; nBytes = mbtowc(pwc, mbchar, 3); printf("%d\n", nBytes); // 3 printf("%lc\n", *pwc); // 牛上面示例中,mbtowc()将多字节字符“牛”转为宽字符wc,返回值是mbchar占用的字节数(占用3个字节)。wcstombs()wcstombs()用来将宽字符串转换为多字节字符串。它的原型定义在头文件stdlib.h。size_t wcstombs( char* mbstr, const wchar_t* wcstr, size_t count );它接受三个参数,第一个参数mbstr是目标的多字节字符串指针,第二个参数wcstr是待转换的宽字符串指针,第三个参数count是用来存储多字节字符串的最大字节数。如果转换成功,它的返回值是成功转换后的多字节字符串的字节数,不包括尾部的字符串终止符;如果转换失败,则返回-1。下面是一个例子。setlocale(LC_ALL, ""); char mbs[20]; wchar_t* wcs = L"春天"; int nBytes = 0; nBytes = wcstombs(mbs, wcs, 20); printf("%s\n", mbs); // 春天 printf("%d\n", nBytes); // 6上面示例中,wcstombs()将宽字符串wcs转为多字节字符串mbs,返回值6表示写入mbs的字符串占用6个字节,不包括尾部的字符串终止符。如果wcstombs()的第一个参数是 NULL,则返回转换成功所需要的目标字符串的字节数。mbstowcs()mbstowcs()用来将多字节字符串转换为宽字符串。它的原型定义在头文件stdlib.h。size_t mbstowcs( wchar_t* wcstr, const char* mbstr, size_t count );它接受三个参数,第一个参数wcstr是目标宽字符串,第二个参数mbstr是待转换的多字节字符串,第三个参数是待转换的多字节字符串的最大字符数。转换成功时,它的返回值是成功转换的多字节字符的数量;转换失败时,返回-1。如果返回值与第三个参数相同,那么转换后的宽字符串不是以 NULL 结尾的。下面是一个例子。setlocale(LC_ALL, ""); char* mbs = "天气不错"; wchar_t wcs[20]; int nBytes = 0; nBytes = mbstowcs(wcs, mbs, 20); printf("%ls\n", wcs); // 天气不错 printf("%d\n", nBytes); // 4上面示例中,多字节字符串mbs被mbstowcs()转为宽字符串,成功转换了4个字符,所以该函数的返回值为4。如果mbstowcs()的第一个参数为NULL,则返回目标宽字符串会包含的字符数量。
2023年09月20日
3 阅读
0 评论
0 点赞
2023-09-20
命令行环境
命令行环境命令行参数C 语言程序可以从命令行接收参数。$ ./foo hello world上面示例中,程序foo接收了两个命令行参数hello和world。程序内部怎么拿到命令行参数呢?C 语言会把命令行输入的内容,放在一个数组里面。main()函数的参数可以接收到这个数组。#include <stdio.h> int main(int argc, char* argv[]) { for (int i = 0; i < argc; i++) { printf("arg %d: %s\n", i, argv[i]); } }上面示例中,main()函数有两个参数argc(argument count)和argv(argument variable)。这两个参数的名字可以任意取,但是一般来说,约定俗成就是使用这两个词。第一个参数argc是命令行参数的数量,由于程序名也被计算在内,所以严格地说argc是参数数量 + 1。第二个参数argv是一个数组,保存了所有的命令行输入,它的每个成员是一个字符串指针。以./foo hello world为例,argc是3,表示命令行输入有三个组成部分:./foo、hello、world。数组argv用来获取这些输入,argv[0]是程序名./foo,argv[1]是hello,argv[2]是world。一般来说,argv[1]到argv[argc - 1]依次是命令行的所有参数。argv[argc]则是一个空指针 NULL。由于字符串指针可以看成是字符数组,所以下面两种写法是等价的。// 写法一 int main(int argc, char* argv[]) // 写法二 int main(int argc, char** argv)另一方面,每个命令行参数既可以写成数组形式argv[i],也可以写成指针形式*(argv + i)。利用argc,可以限定函数只能有多少个参数。#include <stdio.h> int main(int argc, char** argv) { if (argc != 3) { printf("usage: mult x y\n"); return 1; } printf("%d\n", atoi(argv[1]) * atoi(argv[2])); return 0; }上面示例中,argc不等于3就会报错,这样就限定了程序必须有两个参数,才能运行。另外,argv数组的最后一个成员是 NULL 指针(argv[argc] == NULL)。所以,参数的遍历也可以写成下面这样。for (char** p = argv; *p != NULL; p++) { printf("arg: %s\n", *p); }上面示例中,指针p依次移动,指向argv的每个成员,一旦移到空指针 NULL,就表示遍历结束。由于argv的地址是固定的,不能执行自增运算(argv++),所以必须通过一个中间变量p,完成遍历操作。退出状态C 语言规定,如果main()函数没有return语句,那么结束运行的时候,默认会添加一句return 0,即返回整数0。这就是为什么main()语句通常约定返回一个整数值,并且返回整数0表示程序运行成功。如果返回非零值,就表示程序运行出了问题。Bash 的环境变量$?可以用来读取上一个命令的返回值,从而知道是否运行成功。$ ./foo hello world $ echo $? 0上面示例中,echo $?用来打印环境变量$?的值,该值为0,就表示上一条命令运行成功,否则就是运行失败。注意,只有main()会默认添加return 0,其他函数都没有这个机制。环境变量C 语言提供了getenv()函数(原型在stdlib.h)用来读取命令行环境变量。#include <stdio.h> #include <stdlib.h> int main(void) { char* val = getenv("HOME"); if (val == NULL) { printf("Cannot find the HOME environment variable\n"); return 1; } printf("Value: %s\n", val); return 0; }上面示例中,getenv("HOME")用来获取命令行的环境变量$HOME,如果这个变量为空(NULL),则程序报错返回。
2023年09月20日
3 阅读
0 评论
0 点赞
2023-09-20
多文件项目
多文件项目简介一个软件项目往往包含多个源码文件,编译时需要将这些文件一起编译,生成一个可执行文件。假定一个项目有两个源码文件foo.c和bar.c,其中foo.c是主文件,bar.c是库文件。所谓“主文件”,就是包含了main()函数的项目入口文件,里面会引用库文件定义的各种函数。// File foo.c #include <stdio.h> int main(void) { printf("%d\n", add(2, 3)); // 5! }上面代码中,主文件foo.c调用了函数add(),这个函数是在库文件bar.c里面定义的。// File bar.c int add(int x, int y) { return x + y; }现在,将这两个文件一起编译。$ gcc -o foo foo.c bar.c # 更省事的写法 $ gcc -o foo *.c上面命令中,gcc 的-o参数指定生成的二进制可执行文件的文件名,本例是foo。这个命令运行后,编译器会发出警告,原因是在编译foo.c的过程中,编译器发现一个不认识的函数add(),foo.c里面没有这个函数的原型或者定义。因此,最好修改一下foo.c,在文件头部加入add()的原型。// File foo.c #include <stdio.h> int add(int, int); int main(void) { printf("%d\n", add(2, 3)); // 5! }现在再编译就没有警告了。你可能马上就会想到,如果有多个文件都使用这个函数add(),那么每个文件都需要加入函数原型。一旦需要修改函数add()(比如改变参数的数量),就会非常麻烦,需要每个文件逐一改动。所以,通常的做法是新建一个专门的头文件bar.h,放置所有在bar.c里面定义的函数的原型。// File bar.h int add(int, int);然后使用include命令,在用到这个函数的源码文件里面加载这个头文件bar.h。// File foo.c #include <stdio.h> #include "bar.h" int main(void) { printf("%d\n", add(2, 3)); // 5! }上面代码中,#include "bar.h"表示加入头文件bar.h。这个文件没有放在尖括号里面,表示它是用户提供的;它没有写路径,就表示与当前源码文件在同一个目录。然后,最好在bar.c里面也加载这个头文件,这样可以让编译器验证,函数原型与函数定义是否一致。// File bar.c #include "bar.h" int add(int a, int b) { return a + b; }现在重新编译,就可以顺利得到二进制可执行文件。$ gcc -o foo foo.c bar.c重复加载头文件里面还可以加载其他头文件,因此有可能产生重复加载。比如,a.h和b.h都加载了c.h,然后foo.c同时加载了a.h和b.h,这意味着foo.c会编译两次c.h。最好避免这种重复加载,虽然多次定义同一个函数原型并不会报错,但是有些语句重复使用会报错,比如多次重复定义同一个 Struct 数据结构。解决重复加载的常见方法是,在头文件里面设置一个专门的宏,加载时一旦发现这个宏存在,就不再继续加载当前文件了。// File bar.h #ifndef BAR_H #define BAR_H int add(int, int); #endif上面示例中,头文件bar.h使用#ifndef和#endif设置了一个条件判断。每当加载这个头文件时,就会执行这个判断,查看有没有设置过宏BAR_H。如果设置过了,表明这个头文件已经加载过了,就不再重复加载了,反之就先设置一下这个宏,然后加载函数原型。extern 说明符当前文件还可以使用其他文件定义的变量,这时要使用extern说明符,在当前文件中声明,这个变量是其他文件定义的。extern int myVar;上面示例中,extern说明符告诉编译器,变量myvar是其他脚本文件声明的,不需要在这里为它分配内存空间。由于不需要分配内存空间,所以extern声明数组时,不需要给出数组长度。extern int a[];这种共享变量的声明,可以直接写在源码文件里面,也可以放在头文件中,通过#include指令加载。static 说明符正常情况下,当前文件内部的全局变量,可以被其他文件使用。有时候,不希望发生这种情况,而是希望某个变量只局限在当前文件内部使用,不要被其他文件引用。这时可以在声明变量的时候,使用static关键字,使得该变量变成当前文件的私有变量。static int foo = 3;上面示例中,变量foo只能在当前文件里面使用,其他文件不能引用。编译策略多个源码文件的项目,编译时需要所有文件一起编译。哪怕只是修改了一行,也需要从头编译,非常耗费时间。为了节省时间,通常的做法是将编译拆分成两个步骤。第一步,使用 GCC 的-c参数,将每个源码文件单独编译为对象文件(object file)。第二步,将所有对象文件链接在一起,合并生成一个二进制可执行文件。$ gcc -c foo.c # 生成 foo.o $ gcc -c bar.c # 生成 bar.o # 更省事的写法 $ gcc -c *.c上面命令为源码文件foo.c和bar.c,分别生成对象文件foo.o和bar.o。对象文件不是可执行文件,只是编译过程中的一个阶段性产物,文件名与源码文件相同,但是后缀名变成了.o。得到所有的对象文件以后,再次使用gcc命令,将它们通过链接,合并生成一个可执行文件。$ gcc -o foo foo.o bar.o # 更省事的写法 $ gcc -o foo *.o以后,修改了哪一个源文件,就将这个文件重新编译成对象文件,其他文件不用重新编译,可以继续使用原来的对象文件,最后再将所有对象文件重新链接一次就可以了。由于链接的耗时大大短于编译,这样做就节省了大量时间。make 命令大型项目的编译,如果全部手动完成,是非常麻烦的,容易出错。一般会使用专门的自动化编译工具,比如 make。make 是一个命令行工具,使用时会自动在当前目录下搜索配置文件 makefile(也可以写成 Makefile)。该文件定义了所有的编译规则,每个编译规则对应一个编译产物。为了得到这个编译产物,它需要知道两件事。依赖项(生成该编译产物,需要用到哪些文件)生成命令(生成该编译产物的命令)比如,对象文件foo.o是一个编译产物,它的依赖项是foo.c,生成命令是gcc -c foo.c。对应的编译规则如下:foo.o: foo.c gcc -c foo.c上面示例中,编译规则由两行组成。第一行首先是编译产物,冒号后面是它的依赖项,第二行则是生成命令。注意,第二行的缩进必须使用 Tab 键,如果使用空格键会报错。完整的配置文件 makefile 由多个编译规则组成,可能是下面的样子。foo: foo.o bar.o gcc -o foo foo.o bar.o foo.o: bar.h foo.c gcc -c foo.c bar.o: bar.h bar.c gcc -c bar.c上面是 makefile 的一个示例文件。它包含三个编译规则,对应三个编译产物(foo.o、bar.o和foo),每个编译规则之间使用空行分隔。有了 makefile,编译时,只要在 make 命令后面指定编译目标(编译产物的名字),就会自动调用对应的编译规则。$ make foo.o # or $ make bar.o # or $ make foo上面示例中,make 命令会根据不同的命令,生成不同的编译产物。如果省略了编译目标,make命令会执行第一条编译规则,构建相应的产物。$ make上面示例中,make后面没有编译目标,所以会执行 makefile 的第一条编译规则,本例是make foo。由于用户期望执行make后得到最终的可执行文件,所以建议总是把最终可执行文件的编译规则,放在 makefile 文件的第一条。makefile 本身对编译规则没有顺序要求。make 命令的强大之处在于,它不是每次执行命令,都会进行编译,而是会检查是否有必要重新编译。具体方法是,通过检查每个源码文件的时间戳,确定在上次编译之后,哪些文件发生过变动。然后,重新编译那些受到影响的编译产物(即编译产物直接或间接依赖于那些发生变动的源码文件),不受影响的编译产物,就不会重新编译。举例来说,上次编译之后,修改了foo.c,没有修改bar.c和bar.h。于是,重新运行make foo命令时,Make 就会发现bar.c和bar.h没有变动过,因此不用重新编译bar.o,只需要重新编译foo.o。有了新的foo.o以后,再跟bar.o一起,重新编译成新的可执行文件foo。Make 这样设计的最大好处,就是自动处理编译过程,只重新编译变动过的文件,因此大大节省了时间。
2023年09月20日
3 阅读
0 评论
0 点赞
1
...
11
12
13
...
35