本章问题

1.如果一个值的类型无法简单的通过观察它的位模式来判断,那么机器是如何知道应该怎样对这个值进行操纵的?

answer:The machine doesn't make this determination.The compiler creates the appropriate instructions(指令) based on the declared type of the value,and the machine blindly executes the instructions.

(机器无法检测这个值,编译器在声明一个值的类型的基础上创建一个合适的指令,然后机器只能够执行指令)

 

2.C为什么没有一种方法来声明字面值指针常量呢?

answer:They are rarely used because you can't tell ahead of time where the compiler will put variables.

(它们几乎很少用,因为你不可能在编译器创建变量之前知道它在哪个位置)

 

3.假定一个整数的值为244,为什么机器不会把这个值解释为一个内存地址呢?

answer:The value is an integer,so the compiler will not generate instructions to dereference it.

(这个值是一个整型,所以编译器不会产生这样的指令)

 

4.在有些机器上,编译器在内存位置零存储0这个值,对NULL指针进行解引用操作将访问这个位置,这种方法产生什么后果?

answer:It is dangerous,First,the result of dereferencing a NULL pointer is implementation specific,so programs should not do it,Allowing a program to continue after such an access is unfortunate,because of the strong possibility that the program is not operating correctly.

(这是非常危险的,首先,对NULL指针解引用操作的结果因编译器而异,所以程序不该这么做,允许一个程序在这种访问之后继续运行是非常不幸的,因为有很大的可能程序不能正确运行)

 

5.表达式(a)和(b)的求值过程有没有什么区别?如果有的话,区别在哪里?假定变量offset的值为3.

int i[10];
int *p = &i[0];
int offset;

p += offset; (a)
p += 3;      (b)

answer:Even if offset has the same value as the literal(字面值) in the next expression.it is more time consuming(消耗) to evaluate(计算) the first expression because the multiplication to scale(规模) offset to the size of an integer must be done at run time.This is because the variable might contain any value,and the compiler has no way of knowing ahead of time what the value might actually be.On the other hand,The literal three can be scaled to an integer by multiplying it at compiler time,and the result of that multiplication is simply added to p at run time,In other words,the second expression can be implemented by simply adding 12 to p(on a machine with four-byte integers);no runtime multiplication is needed.

(即使offset跟后面一个表达式的字面值的值相等,但它比计算第一个表达式要消耗更多的时间,因为对整型大小的偏移量必须在运行时才知道,这是因为变量可能包括更多的值,而编译器不能在变量赋值之前知道它的值,另一方面,在编译的时候字面值3可以被缩放到一个整数中,结果在运行是添加到p,换句话说,第二个表达式可以简单的用12加上p,在四位机器上,不需要运行时的再相加)

 

6.下面的代码有没有问题,如果有的话,问题在哪里?

int array[ARRAY_SIZE];
int *pi;

for(pi = &array[0];pi < array[ARRAY_SIZE];)
    *++pi = 0;

 

answer:有两个错误,对增值之后的指针进行解引用时,数组的第一个元素没有被初始化为0,另外,指针在越过数组的右边界以后仍然进行解引用,它将把其他内存地址的内容清零。注意pi在数组之后立即声明,如果编译器恰好把它放在紧跟数组之后的位置,结果将是灾难性的,会导致一个微妙的无限循环。

 

7.下面的表显示了几个内存位置的内容。每个位置由它的地址和存储于该位置的变量名标识。所有数字以十进制形式表示。使用这些值,用4中方法分别计算下面各表达式的值,首先,假定所有的变量都是整型,找到表达式的右值,再找到表达式的左值,给出它所指定的内存位置的地址。接着,假定所有的变量都是指向整型的指针,重复上述步骤,注意:在执行地址运算时,假定整型和指针的长度都是4个字节。

变量 地址 内容 变量 地址 内容
a 1040 1028 o 1096 1024
c 1056 1076 q 1084 1072
d 1008 1016 r 1068 1048
e 1032 1088 s 1004 2000
f 1052 1044 t 1060 1012
g 1000 1064 u 1036 1092
h 1080 1020 v 1092 1036
i 1020 1080 w 1012 1060
j 1064 1000 x 1072 1080
k 1044 1052 y 1048 1068
m 1016 1008 z 2000 1000
n 1076 1056      

 

 

 

 

 

 

 

 

 

 

 

 

a. m 
b. v + 1 
c. j – 4 
d. a – d 
e. v – w
f. &c 
g. &e + 1 
h. &o – 4 
i. &( f + 2 ) 
j. *g
k. *k + 1
l. *( n + 1 ) 
m. *h – 4 
n. *( u – 4 )
o. *f – g 
p. *f - *g 
q. *s - *q 
r. *( r – t ) 
s. y > i 
t. y > *i
u. *y > *i 
v. **h 
w. c++ 
x. ++c 
y. *q++ 
z. (*q)++
aa. *++q
bb. ++*q 
cc. *++*q 
dd. ++*(*q)++ 

answer:

           |--------整型------------------|--------指针-------------------|
表达式--------右值--------左值地址--------右值--------左值地址
a. m             1008          1016              1008           1016
b. v + 1        1037           illegal            1040           illegal
c. j – 4         0996           illegal             0984          illegal
d. a – d        12               illegal              3              illegal
e. v – w        -24             illegal              -6             illegal
f. &c             1056           illegal             1056         illegal
g. &e + 1      1036           illegal             1036         illegal
h. &o – 4       1080          illegal              1080        illegal
i. &( f + 2 )     illegal        illegal              illegal        illegal
j. *g               illegal         illegal             1000          1064
k. *k + 1         illegal        illegal              1045          illegal
l. *( n + 1 )      illegal       illegal               1012         1060
m. *h – 4        illegal        illegal               1076          illegal
n. *( u – 4 )     illegal        illegal               1056         1076
o. *f – g           illegal        illegal               illegal         illegal
p. *f - *g           illegal        illegal               52             illegal
q. *s - *q          illegal        illegal               -80             illegal
r. *( r – t )         illegal        illegal              illegal          illegal
s. y > i              0                illegal                0             illegal
t. y > *i             illegal          illegal            illegal          illegal
u. *y > *i           illegal          illegal              1              illegal
v. **h                illegal           illegal           1080          1020
w. c++              1076          illegal              1076         illegal
x. ++c               1077           illegal             1080        illegal
y. *q++              illegal         illegal             1080        1072
z. (*q)++           illegal         illegal              1080        illegal
aa. *++q           illegal          illegal              1056        1076
bb. ++*q           illegal          illegal               1081       illegal
cc. *++*q          illegal          illegal               illegal     illegal
dd. ++*(*q)++   illegal          illegal               1021        illegal

 

本章练习

1.请编写一个函数,它在一个字符串中进行搜索,查找所有在一个给定字符集合中出现的字符。这个函数的原型应该如下:
char *find_char(char const *source,char const *chars);
它的基本想法是查找source字符串中匹配chars字符串中任何字符的第一个字符,函数然后返回一个指向source中第一个匹配所找到的位置的指针,如果任何一个参数NULL,或任何一个参数所指向的字符串为空,函数也返回一个NULL指针。

举个例子,假定source指向ABCDEF。如果chars指向XYZ、JURY或QQQQ,函数就返回一个NULL指针。如果chars指向XRCQEF,函数就返回一个指向source中C字符的指针。参数所指向的字符串是不会被修改的。

碰巧,C函数库中存在一个名叫strpbrk的函数,它的功能几乎和这个你要编写的函数一模一样。但这个程序的目的是让你自己练习操纵指针,所以:

a 你不应该使用任何用于操纵字符串的库函数(如strcpy,strcmp,index等)

b 函数中的任何地方都不应该使用下标


answer:

/*遇到一个头疼的问题,由于两个参数都是const变量,
导致声明char*类型的变量指向source和chars时引出警告
initialization discards ‘const’ qualifier from pointer target type,
类型不对啊,如果声明的char*类型为const变量,
那么就无法改变指针的值,并且返回的类型也不对
所以,顶着警告还是可以运行的*/

char *find_char(char const *source,char const *chars)
{
    char *ps = source;
    char *pc = NULL;
    if(source == NULL || chars == NULL)
        return NULL;
    while(*ps != '\0'){
        pc = chars;
        while(*pc != '\0'){
            if(*pc == *ps)
                return ps;
            pc++;
        }
        ps++;
    }
    return NULL;
}
/*看了下标准答案,里面也有这样一个警告,
避免不了的问题,就是把const指针赋值给普通指针,
这个函数原型的参数为const变量,
却要求返回一个普通指针变量类型*/

/*如果有更好的解决方法,请大神赐教*/
char *find_char(char const *source,char const *chars)
{
    char *p;
    if(source != NULL && chars != NULL){
        for(;*source != '\0';source++){
            for(p = chars; *p != '\0';p++)
                if(*p == *source)
                    return p;
        }
    }
    return NULL;
}
/*另外还写了不用声明任何变量的办法,
不过返回值却不是普通变量,还是const*/

char *find_char(char const *source,char const *chars)
{
    int size = 0;
    if(source != NULL && chars != NULL){
        for(;*chars++ != '\0';size++)
            ;
        for(;*source != '\0';source++){
            for(chars-=size; *chars != '\0';chars++)
                if(*chars == *source)
                    return chars;
        }
    }
    return NULL;
}

 

2.请编写一个函数,删除一个字符串的一部分。函数的原型如下:

int del_substr(char *str,char const *substr);
函数首先应该判断substr是否出现在sub中。如果它并未出现,函数就返回0;如果出现,函数应该把str中位于该子串后面的所有字符复制到该子串的位置;从而删除这个子串,然后函数返回1,如果substr在函数中出现多次,函数只删除第一次出现的子串。函数的第二个参数不能被修改。

举个例子,假定str指向ABCDEFG。如果substr指向FGH,CDF或XABC,函数返回0,str未作任何修改,但如果substr指向CDE,函数就把str修改为指向ABFG,方法是把FG和结尾的NUL字节复制到C的位置,然后函数返回1.不论出现什么情况,第二个参数都不能被修改。

a 你不应该使用任何用于操纵字符串的库函数

b 函数中的任何地方都不应该使用下标引用

一个值得注意的地方是,空字符串是每一个字符串的子串,在字符串中删除一个空字符串不会产生任何变化。

answer:

/*下面的代码无法在ubuntu下面运行,包括后面的标准答案上的答案也一样
我测试了一下,发现在终端里只要有*p++ = *q++这样指针之间赋值的语句
就会出现那句要命的段核心错误,注意是赋值语句而不是比较语句
不知道是编译器的问题还是这样写都不行呢
连strcpy都不行,一使用就段核心错误
不过只要把char *类型改为char[]数组就完全没有问题呢
*/

*int del_substr(char *str, char const *substr)
{
    char *p = str;
    char *ps;
    char *q;
    if(str != NULL){
        if(substr == NULL)
            return 1;
        while(*p != '\0'){
            q = substr;
            ps = p;
            while(*q != '\0'){
                if(*q++ != *ps++)
                    break;
            }
            if(*q == '\0'){
                while(*p++ = *ps++)
                    ;
                return 1;
            }
            p++;
        }
    }
    return 0;
}
/*这是标准答案写法*/
char *match(char *str,char *want)
{
    while(*want != '\0')
        if(*str++ != *want++)
            return NULL;
    return str;
}

int del_substr(char *str,char const *substr)
{
    char *next;
    while(*str != '\0'){
        next = match(str,substr);
        if(next != '\0')
            break;
        str++;
    }
    if(*str == '\0')
        return 0;
    while(*str++ = *next++)
        ;
    return 1;
}

 

 

这个题目告诉我们,指针常量是不可以更改的,字符指针跟字符数组的区别也是大大的。

另外,像char *str = "abcdefg";这条语句中str指向的是一个字符串常量,是不可以修改的昂,

想要使上面的函数正常运行,记得在声明str与substr时务必要使用字符数组。

 

3.编写函数reverse_string,它的原型如下:

void reverse_string(char *string);
函数把参数字符串中的字符反向排列,请使用指针而不是数组下标,不要使用任何C函数库中用于操纵字符串的函数。

提示:不需要声明一个局部变量数组来临时存储参数字符串。

answer:

/*写到这一题的时候,就明白了上面一道题,原因是,当我在main函数里
声明char * string = "ABCDEFG"时,就出现了错误
而我改为char string[] = "ABCDEFG"时,函数就没有一点问题
关于字符指针和字符数组的问题,我将在文章末尾表述我的认识*/
#include <stdio.h>

void reverse_string(char *string);

int main()
{
    char string[] = "ABCDEFG";
    printf("%s\n",string);
    reverse_string(string);
    printf("%s\n",string);
}

void reverse_string(char *string)
{
    char *p = string;
    char *q = string;
    while(*q != '\0')
        q++;
    q--;
    while(p < q){
        char temp = *p;
        *p = *q;
        *q = temp;
        p++;
        q--;
    }
}

 


4.质数就是只能被1和本身整除的整数,Eratosthenes筛选法是一种计算质数的有效办法。这个算法的第一步就是写下所有从2至某个上限之间的所有整数,在算法剩余部分,你遍历整个列表并剔除所有不是质数的整数。

后面的步骤是这样的,找到列表中的第一个不被剔除的数也就是2,然后将列表中所有2的倍数全部剔除,因为它们都可以被2整除,因此不是质数。接着,再回到列表的头部重新开始,此时列表中尚未被剔除的第一个数是3,所以在3之后把每逢第3个数的倍数剔除,完成这一步骤之后,再回到列表开头,3后面的下一个数是4,但它是2的倍数,已经被剔除,所以将其跳过,轮到5,将所有5的倍数全部剔除,这样以此类推,反复执行,最后列表中未被剔除的数均为质数。

编写一个程序,实现这个算法,使用数组表示你的列表。每个数组元素的值用于标记对应的数是否剔除。开始时所有元素的值都设置为FALSE。如果你的程序运行于16位机器上,小心考虑是不是需要把某个变量声明为long。一开始先使用包含1000个元素的数组。如果你使用字符数组,使用相同的空间,你将会比使用整数数组找到更多的质数,你可以使用下标来表示指向数组首元素和尾元素的指针,但你应该使用指针来访问数组元素。

注意除了2以外,所有的偶数都不是质数,稍微多想一下,你可以使程序的空间效率大为提高,方法是数组中的所有元素只对应奇数,这样,你在相同的数组空间内,你可以寻找到的质数的个数大约是原先的两倍。

answer:

 

/*关于筛选法选质数我已经在第四章编程练习的第二题中给出了代码
接下来我将写出以字符数组为存储单位
以及在数组中仅使用奇数的方法筛选质数*/
#include <stdio.h>

#define MAX 1001
void Eratosthenes(char []);

int main()
{
    int i;
    int count = 0;
    char prime[1001];
    for(i = 1; i < MAX; i++)
        prime[i] = 1;

    printf("the prime range is 2 to %d\n",MAX * 2 + 1);
    Eratosthenes(prime);
    printf("2\n");
    for(i = 1; i < MAX; i++){
        if(prime[i] == 1){
            printf("%d ",i * 2 + 1);
            count++;
        }
        if(count % 10 == 0)
            printf("\n");
    }
}

void Eratosthenes(char prime[])
{
    int i = 0;
    int i_p;
    int j;
    int j_p;
    for(i = 1; i < MAX; i++){
        if(prime[i] == 1){
            i_p = 2 * i + 1;
            for(j = 2 * i_p; j < MAX * 2 + 1; j += i_p)
                if(j % 2 == 1){
                    j_p = (j - 1)/2;
                    prime[j_p] = 0;
                }
        }
    }
}

 

5.修改前一题的Eratosthenes程序,使用位的数组而不是字符数组,这里要用到第5章编程练习中所开发的位数组函数。这个修改使程序的空间效率进一步提高,不过代价是时间效率降低,在你的系统中,使用这个方法,你所能找到的最大质数是多少?

answer: 

/*这个题目本身并不难
而它却把我困住了三个小时
而我现在还不知道到底为啥
有兴趣的朋友们可以去试试
原因是在我的Ubuntu下(也不知道是不是它的原因)
那个set_bit和clear_bit函数操作起来似乎有点问题
花了一大块时间调试之后
我决定不继续了,如果有知道原因的大神,请赐教*/

/*下面是答案*/
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include "bitarray.h"

#define MAX_VALUE 10000
#define MAX_BIT_NUMBER ((MAX_VALUE - 3) / 2)
#define SIZE (MAX_BIT_NUMBER / CHAR_BIT + 1)

unsigned character_offset(unsigned bit_number);
unsigned bit_offset(unsigned bit_number);
void set_bit(char bit_array[], unsigned bit_number);
void clear_bit(char bit_array[], unsigned bit_number);
void assign_bit(char bit_array[], unsigned bit_number, int value);
int test_bit(char bit_array[], unsigned bit_number);

int main()
{
    char sieve[SIZE];
    int number;
    int bit_number;
    char *sp;
    
    for(sp = sieve; sp < &sieve[SIZE];)
        *sp++ = 0xff;

    for(number = 3; number < MAX_VALUE; number += 2){
        bit_number = (number - 3) / 2;
        if(test_bit(sieve,bit_number) == 0)
            continue;
        while((bit_number += number) <= MAX_BIT_NUMBER){
//            printf("%d\t",test_bit(sieve,bit_number));
            clear_bit(sieve,bit_number);
//            printf("%d\n",test_bit(sieve,bit_number));
        }
    }

    printf("2\n");
    for(bit_number = 0,number = 3;number <= MAX_VALUE;
            bit_number += 1,number += 2)
        if(test_bit(sieve,bit_number))
            printf("%d\n",number);

    return 0;
}

/*为了方便,我给出一部分输出*/
...
9917
9919
9921
9923
9925
9927
9929
9931
9933
9935
9937
/*这是函数的最后一部分输出,可以清晰的看出9935 9925
等不是质数的数仍然还在函数中*/

/*在我注释了两行语句的地方,可以清楚的看到题目出了问题
在把打印函数注释掉之后,仅仅打印注释的两条printf语句
可以看到下列结果*/
...
0    0
0    0
1    1
0    0
1    1
0    0
1    1
0    0
0    0
0    0
0    0
0    0
...
/*按理来说使用了clearbit函数之后应该只有两种情况*/
0      0
1      0
然而现实却只有
0      0
1      1
这两种情况,意思是没有发生任何改变

 

6.大质数是不是和小质数一样多,换句话说,在50000和51000之间的质数是不是和1000000到1001000之间的质数一样多?使用前面的程序计算0到1000之间有多少个质数,1000到2000之间有多少个质数?以此每隔1000类推,到1000000有多少个质数,每隔1000个数中质数的质量的数量呈什么趋势?

answer:

#include <stdio.h>

#define MAX 1000001

int Eratosthenes(char [], int max);

int main()
{
    int i;
    int sum = 0;
    int count = 0;
    char prime[MAX];
    int max;
    for(i = 1; i < MAX; i++)
        prime[i] = 1;
    
    for(max = 1000; max < MAX; max += 1000){
        count = Eratosthenes(prime,max) - sum;
        printf("%d\t%d\n",max,count);
        sum += count;
    }
}

int Eratosthenes(char prime[], int max)
{
    int count = 0;
    int i = 0;
    int i_p;
    int j;
    int j_p;

    for(i = 1; i < max; i++)
        prime[i] = 1;
    
    for(i = 1; i < max; i++){
        if(prime[i] == 1){
            i_p = 2 * i + 1;
            for(j = 2 * i_p; j < max * 2 + 1; j += i_p)
                if(j % 2 == 1 && prime[(j-1)/2] == 1){
                    j_p = (j - 1)/2;
                    prime[j_p] = 0;
                    count++;
                }
        }
    }
    return count;
}

这个答案的混乱性看起来是没有什么特点的,所以答案算得是平均数昂

for(max = 100000; max < MAX; max += 100000){
        count = Eratosthenes(prime,max);
        printf("%f\n",(float)max/count * 1000);
    }

这是改过之后的,运行结果如下:

1219.274292
1203.803955
1195.686035
1190.302612
1186.234009
1183.287231
1180.689331
1178.423584
1176.581421
1174.995605

规律是不是出来了?

当我想按照答案上把数字改成九个零那么高的精确度之后,编译器很高兴的告诉我,段核心错误,想要那么精确的数位的话,还是把位数组琢磨出来吧,然后运行,睡个觉,没准醒来的时候你就能看到结果了,下面直接给出标准答案,可能也有些数学方法能够证明出来吧,有兴趣的自己去查查呗。

Range of Numbers Average # of primes per Thousand Number
1000000 - 2000000 70.435
2000000 - 3000000 67.883
3000000 - 4000000 66.33
4000000 - 5000000 65.367
5000000 - 6000000 64.336
6000000 - 7000000 63.799
7000000 - 8000000 63.129
8000000 - 9000000 62.712
9000000 - 10000000 62.09
10000000 - 20000000 60.603
20000000 - 30000000 58.725
30000000 - 40000000 57.579
40000000 - 50000000 56.748
50000000 - 60000000 56.098
60000000 - 70000000 55.595
70000000 - 80000000 55.132
80000000 - 90000000 54.757
90000000 - 100000000 54.45
100000000 - 200000000 53.175
200000000 - 300000000 51.734
300000000 - 400000000 50.84
400000000 - 500000000 50.195
500000000 - 600000000 49.688
600000000 - 700000000 49.292
700000000 - 800000000 48.932
800000000 - 900000000 48.63
900000000 - 1000000000 48.383