西邮Linux笔试题详解
无意中看到有一个同学在自己的博客中写到了linux的纳新笔试题的自己的见解,写的还是不错的,但是有些问题还没弄得太明白,这是我之前写在raysnote的一篇文章,现在把它公示出来,如果有感兴趣的同学的话可以参考一下。
1.解释下面程序的输出结果:
int main(int argc, char *argv[]){ int c; memcpy(&c, "linux", 4); printf("%d\n", c); return 0; }
memcpy(void *dest, const void *src, size_n n);
将src开始n个字节拷贝到c中。显然linux有5个字节,所以真正拷贝进入int里面那4个字节的只有linu四个而已。同时我们再进行尝试,会发现n必须是4,我们观察一下如果n = 3的话,字符串只是会发现int的第4个字节位没有被置
而如果要是n = 4的话,显然第4位应该被置00。读入的6C是‘l’的ASCII码值,为108。倘若只读入不足四个字节的单位,出现的就是几个负数,拿这些负数是什么呢,我们不妨转化成十六进制来看一看,会发现分别录入0个字节,一个字节,两个字节,三个字节分别对应的是:
其中有个数据读入了两遍。所产生的1970170220显然是linu四个字符对应的ASCII值组成的十进制数字。
这里还涉及到一个存储的问题,显然读入数据的时候是从第一个字节也就是0018FF44开始顺序由低位向高位存入,当读取的时候是从高位向低位读取,其中的原因是数据结构栈是先进后出的存储与读取规则。
2.
int *func(void){ static int a = 1; a ++; return &a; } int main(int argc, char *argv[]){ int *b; b = func(); printf("%d\n", *b); b = func(); printf("%d\n", *b); return 0; }
这道题主要考察的是staic的用法,其中在函数func中static对变量a进行修饰,延长其生命周期到程序结束,所以调用两次func读出的值不相同,而是2和3。
下面总结一下static的用法和功能:
先了解三种变量:
int a; //全局变量 int main(void){ int b; //栈变量 c = (int *)malloc(sizeof(int)); //堆变量 }
这里如果static修饰的是全局变量a的话,那么其作用就是限定此全局变量只能在这个文件中使用,而不能被其他文件调用,这也就是静态全局变量。而如果static修饰的是堆变量的话,那就会升级栈变量为全局变量,使其寿命延长,不会随着函数的结束而释放,当再次被调用时仍保留其上次的值,只有在程序执行完毕后才会被释放。当然,所谓的升级只是将静态局部变量放在全局存储区.data中,虽然它是局部的,但是在程序的整个生命周期都存在,它同样被能被其他的函数和源文件访问。
另外,static也可以修饰函数,类似上面修饰全局变量,就是其他文件无法调用。
3.
void func(char *a){ printf("%lu\n", sizeof(a)); printf("%lu\n", strlen(a)); } int main(int argc, char *argv[]){ char a[] = "hello world"; func(a); printf("%lu\n", sizeof(a)); printf("%lu\n", strlen(a)); return 0; }
我承认我这个题一开始想当然了,想都没想第一个就写了12,但是仔细一看不禁大喊自己犯傻,最简单的数组作为形参退化为指针都忘了。。。实在是不应该。。
那么这就是这个题的考点,数组作为形参时,退化为整型的指针,所以一开始func中sizeof打印出来的应该是&s[0],也就是一个长度为4字节的地址,然而strlen函数的原理我们都明白就是从一个数组首地址开始往后寻找'\0',找到后计算长度。所以显然第二个打印的是11。sizeof就是要求一种数据(类型)所占内存的字节数,而在书上有说数组仅在一下三种情况不退化为指针:
(1) sizeof(s)
(2) &s;
(3) 用来初始化s的字符串;
而这其中还有一个需要注意的是字符串数组在最后会隐藏一个'\0',这就是为什么sizeof和strlen差1
4.
void func(void){ unsigned int a = 6; int b = -20; (a + b > 6) ? puts(">6") : puts("<6"); }
显然这个题考查无符号和有符号运算之间的关系,编译器里面有标准的准换,规则就是:短的向长的转,有符号的向无符号的转。如果被转换的数据比转换后的数据要长的话,准换可能会丢失bit数据,通常编译器会给出警告。总而言之,运算过程中的转换就是从短的往长的类型转化。而这里我们也很容易可以推出,a + b计算所得的值时2的32次幂减14。
5.关于预处理:
C语言提供的编译预处理功能是与其他高级语言的重要区别之处。C语言提供的预处理功能主要有以下三种。
1.宏定义
2.文件包含
3.条件编译
分别用宏定义命令,文件包含命令,条件编译命令来实现,为了区分一般的C语言语句,这些命令以#开头
宏定义:宏定义我们非常简单理解,就是将一个简单的名字代替一个长的东西,比如字符串,这个名字就是宏名,在预编译的时候将宏名替换成字符串的过程称为宏展开。#define是宏定义命令。
下面是宏定义几点需要注意的地方:1.宏定义是简单的置换,并不作语法检查。只会在编译已被宏展开后的源程序才会报错。2.宏定义不用加分号。否则画蛇添足。3.#define命令出现在程序函数的外面,宏名的有效范围为定义命令之后到源文件结束。3.带参数的宏定义一定注意空格的问题,不应随便打空格。4.带参数宏定义和函数有较大区别,宏定义只是简单的相应参数的替换,而且运行时并不分配空间,也并无返回值。而且对于函数要定义形参的类型,宏定义字符串可以是任何类型的数据。5.宏定义可以嵌套。6.宏定义双括号中的数据不会被替换。
当然也可以用#undef命令终止宏定义的宏定义。
文件包含处理:即我们常用到的#include "文件名",这就相当于将相应文件内容全部复制到#include这个地方。非常方便。
一个include命令只能指定一个被包含的文件。如果要包含n个文件,需n次include,如果文件1包含文件2,而文件2需要用到文件3内容,则可文件1中用两个include命令分别包含文件2和文件3,而且文件3需要出现在文件2之前。
条件编译:形式:
#ifdef 标识符 程序段1 #else 程序段2 #endif
它的作用是:当标识符已经被定义过(一般用#define定义),则对程序段1进行编译,否则编译程序段2,其中#else部分可以没有。
这里的程序段可以是语句组,也可以是命令行。这种条件编译对于提高C源程序的通用性是很有好处的。
#ifndef 标识符 程序段1 #else 程序段2 #endif
这个形式功能是若标识符未被定义则编译程序段1,否则编译程序段2.
#if 表达式 程序段1 #else 程序段2 #endif
它的作用是:当指定的表达式值为真时就编译程序段1,否则编译程序段2
使用条件编译命令可以减少被编译的语句,从而减少程序的长度
6.
int main(int argc, char *argv[]){ int a[3][4]; printf("%p %p %p %p %p %p\n", &a[0][0], a[0], a, a[0]+1, a+1, a[1]); return 0; }
第六道题比较简单,考察二维数组的地址的问题,显然a指向的地址就是数组的首地址,a[0]指向的是a[0]这一行的首地址,这两者指向的地址就是a[0][0]的地址。a[0] + 1的地址就是第0行第一列的地址,所以是a[0][0]的地址加上一个整型的长度,为0x00000004,而a+1和a[1]的地址都类似可得指向第一行的首地址即a[1][0]所以为第一行的尾地址,即为a[0][0]的地址加上4个整型数据的大小的长度。为0x00000012。
7.下列结构体在内存中所占空间大小。
struct node{ int x; char y; double z; }; struct node{ int x; double y; char z; };
这不是去年面试题里面的那个题么struct对齐的问题么,当时看了答案立刻去研究了一下,现在来记录一下:
研究第一个结构体:
struct node_1{
int x;
char y;
double z;
}a;
这个结构体先后定义了三个我们通过调试研究其中在存储器中存储的位置的规律:(在我的电脑上)
a.x 0x0018ff38
a.y 0x0018ff3c
a.z 0x0018ff40
其中我们可以看出x占有了4个字节
其中y占有了4个字节,而z占有了8个字节。
换一个结构体
struct node_2{
int x;
double y;
char z;
}b;
读出他们的地址:
b.x 0x0018ff20
b.y 0x0018ff28
b.z 0x0018ff30
其中int占有了8个字节,double理所应当占有了8个字节,那么b.z呢答案也是8个字节,这就是对齐的问题,struct要求它的大小与结构体中占内存最长的变量的长度成整数倍关系,其他的元素则向最长的元素长度对齐。其实通过上面的举例很容易看出来,第一个结构体,x是4位,char为1位,为了对齐double的8位,所以在char后面又补了3位。同样,第二个结构体x为4位,但是为了补齐8位,就增加四个字街,最后z也是为了对齐,补7个字节,左移最后大小为24
8.
#define f(a,b) a##b #define g(a) #a #define h(a) g(a) int main(void){ printf("%s\n",h(f(1,2))); printf("%s\n",g(f(1,2))); return 0; }
面对这个题,我的第一感觉是不是#是作为一种符号类似%d输出的那种有什么特殊含义,或者两个#号打印一个#,但是看了这个我发现虽然打印的是%s,但是后面并不是指针或是字符串,所以#号应该与其他的东西有关,于是我令其生成.i文件结果惊人的发现
这你敢信,为什么会这样??
于是就上网查了查,原来我们使用#把宏参数变为一个字符串,用##把两个宏参数贴合在一起.
于是我们分析,第一次打印,f(1,2)相当于把1 2粘合在一起,成为12,然后h(12)则是将12变成字符串,所以打印出来就是12,那么问题来了,第二次丫的是怎么回事,为什么第二次就成了f(1,2),甚是不解,所以又去网上查阅资料,终于懂得,当执行第一次打印的时候h(a)中没有#所以先展开里面的f(1,2)得到的是12,而第二次g(a)中有#所以直接展开的是f(1,2)。此题得解。
9.
case 1: printf("I'm case1\n"); break; default: printf("I'm default\n"); case 2: printf("I'm case2\n"); }
此题从我的感觉上是不难的,当i=1时,case1执行打印那句case1的话,同时break跳出switch,当i=2时,执行case2打印case那句话,同时switch跑完跳出,当i为其他值时,进入default此时打印完那句话后,因为没有case顺序执行打印case2中的话。
12.
int main(void){ int a[5] = {1, 2, 3, 4, 5}; int *ptr = (int *)(&a +1); printf("%d,%d\n",*(a+1),*(ptr-1)); }
对于a来说,它本身既是一个指针,另外也可以理解为一个数组,在这里,&a是整个数组a的首地址,而加1后找到的是下一个与a同类型的地址,也就是a[5]的尾地址,反映在程序中就是这样的:
这样可以看出来两者相差的是是20个字节,正好是5个int型的长度,多以答案是2,5.
13.
int main(void){ char *str1 = "WelcomeTo\0XiyouLinux\n"; char str2[] = "WelcomeTo\0XiyouLinux\n"; printf("%d\n",printf("%s",str1)); printf("%d,%d\n",strlen(str1),strlen(str2)); printf("%d,%d\n",sizeof(str1),sizeof(str2)); return 0; }
这个题主要考察难点在于printf的嵌套吧,第一次打印的时候,先打印字符串str1,打印出WelcomeTo,然后返回打印的str1的长度,也就是9,然后打印出9。后面两个打印打印的分别是一个指针的长度和数组名的长度,这个前面有道题就有类似的,显然strlen相同,都为9,而*str1作为一个字符指针,只有四个字节,所以sizeof得到4,而数组则是这一长串字符串,加上最后的'\0'一共22个字节。得到结果。
14.
实现一个函数,该函数将字符串中字符'*'移动到串前部分,前面的非'*'字符后移,但不能改变非'*'字符的先后顺序。函数返回串中‘*’的数量:
int change(char str[]){ for(int i = strlen(str) - 1; i > 0; i --){ if(str[i] == '*'){ for(int j = i - 1; j >= 0; j --){ if(str[j] != '*'){ char t = str[j]; str[j] = str[i]; str[i] = t; break; } } } } for(i = 0; str[i] == '*'; i++) icount++; return icount; }
这个函数感觉稍微还是有点麻烦,因为是移动除了*号之外的字符,所以在计算上不好计算*的个数。但是倘若移动*号的话,那就可能导致其他字符顺序改变。所以暂时采用这种算法。
15.
char* my_strcpy(char s1[], char s2[]){ char *t = s1; while( *(s1 ++) = *(s2 ++) != '\0'); return s1; }
16.知道下面的语句么?
#ifndef _MY_FILE #define _MY_FILE
显然是避免重复定义,举例,如我们有两个文件,同时调用了头文件stdio.h,因为预编译我们知道我们会把stdio.h里面的东西全部插入在include位置处,所以会出现重复定义的问题,而这里就是避免了重复定义,意思为如果没有宏定义_MY_FILE的话,定义_MY_FILE。假设当编译了A文件后,编译B时会发现已经宏定义_MY_FILE,就不再重复插入stdio.h
当然,与此类似功能的是#program once,它用于说明本文件中的内容只使用一次,即只编译一次头文件,也可以避免重复定义。
17.
int main(void){ FILE *fp = fopen("a.txt","r"); //if(!fp) printf("1111111"); char buffer[4096]; fgets(buffer, sizeof(buffer), fp); fprintf(fp, "%s", buffer); return 0; }
这个题让我们找程序有什么问题,从第一行看,我们就发现了问题,如果a.txt文件不存在的话,那么就不能以r的形式打开,否则返回的NULL就会出现问题,由fgets表达的Expression str!=NULL。所以我们在初次创建文件的时候,需要用"w"方式创建。
其次,因为a.txt文件下没有任何数据,所以取buffer长度个大小的字节读入buffer的时候还是没有被改变。所以是我们熟悉的烫烫烫烫。。。,然后下面fprintf函数则会把buffer里面的数据读到a.txt中,从而a.txt中将会是2048个烫再加上两个字符。那么问题就是这些。
18.看下面函数干什么
int compare(int middle, int key){ if(0 > middle - key){ return -1; } else if(0 < middle - key){ return 1; } else return 0; } int binarySearch(const int list[], int key, int length){ int left = 0,right = length - 1, middle; while(left <= right){ middle = (right - left)/2 + left; switch(compare(list[middle], key)){ case -1: left = middle + 1; break; case 0: return middle; case 1: right = middle - 1; } } return -1; }
通过函数名字可知大概就是二分法求数据位置吧。那么二分法的前提条件就是需要知道两个边界和长度,否则无法遍历到全部数据,也就是需要知道length和left两个数据。这个程序之所以可以这样做就是读取了边界。
19.
void func(char *p){ p = (char*)malloc(sizeof(char)); } int main(){ char *s = NULL; func(s); strcpy (s, "i love xiyou_linux"); puts(s); return 0; }
这个是在形参中申请空间,非常明显的指针的考察问题,这里如果为p申请地址好比对局部变量做运算,显然当函数结束的时候,所申请的空间同样被释放。改法类似于对局部变量取地址,我们需要对指针的地址进行处理,这样申请的空间才可以给到指针。所以解答如下:
void func(char **s){ *s = (char*)malloc(sizeof(char)); } int main(){ char *s = NULL; func(&s); strcpy (s, "i love xiyou_linux"); puts(s); free(s); return 0; }
20.
int main(int argc, char *argv[])
main主函数是有带参数的情况的。argc是指命令行参数总个数,包括可执行程序名。而argv[0]就是可执行程序名,argv[i]代表的是第i个参数