C++ 面试基础之一


C/C++的区别和联系

联系

C++是C的超集,兼容C的大部分的语法。

区别

  • C是面向过程的语言,C++是面向对象的语言
  • C不支持函数的重载,C++支持
  • C不支持引用,C++支持引用
  • C++全部变量的默认链接属性是外链接,而C是内连接。
  • C和C++的动态管理内存的方法不一样,C是使用malloc/free函数,而C++除此之外还有new/delete关键字。
  • C++的类是C所没有的。但是C中的struct是可以在C++中正常使用的,并且C++对struct进行了进一步的扩展,使struct在C++中可以和class一样当做类使用,而唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private。

C/C++程序占用的内存空间

  • 堆区:由程序员申请或者释放内存。操作方式是先进先出。
  • 栈区:由编译器自动分配和释放内存。栈存储着函数的参数以及局部的变量。操作方式是先进后出。
  • 全局区:全局变量和静态变量存储在这里。初始化的全局变量和静态变量在一块区域(DATA段), 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(BSS段)。- 程序结束后由系统释放。
  • 常量存储区:存储程序中的常量,不允许修改,程序结束后由系统释放。程序结束后由系统释放。
  • 代码区:存放程序的可执行的二进制的代码。
    |----------------------| 高地址
    | 栈区(Statk) | -->向下增长
    |----------------------|
    | 堆区(Heap) | -->向上增长
    |----------------------|
    | 未初始化(BSS) |
    |----------------------|
    | 初始化(Data) |
    |----------------------|
    | 常量存储区 |
    |----------------------|
    | 正文段(Text) |
    |----------------------| 低地址

C++程序编译链接的过程

  1. 预处理是 C 语言程序从源代码变成可执行程序的第一步,主要是 C 语言编译器对各种预处理命令进行处理,包括头文件的包含、宏定义的扩展、条件编译的选择等。
  2. 编译:编译之前,C 语言编译器会进行词法分析、语法分析 (-fsyntax-only) ,接着会把源代码翻译成中间语言,即汇编语言 。 编译程序工作时,先分析,后综合,从而得到目标程序。所谓分析,是指词法分析和语法分析;所谓综合是指代码优化,存储分配和代码生成。
  3. 汇编:将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。
  4. 链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。
    由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
    链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
    C++ 面试基础之一_第1张图片
    编译过程

    https://www.cnblogs.com/kekec/p/3238741.html
    https://www.cnblogs.com/Lynn-Zhang/p/5377024.html

struct和class的区别

  • struct是C中定义结构体用的关键字,C++对其进行了扩展,使其可以像C++中class一样使用。
  • struct定义的结构体默认的访问的权限是public,而class定义的类的默认访问权限是private。

const关键字

  • 修饰变量,表示变量不可改变
  • 修饰指针,可表示常量指针或者指针常量
  • 常量引用,常用于修饰函数的形参的引用类型。避免了参数的拷贝,而且避免方法对于参数值的修改。
  • 修饰成员函数,说明该成员函数内不能修改成员变量。

volatile

  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
  • 使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据。

static

  • 修饰普通变量,变量存储在C++内存中的全局区。在 main 函数运行前就分配了空间。
  • 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。
  • 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  • 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

什么时候开始分配并初始化的
全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化。这里的变量包含内置数据类型和自定义类型的对象。

https://www.cnblogs.com/33debug/p/7223869.html
https://www.cnblogs.com/kkdd-2013/archive/2013/12/05/3459888.html (比较详细)
https://segmentfault.com/q/1010000004157283/a-1020000004158311 (什么时候初始化)

指针和引用的区别

  • 指针是一个实体,而引用仅是个别名;
  • 引用必须被初始化,指针不必;
  • 引用只能在定义时被初始化一次,之后不可变;指针可以改变所指的对象;
  • 可以有const指针,但是没有const引用;
  • 不存在指向空值的引用,但是存在指向空值的指针,即引用不能为空,指针可以为空;
  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
  • 指针和引用的自增(++)运算意义不一样;
  • 程序为指针变量分配内存区域,而引用不需要分配内存区域;
    https://blog.csdn.net/wanwenweifly4/article/details/6739687

inline 内联函数

特征

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 不能包含循环、递归、switch 等复杂操作;

编译器对inline函数的处理步骤

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。

优缺点

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

C++多态与虚函数的实现原理

1. 概述

虚函数

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

虚函数表

对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。

  • C++ 面试基础之一_第2张图片
    image.png

    每一个含有虚函数的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。
    image.png

    https://www.cnblogs.com/wangxiaobao/p/5850949.html

虚函数表、虚函数、以及虚函数表的指针在内存中的位置

  • 虚函数表的指针是在实例的对象之中。
  • 虚函数表在内存中的全局区里面(每一个类只有一份虚函数表)。
  • 函数存储在内存中的代码段里面(每个个函数在内存中也只有一份)。
  • C++ 面试基础之一_第3张图片
    image.png

2. 虚函数表的构造过程

编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式(仅供参考):

  • C++ 面试基础之一_第4张图片
    image.png

3. 虚函数的调用过程

  • C++ 面试基础之一_第5张图片
    image.png

编译器只知道pb是 B* 类型的指针,并不知道它指向的具体对象类型 :pb可能指向的是B的对象,也可能指向的是D的对象。
但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。
无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。

4. 多重继承

当一个类继承多个类,且多个基类都有虚函数时,子类对象中将包含多个虚函数表的指针(即多个vptr),例:

  • C++ 面试基础之一_第6张图片
    image.png

其中:D自身的虚函数与B基类共用了同一个虚函数表,因此也称B为D的主基类(primary base class)。

虚函数替换过程与前面描述类似,只是多了一个虚函数表,多了一次拷贝和替换的过程。

虚函数的调用过程,与前面描述基本类似,区别在于基类指针指向的位置可能不是派生类对象的起始位置,以如下面的程序为例:

  • C++ 面试基础之一_第7张图片
    image.png

内存分配和管理

malloc、calloc、realloc、alloca

  • malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
  • calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
  • realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
  • alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。

C++ new、delete关键字

  • new/new[]:先底层调用 malloc 分了配内存,然后创建一个对象(调用构造函数)。
  • delete/delete[]:先调用析构函数(清理资源),然后底层调用 free 释放空间。
  • new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。

delete this

  • 保证对象是new方式分配的。
  • 保证调用delete this的成员函数是最后一个调用 this 的成员函数。
  • 保证成员函数的 delete this 后面没有调用 this 。
  • 保证delete this后不再使用该对象。

coredump

https://blog.csdn.net/sunxiaopengsun/article/details/72974548
http://man.linuxde.net/ulimit

参考文章

https://www.cnblogs.com/malecrab/p/5572730.html
https://www.nowcoder.com/questionTerminal/ae72481f412e4c709d00bdca1c78feaf
https://www.cnblogs.com/gofighting/p/5440012.html

你可能感兴趣的:(C++ 面试基础之一)