西邮Linux兴趣小组2021纳新题解
感谢 Zhilu 重新录入题目原件。好人一生平安。
注:
- 本题目仅作
西邮Linux兴趣小组
2021纳新面试题的有限参考。 - 为节省版面本试题的程序源码中省略了
#include
指令。 - 本试题中的程序源码仅用于考察C语言基础,不应当作为C语言代码风格的范例。
- 题目难度与序号无关。
- 所有题目均假设编译并运行
x86_64 GNU/Linux
环境。
Copyright © 2021 西邮Linux兴趣小组, All Rights Reserved.
本试题使用采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
1. 大小和长度竟然不是一个意思
sizeof()
和strlen()
有什么异同之处?他们对于不同参数的结果有什么不同?请试举例子说明。
int main(void) {
char s[] = "I love Linux\0\0\0";//实际上这样定义字符串后面还藏着一个\0
int a = sizeof(s);
int b = strlen(s);
printf("%d %d\n", a, b);
}
输出为:
16 12
sizeof()是一个静态运算符,意义是计算变量所占内存大小,单位是字节,由于它是静态运算符,所以在其中做的运算没有用,不会生效。strlen()是计算字符串长度的函数,到\0时停止,被包含在<string.h>库中,所以使用时头文件一定要有<string.h>。
2. 箱子的大小和装入物品的顺序有关
test1
和test2
都含有:1个short
、1个int
、1个double
,那么sizeof(t1)
和sizeof(t2)
是否相等呢?这是为什么呢?
struct test1 {
int a;
short b;
double c;
};
struct test2 {
short b;
int a;
double c;
};
int main(void) {
struct test1 t1;
struct test2 t2;
printf("sizeof(t1): %d\n", sizeof(t1));
printf("sizeof(t2): %d\n", sizeof(t2));
}
输出为:
sizeof(t1): 16
sizeof(t2): 16
本题考察了结构体中的内存对齐,结构体在内存中的存储不是简单的把各成员所占的内存相加,而是涉及到内存对齐的知识,因为32位机一次能读取4字节的数据,64位机一次能读取8字节的数据,所以为了提高时间效率,选择了一种浪费空间1的方法,就是自动补齐空间。32位机的对齐系数一般为4,64位机的对齐系数一般为8,所以虽然short类型大小为2字节,但在32位机上会被自动对齐为4字节,64位机上会被自动对齐为8字节。故这道题的结构题大小都为16字节,但是这道题刚好是两个结构体大小相等的情况,倘若稍微变一变成员顺序,大小可能就不同了。
3. 哦,又是函数
想必在高数老师的教导下大家十分熟悉函数这个概念。那么你了解计算机程序设计中的函数吗?请编写一个
func
函数,用来输出二维数组arr
中每个元素的值。
/*在这里补全func函数的定义*/
int main(void) {
int arr[10][13];
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 13; j++) {
arr[i][j] = rand();
}
}
func(arr);
}
#include<stdio.h>
#include<stdlib.h>
int func(int a[10][13]);
int main(void)
{
int arr[10][13];
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 13; j++)
{
arr[i][j] = rand();
}
}
func(arr);
}
int func(int a[10][13])
{
for(int i=0;i<10;i++)
{
for(int j=0;j<13;j++)
{
printf("%d\n",a[i][j]);
}
}
}
考察的是简单知识,二维数组的遍历输出。rand()是随机数函数,所以数组元素都是乱码,故不展示输出。
4.就不能换个变量名吗?
- 请结合下面的程序,简要谈谈
传值
和传址
的区别。- 简要谈谈你对C语言中变量的生命周期的认识。
int ver = 123;
void func1(int ver) {
ver++;
printf("ver = %d\n", ver);
}
void func2(int *pr) {
*pr = 1234;
printf("*pr = %d\n", *pr);
pr = 5678;
printf("ver = %d\n", ver);
}
int main() {
int a = 0;
int ver = 1025;
for (int a = 3; a < 4; a++) {
static int a = 5;
printf("a = %d\n", a);
a = ver;
func1(ver);
int ver = 7;
printf("ver = %d\n", ver);
func2(&ver);
}
printf("a = %d\tver = %d\n", a, ver);
}
输出为:
a = 5
ver = 1026
ver = 7
*pr = 1234
ver = 123
a = 0 ver = 1025
这代码写的太乱了,输出的第一行a是在for语句作用域中定义的,第二行ver是在main函数中定义的ver把值传入func1()函数再自增后的结果,第三行的ver是在for语句作用域中定义的,第四行中的*pr是在for语句中定义的ver把地址传到func2()函数中又通过解引用改变这个ver的值,但是在所有函数外面也定义了一个ver,所以如果要在func2()函数中输出ver的话,也就是第五行,只能输出为123。第六行输出的a已经不在for语句的作用域中了,所以只能输出它在main函数中定义的值,也就是0。而第六行的ver也是一样,输出为1025。
传值为变量作为函数参数时,变量只把值传入函数,这时在函数中就不能对函数外的变量进行操作。
传址为指针作为函数参数时,就会把地址传入函数,然后通过指针的解引用就能在函数中改变函数外变量的值。
在不同情况下定义的变量有不同的生命周期:在全局范围内定义的全局变量的生命周期是整个程序;在某个函数或某个作用域内定义的局部变量的生命周期是这个函数或这个作用域名;加了static前缀的局部变量的生命周期也会被延长为整个程序。
5. 套娃真好玩!
请说明下面的程序是如何完成求和的?
unsigned sum(unsigned n)
{
return n ? sum(n - 1) + n : 0;
}
int main(void)
{
printf("%u\n", sum(100));
}
输出为:
5050
函数自己调用自己,利用了递归去求和,返回一次值后就再次调用参数为该值减一的该函数,不断递归直到n=0时停止,此时sum函数的返回值为100+99+……+1+0,即5050。
6. 算不对的算术
void func(void) {
short a = -2;
unsigned int b = 1;
b += a;
int c = -1;
unsigned short d = c * 256;
c <<= 4;
int e = 2;
e = ~e | 6;
d = (d & 0xff) + 0x2022;
printf("a=0x%hx\tb=0x%x\td=0x%hx\te=0x%x\n", a, b, d, e);
printf("c=Ox%hhx\t\n", (signed char)c);
}
输出为:
a=0xfffe b=0xffffffff d=0x2022 e=0xffffffff
c=0xf0
这道题跟补码和位运算有关,计算机是用补码进行运算的,a等于-2,换算为二进制补码等于1111 1111 1111 1110,再换算为十六进制刚好是0xfffe。b等于-1,但b又是unsigned 类型,所以b等于2 ^ 32 - 1,换算为十六进制为0xffffffff。d等于-256,但b又是unsigned short类型,所以d等于2 ^ 16 - 256。2的二进制为0000 0000 0000 0000 0000 0000 0000 0010,取反为1111 1111 1111 1111 1111 1111 1111 1101,再|6的二进制0000 0000 0000 0000 0000 0000 0000 0110等于1111 1111 1111 1111 1111 1111 1111 1111,是-1的补码,再换算为十六进制等于0xffffffff,也就是e的值。c原本等于-1,左移四位变为-16,再转化为补码形式,换算为十六进制就等于0xf0。
7. 指针和数组的恩怨情仇
int main(void) {
int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int(*b)[3] = a;
++b;
b[1][1] = 10;
int *ptr = (int *)(&a + 1);
printf("%d %d %d \n", a[2][1], **(a + 1), *(ptr - 1));
}
输出为:
10 4 9
a是一个二维数组,b是一个指向a数组的数组指针,二维数组本质上也是一维数组,只不过内部的元素是一维数组。++b后,指针会指向数组的下一个元素,所以b[1] [1]这时就等于a[2] [1]。&a+1指下一个数组的地址(但其实并没有下一个数组),* * (a + 1)等于数组第二维的首元素的值4,*(ptr - 1)等于解引用下一个数组首元素地址减1,也就是等于a数组的末元素9。
8. 移形换位之术
下面有
a
、b
、c
三个变量和4个相似的函数。
- 你能说出使用这三个变量的值或地址作为参数分别调用这5个函数,在语法上是否正确吗?
- 请找出下面的代码中的错误。
const int
和int const
是否有区别?如果有区别,请谈谈他们的区别。const int *
和int const *
是否有区别?如果有区别,请谈谈他们的区别。
int a = 1;
int const b = 2;
const int c = 3;
void funco(int n) {
n += 1;
n = a;
}
void func1(int *n) {
*n += 1;
n = &a;
}
void func2(const int *n) {
*n += 1;
n = &a;
}
void func3(int *const n) {
*n += 1;
n = &a;
}
void func4(const int *const n) {
*n += 1;
n = &a;
}
fun0因为局部变量的原因,无法实现它的目的。
fun1没有错误。
func2函数的错误在于参数是const int *型,但在函数中还是通过指针改变了参数的值。
func3函数的错误在于参数是int * const型,但在函数中还是改变了指针指向的变量。
func4函数两个错误都有。
const int和int const相同,都表示变量的值不能被改变。
const int *和int const *也是相同的,都表示不能通过指针去改变变量的值。
如果const在前面,即const int * n或int const * n,说明是常量指针,如果const在后面,即int * const n,说明是指针常量。常量指针是指不能通过这个指针改变变量的值,即不能用 *指针=xxx来改变变量的值,但并不是说指针本身不能改变,常量指针可以指向其他的变量。指针常量是指指针本身是个常量,不能再指向其他的变量,但可以通过这个指针去改变变量的值,即可以用 *指针=xxx来改变变量的值。常量指针和指针常量正好是相反的。
9. 听说翻转字母大小写不影响英文的阅读?
请编写
convert
函数用来将作为参数的字符串中的大写字母转换为小写字母,将小写字母转换为大写字母。返回转换完成得到的新字符串。
char *convert(const char *s);
int main(void) {
char *str = "XiyouLinux Group 2022";
char *temp = convert(str);
puts(temp);
}
char *convert(const char *s);
int main(void) {
char *str = "XiyouLinux Group 2022";
char *temp = convert(str);
puts(temp);
free(temp);//释放空间
}
char *convert(const char *s)
{
char* a=(char*)malloc(sizeof(char)*22);//分配空间
strcpy(a,s);//复制字符串
for(int i=0;i<40;i++)//翻转大小写
{
if(a[i]>='a'&&a[i]<='z')
{
a[i]-=32;
}
else if(a[i]>='A'&&a[i]<='Z')
{
a[i]+=32;
}
}
return a;
}
输出为:
xIYOUlINUX gROUP 2022
10. 交换礼物的方式
- 请判断下面的三种
Swap
的正误,分别分析他们的优缺点。- 你知道这里的
do {...} while(0)
的作用吗?- 你还有其他的方式实现
Swap
功能吗?
#define Swap1(a, b, t) \
do { \
t = a; \
a = b; \
b = t; \
} while (0)
#define Swap2(a, b) \
do { \
int t = a; \
a = b; \
b = t; \
} while (0)
void Swap3(int a, int b) {
int t = a;
a = b;
b = t;
}
Swap1和Swap2正确,Swap3错误,只交换了值,没有改变变量的值,要用指针来做。
使用 do-while 结构是为了确保代码块中的语句能够正确地被处理,同时又不会被外部语句块所影响。使用 do-while 结构的好处是,在使用宏定义时可以像一个独立的语句一样使用,而不会产生语法上的问题。
这里的do-while是定义宏时为了确保其中的代码块在使用时能像一个独立的语句一样被执行,如果直接使用大括号的话,在宏定义替换后会在大括号后有一个分号,但如果使用do-while的话,就能编译成功了。
11. 据说有个东西叫参数
你知道
argc
和argv
的含义吗?请解释下面的程序。你能在不使用argc
的前提下,完成对argv
的遍历吗?
int main(int argc, char *argv[]) {
printf("argc = %d\n", argc);
for (int i = 0; i < argc; i++)
printf("%s\n", argv[i]);
}
输出为:
argc = 1
./a.out
第一个参数argc指的的是命令行参数的个数,它至少为一,因为第一个参数始终是程序的名称第二个参数argv是一个二维的char型指针,存放命令行参数字符串,并且argv[0]一定是程序的路径及名称。
#include<stdio.h>
int main(int argc, char*argv[])
{
int i=0;
while(argv[i]!=NULL)
printf("%s\n",argv[i++]);
return 0;
}
输出为:
./a.out
12. 人去楼空
这段代码有是否存在错误?谈一谈静态变量与其他变量的异同。
int *func1(void) {
static int n = 0;
n = 1;
return &n;
}
int *func2(void) {
int *p = (int *)malloc(sizeof(int));
*p = 3;
return p;
}
int *func3(void) {
int n = 4;
return &n;
}
int main(void) {
*func1() = 4;
*func2() = 5;
*func3() = 6;
}
错误在于fun3函数中返回&n,因为n是一个局部变量,离开函数就会被释放,所以返回它的地址没有意义。
对于局部变量,static可以改变其存储位置,将其从栈区转移到静态区,使其生命周期变长,从原来的在其所在的函数内存活变为在整个程序中存活,但是这个局部变量的作用域没有改变,还是只能在那个局部的范围内使用。
对于全局变量,static可将其的外部链接属性变为内部链接属性,其他源文件不得再使用这个全局变量。
对于函数,与全局变量类似,static可将其的外部链接属性变为内部链接属性,其他源文件不得再使用这个函数。
对于指针,要看这个指针变量是局部变量还是全局变量。
13. 奇怪的输出
int main(void) {
int data[] = {0x636c6557, 0x20656d6f, 0x78206f74,
0x756f7969, 0x6e694c20, 0x67207875,
0x70756f72, 0x32303220, 0x00000a31};
puts((const char*)data);
}
输出为:
Welcome to xiyou Linux group 2021
这道题考察了大小端的知识点,x86电脑是小端存储,低位数据存放在低位地址,所以就会从57开始输出,而十六进制的57刚好是W,又到00也就是\0时停止。
14. 请谈谈对从「C语言文件到可执行文件」的过程的理解
预处理->编译->汇编->链接
预处理会展开头文件,去掉注释,替换#define
编译会把c语言代码转换为汇编语言。
汇编把汇编代码转换为机器语言。
链接生成可执行文件。
15. (选做) 堆和栈
你了解程序中的栈和堆吗?它们在使用上有什么区别呢?请简要说明。
内存分为很多块区域,有栈区,用来存放局部变量和形式参数;有堆区,用来存放动态内存分配的内存;有静态区,用来存放静态变量和全局变量;有常量区,用来存放常量;有代码区,用来存放代码的二进制指令。
16. (选做) 多文件
一个程序在不使用任何头文件的情况下,如何使用另一个文件中的函数。
17. (选做) GNU/Linux
与文件
- 你知道如何在
GNU/Linux
下如何使用命令行创建文件与文件夹吗?- 你知道
GNU/Linux
下的命令ls 的每一列的含义吗?- 你知道
GNU/Linux
下文件的访问时间、修改时间、创建时间如何查看吗?并简单说说他们的区别。
恭喜你做完了整套面试题,快来参加西邮Linux兴趣小组的面试吧!
西邮 Linux兴趣小组面试时间:
2021年10月25日至2021年10月31日晚8点。
听说面试来的早一点更能获得学长学姐的好感哦。我们在FZ103等你!