C语言---Xiyou
前言
🐼个人简介:一名在校的大一新生。
🎉个人wechat:lufei13308378206。
🚀本文由秋落风声,首发CSDN,此篇参考了本校Linux实验室的同学们的题解,在此表示感谢。
🔥如果有知识性的错误,或者逻辑上不清楚,欢迎加我的WeChat,来共同探讨,也期待您提供的宝贵建议。CSDN后台私信我也可以哦。
——————————————————————————————
今日(24_4_18)找书时偶然翻到这份面试题(还有其它实验室的),我能看懂了(大概)。我认为这值得写一份清晰的题解。以下仅供个人解答
这套题考察了大量C语言基础知识,重点囊括二进制,数组,函数,结构体和指针,只能作为C语言的巩固复习有限参考。比如这里不(很少)涉及文件,预处理,编译 链接的有关知识。
适用对象
1. 想扎实巩固C语言的朋友们,这套题可以有效检验你们的学习成果。
2.想面试本校的Linux实验室的新大一同学们,也许你从0开始,不要畏难,也许题解你看得很吃力。我会把每道题考点罗列一遍,请抓紧时间学习。加油吧!
我使用的是windows系统下的vs2022,
可能与Linux系统中的编译器gcc有差别。
🔥
① 本题目只作为西邮 Linux 兴趣小组 2023 纳新面试的有限参考。 ② 为节省版面,本试题的程序源码省去了 #include 指令。 ③ 本试题中的程序源码仅用于考察 C 语言基础,不应当作为 C 语言「代码风格」的范例。 ④ 所有题目编译并运行于 x86_64 GNU/Linux 环境。0.鼠鼠我啊,要被祸害了
有 1000 瓶水,其中有 一瓶 有毒,小白鼠只要尝一点带毒的水, 24 小时后就会准时死亡。 至少要多少只小白鼠才能在 24 小时内鉴别出哪瓶水有毒? 知道二进制吧?二进制由1,0组成。 1表示 turn on(打开) 0表示 turn off(关闭) 理解一: 0和1表示两种相反的状态,即这里0可以表示死掉了,1表示活着。对于一只小白鼠 喝完水,它有两种状态 即0(死了) 1(活了),那么这种结果就可以说明几瓶水有毒?
2瓶,很好理解是吧,小鼠喝了其中一瓶,根据最后的状态,可以确定两瓶总有一瓶有毒。
现在有两只小白鼠,对应4种结果 即00 ,01,10,11。两只都死了(00),两只都活着(11),一死一活(10 01)。可以判断几瓶呢?
4瓶,嗯,还是写一下理解吧。
设四瓶水 分别为A ,B ,C, D。小鼠a,b。
a:A,B ;
b:B,C;
D瓶都不喝。
🆗相信你懂了。
三只小鼠可以证明8瓶水有毒。4只小鼠可以证明16……巴拉巴拉。
因此其实这道题就是解方程 2^n=1000;由于2^9=512,所以实际要至少10只小鼠,最多可以检测1024瓶水。
理解二:
看不明白?呃,这样吧。
1000瓶水,二进制中0表示没喝,1表示喝了。要表示1000中结果,最少要10个二进制位。2^10=1024>1000。每小鼠按照二进制编号来喝水,根据最后结果死掉的小鼠都喝的就是有毒的。
可怜的小白鼠。
考察二进制
1.先预测一下~
按照函数要求输入自己的姓名试试~char* welcome() { // 请你返回自己的姓名 } int main(void) { char* a = welcome(); printf("Hi, 我相信 %s 可以面试成功!\n", a); return 0; }
都说让你返回姓名了,就返回一个字符串呗。
char* welcome() { return "eren";//后面有解释 } int main(void) { char* a = welcome();//创建char* 的指针变量接受返回值 printf("Hi, 我相信 %s 可以面试成功!\n", a);//打印字符串。 return 0; }
要解释一下吗?
welcome 的返回类型是字符指针(char* ),意思是要return 地址。
这里return "eren" ,看起来return 的不是字符串吗?
这个其实是常量字符串,常量字符串是字符数组,可以看作首元素的地址。这里return的就是其首元素的地址。
可以这样写吗?
char* welcome() { char* arr = "eren";//创建临时变量存储常量字符串的地址 return arr;//返回这个指针。 } int main(void) { char* a = welcome(); printf("Hi, 我相信 %s 可以面试成功!\n", a); return 0; }
试运行两种代码,看一下结果!
char* welcome() { char arr[] = "eren";//字符数组 return arr;//返回数组名,数组名即首元素的地址 } int main(void) { char* a = welcome(); printf("Hi, 我相信 %s 可以面试成功!\n", a); return 0; }
第一种方法结果是对的,但我不建议。
第二种写法是错的。
我说得简单一点,你认为两个“eren”一样吗?
也许你听过栈,堆,静态区。
无论是char arr[],char* arr都是welcome函数内部创建的,为局部变量,存储在栈区,函数结束就销毁了。
常量字符串存储在静态区,变量常量在程序运行期间会一直存在,不会释放,且变量常量在其中只有一份拷贝,不会出现相同的变量和常量的不同拷贝。
一之所以对,只不过是把常量字符串地址传回去,常量字符串一直都在就能打印。
二不对,字符数组在栈区上,这个“eren"拷贝了一份在数组上。出函数,内存回收,数据就不在了。
不妨自己调试观察一下。
前面变量加static修饰,或者改成全局变量就可以了。
仅展示第二种运行结果。
2.欢迎来到 Linux 兴趣小组
有趣的输出,为什么会这样子呢~nt main(void) { char* ptr0 = "Welcome to Xiyou Linux!"; char ptr1[] = "Welcome to Xiyou Linux!"; if (*ptr0 == *ptr1) { printf("%d\n", printf("Hello, Linux Group - 2%d", printf(""))); } int diff = ptr0 - ptr1; printf("Pointer Difference: %d\n", diff); }
1.常量字符串和字符数组中字符串在内存中存储。
2.指针解引用
3.函数递归。
4.printf的返回类型
解释一下原因。
自上而下,解读每一行代码
第一个把常量字符串首元素的地址存储在字符指针,存储在静态区。
第二个是字符数组,这里是字符串,存储在栈区。
if语句内指针解引用 *ptr0指向常量字符串的‘w'。*ptr1,(一维)数组数组名即是首元素的地址,*ptr1是字符数组(字符串)的‘w',都是字符’w',执行if下面的语句。
最外面的printf的第一个参数是格式字符串,里面占位符%d , 换行符\n,第二个参数是另一个printf的语句,这个表达式的返回结果是什么呢?
printf的返回值是打印的字符总数。返回类型是int。
继续看第二个printf("Hello, Linux Group - 2%d", printf("")),格式字符串,和又一个printf。
再看第三个printf,总算简单了。printf("")先打印一下空字符串(打印个寂寞),返回值为0。(printf打印字符串打印\0之前的字符,并返回\0的字符总数)。自己查一下printf。
第二个等价于printf("Hello, Linux Group - 2%d",0),先打印,结果为Hello, Linux Group - 20
并返回打印字符个数23个。
第一个等价于printf("%d",23),打印23,返回值被忽略。
按照打印顺序 Hello, Linux Group - 2023 就是打印结果啦。
从递归来看,先递推到终点,再逐步回归。
其实栈的应用,栈的特点:先进后出,后进先出。
最后两个指针(地址)进行减法,不可以这样操作,因为它们不是在同一块空间,内存块都不同,结果为随机值(每次程序运行结果不同)。不是0,不是0,不是0!
🆗
那么今年就是2024了,懂我意思吧,哈哈 !
3.一切都翻倍了吗
① 请尝试解释一下程序的输出。 ② 请谈谈对 sizeof () 和 strlen () 的理解吧。 ③ 什么是 sprintf () ,它的参数以及返回值又是什么呢?int main(void) { char arr[] = { 'L', 'i', 'n', 'u', 'x', '\0', '!' }, str[20]; short num = 520; int num2 = 1314; printf("%zu\t%zu\t%zu\n", sizeof(*&arr), sizeof(arr + 0), sizeof(num = num2 + 4)); printf("%d\n", sprintf(str, "0x%x", num) == num); printf("%zu\t%zu\n", strlen(&str[0] + 1), strlen(arr + 0)); }
解释一下程序输出,那先看看程序输出吧。
1. sizeof()计算数据类型的大小(单位字节),sizeof(int)计算的是int数据类型的大小。
sizeof(变量)本质也是计算变量类型的大小。
sizeof(表达式),计算表达式结果的类型,表达式不运算。
sizeof(函数调用表达式),函数都不调用,计算的是函数返回类型的大小。
2.strlen是专门计算字符串的大小,计算的是字符串'\0'前面的字符个数。
它只能计算字符串吗?
字符数组也适用,但用结束标志'\0'。
前面的考点就是这些。
自上而下分析每个printf和它的对应参数。
1.%zu是针对size_t的占位符,这是一种无符号整型,不了解详情自行查资料。
sizeof(*&arr),这里我提供两种理解,其一,* &均为指针运算符,互为可逆运算,即*&抵消了。等价于sizeof(arr),计算的是整个字符数组的大小,这里是7,(自己数一下)。
其二,先看&arr的类型是什么,是指针数组(char * []),再解引用是字符数组,数组元素是七个,就是(char [7])这个类型的大小。
sizeof(arr+0),arr+0,这里arr是首元素的地址,arr+0还是首元素的地址,类型是(char*),大小为4字节。
sizeof(num = num2 + 4),表达式不进行运算,表达式结果类型是什么?是short类型,注意这里num是short,num2是int。结果为2字节。
2.sprintf函数。
sprintf - C++ Reference (cplusplus.com),如果看不明白下面的,自己网站找。
printf知道吧。参数分别是格式字符串和待打印项。格式字符串有n个占位符,printf就有n+1个参数。
那么sprintf有什么区别呢?
sprintf第一个参数是char* str,其余参数跟printf一模一样,功能是指定格式化后的字符串,拷贝到str所在的空间。(不打印了)
返回类型是int,返回写入数组的字符总数。
分析此处,先格式化字符串,以%x(16进制)将520转换到字符串,520(10进制)=208(16进制),字符串"0x208"拷贝到str数组上。返回字符串的字符总数('\0'之前),返回值是5,那个判断真假的式子为假(0),所以打印结果为0。
3.第三个printf,strlen(&str[0] + 1),从str[1]开始到'\0'之前的字符个数。前面分析了str存了
"0x208",这里就是从'x'开始到'\0'之前的(已标注),这里一共4个,与打印结果相符。
strlen(arr+0),从arr数组首元素开始到'\0'之前的元素。char arr[] = { 'L', 'i', 'n', 'u', 'x', '\0', '!' };即Linux,结果为5。
考察
1.sizeof运算符的理解;
2.strlen函数的参数,返回类型,功能;
3.sprintf函数参数功能返回类型;
4.进制转换(10进制转二进制转16进制);
补充:为什么不把sizeof这个运算符设计成函数?
1.从代码上来看更加简洁。
2.sizeof可以作用于类型,比函数灵活。
3.sizeof可以计算复杂类型的大小,比函数灵活。
4.函数调用会在栈上申请空间(函数栈帧),运行时会加大开销。
4.奇怪的输出
程序的输出结果是什么?解释一下为什么出现该结果吧~int main(void) { char a = 64 & 127; char b = 64 ^ 127; char c = -64 >> 6; char ch = a + b - c; printf("a = %d b = %d c = %d\n", a, b, c); printf("ch = %d\n", ch); }
又要看输出结果分析吗?,先上结果吧。
掌握一下位运算就搞定了,还有进制转换。
64,127在默认为整型(int),先转二进制补码,正整数二进制原码=反码=补码。
64:00000000 00000000 00000000 01000000
127:00000000 00000000 00000000 01111111
按位与(&)运算,任何位*0为0,乘一就为它本身。
这里相应有0变成0,没0的变成1.
64&127 =64;
按位异或 (^)相同为0,相异为一
结果为 00000000 00000000 00000000 00111111(63的二进制)
64^127 =63;
移位操作符(这里☞右移操作符)
c=-64>>6,由于大多编译器采用的是算术右移,-64>>6=-1。
所以前面以%d(10进制有符号整数(signed int))打印的结果就理解了。
因为字符类型本质是整型,如果把右边当成整型即64+63-(-1);则ch是128整型。
但由于char为有符号的char,取值范围为-128~127,取不到128的整型。见图
127-(-1)后,是-128的整型,这里规定10000000是-128的补码。所以ch的打印结果是-128。
分析完毕。
考察
1.位运算符,(尤其注意移位操作符的算术右移和逻辑右移);
2.整数在内存中存储:二进制原码反码补码;
3.signed char的数据溢出,数据范围;
4.进制转换(十进制转二进制);
5.乍一看就不想看的函数
“人们常说互联网凛冬已至,要提高自己的竞争力,可我怎么卷都卷不过别人,只好用一些奇技淫 巧让我的代码变得高深莫测。” 这个 func () 函数的功能是什么?是如何实现的?int func(int a, int b) { if (!a) return b;//a=0时,递归的终止条件。 return func((a & b) INT_MAX->INT_MIN--->0,结束循环。i++ && j++ || k++;
这个考察一个逻辑运算符的知识点:
逻辑运算符总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是 保证的。如果左侧符合逻辑运算符的条件,就不执行右边了。 就拿这个举例,后置自增运算符,先使用,再++。-1为真,使用完自增到0;执行逻辑与右侧0为假,则整体i++ && j++,结果为假(0),i,j都++了。此时i==0,j==1,由于左侧为假,逻辑或推不出结果,要执行||右侧的表达式,k=1,先使用,右侧为真,再自增,最后k=2。 最终三个自增表达式都执行了。 所以最终打印结果为 i=0,j=1,k=2;10.到底是不是 TWO
#define CAL(a) a * a * a #define MAGIC_CAL(a, b) CAL(a) + CAL(b) int main(void) { int nums = 1; if (16 / CAL(2) == 2) { printf("I'm TWO(ノ>ω