C和C++经典面试题(面试必备)

更新时间:2024-06-14 15:16:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

C/C++经典面试题(面试必备)

面试题 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 型数据:

if ( ( flag >= NORM ) && ( flag <= NORM ) ) { A; } 2

注意:应特别注意在 int、指针型变量和“零值”比较的时候,把“零值”放在左边,这样当把“ ==”

误写成“ =”时,编译器可以报错,否则这种逻辑错误不容易发现,并且可能导致很严重的后果。 面试题 3: sizeof 和 strlen 的区别

sizeof 和 strlen 有以下区别:

?????????是一个操作符,????????是库函数。??????????的参数可以是数据的类型,也可以是变量,而????????只能以结尾为‘\\0‘的字符串作参数。

??编译器在编译时就计算出了 sizeof 的结果。而 strlen 函数必须在运行时才能计算出来。并且 sizeof

计算的是数据类型占内存的大小,而 strlen 计算的是字符串实际的长度。

??数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。 注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定

要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就 是 sizeof。

面试题 4: C 语言的关键字 static 和 C++ 的关键字 static 有什么区别

在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而C++

中除了上述功能外,还用来定

义类的成员变量和函数。即静态成员和静态成员函数。

注意:编程时 static 的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,

而 C++的静态成员则可以在多个对象实例间进行通信,传递信息。 面试题 5: 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 释放,不要混用。

因为两者实现的机理不同。 面试题 6: 写一个“ 标准” 宏 MIN #define min(a,b)((a)<=(b)?(a):(b))

注意:在调用时一定要注意这个宏定义的副作用,如下调用: ((++*p)<=(x)?(++*p):(x)。

p 指针就自加了两次,违背了 MIN 的本意。

3 面试题 7: 一个指针可以是 volatile 吗

可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改

一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。 说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型

数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。 面试题 8: a 和&a 有什么区别

请写出以下代码的打印结果,主要目的是考察 a 和&a 的区别。 #include void main( void ) {

int a[5]={1,2,3,4,5}; int *ptr=(int *)(&a+1); printf(\return; }

输出结果: 2, 5。

注意:数组名 a 可以作数组的首地址,而&a 是数组的指针。思考,将原式的 int *ptr=(int *)(&a+1);

改为 int *ptr=(int *)(a+1);时输出结果将是什么呢? 面试题 9: 简述 C、 C++程序编译的内存分配情况

virtual void g() {

cout << \} private: virtual void f() {

cout << \} };

class B : public A { void g() {

cout << \}

virtual void h() {

cout << \} };

typedef void( *Fun )( void );

void main() { B b; Fun pFun;

for(int i = 0 ; i < 3; i++) {

pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i ); pFun(); } }

输出结果: B::g A::f B::h

注意:本题主要考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。

在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。 面试题 17: 简述类成员函数的重写、重载和隐藏的区别 ( 1)重写和重载主要有以下几点不同。

??范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。

??参数的区别:被重写函数和重写函数的参数列表一定相同,而被

重载函数和重载函数的参数列表一 定不同。

??????????的区别:重写的基类中被重写的函数必须要有?????????修饰,而重载函数和被重载函数可以被???

????????修饰,也可以没有。?( 2)隐藏和重写、重载有以下几点不同。

??与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。

??参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。

当参数不相同时,无论基类中的参数是否被 virtual 修饰,基类的函数都是被隐藏,而不是被重写。

说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完

全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。 面试题 18: 简述多态实现的原理

编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对

应虚函数的指针。编译器还会在此类中隐含插入一个指针 vptr(对 vc 编译器来说,它插在类的第一个位

置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行 vptr 与 vtable 的

关联代码,将 vptr 指向对应的 vtable,将类与此类的 vtable 联系了起来。另外在调用类的构造函数时,

指向基础类的指针此时已经变成指向具体的类的 this 指针,这样依靠此 this 指针即可得到正确的 vtable,。

如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。

注意:一定要区分虚函数,纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态

C++面试的重要考点之一,而虚函数是实现多态的基础。 面试题 19: 链表和数组有什么区别 数组和链表有以下几点不同:

( 1)存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,

长度可变,每个结点要保存相邻结点指针。

( 2)数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点, 效率低。

( 3)数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。

( 4)越界问题:链表不存在越界问题,数组有越界问题。 说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插

入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。

面试题 20: 怎样把一个单链表反序 ( 1)反转一个链表。循环算法。 List reverse(List n) {

if(!n) //判断链表是否为空,为空即退出。 { return n; }

list cur = n.next; //保存头结点的下个结点 list pre = n; //保存头结点 list tmp; 8

pre.next = null; //头结点的指针指空,转换后变尾结点 while ( NULL != cur.next ) //循环直到 cur.next 为空 {

tmp = cur; //实现如图 10.3—图 10.5 所示 tmp.next = pre pre = tmp; cur = cur.next; }

return tmp; //f 返回头指针 }

( 2)反转一个链表。递归算法。

List *reverse( List *oldList, List *newHead = NULL ) {

List *next = oldList-> next; //记录上次翻转后的链表

oldList-> next = newHead; //将当前结点插入到翻转后链表的开头 newHead = oldList; //递归处理剩余的链表

return ( next==NULL )? newHead: reverse( t, newHead ); }

说明: 循环算法就是图 10.2—图 10.5 的移动过程,比较好理解和想到。递归算法的设计虽有一点难

度,但是理解了循环算法,再设计递归算法就简单多了。 面试题 21:简述队列和栈的异同

队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是 “后进先出”。

注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分

配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 堆一般由程序员

分配释放, 若程序员不释放,程序结束时可能由 OS 回收。分配方

式类似于链表。

它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

面试题 22: 能否用两个栈实现一个队列的功能 结点结构体: typedef struct node { int data; node *next; }node,*LinkStack; 创建空栈:

LinkStack CreateNULLStack( LinkStack &S) {

S = (LinkStack)malloc( sizeof( node ) ); //申请新结点 if( NULL == S) {

printf(\9

return NULL; } S

->data = 0; //初始化新结点 S->next = NULL;

return S; }

栈的插入函数:

LinkStack Push( LinkStack &S, int data) {

if( NULL == S) //检验栈 {

printf(\return NULL; }

LinkStack p = NULL;

p = (LinkStack)malloc( sizeof( node ) ); //申请新结点 if( NULL == p) {

printf(\return S; }

if( NULL == S->next) {

p->next = NULL; } else

{

p->next = S->next; } p

->data = data; //初始化新结点 S->next = p; //插入新结点 return S; }

出栈函数:

node Pop( LinkStack &S) {

node temp; temp.data = 0; temp.next = NULL; if( NULL == S) //检验栈 {

printf(\return temp; }

temp = *S; 10

if( S->next == NULL ) {

printf(\return temp; }

LinkStack p = S ->next; //节点出栈 S->next = S->next->next; temp = *p; free( p ); p = NULL; return temp; }

双栈实现队列的入队函数:

LinkStack StackToQueuPush( LinkStack &S, int data) { node n;

LinkStack S1 = NULL;

CreateNULLStack( S1 ); //创建空栈

while( NULL != S->next ) //S 出栈入 S1 {

n = Pop( S ); Push( S1, n.data ); }

Push( S1, data ); //新结点入栈

{

int ARRAY[LEN]={ 5, 6, 8, 2, 4, 1, 9, 3, 7 }; //待序数组 printf(\

for( int m = 0; m < LEN; m++ ) //打印排序前数组 {

printf( \}

for (int i = 1; i <= LEN - 1; i++) //{

int t = i - 1; int temp = 0;

for (int j = i; j < LEN; j++) {

if (ARRAY[j] < ARRAY[t]) { t = j; } }

if (t != (i - 1)) {

temp = ARRAY[i - 1]; ARRAY[i - 1] = ARRAY[t];

选择排序 ARRAY[t] = temp; } }

printf( \

printf(\

for( i = 0; i < LEN; i++ ) //打印排序后数组 {

printf( \}

printf( \}

注意:在直接选择排序中,具有相同关键码的对象可能会颠倒次序,因而直接选择排序算法是一种

不稳定的排序方法。在本例中只是例举了简单的整形数组排序,肯定不会有什么问题。但是在复杂的数

据元素序列组合中,只是根据单一的某一个关键值排序,直接选择排序则不保证其稳定性,这是直接选

择排序的一个弱点。 面试题 27: 编程实现堆排序 堆排序编程实现: #include 14

void createHeep(int ARRAY[],int sPoint, int Len) //生成大根堆

{

while( ( 2 * sPoint + 1 ) < Len ) {

int mPoint = 2 * sPoint + 1 ; if( ( 2 * sPoint + 2 ) < Len ) {

if(ARRAY[ 2 * sPoint + 1 ] < ARRAY[ 2 * sPoint + 2 ] ) {

mPoint = 2*sPoint+2; } }

if(ARRAY[ sPoint ] < ARRAY[ mPoint ]) //堆被破坏,需要重新调整 {

int tmpData= ARRAY[ sPoint ]; //交换 sPoint 与 mPoint 的数据 ARRAY[ sPoint ] = ARRAY[ mPoint ]; ARRAY[ mPoint ] = tmpData; sPoint = mPoint ; } else {

break; //堆未破坏,不再需要调整

} } return; }

void heepSort( int ARRAY[], int Len ) //堆排序 { int i=0;

for ( i = ( Len / 2 - 1 ); i >= 0; i-- ) //将 Hr[0, Lenght-1]建成大根堆 {

createHeep(ARRAY, i, Len); }

for ( i = Len - 1; i > 0; i-- ) {

int tmpData = ARRAY[0]; //与最后一个记录交换 ARRAY[0] = ARRAY[i]; ARRAY[i] = tmpData;

createHeep( ARRAY, 0, i ); //将 H.r[0..i]重新调整为大根堆 } return; }

int main( void )

15 {

int ARRAY[] ={ 5, 4, 7, 3, 9, 1, 6, 8, 2}; printf(\打印排序前数组内容 for ( int i = 0; i < 9; i++ ) {

printf(\}

printf(\

heepSort( ARRAY, 9 ); //堆排序

printf(\打印排序后数组内容 for( i = 0; i < 9; i++ ) {

printf( \}

printf( \return 0; }

说明:堆排序,虽然实现复杂,但是非常的实用。另外读者可是自己设计实现小堆排序的算法。虽

然和大堆排序的实现过程相似,但是却可以加深对堆排序的记忆和理解。 面试题 28: 编程实现基数排序

#include #include #define LEN 8

typedef struct node //队列结点 { int data;

struct node * next; }node,*QueueNode;

typedef struct Queue //队列 {

QueueNode front; QueueNode rear; }Queue,*QueueLink;

QueueLink CreateNullQueue( QueueLink &Q) //{ Q = NULL;

Q = ( QueueLink )malloc( sizeof( Queue ) ); if( NULL == Q ) {

printf(\return NULL; }

创建空队列 16

Q->front = ( QueueNode )malloc( sizeof( node ) ); Q->rear = ( QueueNode )malloc( sizeof( node ) ); if( NULL == Q->front || NULL == Q->rear ) {

printf(\return NULL; } Q

->rear = NULL;

Q->front->next= Q->rear; return Q; }

int lenData( node data[], int len) //计算队列中各结点的数据的最大位数 {

int m = 0; int temp = 0; int d;

for( int i = 0; i < len; i++) {

d = data[i].data; while( d > 0)

{ d /= 10; temp ++; }

if( temp > m ) { m = temp; } temp = 0; } return m; }

QueueLink Push( QueueLink &Q , node node ) //将数据压入队列 {

QueueNode p1,p;

p =( QueueNode )malloc( sizeof( node ) ); if( NULL == p ) {

printf(\return NULL; }

p1 = Q->front;

while(p1->next != NULL) {

p1 = p1->next; } p

->data = node.data; p1->next = p; p->next = Q->rear; 17

return NULL; }

node Pop( QueueLink &Q) //数据出队列 {

node temp; temp.data = 0; temp.next = NULL; QueueNode p; p = Q->front->next; if( p != Q->rear ) {

temp = *p;

Q->front->next = p->next; free( p );

p = NULL; }

return temp; }

int IsEmpty( QueueLink Q) {

if( Q->front->next == Q->rear ) { return 0; } return 1; }

int main( void ) {

int i = 0;

int Max = 0; //记录结点中数据的最大位数 int d = 10; int power = 1; int k = 0;

node Array[LEN] ={{450, NULL}, {32,NULL}, { 781,NULL}, { 57 ,NULL},组

{ 145,NULL},{ 613,NULL},{ 401,NULL},{ 594,NULL}};

//队列结点数 QueueLink Queue[10]; for( i = 0; i < 10; i++) {

CreateNullQueue( Queue[i]); //初始化队列数组 }

for( i = 0; i < LEN; i++) {

printf(\}

printf(\

Max = lenData( Array, LEN ); //计算数组中关键字的最大位数 printf(\18

for(int j = 0; j < Max; j++) //按位排序 {

if(j == 0) power = 1; else power = power *d; for(i = 0; i < LEN; i++) {

k = Array[i].data /power - (Array[i].data/(power * d)) * d; Push( Queue[k], Array[i] );

}

for(int l = 0, k = 0; l < d; l++) //排序后出队列重入数组 {

while( IsEmpty( Queue[l] ) ) {

Array[k++] = Pop( Queue[l] ); } }

for( int t = 0; t < LEN; t++) {

printf(\}

printf(\} return 0; }

说明:队列为基数排序的实现提供了很大的方便,适当的数据机构可以减少算法的复杂度,让更多

的算法实现更容易。 面试题 29:谈谈你对编程规范的理解或认识 编程规范可总结为:程序的可行性,可读性、可移植性以及可测试性。 说明: 这是编程规范的总纲目,面试者不一定要去背诵上面给出的那几个例子,应该去理解这几个

例子说明的问题,想一想,自己如何解决可行性、可读性、可移植性以及可测试性这几个问题,结合以

上几个例子和自己平时的编程习惯来回答这个问题。 面试题 30: short i = 0; i = i + 1L;这两句有错吗 代码一是错的,代码二是正确的。

说明:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。 面试题 31: &&和&、 ||和|有什么区别 ( 1)&和|对操作数进行求值运算,&&和||只是判断逻辑关系。 19

( 2)&&和||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。

注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不

可预想的后果(比如当两个操作数一个是 1 另一个是 2 时)。 面试题 32: C++的引用和 C 语言的指针有什么区别 指针和引用主要有以下区别:

( 1)引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配 存储空间。

( 2)引用初始化以后不能被改变,指针可以改变所指的对象。 ( 3)不存在指向空值的引用,但是存在指向空值的指针。 注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,

目的就是想改变这个引用所

指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会 引发错误。所以使用时一定要小心谨慎。

面试题 33: 在二元树中找出和为某一值的所有路径

输入一个整数和一棵二元树。从树的根结点开始往下访问,一直到叶结点所经过的所有结点形成一

条路径。打印出和与输入整数相等的所有路径。例如,输入整数 9 和如下二元树: 3 / \\ 2 6 / \\ 5 4

则打印出两条路径: 3, 6 和 3, 2, 4。 【答案】 typedef struct path {

BiTNode* tree; //结点数据成员 struct path* next; //结点指针成员 }PATH,*pPath; 初始化树的结点栈:

void init_path( pPath* L ) {

*L = ( pPath )malloc( sizeof( PATH ) ); //创建空树 ( *L )->next = NULL; }

树结点入栈函数:

void push_path(pPath H, pBTree T) {

pPath p = H->next; pPath q = H; while( NULL != p ) 20 { q = p; p = p->next; }

p = ( pPath )malloc( sizeof( PATH ) ); //申请新结点 p->next = NULL; //初始化新结点 p->tree = T;

q->next = p; //新结点入栈 }

树结点打印函数:

本文来源:https://www.bwwdw.com/article/6uu6.html

Top