C语言面试题总结

1、预处理阶段都做了什么?

①宏定义指令
②条件编译指令
③处理头文件包含
④特殊符号以及注释

2、请你来说一下一个C++源文件从文本到可执行文件经历的过程

①预处理 gcc -E a.c -o a.i
②编译 gcc -S a.c 生成a.s文件
③汇编 gcc -c a.c 生成a.o文件
④链接 gcc a.c 生成可执行文件a.out

3、请你来说一下C++/C的内存分配?

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间
静态区域:
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
动态区域:
堆区:调用new/malloc函数时在堆区动态分配内存,同时调用delete/free来手动释放申请的内存。
文件映射区:存储动态链接库以及调用mmap函数进行的文件映射
:使用栈空间存储函数的返回地址、参数、局部变量、返回值

4、说一下static关键字的作用?

全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共同使用
类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

5、说一下const关键字的作用?

const是一个C语言的关键字,它限定一个变量不允许被改变。使用const在一定程度上可以提高程序的健壮性,另外,在观看别人代码的时候,清晰理解const所起的作用,对理解对方的程序也有一些帮助。但是在C语言中的const并不是真正的”const“,可以通过指针去修改const修饰的变量,C++中的const才是真正的const,不可以用指针修改,且必须在定义时进行初始化

int const *pi; //这是一个指向整形常量的指针,可以修改指针的值,但是不能修改它所指向的值.
int *const pi; //这个是一个指向整形的常量指针,可以修改它所指向的值,但是不能修改指针的值。
int const *const pi;//这就是上边两种的综合了,都不可以修改.

在C++中:

①const修饰数据成员

const 修饰数据成员,称为常数据成员,可能被普通成员函数和常成员函数来使用,不可以更改。必须初始化,可以在类中,也就是在类中定义这个变量的同是初始化(不推荐),或初始化参数列表中(这是在类对象生成之前唯一一次改变 const 成员的值的机会了)。不可以在构造器中初始化,只能是初始化参数列表中初始化。否则报错。

②const修饰成员函数

const修饰成员函数承诺在本函数内部不会修改类内的数据成员,为此,也只能调用承诺不会改变成员的其它 const 成员函数,而不能调用其它非 const 成员函数。const 修饰函数放在,声明之后,实现体之前,大概也没有别的地方可以放了。

void dis() const

③C++中的static const 类型

如果一个类的成员,既要实现共享,又要实现不可改变,那就用 static const 组合模式来修饰。修饰成员函数,格式并无二异,修饰数据成员,必须要类内部初始化。

6、说一下extern关键字的作用?

①函数的声明extern关键词是可有可无的,因为函数本身不加修饰的话就是extern。但是引用的时候一样需要声明的。

②全局变量在外部使用声明时,extern关键字是必须的,如果变量没有extern修饰且没有显式的初始化,同样成为变量的定义,因此此时必须加extern,而编译器在此标记存储空间在执行时加载内并初始化为0。而局部变量的声明不能有extern的修饰,且局部变量在运行时才在堆栈部分分配内存。

③全局变量或函数本质上讲没有区别,函数名是指向函数二进制块开头处的指针。而全局变量是在函数外部声明的变量。函数名也在函数外,因此函数也是全局的。

在C++中还有:C++调用C函数需要extern “C”,因为C语言没有函数重载。C++ 既然完全兼容 C 语言,那么就面临着,完全兼容 C 的类库。由.c 文件的生成的库文件中函数名,并没有发生 namemangling 行为,而我们在包含.c 文件所对应的.h 文件时,.h文件要发生 name manling 行为,因而会在链接的时候发生的错误。C++为了避免上述错误的发生,重载了关键字 extern。只需要在避免 name manling的函数前,加 extern "C",如有多个,则 extern "C"{ } 将函数的声明放入{}中即可。

我们可以在IDE中点进去C语言中的一个库进去看看,都是把整个声明的头文件包含在 extern "C"{ }的大括号当中的,从而实现了兼容C语言。

7、说一下volatile关键字的作用?

volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问

下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1). 一个参数既可以是const还可以是volatile吗?解释为什么。
是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2). 一个指针可以是volatile 吗?解释为什么。
是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3). 下面的函数有什么错误:

int square(volatile int *ptr)
{
   return *ptr * *ptr;
}

下面是答案:

这段代码的有个恶作剧。这段代码的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)
{
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
}

Linux内核中不应该使用volatile,会使效率变慢,内核中使用了很多原语比如互斥锁等,防止了编译器的优化

8、说一下register关键字的作用?

在C语言中的register修饰的变量表示将此变量存储在CPU的寄存器中,由于CPU访问寄存器比访问内存快很多,可以大大提高运算速度。但在使用register时有几点需要注意。

①用register修饰的变量只能是局部变量,不能是全局变量。CPU的寄存器资源有限,因此不可能让一个变量一直占着CPU寄存器
②register变量一定要是CPU可以接受的值。
③不可以用&运算符对register变量进行取址。
④register只是请求寄存器变量,不一定能够成功。

9、请你来回答一下include头文件的顺序以及双引号””和尖括号<>的区别?

双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。

①对于使用双引号包含的头文件,查找头文件路径的顺序为:当前头文件目录,编译器设置的头文件路径(编译器可使用-I显式指定搜索路径),系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

②对于使用尖括号包含的头文件,查找头文件的路径顺序为:编译器设置的头文件路径(编译器可使用-I显式指定搜索路径),系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

10、C语言中的inline内联函数

在c中,为了解决一些频繁调用的小函数大量消耗栈空间或是叫栈内存的问题,特别的引入了inline修饰符,表示为内联函数。inline函数仅仅是一个建议,对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已.为了方便,将内联函数直接声明时就定义,放在头文件中.这样其它文件包含了该头文件,就在每个文件都出现了内联函数的定义.就可以内联了.
inline关键字仅仅是建议编译器做内联展开处理,即是将函数直接嵌入调用程序的主体,省去了调用/返回指令。

内联函数一般都是一个函数大概在几行的时候才会去实现

11、静态链接、动态链接

静态链接:在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个.c文件会形成一个.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接

静态链接库的制作:

①.gcc -c a.c -o a.o //生成二进制文件
②.ar -rc liba.a a.o //打包成静态库

动态链接:

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

下面简单介绍动态链接的过程:假设现在有两个程序program1.o和program2.o,这两者共用同一个库lib.o,假设首先运行程序program1,系统首先加载program1.o,当系统发现program1.o中用到了lib.o,即program1.o依赖于lib.o,那么系统接着加载lib.o,如果program1.o和lib.o还依赖于其他目标文件,则依次全部加载到内存中。当program2运行时,同样的加载program2.o,然后发现program2.o依赖于lib.o,但是此时lib.o已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的lib.o映射到program2的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。

静态链接时地址的重定位,那我们现在就在想动态链接的地址又是如何重定位的呢?虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

动态链接库制作:

gcc -c -fPIC -shared a.c -o liba.so

12、gcc编译选项

-l参数就是用来指定程序要链接的库,-l参数紧接着就是库名,那么库名跟真正的库文件名有什么关系呢?就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的头lib和尾.so去掉就是库名了。放在/lib和/usr/lib和/usr/local/lib里的库直接用-l参数就能链接了,否则应使用-L参数跟着的是库文件所在的目录名。再比如我们把libtest.so放在/aaa/bbb/ccc目录下,那链接参数就是-L /aaa/bbb/ccc -ltest

但是如果头文件不在/usr/include里我们就要用-I参数指定了,比如头文件放在/myinclude目录里,那编译命令行就要加上-I /myinclude参数

13、指针

①指针数组:指针数组,是个数组,里边放的东西都是指针。char *p[2]={"china","linux"};
②数组指针:数组指针,是个指针,指向数组的指针.int (*p)[5]=a; //指向二维数组
③函数指针:int (*p)(int a,char b);//函数指针p指向返回值类型为int的,两个参数为int和char的函数
void *类型可以指向任何一个类型的指针

void (*signal(int sig, void (*func) (int))) (int)signal仍然是一个函数,他返回一个函数指针,这个指针指向的函数没有返回值,只有一个int类型的参数

④字符数组与字符串指针:其中char *p="linux" 这种情况字符串Linux只存在于只读数据段中(rodata),所以p所指向的内容不可以被更改。所以有的字符串操作函数例如char *strcat(char *dst,char const *src);前面的参数dst,需要修改,只能传数组,不能直接传一个字符串,因为字符串不可改变

一定程度上可以认为一级指针与一维数组名等价,二级指针与指针数组名等价,数组指针与二维数组名等价。而二级指针和二维数组名没有一毛钱关系。

*p++:等同于:*p; p += 1;先运算再++

*++p:等同于 p += 1; *p;先++再运算

⑥数组名引用+1 , &a+1,是数组整个大小+1。数组名+1 , a+1是加一个数组元素。int等+1是真正的算术+1

14、结构体大小的计算

①整体所占的内存大小应该是结构中成员类型最大的整数倍,如果此处最大的类型是int_64t,占8个字节。即最后所占字节的总数应该是8的倍数,不足的补足
②数据对齐原则-内存按结构体成员的先后顺序排列,当排到该成员变量时,其前面所有成员已经占用的空间大小必须是该成员类型大小的整数倍,如果不够,则前面的成员占用的空间要补齐,使之成为当前成员类型的整数倍。假设是地址是从0开始,结构体中第一个成员类型char型占一个字节,则内存地址0-1,第二成员从2开始,int型所占内存4个字节根据原则b,第一个成员所占内存补齐4的倍数,故第一个成员所占内存:1+3=4;第二个成员占5-8.第三个成员占8个字节,满足原则b,不需要补齐,占9-16第四个成员占一个字节,占17.故总内存为1+3+4+8+1=17个字节,但根据原则1总字节数需是8的倍数,需将17补齐到24.故此结构体总字节数为:24字节

15、共用体与大小端的判断

共用体占用空间的大小取决于类型长度最大的,共用体变量的地址和它的各成员的地址都是同一地址。同一个内存段可以用来存放几种不同类型的成员,但在每一瞬时只能存放其中一种,而不是同时存放几种。共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用。

所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;所谓的小端模式,是指数据的低位保存在内存的低地址中,而数 据的高位保存在内存的高地址中。

判断代码:用联合体判断大小端,大端返回0,小端返回1

typedef union w
{
     int a;
     char b; 
}u;
int judge()
{
    u test;
    test.a = 1;
    if(test.b == 1)
    {
           return 1;
    }
    else
    {
        return 0;
    }
}

16、strlen求字符串长度

strlen求字符串长度为字符串的长度,不包括"0"

17、C语言操作寄存器

*((uint32 volatile *)(reg_addres)) = value;将地址强制转化为uint32类型的指针,再解引用

18、文件操作

“r” 表示只读,“w” 表示只写,“rw” 表示读写,“a” 表示追加写入,b表示二进制文件,t表示文本文件,默认不写就是文本文件。

二进制读写:fread、fwrite

字符读写:fputc、fgetc、fgets、fputs

格式化读写:fprintf、fscanf

文件指针:rewind、fseek

19、gcc工具链

objdump:反汇编使用 参数:-d

readelf 显示elf文件信息

objcopy 把目标文件的内容从一种文件格式复制到另一种格式的目标文件中。

strip 放弃所有符号连接,一般应用程序最终都要strip处理

nm 从目标文件列举所有变量

add2line 将地址转换成文件名或行号对,以便调试程序

20、*“当表达式中存在有符号类型和无符号类型时,默认情况下计算的结果将转化为无符号类型”

21写一个“标准”宏,这个宏输入两个参数并返回较小的一个.#与##的作用?

#define MIN(x, y) ((x)<(y)?(x):(y))
#是把宏参数转化为字符串的运算符,##是把两个宏参数连接的运算符。

22、sizeof关键字的作用?

sizeof是在编译阶段处理,且不能被编译为机器码。sizeof的结果等于对象或类型所占的内存字节数。sizeof的返回值类型为size_t。
变量:int a; sizeof(a)为4;
指针:int *p; sizeof(p)为4;
数组:int b[10]; sizeof(b)为数组的大小,4*10;int c[0]; sizeof(c)等于0
结构体:struct (int a; char ch;)s1; sizeof(s1)为8 与结构体字节对齐有关。
sizeof(void)等于1
sizeof(void *)等于4

23、堆栈溢出一般是由什么原因导致的?

1.没有回收垃圾资源
2.层次太深的递归调用

24、分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。

BOOL :    if ( !a ) or if(a)
int :     if ( a == 0)
float :   const EXPRESSION EXP = 0.000001
          if ( a < EXP && a >-EXP)
pointer : if ( a != NULL) or if(a == NULL)

25、指针与数组的区别。

数组是连续的,数组名就是数组的首地址,和一维指针类似。指针变量中存放的内容是一个地址变量,通过指针的解引用就可以访问到那个地址中的内容。
25、指针与数组的区别。

int a[4] = {1, 2, 3, 4};
int *ptr1=(int *)(&a+1);//这里对数组a取地址,加的大小是整个数组的大小。
int *ptr2=(int *)((int)a+1);//这时是地址+1,比如地址是0x2000000,加1之后就是0x20000001
int *ptr3=(int *)(a+1);//这是数组内容大小+1,比如地址是0x2000000,加1之后就是0x20000004

26、int main(){}里面有几个参数,分别代表什么意思。

其中第一个表示参数的个数;第二个参数中argv[0]为自身运行目录路径和程序名,argv[1]指向第一个参数、argv[2]指向第二个参数……

Last modification:February 27th, 2020 at 07:29 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment