C语言经典笔面试题(上)

1、sizeof在什么阶段执行?它是函数还是啥?

编译,单目运算符。

如下代码段会出现什么问题

void main()
{  
    char aa[10];
    printf("%d",strlen(aa));
}

sizeof()和初不初始化没有关系,strlen()和初始化有关,打印结果值未知。

2、指针的指针

下面这段程序会怎样?

void GetMemory(char *p) 
{
    p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}

答案当然是段错误,试图修改指针的值,只能传递指针的地址。

3、什么是大小端?写一个简单的程序判断系统的大小端。

小端:低位字节数据存储在低地址
大端:高位字节数据存储在低地址

例如:int a=0x12345678;(假设a首地址为0x2000)
地址: 0x2000  0x2001  0x2002  0x2003 
值 :  0x12   0x34    0x56   0x78     
这就是大端格式。

基于以上特点,我们不难写出判断程序:

#include <stdio.h>
union{
    unsigned int a;
    char b;
}test;
int main()
{
    test.a = 0x01;
    if(test.b)
        printf("little endian");
    else
        printf("little endian");
    return 0;
}

上面的程序巧妙的利用了联合体共用地址空间的特点,如果是大端,01会被存放在高地处,那么b就不会等于1。

4、用变量a定义

  • 一个整型数 int a;
  • 一个指向整型数的指针 int *a;
  • 一个指向指针的指针,它指向的指针式指向一个整型数 int **a;
  • 一个有10个整型数的数组 int a[10];
  • 一个有10个指针的数组,该指针是指向一个整型数 int *a[10];
  • 一个指向有10个整型数数组的指针 int (*a)[10];
  • 一个指向函数的指针,该函数有一个整型数参数并返回一个整型数 int (*a)(int);
  • 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型数参数并返回一个整型 int (*a[10])(int);
  • 声明一个函数,它接受一个int参数,返回值是一个函数指针,函数指针指向的函数接受一个参数int且返回值是void void (*a(int))(int);

5、位操作

给定一个整型变量a,写两段代码,第一个设置a的bit3,第二个清除a的bit,在以上两个操作中,要保持其它位不变。

#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{
   a |= BIT3;
}
void clear_bit3(void)
{
   a &= ~BIT3;
}

6、符号转换

int main(void)
{
  unsigned int a = 6;
  int b = -20;
  char c;
  (a+b>6)?(c=1):(c=0);
  return 0;
}

当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。

7、typedef和define的区别

typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。思考一下下面的例子:

#define dPS struct s*
typedef struct s* tPS;

以上两种情况的意图都是要定义dPS 和 tPS 为结构体指针类型。哪种方法更好呢?为什么?

这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:

dPS p1,p2;
tPS p3,p4;

第一个扩展为

struct s * p1, p2;

上面的代码定义p1为一个指向结构体的指针,p2为一个实际的结构体,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个结构体指针。这也就是我们平常开发时不使用#define来定义新类型(或者同义类型)的原因。

8、C语言程序代码优化方法

  • 满足需要的情况下,使用尽量小的数据类型
  • 求余运算用与实现(a=a%8改为a=a&7)
  • 用移位实现乘除法运算
  • switch语句中根据发生频率来进行case排序,对于if,else if语句也同样。

9、关键字static的作用

1、static局部变量定义时的赋值只在第一次有效,通常可以用于区分初始化调用和后续调用,如:

int fun(...)
{
    static int a = 0;
    if(a = 0)
    {
     //初始化操作
     a = 1;//标记初始化完成
    }
    ...
}

2、static全局变量和函数用于限制该变量或者函数的使用范围为本文件

10、const的使用

const int a;--1
int const a;--2
const int *a;--3
int * const a;--4
int const * a const;--5

前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。

对于2和3我的记忆技巧是,看const和哪一个在前面,比如const在后面,表示修饰的是a,即a是个常数;如果const在前面,表示修饰的是,即a是一个常指针。

合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。const给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。
比如:

int fun(const char *p);

11、评价下面的代码片断

unsigned int compzero = 0xFFFF;`

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;

12、写一个标准宏MIN,输入两个参数返回较小的一个

#define MIN(a,b)((a)>=(b)?(b):(a))

此题考查的是问号表达式和宏的展开问题,该有的括号不能省。

13、嵌入式系统中经常要用到无限循环,你能用C编写多少种死循环呢?

这个问题有几个解决方案。我首选的方案是:

while(1){
   ...
}

一些程序员更喜欢如下方案:

for(;;){
   ...
}for(;1;){
   ...
}

第三个方案是用goto

Loop:
        ...
    goto Loop;

第三种比较难想到。

14、关键字volatile作用,以及常用场合。

参考阅读:什么时候需要使用volatile关键字

告诉编译器不要随便优化我的代码,在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。常用场合:

  • 一个硬件寄存器
  • 中断中用到的变量
  • 线程之间共享变量

15、宏和函数的优缺点?

(1)、函数调用时,先求出实参表达式的值,然后带入形参。而使用带参数的宏只是进行简单的字符替换。

(2)、函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。

(3)、对函数中的实参和形参都要定义类型,二者的类型要求一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也是无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。

(4)、使用宏次数多时,宏展开后源程序长,因为每次展开一次都使程序增长,而函数调用不使源程序变长。

(5)、宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。

16、register关键字的含义和场合

使用register修饰符有几点限制。

1.register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。

2.因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。

3.它和volatile是截然相反,所以对于一些可能被硬件修改的变量,谨慎使用。

我在ubuntu上做过尝试,编译时加-O2选项,程序会将频繁使用到的变量做类似于寄存器变量的优化。对于一个for循环,将循环变量或者循环内部操作的变量定义成寄存器变量,可以大大提高效率。

17、用typedef简化函数的声明

声明signal函数:

void (signal(int,void()(int)))(int);

使用typedef简化函数声明:

typedef void (*HANDLER)(int);
HANDLER signal(int,HANDLER);

typedef定义了一种类型HANDLER,其是函数指针,指向接受一个int型参数无返回值的函数。typedef在定义回调函数指针类型的时候大有用途。

18、说出(* (void( *)())0)();的含义

参考阅读:我对C语言的理解—指针

从最里层入手,void(* )( )表示一种函数指针类型,回想下

typedef void(* )( ) func

所以简化成

(*(func)0)( );

(类型)表示强制类型转换,(func)0表示将地址0转换成func类型的函数指针。回想下如何调用函数指针指向的函数,即

(*p)();

串起来就是调用了存放在0地址的函数。

19、int strlen(char s[]) 和 int strlen(char *s)是否等价

等价。

更多阅读:关于int *const p,const int *p和int const *p的区别

20、数组外部类型声明陷阱

文件1:

char filename[] = "/ect/passwd" ; 

文件2:

extern char* filename;
filename[2] = 't';

这样会出现段错误。保证一个特定的名称的所有外部定义在每个目标模块中具有相同的类型,是程序员的责任,编译器可能检测不到这种错误。文件2中正确的声明格式是:

extern char filename[];

21、. 、->、[ ]、( )、*的优先级问题

在这里插入图片描述
[ ]、( )、. 、->这四个运算符同级,且是最高优先级,结合性是从左到右。注意此处的()是指函数调用or表达式那个括号,注意与强制类型转换区分。强制类型转换( )和*属于第二优先级,结合性是从右到左。

记住这两点便能解决以下问题:
1、

int *p[10];//[]的优先级比*高,所以p和[]先结合,表明这是个数组。
int (*p)[10];//加了括号改变了优先级,p和*先结合,表明这是个指针。

2、下面是先p,再访问p的成员,还是先访问p->data,再取*?

*p->data;
*p.data;

由于->的优先级比 * 高,所以是先p->data,再取 * 。这个例子告诉我们,如果要先*p,需要加括号,即:

(*p)->data

3、

int *fun(int);//由于()的优先级比*高,所以fun先和括号结合,成为函数。
int (*fun)(int);//括号改变了优先级,*和fun结合成为指针。

4、

struct test *a = (struct test *)p->a;

由于->的优先级比强制类型转换的优先级高,所以上面相当于:

struct test *a = (struct test *)(p->a);

22、交换两个变量的值,不使用第三个变量。即a=3,b=5,交换之后a=5,b=3;

有两种解法, 一种用算术, 一种用^(异或)

a = a + b; b = a - b; a = a - b;

或者:

a = a^b;
b = a^b;
a = a^b;

23、用两个栈实现一个队列的功能。

首先要明白队列是先进先出,而栈是先进后出。一个队列主要就是实现两个操作,入队和出队。设2个栈为A,B, 一开始均为空。

入队: 将新元素push入栈A;
出队:
(1)判断栈B是否为空,如果非空则执行第三步(最开始肯定是空的);
(2)如果为空,则将栈A中所有元素依次pop出并push到栈B(这个操作就将先入后出的顺序颠倒成了符合要求的先进先出)
(3)将栈B的栈顶元素pop出

这样实现的队列入队和出队的平摊复杂度都还是O(1)。

具体实现:
设两个栈分别为stack1和stack2,stack1主要负责“进入”,模拟的是“队尾”;stack2主要负责“弹出”,模拟的是“队头”。具体思路如下:
1、对于“数据入队”操作,只需要将数据压入stack1即可
2、对于“数据出队”操作,若stack2非空,直接弹出栈顶元素;若stack2是空,将stack1中所有元素弹入stack2之后,弹出stack2栈顶元素
在这里插入图片描述
注意,最右边倒回这步操作不需要。

24、如何避免头文件被重复包含?

例如,为避免头文件my_head.h被重复包含,可在其中使用条件编译:

#ifndef _MY_HEAD_H
#define _MY_HEAD_H    /*空宏*/
/*其他语句*/
#endif

25、局部变量能否和全局变量重名?

能,局部会屏蔽全局。

26、int (*a[10])(int)

一个有10个指针的数组,即函数指针数组,该指针指向一个int func(int param)的函数,该函数有一个整型参数并返回一个整型数

27、main函数既然不会被其他函数调用,为什么要返回1

void main()执行完后默认的返回值是void,即不用写return,如果是int main那么后就要跟return 0,或return其他整数值了,main不可以被调用,但它是一个特殊的函数。在main中,C标准认为0表示成功,非0表示错误。具体的值是某中具体的出错信息。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只嵌入式爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值