西邮Linux小组2019-2021面试试题二三事

目录

1.关于static

  2.)关于printf()返回值

3.sizeof 和strlen()

4.数据的溢出

5.有关运算符

1.)|和||

2.)<<和>>

6.关于const

7.define 宏定义嵌套规律

8.数组,指针,指针数组,数组指针。

1)数组。

2.)指针

3.)  指针数组和数组指针。

9.)关于排序

  1.)冒泡排序(经典基础)

2.插入排序

  3.)快速排序。


1.关于static

首先我们得先明白一个东西,什么是static ?static是静态变量,存储在静态缓存区中,静态静态,并不是说不能它的改变值,不能改变值的量叫常量。 其拥有的值是可变的 ,只是它的值一直都是最新的。说其静态,是因为它不会随着函数的调用和退出而发生变化。即上次调用函数的时候,如果我们给静态变量赋予某个值的话,下次函数调用时,这个值保持不变。举个例子:

#include <stdio.h>
void print(int n);
main()
{
	int n,i;
	printf("次数:\n");
	scanf("%d",&n);
	for(i=0;i<n;i++)
	  print(n);
}
void print(int n)
{
	static a=1;
	a++;
	printf("%d\n",a);
	
}

 正常情况下我们重复引用print()函数,a初始化为1,应该输出的都是2,但实际上,a的值在第一次调用完函数后就更新了,a=2,依次,所以假设n=5,其输出依次为2,3,4,5,6.

static int 不管在函数内还是函数外,都作为一个全局变量可以保存它被修改以后的值。

而 int 则没有这一功能,只有作为全局变量时能保存修改。放在函数内部时,每次调用都用的是一个新的数。

上2019题:

​
3.有如下代码段所示的函数 f,当我们执行该函数时,会产生什么样的输出结果?在同一程序中
多次执行该函数,输出结果是否一致?
void f() {
 static int a = 0;
 int b = 0;
 printf("%d, %d\n", ++a, ++b);
}

​

回到问题就迎刃而解了,每次调用后a的值会更新即保留下来,而b每次调用时又被重新赋为1.

  2.)关于printf()返回值

上题目:

4.下面程序段的输出是什么?请解释该现象并说出与之相关尽可能多的知识。

int main(void) {
 printf("%d\n", printf("Xiyou Linux Group2%d", printf("")));
}

先说答案吧,因为是2019年,所以答案是Xiyou Linux Group2019.

言归正传,我们首先要搞懂printf()返回的是什么东西。其返回一个int值,表示被打印的字符数,比如说:

#include<stdio.h>
main()
{
    int a=123;
	printf("%d\n",printf("%d",a));
 } 

 从内层往外展开,首先看到的是printf("%d",a),打印出123,再把printf("%d",a)看成整体,其返回的是打印的字符数,即123的字符数,为3,输出结果为1233,你可能想打个空格方便看:

#include<stdio.h>
main()
{
    int a=123;
	printf("%d\n",printf("%d ",a));
 } 

 可你会发现其输出变成了123 4,为什么呢?因为在打印里层的的时候你打印的是123 ,多打印了一个空格,printf()的返回值变成了4。

Ok,回到原题,首先打印最里层的print("")注意没有空格,printf没有打印内容,printf()返回0,再到

printf("Xiyou Linux Group2%d"),先打印Xiyou Linux Group20,为什么是0,因为上一次printf()返回的是0,同理,Xiyou Linux Group20返回字符数即19.最后整理输出的为Xiyou Linux Group2019.

3.sizeof 和strlen()

sizeof 和strlne()算是小组比较常考的题型了。他两有啥区别呢?

首先,sizeof 是运算符,而strlen 是一个在<string.h>里的函数。

其次,sizeof 可以用类型做参数,它可以计算int,float,char等等类型,而strlen 只能用 char* 做参数,且必须是以 \0 结尾的。说白了strlen只能用于字符串。

再一,strlen 是一个函数,它用来计算指定字符串 str 的长度,但不包括结束字符(即 ‘'\0’符),而sizeof计算输入的总长度。

啥意思,比如2021的面试题:

 sizeof(s)时,其计算所有的字符,并不会在遇到\0时就停止,在题目中,其值为16(不是15!)

而strlen(s)时,strlen在遇到第一个\0时就结束进程,其返回12;

另外sizeof还有个用途:其可以计算相应类型所占的字节数, 比如sizeof(int)=4;

4.数据的溢出

5.执行下面的代码段,会输出什么?请试着解释其原因,并叙述相关知识。

int main(int argc, char *argv[]) {
 char ch = 255;
 int d = ch + 1;
 printf("%d %d", ch, d);
}

 结果是-1和0,这里需要注意的是char 的范围【-128,,127】,超出范围就会溢出,上溢,从-128往回,下溢从128往回。

  • 取值范围

数据类型的取值范围有一个公式:-2(n-1)~2(n-1)-1
n为整型的内存占用位数,所以int类型32位那么就是-(231)~231 -1即-2147483648~2147483647

数据溢出是怎么回事呢?计算机内部以补码形式进行运算的,所以运算结果为补码,补码转原码的过程为:先减一,然后符号位不变其他位取反。

具体可翻阅  C语言的整型溢出问题 | 酷 壳 - CoolShell

5.有关运算符

1.)|和||

||:或运算,有真必真,例如:t=3>2||4<1; 3>2为真=1,4<1为假=0, 1||0 =1,t=1;

|:按位或,按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1。当参与运算的是负数时,参与两个数均以补码出现。

例如:char x = -2, y = 3;

          char t = (++x) | (y++);

          x=-1=1001;

         y=4=0100; 

        x|y=1101;

         取补码后得1001=-1;

2.)<<和>>

移位运算符。

<<:左移运算符 格式:需要移位的数字<<移位的次数n;

1.无符号:

就是把数值转化为二进制,再进行左移,高位舍去,低位补零。相当于乘上2的N次方。

比如6<<2  就等于6*2的2次方=24;

2有符号:

不确定,因为符号位在左移的过程中丢失了。

<<:右移运算符 格式:需要移位的数字<<移位的次数n;

1.无符号

按二进制形式把所有数字向右移动相应的位数,低位移出(舍弃),高位的空位补0。相当于除以2的n次方

2.有符号

格式:需要移位的数字>>移位的次数n

按二进制形式把所有数字向右移动相应的位数,低位移出(舍弃),正数,高位的空位补0。负数,高位的空位补1.

6.关于const

 const 是 constant 的缩写,本意是不变的,不易改变的意思

const修饰普通的变量时,比如 const int a=3; a就变成了一个不可改变的值,但是可以把a的值赋给其他变量。

当const 修饰指针变量时,要注意:

当const 修饰指针变量时,指针的值不可修改,例如char* const p;

当const 修饰指针所指的内容时,指针的所指的内容不可修改,例如 char const *p;

当const 既修饰指针所指的内容又修饰指针时,两者都不可以修改,例如 const char *const  p;

21年第八题:

 要注意,其实const 会先寻找最靠近它右边的修饰,当右边没东西时,才会看左边。

所以int const 和const int 没有区别,const int *,不能修改的是指针所指向的内容,而int* const ,不能修改的是指针。 const int *const n是两者都不能修改

7.define 宏定义嵌套规律

上题:

#define X a+b
int main(int argc, char *argv[]) {
 int a = 1, b = 1;
 printf("%d\n", X*X);
}

 是不是看起来很简单,x=1+1=2,2*2=4;但实际上答案为3;为什么呢?

这是 C/C++ 里面的宏定义,在编译时,编译器会将其原样展开。

什么是原样展开?

比如说 在上例中define X 1+1; X*X=1+1*1;


 先讲一些宏嵌套的展开规则

  1. 一般的展开规律像函数的参数一样:先展开参数,再分析函数,即由内向外展开
  2. 当宏中有#的时候,不展开参数
  3. 当宏中有##的时候,先展开函数,再分析参数
  4. ##运算符用于将参数连接到一起,预处理过程把出现在##运算符两侧的参数合并成一个符号,注意不是字符串。

   来再做一题:

#define A 2 + 2
#define B 3 + 3
#define C A * B
int main(int argc, char *argv[])
{
 printf("%d\n", C);
 return 0;

C=(2+2)*(3+3)=2+2*3+3=11;

为了避免上诉问题可以添加上括号,如下:

#define s(x) ((x)*(x))

具体宏定义相关可查看:C++宏定义详解 - Boblim - 博客园 (cnblogs.com)

8.数组,指针,指针数组,数组指针。

1)数组。

   其倾向于二维数组(字符串数组是重点),接下来看下20年的第九题:

int main(int argc, char *argv[])
{
 int a[2][3] = {{5, 7}, {5, 2}};
 int b[2][3] = {5, 7, 5, 2};
 int c[2][2] = {{5, 7}, {5, 2}};
 int d[2][2] = {5, 7, 5};
 printf("%d %d\n", a[1][1], b[1][1]);
 printf("%d %d\n", c[1][1], d[1][1]);
 return 0;
}

考察的一个知识点就是未定义的数组元素赋初值为0;

2.)指针

就开始有点晕了,其大概有以下几个基础用法:

p=&a;

&a    表示变量a的地址,&a=100
p    指针变量p的值,p=100=&a
*p    表示指针变量p指向的变量,*p=a=8
&*p    相当于&(*p),&(*p)=&a=100
*&a    相当于*(&a),*(&a)=*p=a=8
&p    表示指针变量p的地址,&p=100
*&p    相当于*(&p),*(&p)=100

看个题:9.执行下列程序段,并输入“Xiyou Linux”(不含引号),那么程序的输出结果是什么?

int main(int argc, char *argv[]) {
 char *p = (char *)malloc(sizeof(char) * 20),
 *q = p;
 scanf("%s %s", p, q);
 printf("%s %s\n", p, q);

是不是理所当然的以为是Xiyou Linux,可实际上输出的结果是Linux Linux,为什么?

第二行代码为20个char类型的值请求内存空间,并设置p指向该位置.
第三行又使得p和q指向了同一个地址.

p先获取Xiyou,当q再获取Linux时,,由于p和q指向相同的地址,p的值也被改变。

3.)  指针数组和数组指针。

什么是指针数组? 形如   int*a[5],其是一个包含五个int*类型的数组,经常用于二维数组当中,在稍后的21年例题中会讲解。

什么是数组指针呢?形如(int*a)[5],其是一个指向含有5个int的数组的指针。

我们很容易把数组指针和指针数组搞混,那么我们如何能够将其正确分辨呢?

   记住 “[]”的优先级比“*”要高。当int*没有括号时,a首先是个数组,其包含5个指针,当int*有括号时,(int*)的优先级高,它首先是个指针,指向数组。

数组和指针相结合的时候就让人头大了。

 看看21年的恩怨情仇~:

(*b)[3]是一个数组指针,【3】是列数,int(*b)[3]=a,使得b指向了a[0][0],

 ++b;  使得b指向了a[1][0];

这个时候的b[1][1]可不是a[1][1]了,而是在把b[0][0]看成a[1][0],再进行寻找,也就是说b[1][1]此时为a[2][1];

int *ptr=(int*)(&a+1);

这句话是不是很奇怪,什么意思呢?

(&a+1)先取变量a的地址,并根据a的地址获得下一个与a同类型的相邻地址。

可以看图体会一下。

 因此在原题中&a+1指向的地址为下一个相邻地址的首地址,也就是a[3][3]后的第一个地址,ptr-1,使得其指向了a[3][3]的最后一个元素的地址,也就是9的地址。

 关于指针和数组的具体内容可浏览:指针,C语言指针完全攻略 (biancheng.net)

9.)关于排序

 几年来的面试题排序的身影经常出现在上,我们应该掌握一些排序算法。

  1.)冒泡排序(经典基础)

普通的冒泡就不再多赘述,让我们看下冒泡的优化——鸡尾酒排序。

鸡尾酒排序(cocktail sort)对冒泡排序进行了优化,使得外层循环一次能找出两个已排序的数(最大和最小),可以理解为”双向“的冒泡排序。

  注:因为鸡尾酒排序外层循环一次能找出两个排序数,故其外层循环次数折半,而内层循环则为两个并列的for循环(分别控制正向和反向)。总的来说,鸡尾酒排序大多数情况下要比冒泡排序效率高。

#include <stdio.h>
 
// can be any swap function. This swap is optimized for numbers.
void swap(int *x, int *y) {
	if(x == y)
		return;
	*x ^= *y;
	*y ^= *x;
	*x ^= *y;
}
void cocktailsort(int *a, size_t n) {
	while(1) {
		// packing two similar loops into one
		char flag;
		size_t start[2] = {1, n - 1},
			   end[2] = {n, 0},
			   inc[2] = {1, -1};
		for(int it = 0; it < 2; ++it) {
			flag = 1;
			for(int i = start[it]; i != end[it]; i += inc[it])
				if(a[i - 1] > a[i]) {
					swap(a + i - 1, a + i);
					flag = 0;
				}
			if(flag)
				return;
		}
	}
}
 
int main(void) {
	int a[] = { 5, -1, 101, -4, 0, 1, 8, 6, 2, 3 };
	size_t n = sizeof(a)/sizeof(a[0]);
 
	cocktailsort(a, n);
	for (size_t i = 0; i < n; ++i)
		printf("%d ", a[i]);
	return 0;
}

2.插入排序

你可以理解为打扑克牌时的理顺牌的过程 ,上个图理解

 

 


    //直接插入排序函数
    void InsertSort(int a[], int n)
    {
        for(int i= 1; i<n; i++){
            if(a[i] < a[i-1]){//若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。
                int j= i-1;
                int x = a[i];
                while(j>-1 && x < a[j]){  //采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
                    a[j+1] = a[j];
                    j--;
                }
                a[j+1] = x;      //插入到正确位置
            }
            print(a,n,i);//打印每次排序后的结果
        }
    }
    、

  3.)快速排序。

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。

(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。

(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

2、快速排序的三个步骤:

(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 "基准"(pivot)

(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大

(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。

​void insertsort(int*a,int begin,int end)
{
   int i,j,t;
   for(i=begin;i<=end;i++)
   for(j=i+1;j<=end;j++)
   if(a[i]>a[j])
   {
     t=a[i];
     a[i]=a[j];
     a[j]=t;
   }
}


  
void quick_sort (int *a,int begin,int end)
  { if(begin>end)
   return ;
   if(end-begin+1<10)
   {
     insertsort(a,begin,end);
     return ;
   }
    int i=begin;
    int j=end;
    int t;
    int tmp=a[begin];
    while(i!=j)
    {
      while(a[j]>=tmp&&j>i)
      j--;
      while(a[i]<=tmp&&j>i)
      i++;
      if(j>i)
      {
        t=a[i];
        a[i]=a[j];
        a[j]=t;
      }
    }
    a[begin]=a[i];
    a[i]=tmp;
    quick_sort(a,begin,i-1);
    quick_sort(a,i+1,end);
  }
 
】
​

 

      这是核心代码,注意到其中有个优化,当n<10时,使用了插入排序,为什么呢,当N的数量小时,快排的速度没有其他的快,但是当N的数值很大时,快排的优势就大大地体现出来了;其他的优化如三数取中法等大家可以自行了解下。

快排可以在快排库中调用(侧面体现了它的效率之高)

怎样调用系统提供的快排库函数:qsort,它包含在<stdlib.h>头文件里,函数一共四个参数,在函数头部加上#include<stdlib.h>,就可以直接调用,并且无需声明。一个典型的qsort的写法如下:
qsort(s,n,sizeof(s[0]),cmp);
  其中第一个参数s是参与排序的数组名(或者也可以理解成开始排序的地址,因为可以写成&s[i]这样的表达式,这个问题下面有说明);第二个参数n是参与排序的元素个数; 第三个参数是单个元素的大小,必须sizeof(s[0])这样的表达式,下面也有说明;第四个参数cmp其实是个函数名字,它是为指导qsort如何进行排序而专门写的一个函数,我们称之为比较函数,其目的是为了告诉qsort要以什么样的方式进行排序(降序?升序?或者按照某个关键字进行排序等)(注:写成cmp只是一个名字,可以随便怎么写),cmp这个函数有形参,和返回值(int型),但是在调用时却不需要给它传递实参进去,直接调用其名字即可,这个函数是专门为qsort开发的一种函数形式,这个函数的典型定义是:
intcmp(const void *a,const void*b);(红色字体是其固定模式,必须这样写,黑色的则是自己随便起的名字)(其函数体详见后面),并且规定这个函数只能返回int型值。

     最后欢迎大家加入西邮Linux兴趣小组!

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值