良好的软件架构、清晰的代码结构、掌握硬件、深入理解C语言是防错的要点,人的思维和经验积累对软件可靠性有很大影响。C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步。软件的质量是由程序员的质量以及他们相互之间的协作决定的。因此,我认为防错的重点是要考虑人的因素。

深入一门语言编程,不能仅仅浮于表面。软件的可靠性,与你理解的语言深度密切相关,嵌入式C更是如此。处处皆陷阱。接下来,给大家分享一篇文章,浅谈C语言陷阱和缺陷,希望对大家有所帮助。

一、词法缺陷

编译器的第一个部分常被称为词法分析器。词法分析器检查组成程序的字符序列,并将它们划分为记号,一个记号是一个有一个或多个字符的序列,它在语言被编译时具有一个(相关地)统一的意义。在C中,记号->的意义和组成它的每个独立的字符具有明显的区别,而且其意义独立于->出现的上下文环境。

另外一个例子,考虑下面的语句:

if(x>big)big=x;

该语句中的每一个分离的字符都被划分为一个记号,除了关键字if和标识符big的两个实例。事实上,C程序被两次划分为记号。首先是预处理器读取程序。它必须对程序进行记号划分以发现标识宏的标识符。它必须通过对每个宏进行求值来替换宏调用。最后,经过宏替换的程序又被汇集成字符流送给编译器。编译器再第二次将这个流划分为记号。

1.1=不是==

从Algol派生出来的语言,如Pascal和Ada,用:=表示赋值而用=表示比较。而C语言则是用=表示赋值而用==表示比较。这是因为赋值的频率要高于比较,因此为其分配更短的符号。

此外,C还将赋值视为一个运算符,因此可以很容易地写出多重赋值(如a=b=c),并且可以将赋值嵌入到一个大的表达式中。

这种便捷导致了一个潜在的问题:可能将需要比较的地方写成赋值。因此,下面的语句好像看起来是要检查x是否等于y:

if(x=y)

foo();

而实际上是将x设置为y的值并检查结果是否非零。在考虑下面的一个希望跳过空格、制表符和换行符的循环:

while(c==''||c='t'||c=='n')

c=getc(f);

在与't'进行比较的地方程序员错误地使用=代替了==。这个"比较"实际上是将't'赋给c,然后判断c的(新的)值是否为零。因为't'不为零,这个"比较"将一直为真,因此这个循环会吃尽整个文件。这之后会发生什么取决于特定的实现是否允许一个程序读取超过文件尾部的部分。如果允许,这个循环会一直运行。

一些C编译器会对形如e1=e2的条件给出一个警告以提醒用户。当你确实需要先对一个变量进行赋值之后再检查变量是否非零时,为了在这种编译器中避免警告信息,应考虑显式给出比较符。换句话说,将:

if(x=y)

foo();

改写为:

if((x=y)!=0)

foo();

这样可以清晰地表示你的意图。

1.2&和|不是&&和||

容易将==错写为=是因为很多其他语言使用=表示比较运算。其他容易写错的运算符还有&和&&,或|和||,这主要是因为C语言中的&和|运算符于其他语言中具有类似功能的运算符大为不同。我们将在第4节中贴近地观察这些运算符。

1.3多字符记号

一些C记号,如/、*和=只有一个字符。而其他一些C记号,如/*和==,以及标识符,具有多个字符。当C编译器遇到紧连在一起的/和*时,它必须能够决定是将这两个字符识别为两个分离的记号还是一个单独的记号。C语言参考手册说明了如何决定:"如果输入流到一个给定的字符串为止已经被识别为记号,则应该包含下一个字符以组成能够构成记号的最长的字符串"。因此,如果/是一个记号的第一个字符,并且/后面紧随了一个*,则这两个字符构成了注释的开始,不管其他上下文环境。

下面的语句看起来像是将y的值设置为x的值除以p所指向的值:

y=x/*p/*p指向除数*/;

实际上,/*开始了一个注释,因此编译器简单地吞噬程序文本,直到*/的出现。换句话说,这条语句仅仅把y的值设置为x的值,而根本没有看到p。将这条语句重写为:

y=x/*p/*p指向除数*/;

或者干脆是

y=x/(*p)/*p指向除数*/;

它就可以做注释所暗示的除法了。

这种模棱两可的写法在其他环境中就会引起麻烦。例如,老版本的C使用=+表示现在版本中的+=。这样的编译器会将

a=-1;

视为

a=-1;或a=a-1;

这会让打算写

a=-1;

的程序员感到吃惊。

另一方面,这种老版本的C编译器会将

a=/*b;

断句为

a=/*b;

尽管/*看起来像一个注释。

1.4例外

组合赋值运算符如+=实际上是两个记号。因此,

a+/*strange*/=1

a+=1

是一个意思。看起来像一个单独的记号而实际上是多个记号的只有这一个特例。特别地,

p->a

是不合法的。它和

p->a

不是同义词。

另一方面,有些老式编译器还是将=+视为一个单独的记号并且和+=是同义词。

1.5字符串和字符

单引号和双引号在C中的意义完全不同,在一些混乱的上下文中它们会导致奇怪的结果而不是错误消息。

包围在单引号中的一个字符只是书写整数的另一种方法。这个整数是给定的字符在实现的对照序列中的一个对应的值。因此,在一个ASCII实现中,'a'和0141或97表示完全相同的东西。而一个包围在双引号中的字符串,只是书写一个有双引号之间的字符和一个附加的二进制值为零的字符所初始化的一个无名数组的指针的一种简短方法。

下面的两个程序片断是等价的:

printf("Helloworldn");

charhello[]={'H','e','l','l','o','','w','o','r','l','d','n',0};

printf(hello);

使用一个指针来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替单引号也会得到一个警告消息(反之亦然)。但对于不检查参数类型的编译器却除外。因此,用

printf('n');

来代替

printf("n");

通常会在运行时得到奇怪的结果。

由于一个整数通常足够大,以至于能够放下多个字符,一些C编译器允许在一个字符常量中存放多个字符。这意味着用'yes'代替"yes"将不会被发现。后者意味着"分别包含y、e、s和一个空字符的四个连续存贮器区域中的第一个的地址",而前者意味着"在一些实现定义的样式中表示由字符y、e、s联合构成的一个整数"。这两者之间的任何一致性都纯属巧合。

二、句法缺陷

要理解C语言程序,仅了解构成它的记号是不够的。还要理解这些记号是如何构成声明、表达式、语句和程序的。尽管这些构成通常都是定义良好的,但这些定义有时候是有悖于直觉的或混乱的。

在这一节中,我们将着眼于一些不明显句法构造。

2.1理解声明

我曾经和一些人聊过天,他们那时在书写在一个小型的微处理器上单机运行的C程序。当这台机器的开关打开的时候,硬件会调用地址为0处的子程序。

为了模仿电源打开的情形,我们要设计一条C语句来显式地调用这个子程序。经过一些思考,我们写出了下面的语句:

(*(void(*)())0)();

这样的表达式会令C程序员心惊胆战。但是,并不需要这样,因为他们可以在一个简单的规则的帮助下很容易地构造它:以你使用的方式声明它。

每个C变量声明都具有两个部分:一个类型和一组具有特定格式的期望用来对该类型求值的表达式。最简单的表达式就是一个变量:

floatf,g;

说明表达式f和g--在求值的时候--具有类型float。由于待求值的是表达式,因此可以自由地使用圆括号:

float((f));

这表示((f))求值为float并且因此,通过推断,f也是一个float。

同样的逻辑用在函数和指针类型。例如:

floatff();

表示表达式ff()是一个float,因此ff是一个返回一个float的函数。类似地,

float*pf;

表示*pf是一个float并且因此pf是一个指向一个float的指针。

这些形式的组合声明对表达式是一样的。因此,

float*g(),(*h)();

表示*g()和(*h)()都是float表达式。由于()比*绑定得更紧密,*g()和*(g())表示同样的东西:g是一个返回指float指针的函数,而h是一个指向返回float的函数的指针。

当我们知道如何声明一个给定类型的变量以后,就能够很容易地写出一个类型的模型(cast):只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。因此,由于

float*g();

声明g是一个返回float指针的函数,所以(float*())就是它的模型。

有了这些知识的武装,我们现在可以准备解决(*(void(*)())0)()了。我们可以将它分为两个部分进行分析。首先,假设我们有一个变量fp,它包含了一个函数指针,并且我们希望调用fp所指向的函数。可以这样写:

(*fp)();

如果fp是一个指向函数的指针,则*fp就是函数本身,因此(*fp)()是调用它的一种方法。(*fp)中的括号是必须的,否则这个表达式将会被分析为*(fp())。我们现在要找一个适当的表达式来替换fp。

这个问题就是我们的第二步分析。如果C可以读入并理解类型,我们可以写:

(*0)();

但这样并不行,因为*运算符要求必须有一个指针作为他的操作数。另外,这个操作数必须是一个指向函数的指针,以保证*的结果可以被调用。因此,我们需要将0转换为一个可以描述"指向一个返回void的函数的指针"的类型。

如果fp是一个指向返回void的函数的指针,则(*fp)()是一个void值,并且它的声明将会是这样的:

void(*fp)();

因此,我们需要写:

void(*fp)();

(*fp)();

来声明一个哑变量。一旦我们知道了如何声明该变量,我们也就知道了如何将一个常数转换为该类型:只要从变量的声明中去掉名字即可。因此,我们像下面这样将0转换为一个"指向返回void的函数的指针":

(void(*)())0

接下来,我们用(void(*)())0来替换fp:

(*(void(*)())0)();

结尾处的分号用于将这个表达式转换为一个语句。

在这里,我们就解决了这个问题时没有使用typedef声明。通过使用它,我们可以更清晰地解决这个问题:

typedefvoid(*funcptr)();

(*(funcptr)0)();

2.2运算符并不总是具有你所想象的优先级

假设有一个声明了的常量FLAG是一个整数,其二进制表示中的某一位被置位(换句话说,它是2的某次幂),并且你希望测试一个整型变量flags该位是否被置位。通常的写法是:

if(flags&FLAG)...

其意义对于很多C程序员都是很明确的:if语句测试括号中的表达式求值的结果是否为0。出于清晰的目的我们可以将它写得更明确:

if(flags&FLAG!=0)...

这个语句现在更容易理解了。但它仍然是错的,因为!=比&绑定得更紧密,因此它被分析为:

if(flags&(FLAG!=0))...

这(偶尔)是可以的,如FLAG是1或0(!)的时候,但对于其他2的幂是不行的脚注[2]。

假设你有两个整型变量,h和l,它们的值在0和15(含0和15)之间,并且你希望将r设置为8位值,其低位为l,高位为h。一种自然的写法是:

r=h<<4+1;

不幸的是,这是错误的。加法比移位绑定得更紧密,因此这个例子等价于:

r=h<<(4+l);

正确的方法有两种:

r=(h<<4)+l;

r=h<<4|l;

避免这种问题的一个方法是将所有的东西都用括号括起来,但表达式中的括号过度就会难以理解,因此最好还是是记住C中的优先级。

不幸的是,这有15个,太困难了。然而,通过将它们分组可以变得容易。

绑定得最紧密的运算符并不是真正的运算符:下标、函数调用和结构选择。这些都与左边相关联。

接下来是一元运算符。它们具有真正的运算符中的最高优先级。由于函数调用比一元运算符绑定得更紧密,你必须写(*p)()来调用p指向的函数;*p()表示p是一个返回一个指针的函数。转换是一元运算符,并且和其他一元运算符具有相同的优先级。一元运算符是右结合的,因此*p++表示*(p++),而不是(*p)++。

在接下来是真正的二元运算符。其中数学运算符具有最高的优先级,然后是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西是:

所有的逻辑运算符具有比所有关系运算符都低的优先级。

移位运算符比关系运算符绑定得更紧密,但又不如数学运算符。

在这些运算符类别中,有一些奇怪的地方。乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级。

还有就是六个关系运算符并不具有相同的优先级:==和!=的优先级比其他关系运算符要低。这就允许我们判断a和b是否具有与c和d相同的顺序,例如:

a<b==c<d

在逻辑运算符中,没有任何两个具有相同的优先级。按位运算符比所有顺序运算符绑定得都紧密,每种与运算符都比相应的或运算符绑定得更紧密,并且按位异或(^)运算符介于按位与和按位或之间。

三元运算符的优先级比我们提到过的所有运算符的优先级都低。这可以保证选择表达式中包含的关系运算符的逻辑组合特性,如:

z=a<b&&b<c?d:e

这个例子还说明了赋值运算符具有比条件运算符更低的优先级是有意义的。另外,所有的复合赋值运算符具有相同的优先级并且是自右至左结合的,因此

a=b=c

b=c;a=b;

是等价的。

具有最低优先级的是逗号运算符。这很容易理解,因为逗号通常在需要表达式而不是语句的时候用来替代分号。

赋值是另一种运算符,通常具有混合的优先级。例如,考虑下面这个用于复制文件的循环:

while(c=getc(in)!=EOF)

putc(c,out);

这个while循环中的表达式看起来像是c被赋以getc(in)的值,接下来判断是否等于EOF以结束循环。不幸的是,赋值的优先级比任何比较操作都低,因此c的值将会是getc(in)和EOF比较的结果,并且会被抛弃。因此,"复制"得到的文件将是一个由值为1的字节流组成的文件。

上面这个例子正确的写法并不难:

while((c=getc(in))!=EOF)

putc(c,out);

然而,这种错误在很多复杂的表达式中却很难被发现。例如,随UNIX系统一同发布的lint程序通常带有下面的错误行:

if(((t=BTYPE(pt1->aty)==STRTY)||t==UNIONTY){

这条语句希望给t赋一个值,然后看t是否与STRTY或UNIONTY相等。而实际的效果却大不相同脚注[3]。

C中的逻辑运算符的优先级具有历史原因。B--C的前辈--具有和C中的&和|运算符对应的逻辑运算符。尽管它们的定义是按位的,但编译器在条件判断上下文中将它们视为和&&和||一样。当在C中将它们分开后,优先级的改变是很危险的脚注[4]。

2.3看看这些分号!

C中的一个多余的分号通常会带来一点点不同:或者是一个空语句,无任何效果;或者编译器可能提出一个诊断消息,可以方便除去掉它。一个重要的区别是在必须跟有一个语句的if和while语句中。考虑下面的例子:

if(x[i]>big);

big=x[i];

这不会发生编译错误,但这段程序的意义与:

if(x[i]>big)

big=x[i];

就大不相同了。第一个程序段等价于:

if(x[i]>big){}

big=x[i];

也就是等价于:

big=x[i];

(除非x、i或big是带有副作用的宏)。

另一个因分号引起巨大不同的地方是函数定义前面的结构声明的末尾[译注:这句话不太好听,看例子就明白了]。考虑下面的程序片段:

structfoo{

intx;

}

f(){

...

}

在紧挨着f的第一个}后面丢失了一个分号。它的效果是声明了一个函数f,返回值类型是structfoo,这个结构成了函数声明的一部分。如果这里出现了分号,则f将被定义为具有默认的整型返回值脚注[5]。

2.4switch语句

通常C中的switch语句中的case段可以进入下一个。例如,考虑下面的C和Pascal程序片断:

switch(color){

case1:printf("red");

break;

case2:printf("yellow");

break;

case3:printf("blue");

break;

}

casecolorof

1:write('red');

2:write('yellow');

3:write('blue');

end

这两个程序片断都作相同的事情:根据变量color的值是1、2还是3打印red、yellow或blue(没有新行符)。这两个程序片断非常相似,只有一点不同:Pascal程序中没有C中相应的break语句。C中的case标签是真正的标签:控制流程可以无限制地进入到一个case标签中。

看看另一种形式,假设C程序段看起来更像Pascal:

switch(color){

case1:printf("red");

case2:printf("yellow");

case3:printf("blue");

}

并且假设color的值是2。则该程序将打印yellowblue,因为控制自然地转入到下一个printf()的调用。

这既是C语言switch语句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个break语句,从而导致程序出现隐晦的异常行为。说它是优点,是因为通过故意去掉break语句,可以很容易实现其他方法难以实现的控制结构。尤其是在一个大型的switch语句中,我们经常发现对一个case的处理可以简化其他一些特殊的处理。

例如,设想有一个程序是一台假想的机器的翻译器。这样的一个程序可能包含一个switch语句来处理各种操作码。在这样一台机器上,通常减法在对其第二个运算数进行变号后就变成和加法一样了。因此,最好可以写出这样的语句:

caseSUBTRACT:

opnd2=-opnd2;

/*nobreak;*/

caseADD:

...

另外一个例子,考虑编译器通过跳过空白字符来查找一个记号。这里,我们将空格、制表符和新行符视为是相同的,除了新行符还要引起行计数器的增长外:

case'n':

linecount++;

/*nobreak*/

case't':

case'':

...

2.5函数调用

和其他程序设计语言不同,C要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果f是一个函数,

f();

就是对该函数进行调用的语句,而

f;

什么也不做。它会作为函数地址被求值,但不会调用它脚注[6]。

2.6悬挂else问题

在讨论任何语法缺陷时我们都不会忘记提到这个问题。尽管这一问题不是C语言所独有的,但它仍然伤害着那些有着多年经验的C程序员。

考虑下面的程序片断:

if(x==0)

if(y==0)error();

else{

z=x+y;

f(&z);

}

写这段程序的程序员的目的明显是将情况分为两种:x=0和x!=0。在第一种情况中,程序段什么都不做,除非y=0时调用error()。第二种情况中,程序设置z=x+y并以z的地址作为参数调用f()。

然而,这段程序的实际效果却大为不同。其原因是一个else总是与其最近的if相关联。如果我们希望这段程序能够按照实际的情况运行,应该这样写:

if(x==0){

if(y==0)

error();

else{

z=x+y;

f(&z);

}

}

换句话说,当x!=0发生时什么也不做。如果要达到第一个例子的效果,应该写:

if(x==0){

if(y==0)

error();

}

else{

z=z+y;

f(&z);

}

三、链接

一个C程序可能有很多部分组成,它们被分别编译,并由一个通常称为链接器、链接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。

在这一节中,我们将看到一些这种类型的错误。有一些C实现,但不是所有的,带有一个称为lint的程序来捕获这些错误。如果具有一个这样的程序,那么无论怎样地强调它的重要性都不过分。

3.1你必须自己检查外部类型

假设你有一个C程序,被划分为两个文件。其中一个包含如下声明:

intn;

而令一个包含如下声明:

longn;

这不是一个有效的C程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因此,检查类型的工作只能由链接器(或一些工具程序如lint)来完成;如果操作系统的链接器不能识别数据类型,C编译器也没法过多地强制它。

那么,这个程序运行时实际会发生什么?这有很多可能性:

实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明n在两个文件中具有不同的类型。

你所使用的实现将int和long视为相同的类型。典型的情况是机器可以自然地进行32位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为long(或int)。但这种程序的工作纯属偶然。

n的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将int安排在long的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。

n的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。

这种情况发生的里一个例子出奇地频繁。程序的某一个文件包含下面的声明:

charfilename[]="etc/passwd";

而另一个文件包含这样的声明:

char*filename;

尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。在第二个声明中,filename是一个指针的名字。这个指针可以指向程序员让它指向的任何地方。如果程序员没有给它赋一个值,它将具有一个默认的0值(null)[译注:实际上,在C中一个为初始化的指针通常具有一个随机的值,这是很危险的!]。

这两个声明以不同的方式使用存储区,他们不可能共存。

避免这种类型冲突的一个方法是使用像lint这样的工具(如果可以的话)。为了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但lint可以。

避免该问题的另一种方法是将外部声明放到包含文件中。这时,一个外部对象的类型仅出现一次脚注。

c语言 去掉双引号_技术分享|浅谈C语言陷阱和缺陷相关推荐

  1. c语言函数调用参数调用的太少,浅谈C语言函数调用参数压栈的相关问题

    参数入栈的顺序 以前在面试中被人问到这样的问题,函数调用的时候,参数入栈的顺序是从左向右,还是从右向左.参数的入栈顺序主要看调用方式,一般来说,__cdecl 和__stdcall 都是参数从右到左入 ...

  2. python 去掉双引号_你要偷偷的学Python,然后惊呆所有人(第一天) - python阿喵

    标题无意冒犯,就是觉得这个广告挺好玩的 好,切入正题 文章目录 前言 Python语言概览 Python语言的起源 数据类型 Number数据类型 容器数据类型 str 字符串型 元字符串 字符串的格 ...

  3. python 去掉双引号_你要偷偷的学Python,然后惊呆所有人(第一天)

    标题无意冒犯,就是觉得这个广告挺好玩的 好,切入正题 文章目录 前言 Python语言概览 Python语言的起源 数据类型 Number数据类型 容器数据类型 str 字符串型 元字符串 字符串的格 ...

  4. android hook 第三方app_【MiSRC】技术分享-浅谈android hook技术

    注:本文为"小米安全中心"原创,转载请联系"小米安全中心" 前言 xposed框架 xposed,主页:http://repo.xposed.info/modu ...

  5. 技术分享 | 浅谈滴滴派单算法

    桔妹导读:说到滴滴的派单算法,大家可能感觉到既神秘又好奇,从出租车扬召到司机在滴滴平台抢单最后到平台派单,大家今天的出行体验已经发生了翻天覆地的变化,面对着每天数千万的呼叫,滴滴的派单算法一直在持续努 ...

  6. 技术分享| 浅谈IM 产品中的“缩略图”功能

    在 IM 产品中,发送图片是产品的一个基本功能,如何生成缩略图以及如何显示缩略图都是该模块的组成部分,本文就如何显示缩略图从产品设计的角度去阐述. 从产品上来确认该功能如何去设计,最稳妥的方式就是模仿 ...

  7. 技术分享 | 浅谈 MySQL 的临时表和临时文件

    作者:姚嵩 爱可生南区交付服务部经理,爱好音乐,动漫,电影,游戏,人文,美食,旅游,还有其他.虽然都很菜,但毕竟是爱好. 本文来源:原创投稿 *爱可生开源社区出品,原创内容未经授权不得随意使用,转载请 ...

  8. c语言中很多中括号由外向里,浅谈C语言中的类型声明

    文章目录 [隐藏] 新年第一更!之前群友问了一个 C语言 问题,即int(*(*p)()).int *(*p)()和int *(*p())的区别在哪里.确实,有时C语言的类型声明是很魔性的,看着也很令 ...

  9. C语言布尔类型占几个字节,浅谈C语言中的布尔(bool)类型

    我们知道在C++里有专门的bool类型,用来表示真或假.但是在C语言里没有这样的类型(至少我是一直这么认为的),表达式的值0为假,非0为真.所以条件判断语句( if(-).while(-) )非常灵活 ...

最新文章

  1. 6、ShardingSphere 之 读写分离
  2. python读写kafka集群(转载+自己验证)
  3. JVM垃圾收集器与内存分配策略学习总结
  4. hibernate保存mysql乱码_hibernate保存数据到mysql时的中文乱码问题
  5. 基础 | 零散的MySql基础记不住,看这一篇就够啦
  6. 友盟统计集成_友盟+智能超链U-Link,助力开发者拥有更极致的分享体验_互联网_科技快报...
  7. 初学计算机语言者(C语言,C++,java,pytion,C#)
  8. i2c时序图的详细讲解_外脚手架及外架防护棚搭设详细讲解,附做法图
  9. java收到邮件后短信提醒_java邮件发送和短信发送(二)
  10. 京瓷4501i打印机扫描步骤_「硬件」如何使用打印机扫描文件传送到电脑本地?...
  11. heartbeat如何监控程序_Nagios监控Heartbeat
  12. Java解析省市县树形结构工具类
  13. 整理arm的一些概念(ARM7、ARM9、ARM11、Cortex-A、Cortex-R、Cortex-M的区别)
  14. 32位与64位CPU字长
  15. 行业分析报告|项目货运物流市场现状及未来发展趋势
  16. 为什么你一直学不好SEO优化?
  17. UGI九宫格sliced显示问题
  18. c语言返回结构体,(C/C++) 用函数返回一个结构体
  19. LMG3422R030RQZR 600V GaN晶体管LMG3425R030RQZR电路图
  20. 2022 GIS保研面试:武大国重、资环、中科院空天院;地信GIS专业/遥感专业保研夏令营面试、预推免面试

热门文章

  1. CentOS 7本地镜像部署NFS服务
  2. chrome下老是弹出网页显示 true
  3. win7 path环境变量被覆盖了怎么恢复_系统小技巧:还原Windows10路径环境变量
  4. java 静态成员 创建_Java学习笔记11---静态成员变量、静态代码块、成员变量及构造方法的初始化或调用顺序...
  5. django使用Paginator分页展示数据
  6. 博弈树α-β剪枝搜索学习参考资料
  7. Ndarry 拉伸为一个list
  8. c语言的学生理系统,C语言学习系统的教程
  9. 解决vue视图不渲染
  10. 昂贵的聘礼(枚举区间+最短路)