Fork me on GitHub

那些不能遗忘的知识点回顾——C/C++系列(笔试面试高频题)

有那么一些零碎的小知识点,偶尔很迷惑,偶尔被忽略,偶然却发现它们很重要,这段时间正好在温习这些,就整理在这里,一起学习一起提高!后面还会继续补充。

——前言

1.面向对象的特性

  封装、继承、多态。

  封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。(优点:可以隐藏实现细节,使得代码模块化)

  继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。(优点:可以扩展已存在的代码模块(类))

  多态:一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。虽然针对不同对象的具体操作不同,但通过一个公共的类,这些操作可以通过相同的方式被调用。

  多态实现的两种方式:父类指针指向子类对象 或 将一个基类的引用类型赋值为它的派生类实例。(重要:虚函数 + 指针或引用)

构造函数、复制构造函数、析构函数、赋值运算符不能被继承。

 

2.堆和栈

  从内存角度来说:栈区(stack)由编译器自动分配释放,存放函数的参数值,局部变变量的值等,其操作方式类似于数据结构中的栈,可静态亦可动态分配

  堆区(heap)一般由程序员分配释放,若程序员不释放,可能造成内存泄漏,程序结束时可能由OS回收。只可动态分配,分配方式类似于链表。

  从数据结构角度来说:堆可以被看成是一棵树,如:堆排序。

  而栈是一种先进后出的数据结构。

 

3.malloc和new

  1.malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。但它们都可用于申请动态内存和释放内存。

  2.对于非内部数据类型的对象而言,用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,和一个能完成清理与释放内存工作的运算符delete。

  3.new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。new delete在实现上其实调用了malloc,free函数。

  4.new 建立的是一个对象;malloc分配的是一块内存。

 

4.虚函数实现机制,虚继承在sizeof中有没有影响,构造函数能否为虚函数,与纯虚函数

  虚函数表:类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。

  编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚函数占据虚函数表中的一块。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。

  在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

  编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

  ->有虚函数或虚继承的类实例化后的对象大小至少为4字节(确切的说是一个指针的字节数;说至少是因为还要加上其他非静态数据成员,还要考虑对齐问题);没有虚函数和虚继承的类实例化后的对象大小至少为1字节(没有非静态数据成员的情况下也要有1个字节来记录它的地址)。

  有纯虚函数的类为抽象类,不能定义抽象类的对象,它的子类要么实现它所有的纯虚函数变为一个普通类,要么还是一个抽象类。

  特别的

  (1)当存在类继承并且析构函数中有必须要进行的操作时(如需要释放某些资源,或执行特定的函数)析构函数需要是虚函数,否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,从而造成内存泄露或达不到预期结果;

  (2)内联函数不能为虚函数:内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;

  (3)构造函数不能为虚函数:构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,因此不存在动态绑定的概念;但是构造函数中可以调用虚函数,不过并没有动态效果,只会调用本类中的对应函数;

  (4)静态成员函数不能为虚函数:静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的。

  更多关于虚函数的细节,请移步博文:关于C++虚函数表的那些事儿

 

5.面向对象的多态、多态的实现机制,多态的例子

   见知识点4

 

6.对一个类求sizeof需要考虑的内容

   见知识点4。同时,对于一个结构体和一个类执行sizeof()运算时情况比较复杂,详细分析请移步另一篇博文struct/class等内存字节对齐问题详解

 

7.重载和重写(覆盖)

  方法的重写Overriding和重载Overloading是多态性的不同表现。

  重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。

  如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了,而且如果子类的方法名和参数类型和个数都和父类相同,那么子类的返回值类型必须和父类的相同;如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloading的方法是可以改变返回值的类型。也就是说,重载的返回值类型可以相同也可以不同。

 

8.“引用”与多态的关系?

  引用是除指针外另一个实现多态的方式。这意味着,一个基类的引用可以指向它的派生类实例。例:

  Class A; Class B : Class A{…};

  B b; A& ref = b;

 

9.计算机加载程序包括哪几个区?

  一个由C/C++编译的程序占用的内存分为以下几个部分:

  (1)栈区(stack):—由编译器自动分配释放,存放函数的参数值,局部变量的值等。可静态也可动态分配。其操作方式类似于数据结构中的栈。 

  (2)堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。动态分配。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 

  (3)全局区(静态区):—程序结束后由系统释放,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域;未初始化的全局变量和静态变量在相邻的另一块区域(BSS,Block Started by Symbol),在程序执行之前BSS段会自动清0。 

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

  (5)程序代码区:—存放函数体的二进制代码。

 

 

10.派生类中构造函数与析构函数,调用顺序

  构造函数的调用顺序总是如下:

  1.基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。

  2.成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。如果有的成员不是类对象,而是基本类型,则初始化顺序按照声明的顺序来确定,而不是在初始化列表中的顺序。

  3.派生类构造函数。

  析构函数正好和构造函数相反。

 

11.extern “C”的作用

  extern "C"实现C++与C及其它语言的混合编程,是用在C和C++之间的桥梁。之所以需要这个桥梁是因为C编译器编译函数时不带函数的类型信息,只包含函数符号名字;而C++编译器为了实现函数重载,编译时会带上函数的类型信息,如他把上面的a函数可能编译成_a_float这样的符号为了实现重载。

  extern "C"的惯用法:

  在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

  extern "C"{

    #include "cExample.h"

  }

  而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

  extern本身作为关键字修饰变量(函数)时声明该变量(函数)是外部变量(函数),通常全局变量在头文件中用这种方式声明,在对应源文件中定义,来防止重定义的错误。

 

12.析构函数、构造函数能不能被继承

  见知识点1

 

13.C++为什么用模板类,为什么用泛型

  通过泛型可以定义类型安全的数据结构(类型安全),而无须使用实际的数据类型(可扩展)。这能够显著提高性能并得到更高质量的代码(高性能),因为您可以重用数据处理算法,而无须复制类型特定的代码(可重用)。

 

14.结构体内存对齐,与什么有关(CPU)

  在系统默认的对齐方式下:每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍,且最终占用字节数为成员类型中最大占用字节数的整数倍。

  详细分析见博客struct/class等内存字节对齐问题详解

  为什么要对齐?当CPU访问正确对齐的数据时,它的运行效率最高,当数据大小的数据模数的内存地址是0时,数据是对齐的。例如:WORD值应该是总是从被2除尽的地址开始,而DWORD值应该总是从被4除尽的地址开始,数据对齐不是内存结构的一部分,而是CPU结构的一部分。当CPU试图读取的数值没有正确的对齐时,CPU可以执行两种操作之一:产生一个异常条件;执行多次对齐的内存访问,以便读取完整的未对齐数据,若多次执行内存访问,应用程序的运行速度就会慢。

 

15.指针和引用

  1.指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。

  2.指针可以有多级,但是引用只能是一级;

  3.指针的值可以为空,也可能指向一个不确定的内存空间,但是引用的值不能为空,并且引用在定义的时候必须初始化为特定对象;(因此引用更安全)

  4.指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变引用对象了;

  5.sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小;

  6.指针和引用的自增(++)运算意义不一样;

 

16.static关键字作用

  在C语言中,关键字static有三个明显的作用:

  1)在函数体内,一个被声明为静态的变量在这一函数被调用过程中维持上一次的值不变,即只初始化一次(该变量存放在静态变量区,而不是栈区)。

  2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外访问。(注:模块可以理解为文件)

  3)在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

      【补充】《C和指针》中说static有两层含义:指明存储属性;改变链接属性。

      具体解释:(1)全局变量(包括函数)加上static关键字后,链接属性变为internal,也就是将他们限定在了本作用域内;(2)局部变量加上static关键字后,存储属性变为静态存储,不存储在栈区,下一次将保持上一次的尾值。

  除此之外,C++中还有新用法:

  4)在类中的static成员变量意味着它为该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;

  5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量(当然,可以通过传递一个对象来访问其成员)。

 

17.虚表,基类的虚表是什么样的,派生类虚表

  (1)单继承情况

  (2)多重继承(无虚函数覆盖)

  (3)多重继承(有虚函数覆盖)

   详细的内容参考博文:关于C++虚函数表的那些事儿

 

18.volatile

  volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

  当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

  volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样一来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

 

19.#define与const的区别

  • define不会做类型检查(容易出错),const拥有类型,会执行相应的类型检查 
  • define仅仅是宏替换,不占用内存,而const会占用内存 
  • const内存效率更高,编译器可能将const变量保存在符号表中,而不会分配存储空间,这使得它成 为一个编译期间的常量,没有存储和读取的操作

  当使用#define定义一个简单的函数时,强烈建议使用内联函数替换!

 

20.C++中的强制类型转换

  • reinterpret_cast: 转换一个指针为其它类型的指针。它也允许从一个指针转换为整数类型,反之亦 然. 这个操作符能够在非相关的类型之间转换. 操作结果只是简单的从一个指针到别的指针的值的 二进制拷贝. 在类型之间指向的内容不做任何类型的检查和转换?

class A{}; 
class B{}; 
A* a = new A;
B* b = reinterpret_cast(a); 

  • static_cast: 允许执行任意的隐式转换和相反转换动作(即使它是不允许隐式的),例如:应用到类 的指针上, 意思是说它允许子类类型的指针转换为父类类型的指针(这是一个有效的隐式转换), 同 时, 也能够执行相反动作: 转换父类为它的子类

class Base {}; 
class Derive:public Base{}; 
Base* a = new Base; 
Derive *b = static_cast(a); 

  • dynamic_cast: 只用于对象的指针和引用. 当用于多态类型时,它允许任意的隐式类型转换以及相 反过程. 不过,与static_cast不同,在后一种情况里(注:即隐式转换的相反过程),dynamic_cast 会检查操作是否有效. 也就是说, 它会检查转换是否会返回一个被请求的有效的完整对象。检测在 运行时进行. 如果被转换的指针不是一个被请求的有效完整的对象指针,返回值为NULL. 对于引用 类型,会抛出bad_cast异常。
  • const_cast: 这个转换类型操纵传递对象的const属性,或者是设置或者是移除,例如:

class C{}; 
const C* a = new C; 
C *b = const_cast(a);

 

21.析构函数中抛出异常时概括性总结
  (1) C++中析构函数的执行不应该抛出异常;
  (2) 假如析构函数中抛出了异常,那么系统将变得非常危险,也许很长时间什么错误也不会发生;但也许系统有时就会莫名奇妙地崩溃而退出了,而且什么迹象也没有;
  (3) 当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外,即在析构函数内部写出完整的throw...catch()块。

 

22.C++11新特性

  Lambda、变参模板、auto、decltype、constexpr、智能指针、列表初始化、正则表达式、线程库、静态断言、委托构造。

  weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

  使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。

  weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr.

 

————————————————————————我是分割线———————————————————————

C++系列的暂时整理到这里吧,如果读者发现还有哪些这方面的经典常考知识点也请指出,待续~

转载请注明出处:http://www.cnblogs.com/webary/p/4754522.html,谢谢!

posted @ 2015-08-24 13:34  闻波  阅读(13121)  评论(3编辑  收藏  举报
友情链接: