1. 有以下程序片段:
int a[] = { 1, 2 };
int *p = a;
问:
当sizeof(int) = 4时,
sizeof(a)=?
sizeof(p)=?
分别为什么?
答案:
首先要知道数组名和指针之间的区别,这并不成问题。
但是,请注意这道题有一个陷阱,一些朋友可能认为p是指向int类型的指针,所以sizeof(p)才等
于4,这种想法是不正确的。即便p是一个指向char类型的指针,sizeof(p)也等于4,可以编程试
试,可见指针的大小不取决于指向的类型。
我们个人常用的计算机体系结构是32位的,这样的体系结构CPU有32条地址线和32条数据线,寄存
器也是32位,而指针存储的是一个地址值,它的大小取决于CPU的寻址能力以及操作系统的支持,
在32位体系结构上,它占4个字节,也就是32位,但在64位体系结构上它占8个字节,也就是64位
。
此题考察点:
区分数组名和指针。
计算机体系结构相关知识。
此题要传达的信息:
即使是一个很小的程序,也潜藏着很多基础知识。
答案:不是死循环。
有朋友不是死循环的原因是当i大到一定值再加1就变成负数了,这只是知道表面现象,不知深层
原因。
可谓不识庐山真面目,只缘身在此山中。
以下以x86、以及大多数计算机使用的2's Complement表示法这样的体系结构举例:
有符号整数的最高位用来表示符号位,正数的表示区间为0x00000000-0x7fffffff,负数的表示区
间为0x80000000-0xffffffff。正数最大值为0x7fffffff,也就是2147483647,二进制为
1111111111111111111111111111111,加1后,变为:10000000000000000000000000000000,即
0x80000000,所以成了负数。
具体为什么要这样规定,以及负数在内存当中的补码表示法等,请参阅相关资料,这里不详谈了。
此题考察点:
计算机中数的表示。
此题要传达的信息:
基础才是一切的根本,有了基础你才能既知道表面现象,又知道内在原因。
3. 有以下程序片段:
int a[] = { 1, 2 };
int i = 5;
printf("%d\n", a[-1]);
输出结果可能是什么?
这道题要注意可能二字,不然就无法自圆其说。
既然问你可能是什么,那要你回答的重点在于具体值,而不是那些未知值,当然你可以补充说,
可能是不确定值。
以下是可以得到期望结果的编译器编译后的代码:
10: int a[] = { 1, 2 };
0040DA68 mov dword ptr [ebp-8],1
0040DA6F mov dword ptr [ebp-4],2
11: int i = 5;
0040DA76 mov dword ptr [ebp-0Ch],5
12: printf("%d\n", a[-1]);
0040DA7D lea eax,[ebp-8]
0040DA80 mov ecx,dword ptr [eax-4]
0040DA83 push ecx
0040DA84 push offset string "ABCDEF" (0042201c)
0040DA89 call printf (00401070)
0040DA8E add esp,8
至于有些朋友大喊大叫抓住这是未定义行为不放,只能说是没学到才这样说,建议学完汇编后再
回来自己画个图分析一下。
谁都知道这是未定义行为,这里谈的不是标准规定问题。但如果你具备本题考察的知识,不编译
运行也知道结果可能就是5,也就是变量i的值。
此题考察点:
函数栈帧中变量的存储布局
此题要传达的信息:
对一些未定义用法的研究很可能会给你带入一片新的天地。做事要老实,写工程肯定不要用搞未
定义用法,但思想要开放,不要太老实,对一些用法的研究可以帮你更深刻地了解程序的背后机
理。
4. 有以下程序片段:
int i = 5;
int a[] = { 1, 2 };
printf("%d\n", a[3]);
输出结果可能是什么?
int array[ARRAY_SIZE];
int *pi;
for (pi = &array[0]; pi < &array[ARRAY_SIZE];)
*++pi = 0;
(1)修改for (pi = &array[0]; pi < &array[ARRAY_SIZE];)这一行,改为等价程序。
(2)谈谈这个程序可能运行结果。
(3)小明说这个程序如果后来pi被设置为空指针的话,会导致死循环,还会破坏N多数据。你同意
小明的观点吗?如果之后pi真的变成空指针的话,那一般在Windows/Linux下这个程序会是死循环
吗?不是的话会发生什么?
答案:
(1)for (pi = array; pi < array + ARRAY_SIZE;)
(2)可能被kill了。
(3)现代操作系统(所以我说一般在windows/linux下)普遍采用虚拟内存管理机制,提供支持的
是处理器当中的Memory Management Unit(即内存管理单元)。而且通常操作系统把虚拟地址空
间划分为用户空间和内核空间,比如在linux下当pi指向地址为1的内存区域并要访问时,访问的
不是物理地址,而是Virtual Address,MMU会产生一个异常,CPU中用户模式切换到特权模式,跳
转到内核中执行异常服务程序,然后内核把异常解释为段错误,然后把引发异常的进程终止掉。
此题考察点:
数组名和指针的关系,数组名做右值时转换成指向首元素的指针,做左值表示整个数组的存储空
间。
操作系统内存管理、MMU、处理器模式、异常处理
此题要传达的信息:
同样,一个小小的程序背后有那么多的底层知识。你意识到了吗?
6. 有一个函数如下:
char *strcpy(char *dest, const char *src)
{
char *temp = dest;
if (dest == src)
return dest;
if (dest == NULL || src == NULL)
return NULL;
while (*dest++ = *src++)
;
return temp;
}
小明说如果把if (dest == NULL || src == NULL) return NULL;改为if (dest != NULL || src
!= NULL)就可以巧妙地利用顺序结构的特点简化掉一个return NULL;,你同意他的做法吗?
答案:
看似很聪明,其实可能白白浪费了CPU的指令周期。改之前如果遇到dest == NULL || src ==
NULL直接ret,改了之后很可能还要跳到ret处再执行ret,这样就浪费了CPU的指令周期,一些编
译器对此并不优化。
另外从工程角度上来说,直接返回显然更干净利落,更加一目了然,不至于还要看到程序末尾才
知道怎么回事。所以能赶紧返回就赶紧返回。
此题考察点:
函数调用原理(call、ret),if...else的底层原理。
此题要传达的信息:
看似逻辑相等的程序编译后却不一定相同,对程序运行机制的理解能让你写出更好的程序。
7. 有这样一个程序:
#include <stdio.h>
void foo (void)
{
int i;
printf("%d\n", i);
i = 999;
}
int main (void)
{
foo();
foo();
return 0;
}
运行结果可能为什么?为什么?在foo();之间加个printf("hello\n");呢?
答案:
第一次调用结果不确定,第二次可能就是上次栈帧中留下的999。
有些C语言的教程上说,局部变量在函数返回时释放,这个释放是如何释放呢?
实际上大多时候编译器并不会给你把栈帧清空,那多费事啊,人家直接把相关寄存器(比如esp)
一改就完事,所以才出现遗留情况。具体请见反汇编代码才能知道具体怎么回事。
此题考察点:
函数调用原理。
此题要传达的信息:
不要书上说释放就当成真的全空了,要去具体看看这个“释放”到底是怎么个“释放”。
8. 某人为了给学生讲讲什么是指针写了如下程序:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[5] = { 1, 2, 3, 4, 5 };
unsigned int p;
}
程序还没写完,请把p当作指针用它来遍历数组a,输出数组a的所有元素。
写完后思考一下,不用unsigned int用int行不行?为什么?
答案:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[5] = { 1, 2, 3, 4, 5 };
unsigned int p;
for ((int *)p = a; (int *)p < a+5; p += sizeof(*a))
printf("%d ", *(int *)p);
printf("\n");
return 0;
}
不用unsigned int用int行不行?为什么?
有何不可?sizeof(unsigned int)和sizeof(int)相等!
看例子:
int p = 0x80000001;
00401038 mov dword ptr [ebp-4],80000001h
11: printf("%d\n", *(int *)p);
0040103F mov eax,dword ptr [ebp-4]
00401042 mov ecx,dword ptr [eax]
00401044 push ecx
00401045 push offset string "ABCDEF" (0042201c)
0040104A call printf (00401070)
0040104F add esp,8
当然我这里运行到00401042 mov ecx,dword ptr [eax],访问不到那去,但eax的值的确就是
0x80000001。
这是如何解释值的问题,不是这个值本身的问题。一个值0x80000001,你解释成有符号数是-
2147483647,无符号数是2147483649,地址就是0x80000001,值本身就是那一个。一个0xCC,解
释成指令是int 3h,两个0xCC连到一起可以被VC解释成烫(这就是烫烫烫烫烫的来源)。
此题考察点:
内存地址、指针的本质
此题要传达的信息:
不用声明指针变量,照样可以用出指针,可谓手中无剑,心中有剑。
高级语言的一个优点就是它能让你快速地进行编程,这个优点来自于它的抽象性。然而,抽象性
也隐藏了程序背后的原理,通过这个程序的编写,我想你会知道,所谓指针就是个语言设计者为
了方便大家编程创造出的一个概念,只是个花俏东西。其实,语言都是表面的花俏东西,到了底
层层面上它也就失去了它原有的意义。
你要根据语言的规定进行编程,但你不能被语言设计者束缚了头脑,浮在表面。那就不识庐山真
面目,只缘身在此山中了。
另外,计算机的学习要融会贯通,大学里分科是为了更好的教学,但这个融会贯通,把各科都融
合到一起的过程是要由你来做的,而这些题也可以帮助你融会贯通,体会一下,这些题是不是把
计算机体系结构、操作系统、汇编语言、编译原理、C语言这些课融汇到了一起呢?
不要人云亦云,跟着人家后面跑,要知道根本所在。
每一行高级语言代码对应什么低级代码你应该做到心中有数,这样才能做到真正理解这门语言的
语法和机制,真正地把握程序,真正玩转程序,更能在软件调试中游刃有余。
学好基础课,不去随大流追赶所谓时髦但没多大含金量的技术,能让你在技术面前感到踏实与自
信。
int a[] = { 1, 2 };
int *p = a;
问:
当sizeof(int) = 4时,
sizeof(a)=?
sizeof(p)=?
分别为什么?
答案:
首先要知道数组名和指针之间的区别,这并不成问题。
但是,请注意这道题有一个陷阱,一些朋友可能认为p是指向int类型的指针,所以sizeof(p)才等
于4,这种想法是不正确的。即便p是一个指向char类型的指针,sizeof(p)也等于4,可以编程试
试,可见指针的大小不取决于指向的类型。
我们个人常用的计算机体系结构是32位的,这样的体系结构CPU有32条地址线和32条数据线,寄存
器也是32位,而指针存储的是一个地址值,它的大小取决于CPU的寻址能力以及操作系统的支持,
在32位体系结构上,它占4个字节,也就是32位,但在64位体系结构上它占8个字节,也就是64位
。
此题考察点:
区分数组名和指针。
计算机体系结构相关知识。
此题要传达的信息:
即使是一个很小的程序,也潜藏着很多基础知识。
2. 以下循环是死循环吗?
for (int i = 1; i > 0; i++)
printf("%d\n", i);
答案:不是死循环。
有朋友不是死循环的原因是当i大到一定值再加1就变成负数了,这只是知道表面现象,不知深层
原因。
可谓不识庐山真面目,只缘身在此山中。
以下以x86、以及大多数计算机使用的2's Complement表示法这样的体系结构举例:
有符号整数的最高位用来表示符号位,正数的表示区间为0x00000000-0x7fffffff,负数的表示区
间为0x80000000-0xffffffff。正数最大值为0x7fffffff,也就是2147483647,二进制为
1111111111111111111111111111111,加1后,变为:10000000000000000000000000000000,即
0x80000000,所以成了负数。
具体为什么要这样规定,以及负数在内存当中的补码表示法等,请参阅相关资料,这里不详谈了。
此题考察点:
计算机中数的表示。
此题要传达的信息:
基础才是一切的根本,有了基础你才能既知道表面现象,又知道内在原因。
3. 有以下程序片段:
int a[] = { 1, 2 };
int i = 5;
printf("%d\n", a[-1]);
输出结果可能是什么?
这道题要注意可能二字,不然就无法自圆其说。
既然问你可能是什么,那要你回答的重点在于具体值,而不是那些未知值,当然你可以补充说,
可能是不确定值。
以下是可以得到期望结果的编译器编译后的代码:
10: int a[] = { 1, 2 };
0040DA68 mov dword ptr [ebp-8],1
0040DA6F mov dword ptr [ebp-4],2
11: int i = 5;
0040DA76 mov dword ptr [ebp-0Ch],5
12: printf("%d\n", a[-1]);
0040DA7D lea eax,[ebp-8]
0040DA80 mov ecx,dword ptr [eax-4]
0040DA83 push ecx
0040DA84 push offset string "ABCDEF" (0042201c)
0040DA89 call printf (00401070)
0040DA8E add esp,8
至于有些朋友大喊大叫抓住这是未定义行为不放,只能说是没学到才这样说,建议学完汇编后再
回来自己画个图分析一下。
谁都知道这是未定义行为,这里谈的不是标准规定问题。但如果你具备本题考察的知识,不编译
运行也知道结果可能就是5,也就是变量i的值。
此题考察点:
函数栈帧中变量的存储布局
此题要传达的信息:
对一些未定义用法的研究很可能会给你带入一片新的天地。做事要老实,写工程肯定不要用搞未
定义用法,但思想要开放,不要太老实,对一些用法的研究可以帮你更深刻地了解程序的背后机
理。
4. 有以下程序片段:
int i = 5;
int a[] = { 1, 2 };
printf("%d\n", a[3]);
输出结果可能是什么?
同上,不赘述。
int array[ARRAY_SIZE];
int *pi;
for (pi = &array[0]; pi < &array[ARRAY_SIZE];)
*++pi = 0;
(1)修改for (pi = &array[0]; pi < &array[ARRAY_SIZE];)这一行,改为等价程序。
(2)谈谈这个程序可能运行结果。
(3)小明说这个程序如果后来pi被设置为空指针的话,会导致死循环,还会破坏N多数据。你同意
小明的观点吗?如果之后pi真的变成空指针的话,那一般在Windows/Linux下这个程序会是死循环
吗?不是的话会发生什么?
答案:
(1)for (pi = array; pi < array + ARRAY_SIZE;)
(2)可能被kill了。
(3)现代操作系统(所以我说一般在windows/linux下)普遍采用虚拟内存管理机制,提供支持的
是处理器当中的Memory Management Unit(即内存管理单元)。而且通常操作系统把虚拟地址空
间划分为用户空间和内核空间,比如在linux下当pi指向地址为1的内存区域并要访问时,访问的
不是物理地址,而是Virtual Address,MMU会产生一个异常,CPU中用户模式切换到特权模式,跳
转到内核中执行异常服务程序,然后内核把异常解释为段错误,然后把引发异常的进程终止掉。
此题考察点:
数组名和指针的关系,数组名做右值时转换成指向首元素的指针,做左值表示整个数组的存储空
间。
操作系统内存管理、MMU、处理器模式、异常处理
此题要传达的信息:
同样,一个小小的程序背后有那么多的底层知识。你意识到了吗?
6. 有一个函数如下:
char *strcpy(char *dest, const char *src)
{
char *temp = dest;
if (dest == src)
return dest;
if (dest == NULL || src == NULL)
return NULL;
while (*dest++ = *src++)
;
return temp;
}
小明说如果把if (dest == NULL || src == NULL) return NULL;改为if (dest != NULL || src
!= NULL)就可以巧妙地利用顺序结构的特点简化掉一个return NULL;,你同意他的做法吗?
答案:
看似很聪明,其实可能白白浪费了CPU的指令周期。改之前如果遇到dest == NULL || src ==
NULL直接ret,改了之后很可能还要跳到ret处再执行ret,这样就浪费了CPU的指令周期,一些编
译器对此并不优化。
另外从工程角度上来说,直接返回显然更干净利落,更加一目了然,不至于还要看到程序末尾才
知道怎么回事。所以能赶紧返回就赶紧返回。
此题考察点:
函数调用原理(call、ret),if...else的底层原理。
此题要传达的信息:
看似逻辑相等的程序编译后却不一定相同,对程序运行机制的理解能让你写出更好的程序。
7. 有这样一个程序:
#include <stdio.h>
void foo (void)
{
int i;
printf("%d\n", i);
i = 999;
}
int main (void)
{
foo();
foo();
return 0;
}
运行结果可能为什么?为什么?在foo();之间加个printf("hello\n");呢?
答案:
第一次调用结果不确定,第二次可能就是上次栈帧中留下的999。
有些C语言的教程上说,局部变量在函数返回时释放,这个释放是如何释放呢?
实际上大多时候编译器并不会给你把栈帧清空,那多费事啊,人家直接把相关寄存器(比如esp)
一改就完事,所以才出现遗留情况。具体请见反汇编代码才能知道具体怎么回事。
此题考察点:
函数调用原理。
此题要传达的信息:
不要书上说释放就当成真的全空了,要去具体看看这个“释放”到底是怎么个“释放”。
8. 某人为了给学生讲讲什么是指针写了如下程序:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[5] = { 1, 2, 3, 4, 5 };
unsigned int p;
}
程序还没写完,请把p当作指针用它来遍历数组a,输出数组a的所有元素。
写完后思考一下,不用unsigned int用int行不行?为什么?
答案:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[5] = { 1, 2, 3, 4, 5 };
unsigned int p;
for ((int *)p = a; (int *)p < a+5; p += sizeof(*a))
printf("%d ", *(int *)p);
printf("\n");
return 0;
}
不用unsigned int用int行不行?为什么?
有何不可?sizeof(unsigned int)和sizeof(int)相等!
看例子:
int p = 0x80000001;
00401038 mov dword ptr [ebp-4],80000001h
11: printf("%d\n", *(int *)p);
0040103F mov eax,dword ptr [ebp-4]
00401042 mov ecx,dword ptr [eax]
00401044 push ecx
00401045 push offset string "ABCDEF" (0042201c)
0040104A call printf (00401070)
0040104F add esp,8
当然我这里运行到00401042 mov ecx,dword ptr [eax],访问不到那去,但eax的值的确就是
0x80000001。
这是如何解释值的问题,不是这个值本身的问题。一个值0x80000001,你解释成有符号数是-
2147483647,无符号数是2147483649,地址就是0x80000001,值本身就是那一个。一个0xCC,解
释成指令是int 3h,两个0xCC连到一起可以被VC解释成烫(这就是烫烫烫烫烫的来源)。
此题考察点:
内存地址、指针的本质
此题要传达的信息:
不用声明指针变量,照样可以用出指针,可谓手中无剑,心中有剑。
高级语言的一个优点就是它能让你快速地进行编程,这个优点来自于它的抽象性。然而,抽象性
也隐藏了程序背后的原理,通过这个程序的编写,我想你会知道,所谓指针就是个语言设计者为
了方便大家编程创造出的一个概念,只是个花俏东西。其实,语言都是表面的花俏东西,到了底
层层面上它也就失去了它原有的意义。
你要根据语言的规定进行编程,但你不能被语言设计者束缚了头脑,浮在表面。那就不识庐山真
面目,只缘身在此山中了。
另外,计算机的学习要融会贯通,大学里分科是为了更好的教学,但这个融会贯通,把各科都融
合到一起的过程是要由你来做的,而这些题也可以帮助你融会贯通,体会一下,这些题是不是把
计算机体系结构、操作系统、汇编语言、编译原理、C语言这些课融汇到了一起呢?
不要人云亦云,跟着人家后面跑,要知道根本所在。
每一行高级语言代码对应什么低级代码你应该做到心中有数,这样才能做到真正理解这门语言的
语法和机制,真正地把握程序,真正玩转程序,更能在软件调试中游刃有余。
学好基础课,不去随大流追赶所谓时髦但没多大含金量的技术,能让你在技术面前感到踏实与自
信。