西邮Linux兴趣小组2022纳新面试题题解
前言
- 本题目只作为
Xiyou Linux兴趣小组
2022纳新面试的有限参考。 - 为节省版面,本试题的程序源码省去了
#include
指令。 - 本试题中的程序源码仅用于考察C语言基础,不应当作为C语言「代码风格」的范例。
- 题目难度随机排列。
- 所有题目编译并运行于
x86_64 GNU/Linux
环境。 - 小编是C语言初学者,若有知识性错误敬请谅解。
学长寄语:
长期以来,西邮Linux兴趣小组的面试题以难度之高名扬西邮校内。我们作为出题人也清楚的知道这份试题略有难度。请别担心。若有同学能完成一半的题目,就已经十分优秀。 其次,相比于题目的答案,我们对你的思路和过程更感兴趣,或许你的答案略有瑕疵,但你正确的思路和对知识的理解足以为你赢得绝大多数的分数。最后,做题的过程也是学习和成长的过程,相信本试题对你更加熟悉的掌握C语言的一定有所帮助。祝你好运。我们FZ103见!
Copyright © 2022 西邮Linux兴趣小组, All Rights Reserved.
本试题使用采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
0. 我的计算器坏了?!
2^10 = 1024
对应于十进制的4位,那么2^10000
对应于十进制的多少位呢?
我们知道,十进制从右往左数第n位代表10的n-1次方,那么,我们只需要求出一个数所包含的10的最大次方再加1,便可得到这个数的位数。
我们可以以这个数为底数,10为真数,进行对数运算,再加上1,便得到对应十进制的位数。例如(lg2≈0.3010):lg2^10+1=10lg2+1≈4。
lg2^10000+1=10000lg2+1≈3011,答案就为3011。
1. printf还能这么玩?
尝试着解释程序的输出。
int main(void) {
if ((3 + 2 < 2) > (3 + 2 > 2))
printf("Welcome to Xiyou Linux Group\n");
else
printf("%d\n", printf("Xiyou Linux Group - 2%d", printf("")));
}
if ((3 + 2 < 2) > (3 + 2 > 2))
:3+2<2为假,值为0;3 + 2 > 2为真,值为1;0>1为假,因此不执行if语句,执行else。
printf("%d\n", printf("Xiyou Linux Group - 2%d", printf("")));
这句话是printf嵌套,对于printf嵌套,应从里层到外层依次打印,printf 函数返回值返回的是成功打印到标准输出的字符数,在 printf(" ")中,由于字符串中没有任何字符,因此打印的字符数为0,返回值为0。**因此,该语句打印出 Xiyou Linux Group - 2022。
2. 你好你好你好呀!
程序的输出有点奇怪,请尝试解释一下程序的输出吧。
请谈谈对
sizeof()
及strlen()
的理解吧。
int main(void) {
char p0[] = "Hello,Linux";
char *p1 = "Hello,Linux";
char p2[11] = "Hello,Linux";
printf("p0 == p1: %d, strcmp(p0, p2): %d\n", p0 == p1, strcmp(p0, p2));
printf("sizeof(p0): %zu, sizeof(p1): %zu, sizeof(*p2): %zu\n",
sizeof(p0), sizeof(p1), sizeof(*p2));
printf("strlen(p0): %zu, strlen(p1): %zu\n", strlen(p0), strlen(p1));
}
char p0[] = "Hello,Linux";
定义了一个字符数组p0,当对字符数组整体赋值时,不需要标明长度,C编译器会在其后添加"\0",并自动计算字符数组的长度,包括"\0"在内。char *p1 = "Hello,Linux";
定义了一个字符指针p1,指向一个字符串常量"Hello,Linux"。char p2[11] = "Hello,Linux";
定义了一个字符数组p2,因长度限制,其后并不会自动补充"\0"。p0 == p1
:**直接引用数组名,值为数组首元素的地址;直接引用指针名,值为指针指向的地址。**这两个值不相等,因此该表达式返回值为0。strcmp(p0, p2)
:strcmp是比较函数,它按照ASCll码顺序比较两个字符串,若相等,返回0;若不相等,一般返回1(前面的>后面的)或-1(前面的<后面的),有时返回不相等的两个字符的ASCII码之差。这里比较到p0的"\0"时,p2的下一个字符为随机值,因此返回值也为随机值。这里返回-72。sizeof(p0)
:为这个数组的大小,即12。sizeof(p1)
:为这个指针存放地址的大小,即8。sizeof(*p2)
:为这个数组首元素(“H”)的大小,即1。strlen(p0)
:为数组p0存放字符串的长度,到"\0"截止(不包含"\0"),即11。strlen(p1)
:为指针p1所指字符串的长度,到"\0"截止(不包含"\0"),即11。
3. 换个变量名不行吗?请结合本题,分别谈谈你对C语言中「全局变量」和「局部变量」的「生命周期」理解。
int a = 3;
void test() {
int a = 1;
a += 1;
{
int a = a + 1;
printf("a = %d\n", a);
}
printf("a = %d\n", a);
}
int main(void) {
test();
printf("a= %d\n", a);
}
- 首先在全局作用域定义了一个变量a,这是全局变量,作用域覆盖整个程序。
- 接着在test函数中,重新定义了一个局部变量a,这将遮盖全局作用域定义的变量a。
- 在test函数中的内部块又定义了一个局部变量a,但并未进行初始化,因此在第一个printf语句中打印a的值是不规范的,这将打印出一个随机值。
- 第二个printf语句,打印a=2。
- 在main函数中的第三个printf语句,对全局变量a进行访问,打印a=3。
- 全局变量:
- 生命周期:程序运行期一直存在,从程序开始到程序结束。
- 作用域:全局作用域(只需要在一个源文件中定义,就可以在其他所有源文件中访问)。
- 局部变量:
- 生命周期:随着函数的结束,即被销毁。
- 作用域:局部作用域。
详情见这里
4. 内存对不齐
union
与struct
各有什么特点呢,你了解他们的内存分配模式吗。
typedef union {
long l;
int i[5];
char c;
} UNION;
typedef struct {
int like;
UNION coin;
double collect;
} STRUCT;
int main(void) {
printf("sizeof (UNION) = %zu\n", sizeof(UNION));
printf("sizeof (STRUCT) = %zu\n", sizeof(STRUCT));
}
- union与struct都是由多个不同的数据类型成员组成,但在任何同一时刻,union中只存放了一个被选中的成员,而struct的所有成员都存在。
- union:所有成员不能同时占用它的内存空间,它们不能同时存在。
- struct:各成员都占有自己的内存空间,它们是同时存在的。
计算内存:
这里存在一个词“对齐数”,默认对齐数由处理器架构和编译器决定。
如有#pragma pack(n)预编译指令,将会把默认对齐数修改为n字节。
- 联合体内存对齐:
- 一般来说以最大成员长度作为总长度。
- 总长度必须是默认对齐数的整数倍。
- 结构体内存对齐:
- 结构体中第一个成员的偏移量为0
- 每个成员都有一个对齐数,为min(默认对齐数,该成员内存大小),每个成员的偏移量必须是其对齐数的整数倍
- 在所有成员各自对齐后,结构体还要进行一次整体对齐,即总长度必须是最大对齐数的整数倍。
这里我们以默认对齐数为8对这道题进行分析:
typedef union {
long l;//sizeof(l)=4
int i[5];//sizeof(i[5])=20
char c;//sizeof(c)=1
} UNION;//sizeof(UNION)=20+4=24
typedef struct {
int like;//sizeof(int)=4
UNION coin;//sizeof(UNION)=24,偏移量为4,补充对齐4
double collect;//sizeof(double)=8,偏移量为32
} STRUCT;//sizeof(STRUCT)=4+4+24+8=40
5. Bitwise
- 请使用纸笔推导出程序的输出结果。
- 请谈谈你对位运算的理解。
int main(void) {
unsigned char a = 4 | 7;//0000 0100|0000 0111=0000 0111=7
a <<= 3;//0000 0111<<3=0011 1000=56
unsigned char b = 5 & 7;//0000 0101&0000 0111=0000 0101=5
b >>= 3;//0000 0101>>3=0000 0000=0
unsigned char c = 6 ^ 7;//0000 0110^0000 0111=0000 0001=1
c = ~c;//~0000 0001=1111 1110=254
unsigned short d = (a ^ c) << 3;//(0011 1000^1111 1110=1100 0110)<<3=0000 0110 0011 0000=1584
signed char e = -63;//有符号数,首位为1,1100 0001
e <<= 2;//1100 0001<<2=0000 0100=4
printf("a: %d, b: %d, c: %d, d: %d\n", a, b, c, (char)d);
printf("e: %#x\n", e);
}
打印结果:a: 56, b: 0, c: 254, d: 48
e: 0x4
a,b,c都没什么问题,下面具体分析以下d和e:
- short占2个字节,char占1个字节,8个位=1个字节,d是以short定义的,但在打印d的值时使用了强制类型转换,以char的形式打印,将会丢弃前八位,打印后八位的结果,因此打印48。
- e打印以%#x的形式,%x是十六进制的占位符,加"#“的作用是使输出结果带进制前缀"0x”。
关于位运算:
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都为1时,结果才为1 |
| | 或 | 两个位有一个为1,结果就为1 |
^ | 异或 | 两个位相同时为0,不同时为1 |
~ | 取反 | 0变为1,1变为0 |
<< | 左移 | <<n:各二进制位全部左移n位,高位丢弃,低位补0 |
>> | 右移 | >>n:各二进制位全部右移n位,对于无符号数,高位补0;有符号数,右移补1 |
6. 英译汉
请说说下面数据类型的含义,谈谈const
的作用。
-
char *const p
。常量指针p,指向一个字符(串),且该指针指向不能再改变,但存放地址对应的值可以改变。
-
char const *p
。 -
const char *p
。这两个含义是一样的,表示指针p指向一个常量字符(串),p的指向可以改变,但不能通过指针p改变所指的内容。
**const:**用来定义常量,如果一个变量被const修饰,那么它的值不能再被改变。
对于哪个是常量可以根据const在*的前面还是后面来判断:
- const*(常量指针):所指是const,不能通过指针改变所指的内容
- *const(指针常量):指针是const,指针的指向不能改变
- const*const:指向常量的常量指针,指针的指向不能改变,也不能通过指针改变所指的内容
7. 汉译英
请用变量p
给出下面的定义:
-
含有10个指向
int
的指针的数组。int *p[10]
-
指向含有10个
int
数组的指针。int (*p)[10]
-
含有3个「指向函数的指针」的数组,被指向的函数有1个
int
参数并返回int
。int (*p[3])(int)
8. 混乱中建立秩序
你对排序算法了解多少呢?
请谈谈你所了解的排序算法的思想、稳定性、时间复杂度、空间复杂度。
提示:动动你的小手敲出来更好哦~
排序算法稳定性:
如果在一个待排序的序列中,存在2个相等的数,在排序后这2个数的相对位置保持不变,那么该排序算法是稳定的;否则是不稳定的。
**时间复杂度:**算法中基本操作重复执行的次数。通常我们计算时间复杂度都是计算最坏情况。用T(n)表示。
**空间复杂度:**指运行完一个程序所需内存的大小。用S(n)表示。
冒泡排序:
- **思想(以从小到大排序为例):**一开始交换的区间为0N-1,将第1个数和第2个数进行比较,前面大于后面,交换两个数,否则不交换。再比较第2个数和第三个数,前面大于后面,交换两个数否则不交换。依次进行,最大的数会放在数组最后的位置。然后将范围变为0N-2,数组第二大的数会放在数组倒数第二的位置。依次进行整个交换过程,最后范围只剩一个数时数组即为有序。
- **稳定性:**冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定的排序算法。
- T(n)=O(n^2)
- S(n)=O(1)
- 核心代码:
//array[]为待排序数组,n为数组长度
void BubbleSort(int array[], int n)
{
int i, j, k;
for(i=0; i<n-1; i++)
for(j=0; j<n-1-i; j++)
{
if(array[j]>array[j+1])
{
k=array[j];
array[j]=array[j+1];
array[j+1]=k;
}
}
}
选择顺序:
-
**思想(以从小到大排序为例):**选择排序从小到大排序:一开始从0n-1区间上选择一个最小值,将其放在位置0上,然后在1n-1范围上选取最小值放在位置1上。重复过程直到剩下最后一个元素,数组即为有序。
-
**稳定性:**选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序是不稳定的排序算法。
-
T(n)=O(n^2)
-
S(n)=O(1)
-
核心代码:
//array[]为待排序数组,n为数组长度 void selectSort(int array[], int n) { int i, j ,min ,k; for( i=0; i<n-1; i++) { min=i; //每趟排序最小值先等于第一个数,遍历剩下的数 for( j=i+1; j<n; j++) //从i下一个数开始检查 { if(array[min]>array[j]) { min=j; } } if(min!=i) { k=array[min]; array[min]=array[i]; array[i]=k; } } }
9. 手脑并用
请实现ConvertAndMerge函数:
拼接输入的两个字符串,并翻转拼接后得到的新字符串中所有字母的大小写。
提示:你需要为新字符串分配空间。
char* convertAndMerge(/*补全签名*/);
int main(void) {
char words[2][20] = {"Welcome to Xiyou ", "Linux Group 2022"};
printf("%s\n", words[0]);
printf("%s\n", words[1]);
char *str = convertAndMerge(words);
printf("str = %s\n", str);
free(str);
}
补全后的代码是这样的:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char* convertAndMerge(char words[2][20]){
char *result=(char *)malloc(40*sizeof(char));
strcpy(result,words[0]);
strcat(result,words[1]);
int n=strlen(result);
for(int i=0;i<n;i++){
if(result[i]>='A'&&result[i]<='Z')
result[i]+=32;
else if(result[i]>='a'&&result[i]<='z')
result[i]-=32;
}
return result;
}
int main(void) {
char words[2][20] = {"Welcome to Xiyou ", "Linux Group 2022"};
printf("%s\n", words[0]);
printf("%s\n", words[1]);
char *str = convertAndMerge(words);
printf("str = %s\n", str);
free(str);
}
10. 给你我的指针,访问我的心声
程序的输出有点奇怪,请尝试解释一下程序的输出吧。
int main(int argc, char **argv) {
int arr[5][5];
int a = 0;
for (int i = 0; i < 5; i++) {
int *temp = *(arr + i);
for (; temp < arr[5]; temp++)
*temp = a++;
}
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
printf("%d\t", arr[i][j]);
}
}
}
先看输出结果:
0 1 2 3 4
25 26 27 28 29
45 46 47 48 49
60 61 62 63 64
70 71 72 73 74
分析:
for (int i = 0; i < 5; i++) {
int *temp = *(arr + i);
for (; temp < arr[5]; temp++)
*temp = a++;
}
外层循环表示遍历行数,int *temp = *(arr + i);
是让指针temp指向当前行的的一个元素;
内层循环表示遍历从当前行到最后一行的所有元素,并对每个元素进行赋值。
对具体过程进行分析:
当执行完第1次外层循环后,数组 arr[5][5]
被赋值为"0~24";
接着执行第2次,从第二行到最后一行的所有元素被重新赋值为"25~44";
以此类推,就得到上述输出结果。
11. 奇怪的参数
-
你了解argc和argv吗?
-
直接运行程序argc的值为什么是1?
-
程序会出现死循环吗?
#include <stdio.h>
int main(int argc, char **argv) {
printf("argc = %d\n", argc);
while (1) {
argc++;
if (argc < 0) {
printf("%s\n", (char *)argv[0]);
break;
}
}
}
-
argc和argv是两个用于传递命令行参数的参数。
argc(argument count)是一个整数,表示传递给程序的命令行参数的数量(包括程序本身)。它至少为1,因为第一个参数始终是程序的名称。
argv(argument vector)是一个指向字符串数组的指针,每个字符串表示一个命令行参数。第一个元素argv[0]是程序的名称,后续元素argv[1]、argv[2],以此类推,表示其他命令行参数。
-
不会出现死循环。因为argc为int类型,argc的值不断增加,直至达到int类型的最大范围,int类型的取值范围为
(-2^31,2^31-1)
,即argc=2^31-1
之后再自增产生数据溢出,argc将会变为-2^31
,这时将会执行if语句,打印argv[0],并退出while循环。
12. 奇怪的字符
程序的输出有点奇怪,请尝试解释一下程序的输出吧。
int main(int argc, char **argv) {
int data1[2][3] = {{0x636c6557, 0x20656d6f, 0x58206f74},
{0x756f7969, 0x6e694c20, 0x00000000}};
int data2[] = {0x47207875, 0x70756f72, 0x32303220, 0x00000a32};
char *a = (char *)data1;
char *b = (char *)data2;
char buf[1024];
strcpy(buf, a);
strcat(buf, b);
printf("%s \n", buf);
}
- 首先,定义了二维数组data1和一维数组data2,都储存了字符串的地址
- 接着,将data1和data2强制类型转换为字符指针并分别赋给a和b
- 然后,定义了一个字符数组buf,并使用strcpy函数将a复制到buf,使用strcat函数将b拼接到buf后面
- 最后,打印buf,结果为
Welcome to Xiyou Linux Group 2022
这里特别注意一下:
将整数数组强制类型转换为字符指针并进行字符串操作可能会导致不可预测的结果,因为整数和字符在内存中的表示方式不同。这种操作在某些情况下可能会导致错误或不符合预期的行为。如果要将整数数据转换为字符串,通常应使用适当的函数(如 sprintf
)来进行转换。此外,strcpy
和 strcat
函数在处理不以 null 结尾的字符串时会导致未定义的行为。在这段代码中,data2
并没有以 null 结尾,因此在拼接到 buf
后面时可能会导致问题。
13. 小试宏刀
- 请谈谈你对
#define
的理解。 - 请尝试着解释程序的输出。
#define SWAP(a, b, t) t = a; a = b; b = t
#define SQUARE(a) a*a
#define SWAPWHEN(a, b, t, cond) if (cond) SWAP(a, b, t)
int main() {
int tmp;
int x = 1;
int y = 2;
int z = 3;
int w = 3;
SWAP(x, y, tmp);
printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
if (x>y) SWAP(x, y, tmp);
printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
SWAPWHEN(x, y, tmp, SQUARE(1 + 2 + z++ + ++w) == 100);
printf("x = %d, y = %d\n", x, y);
printf("z = %d, w = %d, tmp = %d\n", z, w, tmp);
}
先看输出结果:
x = 2, y = 1, tmp = 1
x = 1, y = 2, tmp = 2
x = 2, y = 2
z = 5, w = 5, tmp = 2
-
#define
:简单来说,就是进行文本替换,而并不会自动添加其他符号。
具体的请看这里
-
分析代码:
-
#define SWAP(a, b, t) t = a; a = b; b = t
:将(a, b, t)
替换为(b, a, a)
-
#define SQUARE(a) a*a
:将SQUARE(a)
替换为a*a
-
#define SWAPWHEN(a, b, t, cond) if (cond) SWAP(a, b, t)
:if (cond) t = a; a = b; b = t
-
SWAPWHEN(x, y, tmp, SQUARE(1 + 2 + z++ + ++w) == 100);
这句话可以翻译为if (1 + 2 + z++ + ++w *1 + 2 + z++ + ++w == 100) tmp = x; x = y; y = tmp;
if条件不成立,不会执行if语句。但会执行
x = y; y = tmp;
-
-
若想达到预期效果,宏定义应这样修改:
#define SWAP(a, b, t) {t = a; a = b; b = t;} #define SQUARE(a) (a*a) #define SWAPWHEN(a, b, t, cond) if (cond) SWAP(a, b, t)
恭喜你做到这里!你的坚持战胜了绝大多数看到这份试题的同学。
或许你自己对答题的表现不满意,但别担心,请自信一点呐。
坚持到达这里已经证明了你的优秀。
还在等什么,快带上你的笔记本电脑,来FZ103面试吧!