C++软件开发面试题总结

   面试题有难有易,不能因为容易,我们就轻视,更不能因为难,我们就放弃。我们面对高薪就业的态度永远不变,那就是坚持、坚持、再坚持。出现问题,找原因;遇到困难,想办法。我们一直坚信只有在坚持中才能看到希望,而不是看到希望才去坚持。

  人生没有如果,只有结果和后果。既然选择了,就不后悔。年轻就是资本,年轻就要吃苦,就要历练。就要学会在坚持中成长。如此感慨,至深的心得体会,绝对的经验之谈。

1、 Static有什么用途?

(1)函数体内static变量的作用范围是该函数体,该变量的内存只被分配一次,因此它的值在下次调用时不变;如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。
(2)模块内的static全局变量同样只能在该模块内的函数访问和调用,不能被模块外的其他函数访问;
(3)在类中的static成员变量属于整个类所有,对类的所有对象只有一份拷贝静态成员函数内部不能调用非静态成员函数,原因是,非静态成员函数需要传入一个this指针,这让静态成员函数很为难,它并不知道与之相关的信息,也就无法提供this指针。

普通成员变量每个对象都有各自的一份,但是静态成员变量一共只有一份,被所有的本类对象共享。如果使用sizeof运算符计算对象的大小,得到的结果是不包含静态成员变量在内的。

 

静态成员函数与普通成员函数的区别:

  • 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
  • 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。

 

2、 const

(1)不管在函数声明修饰形参、还是修饰类的成员变量,表示该成员变量不能被改变,而且通常需要进行初始化,因为之后不能再改变
(2)对于指针来说,可以修饰指针所指向的变量(在*左边,即指针指向内容为常量),也可以指定指针本身为const(在*右边,指针本身是常量),或者两者同时指定为const(都是常量)。

3、 this指针

(1)this指针本质是一个函数参数,只是编译期隐藏起形式的,语法层面上的参数,且this指针只能在成员函数中使用,全局函数、静态函数都不能使用;
(2)this在成员函数开始前构造,在成员结束后清除
(3)this指针不占用对象的空间。

4、 ifndef/define/endif的作用

  防止头文件被重复引用和定义;

5、 C和C++的区别

(1)C主要面向过程,C++面向对象
(2)C是一种结构化语言,重点在于算法和数据结构。C主要考虑通过一个过程将输入进行各种运算后得到输出,C++主要考虑的是如何构造一个对象模型,契合与之相对应的问题域,这样就可以通过获得对象的状态信息得到输出。

(3)什么是面向对象:面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想

(4)请用简单的语言告诉我C++ 是什么?

  C++是在C语言的基础上开发的一种面向对象编程语言,应用广泛。C++支持多种编程范式-------面向对象编程、泛型编程和过程化编程。 其编程领域众广,常用于系统开发,引擎开发等应用领域,是最受广大程序员受用的最强大编程语言之一,支持类、封装、重载等特性!

6、 C++函数值传递的方式

  值传递、指针传递和引用传递

7、 extern “C”的作用

  实现C和C++的混合编程;因为函数被C++编译后的名字会变长,与C生成的不一致,造成C++不能直接调用C函数

  extern关键字的作用

       extern置于变量或函数前,用于标示变量或函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。它只要有两个作用:

    当它与“C”一起连用的时候,如:extern "C" void fun(int a,int b);则告诉编译器在编译fun这个函数时候按着C的规矩去翻译,而不是C++的(这与C++的重载有关,C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同)

    当extern不与“C”在一起修饰变量或函数时,如:extern int g_Int;它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用。记住它是一个声明不是定义!也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

  如何引用一个已经定义过的全局变量?

    引用头文件和extern关键字。 如果采用引用头文件, 若变量写错了,则在编译期间便会出错。 如果用extern则在链接阶段报错。

8、 struct 和class 的区别

  (1)struct的成员默认是公有的,而类的成员默认是私有的
  (2)C中的struct不能包含成员函数,C++中的class可以包含成员函数。

   结构与联合有和区别?

  (1)结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。

  (2)对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。

9、 new和malloc

(1)都可用来申请动态内存和释放内存,都是在堆(heap)上进行动态的内存操作
(2)malloc和free是C语言的标准库函数,new和delete是C++的运算符
(3)new会自动调用对象的构造函数,delete 会调用对象的析构函数, 而malloc返回的都是void指针

10、 heap与stack(堆与栈)的差别

(1)heap是堆,stack是栈;
(2)stack的空间由操作系统自动分配和释放,存放函数的参数值、 局部变量的值等。heap上的空间一般由程序员分配和释放,并要指明大小;
(3)栈空间有限而且是一块连续的内存区域,堆是很大的自由存储区;
(4)C中的malloc函数分配的内存空间就是在堆上,C++是new;
(5)程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时的参数传递也在栈上进行

  堆栈溢出原因:数组越界, 没有回收内存, 深层次递归调用

11、 Vector、List和Deque的区别

  Vector:表示一段连续的内存区域,每个元素被顺序存储在这段内存中,对vector的随机访问效率很高,但对非末尾元素的插入和删除则效率非常低
  List:表示非连续的内存区域并通过一对指向首尾元素的指针双向链接起来,插入删除效率高,随机访问效率低
  Deque:也表示一段连续的内存区域,但与vector不同的是它支持高效地在其首部插入和删除元素,它通过两级数组结构来实现,一级表示实际的容器,第二级指向容器的首和尾。

  vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。
  list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”
  vector::iterator和list::iterator都重载了“++”运算符。
  总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。

13、 内联函数和宏的差别

  内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。而宏只是一个简单的替换
  内联函数要做参数类型检查,这是内联函数的优势;
  inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。
  对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏是一模一样的,但是inline在和宏相比没有付出任何额外代价的情况下更安全。 至于是否需要inline函数,就需要根据实际情况来取舍了。
  inline一般只用于如下情况

  (1)一个函数不断被重复调用。
  (2)函数只有简单的几行,且函数内不包含for、 while、 switch语句
  宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
  宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。
  关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。
  inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。 内联能提高函数的执行效率,至于为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  以下情况不宜使用内联:

  (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。 (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。 类的构造函数和析构函数容易让人误解成使用内联更有效。 要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。 所以不要随便地将构造函数和析构函数的定义体放在类声明中。 一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。

  请说出const与#define 相比,有何优点?

  const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。

14、 引用和指针的区别

(1)指针是一个变量,用于存放地址的变量,指向内存的一个存储单元,引用仅是别名
(2)引用必须初始化,指针不必
(3)不存在指向空值的引用,但是存在指向空值的指针
(4)sizeof() 引用对象得到的是所指对象变量的大小,sizeof() 指针得到是指针本身的大小
(5)内存分配上,程序为指针分配内存,不用为引用分配内存

15、 数组和链表的区别

  C++语言中可以用数组处理一组数据类型相同的数据,但在使用数组之前必须确定数组的大小。而在实际应用中,用户使用数组之前有时无法准确确定数组的大小,只能将数组定义成足够大小,这样数组中有些空间可能不被使用,从而造成内存空间的浪费。
  链表是一种常见的数据组织形式,它采用动态分配内存的形式实现。需要时可以用new分配内存空间,不需要时用delete将已分配的空间释放,不会造成内存空间的浪费。
 (1)从逻辑结构来看:

  数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况,即数组的大小一旦定义就不能改变。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;
  链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)。
 (2)从内存存储来看:

  静态数组从栈中分配空间(用NEW创建的在堆中),对于程序员方便快速,但是自由度小;
  链表从堆中分配空间, 自由度大,但是申请管理比较麻烦。
 (3)从访问方式来看:

  数组在内存中是连续存储的,因此,可以利用下标索引进行随机访问;
  链表是链式存储结构,在访问元素的时候只能通过线性的方式由前到后顺序访问,所以访问效率比数组要低。

16、 链表
(1)链表是否有环

  算法的思想是使用追赶的方法设定两个指针p, q,其中p每次向前移动一步,q每次向前移动两步。那么如果单链表存在环,则p和q相遇;否则q将首先遇到null退出。
(2)如何知道环的长度

  记录下上个问题的碰撞点p,slow、fast从该点开始,再次碰撞所走过的操作数就是环的长度s。
(3)如何找出环的连接点

  有定理:碰撞点p到连接点的距离 = 头指针到连接点的距离,因此,分别从碰撞点、头指针开始走,相遇的那个点就是连接点。
(4)带环链表的长度

  根据已经求出连接点距离头指针的长度,加上求出的环的长度,二者之和就是带环单链表的长度
(5)单链表的逆置

  输入一个链表,反转链表后,输出链表的所有元素。

  方法一:

    思路:从原链表的头部一个一个取节点并插入到新链表的头部

  1)

 1 struct ListNode{
 2         int val;
 3         struct ListNode *next;
 4         ListNode(int x):val(x),next(NULL){}
 5 };
 6  
 7 class Solution{
 8 public:
 9         ListNode* ReverseList(ListNode *pHead){
10                 ListNode* newh = NULL;
11                 for(ListNode *p = pHead; p; )
12                   {
13                          ListNode *tmp = p->next;
14                          p->next=newh;
15                          newh=p;
16                          p=tmp;
17                   }
18                  return newh;
19         }
20 };

  2)

 1 class Solution {
 2 public:
 3     ListNode* ReverseList(ListNode* pHead) {
 4         if(pHead == NULL)
 5             return pHead;      
 6         ListNode *res,*cur,*next;
 7         res = new ListNode(-1);
 8         cur = pHead;
 9         next = cur->next;
10         while(cur != NULL)
11         {       
12             ListNode *first = res->next;
13             cur->next = first;
14             res->next = cur;
15  
16             cur = next;
17             next = next->next;       
18         }   
19         return res->next;  
20     }
21 };

  方法二:
    思路:每次都将原第一个结点之后的那个结点放在新的表头后面。 
    比如1,2,3,4,5 
    第一次:把第一个结点1后边的结点2放到新表头后面,变成2,1,3,4,5 
    第二次:把第一个结点1后边的结点3放到新表头后面,变成3,2,1,4,5 
    …… 
    直到: 第一个结点1,后边没有结点为止。 
  代码如下:

 1 class Solution {
 2 public:
 3     ListNode* ReverseList(ListNode* pHead) {
 4         if(pHead == NULL)
 5             return pHead;
 6  
 7         ListNode *res,*first,*temp;
 8         res = new ListNode(-1);
 9         res->next = pHead;
10  
11         first = res->next;       //first 始终为第一个结点,不断后移
12         while(first->next!=NULL) //temp为待前插的
13         {
14             temp = first->next;
15             first->next = temp->next;
16             temp->next = res->next;
17             res->next = temp;          
18         }
19  
20         return res->next;
21     }
22 };

  方法三

    第三种方法跟第二种方法差不多,第二种方法是将后面的结点向前移动到头结点的后面,第三种方法是将前面的结点移动到原来的最后一个结点的后面,思路跟第二种方法差不多,就不贴代码了。

17、 重载和重写(覆盖)的区别

 (1)从定义来说:
    重载:是指存在多个重名函数,而这些函数的参数表不同(参数个数,类型不同);
    重写:是指子类重新定义父类虚函数的方法。
 (2)从实现原理上来说:
    重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。而函数的调用,在编译时就已经确定是静态的,也就是说它们的地址在编译期就绑定(早绑定),因此重载与多态无关。
    重写:和多态有关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,它们的地址是在运行期绑定的(晚绑定)

18、 封装、继承、多态、虚函数
  封装

    封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元(类)中。其意义在于保护或者防止代码被无意中破坏
  继承

    继承主要实现重用代码,扩展已存在的代码,节省开发时间。子类可以继承父类的一些东西。
  多态

    定义:“一个接口,多种方法”,程序运行时才决定调用的函数
    实现:C++多态主要是通过虚函数实现。虚函数允许子类重写。
    目的:封装可以使代码模块化,继承可以扩展已存在的代码,它们的目的都是为了代码重用。而多态的目的是为了接口重用,将接口与实现分离

  虚函数

    定义:被virtual关键字修饰的成员函数,就是虚函数。其作用就是实现多态性。
  为什么虚函数效率低?

    因为虚函数需要一次间接的寻址,而一般的函数可以在编译时定位到函数的地址,虚函数(动态类型调用)要根据某个指针定位到函数的地址。多增加了一个过程,效率虽然会低一些,但带来了运行时的多态。
  纯虚函数
    为什么要用纯虚函数?

      在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数。则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
    使用纯虚数的情况:

    (1)当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
    (2)这个方法必须在派生类(derived class)中被实现;
  虚函数与纯虚函数的区别:
    虚函数是为了重载和多态。在基类中是有定义的,即便定义为空。在子类中可以重写。
    纯虚函数在基类中没有定义,必须在子类中加以实现

    多态的基础是继承,需要虚函数的支持。子类继承父类大部分的资源,不能继承的有构造函数,析构函数,拷贝构造函数, operator=函数,友元函数等等。

19、 内存
  内存类别

    栈 ——由编译器自动分配释放, 局部遍历存放位置
    堆 ——由程序员分配和释放.
    全局区(静态区) ——全局变量和静态变量的存储是放在一起的, 初始化的全局变量和static静态变量在一块区域.
    程序代码区 ——存放二进制代码.
  内存分配方式

    静态存储区,程序编译时便分好, 整个运行期间都存在,比如全局变量,常量;
    栈上分配;
    堆上分配。

  内存泄漏

    原因:动态分配的内存没有手动释放完全。
    避免:使用的时候应记得指针的长度; 分配多少内存应记得释放多少, 保证一一对应的关系; 动态分配内存的指针最好不要再次赋值。

  内存溢出

    内存溢出是指程序在申请内存时,没有足够的内存空间供其使用。原因可能如下:

      内存中加载的数据量过于庞大,如一次从数据库取出过多数据

      代码中存在死循环或循环产生过多重复的对象实体

      递归调用太深,导致堆栈溢出等

      内存泄漏最终导致内存溢出

  缓冲区溢出(栈溢出)

       程序为了临时存取数据的需要,一般会分配一些内存空间称为缓冲区。如果向缓冲区中写入缓冲区无法容纳的数据,就会造成缓冲区以外的存储单元被改写,称为缓冲区溢出。而栈溢出是缓冲区溢出的一种,原理也是相同的。分为上溢出和下溢出。其中,上溢出是指栈满而又向其增加新的数据,导致数据溢出;下溢出是指空栈而又进行删除操作等,导致空间溢出。

20、 进程和线程的差别

  进程是最小的分配资源单位,线程是最小的执行单位。
  进程是程序的一次执行。线程可以理解为进程中执行的一段程序片段。 在一个多任务环境中下面的概念可以帮助我们理解两者间的差别。
  进程间是独立的,这表现在内存空间、 上下文环境上;线程运行在进程空间内。 一般来讲(不使用特殊技术),进程无法突破进程边界存取其他进程内的存储空间;而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。
  同一进程中的两段代码不能够同时执行,除非引入线程。
  线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除。 线程占用的资源要少于进程所占用的资源。 进程和线程都可以有优先级。

21、 TCP和UDP

  TCP是传输控制协议提供的是面向连接、 可靠的字节流服务。 当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。 TCP提供超时重发、 丢弃重复数据、 检验数据、 流量控制等功能,保证数据能从一端传到另一端。
  UDP是用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不保证它们能到达目的地。 由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

  TCP和UDP通信的差别?

  (1)TCP面向连接, UDP面向无连接的

  (2)TCP有保障的,UDP传输无保障的

  (3)TCP是效率低的,UDP效率高的

  (4)TCP是基于流的,UDP基于数据报文

  (5)TCP传输重要数据,UDP传输不重要的数据

22、 编写Socket套接字

  Socket相当于进行网络通信两端的插座,只要对方的Socket和自己的Socket有通信联接,双方就可以发送和接收数据了。 如果你要编写的是一个服务程序,那么先调用socket()创建一个套接字,调用bind()绑定IP地址和端口,然后启动一个死循环,循环中调用accept()接受连接。对于每个接受的连接,可以启动多线程方式进行处理,在线程中调用send()、 recv()发送和接收数据。
  如果你要编写的是一个客户端程序,那么就简单多了。 先调用socket()创建一个套接字,然后调用connect()连接服务器,之后就是调用send()、 recv()发送和接收数据了。

  服务器端程序编写:

  (1)调用ServerSocket(int port)创建一个服务器端套接字,并绑定到指定端口上。
  (2)调用accept(),监听连接请求,则接收连接,返回通信套接字。
  (3)调用Socket类的getOutStream()和getInputStream获取输出流和输入流,开始网络数据的发送和接收。
  (4)关闭通信套接字.Socket.close()。
  客户端程序编写:

  (1)调用Socket()创建一个流套接字,并连接到服务器端。
  (2)调用Socket类的getOutputStream()和fetInputStream获取输出流和输入流,开始网络数据的发送和接收。
  (3)关闭通信套接字.Socket.close()。
23、 三次握手和四次挥手
  1、建立连接协议(三次握手

  (1)客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1。
  (2)服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。
  (3)客户必须再次回应服务段一个ACK报文,这是报文段3。
  2、连接终止协议(四次握手

   由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
  (1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段1)。
  (2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段2)。和SYN一样,一个FIN将占用一个序号。
  (3) 服务器关闭客户端的连接,发送一个FIN给客户端(报文段3)。
  (4) 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段4)。
24、 排序

http://www.cnblogs.com/eniac12/p/5329396.html#s3
http://www.cnblogs.com/eniac12/p/5332117.html
25、 linux基本命令

http://www.cnblogs.com/laov/p/3541414.html#grep
26、 gdb调试

http://www.jb51.net/article/36393.htm

参考:http://www.cnblogs.com/bozhicheng/p/6259784.html      程序员面试宝典

27、设计模式懂嘛,简单举个例子?

  设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

   (1)比如单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    适用于:当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时;当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。

   (2)比如工厂模式,定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。

    适用于:当一个类不知道它所必须创建的对象的类的时候;当一个类希望由它的子类来指定它所创建的对象的时候;当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。

   C++中常用的设计模式有哪些?

    共有23种设计模式,但真正在开发中常用的模式有:

      (1)Factory Method(工厂模式);

      (2)Strategy(策略模式);

      (3)Singleton(单例模式);

      (4)Iterator(迭代器模式);

      (5)Abstract Factory(抽象工厂模式);

      (6)Builder(建造者模式);

      (7)Adapter(适配器模式);

      (8)Bridge(桥接模式);

      (9)Composite(组合模式);

      (10)Interpreter(解释器模式);

      (11)Command(命令模式);

      (12)Mediator(中介者模式);

      (13)Observer(观察者模式);

      (14)State(状态模式);

      (15)Proxy(代理模式)。

  编写一个单例模式的例子:

  单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。该实例被所有程序模块共享。

 1 #include<iostream>
 2 using namespacestd;
 3 
 4 classCSingleton
 5 {
 6 private:
 7        CSingleton(){}//构造函数是私有的
 8        static CSingleton *m_pInstance; // static
 9 
10 public:
11        static CSingleton *GetInstance() // static
12        {
13            if(m_pInstance==NULL) // 判断是否第一次调用
14                 m_pInstance=newCSingleton();
15 
16            return m_pInstance;
17        }
18 };
19 
20 CSingleton * CSingleton::m_pInstance=NULL; // static属性类外初始化
21 
22 void main()
23 {
24        CSingleton *p1= CSingleton::GetInstance();
25        CSingleton *p2= CSingleton::GetInstance();
26        cout<<(p1==p2)<<endl;//结果为true表示单例
27 }       

 

28、STL库用过吗?常见的STL容器有哪些?算法用过哪几个?

  STL包括两部分内容:容器和算法。(重要的还有融合这二者的迭代器)

  (1)容器,即存放数据的地方。比如array等。

      在STL中,容器分为两类:序列式容器和关联式容器。

        序列式容器,其中的元素不一定有序,但都可以被排序。如:vector、list、deque、stack、queue、heap、priority_queue;

        关联式容器,内部结构基本上是一颗平衡二叉树。所谓关联,指每个元素都有一个键值和一个实值,元素按照一定的规则存放。如:RB-tree、set、map、multiset、multimap、hashtable、ash_set、hash_map、hash_multiset、hash_multimap。

  下面各选取一个作为说明。

    vector:它是一个动态分配存储空间的容器。区别于c++中的array,array分配的空间是静态的,分配之后不能被改变,而vector会自动重分配(扩展)空间。

    set:其内部元素会根据元素的键值自动被排序。区别于map,它的键值就是实值,而map可以同时拥有不同的键值和实值。

  (2)算法,如排序,复制……以及个容器特定的算法。这点不用过多介绍,主要看下面迭代器的内容。

  (3)迭代器是STL的精髓,我们这样描述它:迭代器提供了一种方法,使它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构。它将容器和算法分开,好让这二者独立设计。

  STL中unordered_map和map的区别?

    map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。

    unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。

28、请编写能直接实现strlen()函数功能的代码

1 int strlen(char*str)
2 {
3     int i= 04     for(;str[i]!=’\0’;i++);
5     return i;
6 }

  请编写能直接实现strstr()函数功能的代码

 1 char *strstr(char *str, const char *sub)
 2 {
 3      4     for(int i=0;i<strlen(str);i++)
 5     {
 6         char *p = &str[i];
 7         char *q=sub;
 8         while(*p == *q)
 9         {
10             p++;
11             q++;
12             if(*q==’\0’)
13                return &str[i];
14         }
15 
16     }
17   return null;
18 }                  

  请编写能直接实现strcmp()函数功能的代码

 1 int strcmp(const char *str1, const char *str2)
 2 {
 3     assert(str1 != NULL && str2 != NULL);
 4     while (*str1 && *str1 == *str2)
 5     {
 6         str1++;
 7         str2++;
 8     } 
 9     if (*(unsigned char*)str1 < *(unsigned char*)str2)
10     {
11         return -1;
12     } 
13     else if (*(unsigned char*)str1 > *(unsigned char*)str2)
14     {
15         return 1;
16     } 
17     else
18     {
19         return 0;
20     }
21  }
  注意:
  1.参数是 const
  2.异常输入处理 assert(str1 != NULL&&str2 != NULL);
  3.字符之间大小比较时一定要先将 char* 型指针先转换为 unsigned char*
    因为有符号字符值的范围是-128~127,无符号字符值的范围是0~255,而字符串的ASCII没有负值。
    例如 *str1的值为1,*str2的值为255。
    本来 *str1 < *str2,但是有符号数中255是-1.

  请编写能直接实现strcat()函数功能的代码

 1 char *strcat(char *strDest, const char *strSrc)
 2 { 
 3     assert(strDest != NULL && strSrc != NULL); 
 4     char* address = strDest; 
 5     while (*strDest != '\0') strDest++; 
 6     while (*strSrc != '\0')
 7     { 
 8         *strDest = *strSrc; 
 9         strDest++; strSrc++; 
10     } 
11     *strDest = '\0'; // return address; 
12 }        
  1.注意参数和返回值
  2.要覆盖原字符串’\0’,结尾添加’\0

  请编写能直接实现strcpy()函数功能的代码

 1 char *strcpy(char* strDest, const char*strSrc)
 2 { 
 3     assert(strDest != NULL&&strSrc != NULL);
 4 
 5     char* address = strDest; 
 6     while (*strSrc != '\0')
 7     { 
 8         *strDest = *strSrc; 
 9         strDest++; 
10         strSrc++; 
11     } 
12     *strDest = '\0'; 
13 
14     return address; 
15 }
  如果有必要也应该考虑地址重叠问题

  请编写能直接实现memcpy()函数功能的代码

 1 // 自己动手实现memcpy()时就需要考虑地址重叠的情况。
 2 void *memcpy(void *dest, const void *src, size_t count)
 3 {
 4      char *d;
 5      const char *s;
 6  
 7      if (dest > (src + count)) || (dest < src))
 8     {
 9         d = dest;
10         s = src;
11         while (count--)
12             *d++ = *s++;        
13     }
14      else /* overlap */
15     {
16         d = (char *)(dest + count - 1); /* offset of pointer is from 0 */
17         s = (char *)(src + count -1);
18         while (count --)
19             *d-- = *s--;
20     }
21   
22  return dest;
23 }

  如何重载前++和后++运算符?

 1 // 前++不带参数,后++带一个int型参数以示区分。
 2 
 3 iCount &operator ++() // 前缀++
 4 {
 5        cout<<”前缀++”<<endl;
 6        m_data++;
 7        return *this;
 8 }
 9 
10 iCount &operator ++(int) // 后置++
11 {
12        cout<<”后缀++”<<endl;
13        iCount temp=*this;
14        m_data++;
15        return temp;
16 }

  C++中哪些运算符不可以重载?

    不能重载的5个运算符:

      (1) .     (2) ?:      (3) sizeof      (4) ::     (5) *

  memset、memcpy和strcpy的根本区别

    (1) memset用来对一段内存空间内全部设置为某个字符,一般用在对定义的字符串进行初始化为指定值;

    (2) memcpy用来做内存拷贝,可以用来拷贝任何数据类型的对象,可以指定拷贝的数据长度;

    (3) strcpy只能拷贝字符串,遇到’\0’就结束拷贝。

  分别给出bool、int、float、指针变量与零值比较的if语句

    (1)bool型变量:if(!var)

    (2)int型变量:if(var==0)

    (3)float型变量:

      const float EPSINON=0.000001;

      if((x>=- EPSINON)&&(x<= EPSINON))

    (4)指针变量:if(var==NULL)

  深度遍历二叉树

    (1)深度优先搜索算法:沿着树的深度遍历树的节点,尽可能深地搜索树的分支;

    (2)广度优先搜索算法:又叫宽度优先搜索,或横向优先搜索,是从根节点开始,沿着树的宽度遍历树的节点,如果所有节点均被访问则算法停止。

 1 struct Node
 2 {
 3     Node *Parent;
 4     Node *Left,*Right;
 5 };
 6 
 7 void Through(Node *Root)
 8 {
 9     if(Root) printf(Root->data);
10     if(Root->Left != null) Through(Root->Left); //
11     if(Root->Right != null) Through(Root-> Right); //
12 }

29、一个数据成员是否可以既是const又是static,如果不行,为什么?

  (1) 一个数据成员可以既是const又是static,表示为静态常量;

  (2) 常量一般在构造函数后初始化; 参考:https://blog.csdn.net/fengguangle/article/details/78019905

  (3) 静态成员一般在类外初始化;

  (4) 静态常量在类外初始化,但要在类外初始化的同时声明为const。

30、构造函数与析构函数的异同点

  1.构造函数有如下特点:

    (1) 构造函数的名字必须与类名相同;

    (2) 构造函数可以有任意类型的参数,但不能有返回类型

    (3) 定义对象时,编译系统会自动调用构造函数

    (4) 构造函数是特殊的成员函数,函数体可以在类体内也可以在类体外;

    (5) 构造函数被声明为公有函数,但它不能像其他成员函数那样被显式调用,它是在定义对象的同时被调用的。

  2.析构函数有如下特点:

    (1)析构函数的名字必须与类名相同,但它前面必须加一个波浪号;

    (2)析构函数没有参数,也没有返回值,而且不能被重载,因此在一个类中只能有一个析构函数;

    (3)当撤销对象时,编译系统会自动调用析构函数;

    (4)析构函数可以是virtual,而构造函数不能是虚函数

  构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

    1、构造函数不能声明为虚函数

      1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等。

      2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了。

    2、析构函数最好声明为虚函数

      首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

      如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

31、自动调用复制(拷贝)构造函数的几种情形

  1. 拷贝构造函数的功能是用一个已知对象来初始化另一个同类的对象。拷贝构造函数其实也是类的构造函数,与类名相同,有且只有一个参数,是该类对象的引用每个类必须有一个拷贝构造函数。如果定义类的时候没有编写,编译器编译时会自动生成一个拷贝构造函数。

  2. 拷贝构造函数在三种情况下会自动被调用:

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

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

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

  拷贝构造函数和赋值函数重载 (operator=):

    定义会调用拷贝构造函数, 赋值会调用赋值函数(operator=);

    拷贝构造函数是一种特殊的构造函数, 参数是一个变量实例而已。

32、成员函数和友元函数的区别

  (1)成员函数是类定义的一部分,通过特定的对象来调用。成员函数既可以隐式访问调用对象的成员,而无须使用成员操作符;

  (2)友元函数不是类的组成部分,因此被称为直接函数调用。友元函数不能隐式访问类成员,而必须将成员操作符用于作为参数传递的对象。类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。

33、函数模板与函数重载的异同?

  (1)函数的重载是指定义了几个名字相同,但参数的类型或参数的个数不同的函数;

  (2)模板函数是指的几个函数的具体算法相同,而参数类型不同的函数;

  (3)模板函数可以减少重载函数,但也可能引发错误。

34、静态绑定和动态绑定的介绍

  静态绑定和动态绑定是C++多态性的一种特性

    1)对象的静态类型和动态类型

      静态类型:对象在声明时采用的类型,在编译时确定;

      动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改;

    2)静态绑定和动态绑定

      静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定

      动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定

  只有虚函数才使用的是动态绑定,其他的全部是静态绑定

35、引用是否能实现动态绑定,为什么引用可以实现

  可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。

36、深拷贝和浅拷贝的区别

  深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。

  在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。

  总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

37、C++的四种强制转换

  类型转化机制可以分为隐式类型转换和显示类型转化(强制类型转换)

    (new-type) expression
    new-type (expression)

  隐式类型转换比较常见,在混合类型表达式中经常发生;

  四种强制类型转换操作符:

    static_cast、dynamic_cast、const_cast、reinterpret_cast

    1)static_cast :编译时期的静态类型检查

      static_cast < type-id > ( expression )

      该运算符把expression转换成type-id类型,在编译时使用类型信息执行转换,在转换时执行必要的检测(指针越界、类型检查),其操作数相对是安全的;

    2)dynamic_cast:运行时的检查

      用于在集成体系中进行安全的向下转换downcast,即基类指针/引用->派生类指针/引用;

      dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查;

      dynamic_cast如果不能转换返回NULL;

      源类中必须要有虚函数,保证多态,才能使用dynamic_cast<source>(expression);

    3)const_cast

      去除const常量属性,使其可以修改 ; volatile属性的转换;

    4)reinterpret_cast

      通常为了将一种数据类型转换成另一种数据类型;
38、调试程序的方法 

  windows下直接使用vs的debug功能;

  linux下直接使用gdb,我们可以在其过程中给程序添加断点,监视等辅助手段,监控其行为是否与我们设计相符。(上文有gdb的指令集合链接);

39、volatile关键字在程序设计中有什么作用

  volatile是“易变的”、“不稳定”的意思。volatile是C的一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题;

40、引用作为函数参数以及返回值的好处

  对比值传递,引用传参的好处:

    1)在函数内部可以对此参数进行修改;

    2)提高函数调用和运行的效率(所以没有了传值和生成副本的时间和空间消耗);

  如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。

  用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。但是有以下的限制:

    1)不能返回局部变量的引用,因为函数返回以后局部变量就会被销毁;

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

    3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。

41、线程安全和线程不安全

  线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可以使用,不会出现数据不一致或者数据污染。

  线程不安全就是不提供数据访问保护,有可能多个线程先后更改数据所得到的数据就是脏数据。

42、友元函数和友元类

  友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。

  通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。

  友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

  1)友元函数

    友元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明:

    friend 类型 函数名(形式参数);

    一个函数可以是多个类的友元函数,只需要在各个类中分别声明。

  2)友元类

    友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

    friend class 类名;

    使用友元类时注意:

      (1) 友元关系不能被继承。
      (2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
      (3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

43、C++线程中的几种锁机制

  线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能越强大,性能就会越低

  1)互斥锁

    互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。

    在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。

  2)条件锁

    条件锁就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。这个过程中就使用到了条件变量pthread_cond_t。

  3)自旋锁

    前面的两种锁是比较常见的锁,也比较容易理解。下面通过比较互斥锁和自旋锁原理的不同,这对于真正理解自旋锁有很大帮助。

    假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。

    首先我们说明互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。而自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。

    从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。

    注意:自旋锁适合于短时间的的轻量级的加锁机制。

  4)读写锁

    说到读写锁我们可以借助于“读者-写者”问题进行理解。首先我们简单说下“读者-写者”问题。

    计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。这就是一个简单的读者-写者模型。

44、i++是否为原子操作?

  不是。操作系统原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断,分为两种情况(两种都应该满足):

   (1) 在单线程中, 能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。

   (2) 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。

  i++分为三个阶段:

    内存到寄存器
    寄存器自增
    写回内存
   这三个阶段中间都可以被中断分离开.

45、指针数组 数组指针 指针函数 函数指针

  指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
  数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
  根据上面的解释,可以了解到指针数组和数组指针的区别,因为二者根本就是种类型的变量。

int   *p[4];         //指针数组。  是个有4个元素的数组, 每个元素的是指向整型的指针。(数组的每个元素都是指针)
int   (*p)[4];       //数组指针。 它是一个指针,指向有4个整型元素的数组。(一个指针指向有4个整型元素的数组)
int *func(void);     //指针函数。 无参函数, 返回整型指针。(函数的返回值为 int*)    
int (*func)(void);   //表示函数指针,可以指向无参, 且返回值为整型指针的函数。(函数的返回值为int)

  右左规则:

  因为C语言所有复杂的指针声明,都是由各种声明嵌套构成的。如何解读复杂指针声明呢?右左法则是一个既着名又常用的方法。不过,右左法则其实并不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的,两者可以说是相反的;

  右左法则:首先从最里面的圆括号(未定义的标识符)看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明;

  笔者要对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可能有多个标识符,但未定义的标识符只会有一个。现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐步加深:
  int (*func)(int *p);
    首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是 int。

  int (*func)(int *p, int (*f)(int*));
    func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型。再来看一看func的形参int (*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。

  int (*func[5])(int *p);
    func右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰 func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int。

 46、C++四种类型转换的关键字及其特点

  C++的四种强制类型转换,所以C++不是类型安全的。关键字分别为:static_cast , dynamic_cast , const_cast , reinterpret_cast

   四种转换的区别:
    static_cast:可以实现C++中内置基本数据类型之间的相互转换。如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数。

    const_cast: const_cast操作不能在不同的种类间转换。相反,它仅仅把一个它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。

    reinterpret_cast: 有着和C风格的强制转换同样的能力。它可以转化任何内置的数据类型为其他任何的数据类型,也可以转化任何指针类型为其他的类型。它甚至可以转化内置的数据类型为指针,无须考虑类型安全或者常量的情形。不到万不得已绝对不用。
    dynamic_cast: 

      (1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。
      (2)不能用于内置的基本数据类型的强制转换。
      (3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。
      (4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。
      (5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。向上转换即为指向子类对象的向下转换,即将父类指针转化子类指针。向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。

47、内存对齐的原则以及作用?

  • 结构体内的成员按自身长度自对齐(32位机器上,如char=1,short=2,int=4,double=8),所谓自对齐是指该成员的起始地址必须是它自身长度的整数倍。如int只能以0,4,8这类地址开始。
  • 结构体的总大小为结构体的有效对齐值的整数倍(默认以结构体中最长的成员长度为有效值的整数倍,当用#pragrma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值)。即sizeof的值,必须是其内部最大成员的整数倍,不足的要补齐。

  内存对齐的作用:

    1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

    2、性能原因:经过内存对齐后,CPU的内存访问速度大大提升。

48、auto_ptr类与shared_ptr类

  从c++11开始, auto_ptr已经被标记为弃用, 常见的替代品为shared_ptr。shared_ptr的不同之处在于引用计数, 在复制(或赋值)时不会像auto_ptr那样直接转移所有权。 两者都是模板类,却可以像指针一样去使用。只是在指针上面的一层封装。

   auto_ptr实际也是一种类, 拥有自己的析构函数, 生命周期结束时能自动释放资源,正因为能自动释放资源, 特别适合在单个函数内代替new/delete的调用, 不用自己调用delete,也不用担心意外退出造成内存的泄漏。

  atuo_ptr的缺陷:

  •  auto_ptr不能共享所有权,即不要让两个auto_ptr指向同一个对象(因为它采用的是转移语义的拷贝,原指针会变为NULL)。
  •  auto_ptr不能管理对象数组(因为它内部的析构函数调用的是delete而不是delete[])。
  •  auto_ptr不能作为容器对象,STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权。

  shared_ptr 使用引用计数的方式来实现对指针资源的管理。同一个指针资源,可以被多个 shared_ptr 对象所拥有,直到最后一个 shared_ptr 对象析构时才释放所管理的对象资源。

      可以说,shared_ptr 是最智能的智能指针,因为其特点最接近原始的指针。不仅能够自由的赋值和拷贝,而且可以安全的用在标准容器中。

  参考STL智能指针:https://blog.csdn.net/k346k346/article/details/81478223

posted @ 2019-09-06 22:52  thinking~  阅读(10183)  评论(0编辑  收藏  举报