【C++】自己整理的一些C/C++面试题

【题1】嵌入式系统中经常会用到死循环,你怎么样用C语言编写死循环呢?
while(1)
{

}

for(;1;)
{

}
//个人觉得常用第一种,还有其他方法就不说了

【题2】 关键字static的作用是什么?
答:在C语言中,关键字static有三个明显的作用:
1)限制变量的作用域(隐藏):当我们同时编译多个文件时,所有未加static的全局变量和函数都具有全局可见性。如果加了static,就会对其他源文件隐藏,无法访问。利用这一特性,就可以在不同的源文件中定义同名变量和函数而不用担心会发生命名冲突。
2)保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
3)默认初始化为0:其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00开头,某些时候这一特性可以减少程序员的工作量。
总结首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认初始化为0。
补充:
(4) 在类中的static成员变量属于整个类所拥有,对类的所有对象而言只有一份 拷贝;
(5) 在类中的static成员函数属于整个类所拥有,这个成员函数不接受this指针,而只能访问类的static成员变量。

【题3】const修饰符。
答:
总的来说:被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1)可以定义const常量,具有不可变性;
2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。例如:void fun(const int data){……},编译器就会知道data是一个常量,不允许被修改;
3)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。拿2)中例子来说,如果在函数体内修改data的值,那么编译器就会报错;
4)可以节省空间,避免不必要的内存分配。const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
5)提高效率。编译器通常不为普通的const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高;
6)可以避免意义模糊的数字出现,同样可以很方便的进行参数的调整和修改;
7)为函数重载提供了一个参考。
class A
{
void fun(int i) {……} //一个函数
void fun(int i)const {……} //上一个函数的重载
};

【题4】const和#define有什么不同?
答:
1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只是单纯的字符替换,没有类型安全检查,并且在字符替换的过程中可能会产生意想不到的错误;
2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。

【题5】内联函数inline和宏#define的区别。
答:宏分为带参数的宏和不带参数的宏。不带参数的宏相当于重命名,给变量起个别名,见名知意。
带参数的宏定义不是函数,但是看起来像函数。是在预编译阶段把所有的宏名用宏体来替换,简单来说就是字符串替换。宏定义是没有类型检查的,无论对错都是直接替换。
内联函数本质上是一个函数,只是函数体比较短小简单,如果内联函数较大,编译器会自动将内联函数变成普通函数。内联函数是在编译时进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用开销(压栈退栈),提高效率。内联函数在编译时,会进行类型检查,因为内联函数满足函数的性质,比如有返回值,参数等。

【题6】头文件中的#ifndef 宏名 #define 宏名 #endif的作用是什么?
答:用在头文件中:防止头文件重复包含,出现大量重复定义的错误。

【题7】#include <xxx.h> 和 #include "xxx.h"的区别?
答:前者编译器是从标准库路径开始搜索该文件,而后者编译器是从源程序所在的目录搜索,若搜索不到,则去标准库路径搜索。

【题8】面向对象的三大特性。
答:
1)封装:将客观事物抽象为类,每个类都有其属性和方法。
作用:可以隐藏细节,使代码模块化。
2)继承:分层次的设计类,分为基类和派生类。
作用:可以扩展已经存在的代码模块,提高代码重复利用率。
实现:
派生类名 (参数总表):基类构造方法(实参)
{
//初始化增加属性:直接初始化。
}
3)多态:相同的消息发送给不同的对象,各个对象表现出不同的行为。
作用:提高程序可扩展性,使模块和模块之间尽量使用接口来访问。
实现:多态性是靠虚函数机制实现的。
虚函数机制:
1)基类中声明虚函数,子类中重写;
2)基类的引用赋值子类对象;
3)通过基类引用可以调用到子类中重写的虚函数。

【题9】 C++中重载、重写、重定义的区别?
答:(1)成员函数重载特征:
a.相同的范围,在同一个类中;
b.函数名字相同;
c.参数表不同。
(2)重写(覆盖):是指派生类函数重新定义覆盖基类函数。主要用于实现多态。
特征是:
a.不同的范围,分别位于基类和派生类中;
b.函数的名字相同;
c.参数表相同;
d.基类函数必须有virtual关键字。
(3)重定义(隐藏):是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
a.如果派生类的函数和基类的函数同名,但是参数不同,此时不管有无virtual,基类的函数都会被隐藏;
b.如果派生类的函数与基类的函数同名,并且参数相同,而且基类函数没有virtual关键字,此时基类函数被隐藏。
实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数;
重写:当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。

【题10】 变量的声明和定义有什么区别?
答:声明变量不分配空间,定义变量要分配空间。声明主要是告诉编译器,后面的引用都按声明的格式。定义其实包含了声明的意思,同时也要分配内存空间。

【题11】 简述数组和指针的区别。
答:
1)数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
2)修改内容上的区别:数组通过下标值进行修改。指针通过偏移量进行修改。
3)用运算符sizeof计算的区别:sizeof数组名计算的是数组成员个数,用sizeof指针名得到的是一个指针变量的字节数。

【题12】 解释堆和栈的区别。
答:
堆(heap):一般是由程序员手动分配释放,若程序员不进行释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。速度比较慢,而且容易产生内存碎片,不过用起来最方便。
栈(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。由系统自动分配,速度较快。但程序员是无法控制的。

【题13】一个数据成员是否可以即使const类型又是static类型?为什么?
【解答】
(1)一个数据成员可以既是const又是static类型,该数据成员表示为静态常量;
(2)常量一般是在构造函数后初始化;
(3)静态成员一般在类外初始化;
(4)静态常量在类外初始化,但是要在类外初始化的同时声明为const。

【题14】简述C++异常处理方式
【解答】
一个典型的C++异常处理包含以下几个步骤:
(1)程序执行时发生错误;
(2)以一个异常对象(最简单是一个整数)记录错误的原因以及相关信息;
(3)程序监测到这个错误(读取异常对象);
(4)程序决定如何处理错误;
(5)进行错误处理,并在此后恢复/终止程序的执行。

【题15】成员函数和友元函数的区别
【解答】
(1)成员函数是类定义的一部分,通过特定的对象来调用。成员函数既可以隐式访问调用对象的成员,而无须使用成员操作符;
(2)友元函数不是类的组成部分,因此被称为直接函数调用。友元函数不能隐式访问类成员,而必须将成员操作符用于作为参数传递的对象。

【题16】如何重载前++和后++运算符?
【解答】
前++不带参数,后++带一个int型参数作为二者之间的区分标准。

//前缀++
iCount &operator ++()
{
	cout << "前缀++" << endl;
	m_data++;
	return *this;
}

//后缀++
iCount &operator ++(int)
{
	cout << "后缀++" << endl;
	iCount temp = *this;
	m_data++;
	return temp;
}

【题17】函数模板与函数重载的异同?
【解答】
(1)函数的重载是指定义了几个名字相同,但是参数的类型或者参数个数不同的函数;
(2)模板函数是指几个函数的具体算法相同,而参数类型不同的函数;
(3)模板函数可以减少重载函数,但也可能引发错误。

【题18】C++中的空类,默认会产生哪些类成员函数?
【解答】

class Empty
{
public:
	Empty();//缺省构造函数
	Empty(const Empty&);//拷贝构造函数
	~Empty();//析构函数
	Empty& operator =(const Empty&);//赋值运算符
	Empty* operator &();//取址运算符
	const Empty* operator&() const;//取址运算符const
};

默认构造函数
拷贝构造函数
析构函数
赋值运算符(operator =)
取址运算符(operator &) (一对,一个非const的,一个const的)

【题19】引用和指针有什么区别?
【解答】
(1)定义一个指针变量编译器会为它分配内存,而引用不占用任何内存;
(2)引用必须在定义时被初始化,指针不必;
(3)不存在指向空值的引用,但存在指向空值的指针。

【题20】函数参数传递中,值传递、地址传递、引用传递有什么区别?
【解答】
(1)值传递
会为形参重新分配内存空间,将实参的值拷贝给形参,形参的值不会影响实参的值,函数调用结束后形参被释放;
(2)地址传递
形参为指针变量,将实参的地址传递给函数,可以在函数中改变实参的值,调用时为形参指针变量分配内存,结束时释放指针变量;
(3)引用传递
不会为形参重新分配内存空间,形参只是实参的别名,形参的改变直接影响实参的值,函数调用结束后形参不会被释放。

【题21】链表和数组的区别在哪?
【解答】
(1)链表和数组都可以被称为线性表。数组又叫顺序表。主要区别在于,顺序表(数组)是在内存中开辟一段连续的空间来存储数据,而链表是靠指针来连接多块不连续的空间,在逻辑上形成一片连续的空间来存储数据;
(2)数据要求空间连续,占用总空间小,链表不要求空间连续,占用总空间大;
(3)数组通过下标访问,所以方便排序和查找,但是删除和插入较慢;链表通过指针访问,所以方便删除和插入,但是查找较慢,不方便排序。

【题22】简述进程和线程的差别。
【解答】线程是指进程内的一个执行单元,也是进程内的可调度实体。
进程和线程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
(2)并发性:不仅进程之间可以并发执行,同一个进程中的多个线程也可以并发执行;(比如:杀毒软件中的清理垃圾和病毒查杀等可以同时进行)
(3)拥有资源:进程是拥有资源的一个独立的单元,线程不拥有系统资源但是可以访问隶属于进程的资源;
(4)系统开销:在创建或者撤销进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或者撤销线程时的开销。

【题23】memset、memcpy和strcpy的区别。
【解答】
(1)memset用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为指定值;
(2)memcpy用来做内存拷贝,可以用来拷贝任何数据类型的对象,可以指定拷贝的数据长度;
(3)strcpy只能拷贝字符串,遇到’\0’就结束拷贝。

【题24】析构函数有什么特点?
【解答】
(1)析构函数也是特殊的类成员函数,它没有返回类型;
(2)没有参数;
(3)没有重载;
(4)public、private、protected等权限控制对析构函数无效;
(5)析构函数不能手动调用,只是在类对象生命周期结束的时候,由系统自动调用释放在构造函数中分配的资源。

【题25】虚函数有什么作用?
【解答】
(1)虚函数的功能是使子类可以用同名的函数对父类函数进行覆盖,并且在通过父类指针调用时,如果有覆盖则自动调用子类覆盖函数,如果没有覆盖则调用父类中的函数,从而实现灵活扩展和多态性;
(2)如果是纯虚函数,则纯粹是为了在子类覆盖时有个统一的命名而已,子类必须覆盖纯虚函数,否则子类也是抽象类;
(3)含有纯虚函数的类称为抽象类,不能实例化对象,主要用作接口类。

【题26】虚析构函数有什么作用?
【解答】
(1)析构函数的工作步骤是:最底层的派生类的析构函数最先被调用,然后调用每一个基类的析构函数;
(2)在C++中,当一个派生类的对象通过使用一个基类指针删除,而这个基类有一个非虚的析构函数,则可能导致运行时派生类不能被销毁。然而基类部分很有可能已经被销毁,这就导致“部分析构”现象,造成内存泄漏;
(3)给基类一个虚析构函数,删除一个派生类对象的时候就将销毁整个对象,包括父类和全部的派生类部分。

【题27】什么是拷贝构造函数?什么时候用到拷贝构造函数?
【解答】
拷贝构造函数:Student(const Student& stu)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数就是本类的一个引用变量,该参数是const类型,不可变的。
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
1、程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的一样;
2、当函数的参数为类的对象时,在调用函数时需要将实参对象完整的传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值;
3、函数的返回值是类的对象。在函数调用完毕后将返回值待会函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。

【题28】产生内存泄漏的原因有什么?如何判断内存泄漏?
【解答】
产生内存泄漏原因:
1、指针指向未知区域;
2、申请了内存没有释放掉。
如何判断:
1、linux环境下:使用内存检查工具Valgrind;
2、书写习惯:在写代码时添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致。

【题29】什么时候会发生段错误?
【解答】段错误一般发生在非法访问内存地址的时候。比如:
1、使用野指针;
2、试图修改字符串常量的内容;
3、访问内存不能越界。//这里最容易在字符串的访问出错。
提一下如何避免野指针:
野指针指向的是一个未知的地方,不能通过判断它是否为NULL来避免。最好的避免方式就是依靠我们的编程习惯:比如当我们malloc申请空间后,可以给它初始化为NULL。free释放掉之后,也可以将这个指针变量置为NULL。

【题30】new和malloc的区别。
【解答】
最主要的区别是:1、new不仅可以申请内存,还可以调用构造函数,而malloc不会调用构造函数;
2、new分配内存是按照数据类型进行分配的,malloc分配内存是按照指定的大小分配;
3、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化;
4、new分配的内存需要用delete销毁,malloc要用free来销毁;delete销毁时还会调用对象的析构函数,而free不会;
5、new是一个操作符可以重载,malloc是一个库函数;
6、malloc分配的内存不够时,可以用realloc进行扩容。new没有这个操作;
7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL;
8、申请数组时:new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int *) n。

【题31】继承的优缺点。
【解答】
类继承是在编译时刻静态定义的,且可直接使用,类继承可以较方便地改变父类的实现。但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了子类的部分行为,父类的任何改变都可能影响子类的行为。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。

【题32】引用和多态的关系。
【解答】
引用是除了指针之外另一种可以产生多态的手段。这意味着:一个基类的引用可以指向它的派生类实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值