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

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

1.变量声明和定义

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

2.sizeof和strlen的区别

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

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

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

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

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

3.static作用

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

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

11.const作用

(1)定义const常量

(2)修饰函数参数、返回值、函数定义体

被const修饰的东西受保护,可预防意外变动,提高程序健壮性

1)该变量只能读取不能修改。(编译器进行检查)

2)定义时必须初始化。

3)C++中喜欢用const来定义常量,取代原来C风格的预编译指令define。

1 const int var; // Error:常量 变量"var"需要初始化设定项
2 const int var1 = 42;
3 var1 = 43; // Error:表达式必须是可以修改的左值

4.C/C++中动态内存分配方法,区别

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

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

       最长被问到的问题之一,基础中的基础。对C和C++而言,内存分配方式有5种:

       1)从静态存储区域分配。例如程序中定义的全局变量和static变量就是这种方式分配内存的。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。 

       2)在栈上创建。这是出现最多的情况,我们程序中的int var就是这种情况的内存分配方式【函数参数及函数局部变量】。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 

       3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

      4)、文字常量区:常量字符串就是放在这里的。程序结束后由系统释放。

               5)、程序代码区:既可执行代码。

       详细介绍

6.strcpy、sprintf与memcpy

       三个函数的功能分别为:

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

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

       它们的区别有:

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

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

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

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

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

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

8.类成员函数的重写overriding)、重载(overload)和隐藏

1)关于重载与覆盖

      成员函数被重载的特征:

       (1)相同的范围(在同一个类中);(2)函数名字相同;(3)参数不同;(4)virtual关键字可有可无。

      覆盖是指派生类函数覆盖基类函数,特征是:

      (1)不同的范围(分别位于派生类与基类);(2)函数名字相同;(3)参数相同;(4)基类函数必须有virtual关键字。

 2)令人迷惑的隐藏规则

      本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。

      这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

      (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

      (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

10.引用和指针

      简单说来,引用即别名,指针即地址。具体的部分参见找工作笔试面试那些事儿(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、地址相同,大小不同
示例代码:

  1. int arr[10];  
  2. int* p=arr;  
  3. cout<<arr<<endl;    // 0x28fed4
  4. cout<<p<<endl;      // 0x28fed4
  5. cout<<sizeof(arr)<<endl;//结果为40  
  6. cout<<sizeof(p)<<endl;//结果为4 

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

示例程序: 

  1. void fun(int* p)  
  2.      {  
  3.          cout<<p[0]<<endl;  
  4.      }  
  5.   
  6.      int main()  
  7.     {  
  8.          int arr[10]={0};  
  9.     int* p=arr;  
  10.     fun(arr);  
  11.     return 0;  


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

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

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

      构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。

      析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。 

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

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

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

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

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

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

    简单说来,delete[]删除一个数组,delete 删除一个指针。


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

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

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

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

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

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

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

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

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

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

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

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

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

先定义一下链表:

  1. typedef struct node  
  2. {  
  3. ElemType data;  
  4. struct node * next;  
  5. }ListNode;  
  6. typedef struct  
  7. {  
  8. ListNode *head;  
  9. int size;  
  10. ListNode *tail;  
  11. }List;  
  12. /********************************************************* 
  13. 非递归的翻转实际上就是使用循环,依次后移指针, 
  14. 并将遇到的链表指针反转 
  15. *********************************************************/  
  16. void ReserveList(List * plist)        //非递归实现,  
  17. {  
  18. ListNode * phead;   //新链表的头 开始的第一个节点  
  19. ListNode * pt;   //旧链表的头 开始的第二个节点  
  20. ListNode * pn;   //旧链表头的下一个  
  21. phead = plist->head;  
  22. if(phead && phead->next&& phead->next->next)    //首先确定  
  23. {  
  24. phead = plist->head->next;    //新链表就是以第一个节点开始,依次在表头添加节点,添加的节点是旧链表的第一个节点  
  25. pt = phead->next;     //旧链表,旧链表被取走头结点之后放入新链表的表头,  
  26. pn = pt->next;  
  27. phead->next = 0;  
  28. while(pt)  
  29. {  
  30. pn = pt->next;    //pn是旧链表的第二个节点  
  31. pt ->next = phead;   //取旧链表的第一个节点插入新链表  
  32. phead = pt;  
  33. pt = pn;     //旧链表往后移动  
  34. }  
  35. }  
  36. plist->head->next = phead;     //新链表重新赋值到整个链表  
  37. }  
  38. /********************************************************* 
  39. 递归思想,原理也是从就链表上依次取元素放入到新链表 
  40. 直到原始链表被取完,得到新链表 
  41. *********************************************************/  
  42. ListNode * ReserveListRe(ListNode * oldlist,ListNode * newlist)  
  43. {  
  44. ListNode * pt;  
  45. pt = oldlist->next;   //取旧链表的表头,pt是现在的旧链表  
  46. oldlist->next = newlist; //就旧链表插入到新链表  
  47. newlist = oldlist;   //如果旧链表是空,表示旧链表被取完了,新链表就是翻转之后的链表  
  48. return (pt == NULL) ? newlist : ReserveListRe(pt,newlist);  
  49. }  

转 http://blog.csdn.net/han_xiaoyang/article/details/10949147

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值