首页
前端面试题
前端报错总结
电子书
更多
插件下载
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语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
41
篇与
的结果
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 点赞
2023-09-20
变量说明符
变量说明符C 语言允许声明变量的时候,加上一些特定的说明符(specifier),为编译器提供变量行为的额外信息。它的主要作用是帮助编译器优化代码,有时会对程序行为产生影响。constconst说明符表示变量是只读的,不得被修改。const double PI = 3.14159; PI = 3; // 报错上面示例里面的const,表示变量PI的值不应改变。如果改变的话,编译器会报错。对于数组,const表示数组成员不能修改。const int arr[] = {1, 2, 3, 4}; arr[0] = 5; // 报错上面示例中,const使得数组arr的成员无法修改。对于指针变量,const有两种写法,含义是不一样的。如果const在*前面,表示指针指向的值不可修改。// const 表示指向的值 *x 不能修改 int const * x // 或者 const int * x下面示例中,对x指向的值进行修改导致报错。int p = 1 const int* x = &p; (*x)++; // 报错如果const在*后面,表示指针包含的地址不可修改。// const 表示地址 x 不能修改 int* const x下面示例中,对x进行修改导致报错。int p = 1 int* const x = &p; x++; // 报错这两者可以结合起来。const char* const x;上面示例中,指针变量x指向一个字符串。两个const意味着,x包含的内存地址以及x指向的字符串,都不能修改。const的一个用途,就是防止函数体内修改函数参数。如果某个参数在函数体内不会被修改,可以在函数声明时,对该参数添加const说明符。这样的话,使用这个函数的人看到原型里面的const,就知道调用函数前后,参数数组保持不变。void find(const int* arr, int n);上面示例中,函数find的参数数组arr有const说明符,就说明该数组在函数内部将保持不变。有一种情况需要注意,如果一个指针变量指向const变量,那么该指针变量也不应该被修改。const int i = 1; int* j = &i; *j = 2; // 报错上面示例中,j是一个指针变量,指向变量i,即j和i指向同一个地址。j本身没有const说明符,但是i有。这种情况下,j指向的值也不能被修改。staticstatic说明符对于全局变量和局部变量有不同的含义。(1)用于局部变量(位于块作用域内部)。static用于函数内部声明的局部变量时,表示该变量的值会在函数每次执行后得到保留,下次执行时不会进行初始化,就类似于一个只用于函数内部的全局变量。由于不必每次执行函数时,都对该变量进行初始化,这样可以提高函数的执行速度,详见《函数》一章。(2)用于全局变量(位于块作用域外部)。static用于函数外部声明的全局变量时,表示该变量只用于当前文件,其他源码文件不可以引用该变量,即该变量不会被链接(link)。static修饰的变量,初始化时,值不能等于变量,必须是常量。int n = 10; static m = n; // 报错上面示例中,变量m有static修饰,它的值如果等于变量n,就会报错,必须等于常量。只在当前文件里面使用的函数,也可以声明为static,表明该函数只在当前文件使用,其他文件可以定义同名函数。static int g(int i);autoauto说明符表示该变量的存储,由编译器自主分配内存空间,且只存在于定义时所在的作用域,退出作用域时会自动释放。由于只要不是extern的变量(外部变量),都是由编译器自主分配内存空间的,这属于默认行为,所以该说明符没有实际作用,一般都省略不写。auto int a; // 等同于 int a;externextern说明符表示,该变量在其他文件里面声明,没有必要在当前文件里面为它分配空间。通常用来表示,该变量是多个文件共享的。extern int a;上面代码中,a是extern变量,表示该变量在其他文件里面定义和初始化,当前文件不必为它分配存储空间。但是,变量声明时,同时进行初始化,extern就会无效。// extern 无效 extern int i = 0; // 等同于 int i = 0;上面代码中,extern对变量初始化的声明是无效的。这是为了防止多个extern对同一个变量进行多次初始化。函数内部使用extern声明变量,就相当于该变量是静态存储,每次执行时都要从外部获取它的值。函数本身默认是extern,即该函数可以被外部文件共享,通常省略extern不写。如果只希望函数在当前文件可用,那就需要在函数前面加上static。extern int f(int i); // 等同于 int f(int i);registerregister说明符向编译器表示,该变量是经常使用的,应该提供最快的读取速度,所以应该放进寄存器。但是,编译器可以忽略这个说明符,不一定按照这个指示行事。register int a;上面示例中,register提示编译器,变量a会经常用到,要为它提供最快的读取速度。register只对声明在代码块内部的变量有效。设为register的变量,不能获取它的地址。register int a; int *p = &a; // 编译器报错上面示例中,&a会报错,因为变量a可能放在寄存器里面,无法获取内存地址。如果数组设为register,也不能获取整个数组或任一个数组成员的地址。register int a[] = {11, 22, 33, 44, 55}; int p = a; // 报错 int a = *(a + 2); // 报错历史上,CPU 内部的缓存,称为寄存器(register)。与内存相比,寄存器的访问速度快得多,所以使用它们可以提高速度。但是它们不在内存之中,所以没有内存地址,这就是为什么不能获取指向它们的指针地址。现代编译器已经有巨大的进步,会尽可能优化代码,按照自己的规则决定怎么利用好寄存器,取得最佳的执行速度,所以可能会忽视代码里面的register说明符,不保证一定会把这些变量放到寄存器。volatilevolatile说明符表示所声明的变量,可能会预想不到地发生变化(即其他程序可能会更改它的值),不受当前程序控制,因此编译器不要对这类变量进行优化,每次使用时都应该查询一下它的值。硬件设备的编程中,这个说明符很常用。volatile int foo; volatile int* bar;volatile的目的是阻止编译器对变量行为进行优化,请看下面的例子。int foo = x; // 其他语句,假设没有改变 x 的值 int bar = x;上面代码中,由于变量foo和bar都等于x,而且x的值也没有发生变化,所以编译器可能会把x放入缓存,直接从缓存读取值(而不是从 x 的原始内存位置读取),然后对foo和bar进行赋值。如果x被设定为volatile,编译器就不会把它放入缓存,每次都从原始位置去取x的值,因为在两次读取之间,其他程序可能会改变x。restrictrestrict说明符允许编译器优化某些代码。它只能用于指针,表明该指针是访问数据的唯一方式。int* restrict pt = (int*) malloc(10 * sizeof(int));上面示例中,restrict表示变量pt是访问 malloc 所分配内存的唯一方式。下面例子的变量foo,就不能使用restrict修饰符。int foo[10]; int* bar = foo;上面示例中,变量foo指向的内存,可以用foo访问,也可以用bar访问,因此就不能将foo设为 restrict。如果编译器知道某块内存只能用一个方式访问,可能可以更好地优化代码,因为不用担心其他地方会修改值。restrict用于函数参数时,表示参数的内存地址之间没有重叠。void swap(int* restrict a, int* restrict b) { int t; t = *a; *a = *b; *b = t; }上面示例中,函数参数声明里的restrict表示,参数a和参数b的内存地址没有重叠。
2023年09月20日
4 阅读
0 评论
0 点赞
1
...
7
8
9
...
14