C/C++整理

Catalogue
  1. 1. 一、基础知识
    1. 1.1. static关键字作用
    2. 1.2. c++中四种 cast 转换
      1. 1.2.1. 为什么不使用 C 的强制转换?
    3. 1.3. C/C++ 中指针和引用的区别?
    4. 1.4. 野指针是什么?
    5. 1.5. 智能指针有没有内存泄露的情况?
      1. 1.5.1. 智能指针的内存泄漏如何解决?
    6. 1.6. 函数指针
    7. 1.7. 为什么析构函数必须是虚函数?为什么 C++默认的析构函数不是虚函数
    8. 1.8. C++中析构函数的作用
    9. 1.9. 静态函数和虚函数的区别
    10. 1.10. 重载和覆盖
    11. 1.11. 虚函数和多态
    12. 1.12. 虚函数表具体是怎样实现运行时多态的?
    13. 1.13. ++i 和 i++的实现
    14. 1.14. C++里是怎么定义常量的?常量存放在内存的哪个位置?
    15. 1.15. extern “C”
    16. 1.16. new/delete 与 malloc/free 的区别是什么
    17. 1.17. C++中拷贝赋值函数的形参能否进行值传递?
  2. 2. STL容器
    1. 2.1. map 和 set 有什么区别,分别又是怎么实现的?
    2. 2.2. STL 迭代器删除元素
    3. 2.3. vector 和 list 的区别,应用
    4. 2.4. STL 中迭代器的作用,有指针为何还要迭代器
    5. 2.5. STL 里 resize 和 reserve 的区别
  3. 3. 类和数据抽象
    1. 3.1. C++中类成员的访问权限
    2. 3.2. C++中 struct 和 class 的区别
  4. 4. 泛型编程
    1. 4.1. 什么是右值引用,跟左值又有什么区别?
  5. 5. 编译与底层
    1. 5.1. C++源文件从文本到可执行文件经历的过程
    2. 5.2. include 头文件的顺序以及双引号””和尖括号<>的区别?
    3. 5.3. malloc 的原理
    4. 5.4. new 和 malloc 的区别
    5. 5.5. C++的内存管理是怎样的
    6. 5.6. 什么是 memory leak,也就是内存泄漏
    7. 5.7. 内存泄漏分类
    8. 5.8. 如何判断内存泄漏?
    9. 5.9. C++如何处理内存泄漏?
    10. 5.10. 什么时候会发生段错误
    11. 5.11. 如何采用单线程的方式处理高并发
  6. 6. C++11
    1. 6.1. C++11 有哪些新特性?
    2. 6.2. 介绍一下 C++11 中的可变参数模板、右值引用和 lambda 这几个新特性
      1. 6.2.1. 可变参数模板:
      2. 6.2.2. 右值引用
      3. 6.2.3. 移动语义
      4. 6.2.4. 完美转发
      5. 6.2.5. Lambda 表达式:
  7. 7. 操作系统
    1. 7.1. 进程与线程的概念,以及为什么要有进程线程,其中有什么区别?
    2. 7.2. 进程间的通信方式
    3. 7.3. 线程间通信的方式?
    4. 7.4. Linux 虚拟地址空间
    5. 7.5. 操作系统中的程序的内存结构?
    6. 7.6. 操作系统中的缺页中断?
    7. 7.7. 操作系统中的页表寻址?
    8. 7.8. 并发(concurrency)和并行(parallelism)
    9. 7.9. 各数据库的默认端口

一、基础知识

static关键字作用

  1. 全局静态变量

在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量

存放地址:静态存储区

初始化:未经初始化的全局静态变量会被自动初始化为 0

作用域:全局静态变量在声明他的文件之外是不可见的, 准确地说是从定义之处开始, 到文件结尾。

  1. 局部静态变量

在局部变量之前加上关键字 static,局部变量就成为一个局部静态变量。

存放地址:静态存储区

初始化:自动初始化为 0

作用域:作用域仍为局部作用域

  1. 静态函数

在函数返回类型前加 static,函数就定义为静态函数。函数的定义和声明在默认情况下都是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用(函数的实现使用 static 修饰,那么这个函数只可在本 cpp 内使用)

  1. 类的静态成员

在类中, 静态成员可以实现多个对象之间的数据共享, 并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用

  1. 类的静态函数

在静态成员函数的实现中不能直接引用类中说明的非静态成员, 可以引用类中说明的静态成员, 调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

c++中四种 cast 转换

C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast

  1. const_cast: 用于将 const 变量转为非 const

  2. static_cast: 用于各种隐式转换,比如非 const 转 const,void*转指针等, static_cast 能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

  3. dynamic_cast: 用于动态类型转换。 只能用于含有虚函数的类, 用于类层次间的向上和向下转化。 只能转指针或引用。向下转化时,如果是非法的对于指针返回 NULL

  4. reinterpret_cast

为什么不使用 C 的强制转换?

C 的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查, 容易出错。

C/C++ 中指针和引用的区别?

1.指针有自己的一块空间,而引用只是一个别名;

2.使用 sizeof 看一个指针的大小是 4,而引用则是被引用对象的大小;

3.指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象 的引用;

4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;

5.可以有 const 指针,但是没有 const 引用;

6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

7.指针可以有多级指针(**p),而引用至于一级;

8.指针和引用使用++运算符的意义不一样;

9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

野指针是什么?

野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针

智能指针有没有内存泄露的情况?

当两个对象相互使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。例如:

上述代码中, parent 有一个 shared_ptr 类型的成员指向孩子, 而 child 也有一个 shared_ptr 类型的成员指向父亲。然后在创建孩子和父亲对象时也使用了智能指针 c 和 p,随后将 c 和 p 分别又赋值给 child 的智能指针成员 parent 和 parent 的智能指针成员 child。 从而形成了一个循环引用

智能指针的内存泄漏如何解决?

为了解决循环引用导致的内存泄漏,引入了 weak_ptr 弱指针,weak_ptr 的构造函数不会修改引用计数的值, 从而不会对对象的内存进行管理, 其类似一个普通指针, 但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

函数指针

  1. 定义:函数指针是指向函数的指针变量。

函数指针本身首先是一个指针变量, 该指针变量指向一个具体的函数。 这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。

C 在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后, 可用该指针变量调用函数, 就如同用指针变量可引用其他类型变量一样, 在这些概念上是大体一致的。

  1. 示例:

为什么析构函数必须是虚函数?为什么 C++默认的析构函数不是虚函数

将可能会被继承的父类的析构函数设置为虚函数, 可以保证当我们 new 一个子类, 然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此 C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

C++中析构函数的作用

  1. 析构函数与构造函数对应, 当对象结束其生命周期, 如对象所在的函数已调用完毕时, 系统会自动执行析构函数。

析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括 void 类型)。只能有一个析构函数,不能重载。

如果用户没有编写析构函数, 编译系统会自动生成一个缺省的析构函数 (即使自定义了析构函数, 编译器也总是会为我们合成一个析构函数, 并且如果自定义了析构函数, 编译器在执行时会先调用自定义的析构函数再调用合成的析构函数) , 它也不进行任何操作。 所以许多简单的类中没有用显式的析构函数。

如果一个类中有指针, 且在使用的过程中动态的申请了内存, 那么最好显示构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。

  1. 类析构顺序: 1)派生类本身的析构函数; 2)对象成员析构函数; 3)基类析构函数。

静态函数和虚函数的区别

静态函数在编译的时候就已经确定运行时机, 虚函数在运行的时候动态绑定。 虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销

重载和覆盖

重载: 两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中

重写: 子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写

虚函数和多态

多态的实现主要分为静态多态和动态多态

静态多态: 主要是重载, 在编译的时候就已经确定;

动态多态: 用虚函数机制实现的, 在运行期间动态绑定。 举个例子: 一个父类类型的指针指向一个子类对象时候, 使用父类的指针去调用子类中重写了的父类中的虚函数的时候, 会调用子类重写过后的函数,在父类中声明为加了 virtual 关键字的函数,在子类中重写时候不需要加 virtual 也是虚函数。

虚函数表具体是怎样实现运行时多态的?

子类若重写父类虚函数,虚函数表中,该函数的地址会被替换

++i 和 i++的实现

C++里是怎么定义常量的?常量存放在内存的哪个位置?

常量在 C++里的定义就是一个 top-level const 加上对象类型,常量定义必须初始化。

对于局部对象,常量存放在栈区

对于全局对象,常量存放在全局/静态存储区。

对于字面值常量,常量存放在常量存储区。

extern “C”

C++调用 C 函数需要 extern C,因为 C 语言没有函数重载。

new/delete 与 malloc/free 的区别是什么

首先, new/delete 是 C++的关键字, 而 malloc/free 是 C 语言的库函数, 后者使用必须指明申请内存空间的大小,对于类类型的对象,后者不会调用构造函数和析构函数

C++中拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。

STL容器

map 和 set 有什么区别,分别又是怎么实现的?

map 和 set 都是 C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和 set 的操作行为,都只是转调 RB-tree 的操作行为。

map 和 set 区别在于:

(1)map 中的元素是 key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set 与之相对就是关键字的简单集合,set 中每个元素只包含一个关键字。

(2)set 的迭代器是 const 的,不允许修改元素的值; map 允许修改 value,但不允许修改key。其原因是因为 map 和 set 是根据关键字排序来保证其有序性的,如果允许修改 key的话, 那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了 map 和 set 的结构,导致 iterator 失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以 STL 中将 set 的迭代器设置成 const,不允许修改迭代器的值;而 map 的迭代器则不允许修改 key 值,允许修改 value 值。

  1. map 支持下标操作, set 不支持下标操作。 map 可以用 key 做下标, map 的下标运算符[ ] 将key作为下标去执行查找,如果key-value不存在,则插入一个具有该关键码和 mapped_type 类型默认值的元素至 map 中,因此下标运算符[ ]在 map 应用中需要慎用,如果 find 能解决需要,尽可能用 find

STL 迭代器删除元素

1.对于序列容器 vector,deque 来说,使用erase(itertor)后, 后边的每个元素的迭代器都会失效, 但是后边每个元素都会往前移动一个位置,但是 erase 会返回下一个有效的迭代器;

2.对于关联容器 map set 来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器, 所以在调用 erase 之前, 记录下一个元素的迭代器即可。

3.对于 list 来说,它使用了不连续分配的内存,并且它的 erase 方法也会返回下一个有效的 iterator,因此上面两种正确的方法都可以使用。

vector 和 list 的区别,应用

  1. vector:
  • 连续存储的容器
  • 动态数组: 在堆上分配空间
  • 底层实现: 数组
  • 两倍容量增长: vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
  • 适用场景: 经常随机访问,且不经常对非尾节点进行插入删除. vector 拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用 vector。
  • 性能:
  1. list:
  • 动态链表,在堆上分配空间
  • 每插入一个元数都会分配空间,每删除一个元素都会释放空间。
  • 底层: 双向链表
  • 适用场景: 经常插入删除大量数据. list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用 list。
  • 性能:

STL 中迭代器的作用,有指针为何还要迭代器

  1. 迭代器

Iterator(迭代器)模式又称 Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator 模式是运用于聚合对象的一种模式, 通过运用该模式, 使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由 iterator 提供的方法)访问聚合对象中的各个元素。

  1. 迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符, ->、 *、 ++、 --等。 迭代器封装了指针, 是一个 “可遍历 STL ( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。

**迭代器返回的是对象引用而不是对象的值,所以 cout 只能输出迭代器使用*取值后的值而不能直接输出其自身。**

  1. 迭代器产生原因

Iterator 类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

STL 里 resize 和 reserve 的区别

  1. resize():
  • 改变当前容器内含有元素的数量, eg: vector<int>v; v.resize(len); , v 的 size 变为 len
  • 如果原来 v 的 size() 小于 len, 那么容器新增(len-size)个元素, 元素的值为默认为 0.
  1. reserve():
  • 改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,
  • 如果 reserve(len)的值大于当前的 capacity(),那么会重新分配一块能存 len 个对象的空间,然后把之前 v.size() 个对象通过 copy construtor 复制过来,销毁之前的内存;
  • 注意,这个操作并不会创建新的元素对象

类和数据抽象

C++中类成员的访问权限

C++通过 public、 protected、 private 三个关键字来控制成员变量和成员函数的访问权限, 它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

C++中 struct 和 class 的区别

在 C++中,可以用 struct 和 class 定义类,都可以继承。 区别在于: struct 的默认继承权限和默认访问权限是 public , 而 class 的默认继承权限和默认访问权限是 private。 另外,class 还可以定义模板类形参,比如 template <class T, int i>

泛型编程

什么是右值引用,跟左值又有什么区别?

右值引用是 C++11 中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面: - 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。 - 能够更简洁明确地定义泛型函数。

左值和右值的概念: - 左值: 能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。 - 右值: 不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。 - 举例:

1
2
3
A a;
A& a_ref=a;
A&& temp_rref = A();

右值引用和左值引用的区别: - 左值可以寻址,而右值不可以。 - 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。

《左右值与右值引用》

编译与底层

C++源文件从文本到可执行文件经历的过程

对于 C++源文件,从文本到可执行文件一般需要四个过程: 1) 预处理阶段: 对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。 2) 编译阶段: 将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件 3) 汇编阶段: 将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件 4) 链接阶段: 将多个目标文件及所需要的库连接成最终的可执行目标文件

include 头文件的顺序以及双引号””和尖括号<>的区别?

Include 头文件的顺序: 对于 include 的头文件来说, 如果在文件 a.h 中声明一个在文件 b.h 中定义的变量,而不引用 b.h。那么要在 a.c 文件中引用 b.h 文件,并且要先引用 b.h,后引用a.h,否则汇报变量类型未声明错误。

双引号和尖括号的区别: 编译器预处理阶段查找头文件的路径不一样 1) 对于使用双引号""包含的头文件,查找头文件路径的顺序为: (1) 当前头文件目录 (2) 编译器设置的头文件路径(编译器可使用-I 显式指定搜索路径) (3) 系统变量 CPLUS_INCLUDE_PATH/C_INCLUDE_PATH 指定的头文件路径 2) 对于使用尖括号包含的头文件,查找头文件的路径顺序为: (1) 编译器设置的头文件路径(编译器可使用-I 显式指定搜索路径) (2) 系统变量 CPLUS_INCLUDE_PATH/C_INCLUDE_PATH 指定的头文件路径

malloc 的原理

Malloc 函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc 其采用内存池的方式, 先申请大块内存作为堆区, 然后将堆区分为多个内存块, 以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。

new 和 malloc 的区别

  1. new 分配内存按照数据类型进行分配, malloc 分配内存按照指定的大小分配;
  2. new 返回的是指定对象的指针, 而 malloc 返回的是 void*, 因此 malloc 的返回值一般都需要进行类型转化。
  3. new 不仅分配一段内存,而且会调用构造函数,malloc 不会。
  4. new 分配的内存要用 delete 销毁,malloc 要用 free 来销毁;delete 销毁的时候会调用对象的析构函数,而 free 则不会。
  5. new 是一个操作符可以重载,malloc 是一个库函数。
  6. malloc 分配的内存不够的时候, 可以用 realloc 扩容。
  7. new 如果分配失败了会抛出 bad_malloc 的异常,而 malloc 失败了会返回 NULL。
  8. 申请数组时: new[]一次分配所有内存, 多次调用构造函数, 搭配使用 delete[], delete[] 多次调用析构函数,销毁数组中的每个对象。而 malloc 则只能 sizeof(int) * n

C++的内存管理是怎样的

在 C++中,虚拟内存分为代码段数据段BSS 段堆区文件映射区以及栈区六部分。 1) 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。 2) 数据段:存储程序中已初始化的全局变量和静态变量 3) bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为 0 的全局变量和静态变量。 4) 堆区: 调用 new/malloc 函数时在堆区动态分配内存, 同时需要调用 delete/free 来手动释放申请的内存。 5) 映射区:存储动态链接库以及调用 mmap 函数进行的文件映射 6) 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

什么是 memory leak,也就是内存泄漏

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。 内存泄漏并非指内存在物理上的消失, 而是应用程序分配某段内存后, 由于设计错误, 失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏分类

  1. 堆内存泄漏 (Heap leak): 指的是程序运行中根据需要分配通过 malloc,realloc new 等从堆中分配的一块内存,再是完成后必须通过调用对应的 free 或者 delete 删掉。如果程序的设计的错误导致这部分内存没有被释放, 那么此后这块内存将不会被使用, 就会产生 Heap Leak.
  2. 系统资源泄露(Resource Leak): 主要指程序使用系统分配的资源比如Bitmap,handle ,SOCKET 等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
  3. 没有将基类的析构函数定义为虚函数: 当基类指针指向子类对象时,如果基类的析构函数不是 virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

如何判断内存泄漏?

内存泄漏通常是由于调用了 malloc/new 等内存申请的操作, 但是缺少了对应的 free/delete。 为了判断内存是否泄露, 我们一方面可以使用 linux 环境下的内存泄漏检查工具 Valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能, 统计当前申请和释放的内存是否一致, 以此来判断内存是否泄露。

C++如何处理内存泄漏?

使用 varglind,mtrace 检测

什么时候会发生段错误

段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况: 1) 使用野指针 2) 试图修改字符串常量的内容

如何采用单线程的方式处理高并发

在单线程模型中, 可以采用 I/O 复用来提高单线程处理多个请求的能力, 然后再采用事件驱动模型,基于异步回调来处理事件来

C++11

C++11 有哪些新特性?

C++11 最常用的新特性如下: 1) auto 关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导 2) nullptr 关键字: nullptr 是一种特殊类型的字面值, 它可以被转换成任意其它的指针类型; 而 NULL 一般被宏定义为 0,在遇到重载时可能会出现问题。 3) 智能指针: C++11 新增了 std::shared_ptr、std::weak_ptr 等类型的智能指针,用于解决内存管理的问题。 4) 初始化列表:使用初始化列表来对类进行初始化 5) 右值引用: 基于右值引用可以实现移动语义和完美转发, 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率 6) atomic 原子操作用于多线程资源互斥操作 7) 新增 STL 容器 array 以及 tuple

介绍一下 C++11 中的可变参数模板、右值引用和 lambda 这几个新特性

可变参数模板:

C++11 的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在 class 或 typename 后面带上省略号...

省略号作用如下: 1) 声明一个包含 0 到任意个模板参数的参数包 2) 可以将参数包展成一个个独立的参数

1
2
3
4
5
//T 叫做模板参数包,args 叫做函数参数包
Template<class ... T>
void func(T ... args){
cout<<"num is"<<sizeof ...(args)<<endl;
}
  • func();//args 不含任何参数
  • func(1);//args 包含一个 int 类型的实参
  • func(1,2.0)//args 包含一个 int 一个 double 类型的实参

C++11 可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
//重载最终递归函数
void print(){
cout<<"empty"<<endl;
}
// 展开函数
template void print(T head, Args ... args){
cout<<head<<",";
print(args...);
}
int main(){
print(1,2,3,4);
return 0;
}

参数包 Args ...在展开的过程中递归调用自己,没调用一次参数包中的参数就会少一个, 直到所有参数都展开为止。当没有参数时就会调用非模板函数 printf 终止递归过程。

右值引用

基于右值引用可以实现转移语义和完美转发新特性。

移动语义

对于一个包含指针成员变量的类, 由于编译器默认的拷贝构造函数都是浅拷贝, 所有我们一般需要通过实现深拷贝的拷贝构造函数, 为指针成员分配新的内存并进行内容拷贝, 从而避免悬挂指针的问题。

如下代码所示:

上述代码中,函数GetTemp()返回了一个类型为HasPtrMem的对象,此外,该类定义了深拷贝的拷贝构造函数,因此,main()函数,两次调用拷贝构造函数,第一次是调用GetTemp()函数返回时的临时变量,第二次是将函数返回值拷贝给变量a时。

而在上述过程中, 使用临时变量构造 a 时会调用拷贝构造函数分配对内存, 而临时对象在语句结束后会释放它所使用的堆内存。 这样重复申请和释放内存, 在申请内存较大时会严重影响性能。因此 C++使用移动构造函数,从而保证使用临时对象构造 a 时不分配内存,从而提高性能.

解决方案:

如下列代码所示, 移动构造函数接收一个右值引用作为参数, 使用右值引用的参数初始化其指针成员变量

其原理就是使用在构造对象 a 时,使用 h.d 来初始化 a,然后将临时对象 h 的成员变量 d 指向 nullptr,从而保证临时变量析构时不会释放对内存。

完美转发

完美转发是指在函数模板中, 完全依照模板的参数的类型, 将参数传递给函数模板中调用的另一个函数, 即传入转发函数的是左值对象, 目标函数就能获得左值对象, 转发函数是右值对象, 目标函数就能获得右值对象,而不产生额外的开销。

因此转发函数和目标函数参数一般采用引用类型, 从而避免拷贝的开销。 其次, 由于目标函数可能需要能够既接受左值引用,又接受右值引用,所以考虑转发也需要兼容这两种类型。

C++11 采用引用折叠的规则,结合新的模板推导规则实现完美转发。其引用折叠规则如下:

Lambda 表达式:

Lambda 表达式定义一个匿名函数,并且可以捕获一定范围内的变量,其定义如下:

[capture](params)mutable->return-type{statement}

其中, - [capture]:捕获列表,捕获上下文变量以供 lambda 使用。同时[]是 lambda 寅初复,编译器根据该符号来判断接下来代码是否是 lambda 函数。 - (Params):参数列表,与普通函数的参数列表一致,如果不需要传递参数,则可以连通括号一起省略。 - mutable 是修饰符, 默认情况下 lambda 函数总是一个 const 函数, Mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略。 - ->return-type:返回类型是返回值类型 - {statement}:函数体, 内容与普通函数一样, 除了可以使用参数之外, 还可以使用所捕获的变量

特点: Lambda 表达式与普通函数最大的区别就是其可以通过捕获列表访问一些上下文中的数据。

操作系统

进程与线程的概念,以及为什么要有进程线程,其中有什么区别?

基本概念 - 进程是对运行时程序的封装, 是系统进行资源调度和分配的的基本单位, 实现了操作系统的并发; - 线程是进程的子任务, 是CPU调度和分派的基本单位, 用于保证程序的实时性, 实现进程内部的并发; 每个线程都独自占用一个虚拟处理器: 独自的寄存器组, 指令计数器和处理器状态。 每个线程完成不同的任务, 但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等), 打开的文件队列和其他内核资源。

区别: 1. 一个线程只能属于一个进程, 而一个进程可以有多个线程, 但至少有一个线程。 线程依赖于进程而存在。 2. 进程在执行过程中拥有独立的内存单元, 而多个线程共享进程的内存。 3. 进程是资源分配的最小单位,线程是 CPU 调度的最小单位; 4. 系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。进程切换的开销也远大于线程切换的开销。 5. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快, 但是编程调试相对复杂 6. 进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉

进程间的通信方式

进程间通信主要包括管道、系统 IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字 socket。

1.管道

管道主要包括无名管道命名管道:

管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信

1.1 普通管道 PIPE:

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
  3. 它可以看成是一种特殊的文件, 对于它的读写也可以使用普通的 read、 write 等函数。 但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

1.2 命名管道 FIFO:

  1. FIFO 可以在无关的进程之间交换数据
  2. FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2.系统 IPC

2.1 消息队列:

  1. 消息队列,是消息的链接表,存放在内核中。
  2. 消息队列克服了信号传递信息少, 管道只能承载无格式字节流以及缓冲区大小受限等特点
  3. 一个消息队列由一个标识符(即队列 ID)来标记
  4. 具有写权限得进程可以按照一定得规则向消息队列中添加新信息;
  5. 对消息队列有读权限得进程则可以从消息队列中读取信息;

2.2 信号量 semaphore:

  1. 信号量(semaphore)是一个计数器,可以用来控制多个进程对共享资源的访问。 信号量用于实现进程间的互斥与同步, 而不是用于存储进程间通信数据。
  2. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  3. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  4. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  5. 支持信号量组。

2.3 信号 signal

  1. 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

2.4 共享内存(Shared Memory):

  1. 它使得多个进程可以访问同一块内存空间, 不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等
  2. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取
  3. 因为多个进程可以同时操作,所以需要进行同步
  4. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

3.套接字 SOCKET

socket 也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。

线程间通信的方式?

  1. 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
  2. 互斥量 Synchronized/Lock: 采用互斥对象机制, 只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
  3. 信号量 Semphare: 为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
  4. 事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作

Linux 虚拟地址空间

为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏, 采用了虚拟内存。

虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的 4G 内存。 所有进程共享同一物理内存, 每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。

事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表, 实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data 段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。

还有进程运行过程中,要动态分配内存,比如 malloc 时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

请求分页系统、 请求分段系统和请求段页式系统都是针对虚拟内存的, 通过请求实现内存与外存的信息置换。

虚拟内存的好处: 1. 扩大地址空间; 2. 内存保护: 每个进程运行在各自的虚拟内存地址空间, 互相不能干扰对方。 虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。 3. 公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。 4. 当进程通信时,可采用虚存共享的方式实现。 5. 当不同的进程使用同样的代码时, 比如库文件中的代码, 物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存 6. 虚拟内存很适合在多道程序设计系统中使用, 许多程序的片段同时保存在内存中。 当一个程序等待它的一部分读入内存时, 可以把 CPU 交给另一个进程使用。 在内存中可以保留多个进程, 系统并发度提高 7. 在程序需要分配连续的内存空间的时候, 只需要在虚拟内存空间分配连续空间, 而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价: 1. 虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存 2. 虚拟地址到物理地址的转换,增加了指令的执行时间。 3. 页面的换入换出需要磁盘 I/O,这是很耗时的 4. 如果一页中只有一部分数据,会浪费内存。

操作系统中的程序的内存结构?

  1. 数据段: 存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配
  2. bss段: (未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为 0。需要存放在程序文件中的只有正文段和初始化数据段。
  3. data段: (已经初始化的数据)则为数据分配空间,数据保存到目标文件中
  4. 代码段: 存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定, 并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量
  • text 段和 data 段在编译时已经分配了空间,而 BSS 段并不占用可执行文件的大小,它是由链接器来获取内存的。
  • 数据段包含经过初始化的全局变量以及它们的值。 BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块, 紧跟在数据段的后面。 当这个内存进入程序的地址空间后全部清零。包含数据段和 BSS 段的整个区段此时通常称为数据区。

可执行程序在运行时又多出两个区域:栈区和堆区

  1. 栈区: 由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中,然后这个被调用的函数再为他的自动变量和临时变量在栈上进行分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
  2. 堆区: 用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的 malloc/free 造成内存空间的不连续, 产生碎片。 当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。 因此堆的效率比栈要低的多。

操作系统中的缺页中断?

malloc()mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。 当进程访问这些没有建立映射关系的虚拟内存时, 处理器自动触发一个缺页异常。 缺页中断: 在请求分页系统中, 可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。 每当所要访问的页面不在内存是, 会产生一次缺页中断, 此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

缺页中断是由于所要访问的页面不存在于内存时, 由硬件所产生的一种特殊的中断, 因此,与一般的中断存在区别: 缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令

操作系统中的页表寻址?

页式内存管理, 内存分成固定长度的一个个页片。 操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构, 叫页表, 页表的内容就是该进程的虚拟地址到物理地址的一个映射。 页表中的每一项都记录了这个页的基地址。 通过页表, 由逻辑地址的高位部分先找到逻辑地址对应的页基地址, 再由页基地址偏移一定长度就得到最后的物理地址, 偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。

页式内存管理的优点就是比较灵活, 内存管理以较小的页为单位, 方便内存换入换出和扩充地址空间。

并发(concurrency)和并行(parallelism)

  • 并发(concurrency): 指宏观上看起来两个程序在同时运行,比如说在单核 cpu 上的多任务。 但是从微观上看两个程序的指令是交织着运行的, 你的指令之间穿插着我的指令, 我的指令之间穿插着你的, 在单个周期内只运行了一个指令。 这种并发并不能提高计算机的性能, 只能提高效率。
  • 并行(parallelism): 指严格物理意义上的同时运行,比如多核 cpu,两个程序分别运行在两个核上, 两者之间互不影响, 单个周期内每个程序都运行了自己的指令, 也就是运行了两条指令。这样说来并行的确提高了计算机的效率。

各数据库的默认端口

查看端口号: 使用命令 show global variables like 'port' - mysql 的默认端口是 3306 - sqlserver 默认端口号为: 1433 - oracle 默认端口号为: 1521 - DB2 默认端口号为: 5000 - PostgreSQL 默认端口号为:5432