西邮Linux兴趣小组2021纳新面试题题解

西邮Linux兴趣小组2021纳新面试题题解

  • 本题目仅作西邮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";
    int a = sizeof(s);
    int b = strlen(s);
    printf("%d %d\n", a, b);
}
  1. sizeof 是一种单目运算符,它以字节形式给出参数的存储大小,其参数可以为多种类型,如基本数据类型、结构体、联合体等等。!!!若参数为表达式,并不会进行运算。

    若参数为字符数组,sizeof会统计’\0’所占空间

    更详细的,看这里

  2. strlen 是string库中的一个库函数,其参数必须是一个指向字符串常量或字符数组的指针,用于计算指针指向的字符串的大小,当遇到’\0’停止计算,不会统计’\0’

若参数为一个没有’\0’的字符数组,返回值将会是一个随机值,因为它必须遇到内存中一个随机的’\0’。

2. 箱子的大小和装入物品的顺序有关

test1test2都含有: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));
}

计算结构体内存:

这里存在一个词“对齐数”,默认对齐数由处理器架构和编译器决定。

如有#pragma pack(n)预编译指令,将会把默认对齐数修改为n字节。

  • 结构体中第一个成员的偏移量为0,排在后面的成员的偏移量必须是该成员长度的整数倍,否则在前面进行补充对齐。
  • 在所有成员各自对齐后,结构体还要进行一次整体对齐,即总长度必须是 **min(默认对齐数,最长成员的长度)**的整数倍。

在这里默认对齐数为8

下面我们分别计算t1,t2的内存大小:

struct test1 t1{
    int a;//sizeof(int)=4
    short b;//sizeof(short)=2
    double c;//sizeof(double)=8,偏移量为6,补充对齐2
};//sizeof(t1)=4+2+2+8=16
struct test2 t2{
    short b;//sizeof(short)=2
    int a;//sizeof(int)=4,偏移量为2,补充对齐2
    double c;//sizeof(double)=8
};//sizeof(t2)=2+2+4+8=16

这里 sizeof(t1)sizeof(t2) 只是碰巧相等了,一般情况下,定义的顺序不同,结构体的内存大小也会不同。

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>

void func(int arr[][13], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

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, 10, 13);
    return 0;
}

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

下面分析代码:

int ver = 123;//定义全局变量
void func1(int ver) {//传值:修改后的ver的值只在func1中有效
    ver++;
    printf("ver = %d\n", ver);
}
void func2(int *pr) {//传址:将局部变量ver的地址传递给指针pr
    *pr = 1234;//
    printf("*pr = %d\n", *pr);
    pr = 5678;
    printf("ver = %d\n", ver);//ver不是指针,它在这个函数中并没有被定义,因此它的值为全局变量ver的值,即123
}
int main() {
    int a = 0;
    int ver = 1025;
    for (int a = 3; a < 4; a++) {
        static int a = 5;//对a重新赋值
        printf("a = %d\n", a);
        a = ver;
        func1(ver);//ver++并打印
        int ver = 7;
        printf("ver = %d\n", ver);
        func2(&ver);
    }
    printf("a = %d\tver = %d\n", a, ver);
}
  • 传值是指将参数的值复制一份,在函数调用时将这份复制的值传递给函数。这意味着在函数内部对参数的修改不会影响到函数外部的原始值。

    传址是指将参数的内存地址(指针)传递给函数,函数可以通过这个地址访问并修改原始值。

  • 生命周期:变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。

    1. 全局/静态变量生命周期:程序运行期一直存在,从程序开始到程序结束。

    2. 局部变量生命周期:随着函数的结束,即被销毁。

5. 套娃真好玩!

请说明下面的程序是如何完成求和的?

unsigned sum(unsigned n) { return n ? sum(n - 1) + n : 0; }
int main(void) { printf("%u\n", sum(100)); }

unsigned sum(unsigned n) { return n ? sum(n - 1) + n : 0; }:

对函数体进行翻译:

if (n)
    return sum(n - 1) + n;
else
    return 0;

使用递归,即sum函数调用自身,当参数为0的时候函数结束。

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);
}

先了解一下位运算:

符号描述运算规则
&两个位都为1时,结果才为1
|两个位有一个为1,结果就为1
^异或两个位相同时为0,不同时为1
~取反0变为1,1变为0
<<左移<<n:各二进制位全部左移n位,高位丢弃,低位补0
>>右移>>n:各二进制位全部右移n位,对于无符号数,高位补0;有符号数,右移补1

**0x是十六进制数字的前缀,**十六进制每一位从小到大可以为0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F 这16个不同的数,即逢16进1,其中用A,B,C,D,E,F这六个字母来分别表示10,11,12,13,14,15

在printf中,%hh的作用是限定输出格式为8位,即一个字节;

%h的作用是限定输出格式为16位,即两个字节;

了解了这些之后,这道题就迎刃而解啦!

有一篇好东西:C语言格式化输出函数printf详解

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));
}

在这里插入图片描述

  • int(*b)[3] = a;定义了一个指向有3个int类型数据的一维数组的指针,并让它指向数组a的第一行;

  • ++b;是让b指向下一行,即数组a的第二行;

  • b[1][1] = 10;现在的b[1][1]a[2][1],将a[2][1]的值修改为10;

  • int *ptr = (int *)(&a + 1);定义了一个int *型指针,并让它指向数组a后一个元素;

数组名&数组名 的区别:

数组名是指向首个元素的指针,&数组名是指向整个数组的指针

更详细的请看这里

  • printf中**(a + 1)*(*(a+1)+0),即a[1][0];

    *(ptr - 1))a[2][2].

    因此打印结果为10 4 9.

8. 移形换位之术

下面有abc三个变量和4个相似的函数。

  • 你能说出使用这三个变量的值或地址作为参数分别调用这5个函数,在语法上是否正确?
  • 请找出下面的代码中的错误。
  • const intint 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;
}
  • 前两个函数都是没有问题的,下面我们分析一下后三个:

    void func2(const int *n) {//n是常量指针,n指向的值不能改变
        *n += 1;//错误
        n = &a;
    }
    void func3(int *const n) {//n是指针常量,n存放的地址不能改变
        *n += 1;
        n = &a;//错误
    }
    void func4(const int *const n) {//n是指向常量的常量指针,地址与值都不能改变
        *n += 1;//错误
        n = &a;//错误
    }
    
  • const intint const没有区别,都是表示定义了一个int类型的数,且这个数不能改变

  • const int *int const *没有区别,判断const与*结合时是否有区别应根据const与*的先后顺序:

    const*:指针常量,表示指针指向的地址可以变,而指向的值不能变;

    *const:常量指针,表示指针存放的地址不可以变,但是这个地址对应的值是可以改变;

    const * const:指向常量的常量指针,表示指针存放的地址不能改变,指向的值也不能改变。

9. 听说翻转字母大小写不影响英文的阅读?

请编写convert函数用来将作为参数的字符串中的大写字母转换为小写字母,将小写字母转换为大写字母。返回转换完成得到的新字符串。

char *convert(const char *s);
int main(void) {
    char *str = "XiyouLinux Group 2022";
    char *temp = convert(str);
    puts(temp);
}

二话不说,上代码:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
char *convert(const char *s);
int main(void) {
    char *str = "XiyouLinux Group 2022";
    char *temp = convert(str);
    puts(temp);
    free(temp);
    return 0;
}
char *convert(const char *s) {
    int n = strlen(s), i;
    char *result = (char *)malloc((n + 1) * sizeof(char));
    strcpy(result, s);
    for(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;
}          

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 使用宏定义实现a和b的交换,是正确的

    #define Swap1(a, b, t)   //接收3个参数,其中t为中间变量
    do {                 
        t = a;           
        a = b;           
        b = t;           
    } while (0)
    
    #define Swap2(a, b)    //接收2个参数  
    do {                 
        int t = a;  //在这里定义中间变量t,比Swap1更简洁     
        a = b;           
        b = t;           
    } while (0)
    

    Swap3 企图使用函数交换a与b,但这种做法是错误的

    void Swap3(int a, int b) {//a与b是局部变量
        int t = a;
        a = b;
        b = t;
    }//局部变量a与b出了函数就会被销毁,在这里只是交换了形参a和b,并不能实现实参的交换
    
  • do {...} while(0)的作用:

    定义宏,实现局部作用域。

    #define 在预处理的时候进行直接替换!因此我们使用宏定义时,常常得不到我们想要的结果,2022年第13题就给出了一个很好的例子。有人说,那我用{ }将#define 的值括住不久可以了吗?事实上,这也是不行的。代码说明:

    #define Swap2(a, b){                 
        int t = a;       
        a = b;           
        b = t;           
    }
    int main(){
        int a=1,b=2;
        if(1)
        Swap(a,b);//我们习惯在调用宏定义时,后面加上";"
    			  //这里我们进行宏展开,if(1){                 
      			  //                 int t = a;       
      			  //                 a = b;           
     			  //                 b = t;           
                  //                 };                    代码就会变成酱紫,可以看到后面多了一个";"
    }
    

    而如果我们用do {...} while(0)就可以很好的解决这个问题啦。

  • #define Swap(a,b) 
    do {
      	a = a ^ b;
        b = a ^ b;
        a = a ^ b;
    } while (0)
    

11.据说有个东西叫参数

你知道argcargv的含义吗?请解释下面的程序。你能在不使用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和argv是两个用于传递命令行参数的参数。

    argc(argument count)是一个整数,表示传递给程序的命令行参数的数量(包括程序本身)。它至少为1,因为第一个参数始终是程序的名称。

    argv(argument vector)是一个指向字符串数组的指针,每个字符串表示一个命令行参数。第一个元素argv[0]是程序的名称,后续元素argv[1]、argv[2],以此类推,表示其他命令行参数。

  • 因为argv[argc]=NULL,NULL等价于’\0’,因此代码还可以这样写:

    int main(int argc, char *argv[]) {
        printf("argc = %d\n", argc);
        for (int i = 0;argv[i]!='\0'; i++)
            printf("%s\n", argv[i]);
    }
    

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;
}
  • 分析代码:
int *func1(void) {
    static int n = 0;//用static修饰n,延长n的生命周期到整个程序结束
    n = 1;
    return &n;//静态变量n出了函数后不会被销毁,因此这个传址是有效的
}
int *func2(void) {
    int *p = (int *)malloc(sizeof(int));//手动为p分配内存空间,到调用完此函数后才释放内存,这段内存在这段时间是一直存在的
    *p = 3;
    return p;
}
int *func3(void) {
    int n = 4;
    return &n;
}//错误的,因为这里的n是局部变量,它只存在于这个函数,一旦出了函数,n就会被销毁,因此无法完成对n的传址
int main(void) {
    *func1() = 4;
    *func2() = 5;
    *func3() = 6;
    free(p); //切记释放内存,否则会造成内存泄漏
}

13. 奇怪的输出

int main(void) {
    int data[] = {0x636c6557, 0x20656d6f, 0x78206f74,
                  0x756f7969, 0x6e694c20, 0x67207875,
                  0x70756f72, 0x32303220, 0x00000a31};
    puts((const char*)data);
}

要理解这个程序,我们需要了解字节序大小端

详情请看23年题解第12题

或者看这里

14. 请谈谈对从「C语言文件到可执行文件」的过程的理解

在C语言中,将C源代码文件编译成可执行文件通常需要几个步骤。以下是一般的步骤:

1.编写C源代码文件:首先,你需要编写C源代码文件,这些文件包含了你的程序的源代码。这些文件通常以.c为扩展名。
2.编译源代码:接下来,你需要使用C编译器将源代码文件编译成目标代码文件。这可以通过命令行完成,例如:

gcc -o myprogram mysource.c

这个命令将 mysource.c 编译成一个可执行文件 myprogram。编译过程会将C源代码翻译成机器码,并创建一个可执行文件。

3.链接目标文件:在某些情况下,如果你的程序依赖于多个源代码文件,你可能需要将它们链接在一起,创建一个可执行文件。这可以通过编译器自动完成,但你也可以手动执行链接操作。例如:

gcc -o myprogram mysource1.c mysource2.c

这个命令将 mysource1.cmysource2.c 编译成一个可执行文件 myprogram

4.运行可执行文件:一旦你成功生成了可执行文件,你可以运行它。在大多数系统中,你只需输入可执行文件的名称来运行它,例如:

./myprogram

这将执行你的C程序。

恭喜你做完了整套面试题,快来参加西邮Linux兴趣小组的面试吧!

西邮 Linux兴趣小组面试时间:
2021年10月25日至2021年10月31日晚8点。
听说面试来的早一点更能获得学长学姐的好感哦。

我们在FZ103等你!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值