Linux兴趣小组面试题总结
做了近几年的纳新面试题,感觉发现了新大陆,自己的菜emmm…虽然C语言是一门高级语言,但是它很接近底层,能直接操作内存。通过做这几份面试题,我对这句话有了更深的理解。
大小端
对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的地址上,多字节对象都被存储为连续的字节序列,对象的地址为使用字节中最小的地址。
某些机器选择在内存中按照从最低有效字节的顺序到最高有效字节的顺序存储对象,而另一种机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则——最低有效字节在最前面的方式,称为 小端法 。后一种规则——最高有效字节在最前面的方式,称为 大端法 。
小端法 即高地址对高字节。大端法 即低地址对高字节。
比如整形十进制数字:305419896 ,转化为十六进制表示 : 0x12345678 。其中按着十六进制的话,每两位占8个字节。
int main()
{
int a=0x12345678;
printf("%x\n",a);
}
输出结果为12345678;
整型变量a小端模式内存分配如下:
可看出小端模式的读出方式:从字节中最大的地址(首地址+内型字节数-1)到最小的地址。上题中从0x0003到0x0002到0x0001最后是0x0000。
例题 2015.1
int main()
{
int c;
memcpy(&c,"linux",4);
printf("%d\n",c);
return 0;
}
输出结果为:1970170188
1970170188的十六进制值为0x756e694c
小端法内存分配如下:
地址 | 数据 |
---|---|
0x0000 | 0x4c('l’的ASCAII码值) |
0X0001 | 0x69('i’的ASCAII码值) |
0X0002 | 0x6e('n’的ASCAII码值) |
0X0003 | 0x75('u’的ASCAII码值) |
0X0004 | 0x78('x’的ASCAII码值) |
int 占4个字节,所以为0x0000到0x0003
读出:首地址(0x0000)+sizeof(int)-1=0x0003,从高地址到低地址读出,以16进制形式读出:0x756e694c,10进制数为1970170188。
例题 2017.17
struct node
{
char a;
short b;
int c;
}
int main()
{
struct node s;
memset(&s,0,sizeof(s));
s.a=3;
s.b=5;
s.c=7;
struct node *pt=&s;
printf("%d\n",*(int *)pt);
printf("%lld\n",*(long long*)pt);
return 0;
}
输出结果为
327683
30065098755
327683的16进制为0x50003。
小端法内存分配如下:
读出
1.地址(0x0000)+sizeof(int)-1=0x0003,从高地址到低地址读出,以16进制形式读出:0x00050003,10进制数为327683
2.地址(0x0000)+sizeof(long long)=0x0007,从高地址到低地址读出,以16进制形式读出:0x0000000700050003,即0x700050003,10进制数为30065098755.
例题2017.18
4 #define NAME(n) x##n
5 #define PRINTVAL(y,...) printf("x"#
6 int main()
7 {
8 int NAME(1);
9 short *NAME(2)=(short *)&NAME(1);
10 char *NAME(3)=(char *)&NAME(1);
11 NAME(1)=0;
12 *NAME(2)=-1;
13 PRINTVAL(1,"%x\n",NAME(1));
14 PRINTVAL(2,"%x\n",*NAME(2));
15 PRINTVAL(3,"%x\n",*NAME(3));
16 }
输出结果为:ffff
ffffffff
ffffffff
读入:NAME(1)=x1; *NAME(2)=*x2=x1; *NAME(3)=*x3=x1;
x1 为int 类型,小端模式。
指针的类型是指被指向的所占内存大小,与自身无关,x86-64,Linux-gcc编译下指针自身被分配8个字节,short占2位,char占1位。
x2为 short 类型指针,所占内存为0x0000和0x0001
x3为 char 类型指针,所占内存为0x0000。
*x2=-1,-1的补码为0xffff,所以0x0000和0x0001被改为为0xffff。
读出:
int 类型以小端模式读出:0x0000ffff即ffff;
*x2 为short类型,读两个字节(0x0000和0x0001)为0xffff,%x 读出默认4字节,补两个字节,符号扩展添加最高有效位的副本。输出为0xffffffff。
*x3为char类型,读一个字节(0x0000),补三个字节。输出为0xffffffff。
例题2018.9
int main()
{
char n[]={1,0,0,0};
printf("%d\n",*(int *)n);
return 0;
}
输出结果为:1;//结果简单吧,其实并不简单哦*~* !
读入:数组的存放在内存中是从低到高按顺序的,大小端不适用哦。
读出:强制转换为int *类型,采用大小端模式读出。
看到这,你是不是在想,这人是不是傻逼啊。哪有那么复杂~~…
int main()
{
char n[]={0x12,0x34,0x56,0x78};
printf("%x\n",*(int *)n);
return 0;
}
你猜他的输出是什么?
输出结果为:78563412
例题2018.12
int main()
{
FILE *fp=fopen("Linux.txt","wb"); //注意是以二进制形式写入
long long a=0x78756e694c;
fwrite(&a,sizeof(a),1,fp);
fclose(fp);
return 0;
}
文件(运行程序前在源文件所在路径有文件linux.txt)中将会存入"linux"字符串。
读入:longlong 类型占8个字节,内存分配如下表:
地址 | 数据 |
---|---|
0x0000 | 0x4c('l’的ASCAII码值) |
0X0001 | 0x69('i’的ASCAII码值) |
0X0002 | 0x6e('n’的ASCAII码值) |
0X0003 | 0x75('u’的ASCAII码值) |
0X0004 | 0x78('x’的ASCAII码值) |
0X0005 | 0x00 |
0X0006 | 0x00 |
0X0007 | 0x00 |
所以读出为Linux。00为字符串结束符,不打印。
const指针
例题2015.10
int a=3;
const int *p1; //不能改变p1所指向的值
int const *p2; //不能改变p2所指向的值
int *const p3=&a; //不能改变p3的地址,即p3的指向,但能改变a的值。
2017.7
const char *p;
char const *p;
char *const p;
const char *const p; //既不能改变p3所指向的值,也不能改变p3的指向(p3的值);
2018.5
char y[]="XiyouLinuxGroup",x[]="2018";
char *const p1=y;
const char *p2=y;
p1=x; //是错的
p2=x;
*p1='x';
*p2='x'; //是错的
static问题
例题2015.2
static用法包括:
(1)局部变量:程序运行过程中不释放内存。
(2)全局变量和函数:限定只能在本文件中使用,别的文件无法使用;如果不限定,大型文件合并过程中可能出现名称冲突。所以全局变量前加static是一个良好的编程习惯。
例题2015.2
int *func(void) //static局部变量用法
{
static int a=1;
a++;
return &a;
}
int main()
{
int *b;
b=func();
printf("%d\n",*b);
b=func();
printf("%d\n",*b);
return 0;
}
输出结果: 2
3
extern a;
void func()
{
printf("a=%d\n",a);
}
int a=10;
int main()
{
func();
return 0;
}
是不是很好奇我为什么把这个代码放在这里,是不是感觉没啥关系?
当然有关系,如果全局变量或者函数前没有加static关键字,用extern关键字来声明
例题 2017.5
static 全 局 变 量 与 普 通 的 全 局 变 量 有 什 么 区 别?
static 局部变量和普通局部变量有什么区别? static
函数与普通函数有什么区别?
例题 2018.3
static int a=2018;
stativ void func()
{
static int b;
printf("a=%d,b=%d\n",a++,++b);
}
int main()
{
func();
func();
func();
return 0;
}
输出结果为:a=2019,b=1;
a=2019,b=2;
a=2019,b=3;
static 全局变量改变作用域
static 局部变量改变存储期
sizeof与strlen问题
sizeof运算符,在编译时即计算好了 ,参数可以是数组,指针,类型,对象函数。是关键字,在头文件中typedef 为 unsigned int。功能是:获得保证能容纳实现所建立的最大对象的字节大小。
strlen 是函数,要在运行时才计算,参数必须是字符型指针。当数组把它作为参数传入时,实际上数组 就化成指针 了。功能是返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的。该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,知道遇到结束符NULL,返回值不包括NULL .
例题2015.3
void func(char *a)
{
printf("%lu\n",sizeof(a));
printf("%lu\n",strlen(a));
}
int main()
{
char a[]="hello world";
func(a);
printf("%lu\n",sizeof(a));
printf("%lu\n",strlen(a));
return 0;
}
输出结果为:8 //传入函数是指针化了,输出为指针内存大小,linux下指针为8字节。
11 //当传入strlen函数是就化为指针了,所以都为11字节,不包括NULL(字符串结束符)。
12 //获得保证能容纳实现所建立的最大对象的字节大小,在这为数组,包括NULL。
11
例题2015.13
int main()
{
char *str1 = "WelcomeTo\0XiyouLinux";
char str2[]= "WelcomeTo\0XiyouLinux";
printf("%d\n",printf("%s",str1));
printf("%d,%d\n",strlen(str1),strlen(str2));
printf("%d,%d\n",sizeof(str1),sizeof(str2));
}
输出结果为:WelcomeTo9 //9为printf返回值
9,9 //遇到'\0'就结束读取
8,22 //第一个为指针,第二个一直到字符串结束,包括结束符。
例题2017.1
int main()
{
int t=4;
printf("%lu\n",sizeof(t--)); //sizeof运算符,在编译时即计算好了 ,t的值是不变的。
printf("%lu\n",sizeof(ab c\nt\012\xa1*2));
}
输出结果为:4
11
例题2018.2
int main()
{
int a[3][2]={2,0,1,8};
char *str=(char *)malloc(sizeof(char)*20);
strcpy(str,"\0101\\xb2");
printf("%zu\n",sizeof(a));
printf("%zu %d\n",sizeof(a[1][1]=0),a[1][1]);
printf("%zu %zu\n",sizeof(str),strlen(str));
return 0;
}
输出结果为:24 //一个int类型的占4个类型,所以共24个字节
4 8 //sizeof编译阶段即计算好了,所以a[1][1]的值不改变
8 6 //sizeof计算指针大小,strlen计算str数组内字节数'\010','\\','x','b','2'各占一个
字节对齐
结构体中数据成员对齐规则:
1. 结构体(struct)或联合(union) 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员的大小或该成员的子成员大小(只要该成员有子成员)的整数倍开始 .
2.结构体作为成员:如果一个结构体里有某些结构体成员,则结构体成员要从***内部最大元素大小的整数倍地址开始存储***。
3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是内部最大成员的整数倍,不足要补齐。
例题2018.11 (2015.7)
struct icd
{
int a;
char b;
double c;
};
struct cdi
{
char a;
double b;
int c;
};
int main()
{
printf("%zu %zu\n",sizeof(struct icd),sizeof(struct cdi));
return 0;
}
输出结果为:16 24
数组地址含义问题
例题2015.6(二维数组)
int main()
{
int a[3][4];
printf("&a=%p\t&a+1%p\n",&a,&a+1);
printf("a=%p\ta+1%p\n",a,a+1);
printf("&a[0]=%p\t&a[0]+1%p\n",&a[0],&a[0]+1);
printf("a[0]=%p\ta[0]+1%p\n",a[0],a[0]+1);
printf("&a[0][0]=%p\t&a[0][0]+1%p\n",&a[0][0],&a[0][0]+1);
}
输出结果为://地址结果为随机值,但之间是一种必然。
&a=0x7ffc9fb2df40 &a+1=0x7ffc9fb2df70 //int[][],代表整个数组
a=0x7ffc9fb2df40 a+10=x7ffc9fb2df50 //int[]类型 代表一行,a与&a[0]含义相等
&a[0]=0x7ffc9fb2df40 &a[0]+1=0x7ffc9fb2df50
a[0]=0x7ffc9fb2df40 a[0]+1=0x7ffc9fb2df44 //一个int类型,a[0]+1与&a[0][0]+1的含义相等
&a[0][0]=0x7ffc9fb2df40 &a[0][0]=+10x7ffc9fb2df44
观察可知,第一列的地址值是相同的。
例题2015.12(一维数组) (2017.11)(2018.6)
int main()
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1); //&a代表整个数组
printf("%d,%d\n",*(a+1),*(ptr-1));
}
输出结果为:1,5
形参传递
例题2018.13(2015.19)(2017.3)
1.值传递,指函数调用时,为形参分配存储单元,并将实参的值复制到形参,函数调用结束后,形参所占内存单元 被释放,值消失。特点是:形参和实参各占不同的内存单元,函数对形参值的改变不会改变实参的值,这就是参数的单向传递规则。
2.地址传递,是指在函数调用是,将实参数据的存储地址作为参数传递给形参。其特点是:形参和实参占用同样的内存单元,函数中对形参值的改变也会改变实参的值。因此,函数参数的地址传递方式可以实现调用函数与被调函数之间的双向数据传递。
typedef struct a
{
char name[20]; //改之前char *name;或者不改变name的类型,改变name的赋值方式,直接复制。
int num;
}A;
A * func(A *a)
{
a=(A *)malloc(sizeof(A));
strcpy(a-name,"XiyouLinuxGroup");//strcpy只能操作数组,无法操作指针
a->num=2018;
return a; //返回地址,建立形参与实参的传递
}
int main()
{
A *a;
a=func(a);
printf("%s %d\n",a->name,a->num);
return 0;
}
输出结果为:XiyouLinuxGroup 2018
无符号与有符号数相加
例题2015.4(2017.10)
在计算机中,有符号数以二进值补码表示,无符号以原码表示
6的二进数为0000 0000 0000 0110,-20的补码为1111 1111 1110 1100
相加转化为无符号数为1111 1111 1111 1110显而易见大于6的补码1111 1111 1111 1010
void func()
{
unsigned int a=6;
int b=-20;
(a+b>6)?puts(">6"):puts("<6");
}
输出结果为:>6
宏定义参数
例题 2018.10(2015.8)
#是把宏参数变为一个字符串 ##是把两个宏参数连接在一起
如何展开宏参数的规则:在展开宏参数时,如果形参有**#或##不进行宏参数的展开,否则先展开宏参数,再展开当前宏**。
#define YEAR 2018
#define LEVELONE(x) "XiyouLinux"#x"\n"
#define LEVELTWO(x) LEVELONE(x)
#define MULTIPLY(x,y) x*y
int main()
{
int x=MULTIPLY(1+2,3);
printf("%d\n",x);
printf(LEVELONE(YEAR));
printf(LEVELTWO(YEAR));
}
输出结果为:7
XiyouLinuxYEAR
XiyouLinux2018
程序运行过程
预处理过程中,包括注释和宏展开。