C&C++笔试面试高频1000题(一)

C/C++笔试面试1000题第一篇

写在前面

C/C++笔试面试出现频率最高的1000题系列,由于文章篇幅限定,总共1000题的分析将分为10篇讲述,此篇为
第一篇。
掌握这1000的高频题,面对互联网大厂笔试面试时会底气十足!
加油!

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; 
} 

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

3.sizeof 和 strlen 的区别

sizeof 和 strlen 有以下区别:

  • 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 的本意。

7.一个指针可以是 volatile 吗

可以,因为指针和普通变量一样,有时也有变化程序的不可控性。

常见例:子中断服务子程序修改一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。

说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。

8.a 和&a 有什么区别

请写出以下代码的打印结果,主要目的是考察 a 和&a 的区别。

#include<stdio.h> 
void main( void ) 
{ 
 	int a[5]={1,2,3,4,5}; 
 	int *ptr=(int *)(&a+1); 
 	printf("%d,%d",*(a+1),*(ptr-1));  	
 	return; 
} 

输出结果:2,5。
注意:数组名 a 可以作数组的首地址,而&a 是数组的指针。思考,将原式的 int *ptr=(int *)(&a+1); 改为 int *ptr=(int *)(a+1);时输出结果将是什么呢?

9.简述 C、C++程序编译的内存分配情况

C、C++中内存分配方式可以分为三种:

  1. 从静态存储区域分配:

    内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static 变量等。

  2. 在栈上分配:

    在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  3. 从堆上分配:

    即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

一个 C、C++程序编译时内存分为 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。

10.简述 strcpy、sprintf 与 memcpy 的区别

三者主要有以下不同之处:

  1. 操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
  2. 执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
  3. 实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。

说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。

11.设置地址为 0x67a9 的整型变量的值为 0xaa66

int *ptr;  
ptr = (int *)0x67a9;  
*ptr = 0xaa66;  

说明:这道题就是强制类型转换的典型例子,无论在什么平台地址长度和整型数据的长度是一样的,即一个整型数据可以强制转换成地址指针类型,只要有意义即可。

12.面向对象的三大特征

面向对象的三大特征是封装性、继承性和多态性:

封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection(private, protected, public)。

继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。

多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。 这部分需要熟悉掌握原理虚函数,了解一些概念(静态多态、动态多态)等,面试时经常会问。

说明:面向对象的三个特征是实现面向对象技术的关键,每一个特征的相关技术都非常的复杂,程序员应该多看、多练。

13.C++的空类有哪些成员函数

缺省构造函数。
缺省拷贝构造函数。
缺省析构函数。
缺省赋值运算符。
缺省取址运算符。
缺省取址运算符 const。

注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。

14.谈谈你对拷贝构造函数和赋值运算符的认识

拷贝构造函数和赋值运算符重载有以下两个不同之处:

  1. 拷贝构造函数生成新的类对象,而赋值运算符不能。
  2. 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉

注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

15.用 C++设计一个不能被继承的类

template <typename T> class A 
{ 
 	friend T; private: 
 	A() {} 
 	~A() {} 
}; 

class B : virtual public A<B> 
{ 
public: 
 	B() {} 
 	~B() {} 
}; 
class C : virtual public B 
{ 
public: 
 	C() {} 
 	~C() {} 
}; 
void main( void ) 
{ 
 	B b;  //C c; 
 	return; 
} 

注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。

注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。

16.访问基类的私有虚函数

写出以下程序的输出结果:

#include <iostream.h> 
class A 
{ 
 	virtual void g() 
 	{ 
 	 	cout << "A::g" << endl; 
 	} 
private: 
 	virtual void f() 
 	{ 
 	 	cout << "A::f" << endl; 
 	} 
}; 
class B : public A 
{ 
 	void g() 
 	{ 
 	 	cout << "B::g" << endl; 
 	} 
 	virtual void h() 
 	{ 
 	 	cout << "B::h" << endl; 
 	} 
}; 
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. 重写和重载主要有以下几点不同。

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

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

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

    virtual 修饰,也可以没有。

  2. 隐藏和重写、重载有以下几点不同。

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

    参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被 virtual 修饰,基类的函数都是被隐藏,而不是被重写。

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

18.简述多态实现的原理

编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针 vptr(对 vc 编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行 vptr 与 vtable 的关联代码,将 vptr 指向对应的 vtable,将类与此类的 vtable 联系了起来。另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的 this 指针,这样依靠此 this 指针即可得到正确的 vtable,。
如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。

注意:一定要区分虚函数,纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态
C++面试的重要考点之一,而虚函数是实现多态的基础。

19.链表和数组有什么区别

数组和链表有以下几点不同:

  • 存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
  • 数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
  • 数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
  • 越界问题:链表不存在越界问题,数组有越界问题。

说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。

20.怎样把一个单链表反序

  • 反转一个链表。循环算法。
List reverse(List   n)  
{  
 	if(!n)  	 	 	 	 	 	//判断链表是否为空,为空即退出。 
 	{ 
 	 	return n; 
 	} 
 	list cur = n.next; 	 	 	 	//保存头结点的下个结点  
 	list pre = n; 	 	 
 	list tmp; 	 	 	//保存头结点 
 	pre.next = null;  	 	 	 	//头结点的指针指空,转换后变尾结点 
 	while ( NULL != cur.next )  	 	//循环直到 cur.next 为空 
 	{ 
 		tmp = cur;  	 	 	 	
	} 	 	
	tmp.next = pre;    	
	pre = tmp; 
 	cur = cur.next; 
	

return tmp; 	 	 	 	 	//f 返回头指针 

}
  • 反转一个链表。递归算法。
List *reverse( List *oldList, List *newHead = NULL )  
{ 
 	List *next = oldList-> next;  	 	//记录上次翻转后的链表  	
 	oldList-> next = newHead;  	 	//将当前结点插入到翻转后链表的开头  	
 	newHead = oldList;   	 	 	//递归处理剩余的链表 
 	return ( next==NULL )? newHead: reverse( t, newHead );  
} 

说明:循环算法就是移动过程,比较好理解和想到。递归算法的设计虽有一点难度,但是理解了循环算法,再设计递归算法就简单多了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值