注:
-
本题目仅作
西邮Linux兴趣小组
2021纳新面试题的有限参考。 -
为节省版面本试题的程序源码中省略了
#include
指令。 -
本试题中的程序源码仅用于考察C语言基础,不应当作为C语言代码风格的范例。
-
题目难度与序号无关。
-
所有题目均假设编译并运行
x86_64 GNU/Linux
环境。 -
Copyright © 2021 西邮Linux兴趣小组, All Rights Reserved.
本试题使用采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。一、大小和长度竟然不是一个意思
`sizeof()`和`strlen()`有什么异同之处?
他们对于不同参数的结果有什么不同?请试举例子说明。
int main(void) {
char s[] = "I love Linux\0\0\0";
int a = sizeof(s);//15
int b = strlen(s);//12
printf("%d %d\n", a, b);
}
1、sizeof 是库里自带的关键字,不需要在预编译string.h,而strlen硬性需要string.h
2、sizeof求的是占用内存的体积大小,strlen求的是字符串的(有效)长度即有多少个字符,到\0为止,因此sizeof可以跟各种数据类型,strlen只能跟char类型。string可被sizeof替换:strlen(arr)=sizeof(arr)/sizeof(arr[0])-n(n为\0个数)
二、箱子的大小和装入物品的顺序有关
`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));
}
//输出均为16,但是若将double置于short和int之间便会变为24
此处考察的是结构体的内存对齐方式
为了提取数据的效率和完整性,结构体的储存会进行内存对齐
即找到结构体中数据类型所占空间最大的成员,其数据类型大小的整数倍为结构体的大小,其他成员数据应当以其数据类型大小为单元进行填入。
e.g:t1
int |
---|
int |
int |
int |
short |
short |
0 |
0 |
double |
double |
double |
double |
double |
double |
double |
double |
故输出16
三、哦,又是函数
想必在高数老师的教导下大家十分熟悉函数这个概念。那么你了解计算机程序设计中的函数吗?请编写一个`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);
}
补全如下:
/*在这里补全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);
}
四、就不能换个变量名吗?
请结合下面的程序,简要谈谈传值和传址的区别。
简要谈谈你对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
传值:是直接将b的值复制一份给a,a的变化不影响b
传址:将b的地址赋值给a,对a解引用后的操作会影响b的值
生命周期:
局部变量:若变量在一对{}内被声明,则在{}的程序运行结束是该变量所获得的空间将被释放,此变量将被销毁。
全局变量:若变量在程序顶部声明,则其在程序开始时获得内存,在程序终止时销毁,可跨文件运行。
static修饰后:静态变量只会被初始化一次,函数调用后保持其值,在程序开始时获得内存,在程序终止时销毁释放内存。
五、套娃真好玩
请说明下面的程序是如何完成求和的?
unsigned sum(unsigned n) { return n ? sum(n - 1) + n : 0; }
int main(void) { printf("%u\n", sum(100)); }//从n加到零
这里考的是递归思想
当n-1不为0时函数返回的是sum(n-1)+n,n-1=0时return 0,函数终止
即n+n-1+n-2+…+1,第一次保留+n第二次保留+n-1以此类推。
六、算不对的算术
void func(void) {
short a = -2;//1000 0000 0000 0010原码
//1111 1111 1111 1110补码
unsigned int b = 1;//0000 0000 0000 0000 0000 0000 0000 0001
b += a;//1111 1111 1111 1111=(-1)10
int c = -1;//1000 0000 0000 0000 0000 0000 0000 0001原码
//1111 1111 1111 1111 1111 1111 1111 1111补码
unsigned short d = c * 256;//相当于c<<8,1111 1111 1111 1111 1111 1111 0000 0000,同时只保留最低两个字节
c <<= 4;//1111 1111 1111 1111 1111 1111 1111 0000
int e = 2;//0000 0000 0000 0000 0000 0000 0000 0010
e = ~e | 6;//1111 1111 1111 1111 1111 1111 1111 1101|0110=1111 1111 1111 1111 1111 1111 1111 1111=-1=0xfffffffff
d = (d & 0xff) + 0x2022;//1111 1111 0000 0000&0000 0000 1111 1111=0,所以相当于直接输出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=Oxf0
因为在内存中数据以补码的形式储存,正数是本身,负数的补码是反码加一,也以补码进行运算,因此以十六进制输出答案如上
~取反,每一位0变1,1变0
<<右移同理左移
|按位或
&按位与
%hx是short类型按十六进制输出
最后将c转换成字符型只有一个字节保留了1111 0000
七、指针和数组的恩怨情仇
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
int main(void) {
int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int(*b)[3] = a;//声明了一个数组指针b,指向一个有三个元素的数组,把数组a的首地址赋值给了b
++b;//b跳到了后续三个元素的地址
b[1][1] = 10;//原来b[1][1]=5,++后=8,又直接赋值了10
int *ptr = (int *)(&a + 1);//这里将a的下一个数组赋值给了指针ptr
printf("%d %d %d \n", a[2][1], **(a + 1), *(ptr - 1));
}//a[2][1]的值通过指针b被修改为10,解引用a[1]的地址得到首元素4,ptr是a下一个地址,因此ptr上一个地址是a[2][2]=9
八、移形还位之术
下面有`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;
}
funco都可以使用,用的是内容
func1也都可以使用,用的是地址
func2第一行出错,常量指针指向的值无法修改
func3第二行出错,指针常量(?)无法修改
func4两行都出错,其地址和指向的都是常量
int const 和const int等效,因为const修饰的是这个int类型,即这个n是一个常态的int类型。
const int *n 修饰的是这个指针指向一个常量,这个常量不可修改但地址可变
int const *n修饰的是指针,这个指针是一个常量,不可修改
九、听说翻转字母大小写不影响英文的阅读?
请编写`convert`函数用来将作为参数的字符串中的大写字母转换为小写字母,将小写字母转换为大写字母。返回转换完成得到的新字符串。
char *convert(const char *s);
int main(void) {
char *str = "XiyouLinux Group 2022";
char *temp = convert(str);
puts(temp);
}
补全函数:
#include<string.h>
#include<stdlib.h>
char *convert(const char *s){
int i,len;
len=strlen(s);
char *arr;
arr=(char *)malloc(sizeof (char)*len);
for(i=0;i<len;i++){
if(s[i]==" ") continue;
if (s[i]<91&&s[i]>64)
{
arr[i]=s[i]+32;
}
else if(s[i]<123&&s[i]>96) arr[i]=s[i]-32;
}
return arr;
}
十、交换礼物的方式
- 请判断下面的三种`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有误
swap1通过中间量传递数据,方便快捷简单
swap2在宏定义中创建参数,t用完即销毁,节省空间
swap3函数体在运行时是将实参的值复制给形参,形参运算完应当返回给实参地址
do {…} while(0):在宏定义中能够使用语句块,实现局部作用域,保证函数运行
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
//即直接引用实参的地址,交换其地址
十一、听说有个东西叫做参数
你知道`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:计算命令行参数个数
argv:访问目录下文件的路径,即文件或者文件夹或者说命令行参数的指针
#include <stdio.h>
int main(int argc, char *argv[]) {
int i = 0;
while (argv[i] != NULL) {
printf("Argument %d: %s\n", i, argv[i]);
i++;
}
return 0;
}//argv中有一个NULL空指针
十二、人去楼空
这段代码有是否存在错误?谈一谈静态变量与其他变量的异同。
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;
}
func1用了static修饰int n,n的空间不会被释放
func2用了malloc也不会主动释放分配好的内存
func3中n是会被释放的,即n没有确定的地址
十三、奇怪的输出
int main(void) {
int data[] = {0x636c6557, 0x20656d6f, 0x78206f74,
0x756f7969, 0x6e694c20, 0x67207875,
0x70756f72, 0x32303220, 0x00000a31};
puts((const char*)data);
}
/*输出*/
Welcome to xiyou Linux group 2021
此处考察的是大小端的储存模式
十四、请谈谈对从「C语言文件到可执行文件」的过程的理解
写好的C语言文件会经历预处理、编译、汇编、链接的过程
1、预处理将预编译的内容处理、展开、替换,删除软件,包含其他文件(把人需要看的删掉)
2、编译将检查语法、语义等,将程序语言转换成汇编语言(翻译、检查)
3、汇编将把汇编语言转换成及其能够识别运算的机器语言(翻译)
4、链接将目标文件与库文件链接,确保函数、变量等引用正确,开辟入口(规划道路)
5、生成可执行文件
十五、堆和栈
栈区:系统自动划分、自动分配内存的区域,速度快,只需压入和弹出数据,类似于弹匣,生命周期受函数限制
堆区:程序员手动分配内存,手动释放内存(可防止内存泄露),生命周期无限制,储存动态分配,空间很大,速度慢因为管理复杂
十六、多文件
一个程序在不使用任何头文件的情况下,如何使用另一个文件中的函数。
extern声明后直接使用
十七、GNU/Linux与文件
你知道如何在 GNU/Linux下如何使用命令行创建文件与文件夹吗?
你知道GNU/Linux下的命令ls 的每一列的含义吗?
你知道GNU/Linux下文件的访问时间、修改时间、创建时间如何查看吗?并简单说说他们的区别。
1、mdkir创建文件和文件夹,多层嵌套则mkdir -p xxx/xxx/xxx
可以通过使用重定向操作符 >
和echo写入内容
echo "Hello, World!" > my_file.txt
2、文件类型和权限列
硬链接数列(Hard Links)(上头有几个和下头有几个目录)
所有者列(Owner)
所属组列(Owner)
文件大小列(File Size)
修改时间列(Modification Time)
文件名列(File Name)