题目

  1. 一个C++源文件从文本到可执行文件经历的过程。答案
  2. #include 的顺序以及尖叫括号和双引号的区别。答案
  3. 进程和线程的区别,为什么要有线程?答案
  4. C++11有哪些新特性?答案
  5. 为什么可变参数模板至关重要以及右值引用,完美转发,lambda答案
  6. malloc的原理,brk系统调用干什么的,mmap呢?答案
  7. C++的内存管理方式,STL的allocaotr,最新版本默认使用的分配器。答案
  8. C++/C的内存分配,栈和堆的区别,为什么栈要快?答案
  9. C++和C的区别。答案
  10. 如何判断内存泄露,野指针是什么,内存泄漏怎么办?答案
  11. 内存分布答案
  12. 讲讲虚函数(内存布局,虚函数的局限,C++11提供的类似虚函数的新函数,两种动态多态的实现及优缺点)答案
  13. 类什么时候会析构?答案
  14. 虚函数底层机制。答案
  15. C++解决内存泄漏方法。答案
  16. 查内存泄露 (valgrind)答案
  17. C++处理异常可以有两种方式,一种是throw异常,一种是在函数中
    return错误码。你怎么理解这两个方法的优劣.答案
  18. C++11 future和promise,poll,libev优点。答案
  19. 如何查看函数所占用的内存。答案
  20. C++多态是怎么实现的,哪些函数不能是虚函数?答案
  21. malloc(0)返回什么?答案
  22. 静态变量什么时候初始化?答案
  23. 为什么析构函数要是虚函数,为什么C++没有默认析构函数为虚函数。答案
  24. 模板成员函数不可以是虚函数。答案
  25. auto,shared_ptr, weak_ptr, unique_ptr,forward, move完美转发,RAII机制,lock_guard。答案
  26. 修改指向const对象的指针。答案
  27. C++中类成员的访问权限和继承权限问题。答案
  28. C++中static关键字的作用。答案
  29. vector 跟 list 的iterator有什么区别?答案
  30. cout和printf有什么区别?答案
  31. 为什么模板类一般都是放在一个h文件中?答案
  32. STL的基本组成。答案
  33. 必须使用初始化列表初始化的成员变量。答案
  34. struct和class的区别 、 union和struct的区别。答案
  35. c++获得内存的各种方式
  36. malloc和new的区别。答案
  37. delete加[]与不加[]有什么区别?数组对象的时候怎么析构函数怎么释放?答案
  38. 智能指针有没有内存泄露的情况?答案
  39. struct中为什么要字节对齐,怎么修改默认对齐方式。答案
  40. java中有没有指针,所有的都是智能指针
  41. memcpy和memmove的区别,剖析性能差异。答案
  42. volatile的作用。答案
  43. 画一下类的对象的内存布局。答案
  44. 函数重载是怎么实现的(编译器在汇编层结合函数名和参数类型给重载等函数不同的汇编实现、C/C++ 时混编要使用extern告诉编译器这是不支持重载的C的代码)。答案
  45. printf可变参数怎么实现的?答案
  46. 解释一下函数调用是怎么样的过程(怎么传参)。答案
  47. C++引用和指针的区别。答案
  48. 内联函数与宏函数的区别以及各自优缺点。答案
  49. 代码可扩展性
  50. 函数栈帧。答案
  51. 面向对象特性。答案
  52. C实现C++特性。答案
  53. 泛型编程。答案
  54. 模板底层实现原理,如何把int和T绑定?答案
  55. STL库常用的接口
  56. C语言怎么生成随机数;
  57. A* a = new A; a->i = 10;在内核中的内存分配上发生了什么?
  58. 给你一个类,里面有static,virtual,之类的,问你这个类的内存分布。
  59. 如何给指定物理地址赋值,如何跳转到制定物理地址执行。
  60. 析构函数的作用
  61. lambda的实质(编译器层面)
  62. 以下代码中这两个指针的区别,以及第一个指针离开作用域要delete,那第二个指针是否必须要delete?答案
1
2
3
4
5
6
class A;
void func() {
char buff[1024];
A* ptr1 = new ptr();
A* ptr2 = new (buff) A;
}
  1. 什么时候需要重载,重载有什么坏处
  2. 怎么实现类A可以支持以下工作?重载运算符应该是全局的还是类内成员函数,两者有什么区别?
1
2
3
class A;
A a;
a= a + 1;
  1. 如果不想一个类被继承应该怎么办?答案
  2. 宏定义和枚举的区别。 答案
  3. 函数指针和指针函数的区别。写个例子出来。答案
  4. 系统调用是什么。你用过哪些系统调用。什么系统调用会耗时长。
  5. STL中迭代器的作用,有指针为何还要迭代器。答案
  6. C++ STL 内存优化
  7. vector和list的区别的,应用,越详细越好。
  8. 给你1MB的内存,你们怎么设计, 才能使其利用率最高,产生的碎片最少
  9. C++ 类成员初始化,为什么按顺序顺序, 构造函数的调用和 代码扩展,还有初始化列表?
  10. 类成员初始化的方式。
  11. const成员函数的理解和应用。
  12. 这四者的区别:
    • const char* arr = “123”;
    • char* brr = “123”;
    • const char crr[] = “123”;
    • char drr[] = “123”;
  13. i++和++i的区别以及实现;
  14. 能写个函数在main函数执行前先运行的吗?
  15. 手写 share_ptr
  16. C++运行池
  17. 当多个文件包含头文件时,会不会存在多份vector的实现
  18. STL里resize和reserve的区别
  19. 撸一个std::lower_bound,不断优化,直到最坏复杂度也为O(logN)
  20. Q:C++里是怎么定义常量的?常量存放在内存的哪个位置?
  21. C++ 类内可以定义引用数据成员吗?
  22. 隐式类型转换,如何避免
  23. explicit关键字
  24. C++类型转换有四种
  25. 说说你了解的RTTI.答案
  26. C++函数栈空间的最大值
  27. extern “C” ?
  28. 那么C++的锁你知道几种。
  29. C语言如何处理返回值?
  30. STL中vector的实现机制
  31. STL标准库中的数据结构占用的哪一部分的内存
  32. C++ 的类中那些成员会被算入sizeof中
  33. 为什么要内存对齐
  34. 为什么会有大端小端,htol这一类函数的作用
  35. 手写一个智能指针类,怎么处理并发的情况,加锁的话怎么提高效率
  36. 说说右值引用。说说右值移动构造。一定会有控制权移交吗?为什么会有性能提升?你觉得这个新特性用的多吗?
  37. 那如果析构抛出异常怎么办?
  38. inline和virtual能够同时修饰吗?答案
  39. 一个32位的机子,最多有多少的内存,实际内存有这么多吗
  40. 模板的作用,给定一个数组和两个指针,手写一个队列模板
  41. 全局变量和static变量的区别.如果在两个.h文件中声明两个同名变量会怎么样? 如果使用extern 如果没有使用·
  42. 浅拷贝和深拷贝的区别?
  43. 如果一个函数传入一个对象,那么这是浅拷贝还是深拷贝?
  44. 形参实参的区别
  45. c++各种new的原理和应用——operator new,placement new,new operator
  46. string的length和size一样吗
  47. string是怎么增长的
  48. 虚函数,虚函数表,虚函数表是公用的吗
  49. 从汇编层去解释一下引用
  50. STL allocator
  51. iterator 与 container 之间的耦合关系
  52. Type traits的作用
  53. 传入一个char *变量,怎样求这个字符数组的长度 用strlen还是sizeof啊?
  54. 识别手机号的正则表达式
  55. C++中,::有什么意义
  56. 能否显示的调用构造函数和析构函数,举例说明
  57. 知道前向声明吗,有什么作用,什么情况下可以使用,举例
  58. 内联函数与宏函数的区别以及各自优缺点
  59. C++隐式函数举例
  60. 静态链接 动态链接
  61. Vector内存泄漏怎么办
  62. 父类指针查找虚表的过程。
  63. 函数只声明不定义会在什么时候报错?(如果不调用的话没问题,调用的话会在链接时出错)。
  64. 模板底层实现原理,如何把int和T绑定?
  65. 那vs出现链接错误,符号未定义什么情况?
  66. 手动实现STL的equal_range()

参考答案

  1. 包括四个步骤:

    • 预处理(生成.ii文件):
      • 对#define进行宏展开
      • 处理所有条件编译指令,如#if、#ifdef、#elif、#ifndef、#endif等
      • 处理#include语句,此过程是递归的
      • 删除所有的注释
      • 添加行号和文件标识
      • 保留所有的#pragma编译器指令
    • 编译(生成.s汇编文件):
      • 进行一系列词法、语法、语义分析,以及优化后生成相应的汇编代码文件。
    • 汇编(生成.o或.obj目标文件):
      • 将汇编代码转化为机器可以执行的代码,每一个汇编语句几乎都对应一个机器指令。
    • 链接(生成.out或.exe可执行文件):
      • 主要包括地址和空间分配、符号决议和重定位。
        返回原题
    • 尖括号:包含系统标准库头文件时使用,从系统文件目录下查找;
    • 双引号:包含自定义头文件时使用,从当前源文件目录下查找。返回原题
    • 区别:
      • 进程和线程是操作系统不同的资源管理方式。
      • 进程拥有独立的地址空间,在保护模式下,进程崩溃不会影响其它进程,而线程之间共享内存,线程崩溃,会导致同一进程下的其它线程崩溃。
      • 一个程序至少有一个进程,一个进程至少有一个线程。
      • 进程是资源分配的最小单位,线程是CPU调度的最小单位。
      • 不同进程间资源很难共享,但同一进程下的线程间则很容易。
    • 为什么要有线程?
      • 线程相比进程而言,更加轻量、更易创建和销毁。
      • 同一进程中的线程之间共享内存,使得它们之间互相通信非常容易,互相切换也更加容易,极大地提高了运行效率;
      • 线程的划分尺度小于进程,使得多线程的程序并发性高;
        返回原题
    • 对象结构改进:

      • 继承基类的构造函数(using 基类名:基类名);
      • 默认成员值,在声明数据的时候赋一个默认值;
      • 委托构造函数,用其它构造函数来做它的一部分或全部工作;
      • override关键字,防止因拼写错误而没有正确重载成员函数;
      • 可以使用花括号初始化一个类的实例;
      • final关键字,防止成员函数被派生类重载 ;
    • 其他核心语言新增功能:

      • 新增基于范围的for循环(for (auto i: array));

      • 自动类型推断auto;

      • 匿名函数lambda:[捕获列表](参数列表)->返回类型{函数体};

      • long long int类型,通常是64位;

      • 移动语义:如果两个类对象较大时,交换起来非常耗时,这时便可以只转换所有权。例如:

        1
        2
        3
        4
        5
        void swap(type& a, type& b) { 
        type t = std::move(a);
        a = std::move(b);
        b = std::move(t);
        }
      • 右值引用:可以绑定即将销毁的右值。

        1
        type&& rr = rvalue;
      • 智能指针:shared_ptr和unique_ptr,取代了auto_ptr,防止内存泄漏;

      • 原始字符串字面量,可以使用R前缀,将不会识别转义序列。如

        1
        string s = R"C:\user\temp"
      • 静态断言:使用static_assert使在编译时如果未满足条件则对错误信息打标记,这在模板中使用非常方便;

      • 可变参数模板:可使用任意数量参数。如

        1
        2
        3
        4
        5
        template<typename T, typename... TArgs> 
        printv(T v, TArgs... args) {
        cout << v << endl;
        printv(args...);
        }
      • 后置返回类型:

        1
        2
        3
        4
        template auto adding_func(const Lhs &lhs, const Rhs &rhs) -> decltype(lhs+rhs) 
        {
        return lhs + rhs;
        }
      • 空指针常量nullptr;

      • static_cast将左值转化为右值引用;

      • constexpr将变量或函数声明为字面值类型;

    • 标准库扩展:

      • 标准库元组模板tuple;

      • 随机数库random:

        1
        2
        3
        uniform_int_distribution<unsigned> u(0,9);
        default_random_engine e;
        cout << u(3) << endl;
      • 正则表达式库regex
        返回原题

    • 可变参数模板:当我们既不知道想要处理的实参的数目,也不知道它们的类型时,可变参数模板是很有用的。格式:

      1
      2
      3
      4
      template<typename T, typename ...Ts> 
      void func(T v, Ts... args) {
      func(args...);
      }
    • 完美转发:使用forward转发参数时会保留参数的左右值类型(格式:std::forward<T>(t)

    • 右值引用:必须绑定到右值的引用(typename&& val)

    • lambda函数:匿名函数,可以使代码变得简洁,不用费力去定义一个小函数。格式:[捕获列表](参数列表)->返回类型{函数体},参数列表和返回类型可省略。
      返回原题

    • malloc原理:根据用户要求,从堆里分配内存空间。为减少内存碎片和降低内存开销,采用内存池的方式。malloc利用隐式链表,在分配时遍历整个链表,选择合适的内存块分配。
    • brk和mmap系统:内存分配会调用brk或mmap系统,小于128k时调用brk在堆区分配,大于128k时调用mmap在映射区分配。
      返回原题
    • C++内存管理方式:new和delete;
    • STL中的Allocator:负责配置内存,但不负责构造对象(只相当于new的第一步)。为减少内存碎片,STL配置内存时采用两级配置器。当所需空间大于128B时采用第一级空间配置器,即使用malloc()等,小于128B时,采用第二级空间配置器,即内存池技术,通过空闲链表管理内存;
    • 默认分配器:std::alloc。
      返回原题
    • 栈和堆的区别:。
      • 分配方式:栈有动态分配和静态分配两种方式,而堆只有动态分配;
      • 分配大小:栈是由低地址扩展的数据结构,是一块连续的内存区域;而堆是由高地址扩展的数据结构,是不连续的内存区域;
      • 管理方式:栈由编译器自动分配和释放,而堆一般由程序员主动申请分配和释放,如果未能及时释放,可能会造成内存泄漏。
    • 栈更快的原因:
      • 栈是放在一块连续的内存区域上,所以数据定位的速度更快;而堆放在不连续的内存区域上,通过链表访问,访问效率更低;
      • 栈是CPU提供指令支持的,在指令的处理速度上,对栈数据的处理速度自然比操作系统支持的堆数据要快;
      • 栈放在一级缓存中,而堆放在二级缓存中,二者硬件性能差异巨大。
        返回原题
    • C是面向过程的,C++是面向对象的,有封装、继承、多态三大特性;
    • C++是C的超集,有虚函数、内联函数、友元函数、引用、命名空间等;
    • 动态分配内存时,C是malloc/free,而C++是new/delete;
    • 输入和输出,C是scanf/printf,而C++是cin/cout;
      返回原题
    • 内存泄漏:申请的一块内存没能及时释放,且没有指向它的指针了;
    • 野指针:指向不明区域的指针。原因:没有初始化的指针;指向堆区域的指针在释放内存后,没有置为nullptr;
    • 检查内存泄漏:
      • 使用Linux环境下的内存泄漏检测工具Valgrind进行检测;
      • 写代码时添加内存分配和释放的统计功能,统计当前申请和释放的次数是否一致;
      • 使用BoundsChecker,BoundsChecker是一个运行时错误检测工具,它主要用于定位程序运行时所发生的错误;
      • 调试运行DEBUG版程序,运用以下技术:CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号(集成开发环境OUTPUT窗口),综合分析内存泄漏的原因,排除内存泄漏。
    • 如何解决内存泄漏:养成良好的写代码习惯,自己动态分配的内存空间,一定要记得及时释放;使用智能指针。
      返回原题
    • 栈区:由编译器自动分配和释放,存放局部变量、函数参数值等,类似于数据结构中的堆;
    • 堆区:由程序员主动申请分配和释放;
    • 代码区:存放函数体的二进制代码;
    • 全局(静态)区:用于存放全局变量、静态全局变量、静态局部变量;
    • 文字常量区:用于存放常量字符串。
      返回原题
    • 虚函数内存布局:如果一个类定义了虚函数,则会生成一个虚函数指针,一般放在对象内存布局的第一个位置上(即对象地址为虚函数地址),虚函数指针指针虚函数表,虚函数表中存放的是一系列虚函数的地址,虚函数地址的顺序与类中虚函数声明的顺序一致。
    • 虚函数的局限:子类虚函数表与父类虚函数表的关系要么是继承、要么是重写、要么是新增,而对于继承的虚方法需要重复存储,浪费空间。
    • 两种多态:
      • 静态多态:泛型编程(模板函数)和函数重载,编译时关联。
        • 优点:类型安全性较高、耦合性低,集合的元素类型不再局限于指针,具体类可以只实现需要的接口,生成代码性能高;
        • 缺点:不能处理异类集合,可执行代码较大,模板库源码需要发布,对模板实参类型有约束。
      • 动态多态:虚函数,运行时关联。
        • 优点:能处理异类集合,可执行代码较小,可以完全编译而无需发布源码;
        • 缺点:耦合性高(继承 的耦合性高于组合),类型安全性差(编译期无类型检查),性能低(层层继承)。
          返回原题
    • 撤销类对象时;
    • 超出作用域时;
    • 动态分配的对象只有在指向该对象的指针被删除时才调用析构函数。
      返回原题
    • 对于含有虚函数的类或继承自有虚函数的类,每个类使用一个虚函数表,每个类对象使用一个虚表指针,虚表指针指向虚函数表;
    • 基类对象有一个虚表指针,指向基类的虚函数表,派生类对象也有一个虚表指针,指向派生类的虚函数表;
      • 若派生类重写了基类中的虚函数,则该派生类的虚函数表将保存重写的虚函数地址,而非基类的虚函数地址;
      • 若派生类没有重写基类中的虚函数,则继承下来,派生类的虚函数表将保存基类中未被重写的虚函数地址;
      • 若派生类新定义了新的虚函数,则该派生类的虚函数表将添加保存新虚函数的地址。
        返回原题
    • 使用智能指针,无需手动释放内存。
    • 养成良好的写代码习惯。
      返回原题
    • 使用Valgrind软件检测;
    • 写代码时统计内存分配和释放的次数,统计当前二者次数是否一致;
    • 使用BoundsChecker软件检测;
    • 调试运行DEBUG版程序,使用以下技术:CRT(C Run-Time Libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号,综合分析内存泄漏的原因,排除内存泄漏。
      返回原题
    • 使用throw:代码更加简洁,可以不用通过判断返回值就可以确定是否发生异常。不能被忽略,必须停下来处理。
    • 使用错误码返回值:更加轻量级。但需要通过查找错误码才能知道具体发生的错误。错误信息可以被忽略。
      返回原题
    • future(只读):提供了一种访问异步操作结果的机制。有三种状态:deferred(异步操作还没开始)、ready(异步操作已经完成)、timeout(异步操作完成)。
    • promise(只写):每个promise关联一个future,对promise的写入会令future的值可用。
    • libev:高性能事件循环。
    • poll:未知。。。
      返回原题
    • 多态分为动态多态和静态多态:
      • 动态多态:通过虚函数实现。运行时通过指针指向的对象进行动态绑定。
      • 静态多态:通过泛型编程(模板函数)和重载函数实现。编译时关联。
    • 以下函数不能为虚函数:
      • 普通函数:只能被重载,不能被覆盖,声明为虚函数没啥意义;
      • 构造函数:虚函数是在不同对象产生不同的操作,构造函数之前对象还没产生,就无法使用虚函数;假设B类继承自A类,且A类的构造函数为虚函数,则构造B类对象时,只会执行B类的构造函数,这样A就不能构造了。
      • 友元函数:C++不支持友元函数的继承;
      • 静态成员函数:静态成员函数对每个类而言,只有一份代码,所有对象共享,而不归某个类共享,所以没有动态绑定的必要性。
    • 以下函数没有声明为虚函数的必要:
      • 内联函数:即使内联函数被声明为虚函数,编译器遇到这种情况不会把函数内联展开,而是当作普通函数处理;
      • 赋值运算符:赋值运算符要求形参和类本身类型相同,帮基类中的赋值操作符形参为基类类型,即使声明为虚函数,也不能作为子类的赋值操作符。
        返回原题
    • 返回一个nullptr或一个不能用于访问对象的非null的合法指针,可以调用free()函数释放。
    • malloc(-1)返回一个NULL,因为-1转化为无符号整型后是一个很大的值,无法分配这么大的内存空间,所以只能返回NULL。
      返回原题
    • 静态变量的初始化是在编译时进行,只初始化一次,赋值则发生在运行时。
      返回原题
    • 在删除指向派生类的基类类型指针时,如果析构函数不是虚函数,则只会调用基类的析构函数,而不会调用派生类的析构函数,会发生内存泄漏。
    • 为何默认析构函数不是虚函数:如果无需用到继承,虚析构函数便没有任何作用,但虚表指针和虚函数表的存在会导致资源浪费。
      返回原题
    • 原因:模板成员函数可以实例化出很多类型,不同的参数实例化出不同的类型,因此虚函数表中需要在编译时装入所有实例类型,无法实现,而且内存消耗代价过高,因此不能将模板成员函数定义为虚函数。
      返回原题
    • auto:由编译器去分析表达式所属的类型;
    • shared_ptr:智能指针,允许多个shared_ptr指针指向同一个对象,采用引用计数,当计数为0时,自动释放自己所管理的对象;
    • unique_ptr:一个unique_ptr拥有它所指向的对象,某个时刻只能有一个unique_ptr指向一个给定对象,当unique_ptr被销毁时,它所指向的对象也被销毁。不支持普通的拷贝和赋值操作。
    • weak_ptr:是一个不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变其引用计数。一旦最后一个指向对象的shared_ptr被销毁时,即使有weak_ptr指向对象,对象也会被释放。
    • move:将一个左值转为对应的右值引用类型。
    • forward:使用forward转发参数时会保留参数的左右值类型。格式:std::forward<T>(t)
    • 完美转发:使用模板函数转发参数时,会保留参数的左右值类型。
    • RAII机制(Resourse Acquisition Is Initialization,资源获取即初始化):用于管理资源、避免泄漏。通过把资源用类封装起来,对资源操作都在类的内部,在析构函数内释放资源,从而当定义的局部变量生命期结束时,它的析构函数就会自动地被调用,便无需程序员显示地去调用释放资源的操作了。
    • lock_guard:做互斥量mutex的RAII。在构造lock_guard对象时,传入的mutex对象会被当前线程锁住,析构时,它所管理的mutex对象会自动解锁。lock_guard并不负责管理mutex对象的生命周期,只是简化了其上锁和解锁操作,而且不必担心异常安全问题。
      返回原题
    • 使用const_cast:例如:

      1
      2
      3
      const char c = 'C'; 
      const char* pc = &c;
      char* p = const_cast<char*>(pc);

      返回原题

    • 访问权限:private成员仅供类内部成员和友元函数访问;protected成员在没有继承时和private访问权限相同;public成员可供类内部成员、友元函数、类对象实例访问。
    • 继承时的访问权限:
      • public派生:基类private在派生中不可见,基类protected和private在派生类中保留原有访问权限;派生类实例对象仅可访问基类的public成员;
      • protected派生:基类private在派生类中不可见,基类protected和private在派生类中均变成protected成员;派生类对象不可访问基类的任何成员;
      • private派生:基类private在派生类中不可见,基类protected和private在派生类中均变成private成员;派生类对象不可访问基类中的任何成员;
        返回原题
    • 改变生命期:将其存储在全局(静态)区,程序运行结束后,才释放内存;
    • 改变作用域:将全局变量和函数声明为static后,会将其对其他源文件隐藏;
    • 默认初始化为0:全局静态区中所有字节默认都是0x00。
      返回原题
    • vector的iterator在删除或增加一个元素后,后面的就可能失效了;而list的iterator在删除或添加元素后还可以继续使用。
    • vector的iterator支持“+”、“+=”、“>”等操作符,而由于list在内存空间上不是连续的,所以list的iterator不支持以上操作,只支持“++”。
      返回原题
    • printf需要指定格式,而cout无需指定格式;
    • printf是函数,而cout是ostream对象,和<<搭配使用;
    • printf是实参函数,没有类型检查,不安全;cout是通过运算符重载实现的,安全;
    • printf会将输出立刻显示在屏幕上,而cout会先存入缓冲区,在进行刷新缓冲操作后才会显示在屏幕上(常见缓冲刷新操作:flush,endl,调用cerr、clog、cin,退出程序);
    • printf遇到不认识的类型的就没办法了,而cout对于不认识的类型可以通过重载来解决。
      返回原题
    • 当不使用模板类时,编译器不会去实例化它;当使用它时,编译器才会去实例化它。
    • 编译器一次只能处理一个单元,即处理一个cpp文件,所以实例化时需要看到模板的完整定义,因此需要放到.h文件中。
    • cpp在编译期间不能决定模板参数的类型,所以不能生成模板函数的实例,因此会把模板类型带到链接期间,如果这个期间有函数调用了该实例,而由于没有把模板实例到特定类型,就会导致编译错误。
      返回原题
    • 容器:可容纳一些数据的模板类。
    • 迭代器:用于遍历容器中的数据对象。
    • 算法:处理容器中数据的方法或操作。
    • 分配器:给容器分配存储空间。
    • 仿函数:用于协助算法完成各种操作。
    • 配接器:用于套接适配仿函数。
      返回原题
    • const修饰的类成员或引用成员:在声明后马上要初始化,在构造函数中的操作为赋值,是不被允许的。
    • 子类初始化父类的私有成员:需要在参数初始化列表中显示调用父类构造函数;
    • 对象成员:数据成员是对象,并且对象只有含参数的构造函数。
      返回原题
    • struct和class的区别:
      • 都可以定义类,都可以用于继承。但struct的默认访问权限和继承方式是public,而class默认是private;
      • class可定义模板形参;
      • struct可以在定义的时候直接以{}对其成员赋初值,而class不可以。
    • struct和union的区别:
      • 两者都是用不同的数据类型成员组成,但在任何特定时刻,共用体只存放一个被选中的成员,而结构体则存放所有的成员变量;
      • 对共用体的不同成员赋值,会将原有值覆盖,而对结构体的不同成员赋值是互不影响的;
      • struct各成员有自己独立的内存空间,同时存在;而struct的所有成员不能同时占用它的内存空间,不能同时存在;
      • sizeof(struct)会返回对齐之后所有成员大小相加之和,而sizeof(union)返回最大成员变量的大小。
        返回原题
    • malloc是c语言函数,而new是C++的运算符;
    • 都用于申请动态内存,但new比malloc更加智能,new在对象创建的时候自动执行构造函数;
    • new返回指定类型的指针,并自动计算出所需要的大小,而malloc必须用户指定大小,并且返回类型为void*,必须强行转换为实际类型的指针。
      返回原题
    • delete释放new分配的单个对象指针指向的内存,delete[]释放new分配的对象数组指针指向的内存;

    • 对于简单的类型,使用new分配的不管是数组不是非数组形式内存空间,用两种方式均可。

      1
      2
      3
      int *a = new int[10];
      delete a;
      delete[] a; // 与上式等价
    • 对于类类型,如果使用new分配的是数组形式的内存空间a[10],用delete只会调用a[0]的析构函数,从而造成内存泄漏,因此此种情况下只能用delete[]。
      返回原题

    • 循环引用时可引起内存泄漏,即两个shared_ptr相互引用,引用计数都是1,不能自动释放。
    • 解决办法:使用弱引用weak_ptr来打破循环引用。
      返回原题
    • 对齐的原因:
      • 各个硬件平台对存储空间的处理上有很大的不同,一些平台对某些特定类型的数据只能从某些特定地址开始存取。
      • 效率上的考虑。比如一个int型数据存放在偶地址开始的地方,那么一个读周期就可以32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对再次读出的结果的高低字节进行拼凑才能得到32bit数据。
    • 修改对齐:
      • 使用伪指令#pragma pack(n),编译器将按照n个字节对齐;
      • 使用伪指令#pragma pack(),取消自定义字节对齐方式。
        返回原题
    • memcpy:
      • 形式:void *memcpy(void *dest, const void *src, size_t n);
      • 描述:memcpy()函数从src内存中白拷贝n个字节到dest内存区域,但是源和目的内存区域不能重叠。返回指向dest的指针。
    • memmove:
      • 形式:void *memmove(void *dest, const void *src, size_t n);
      • 描述:memmove()函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存可以重叠。返回一个指向dest的指针。
    • 唯一区别:memmove()允许源和目的区域重叠,而memcpy()不允许。
      返回原题
    • 描述:一个定义为volatile的变量是说这变量可能会被意想不到地改变,于是编译器就不会去优化这个变量的值了。即优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
    • 用处:
      • 硬件寄存器(如:状态寄存器)
      • 一个中断服务子程序中会访问到的非自动变量
      • 多线程应用中被几个任务共享的变量
        返回原题
    • 函数重载:在同一作用域内,可以有一组具有相同函数名、不同参数列表的函数,这组函数被称为重载函数。
    • 底层实现:C++利用name mangling(倾轧)技术,即在编译过程中,将函数、变量的名称重新改编的机制,来改变函数名,区分参数不同的同名函数。对于函数void foo(int x, int y);,在被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int的名字,所以编译之后C仍然无法辨别函数名相同、参数不同的函数。
      返回原题
    • printf是从右向左进行入栈的,后面的变量入栈后,前面的字符串再入栈。
    • printf的实现中有va_arg、va_start等函数,用于解析字符串,检查有多少个%d、%x等之类的符号,通过这个确认后面跟了多少参数。
    • 详见:C可变参数实现原理
      返回原题
    • 参数入栈:将参数按照调用约定(C是从右向左)依次压入系统栈中;
    • 返回地址入栈:将函数调用语句的下一条语句的地址保存在栈中,以便函数调用完成后返回;
    • 跳转到函数体处;
    • 如果函数体内定义了变量,将变量压栈;
    • 将每一个形参以栈中对应的实参值取代,执行函数体的功能体;
    • 将函数体中的变量、保存到栈中的实参值,依次从栈中取出,释放栈空间(出栈);
    • 执行return语句返回。从栈中取出刚开始调用函数时压入的地址,跳转到函数的下一条语句。当return语句不带有表达式时,按照保存的地址返回;否则将计算出的return表达式的值保存起来,然后再返回。
      返回原题
    • 指针是一个变量,存储一个地址,指向内存中的一个单元,而引用是另一个变量的别名;
    • 指针和引用的自增运算意义不一样;
    • 对指针进行sizeof()操作,得到的是指针本身的大小,而对引用进行sizeof()操作,得到的是对象的大小;
    • 有指向指针的指针,即多级指针,但没有多级引用;
    • 引用定义时必须赋初值,而指针定义时可以不用赋初值;
    • 有const指针,但没有const引用;
    • 指针的值在初始化之后可以改变,即指向其它的存储单元,而引用在进行初始化后就不能再改变了
      返回原题
    • 区别:
      • 展开的时间不同:宏函数由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。
      • 宏函数只是做简单的文本替换,编译内联函数可以嵌入到目标代码;
      • 内联函数会做类型、语法检查,而宏函数不具有这样的功能。
      • 内联函数是函数,而宏函数不是;
    • 宏函数:
      • 优点:宏在调用的地方,仅仅是参数的替换,不会出现函数调用那种压栈、出栈时的时间和空间的开销,执行效率高;
      • 缺点:没有检查参数类型是否匹配,不安全。
    • 内联函数:
      • 优点:函数代码被放入符号表中,在使用时进行替换,效率很高;会有类型检查,因此够安全;可以作为类的成员函数,可以使用所在类的保护成员及私有成员。
      • 缺点:如果函数代码过长,使用内联将消耗过多内存;如果函数体内有循环,那么 执行函数代码时间比调用开销大。
        返回原题
    • 函数桢栈:本质是一种栈,专门用于保存函数调用过程中的各种信息(参数、返回地址、本地变量)。每调用一个函数,就会生成一个新的栈桢。
    • 桢栈包括:函数的返回地址和参数、临时变量、函数调用的上下文;
    • 函数调用参考:第46题
      返回原题
    • 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏;
    • 继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。继承的过程就是从一般到特殊的过程。
      • 继承的实现方式有三类:实现继承、接口继承和可视继承。
      • 实现继承:使用基类的属性和方法而无需额外编码的能力;(非虚函数)
      • 接口继承:仅使用属性和方法的名称,但是子类必须提供实现的能力;(纯虚函数)
      • 可视继承:子窗体(类)使用基窗体(类)的外观和实现代码的能力。(虚函数)
    • 多态:C++多态意味着调用成员函数时,会根据调用的对象的类型来执行不同的函数。
      • 多态的实现方式有两种:覆盖和重载。
      • 静态多态和动态多态。
        返回原题
    • C语言实现封装性:将属性和方法(函数指针)封装到特定结构体中;
    • C语言实现继承性:使用组合,即在新有类中定义已有类的对象,就可以在新类中通过已有类的对象访问已有类自己的成员变量与函数;
    • C语言实现多态:通过函数覆盖实现。
      返回原题
    • 泛型编程:以独立于任何特定类型的方式编写代码。
    • 在C++中,模板是泛型编程的基础。
      返回原题
    • 编译器并不是把函数模板处理成能够处理任意类的函数。编译器从函数模板通过具体类型产生不同的函数,编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
      返回原题
    • 将类的构造函数和析构函数设为private:缺点是不能定义类的对象,解决办法是写一个静态方法来创建和删除类对象,因而只能在堆上建立对象;

    • 使用虚继承:既可在堆上创建对象,也可在栈上创建对象,代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      template<typename T>
      class Base {
      public:
      friend T;
      private:
      Base() {}
      ~Base() {}
      }
      // 必须是虚继承:
      // 若非虚继承,则由于每个类只初始化自己的直接基类,因此A初始化FinalClass,FinalClass初始化Base类,不会报错;
      // 若是虚继承,则由最低层次的派生类构造函数初始化虚基类,而A不是Base的友元,无法调用其私有构造函数,帮编译报错。
      class FinalClass: virtual public Base<FinalClass> {
      public:
      FinalClass() {}
      }

      class A: public FinalClass {
      public:
      A(); // 继承时报错,无法通过编译
      // 解释:由于FinalClass使用了虚继承,所以要创建A对象,A类的构造函数就要先构造Base类的构造函数,
      // 而Base类的构造函数为私有成员,仅能友元FinalClass访问(友元无法继承),所以无法构造Base类,
      // 因而编译错误。
      }

      返回原题

    • 宏定义是在预处理阶段进行的文本替换,而枚举是在编译的阶段进行的处理;
    • 宏作为指令存储在代码段,而枚举作为常量,存储在数据区;
    • 枚举常量具有类型,而宏没有类型;
    • 枚举一次可以定义大量相关的常量,而宏只能定义一个。
      返回原题
    • 函数指针:指向函数的指针,如int(*pf)(char, bool),为指向返回类型为int、参数为(char, bool)的函数的指针。
    • 指针函数:返回值会指针的函数。如int* fun(char a, bool b),其本身是个函数,返回值为int*型指针。
      返回原题
    • 迭代器和指针的区别:迭代器不是指针,是类模板,但表现得像指针(通过重载指针的一些运算符*、->、++、–等,来模拟指针的功能)。迭代器本质是封装了原生指针,提供了比指针更高级的行为。迭代器返回的是对象的引用而非对象的值。
    • 为何要用迭代器:迭代器就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的结果。
      返回原题
  • inline函数是将该函数的代码展开到调用该函数的地方,所以inline函数是没有地址的。
  • virtual函数的调用时在程序运行时根据调用函数的对象时父类对象还是子类对象来决定该调用哪一个虚函数,其中的原理就是通过vptr,vtbl等实现的,vptr指向vtbl中的虚函数,vbtl保存了虚函数的地址入口,所以每个虚函数都有个地址,这个地址保存在对应的虚函数表中,这个显然与inline函数没有地址是相违背的,所以inline和virtual是存在冲突的。
    返回原题
    • RTTI(Run-Time Type Idetification,运行时类型检查),提供了运行时确定类对象类型的方法。
    • RTTI提供了两个有用的操作符:
      • typeid:返回指针或对象的实际类型。
      • dynamic_cast:将基类指针或引用安全地转化为派生类的指针或引用。
        返回原题