西邮Linux兴趣小组2021纳新题

注:
本题目仅作西邮Linux兴趣小组2021纳新面试题的有限参考。
为节省版面本试题的程序源码中省略了#include指令。
本试题中的程序源码仅用于考察C语言基础,不应当作为C语言代码风格的范例。
题目难度与序号无关。
所有题目均假设编译并运行x86_64 GNU/Linux环境。
Copyright © 2021 西邮Linux兴趣小组, All Rights Reserved.
本试题使用采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
————————————————
版权声明:本文为CSDN博主「纸鹿本鹿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链	接及本声明。
原文链接:https://blog.csdn.net/qq_34427004/article/details/127947730

1.大小和长度竟然不是一个意思

sizeof() 和 strlen() 有什么异动之处
他们对于不同参数的结果有什么不同?请试举例子说明。
int main(void)
{
char s[]="I love Linux\0\0\0";//实际的数组为"I love Linux\0\0\0\0"
int a=sizeof(s);//16
int b=strlen(s);//12 读到x后的'\0'停止
printf("%d %d\n",a,b);
}
  • sizeof 是一个运算符号;而strlen 是一个字符串函数,包含在<string.h>的头文件中。
  • strlen返回值为size_t;sizeof返回值是一个整形。
  • sizeof得出的是数据所占内存空间的大小,单位是字节;strlen结果是字符串的长度,遇到‘\0’便停止计数,返回长度。

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));//16
	printf("sizeof(t2): %d\n",sizeof(t2));//16
	}
  • 不同的编译器存在不同的默认对齐数,对应到题中对齐数为结构体中最大类型默认对齐数的较小值。而每个元素的对齐值为本身大小与对齐数的较小值。
  • vs2022默认对齐数为8,结构体test1,test2最大类型均为double,所以结构体t1,t2对齐数均为8.
  • 最终大小你可以理解为你现在有一个箱子,箱子的层数不确定,但每一层可以存放的大小为对齐数。
  • 所以t1, int占四个, 对齐值为4, ,short占两个,对齐值为2,而double需要占8个,但第一层放不下了,所以它放第二层,最终t1的大小为2*8
  • t2,short占两个,int占4个,double在开一层,最终t2的大小也为2*8.
    详细结构体内存对齐知识点

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

*这里构造一个函数来打印数组的每一位值,数组初始化已经完成,**rand()**只是生成随机数的函数,可以不用管。
*由于该函数只实现打印功能,所以无返回值,所以返回类型为void.

#include<stdlib.h>
void func(int arr[10][13])
{
	for(int i=0;i<10;i++)
	{
		for(int j=0;j<13;j++)
			{
				printf("%d ",arr[i][j]);
			}
			printf("\n");
	}
}
  • rand需要引用<stdlib.h>的头文件

4.换个变量名!

在这里插入图片描述

  • 传值是将函数所需的参数值单纯的传给函数,程序进行的函数部分,函数会形成形参拷贝一份实际参数,所以函数的操作并不会改变main函数中的实际参数。
  • 传址是将函数参数的地址传给函数,函数通过地址对main函数中的值进行操作。可以改变main函数中的函数值.
  • 变量的生命周期,这个就需要首先知道变量的分类:1.局部变量 2.全局变量 3.static修饰的静态变量。
  • 1.局部变量的生命周期是仅限该变量定义所在的最一对{}内,出了定义该变量的{}内就会无效,再次调用需重新定义 2.全局变量的生命周期随着整个程序结束而无效 3.静态变量在程序结束后仍然有效内存不释放,地址仍然存在.
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);//这里的赋值是错误的,pr是一个地址,不能把变量的值赋值给地址。
	}
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);
}
  • 开始定义了全局变量ver,并初始化为123.
  • 进入主函数定义了主函数内的局部变量aver,在这会发现全局变量ver与主函数内的局部变量ver名称相同,当全局变量与局部变量名称相同,优先使用局部变量的值.
  • 然后在for循环内a被初始化为4,进入循环变量astatic定义为静态变量5, static定义使得a的值在循环内保留,所以第一个printf打印的值为a=5.
  • 随后将a赋值为1025,将ver值传入func1函数,对ver加一,然后函数fun1内的printf打印的值为ver= 1026,然后回到主函数将ver赋值为7,所以主函数第二个printf打印的值为ver= 7.
  • ver的地址传给函数fun2**,然后 *pr 解引用将ver的值改为1234,紧接着打印ver的值,在函数内部打印ver的值,主函数没有将ver的值传进去所以在这就打印的是全局变量ver的值123.
  • 然后此时a=1025,不满足a<4推出循环.
  • 最后出for循环,执行最后一个printf,在循环内对aver的赋值在出循环后就已经失效,所以打印的是开始定义的aver的值.

5.套娃真好玩!

请说明下面的程序是如何完成求和的?
unsigned sum(unsigned n){return n?sum(n-1)+n:0;}
int main(void){
	printf("%u\n",sum(100));
	}
  • 首先该程序运用了一个三目操作符,x?y:z 该操作符的意思为 判断x是否成立,如果成立,则该程序的结果为y,否则为在。
  • 所以该题为将100传进去,先然后到函数sum判断100100不为0,所以成立进入开始递归(自己调用自己递归),知道n为0时开始累加返回值,从0+一直加到100;该程序实际为一个计算累加和的函数.

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=0x%hhx\t\n",(signed char )c);
	}

这道题主要考察了位运算:

  • 定义的变量在内存中都是以二进制存储的,且存储的是补码.
  • 这就需要了解原码,反码,补码之间的关系,将变量转换为10进制,第一位为符号位1为-- ,0为+ ,然后原码的符号位不变,其他位按位取反得到反码,反码加1得到补码.
  • 整形提升
  • << 左移运算符,将补码左边丢弃,有变补0.
  • ~ 取反操作符,二进制位全部取反
  • & 按位于 二进制未全为1则为1,否则为0
  • | 按位或 二进制为有全为0则为0,否则为1
void func(void){
	short a=-2;        // 1000 0000 0000 0010
	                   // 1111 1111 1111 1101
	                   // 1111 1111 1111 1110 a的补码
	unsigned int b=-1; //11111111 11111111 11111111 11111111 b的补码
	b+=a;//运算的时候是补码进行想加,所以b的补码变为11111111 11111111 11111111 11111101
	//这里运算的时候由于a为short类型,b为无符号整形,所以会发生整形提升。
	int c=-1;          //11111111 111111111 11111111 11111111
	unsigned short d=c*256;//00000000 00000000
	c<<=4;            //所以c的补码11111111 11111111 11111111 11110000
	int e=2;
	e=~e | 6;         //运算完之后11111111 11111111 11111111 11111111 
	d=(d & 0xff)+0x2022;//ff为16进制,f代表15,所以,d&11111111,所以最后的结果为0x2022
	printf("a=0x%hx\tb=0x%x\td=0x%hx\te=0x%x\n",a,b,d,e);//%hx表示输出的是16进制的short类型,%x表示16进制,16进制为二进制四位运算和,
	                  //例如b 11111111 11111111 11111111 11111111
	                  //     前四位1111和为15,对应十六进制为f,所以a的结果为0xfffffffd 
	printf("c=0x%hhx\t\n",(signed char )c);//将c的补码截断输出后8个字节
	}
  • 结果
  • a=0xfffe b=0xfffffffd d=0x2022 e=0xffffffff
    c=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));
	}
数组的下标是从0开始的.
int (*b)[3]定义了一个数组指针,指向了数组a的首元素地址,在这里就是二维数组第0行的地址,++b使得该指针指向了数组的a[1]行。
然后b[1][1]代表 将b为一个数组向后找第一个[1]表示b的下标为1的行,第二个为[1]那一列,即将8变为10。
&a取出的整个数组的地址,所以*ptr指向的是二维数组尾元素的下一个地址,所以最后(ptr-1)指向的是9的地址,解引用打印9,
a[2][1]是数组原先的8,但被替换为10,所以打印10,a+1得到的为数组4这一行的地址,解引用先找到数组,在解引用打印4.

-----[0][1][2]
b-> 1 2 3
++b 4 5 6
b[1] 7 8 9

8.移形换位之术

下面有a、b、c三个变量和 4个相似函数.
  • 你能说出使用这三个变量的值或地址作为参数分别调用这5个函数,在语法上是否正确?
  • 请找出下面的代码中的语法错误。
  • const intint const是否区别?如果有区别,请谈谈他们的区别。
  • const int * 和int const * 是否有区别?如果有区别,请谈谈他们的区别.
    在这里插入图片描述
  • const int 与int const 都表示变量的值不能被改变,可以在观察变量的值是用const 修饰
  • const int 这里const修饰的是int ,所以不能通过解引用来改变值,例如,const intp,那么p=10就是错误的,但p指向的地址是可变的.
  • **int const * **修饰的是地址,那么该变量指向的地址是不变的,例如int const *p,p指向的地址是固定的.可以解引用.
    后两个你可以理解为一个是我给我的杯子贴了一个标签,但我放那里无所谓,另一个则为给一块地方贴了个标签,但我放啥无所谓。
  • 所以fun2,fun3,fun4,有错误,都不能对参数进行赋值操作。
  • fun2不能进行*n,fun3不能进行n=&a,fun4两个都不能进行。

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

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

在这里插入图片描述

#include<stdio.h>
#include<string.h>
#include<ctype.h>
 
char *convert(const char *s)//const限定,所以s的内容不可被修改.
{
	int len=(int)strlen(s);//强制类型转换(int),strlen的返回值为size_t.
	char *word=(char *)malloc(sizeof(char)*(len+1));//创建新的数组来进行翻转和存放操作.
	strcpy(word,s);
	for(int i=0;i<len;i++)
	{
		if(islower(word[i]))//判断小写
		{
			word[i]=toupper(word[i]);
		}
		else if(isupper(word[i]))//判断大写
		{
			word[i]=tolower(word[i]);
		}
	}
	return word;//返回创建的数组地址。
}

int main(void)
{
	char *str="XiyouLinux Group 2022";
	char *temp=convert(str);
	puts(temp);
	free(temp);
}
  • 这里的islower,toupper,isupper,tolower是两组字符函数,用来判断大小写,并转化为大小写,头文件为<ctype.h>.
  • 这里需要在主函数内部加入free(函数)来释放函数内部开辟的动态内存,否则会造成内存泄漏。但这里必须用malloc进行创建,因为函数的参数通过const进行了限制,不能够通过指针对原函数进行改变.

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是通过定义宏来实现,do while 的作用是防止在宏替换对应的代码后会发生语义错误,而得出不是我们想要的结果,1和2的区别是中间变量的创建位置。
  • swap3会发生错误,因为函数内的a,b是实际参数的一份拷贝,不会实际改变真实值,所以达不到交换的效果.
void swap3(int *a,int *b)
{
	int t=*a;
	*a=*b;
	*b=t;
}

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相当于数组的大小,通过 i 的递增,直到到达数组的最后一行,来打印argv数组中的每一个指针对应的字符串.

 不用argc的遍历。
int main(char *argv[])
{
 	while(*argv[i]!=NULL)
 	{
 		printf("%s\n",*argv[i]);
 		i++;
 	}
 	return 0;
 }

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

  • func3函数存在错误,在函数内部创建变量n,并返回n的地址,但在出了函数,n对应的内存就会被释放,所以解引用n就相当于解引用一个野指针,对未知的内存赋值这是非常危险的行为.
  • 而fun1和fun2一个用static 修饰,fun1变为了静态变量,内存不会被释放,fun2用malloc自己开辟了一块空间,程序员自身不释放他是不会释放的.

13.奇怪的输出

int main(void) {
    int data[] = {0x636c6557, 0x20656d6f, 0x78206f74,
                  0x756f7969, 0x6e694c20, 0x67207875,
                  0x70756f72, 0x32303220, 0x00000a31};
    puts((const char*)data);
}
int main(void) {
    int data[] = {0x63  6c 65 57, 0x20 65 6d  6f, 0x78 20 6f 74,
                     c  l   e  w        e  m   o     x     o   t 
                  0x75 6f 79 69, 0x6e 69 4c 20, 0x67 20 78 75,
                     u  n  y  i     n  i  l        g     x  u
                  0x70 75 6f 72, 0x32 30 32 20, 0x00 00 0a 31};
                     p  u  o  r     2  0  2              \0  1   
    puts((const char*)data);
    //输出和存储为什么是倒的,其实不是倒的,这里的63是小地址,在输出的时候对应的为welc中的c,是,低位。这里涉及到了小端存储模式.
}
结果为:
Welcome to xiyou Linux group 2021

大小端存储模式

14.请谈谈对从【c语言文件到可执行文件】的过程的理解.

c语言文件—>预处理(将c语言文件变为 .i 文件)—>编译(将 .i 文件变为 .s 文件)—>汇编(变为 .o 文件)—>exe可执行文件.

  • .c—>.i ,1.将#define定义的宏直接替换,以及#define定义的常量。2.删除.c文件中的注释.3.处理预编译指令,例如#if,#else等,4.进行相应头文件的包含.
  • .i—>.s 进行语法,词法,语义分析和符号汇总.
  • .s—>.o 形成符号表,汇编指令---->二进制指令------>test.o
  • 链接 1.合并段表2.符号表的合并和符号表的重定位.

15.(选做)堆和栈

你了解程序中的栈和堆吗?它们在使用上有什么区别呢?请简要说明

堆:动态内存管理函数开辟的内存就是在堆区开辟的,由我们自己管理,但用完记得用free函数释放,要不然就会内存泄漏.(自己管理越要小心,毕竟想拥有自由就得时刻保持警惕!!)
栈:在这里插入图片描述
参考

16(选做)多文件

一个程序在不使用任何头文件的情况下,如何使用另一个文件中的函数。
  • 使用extern进行声明,然后直接用即可.

17(选做) GNU/Linux与文件

你知道如何在 GNU/Linux下如何使用命令行创建文件与文件夹吗?
你知道GNU/Linux下的命令ls 的每一列的含义吗?
你知道GNU/Linux下文件的访问时间、修改时间、创建时间如何查看吗?并简单说说他们的区别。
  • 创建文件在终端中 touch 然后+对应的文件名 进行创建 ,删除 用 rm +对应的文件名 进行删除.

      	如有错误请联系作者进行改正,共同进步!!!
    
  • 14
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值