> 文章列表 > C语言常见面试题汇总

C语言常见面试题汇总

C语言常见面试题汇总

目录

      • 题1:变量的声明和定义有什么区别
      • 题2:写出bool 、int、 float、指针变量与“零值”比较的if语句
      • 题3:sizeof和strlen的区别
      • 题4:C中的malloc和C++中的new有什么区别
      • 题5:说明关键字volatile有什么含意
      • 题6:写一个“标准”宏MIN
      • 题7:设置地址为0x67a9的整型变量的值为0xaa66
      • 题8:链表和数组有什么区别
      • 题9:用变量a定义
      • 题10:引用和指针的区别
      • 题11:static的用法(定义和用途)
      • 题12:const的用法(定义和用途)
      • 题13:内存四区
      • 题14:大小端问题
      • 题15:静态局部变量在什么时候分配内存
      • 题16:什么是不可重入函数和可重入函数
      • 题17:#define宏定义与typedef的区别
      • 值传递改变值这类问题
      • 计算sizeof的值
      • 计算strlen的值
      • char str[]和char *str
      • 数组作为函数参数传递
      • 数组指针和指针数组的问题
      • \\*p++、 (\\*p)++、 \\*++p、 ++\\*p
      • const 二级指针

题1:变量的声明和定义有什么区别

为变量分配地址和存储空间的称为定义,不分配地址的称为声明。
一个变量可以在多个地方声明,但是只在一个地方定义。
加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。

题2:写出bool 、int、 float、指针变量与“零值”比较的if语句

bool型数据:
if( flag ) { A; } else { B; }

int型数据:
if( 0 != flag ) { A; } else { B; }

指针型数:
if( NULL == flag ) { A; } else { B; }

float型数据:
const float EPSINON = 0.000001;
if (( flag >= -EPSINON ) && ( x <= EPSINON )) { A; }

注意:应特别注意在int、指针型变量和“零值”比较的时候,把“零值”放在左边,这样当把“==”误写成“=”时,编译器可以报错,否则这种逻辑错误不容易发现,并且可能导致很严重的后果。

题3:sizeof和strlen的区别

sizeof和strlen有以下区别:
sizeof是一个操作符,strlen是库函数。
sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\\0‘的字符串作参数。
编译器在编译时就计算出了sizeof的结果。而strlen函数必须在运行时才能计算出来。
sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
数组做sizeof的参数不退化,传递给strlen就退化为指针了。

题4:C中的malloc和C++中的new有什么区别

malloc和new有以下不同:

(1)new、delete 是操作符,可以重载,只能在C++中使用。

(2)malloc、free是函数,可以覆盖,C、C++中都可以使用。

(3)new 可以调用对象的构造函数,对应的delete调用相应的析构函数。

(4)malloc仅仅分配内存,free仅仅回收内存,并不执行构造和析构函数

(5)new、delete返回的是某种数据类型指针,malloc、free返回的是void指针。

注意:malloc申请的内存空间要用free释放,而new申请的内存空间要用delete释放,不要混用。因为两者实现的机理不同。

题5:说明关键字volatile有什么含意

volatile的作用:

作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

一个定义为volatile的变量就是说这个变量可能会被意想不到的改变,这样,编译器就不会去随便假设这个变量的值了。精确的说,优化器在用到这个变量的值的时候,必须每次都小心的重新读取这个变量的值,而不是使用保存在寄存器里面的备份。

下面是volatile变量的几个例子:

1). 并行设备的硬件寄存器(如:状态寄存器)

2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

3). 多线程应用中被几个任务共享的变量

题6:写一个“标准”宏MIN

#define min(a,b) ( (a)<=(b)?(a):(b) )
注意:在调用时一定要注意这个宏定义的副作用,如下调用:
min(++*p,x) 展开为 ((++*p)<=(x)?(++*p):(x)。
p指针就自加了两次,违背了MIN的本意。

题7:设置地址为0x67a9的整型变量的值为0xaa66

*(int *)0x67a9 = 0xaa66;

题8:链表和数组有什么区别

数组和链表有以下几点不同:
(1)存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
(2)数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
(3)数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
(4)越界问题:链表不存在越界问题,数组有越界问题。
说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。

题9:用变量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);

题10:引用和指针的区别

(1). 指针是一个实体,而引用仅是个别名;
(2). 引用使用时无需解引用(*),指针需要解引用;
(3). 引用只能在定义时被初始化一次,之后不可变;指针可变;
(4). 引用没有 const,指针有 const,const 的指针不可变;
(5). 引用不能为空,指针可以为空;
(6). “sizeof 引用”得到的是所指向的变量(对象)的大小,
而“sizeof指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
(7). 指针和引用的自增(++)运算意义不一样;

题11:static的用法(定义和用途)

1)用static修饰局部变量:使其变为静态存储方式(静态数据区),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。

2)用static修饰全局变量:使其只在本文件内部有效,而其他文件不可连接或引用该变量。

3)用static修饰函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的(这一点在大工程中很重要很重要,避免很多麻烦,很常见)。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。

题12:const的用法(定义和用途)

const主要用来修饰变量、函数形参和类成员函数:

1)用const修饰常量:定义时就初始化,以后不能更改。

2)用const修饰形参:func(const int a){};该形参在函数里不能改变

3)用const修饰类成员函数:该函数对成员变量只能进行只读操作,就是const类成员函数是不能修改成员变量的数值的。

被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

const int a; 
int const a; 
const int *a; //等于 int const *a;
int * const a; 
int const * const a; //等于 const int* const a;

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

*为分界点,const*的左边时,实际物体值不变
当const*的右边时,指针不变,, 左物右指
这样来记比较方便!! 

题13:内存四区

由C/C++编译的程序占用的内存分为以下几个部分
1.代码区:存放函数体内的二进制代码,有操作系统管理。
2.全局区:存放全局变量,静态变量以及常量。
这部分可以细分为data区和bss区
2.1 .data区
data区里主要存放的是已经初始化的全局变量、静态变量和常量
2.2 .bss区
bss区主要存放的是未初始化的全局变量、静态变量,这些未初始化的数据在程序执行前会自动被系统初始化为0或者NULL
2.3 常量区
常量区是全局区中划分的一个小区域,里面存放的是常量,如const修饰的全局变量、字符串常量等
3.栈区:由编译器自动分配和释放,存放函数参数,局部变量等
4.堆区:由程序员分配释放,若程序员未释放,则程序结束之后,编译器自动释放。
注意:
const修饰的全局变量也储存在常量区;
const修饰的局部变量依然在栈上。
有的分为5个区,即代码区,全局区,常量区,栈区,堆区

#include<stdio.h>
#include<malloc.h>int a = 0; //全局初始化区 .data段char *p1; //全局未初始化区 .bss段void main()
{ int b;// 栈 char s[] = "abc"; //s在栈上,大小4字节。 "abc\\0"为s的初始值,可以不用纠结其存在哪里char *p2; //栈 char *p3 = "123456"; //"123456\\0"在常量区,p3在栈上。 static int c =0; //全局(静态)初始化区 .data段p1 = (char *)malloc(10); p2 = (char *)malloc(20); //分配得来的10和20字节的区域就在堆区。 strcpy(p1, "123456"); //"123456\\0"放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
} 

https://blog.csdn.net/qq_42762607/article/details/110391819

题14:大小端问题

大端:一个多字节整数,数字的高位部分存放在内存的低地址单元。低地址存高字节
小端:一个多字节整数,数字的低位部分存放在内存的低地址单元。低地址存低字节
大小端格式问题。
方法一:

 void checkCpuMode(void){int i = 0x12345678;char *cp = (char *)&i;if(*cp == 0x78)printf("little endian");elseprintf("big endian\\n");}

方法二:

 void checkCpuMode(void){int a = 0x12345678;if((char)a == 0x12)printf("big endian\\n");else printf("little endian\\n");}

方法三:

void checkCpuMode(void)
{union {short s;char c[sizeof(short)];}un;un.s=0x0102;if(un.[0]==1&&un.c[1]==2)printf("big endian\\n");elseprintf("little endian\\n");}

题15:静态局部变量在什么时候分配内存

1、
① static局部变量在编译阶段就已经分配内存空间了,也就是函数没有调用前它就已经存在了。
② 普通局部变量只有运行到定义该变量的时候才会分配内存空间。
2、
① 当执行完定义该static局部变量的函数体后,该static变量的内存空间不会被释放,只有程序结束时static变量才会自动释放。
② 当执行完定义该普通局部变量的函数体后,该普通局部变量的内存空间就会被释放。
3、
① 如果static局部变量不初始化,那么它默认为0。
② 如果普通局部变量不初始化,那么它的值为随机数。
4、
① static局部变量的初始化语句只会执行一次,下次再运行该初始化语句就不会被初始化,但是它可以被多次赋值。
② 每次运行该普通局部变量的定义时该变量都会被初始化。

题16:什么是不可重入函数和可重入函数

不可重入函数: 不能由超过一个任务所共享。不可以在它还没有返回就再次被调用。例如printf,malloc,free等都是不可重入函数。
可重入函数可以由多个任务并发使用,而不必担心数据错误。可重入函数是在运行期间被打断,而他的打断者又调用了它本身的情况下,不会产生不可预期的运行结果的函数。
可重入函数
不为连续的调用持有静态数据
不返回指向静态数据的指针,所有数据都由函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
如果必须访问全局变量,记住利用互斥信号量来保护全局变量
绝不在函数内调用任何不可重入函数
不可重入函数
函数中使用了静态变量,无论是全局变量还是局部静态变量
函数返回静态变量
函数中调用了不可重入函数
函数体内使用了静态的数据结构
函数体内调用了malloc()或者free()函数
函数体内调用了其他标准I/O函数
总得来说,如果一个函数在重入条件下使用了未受保护的共享资源,那么他就是不可重入的

题17:#define宏定义与typedef的区别

#define宏定义是字符替换,typedef是定义类型,是声明一种新的类型,等同自带的基本类型。

#define是宏,处理的时候位于编译前阶段,宏处理器基本上对你的C/C++程序不会有任何的感知。它只处理宏的语法。而编译阶段的“程序”得到的是宏处理完的结果。

typedef是编译阶段的一部分。它的意义是单一的。

宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符进行重新命令。被命名的标识符具有类型定义说明的功能。

#include<stdio.h>#define PIN1 char*typedef char* PIN2;void main() 
{//使用#define相当于:char* x, y;就是说x是char指针类型,而y是char类型,如果要连续定义两个指针需要写成char *x, *y;PIN1 x, y;//使用typedef相当于:char* x, char* y;就是说x是char指针类型,而y也是char指针类型PIN2 a, b;printf("By #define :%d  %d\\n\\n",sizeof(x), sizeof(y));printf("By typedef :%d  %d\\n\\n", sizeof(a), sizeof(b));//system("pause");return 0;
}

C语言常见面试题汇总

值传递改变值这类问题

下面的代码有什么问题?

void GetMem(char *p)
{p = (char*)malloc(100);   
}void main()
{char *str = NULL;GetMem(str);strcpy(str, "hello word!");printf(str);
}

分析:

程序崩溃。在上面已经分析过了,传递给GetMem函数形参的只是一个副本,修改形参p的地址对实参str丝毫没有影响。所以str还是那个str,仍为NULL,这时将字符串常量拷贝到一个空地址,必然引发程序崩溃。下面的方法可以解决这个问题:

void GetMem(char **p)
{*p = (char*)malloc(100);   
}void main()
{char *str = NULL;GetMem(&str);strcpy(str, "hello word!");printf(str);free(str);   //不free会引起内存泄漏
}

看似有点晦涩,其实很好理解。本质上是让指针变量str指向新malloc内存的首地址,也就是把该首地址赋值给指针变量str。前面我们说过,指针传递本质上也是值传递,要想在子函数修改str的值,必须要传递指向str的指针(str本身就是指针,指针的指针是二级指针),因此子函数要传递的是str的地址,这样通过指针方式修改str的值,将malloc的内存首地址赋值给str。

计算sizeof的值

char str1[] = {'a', 'b', 'c', 'd', 'e'};
char str2[] = "abcde";
char *ptr = "abcde";
char book[][80]={"计算机应用基础","C语言","C++程序设计","数据结构"};

sizeof(str1)=?

sizeof(str2)=?

sizeof(ptr)=?

sizeof(book)=?

sizeof(book[0])=?

分析:

sizeof(str1)=5,就是5*sizeof(char)=5;

sizeof(str2)=6,字符串都是以’0’结尾,所以所占字节数为6;

sizeof(ptr)=4,ptr是一个指针,在32位平台上大小为4字节;

sizeof(book)=320,book是一个二维数组,4*80*1

sizeof(book[0])=80,book[0]是第一维数组,因为此80*1

根据sizeof求数组元素的个数也很简单,拿第一个来说,就是sizeof(str1)/sizeof(char)。

计算strlen的值

char  arryA[] = {'a','b','c','0','d','e'};
char  arryB[] = {'a','b','c','d','e'};
char  arryC[6] = {'a','b','c','d','e'};
char *str = "abcde";

分析:

strlen(arryA) = 3,strlen遇到’0’就会返回,无论后面有多少个字符;

strlen(arryB)长度无法确定,没有人为写入‘0’,strlen会继续计算直到找到结束符,结果未知;

strlen(arryC)=5,指定了数组大小,编译器会自动在空余地方添加’0’,这其实跟char arryC[6] = {‘a’,‘b’,‘c’,‘d’,‘e’,‘0’};等价。

strlen(str) = 5,不包括结尾的’0’。

char str[]和char *str

1)下面的操作合法么?出错的话,会是在那个阶段?编译时期还是运行时期?

char str[] = "hello";
str[0] = 's';     //合法么char *str = "hello";
str[0] = 's';      //合法么

分析:

这两个都可以成功编译,只是第二个会在运行时期出现段错误。下面来分析一下:

首先"hello"是一个字符串常量,存储在静态数据区域(data段),这是在编译时期就确定的。第一个是将字符串常量赋值给了一个变量(全局变量在数据段,局部变量在栈区),实际上是将字符串常量拷贝到了变量内存中,因此修改的只是str[]这个变量的值。

第二个是将字符串常量的首地址赋值给指针str,对指针str操作就是对字符串常量进行修改!因此出现了段错误。

(2)理解了上面的知识,判断一下下面的true or false?

char str1[] = "abc";
char str2[] = "abc";const char str3[] = "abc";
const char str4[] = "abc";const char *str5 = "abc";
const char *str6 = "abc";char *str7 = "abc";
char *str8 = "abc";
cout << ( str1 == str2 ) << endl;
cout << ( str3 == str4 ) << endl;
cout << ( str5 == str6 ) << endl;
cout << ( str7 == str8 ) << endl;

分析:

结果是: 0 0 1 1

先理解str1,str2,str3,str4,他们是什么?他们是数组名,也就是数组首元素的地址!”str1 == str2“本质就是比较两个数组的地址是不是相同。上面我们说过,编译器给他们分配了新的存储空间来对字符串"abc"进行拷贝,这些变量在内存里是相互独立的,因此他们的地址肯定不同!

再理解str5,str6,str7,str8,他们是什么?他们是指针,他们的值就是字符串常量的地址!它们都指向“abc"所在的静态数据区,所以他们都相等。

数组作为函数参数传递

看看下面的函数有啥问题:

int func(int a[])
{  int n = sizeof(a)/sizeof(int);  for(int i=0;i<n;i++){  printf("%d ",a[i]);  a[i]++;  }  
}  

sizeof(a)/sizeof(int)一般是计算数组的元素个数,结果却发现n的值总是1,为什么会这样呢?这是因为在C中,将数组传递给一个函数时,无法按值传递,而是会自动退化为指针。下面的三种写法其实是等价的:

“int func(int a[20]);” 等价于 “int func(int a[]);” 等价于 “int func(int *a);”。

数组指针和指针数组的问题

(1)说出下面表达式的含义?

int *p1[10];
int (*p2)[10];

第一个是指针数组,首先他是一个数组,数组的元素都是指针。

第二个是数组指针,首先他是一个指针,它指向一个数组。

下面这张图可以很清楚的说明:

C语言常见面试题汇总
(2)写出下面程序运行的结果

int a[5] = { 1, 2, 3, 4, 5 };  
int *ptr = (int *)(&a + 1);  
printf("%d,%d", *(a + 1), *(ptr - 1)); 

分析:

答案是2,5。本题的关键是理解指针运算,”+1“就是偏移量的问题:一个类型为T的指针移动,是以sizeof(T)为单位移动的。

a+1:在数组首元素地址的基础上,偏移一个sizeof(a[0])单位。因此a+1就代表数组第1个元素,为2;

&a+1:在数组首元素的基础上,偏移一个sizeof(a)单位,&a其实就是一个数组指针,类型为int()[5]。因此&a+1实际上是偏移了5个元素的长度,也就是a+5;再看ptr是int类型,因此"ptr-1"就是减去sizeof(int*),即为a[4]=5;

a是数组首地址,也就是a[0]的地址,a+1是数组下一个元素的地址,即a[1]; &a是对象的首地址,&a+1是下一个对象的地址,即a[5]。

*p++、 (*p)++、 *++p、 ++*p

int a[5]={1, 2, 3, 4, 5};
int *p = a;
*p++ 先取指针p指向的值(数组第一个元素1),再将指针p自增1;cout << *p++;   // 结果为 1cout <<(*p++);  // 1(*p)++ 先取指针p指向的值(数组第一个元素1),再将该值自增1(数组第一个元素变为2cout << (*p)++;  // 1cout <<((*p)++)  // 2
*++p   先将指针p自增1(此时指向数组第二个元素),* 操作再取出该值cout << *++p;  // 2cout <<(*++p)  // 2++*p  先取指针p指向的值(数组第一个元素1),再将该值自增1(数组第一个元素变为2)cout <<++*p;     // 2    cout <<(++*p)  // 2

const 二级指针

给定声明 const char * const *pp;下列操作或说明正确的是?

(A)pp++  (B)(*pp)++  (C)(**pp)=c;  (D)以上都不对

分析:

答案是A。

先从一级指针说起吧:
(1)const char p : 限定变量p为只读。这样如p=2这样的赋值操作就是错误的。

(2)const char *p : p为一个指向char类型的指针,const只限定p指向的对象为只读。这样,p=&a或 p++等操作都是合法的,但如*p=4这样的操作就错了, 因为企图改写这个已经被限定为只读属性的对象。

(3)char *const p : 限定此指针为只读,这样p=&a或 p++等操作都是不合法的。而*p=3这样的操作合法,因为并没有限定其最终对象为只读。

(4)const char *const p :两者皆限定为只读,不能改写。

再来看二级指针问题:
(1)const char **p : p为一个指向指针的指针,const限定其最终对象为只读,显然这最终对象也是为char类型的变量。故像**p=3这样的赋值是错误的, 而像*p=? p++这样的操作合法。

(2)const char * const *p :限定最终对象和 p指向的指针为只读。这样 *p=?的操作也是错的,但是p++这种是合法的。

(3)const char * const * const p :全部限定为只读,都不可以改写

下面是在VC环境中实测,前面有红色标记则为报错:
C语言常见面试题汇总
C语言常见面试题汇总
C语言常见面试题汇总

好文推荐:
https://blog.csdn.net/jianpeng5/article/details/44998597
https://blog.csdn.net/lyh290188/article/details/104304322
https://blog.csdn.net/weixin_29133151/article/details/112114731