第七课¶
学习编程,快乐与收获时时相伴,最快乐的时光莫过于,头脑一阵风暴,产生一个想法,一通鼓捣,竟然实现了自己的所思所想,最后一声长啸 “神功练成第一重,我终于让计算机动起来了!”。本节可能不会让您产生这种感慨,但是,和一日三餐一样,开卷有益,总有好处。但是在继续之后的课程之前,请确保您已经理解了本节的内容。
内存:堆与栈¶
您可能已经注意到了,每个变量都会占用一定的计算机内存。如果您未曾注意到这点,那么相信现在您已经开始对它进行关注。您可能不了解的是,系统执行的每个程序都有两种不同类型的内存,也就是变量被创建的地方:堆和栈。
栈(Stack) 是具有限制大小的快速内存空间。在您的代码执行前,系统将其载入内存时,会为您的程序预留一个主内存块,用于栈内存。无需深入了解过多细节,简单地说,它的运行速度很快,空间大小也是受限制的,如果您的程序耗尽了栈空间,它将会崩溃。为许多大型数组使用栈空间可是个坏习惯。目前我们使用的所有变量都从栈内存进行分配。
堆(Heap) 是计算机系统内存的主要部分。从堆获取内存耗费时间较长,但是您不需要担心会将其耗尽,除非,您希望为其分配“卑鄙”的数量。使用堆内存的不便之处在于您必须保证,在处理完成之后,将所有从系统中获取的内存返回给系统。如果不这样做将会导致“内存泄露”,也就是在分配内存后未将其释放。这样的话,您的程序将会逐渐耗费更多的系统内存,直到您的程序优雅地或者不甚优雅地退出时,它们才会返还系统。
截至目前,我已经接连的涉及到了 C 和 C++。我们所学的内容对两者来说基本上是相同的。尽管两者的方法都需要指针(也就是我们将要提到的内存指针),但是在内存管理方面,两者是非常不同的。接下来我们来探讨一下 C++ 的方式,因为之后我们会很快涉及到特定于 C++ 的内容。
C 语言的内存分配为使用 malloc() 来获取内存空间的指针以供使用,然后通过对该指针执行 free() 函数将内存块返还系统。也可以使用 realloc() 函数修改通过 malloc() 分配的RAM空间大小。这些函数的使用可能需要一些详细的解释,那么我们通过一个函数来看一下它们的声明,以及细节处理。
void * malloc(size_t size);
void free(void *pointer);
void * realloc(void *pointer, size_t newSize);
之前,我们可能会去思考这些都是用来做什么的,显然这里有一些类型我们未曾见过。第一个新类型是 size_t,但它并不是全新的。它只是我们所熟识的整形类型的新名称而已。这种技巧通常是为了增强可移植性,即,这样的代码很容易在一种系统上完成,并且在另一类系统上进行编译。您只要把它当成 int,一切万事大吉。
另一个新的类型是 空指针 (void指针)。空指针本身并没有类型,可能听起来毫无用处,您可能也会认为我们无法使用 * 解引用操作符来获取它们的值。即便如此,它们的便利之处在于能够传递任何类型的指针,并且当我们需要获取它们的值时,我们可以通过类型转换将其转换为其他类型。
类型转换(Typecasting) ,也可以简称转换,用于告诉编译器将一种类型以其他类型对待。虽然内存保存的值未发生改变,但是它们解析的方式却变了。之后,我们讨论 C++ 优于 C 的特性时,这点就会显得尤为有用。
malloc() 用于为我们获取指定大小的堆内存块。内存块的地址会以空指针的形式返回给我们,因此通常调用 malloc() 时,它的返回值会被转换为其他指针类型。如果 malloc() 无法获取要求大小的内存块,它将返回 NULL。
free() 用于将堆内存返回给系统。当一个函数,如 free() 接受空指针作为参数时,这也就意味着该函数能够接受任何数据类型的指针。这样就在参数传递时,就无需进行类型转换,例如传递堆分配字符串 给free()。一旦将一个指针传递给 free() 以将其内存返回给系统,这个指针在重新赋予有效地址之前将无法使用。尝试使用已经被释放的内存块将会导致无法预期的结果,但是通常会导致段错误(segfault)。如果一个指针在其内存释放之后还将会用到,通常需要将其赋值为 NULL,这样就指明它是无效的。
realloc() 用于修改先前通过 malloc() 获取的内存块大小。需要传递的参数为需要修改大小的内存块指针和希望获得的大小。如果指针 为NULL,并且新的大小大于0,那么 realloc() 函数就和 malloc() 很相像了。如果给定的指针有效,而新大小为 0,那么它就和 free() 相仿了。但是这两种情况不常使用,因为直接使用 malloc() 和 realloc() 会使代码更加清晰。realloc() 可能会将内存移动到不同的地址,并且如果是这样的话,它将会返回新的地址。即使指针移动到了不同的地址,内存块中的数据仍会保留,但是通过该调用获取到的新内存将包含随机数据。
这些内存函数的使用并不困难。我们通过下述简单的示例来看一下它们的实际应用。
#include <stdio.h>
// 一个新的包含文件!malloc.h包含了用于分配内存
// 和相关内存分配程序的定义。
#include <malloc.h>
int main(void)
{
// 在这个示例中,我们将要打印一个字符串,但是这里我们将会使用堆内存
// 而不是栈。这个指针将会保存字符串的内存块地址。暂时我们还未为其分
// 配内存,因此将其赋值为NULL。
char *string = NULL;
// 现在我们将向系统申请一块内存,然后将其值赋予我们的字符串指针。
// (char *)即用于将我们的内存块类型从空指针(void *)转换为字符指针。
// 我们申请了50字节,它足以保存49个字符以及一个NULL结束符。
string = (char *)malloc(50);
// sprintf 是用于将数字转换为字符串的较为简单的方式。
sprintf(string,"The number pi is approx. %f",3.1415927);
printf("%s\n",string);
// free 函数将会把我们先前申请的50个字节内存块返还系统。
free(string);
// 我们的指针保存的地址将不再有效。任何尝试使用它的行为都将导致无法
// 预期的结果,但是通常都会产生段错误。在这个示例中,我们已经完工了,
// 但是如果我们希望继续使用该指针,我们就需要为其赋予一个有效的地址。
return 0;
}
超越十进制:二进制数学¶
现在让我们花点时间来学习一些数学,在之后的课程中,我们可能会用到它们。就像计算机和人类从不同的数字开始计数一样,他们所使用的计数系统也彼此相异。
人类使用 十进制系统,也就是说 10进制计数法 。.它之所以称之为 10 进制计数是因为每一位都有 10 个可选值,从 0 至 9。当您还很小的时候,您可能听自己的数学老师讲过“个位”,“十位”,以及“百位”。从右至左,从 0 算起,每个数位的值和对应的 10 的指数相等。
- <table width=”400” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”100”>“千位”</td> <td width=”100”>“百位”</td> <td width=”100”>“十位”</td> <td width=”100”>“个位”</td>
</tr> <tr>
<td>1000=10<sup>3</sup></td> <td>100=10<sup>2</sup></td> <td>1000=10<sup>1</sup></td> <td>1000=10<sup>0</sup></td></tr>
</table>
对于数字 5234,总计为 (1000 x 5)+(100 x 2)+(10 x 3)+ 4。
在赤裸裸的稀有金属层和计算机进行交互中涉及的数学和我们日常使用的相差甚远。计算机使用二进制数学,即**二进制计数法**。其所使用的值仅有两个,0 和 1。二进制数学中的数位如下表所示:
- <table width=”400” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”100”>“8位”</td> <td width=”100”>“4位”</td> <td width=”100”>“2位”</td> <td width=”100”>“1位”</td>
</tr> <tr>
<td>8=2<sup>3</sup></td> <td>4=2<sup>2</sup></td> <td>2=2<sup>1</sup></td> <td>2=2<sup>0</sup></td></tr>
</table>
每个二进制数字中的单个数位都成为 位(bit) 。它只是一个位的信息,并且本身并没有很大用处。位都是八个组织在一起,成为 字节(byte) 。编程时,我们执行的任何二进制计算,它都会至少使用一个字节甚至更多,但是现在,我们尽量让这个让人迷惑的话题变得简单。
请谨记,二进制只是一种不同的数字书写方式而已,就跟数字 68 和罗马数字 LXVIII 的不同一样。
为了将数字从二进制转换为十进制,您需要在每处地方添加下列数值。字节中的每个位都是2的指数:
- <table width=”630” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td>位编号</td> <td>7</td> <td>6</td> <td>5</td> <td>4</td> <td>3</td> <td>2</td> <td>1</td> <td>0</td>
</tr> <tr>
<td>十进制值</td> <td>128</td> <td>64</td> <td>32</td> <td>16</td> <td>8</td> <td>4</td> <td>2</td> <td>1</td></tr>
</table>
对于二进制数为1的列,您需要加上相对应的2的指数。例如,二进制10000000的十进制数位128。仅能够具有1的列是第一列,其十进制值为128。二进制数10000111的十进制为135。如何得来呢?128 + 4 + 2 + 1。
在二进制中,您也可以执行加法,减法以及其他的常规数学运算,但是它基本上是没有必要的,因此在这里我们将不会涉及相关内容。但是,一些其他的操作则非常常用,并且是二进制数学所特有的,C 和 C++ 也提供了相应的操作符。它们和我们所讲过的布尔逻辑运算符非常相似。
- <table width=”600” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <th width=”150”>操作符</th> <th width=”150”>名称</th> <th width=”150”>描述</th>
</tr> <tr>
<td>&</td> <td>位与</td> <td>关闭位</td></tr> <tr>
<td>|</td> <td>位或</td> <td>开启位</td></tr> <tr>
<td>^</td> <td>位异或</td> <td>翻转位</td></tr> <tr>
<td>~</td> <td>位求反</td> <td>翻转数字中的所有位</td></tr>
</table>
该表格没有足够的地方来给出所有的信息。但是我们需要更多的讲解。它们都是具有特殊用途的特别数学操作符。布尔与,或,和非操作符用于程序的逻辑运算,例如if条件语句的连接和类似情况。而上表中的位操作符则用于操作数字中的位。
位与操作¶
位与操作符用于关闭数字中的位,即,将其中为 1 的位设为 0。通过比较每个数中对应的位,然后使用布尔逻辑来决定将该位设为 1 或是 0。下面两个示例将有助于我们的理解。
- <div style=”float:left;margin-right:5px;”><table width=”300” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”60”> </td> <td width=”116”>十进制</td> <td width=”116”>二进制</td>
</tr> <tr>
<td> </td> <td>255</td> <td>11111111</td></tr> <tr>
<td>位与</td> <td>240</td> <td>11110000</td></tr> <tr>
<td> </td> <td>240</td> <td>11110000</td></tr>
</table></div>
- <div style=”float:left;”><table width=”300” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”60”> </td> <td width=”116”>十进制</td> <td width=”116”>二进制</td>
</tr> <tr>
<td> </td> <td>240</td> <td>11110000</td></tr> <tr>
<td>位与</td> <td>170</td> <td>10100000</td></tr> <tr>
<td> </td> <td>150</td> <td>10100000</td></tr>
</table></div>
仅当前后两个数的对应位都为1时,该位才会保持为1。这就是为何位与操作符可用于关闭位的原因。
为了关闭指定位,我们必须知道使用哪些数字才能够仅关闭需要的位,而不改变其他的位。这非常简单,您只需要使用那个除了我们希望关闭的位为0而其他位为1的数字即可。
假如我们有一个变量,其值为199,而我们仅希望关闭其第2位。我们从255开始,因为它所有的位都为1,然后减去22,也就是2的二次方,我们希望关闭的位为1时的值。那么我们希望使用的位与操作数就是251,即 255 - 4。199位与251的结果就是195。
- <table width=”400” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”60”> </td> <td width=”116”>十进制</td> <td width=”116”>二进制</td>
</tr> <tr>
<td> </td> <td>199</td> <td>11000111</td></tr> <tr>
<td>位与</td> <td>251</td> <td>11111011</td></tr> <tr>
<td> </td> <td>195</td> <td>11000011</td></tr>
</table>
位或操作¶
位或用于和位与相反的用途,其用于开启数字中的位,即将某个为0的位设置为1。
- <table width=”400” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”60”> </td> <td width=”116”>十进制</td> <td width=”116”>二进制</td>
</tr> <tr>
<td> </td> <td>192</td> <td>01000000</td></tr> <tr>
<td>位或</td> <td>15</td> <td>01001000</td></tr> <tr>
<td> </td> <td>72</td> <td>01001000</td></tr>
</table>
如果两个数中对应位有一个为1则其结果中该位为1。在开启指定位时,其运算相对简单。它仅仅是对我们希望修改的数做了一个或运算,其对象为该数相应位上的二进制指数。如果我们有一个变量包含36,我们希望开启其第1位,那么我们将该变量与21,即2,执行或运算。
- <table width=”400” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”60”> </td> <td width=”116”>十进制</td> <td width=”116”>二进制</td>
</tr> <tr>
<td> </td> <td>36</td> <td>00000010</td></tr> <tr>
<td>位或</td> <td>2</td> <td>00100110</td></tr> <tr>
<td> </td> <td>38</td> <td>00100110</td></tr>
</table>
位异或操作¶
位异或可能是所有位操作符中最神秘的操作符。异或,其意为异常或操作。其用于翻转位,因为如果对应位不同其结果为1,若相同则为0。
- <table width=”400” border=”1” cellpadding=”0” cellspacing=”0”>
- <tr>
- <td width=”60”> </td> <td width=”116”>十进制</td> <td width=”116”>二进制</td>
</tr> <tr>
<td> </td> <td>36</td> <td>00100100</td></tr> <tr>
<td>位异或</td> <td>255</td> <td>11111111</td></tr> <tr>
<td> </td> <td>219</td> <td>11011011</td></tr>
</table>
位求反¶
位求反也用于翻转位,和位异或相类似,但是可控性较小。它会翻转数值中的所有位,例如上述示例,但是它不需要其他参数。下面是一个使用示例:
int a = 5;
printf("The value of ~%d is %d\n", a, ~a);
移位操作符¶
除了将位设为开启关闭,进行翻转之外,C和C++还提供了移位操作符,我们可以快速的执行一些乘法和除法。
//将A的数值向左移动B位。也就是将A乘以2B。
A << B;
//将A的数值向右移动B位。也就是将A除以2B。
A >> B;
我们可以通过移位操作符对位的实际操作,很容易的明白为何它们被称为移位操作符。
- <table border=”1” cellspacing=”0” cellpadding=”0”>
- <tr>
- <td>代码</td> <td>数学等价公式</td> <td>结果</td> <td>二进制数(结果)</td> <td>二进制数(结果)</td>
</tr> <tr>
<td>5 << 2</td> <td>5 * 2<sup>2</sup></td> <td>20</td> <td>00010100</td> <td>00010100</td></tr> <tr>
<td>32 << 1</td> <td>32 * 2<sup>1</sup></td> <td>64</td> <td>00100000</td> <td>00100000</td></tr> <tr>
<td>64 >> 1</td> <td>64 / 2<sup>1</sup></td> <td>32</td> <td>01000000</td> <td>00000111</td></tr> <tr>
<td>7 >> 2</td> <td>7 / 2<sup>1</sup></td> <td>1</td> <td>00000111</td> <td>00000001</td></tr>
</table>
在进行位操作时,了解移位操作符显得尤为有用,因为它们允许我们非常快速的执行指定数学运算。在这种情形下,乘或除以 2 的指数非常的普遍,并且其等价调用 pow() 或者常规除法运算速度都非常迟缓。
错误查找¶
找错 #1¶
代码:
#include <stdio.h>
#include <string.h>
#include <malloc.h>
char *ReverseString(char *string)
{
// 该函数用于对字符串进行重新排序,例如 abcdef -> fedcba
if (!string)
return NULL;
int length = strlen(string);
int count = length / 2;
for (int i = 0; i < count; i++)
{
char temp = string[length - 1 - i];
string[length - 1 - i] = string[i];
string[i] = temp;
}
return string;
}
int main(void)
{
char firstString[100],secondString[100];
char *combinedString = NULL;
printf("Enter your first word: ");
gets(firstString);
printf("Enter your second word: ");
gets(secondString);
sprintf(combinedString,"%s %s",ReverseString(secondString), ReverseString(firstString));
printf("If you saw these two words in a mirror, it would read '%s'\n",combinedString);
}
错误:
在运行时,程序打印“segmentation fault”,没有其他任何结果。
找错 #2¶
代码:
#include <stdio.h>
#include <math.h>
void MakeBinaryString(char *outString, char valueToConvert)
{
// 将一个1字节的值转换为字符串,即以二进制的方式显示其值。
// 我们检查其每位开启与否,如果位为开启状态,则将字符的值置
// 为‘1’,反之则置为‘0’,以这样的方式来进行此操作。
for (int i = 0; i < 8; i++)
{
// 该位是否为 1?
// 移位操作用于快速的产生2的指数,这样我们可以一次检查一位,
// 从第7位开始,直到第0位。
if (valueToConvert & (1 << (7 - i)))
outString[i] = '1';
else
outString[i] = '0';
}
outString[8] = '\0';
}
int main(void)
{
char value = 5;
char binaryString[6];
MakeBinaryString(binaryString,value);
printf("The binary equivalent of %d is %s\n",value, binaryString);
return 0;
}
错误:
在运行时,程序打印如下内容:
The binary equivalent of 48 is 00000101
Segmentation fault