找工作笔试面试那些事儿(8)---常问的CC++基础题

时间:2022-09-24 23:08:12

这一部分是C/C++程序员在面试的时候会被问到的一些题目的汇总。来源于基本笔试面试书籍,可能有一部分题比较老,但是这也算是基础中的基础,就归纳归纳放上来了。大牛们看到一笑而过就好,普通人看看要是能补上一两个模糊的知识点,也算有点进步吧。

1.简述变量声明和定义的区别。

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

2.简述sizeof和strlen的区别

最常考察的题目之一。主要区别如下:

1)sizeof是一个操作符,strlen是库函数。

2)sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0‘的字符串作参数。

3)编译器在编译时就计算出了sizeof的结果。而strlen 函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。

4)数组做sizeof的参数不退化,传递给strlen就退化为指针了。

3.说说C和C++中的static有什么作用

这个真的在面试的时候被问过。

在C中static用来修饰局部静态变量和外部静态变量、函数。而C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。编程时最常用的是static的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而C++的静态成员则可以在多个对象实例间进行通信,传递信息。

4.C和C++中动态内存分配有什么方法,有何区别

C里面一般用malloc/free,C++可用malloc/free和new/delete。区别见找工作笔试面试那些事儿(3)---内存管理那些事中的内容。

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

之前提过,主要有静态存储区分配,在堆上和栈上分配三种,具体场景见找工作笔试面试那些事儿(3)---内存管理那些事中的内容。

6.说说strcpy、sprintf与memcpy三个函数

三个函数的功能分别为:

strcpy:实现字符串变量间的拷贝
       sprintf:主要实现其他数据类型格式到字符串的转化

Memcpy:主要是内存块间的拷贝

它们的区别有:

(1)操作对象不同,strcpy的两个操作对象均为字符串,sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

(2)执行效率不同,memcpy最高,strcpy次之,sprintf的效率最低。

7.说说拷贝构造函数和赋值运算符

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

(1)拷贝构造函数生成新的类对象,而赋值运算符不能。

(2)由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉(这一点在之前找工作笔试面试那些事儿(5)---构造函数、析构函数和赋值函数中提到了)。

8.简述类成员函数的重写、重载和隐藏的区别

找工作笔试面试那些事儿(4)---C++函数高级特征中所述。

9.用递归和非递归两种方法翻转一个链表

先定义一下链表:

typedef struct node
{
ElemType data;
struct node * next;
}ListNode;
typedef struct
{
ListNode *head;
int size;
ListNode *tail;
}List;
/*********************************************************
非递归的翻转实际上就是使用循环,依次后移指针,
并将遇到的链表指针反转
*********************************************************/
void ReserveList(List * plist) //非递归实现,
{
ListNode * phead; //新链表的头 开始的第一个节点
ListNode * pt; //旧链表的头 开始的第二个节点
ListNode * pn; //旧链表头的下一个
phead = plist->head;
if(phead && phead->next&& phead->next->next) //首先确定
{
phead = plist->head->next; //新链表就是以第一个节点开始,依次在表头添加节点,添加的节点是旧链表的第一个节点
pt = phead->next; //旧链表,旧链表被取走头结点之后放入新链表的表头,
pn = pt->next;
phead->next = 0;
while(pt)
{
pn = pt->next; //pn是旧链表的第二个节点
pt ->next = phead; //取旧链表的第一个节点插入新链表
phead = pt;
pt = pn; //旧链表往后移动
}
}
plist->head->next = phead; //新链表重新赋值到整个链表
}
/*********************************************************
递归思想,原理也是从就链表上依次取元素放入到新链表
直到原始链表被取完,得到新链表
*********************************************************/
ListNode * ReserveListRe(ListNode * oldlist,ListNode * newlist)
{
ListNode * pt;
pt = oldlist->next; //取旧链表的表头,pt是现在的旧链表
oldlist->next = newlist; //就旧链表插入到新链表
newlist = oldlist; //如果旧链表是空,表示旧链表被取完了,新链表就是翻转之后的链表
return (pt == NULL) ? newlist : ReserveListRe(pt,newlist);
}

10.谈谈对C++的引用和C语言的指针的认识

简单说来,引用即别名,指针即地址。具体的部分参见找工作笔试面试那些事儿(2)---函数那些事中的“关于指针和引用”。

重点谈一下它们的区别吧:

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

(2)引用初始化以后不能被改变,指针可以改变所指的对象。

(3)不存在指向空值的引用,但是存在指向空值的指针。

11.简述指针常量与常量指针区别

这是一个常见的问题。也就是const char *p和char * const p的差别,前者称为常量指针(指针指向的内容不可变),后者是指针常量(指针本身不可再被重新赋值)。下面是一个比较好记的方法,根据const的位置确定其修饰的内容:

const char* p : 因为const 修饰符在 * 号前面,因此const 修饰的是 (*p),因此p指向的字符串是const的.

char const* p : 等价于const char* p, 因为const 修饰符在 * 号前面,因此const 修饰的是 (*p),因此p指向的字符串是const的.

char* const p: const修饰的是变量p,而变量p是 char* 类型的,所以这个char* 变量本省是const,它的值初始化后就不能变了.

12.数组名和指针的区别

指针是一个变量,有自己对应的存储空间,而数组名仅仅是一个符号,不是变量,因而没有自己对应的存储空间。

1、地址相同,大小不同
示例代码:

      int arr[10];
int* p=arr;
cout<<arr<<endl;
cout<<p<<endl;
cout<<sizeof(arr)<<endl;//结果为40
cout<<sizeof(p)<<endl;//结果为4

2、都可以用指针作为形参

示例程序:

 void fun(int* p)
{
cout<<p[0]<<endl;
} int main()
{
int arr[10]={0};
int* p=arr;
fun(arr);
return 0;
}

3、指针可以自加,数组名不可以

4、作为参数的数组名的大小和指针的大小相同

13.构造函数能否为虚函数,为什么?

构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的虚函数表,调用相应的虚函数。

14.谈谈你对面向对象的认识

说实话,这种开放式的题目实则挺考察对知识的深层把握程度的。

面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加的简洁清晰。

编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。

15.delete 与 delete []有什么区别? 

这个特别在找工作笔试面试那些事儿(3)---内存管理那些事中提到了,简单说来,delete[]删除一个数组,delete 删除一个指针。

16.写个小程序确定一个数转化成二进制后是1的位的个数

很久以前就开始流传的一道微软面试题。

int func(x)
{
int countx = 0;
while(x)
{
countx ++;
x = x&(x-1);
}
return countx;
}

17.将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?

格式:类型标识符&函数名(形参列表及类型说明){ //函数体}

好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生

runtime error!) 注意事项:

(1)不能返回局部变量的引用。

主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。

(2)不能返回函数内部new分配的内存的引用。

虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成内存泄露。

(3)可以返回类成员的引用,但最好是const。

(4)流操作符重载返回值申明为“引用”的作用:

流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl;

因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。

(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用。 主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。

18.谈谈对于关联、聚合(Aggregation)以及组合(Composition)的认识

涉及到UML中的一些概念:

关联是表示两个类的一般性联系,比如“学生”和“老师”就是一种关联关系;

聚合表示has-a的关系,是一种相对松散的关系,聚合类不需要对被聚合类负责,用空的菱形表示聚合关系:从实现的角度讲,聚合可以表示为:

class A {...}  class B { A* a; .....}

组合表示contains-a的关系,关联性强于聚合:组合类与被组合类有相同的生命周期,组合类要对被组合类负责,采用实心的菱形表示组合关系:实现的形式是:

class A{...} class B{ A a; ...}

19.当一个类C 中没有任何成员变量与成员函数,这时sizeof(C)的值是多少。如果不是零,请解释一下编译器为什么没有让它为零。

一个空类对象的大小是1byte。这是被编译器安插进去的一个字节,这样就使得这个空类的两个实例得以在内存中配置独一无二的地址。

20.用变量a给出下面的定义

a) 一个整型数(An integer)

b) 一个指向整型数的指针(A pointer to an integer)

c) 一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an

integer)

d) 一个有10个整型数的数组(An array of 10 integers)

e) 一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointers to integers)

f) 一个指向有10个整型数数组的指针(A pointer to an array of 10 integers)

g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function

that takes an integer as an argument and returns an integer)

h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型

数( An array of ten pointers to functions that take an integer argument and return an integer )

非常非常经典的一道题,很多笔试面试题是从上述a-h中的一个或者几个,答案如下:

a) int a; // An integer

b) int *a; // A pointer to an integer

c) int **a; // A pointer to a pointer to an integer

d) int a[10]; // An array of 10 integers

e) int *a[10]; // An array of 10 pointers to integers

f) int (*a)[10]; // A pointer to an array of 10 integers

g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer

h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return

an integer

21.成员函数通过什么来区分不同对象的成员数据?为什么它能够区分?

通过this指针来区分的, 因为它指向的是对象的首地址。

22.拷贝构造函数在哪几种情况下会被调用?

1).当类的一个对象去初始化该类的另一个对象时;

2).如果函数的形参是类的对象,调用函数进行形参和实参结合时;

3).如果函数的返回值是类对象,函数调用完成返回时。

23. 流运算符为什么不能通过类的成员函数重载?一般怎么解决?

因为通过类的成员函数重载必须是运算符的第一个是自己,而对流运算的重载要求第一个参数是流对象。一般通过友元来解决。

24. 虚拟函数与普通成员函数的区别?内联函数和构造函数能否为虚拟函数?

区别:虚拟函数有virtual关键字,有虚拟指针和虚函数表,虚拟指针就是虚拟函数的接口,而普通成员函数没有。内联函数和构造函数不能为虚拟函数。