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.
- 进入主函数定义了主函数内的局部变量a,ver,在这会发现全局变量ver与主函数内的局部变量ver名称相同,当全局变量与局部变量名称相同,优先使用局部变量的值.
- 然后在for循环内a被初始化为4,进入循环变量a被static定义为静态变量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,在循环内对a与ver的赋值在出循环后就已经失效,所以打印的是开始定义的a, ver的值.
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判断100,100不为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 int 和int 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 +对应的文件名 进行删除.
如有错误请联系作者进行改正,共同进步!!!