3.4 避免”举隅 [yú] 法”

“举隅法”(synecdoche)是一种文学修辞上的手段,有点类似于以微笑表示喜悦、赞许之情,或以隐喻表示指代物与被指物的相互关系。在《牛津英语辞典》 中,对“举隅法”(synecdoche)是这样解释的:“以含义更宽泛的词语来代替含 义相对较窄的词语,或者相反;例如,以整体代表部分,或者以部分代表整体, 以生物的类来代表生物的种,或者以生物的种来代表生物的类,等等。”

《牛津英语辞典》中这一词条的说明,倒是恰如其份地描述了 C语言中一个 常见的“陷阱”:混淆指针与指针所指向的数据。对于字符串的情形,编程者更是经常犯这种错误。例如:

char *p,*q;
p = "xyz";

尽管某些时候我们可以不妨认为,上面的赋值语句使得p的值就是字符串 “xyz”,然而实际情况并不是这样,记住这一点尤其重要。实际上,p的值是一个 指向由’x’、’y’、’z’和’ \0’,4个字符组成的数组的起始元素的指针。因此,如果我们执行下面的语句:

q = p;

p和q现在是两个指向内存中同一地址的指针。这个赋值语句并没有同时复制内 存中的字符。

我们需要记住的是,复制指针并不同时复制指针所指向的数据。

因此,当我们执行完下面的语句之后:

q[1] =’Y’;

q所指向的内存现在存储的是字符串’xYz’。因为p和q所指向的是同一块内存, 所以p指向的内存中存储的当然也是字符串’xYz’。

3.5 空指针并非空字符串

​ 除了一个重要的例外情况,在C语言中将一个整数转换为一个指针,最后得到的结果都取决于具体的C编译器实现。这个特殊情况就是常数0,编译器保证由0转换而来的指针不等于任何有效的指针。出于代码文档化的考虑,常数0这 个值经常用一个符号来代替:

#define NULL 0

当然无论是直接用常数0,还是用符号NULL,效果都是相同的。需要记住的重要一点是,当常数0被转换为指针使用时,这个指针绝对不能被解除引用 (dereference)。换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。下面的写法是完全合法的:

if (p == (char *) 0)...

但是如果要写成这样:

if (strcmp(char *) 0) == 0)...

就是非法的了,原因在于库函数strcmp的实现中会包括査看它的指针参数所指向 内存中的内容的操作。

如果p是一个空指针,即使

printf(p);

printf("%s", p);

的行为也是未定义的。而且,与此类似的语句在不同的计算机上会有不同的效果。

3.6 边界计算与不对称边界

​ 如果一个数组有10个元素,那么这个数组下标的允许取值范围是什么昵?

在标准的 Basic语言中,声明一个拥有10个元素的数组,实际上编译器分配了 11个元素的空间,下标范围从0到10。

​ 在C语言中,这个数组的下标范围是从0到9。一个拥有10个元素的数组中, 存在下标为0的元素,却不存在下标为10的元素。C语言中一个拥有n个元素的数组,却不存在下标为n的元素,它的元素的下标范围是从0到n-1为此,由其他程序语言转而使用C语言的程序员在使用数组时特别要注意。

例如,让我们仔细地来看看本书导读中的一段代码:

int i, a[10];
for (i=1; i<=10;i++)
a[i] = 0;

​ 这段代码本意是要设置数组a中所有元素为0,却产生了一个出人意料的“副效果”。在for语句的比较部分本来是i <10,却写成了 i <=10,因此实际上并不存在的a[10]被设置为0,也就是内存中在数组a之后的一个字(word)的内存被设置为0。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么内存中数组a之后的一个字(word)实际上是分配给了整型变量i。此时,本来循环计数器i的值为10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0.这就陷入了一个死循环。

​ 尽管C语言的数组会让新手感到麻烦,然而C语言中数组的这种特别的设计 正是其最大优势所在。要理解这一点,需要作一些解释。

​ 在所有常见的程序设计错误中,最难于察觉的一类是“栏杆错误”,也常被称为“差一错误”(off-by-one error)。

前面一段讨论了解决这个问题的两种方法,实际上提示了我们避免“栏杆错误”的两个通用原则;

(1) 首先考虑最简单情况下的特例,然后将得到的结果外推,这是原则一。

(2) 仔细计算边界,绝不掉以轻心,这是原则二。

​ 将上面总结的内容牢记在心以后,我们现在来看整数范围的计算。例如,假定整数x满足边界条件x>=16且x<=37,那么此范围内x的可能取值个数有多少? 换句话说,整数序列16, 17, …,37 一共有多少个元素?很显然,答案与37-16 (亦即21)非常接近,那么到底是20, 21还是22呢?

​ 根据原则一,我们考虑最简单情况下的特例。这里假定整数x的取值范围上 界与下界重合,即x>=16且x<=16,显然合理的x取值只有1个整数,即16。所以当上界与下界重合时,此范围内满足条件的整数序列只有1个元素。

​ 再考虑一般的情形,假定下界为1,上界为h。如果满足条件“上界与下界重合”,即l1= h,亦即h-1= 0。根据特例外推的原则,我们可以得出满足条件的整数序列有h-1+ 1个元素。在本例中,就是37-16+1,即22。

造成“栏杆错误”的根源正是“h-1+1”中的“+1”。一个字符串中由下标为16到下标为37的字符元素所组成的子串,它的长度是多少呢?稍不留意,就会得到错误的结果21。很自然地,人们会问这样一个问题:是否存在一些编程技巧,能够降低这类错误发生的可能性昵?

这个编程技巧不但存在,而且可以一言以蔽之:用第一个入界点和第一个出界点来表示一个数值范围。具体而言,前面的例子我们不应说整数x满足边界条件x>=16且x<=37,而是说整数x满足边界条件x>=16且x<38。注意,这里下界是“入界点”,即包括在取值范围之中;而上界是“出界点”,即不包括在取值范围之中。这种不对称也许从数学上而言并不优美,但是它对于程序设计的简化效果却足以令人吃惊:

  1. 取值范围的大小就是上界与下界之差。38—16的值是22,恰恰是不对称边界16和38之间所包括的元素数目。

  2. 如果取值范围为空,那么上界等于下界。这是第1条的直接推论。

  3. 即使取值范围为空,上界也永远不可能小于下界。

    ​ 对于像C这样的数组下标从0开始的语言,不对称边界给程序设计带来的便 利尤其明显:这种数组的上界(即第一个’'出界点”)恰是数组元素的个数!因此, 如果我们要在C语言中定义一个拥有10个元素的数组,那么0就是数组下标的 第一个“入界点”(指处于数组下标范围以内的点,包括边界点),而10就是数组下标中的第一个“出界点"(指不在数组下标范围以内的点,不含边界点)。正因为此,我们这样写:

int a[10], i;
for (i =0; i < 10; i++)
a[i] = 0;

而不是写成下面这样:

int a[10], i;
for (i = 0; i <= 9; i ++)a[i] = 0;

让我们作一个假设,如果C语言的for语句风格类似Algol或者Pascal语言, 那么就会带来一个问题:下面这个语句的含义究竟是什么?

for (i = 0 to 10)a[i]= 0;

如果10是包括在取值范围内的“入界点”,那么i将取11个值,而不是10个值。如果10是不包括在取值范围内的“出界点”,那么原来以其他程序语言为背景的编程者会大为惊讶。

另一种考虑不对称边界的方式是,把上界视作某序列中第一个被占用的元素, 而把下界视作序列中第一个披释放的元素。

​ 当处理各种不同类型的缓冲区时,这种看待问题的方式就特别有用。例如, 考虑这样一个函数,该函数的功能是将长度无规律的输入数据送到缓冲区(即一 块能够容纳N个字符的内存)中去,每当这块内存被“填满”时,就将缓冲区的内容写出。缓冲区的声明可能是下面这个样子:

#define N 1024
static char buffer[N];

我们再设置一个指针变量,让它指向缓冲区的当前位置:

static char *bufptr;

​ 对于指针bufptr,我们应该把重点放在哪个方面呢?是让指针bufptr始终指向缓冲区中最后一个已占用的字符,还是让它指向缓冲区中第一个未占用的字符?前一种选择很有吸引力,但是考虑到我们对“不对称边界”的偏好,后一种选择更为适合。

按照“不对称边界”的惯例,我们可以这样编写语句:

*bufptr++ = c;

这个语句把输入字符c放到缓冲区中,然后指针bufptr递增1,又指向缓冲区中第1个未占用的字符。

根据前面对“不对称边界”的考察,当指针bufptr与&buffer[0]相等时,缓冲区存放的内容为空,因此初始化时声明缓冲区为空可以这样写:

bufptr = &buffer[0];

或者,更简洁一点,直接写成:

bufptr = buffer;

​ 任何时候缓冲区中己存放的字符数都是bufptr - buffer,因此我们可以通过将这个表达式与N作比较,来判断缓冲区是否已满。当缓冲区全部“填满”时,表达式bufptr - buffer就等于N,可以推断缓冲区中未占用的字符数为N - (bufptr - buffer)。

​ 前面所有的这些预备知识一旦掌握,我们就可以开始编写程序了,假设这 函数的名称是bufwrite。函数bufwrite有两个参数,第一个参数是一个指针,指 向将要写入缓冲区的第1个字符;第二个参数是一个整数,代表将要写入缓冲区的字符数。假定我们可以调用函数flushbuffer来把缓冲区中的内容写出,而且函数fiushbuffer会重置指针bufptr,使其指向缓冲区的起始位置。如卜所示;

void bufwrite(char *p,int n)
{while (--n >= 0) {if (bufptr == &buffer[N])flushbuffer();*bufptr++ = *p++;}
}

重复执行表达式–n >= 0只是进行n次迭代的一种方法。要验证这一点,我们可以考察最简单的特例情形,n = 1。因为循环执行n次,每次迭代从输入缓冲区中取走一个字符,所以输入的每个字符都将得到处理,而且也不会额外执行多余的处理操作。

​ 我们注意到前面代码段中出现了bufptr与&buffer[N]的比较,而buffer[N]这 个元素是不存在的!数组buffer的元素下标从0到N-1,根本不可能是N。我们用这种写法:

if (bufptr == &buffer[N])

代替了下面等效的写法:

if (bufptr > &buffer[N - 1])

原因在于我们要坚持遵循“不对称边界”的原则:我们要比较指针bufptr与缓冲区后第一个字符的地址,而&buffer[N]正是这个地址。但是,引用一个并不存在的元素又有什么意义呢?

幸运的是,我们并不需要引用这个元素,而只需要引用这个元素的地址,并且这个地址在我们遇到的所有C语言实现中又是“千真万确”存在的。而且,ANSI C标准明确允许这种用法:数组中实际不存在的“溢界”元素的地址位于数组所占内存之后,这个地址可以用于进行赋值和比较。当然,如果要引用该元素,那就是非法的了。

照前面的写法,程序已经能够工作,但是我们还可以进一步优化,以提高程序的运行速度。尽管一般而论程序优化问题超过了本书所涉及的范围,但这个特定的例子中还是有值得我们考察其有关计数方面的特性。

这个程序绝大部分的开销来自于每次迭代都要进行的两个检査:一个检査用 于判断循环计数器是否到达终值;另一个检査用于判断缓冲区是否已满。这样做的结果就是一次只能转移一个字符到缓冲区。

假定我们有一种方法能够一次移动k个字符。大多数C语言实现(以及全部 正确的ANSIC实现)都有一个库函数memcpy,可以做到这一点,而且这个函数 通常是用汇编语言实现的以提高运行速度。即使你的C语言实现没有提供这个函

数,自己写一个也很容易:

void rnemcpy (char *dest, const char *source,int k)
{while (--k >= 0)*dest++ = *source++;
}

我们现在可以让函数bufwrite利用库函数memcpy来一次转移一批字符到缓 冲区,而不是一次仅转移一个字符。循环中的每次迭代在必要时会刷新缓存,计 算需要移动的字符数,移动这些字符,最后恰当地更新计数器。如下所示:

void bufwrite(char int n){while (n > 0) {​    int k, rem;​    if (bufptr == &buffer[N])​     flushbuffer();​    rem 二 N - (bufptr - buffer);​    k = n > rem? rem: n;​    memcpy(bufptr, p, k);​    bufptr += k;​    p += k;​    n -= k;}}

很多编程者在写出这样的程序时,总是感到有些犹豫不决,他们担心可能会 写错。而有的程序员似乎很有些“大无畏"精神,最后结果还是写错了。确实, 像这样的代码技巧性很强,如果没有很好的理由,我们不应该尝试去做。但是如 果是“师出有名”,那么理解这样的代码应该如何写就很重要了。只要我们记住前面的两个原则,特例外推法和仔细计算边界,我们应该完全有信心做对。

在循环的入口处,n是需要转移到缓冲区的字符数。因此,只要n还大于0,

也就是还有剩余字符没有被转移,循环就应该继续进行下去。每次进入循环体, 我们将要转移k个字符到缓冲区中,而不是像过去一样每次只转移一个字符。上 面的代码中,最后四行语句管理着字符转移的过程,(1)从缓冲区中第1个未占用字符开始,复制k个字符到其中;(2)将指针bufptr指向的地址前移k个字符,使其仍然指向缓冲区中第1个未占用字符;(3)输入字符串的指针p前移k个字符;(4)将n(即待转移的字符数)减去k。我们很容易看到,这些语句正确地完成了各自任务。

在循环的一开始,仍然保留了原来版本中的第一个检查,如果缓冲区已满, 则刷新之,并重置指针bufptr。这就保证了在检查之后,缓冲区中还有空间。

惟一困难的部分就是确定k,即在保证缓冲区安全(不发生溢岀)的情况下可以一次转移的最多字符数。k是下面两个数中较小的一个:输入数据中还剩余 的待转移字符数(即n),以及缓冲区中未占用的字符数(即rem)。

计算rem的方法有两种。前面的例子显示了其中的一种:缓冲区中当前可用 字符数(即rem),是缓冲区中总的字符数(N)减去已占用的字符数(即bufptr- buffer)的差,也就是 N - (bufptr - buffer)。

另一种计算rem的方法是把缓冲区中的空余部分看成一个区间,直接计算这 个区间的长度。指针bufptr指向这个区间的起点,而buffer + N(也就是&buffer[N]) 指向这个区间的终点(出界点)。并且它们满足“不对称边界"的条件,指针bufptr 由于指向的是第1个未占用字符,因此是“入界点”;而&buffer[N]所代表的位置 在数组buffer最后一个元素buffer[N - 1]之后,因此是“出界点”。所以,根据我们的这一观点,缓冲区中的可用字符数为(buffer + N) - bufptr。稍稍思考,我们就会发现

(buffer + N)- bufptr

完全等价于

N - (bufptr - buffer)

再看一个与计数有关的例子。这个例子中,我们需要编写一个程序,该程序 按一定顺序生成一些整数,并将这些整数按列输出。把这个例子的要求说得更明 确一点就是:程序的输出可能包括若干页的整数,每页包括NCOLS列,每列又

包括NROWS个元素,每个元素就是一个待输出的整数。还要注意,程序生成的 整数是按列连续分布的,而不是按行分布的。

对这个例子,我们关注的重点应该放在与计数有关的特性方面,因此不妨再 做一些简化的假设。首先,我们假定这个程序是由两个函数print和flush来实现。而决定哪些数值应该打印,是其他程序的责任。每次当有新的数值生成时,这个另外的程序就会把该数值作为参数传递给函数print,要注意函数print仅当缓冲区已满时才打印,未满时将该数值存入缓冲区;而当最后一个数值生成出来之后,就会调用函数flush刷新,此时无论缓冲区是否已满,其中所有的数值都将被打印。其次,我们假定打印任务分别由三个函数完成:函数printnum在木页的当前位置打印一个数值;函数printnl则打印一个换行符,另起新的一行;函数printpage 则打印一个分页符,另起新的一页。每一行都必须以换行符结束,即使是一页中的最后一行也必须以换行符结束后,然后再打印一个分页符。这些打印函数按照从左到右的顺序“填充”每个输出行,一行被打印后就不能被撤销或变更。

対于这个问题,我们需要意识到的第一点就是,如果要完成程序要求的任务, 某种形式的缓冲区必不可少。我们必须在看到第1列的所有元素之后,才可能知 道第2列的第1个元素(也就是第1行的第2个元素)的内容。但是,我们又必 须在打印完第1行之后,才有可能打印第1列的第2个元素(即第2行的第1个 元素)。

这个缓冲区应该有多大呢?乍一看来,缓冲区似乎需要能够大到足以容纳一 整页的数值;细细一想,并不需要这么大的空间:因为按照问题的定义,我们知 道每页的列数与行数,那么对于最后一列中的每个元素,也就是相应行的最后- 个元素,只要我们得到它的数值,就可以立即打印出来。因此,我们的缓冲区不 必包括最后…列:

#define BUFSIZE (NROWS*(NC0LS-1))static int buffer[BUFSIZE];

我们之所以声明buffer为静态数组,是为了预防它被程序的其他部分存取到。 本书的4.3节详细讨论了 static声明。

我们对函数print的编程策略大致如下:如果缓冲区未满,就把生成的数值放 到缓冲区中;而当缓冲区已满时,此时读入的数值就是一页中最后1列的某个元 素,这时就打印出该元素所对应的行(按照上一段中所讲的,这个元素可以直接

打印,不必放入缓冲区九当一页中所有的行都已经输出,我们就清空缓冲区。

需要注意,这些整数进入缓冲区的顺序与出缓冲区的顺序并不一致:我们是 按列接受数值,却是按行打印数值。这就出现了一个问题,在缓冲区中是同一行 的元素相邻排列还是同一列的元素相邻排列?我们可以任意选择一种方式,这里 假定是同一列的元素相邻排列。这种选择使所有的数值进入缓冲区非常地直截了 当,径直连续排列下去就是了,但是出缓冲区的方式却相对复杂一些。要跟踪元 素进入缓冲区时所处的位置,一个指针就足够了。我们可以初始化这个指针,使 其指向缓冲区的第1个元素:

static int *bufptr 二 buffer;

现在,我们对函数print的结构算是有了一点眉目。函数print接受一个整型 参数,如果缓冲区还有空间,就将其置入缓冲区;否则,执行“某些暂时不能确 定的操作”。让我们把到目前为止对函数print的一些认识记录下来:

voidprint(int n){if (bufptr == &buffer[BUFSIZE]) (/*某些暂时不能确定的操作*/)else*bufptr++ == n;)

这里的“某些暂时不能确定的操作”包括了打印当前行的所有元素,使当前 行的序号递增1,如果一页内的所有行都已经打印,则另起新的一页。为了做到 这些,很显然我们需要记住当前行号;因此,我们声明一个局部静态变量row来 存储当前行号。

我们如何做到打印当前行的所有元素呢?乍一想似乎漫无头绪,实际上如果 看待问题的方式恰当,也就是俗话所说"思路对了”,则相当简单。我们知道,对 于序号为row的行,其第1个元素就是buffer[row]»并且元素buffer[row]肯定存 在。因为元素buffer[row]属于第1列,如果它不存在,则我们根本不可能通过if 语句的条件判断。我们还知道,同一行中的相邻元素在缓冲区中是相隔NROWS 个元素排列的。最后,我们知禮指针bufptr指向的位置刚好在缓冲区中最后一个 已占用元素之后。因此,我们可以通过下面这个循环语句来打印缓冲区中属于当 前行的所有元素(注意,当前行的最后一个元素不在缓冲区,所以是“缓冲区中 属于当前行的所有元素”,而不是“当前行的所有元素”):

int *p;for {p = buffer+row; p < bufptr; p += NROWS)printnum(*p);

这里为了简洁起见,我们用buffer+row代替了&buffer[row]。

剩下的“暂时不能确定的操作”就很简单了:打印当前输入数值(即当前行 的最后一个元素),打印换行符以结束当前行,如果是一页的最后一行还要另起新 的一页:

printnum(n) ;  /★打印当前行的最后一个元素*/printnl {) ; /*另起新的一行*/if (++row == NROWS) {printpage();row二0; /*重置当前行号*/bufptr = buffer; /*重置指针bufptr */}

因此,最后的print函数看上去就像这样:

voidprint(int n)(if (bufptr == &buffer[BUFSIZE]) (static int row = 0;int *p;for (p = buffer+row; p < bufptr;p += NROWS)printnum(*p);printnum(n) ; /*打印当前行的最后一个元素*/printnl () ; /*另起新的一行7if (++row -- NROWS) {printpage();row = 0; /*重置当前行序号*/bufptr 二 buffer;  /*重置指针 bufptr */)} else*bufptr++ = n;

现在我们接近大功告成了:只需要编写函数flush,它的作用是打印缓冲区中 所有剩余元素。要做到这一点,基本机制与函数print中打印当前行所有元素类似, 只需要将其作为内循环,在其上另外套一个外循环(作用是遍历一页中的每一行):

voidflush()int row;for (row = 0; row < NROWS; row++) {int *p;for (p = buffer 4- row; p < bufptr;p += NROWS)printnum(*p);printnl();}printpage();}

函数flush的这个版本显得有些太中规中矩、平白无奇了:如果最后一页只包 括仅仅一列甚至是不完全的一列,函数flush仍然会逐行打印出全部的一页,只不 过没有元素的地方都是空白而已。事实上,即使最后一页为空,函数flush仍然还 会全部打印出来,只不过一页全是空白而已。从技术上说,这种做法虽然也满足 了问题定义中的要求,但却不符合程序美学的观点。如果没有数值可供打印,就 应该立即停止打印。我们可以通过计算缓冲区中有多少项来做到这一点。如果缓 冲区中什么也没有,我们并不需要开始新的一页:

voidflush(){int row;int k二bufptr - buffer; /*计算缓冲区中剩余项的数目*/ if (k > NROWS)k 二 NROWS;if (k > 0) (for (row = 0; row < k; row++) (int *p;for (p = buffer 十 row; p < bufptr;p += NROWS)printnum(*p);printnl();}printpage();

C陷阱与缺陷-疑难问题理解06相关推荐

  1. C陷阱与缺陷-疑难问题理解04

    2.3 注意作为语句结束标志的分号 ​ 在c程序中如果不小心多写了一个分号可能不会造成什么不良后果:这个分号也许会被视作一个不会产生任何实际效果的空语句:或者编译器会因为这个多余的分号而产生一条警告信 ...

  2. 《C陷阱与缺陷》学习笔记

    第一章 词法陷阱 笔记本:<C陷阱与缺陷> 创建时间:2018/4/23 22:06:21                                                  ...

  3. 《C陷阱与缺陷》一导读

    前 言 C陷阱与缺陷 对于经验丰富的行家而言,得心应手的工具在初学时的困难程度往往要超过那些容易上手的工具.刚刚接触飞机驾驶的学员,初航时总是谨小慎微,只敢沿着海岸线来回飞行,等他们稍有经验就会明白这 ...

  4. 《Java解惑》陷阱和缺陷的目录

    陷阱和缺陷的目录 一.词汇问题 1.字母l在许多字体中都与数字1相像. 2.负的十六进制字面常量看起来像是正的. 3.八进制字面常量与十进制字面常量相像. 4.ASCII字符的Unicode转义字符容 ...

  5. C语言三剑客之《C陷阱与缺陷》一书精华提炼

    点击上方"大鱼机器人",选择"置顶/星标公众号" 福利干货,第一时间送达! 1.C陷阱与缺陷概述 C语言像一把雕刻刀,锋利,并且在技师手中非常有用.和任何锋利的 ...

  6. c语言局限性,C语言陷阱与缺陷.pdf

    C 语言陷阱和缺陷[1] winxos 11-01-28 winxos 11-01-28 原著:Andrew Koenig - AT&T Bell Laboratories Murray Hi ...

  7. 写给大数据从业者:数据科学的5个陷阱与缺陷

    来源 | AI 前线 作者 | 陈炬,责编 | Carol 出品 | CSDN云计算(ID:CSDNcloud) 导读: 这篇分享主要总结了数据从业人员在实践中可能遇到的陷阱与缺陷.跟其他新起的行业一 ...

  8. 《C陷阱与缺陷》一第1章 词法“陷阱”1.1 =不同于==

    本节书摘来自异步社区<C陷阱与缺陷>一书中的第1章,第1.1节,作者 [美]Andrew Koenig,更多章节内容可以访问云栖社区"异步社区"公众号查看 第1章 词法 ...

  9. c语言 去掉双引号_技术分享|浅谈C语言陷阱和缺陷

    良好的软件架构.清晰的代码结构.掌握硬件.深入理解C语言是防错的要点,人的思维和经验积累对软件可靠性有很大影响.C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步.软件的质量是由程 ...

最新文章

  1. 用户控件中动态加入脚本引用
  2. 安装了超图、oracle、eclipse、JDK后系统的java进程情况以及java.exe、javaw.exe
  3. 【XSY3048 】Polynominal 数学
  4. 重磅发布|新一代云原生数据仓库AnalyticDB「SQL智能诊断」功能详解
  5. JS template string 神奇术
  6. java重置_JAVA復制數組和重置數組大小
  7. 【五】每个球队胜率统计
  8. php数组能不能静态,php 为什么常量可以用数组定义 静态变量却不能
  9. 第一学期计算机网络作业,2010-2011学年第一学期计算机网络(33973)试卷
  10. 阿尔伯塔大学的计算机科学专业好吗,去阿尔伯塔大学留学这些专业千万不能错过!...
  11. indesign照片放入太大_照片打印机,小米、华为到底哪家强?
  12. hdu-5867 Water problem(水题)
  13. ajaxpro定时刷新页面
  14. android 开机小企鹅_手机root是好是坏 小编来帮你分析
  15. CentOS安装Etcd
  16. LeetCode笔记
  17. 彻底关闭 wps 热点广告
  18. Android图片转换
  19. html5canvas效果跳一跳小游戏,HTML5 Canvas:制作动画特效
  20. Android UI开发细节Api使用技巧总结

热门文章

  1. org.springframework.context.annotation.AnnotationConfigApplicationContext has not been refreshed yet
  2. php printer使用手册,go/printer
  3. HC-SR04超声波模块程序原理和Proteus ISIS仿真
  4. 高新技术企业申请需要什么条件
  5. hdmi接口线_网友很困惑:连接显示器,DP光纤线和HDMI光纤线,究竟哪个更好?...
  6. 出题软件的典型用户和场景,以及用例图.
  7. Oracle创建表、删除表、修改表(添加字段、修改字段、删除字段)语句总结
  8. Java创建二维三维数组的几种方式
  9. mysql在cmd下启动及操作
  10. 【计算机图形学】结课大作业——光照模型(3D场景)