前言
-
本题目只作为 Xiyou Linux兴趣小组2022纳新面试的有限参考。
-
为节省版面,本试题的程序源码省去了#include指令。
-
本试题中的程序源码仅用于考察C语言基础,不应当作为C语言「代码风格」的范例。
-
题目难度随机排列。
-
所有题目编译并运行于x86_64 GNU/Linux环境。
- 学长寄语:
长期以来,西邮Linux兴趣小组的面试题以难度之高名扬西邮校内。我们作为出题人也清楚的知道这份试题略有难度。请别担心。若有同学能完成一半的题目,就已经十分优秀。 其次,相比于题目的答案,我们对你的思路和过程更感兴趣,或许你的答案略有瑕疵,但你正确的思路和对知识的理解足以为你赢得绝大多数的分数。最后,做题的过程也是学习和成长的过程,相信本试题对你更加熟悉的掌握C语言的一定有所帮助。祝你好运。我们FZ103见!
- Copyright © 2022 西邮Linux兴趣小组, All Rights Reserved.
本试题使用采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 link 进行许可。
0. 我的计算器坏了?!
- 2^10 = 1024对应于十进制的4位,那么2^10000对应于十进制的多少位呢?
计算2^10000对应于十进制的位数,使用对数的性质来估算。
位数 = floor(lg(2^10000)) + 1
floor表示向下取整,+1用于考虑位数最左侧的1位。2^10000的位数:
位数 = floor(lg(2^10000)) + 1
位数 = floor(10000 * lg(2)) + 1lg(2) 约为 0.3010
位数 = floor(10000 * 0.3010) + 1
位数 = floor(3010) + 1
位数 = 3010 + 1
位数 = 3011
所以,2^10000对应于十进制的位数为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("")));
}
1.条件语句:
(3 + 2 < 2) 为 0,5 不小于 2。
(3 + 2 > 2) 为 1, 5 大于 2。
条件 (0 > 1) 是 假,所以程序执行 else 块。2.else块printf 嵌套:
printf("") 打印一个空字符串,但printf 返回打印的字符数,因此返回0接下来是 printf(“Xiyou Linux Group - 2%d”, 0),这里 %d 需要一个整数参数来替代。在这种情况下,整数参数是0。所以它将打印 “Xiyou Linux Group - 20”。3.最终,printf("%d\n", 22) 打印 “22”。
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));
}
代码创建了三个字符数组:p0、p1 和 p2。
p0 是一个字符数组,分配了足够的空间来存储字符串 “Hello,Linux” 及其空字符结束符\0。
p1 是一个指向常量字符串 “Hello,Linux” 的指针。
p2 也是一个字符数组,但是指定了大小为 11,这里的字符串"Hello,Linux" 和空字符结束符总共需要 12 个字符的空间,所以它的大小可能会造成截断。
结果:
p0 == p1 打印为 0,因为它们是不同的类型: p0 是一个字符数组,而 p1 是指向常量字符串的指针。
strcmp(p0, p2) :当读到p0的\0时,此时p2的下一个字符为内存中的随机值,结果不定。
sizeof(p0) 打印为 12,因为 p0 是一个包含 “Hello,Linux” 及其空字符结束符的字符数组。
sizeof(p1) 打印为指针的大小8,不会受到指针指向的字符串长度的影响。
sizeof(*p2) 打印字符’H’的大小1。
strlen(p0) 打印为 11,因为它返回字符串长度,不包括空字符结束符。
strlen(p1) 打印为 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);
}
全局变量:
全局变量是在程序的全局范围内声明的变量,在函数外部声明。int a = 3; 是一个全局变量,它在函数外部声明。全局变量的生命周期从程序启动时开始,一直持续到程序结束。全局变量在整个程序执行期间都是可访问的。
局部变量:
局部变量是在特定函数内部声明的变量,只在该函数的作用域内可见。int a = 1; 是test()函数内的局部变量。局部变量的生命周期从函数被调用时开始,直到函数执行完毕时结束。它们只在函数执行期间可访问,并且在函数执行完毕后将被销毁。
test()函数内有多个不同作用域的局部变量a。在内部最深层的花括号中,使用a = a + 1;,但这里的a在内部作用域中声明,并不影响外部作用域中的a。
总结:
全局变量在整个程序执行期间都可访问,其生命周期与程序的生命周期相同。
局部变量只在其声明的函数内可见,生命周期从函数被调用时开始,直到函数执行完毕时结束。不同作用域内的同名局部变量互相独立,不会相互影响。
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 :
union 只能存储其中的一个成员的值。
所有成员共享同一块内存,因此 union 的大小等于它最大的成员的大小。
由于所有成员共享内存,改变一个成员的值可能会影响其他成员。
union 适合用于节省内存空间或者在不同的数据类型之间进行转换。
struct :
所有成员在内存中依次排列,占据各自的内存空间。
struct 的大小是所有成员大小之和,可能会因为内存对齐而有所增加。
每个成员可以同时保存值,互不影响。
// 若是4字节对齐
typedef union {
long l; // 0 ~ 3
int i[5]; // 0 ~ 19
char c; // 0 ~ 7
} UNION; // 0 ~ 19 -> 20
typedef struct {
int like; // 0 ~ 3
UNION coin; // 4 ~ 23
double collect; // 23 ~ 30
} STRUCT; // 0 ~ 31 -> 32
// 若是8字节对齐
typedef union {
long l; // 0 ~ 3
int i[5]; // 0 ~ 19
char c; // 0 ~ 7
} UNION; // 0 ~ 23 -> 24
typedef struct {
int like; // 0 ~ 3
UNION coin; // 8 ~ 27
double collect; // 32 ~ 39
} STRUCT; // 0 ~ 39 -> 40
5. Bitwise
- 请使用纸笔推导出程序的输出结果。
- 请谈谈你对位运算的理解。
int main(void) {
unsigned char a = 4 | 7;
a <<= 3;
unsigned char b = 5 & 7;
b >>= 3;
unsigned char c = 6 ^ 7;
c = ~c;
unsigned short d = (a ^ c) << 3;
signed char e = -63;
e <<= 2;
printf("a: %d, b: %d, c: %d, d: %d\n", a, b, c, (char)d);
printf("e: %#x\n", e);
}
1.unsigned char a = 4 | 7; 执行按位或操作,4的二进制表示是 00000100,7的二进制表示是 00000111,按位或操作得到 00000111,对应的十进制是7。
2.a <<= 3; 左移操作,将a的二进制值 00000111 左移3位,变成 00111000,对应的十进制是56。
3.unsigned char b = 5 & 7; 执行按位与操作,5的二进制表示是 00000101,7的二进制表示是 00000111,按位与操作得到 00000101,对应的十进制是5。
4.b >>= 3; 右移操作,将b的二进制值 00000101 右移3位,变成 00000000,对应的十进制是0。
5.unsigned char c = 6 ^ 7; 执行按位异或操作,6的二进制表示是 00000110,7的二进制表示是 00000111,按位异或操作得到 00000001,对应的十进制是1。
6.c = ~c; 执行按位取反操作,将c的二进制值 00000001 取反,变成 11111110,对应的十进制是254。
7.unsigned short d = (a ^ c) << 3; 首先执行按位异或操作,a的值是56,c的值是254,它们的二进制异或结果是 11000110,这对应的十进制是 198。然后将这个结果左移3位,得到 0000011000110000,对应的十进制是1584。
8.signed char e = -63; 给e赋值为-63。
9.e <<= 2; 左移操作,将e的值 -63 左移2位,得到 4。
a: 56, b: 0, c: 254, d: 48
e: 0x4
6. 英译汉
- 请说说下面数据类型的含义,谈谈const的作用。
- char *const p。
- char const *p。
- const char *p。
1.char *const p:
声明了一个指向字符(char)类型的常量指针(const指针)。
p是一个常量指针,一旦指向了某个内存地址,就无法再改变p指向的地址。
但可以通过p来修改所指向地址上的字符内容。
2.char const *p:
这是一个指向字符(char)类型的指针,指向的字符是常量。
p是一个指向常量字符的指针,不能通过p来修改所指向地址上的字符内容。
但可以改变p指向的地址,即可以将p指向不同的常量字符。
3.const char *p:
这也是一个指向字符(char)类型的指针,指向的字符是常量。
与2相同,p是一个指向常量字符的指针,通过p来修改所指向地址上的字符内容。
同样,它可以改变p指向的地址,即可以将p指向不同的常量字符。
7. 汉译英
- 请用变量p给出下面的定义:
- 含有10个指向int的指针的数组。
- 指向含有10个int数组的指针。
- 含有3个「指向函数的指针」的数组,被指向的函数有1个int参数并返回int。
int* p[10];
int (*p)[10];
int (*p[3])(int);
8. 混乱中建立秩序
- 你对排序算法了解多少呢?
请谈谈你所了解的排序算法的思想、稳定性、时间复杂度、空间复杂度。
提示:动动你的小手敲出来更好哦~
冒泡排序(Bubble Sort):
思想:通过相邻元素的比较和交换来将最大(或最小)的元素逐步"冒泡"到列表的末尾。
void BubbleSort(int arr[], int len)
{
int i, j, tmp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
{
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
选择排序(Selection Sort):
思想:通过在未排序部分选择最小(或最大)的元素并将其放在已排序部分的末尾。
void SelectionSort(int arr[], int len)
{
int i, j,min;
for (i = 0; i < len - 1; i++)
{
min = i;
for (j = i + 1; j < len; j++)
if (arr[j] < arr[min])
min = j;
int tmp = arr[min];
arr[min] = &arr[i];
&arr[i] = tmp;
}
}
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 <string.h>
#include <malloc.h>
char *convertAndMerge(char strs[2][20])
{
int totalLength = strlen(strs[0]) + strlen(strs[1]);
char* result = (char*)malloc(totalLength + 1);
strcpy(result, strs[0]);
strcat(result, strs[1]);
for (int i = 0; i < totalLength; 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);
}
先计算了两个输入字符串的总长度,然后分配足够的内存来存储拼接后的字符串。接着,使用strcpy和strcat函数将两个输入字符串拼接在一起,并在最后循环遍历拼接后的字符串,翻转其中所有字母的大小写。
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每循环一次第二个for循环次数都会少5,因此为如上结果。
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的值为1是因为直接运行程序,没有提供任何命令行参数。程序名称本身会作为第一个参数传递给main函数。
使用一个无限循环 while (1) 来递增argc的值,然后在检查argc是否小于0的情况下,打印argv[0](程序名称),最后整型argc会自增到溢出并退出循环。
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);
}
定义了一个main函数,然后初始化了两个整数数组data1和data2,将它们转换为字符指针,然后使用strcpy和strcat函数将它们连接到一个字符数组buf中,最后打印buf的内容。
data1是一个二维整数数组,data2是一个一维整数数组。
将data1和data2转换为字符指针a和b,它们将被解释为字符数据,而不是整数数据。使用strcpy函数将a中的数据复制到buf中,然后使用strcat函数将b中的数据追加到buf的末尾。
最后,将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, tmp); printf("z = %d, w = %d, tmp = %d\n", z, w, tmp); }
宏定义只能实现简单的文本替换,并不会自动为表达式补充圆括号或者为代码块补充花括号。
#include <stdio.h>
int main()
{
int tmp;
int x = 1;
int y = 2;
int z = 3;
int w = 3;
// SWAP(x, y, tmp);
tmp = x;
x = y;
y = tmp;
printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
// x = 2, y = 1, tmp = 1
// if (x > y) SWAP(x, y, tmp);
if (x > y)
tmp = x;
// 无论如何以下两行都执行
x = y;
y = tmp;
printf("x = %d, y = %d, tmp = %d\n", x, y, tmp);
// x = 1, y = 2, tmp = 2
// 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;
printf("x = %d, y = %d", x, y, tmp);
// x = 2, y = 2
printf("z = %d, w = %d, tmp = %d\n", z, w, tmp);
// z = 5, w = 5 ,tmp = 2
}
14. GNU/Linux命令 (选做)
-
你知道以下命令的含义和用法吗:
注:
嘿!你或许对Linux命令不是很熟悉,甚至你没听说过Linux。
但别担心,这是选做题,不会对你的面试产生很大的影响!
了解Linux是加分项,但不了解也不扣分哦!- ls
- rm
- whoami
-
请问你还了解哪些GNU/Linux的命令呢。
ls:ls用于列出目录中的文件和子目录。它通常用于命令行终端,可以带有不同的选项以控制列出的详细信息。例如,ls -l会列出文件和目录的详细信息,包括权限、所有者、大小等。
rm:rm用于删除文件或目录。它会永久删除文件,无法撤销。例如,要删除一个文件,可以使用rm 文件名,要删除一个目录及其内容,可以使用rm -r 目录名。
whoami:whoami命令用于显示当前登录用户的用户名。它在命令行终端上执行时,会返回当前用户的用户名。
除了这些,还有很多其他GNU/Linux命令:
pwd:显示当前工作目录的路径。
cd:用于改变当前工作目录。
mkdir:创建新目录。
touch:创建空文件或更新文件的时间戳。
cp:复制文件或目录。
mv:移动文件或目录,也可用于重命名文件。
cat:用于查看文件内容。
grep:用于在文件中搜索文本模式。
chmod:更改文件或目录的权限。
chown:更改文件或目录的所有者。
ps:显示当前运行进程的列表。
kill:终止进程。
top:显示系统资源使用情况。
等等
结束
- 恭喜你做到这里!你的坚持战胜了绝大多数看到这份试题的同学。
或许你自己对答题的表现不满意,但别担心,请自信一点呐。
坚持到达这里已经证明了你的优秀。
还在等什么,快带上你的笔记本电脑,来FZ103面试吧!