编程相关面试整理——cpp&python

编程面试

  • python相关知识
        • Python dict和set的底层原理
        • python的迭代器了解么
        • python的深浅拷贝
        • python多线程、多进程相关
        • 多线程、协程
        • Python锁
        • python装饰器使用(web相关)
        • python可变对象问题
        • python内存管理,垃圾回收原理
        • Python Map使用
        • Python reduce使用
        • python Filter使用
        • python函数式编程
        • 面向对象、继承
        • 下划线的使用
  • C++相关知识
        • 编译相关
        • 静态链接库和动态链接库的区别
        • 内存分区
        • STL中map、hash_map、unordered_map、unordered_set底层实现
        • 指针和引用的区别
        • 左值引用和右值引用
        • static静态函数
        • extern 关键字
        • mutable关键字
        • 类的继承中,保护继承的理解
        • 回调函数
        • 函数指针和指针函数
        • 深拷贝和浅拷贝
        • malloc和new的区别
        • 堆栈区别
        • const int* p 和 int* const q 兩者之差別:
        • 静态变量和全局变量之间的区别
        • 类对象作为函数参数
        • TCP 3次握手、4次挥手的全过程
        • TCP和UDP区别
        • 进程线程区别
        • C++线程中的几种锁
        • C++并发编程如何避免死锁
        • lock_guard用过么
        • 一点点操作系统的知识
        • 多态
        • 虚函数
        • 智能指针
        • STL容器

python相关知识

Python dict和set的底层原理
  • C++中的map和set都是使用红黑树来实现的,是一个排序的结构,插入,查找和删除都可以在logN的时间复杂度完成。另外C++中还提供了unordermap和unorderset数据结构,这两个数据结构是使用哈希表实现的,是没有排序,插入、查找和删除都可以在O(1)的时间复杂度内完成。
  • python中,dict是用来存储键值对结构的数据的,set其实也是存储的键值对,只是默认键和值是相同的。Python中的dict和set都是通过散列表来实现的。下面来看与dict相关的几个比较重要的问题:
    • dict中的数据是无序存放的
    • 操作的时间复杂度,插入、查找和删除都可以在O(1)的时间复杂度
    • 键的限制,只有可哈希的对象才能作为字典的键和set的值。可hash的对象即python中的不可变对象和自定义的对象。可变对象(列表、字典、集合)是不能作为字典的键和set的值的。
    • 与list相比:list的查找和删除的时间复杂度是O(n),添加的时间复杂度是O(1)。但是dict使用hashtable内存的开销更大。为了保证较少的冲突,hashtable的装载因子,一般要小于0.75,在python中当装载因子达到2/3的时候就会自动进行扩容。
python的迭代器了解么
  1. 迭代是Python最强大的功能之一,是访问集合元素的一种方式。

    迭代器是一个可以记住遍历的位置的对象。

    迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

    迭代器有两个基本的方法:iter() 和 next()。

  2. 在 Python 中,使用了 yield 的函数被称为生成器(generator)。

    跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

    在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。

    调用一个生成器函数,返回的是一个迭代器对象。

python的深浅拷贝

所谓浅拷贝,就是对引用的拷贝,所谓深拷贝,就是对对象的资源的拷贝。
首先,我们对赋值操作要有以下认识:

  1. 赋值是将一个对象的地址,赋值给另一个变量,让变量指向该地址。
  2. 修改不可变对象(str、tuple)需要开辟新的空间。浅拷贝(’ = ')之后,修改拷贝后的变量不会影响之前的变量。
  3. 修改可变对象(list)不需要开辟新的空间。浅拷贝(’ = ')之后,修改拷贝后的变量会影响之前的变量。

浅拷贝知识将容器内的元素的地址复制了一份。这可以通过修改后,b中字符串没改变,但是list元素随着a相应改变得到验证。

深拷贝后,a和b的地址以及a和b中的元素地址均不同,这是完全拷贝的一个副本,修改a后,发现b没有发生任何改变,因为b是一个完全的副本,元素地址与a均不同,a修改不影响b。

参考链接

python多线程、多进程相关

进程是程序的一次执行,线程是进程中的执行单元。

  1. 多线程:
    使用方法:import threading
    方法:threading.active_count() # 表示线程数
    threading.enumrate() # 查看线程名,有一个MainThread
    threading.current_thread() # 现在正在运行的线程
    thread = threading.Thread(target = thread_job) # 添加一个线程,给线程定义一个工作,其中thread_job是def的一个函数。
    thread.start() # 让线程开始工作
    join() # 使用join对控制多个线程的执行顺序非常关键
    from queue import Queue
    q = Queue() 将多线程的运算结果存储在队列当中,便于后期取出,不允许return,将计算结果存放在queue
    GIL (Global Interpreter Lock)GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势。因为读写的时间,可以用于开启另外的线程,所以还是有一定的加速作用。在讨论普通的GIL之前,有一点要强调的是GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。 如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。
    编程相关面试整理——cpp&python_第1张图片
    线程锁Lock:如果某个线程处理完一批数据,将结果给其他线程,需要将前一个线程锁住,等处理结束才能给别的线程。一般来说,需要对共享内存加工处理时用到。
    lock = threading .Lock(),然后将lock也定义为全局变量。要锁的地方需要使用lock.acquire(),结束时lock.release()。
    应用:聊天程序,一个线程负责发送,一个线程负责接收。两个线程分工合作,效率会提高很多。但是如果处理大量数据,多线程的优势没有那么明显,要使用多进程,因为每个核有单独的逻辑空间,所以每个核不会受到GIL的影响。
  2. 多进程:
    使用方法:import multiprocessing as mp
    import threading as td
    格式要求,在if 在运用时需要添加上一个定义main函数的语句。
    方法:储存进程输出Queue:q = mp.Queue()定义
    进程池:pool = mp.Pool() # 默认使用所有的核,可以指定参数processes = n
    res = pool.map(job, range(10)),其中job函数可以有返回值return。
    共享内存:只有用共享内存才能让CPU之间有交流
    value1 = mp.Value(‘i’, 0)
    value2 = mp.Value(‘d’, 3.14)
    array = mp.Array(‘i’, [1, 2, 3, 4]) # 只能是一维的
    进程锁
多线程、协程
  • GIL线程全局锁:python为了保证线程的安全而采取的独立线程运行的限制,也就是说,一个核只能在同一时间运行一个线程。
  • 对于IO密集的任务可以采用多线程操作,而对于cpu密集的任务(偏向于用cpu计算,科学计算程序和机器学习程序等),应该采用多进程,如果此时用多线程有可能因为争夺资源而变慢。
  • 协程:是进程和线程的升级版,进程和线程都面临内核态与用户态的切换问题而耗费许多切换时间,而协程就是自己控制切换的时机,不再需要陷入系统的内核态,常见的yield就是协程思想。
Python锁

一、全局解释器锁(GIL)

  1. 什么是GIL
    每个CPU在同一时间只能执行一个线程,那么其他的线程就必须等待该线程的全局解释器,使用权消失后才能使用全局解释器,即使多个线程直接不会相互影响在同一个进程下也只有一个线程使用cpu,这样的机制称为全局解释器锁(GIL)。GIL的设计简化了CPython的实现,使的对象模型包括关键的内建类型,如:字典等,都是隐含的,可以并发访问的,锁住全局解释器使得比较容易的实现对多线程的支持,但也损失了多处理器主机的并行计算能力。

  2. 全局解释器锁的好处

    1)避免了大量的加锁解锁的好处

    2)使数据更加安全,解决多线程间的数据完整性和状态同步

  3. 全局解释器的缺点

    多核处理器退化成单核处理器,只能并发不能并行。

  4. GIL的作用:

    多线程情况下必须存在资源的竞争,GIL是为了保证在解释器级别的线程唯一使用共享资源(cpu)。

二、同步锁
同一时刻的一个进程下的一个线程只能使用一个cpu,要确保这个线程下的程序在一段时间内被cpu执,那么就要用到同步锁。

三、

python装饰器使用(web相关)

Python的闭包与装饰器

一篇文章搞懂Python装饰器所有用法

面试官问我Decorator,我上去就是两个耳刮子
装饰器模板

def decorator(func):
    def wrapper_decorator(*args, **kwargs):
        #调用前操作
        ret_val = func(*args, **kwargs)
        #调用后操作
        return ret_val
    return wrapper_decorator

按照这个模板:
修改装饰器的名字,把decorator替换为具体的名字。
在注释“调用前操作”的地方写自己想写的代码
在注释“调用后操作”的地方写自己想写的代码。

  1. 类装饰器

单例模式,是指一个类只能创建一个实例,是最常见的设计模式之一。

比如网站程序有一个类,统计网站的访问人数,这个类只能有一个实例。如果每次访问都创建一个新的实例,那人数就永远是1了。

在python中可以用装饰器实现单例模式。

类装饰器实际上装饰的是类的初始化方法。只有初始化的时候会装饰一次。

用property装饰器创建虚拟的半径属性
用setter装饰器给半径属性添加赋值操作
用classmethod,实现类方法
用static,实现静态方法

  1. 带状态的装饰器
    装饰器自己保存了一个实例,要的时候就给你这一个,实现单例模式,叫做带状态的装饰器。
  2. 多个装饰器嵌套
    装饰器的本质就是先调用装饰器,装饰器再调用函数。既然这样,那么多调用几层也无妨。
import time
from slow import slow

def timer(func):
 def wrapper():
    start_time = time.perf_counter()
    func()
    end_time = time.perf_counter()
    used_time = end_time - start_time
    print(f'{func.__name__} used {used_time}')
 return wrapper

@slow
@timer
def run():
    print('跑步有利于身体健康,来一圈')

run()
python可变对象问题

链接

  • 可变对象变化后,地址是没有改变的
  • 可变对象作为参数传入时,在函数中对其本身进行修改,是会影响到全局中的这个变量值的;对于不可变对象来说,虽然函数中的a值变了,但是全局中的a值没变,因为函数中的a值已经对应了另外一个地址,而全局中的a值指向的原来地址的值是没有变的。
  • python中向函数传递参数只能是引用传递,表示把它的地址都传进去了。有的编程语言允许值传递,即只是把值传进去,在里面另外找一个地址来放,这样就不会影响全局中的变量
  • 函数默认参数一定要设定为不可变参数,这是因为默认参数在函数定义时就确定下来了,每次调用这个函数,如果不对其重新赋值,那么使用的默认参数都是同一个,就会导致一直在之前的地址上已有数据的基础上继续操作。

dict,list是可变对象。允许值发生变化,如果对变量进行append,+=操作后, 只是改变了变量值,不会新建一个对象,变量引用的对象的地址不会变化。
str,int,tuple,float是 不可变对象。不允许值发生变化,若改变了变量的值,相当于新建了一个对象,对于相同值的对象,内存中只有一个对象。

python内存管理,垃圾回收原理

https://www.bilibili.com/video/BV1F54114761?p=2
引用计数器为主,标记清除和分代回收为辅。

  1. 引用计数器
    1.1 环状双向链表refchain
    1.2 类型封装结构体
    1.3 引用计数器 (为0,回收)
    1.4 循环引用问题

  2. 标记清除
    目的:为了解决引用计数器循环引用的不足
    实现:在python底层再维护一个链表,链表中专门放那些可能存在循环引用的对象(list/tuple/dict/set)。在python内部某种情况下触发,回去扫描可能存在循环引用的链表中的每个元素,检查是否有循环引用,如果有,则让双方的引用计数器-1。如果是0则垃圾回收。
    问题:1. 什么时候扫描,可能存在循环引用的链表扫描代价大,每次扫描耗时大。

  3. 分代回收
    将可能存在循环引用的对象维护成3个链表:
    3.1:0代:对象个数达到700个扫描一次。
    3.2:1代:0代扫描10次,则1代扫描一次。
    3.3:2代:1代扫描10次,则2代扫描一次。

  4. 缓存机制
    4.1 池
    4.2 free_list

Python Map使用

map() 会根据提供的函数对指定序列做映射。
第一个参数 function 以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。

Python 3.x 返回迭代器。

>>> def square(x) :         # 计算平方数
...     return x ** 2
...
>>> map(square, [1,2,3,4,5])    # 计算列表各个元素的平方
<map object at 0x100d3d550>     # 返回迭代器
>>> list(map(square, [1,2,3,4,5]))   # 使用 list() 转换为列表
[1, 4, 9, 16, 25]
>>> list(map(lambda x: x ** 2, [1, 2, 3, 4, 5]))   # 使用 lambda 匿名函数
[1, 4, 9, 16, 25]
>>>
Python reduce使用

reduce(func, seq[, init()])
reduce() 函数即为化简函数,它的执行过程为:每一次迭代,都将上一次的迭代结果(注:第一次为init元素,如果没有指定init则为seq的第一个元素)与下一个元素一同传入二元func函数中去执行。在reduce()函数中,init是可选的,如果指定,则作为第一次迭代的第一个元素使用,如果没有指定,就取seq中的第一个元素。

比如实现阶乘

#未指定init的情况
>>> n = 6
>>> print reduce(lambda x, y: x * y, range(1, n))
120

如果希望得到阶乘的结果再多增加几倍,可以启用init这个可选项。

>>> print reduce(lambda x, y: x * y, range(1, n),2)
240

这个时候,就会将init作为第一个元素,和seq1中的第一个元素1一起传入lambda函数中去执行,返回结果再作为下一次的第一个元素。

python Filter使用

filter函数传入一个迭代对象作为参数并将这个迭代对象当中所有那些你不要的东西滤出去。
通常,filter传入一个函数和一个列表。将这个函数作用于列表当中的任意一个元素加入函数返回True,不做任何事情。加入返回False,将这个元素从列表中删除。

x = range(-5, 5)
all_less_than_zero = list(filter(lambda num: num < 0, x))
python函数式编程

在命令式的编程范式当中,你通过告诉计算机一系列需要执行的任务,并在计算机执行以后以完成你的目的。当执行任务的时候,状态可能发生改变。
在一个函数式编程范式中,你并不告诉计算机要干什么,而是告诉它是干什么的。什么是一个数字的最大公因子,什么是从1到N的乘积,etc。

链接

面向对象、继承
  • self代表实例,不是类。类函数里必须有一个额外的参数,默认self
  • 若是多继承,在类括号里写多个父类名。但要注意继承顺序,若是父类中有相同的方法名,而在子类使用时未指定,python从左到右搜索,即方法在子类中未找到,从左到右查找父类中是否包含方法。
  • 若父类方法功能不能满足要求,可以再子类重写父类方法。使用super(子类名,子类对象).func()调用父类方法
  • 类内属性加两个下划线开头,表示属性私有,不能在类外被直接使用或者访问。
下划线的使用

单下划线:用来指定变量私有,只有类对象和子类对象能够访问,外部访问需要接口, 不能用import 导入
双下划线:私有成员,只有类对象自己能够访问,子类对象也无法访问。通过 对象名._类名__xxx来访问
foo: python内部的名字,用来区别其他用户自定义的命名,以防止冲突。

python知识点参考链接

C++相关知识

编译相关
  1. 预编译
    不进行任何安全性及合法性检查,处理所有预编译指令,如#if #include
  2. 编译
    进行语法分析、词法分析、语义分析优化后,生成相应的汇编代码文件。
  3. 汇编
    生成可重定位的二进制文件(.obj)
  4. 链接
    合并所有的obj文件的段并调整段偏移和段长度;符号的重定位,将符号分配的虚拟地址写回原先未分配正确地址的地方。
静态链接库和动态链接库的区别
  1. 静态库和应用程序编译在一起,在任何情况下都能运行,而动态库是动态链接,在应用程序启动的时候才会链接。所以,当用户的系统上没有该动态库时,应用程序就会运行失败。
  2. 如果程序是在编译时加载库文件的,就是使用了静态库。如果是在运行时加载了目标代码,就成为动态库。如果是使用静态库,则静态库代码在编译时就拷贝到了程序的代码段,程序的体积会膨胀。如果使用动态库,则程序中只保留库文件的名字和函数名,在运行时去查找库文件和函数体,程序的体积基本变化不大。
  3. 静态库的原则是“以空间换时间”,增加程序体积,减少运行时间。
  4. 动态库的原则是“以时间换空间”,增加了运行时间,减少了程序本身的体积。
    静态情况下,它把库直接加载到程序里,而在动态链接的时候,它只是保留接口,将动态库与程序代码独立。这样就可以提高代码的可复用度,和降低程序的耦合度。
    另外,运行时,要保证主程序能找到动态库,所以动态库一般发布到系统目录中,要么就在跟主程序相对很固定的路径里,这样不管主程序在本机何时何地跑,都能找得到动态库。而静态库只作用于链接时,运行主程序后静态库文件没存在意义了。
    参考链接
内存分区
  1. 栈区——由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  2. 堆区——由程序员分配释放,使用new、malloc申请。其分配方式类似于链表。
  3. 全局区(静态区)(static)——全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后系统释放。
STL中map、hash_map、unordered_map、unordered_set底层实现

1、STL的map底层是用红黑树实现的,查找时间复杂度是log(n);

2、STL的hash_map底层是用hash表存储的,查询时间复杂度是O(1);

3、STL的unordered_map和unordered_set使用的是hash表,unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
STL之queue及其底层实现——list模拟
STL之priority_queue底层实现——优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构

哈希的概念

红黑树的概念

指针和引用的区别

1.指针和引用的定义和性质区别:
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来

的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

int a=1;int *p=&a;

int a=1;int &b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。
(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

左值引用和右值引用

参考

  • 右值引用用有什什么作用?
    右值引用用的主要目目的是为了了实现转移语义和完美转发,消除两个对象交互时不不必要的对象拷⻉贝,也能够更更加简洁明确地定义泛型函数
static静态函数

可以使用全局变量达到共享数据的目的,例如在一个程序文件中有多个函数,没一个函数都可以改变全局变量的值,但安全性得不到保证。如果想要在同类的多个对象之间实现数据共享,可以使用静态成员变量。static成员变量必须先初始化才能使用。static成员变量既可以通过对象来访问,也可以通过类来访问。static成员变量不随着对象的创建而分配内存,也不随着对象的销毁而释放内存。普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
普通成员函数可以访问所有成员变量,而静态成员函数只能访问静态成员变量。静态成员函数没有this指针。

静态方法是在类中使用staitc修饰的方法,在类定义的时候已经被装载和分配。而非静态方法是不加static关键字的方法,在类定义时没有占用内存,只有在类被实例化成对象时,对象调用该方法才被分配内存。

  • 静态方法只能访问静态方法和静态成员。

  • 非静态方法要被实例化才能被静态方法调用。

extern 关键字

置于变量或者函数前: 表明该变量或者函数定义在别的文件中

mutable关键字

mutable 只能用来修饰类的数据成员

类的继承中,保护继承的理解

在保护继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的保护成员,而基类的私有成员在派生类中不能访问。因为基类的公有成员和保护成员在派生类中都成了保护成员,所以派生类的新增成员可以直接访问基类的公有成员和保护成员,而派生类的对象不能访问它们,上一讲鸡啄米说过,类的对象也是处于类外的,不能访问类的保护成员。对基类的私有成员,派生类的新增成员函数和派生类对象都不能访问。
假设A类是基类,B类是从A类继承的派生类,A类中有保护成员,则对派生类B来说,A类中的保护成员和公有成员的访问权限是一样的。而对A类的对象的使用者来说,A类中的保护成员和私有成员都一样不能访问。可见类中的保护成员可以被派生类访问,但是不能被类的外部对象(包括该类的对象、一般函数、其他类等)访问。我们可以利用保护成员的这个特性,在软件开发中充分考虑数据隐藏和共享的结合,很好的实现代码的复用性和扩展性。

回调函数

回调函数:一个通过函数指针调用的函数就叫做回调函数

作用:对特定的事件或条件进行响应。

函数指针和指针函数
  1. 函数指针是指向函数的指针变量,即本质是一个指针变量。

int (f) (int x); / 声明一个函数指针 */

f=func; /* 将func函数的首地址赋给指针f */
 
2. 指针函数即返回指针的函数,即本质是一个函数,函数的返回类型是某一类型的指针,如 int *f(x,y);

深拷贝和浅拷贝

如果被拷贝内容是地址,也就是当前变量是个指针,指向另一片存储空间,那么深拷贝是连同地址和地址处的内容一并拷贝。

是针对指针的。

  • 浅拷贝:只拷贝指针地址,意思是浅拷贝指针都指向同一个内存空间,当原指针地址所指空间被释放,那么浅拷贝的指针全部失效,形成悬挂指针。默认拷贝构造函数执行的是浅拷贝。

  • 深拷贝:先申请一块跟拷贝数据一样大的内存空间,把数据复制过去。这样拷贝多次,就有多个不同的内存空间,互不干扰。

深拷贝和浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

malloc和new的区别

编程相关面试整理——cpp&python_第2张图片

参考链接

堆栈区别
  • 栈:由系统自动分配,速度较快。 但程序员是无法控制的。
  • 堆:是由malloc或new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
    参考链接
const int* p 和 int* const q 兩者之差別:

标准教科书答案:const int * p:const右边接近于int这个类型声明,意思是有个指针p,指向的是一个int型的整数常量。即p可变,*p不可变。
int * const p表明指针变量p是const型,它的指向不可修改,但是指向的对象可以修改。

静态变量和全局变量之间的区别
  • 变量可以分为:全局变量、静态全局变量、静态局部变量和局部变量。
  • 按存储区域分,全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。
  • 按作用域分,全局变量在整个工程文件内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,并且程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。

全局变量的声明之前,再冠以static就构成了静态的全局变量。两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。
参考链接

类对象作为函数参数

对象作为函数参数时有三种情况

  • 一为 普通类作形参直接传递参数
  • 二为引用类对象作为函数参数
  • 三为对象指针作为函数参数
struct A{
    A();
    ~A();
    string name;
    string sex;
}
struct B:public A{
    B();
    ~B();
    string addr;
    string job;
}
bool getPerson(B b)

假如我们在调用getPerson函数的时候对B进行的是值传递,那么在函数内要对B调用一次copy构造函数,对其两个成员 addr,job也要初始化,又因为B继承自A,所以也要负责初始化A,以及A的两个成员name,sex,不知不觉进行了6次构造动作,然后在函数结束的时候,又会对这构造的6个对象分别进行了6次销毁动作!
我以前简单的以为值传递只比引用传递多了一个构造操作,今天读完effective c++第20条,才认识到多出来的是这么复杂的行为。

这里推荐使用第二种 第一种应用起来只能修改形参的值不能同时修改函数外的被调用对象的值
参考链接

TCP 3次握手、4次挥手的全过程

TCP采用三次握手建立一个连接,采用四次挥手来关闭一个连接。

  1. TCP三次握手过程和状态变迁
    编程相关面试整理——cpp&python_第3张图片
    一开始,客户端和服务端都处于CLOSED状态。先是服务端主动监听某个端口,处于LISTEN状态。
    客户端会随机初始化序号,将此序号至于TCP首部的序号字段中,同时把SYN标志位置1,表示SYN报文。接着把第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT状态。
    服务端收到客户端的 SYN 报文文后,首首先服务端也随机初始化自自己己的序号( server_isn ),将此序号填入入 TCP 首首部的「序号」字段中,其次把 TCP 首首部的「确认应答号」字段填入入 client_isn + 1 , 接着把 SYN 和 ACK 标志位置为 1 。最后把该报文文发给客户端,该报文文也不不包含应用用层数据,之后服务端处于 SYN-RCVD 状态。
    客户端收到服务端报文文后,还要向服务端回应最后一一个应答报文文,首首先该应答报文文 TCP 首首部 ACK
    标志位置为 1 ,其次「确认应答号」字段填入入 server_isn + 1 ,最后把报文文发送给服务端,这
    次报文文可以携带客户到服务器器的数据,之后客户端处于 ESTABLISHED 状态。
    服务器器收到客户端的应答报文文后,也进入入 ESTABLISHED 状态。
    从上面面的过程可以发现第三次握手手是可以携带数据的,前两次握手手是不不可以携带数据的,这也是面面试常问的题。
    一旦完成三次握手手,双方方都处于 ESTABLISHED 状态,此时连接就已建立立完成,客户端和服务端就可以
    相互发送数据了了。
  2. TCP 断开连接是通过四次挥手手方方式。双方方都可以主动断开连接,断开连接后主机中的「资源」将被释放。
    编程相关面试整理——cpp&python_第4张图片
    客户端打算关闭连接,此时会发送一一个 TCP 首首部 FIN 标志位被置为 1 的报文文,也即 FIN 报
    文文,之后客户端进入入 FIN_WAIT_1 状态。
    服务端收到该报文文后,就向客户端发送 ACK 应答报文文,接着服务端进入入 CLOSED_WAIT 状态。
    客户端收到服务端的 ACK 应答报文文后,之后进入入 FIN_WAIT_2 状态。
    等待服务端处理理完数据后,也向客户端发送 FIN 报文文,之后服务端进入入 LAST_ACK 状态。
    客户端收到服务端的 FIN 报文文后,回一一个 ACK 应答报文文,之后进入入 TIME_WAIT 状态
    服务器器收到了了 ACK 应答报文文后,就进入入了了 CLOSED 状态,至至此服务端已经完成连接的关闭。
    客户端在经过 2MSL 一一段时间后,自自动进入入 CLOSED 状态,至至此客户端也完成
    连接的关闭。
    你可以看到,每个方方向都需要一一个 FIN 和一一个 ACK,因此通常被称为四次挥手。
TCP和UDP区别

TCP是面向连接的、可靠的、基于字节流的传输层通信协议。

  • 面向连接:一定是 " 一对一 "才能连接,不能像UDP协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的。
  • 可靠的:无论网络链路中出现了怎样的链路变化,TCP都可以保证一个报文一定能够到达接收端
  • 字节流:消息是 " 没有边界 " 的,所以无论消息有多大都可以进行传输。并且消息 " 有序 ",当 " 前一个 "消息没有收到的时候,即时它先收到了后面的字节,那么也不能扔给应用层去处理,同时对 " 重复 "的报文会自动丢弃。
  1. 连接

  2. 服务对象

  3. 可靠性

  4. 拥塞控制、流量控制

  5. 首部开销

  6. 传输方式

  7. 分片不同

  8. 应用场景不同
    由于TCP是面向连接,能保证数据的可靠性交付,因此经常用于:

    • FTP文件传输
    • HTTP 、HTTPS

    由于UDP面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于

    1. 包总量较少的通信,如DNS、SNMP等
    2. 视频、音频等多媒体通信
    3. 广播通信
进程线程区别

答:
(1)进程是程序的一次执行,线程是进程中的执行单元。计算机只能在同一个时间处理一个线程,多核可以避免,将任务分给不同的cpu核,避免多线程的劣势。
(2)进程间是独立的,这表现在内存空间、上下文环境上,线程运行在进程中;
(3)一般来讲,进程无法突破进程边界存取其他进程内的存储空间;而同一进程所产生的线程共享内存空间。(相当于车间的空间是工人们共享的,工人就是线程,车间就是进程)
(4)同一进程中的两段代码不能同时执行,除非引入多线程。
漫画进程与线程

C++线程中的几种锁

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

  1. 互斥锁
    互斥锁用于控制多个线程对它们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在同一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。
    在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞的方式进行等待。
  • 头文件:
  • 类型:pthread_mutex_t
  1. 条件锁
    所谓条件锁就是条件变量,某一个线程因为某个条件未满足时,可以使用条件变量使程序处于阻塞状态。一旦条件满足,则以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见的就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。这个过程中就是用到了条件pthread_coud_t
  • 头文件:
  • 类型:pthread_cond_t
  1. 自旋锁
    假设我们有一台双核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不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。

当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道“自旋锁”是比较耗费CPU的

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

  1. 读写锁
    计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,修改数据库中存放的数据。因此,允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。
C++并发编程如何避免死锁
  • 死锁是由于并发进程只能按互斥方式访问临界资源等多种因素引起的。死锁的一般定义:若在一个进程集合中,每一个进程都在等待一个永远不会发生的事件而形成一个永久的阻塞状态,这种阻塞状态就是死锁。
  • 常见的死锁场景:
  1. 一个线程已经拿到了锁,未释放锁,但是又尝试拿同样的锁,这时就会死锁。
  2. 两个以上线程A B……,两个以上的锁a b……,线程A已经拿到a锁,线程B已经拿到b锁,但是线程A在没有释放a锁尝试获取b锁,线程B没有释放b锁尝试获取a锁,这就也会发生死锁。
  • 死锁的产生条件:
    1.互斥(mutual exclusion):系统存在着临界资源;
    2.占有并等待(hold and wait):已经得到某些资源的进程还可以申请其他新资源;
    3.不可剥夺(no preemption):已经分配的资源在其宿主没有释放之前不允许被剥夺;
    4.循环等待(circular waiting):系统中存在多个(大于2个)进程形成的封闭的进程链,链中的每个进程都在等待它的下一个进程所占有的资源;
  • 预防死锁的办法:
  1. 加锁的时候使用try_lock(),如果获取不到锁,那么久释放自己手里面的所有锁。
  2. 可以在加锁的过程中对mutex进行地址的比较,永远从最小地址开始加锁,这样的话就能保证所有的线程都按同一个顺序加锁,这样的话,也能避免死锁。
  3. 可以设置锁的属性为PTHREAD_MUTEX_ERRCHECK,这种锁如果发生死锁会返回错误,不过效率要稍微低一些。
  • 避免死锁的方法:
  1. 一个线程已经获得一个锁时,别再去获取第二个锁。如果能坚持这个建议,因为每个线程只持有一个锁,锁上之后就不会产生死锁。即使互斥锁造成死锁的最常见原因,也可能会在其他方面收到死锁的困扰(比如线程间的互相等待)。当你需要获取多个锁,使用一个std::lock来做这件事(对获取锁的操作上锁),避免产生死锁。
  2. 避免在持有锁时调用用户提供的代码
  3. 使用固定顺序获取锁,当硬性条件要求你获取两个以上的锁,并且不能使用std::lock单独操作来获取它们,那么最好在每个线程上,用固定的顺序获取它们。
  4. 使用锁的层次结构。这个建议需要对应用进行分层,并且识别在给定层上所有可上锁互斥两。当代码试图对一个互斥量上锁,在该层已被低层持有时,上锁时不允许的,你可以在运行时对其进行检查,通过分配层数到每个互斥量上,以及记录被每个线程上锁的互斥量。
    参考链接
  • 银行家算法
    是一个避免死锁的著名算法。当一个进程申请使用资源的时候,银行家算法通过先试探分配给该进程资源,然后通过安全性算法判断分配后的系统,是否处于安全状态,若不安全则试探分配作废,让该进程继续等待。。
    那么此时一个问题是,如何判断系统处于安全状态?
    参考链接

参考链接

lock_guard用过么

std::lock_guard在构造时只被锁定一次,并且在销毁时解锁。
参考链接

一点点操作系统的知识
  1. 32位机和64位到底是指的什么
    计算机所说的32位机指的是CPU通用寄存器的数据宽度为32位,32位指令集就是运行32位数据的指令,也就是说处理器一次可以运行32bit数据。
    32位处理器的寻址空间最大4GB。
    64位指令集可以运行64位数据指令,也就是说处理器一次可提取64位数据。
  2. cache的用途和实现方式
    由于 CPU 和主存所使用的半导体器件工艺不同,两者速度上的差异导致快速的 CPU 等待慢速的存储器,为此需要想办法提高 CPU 访问主存的速度。除了提高 DRAM 芯片本身的速度和采用并行结构技术以外,加快 CPU 访存速度的主要方式之一是在 CPU 和主存之间增加高速缓冲器,也就是我们主角 Cache。Cache 位于 CPU 和内存之间,可以节省 CPU 从外部存储器读取指令和数据的时间。
    cache 是一种小容量高速缓冲存储器,由快速的 SRAM 组成,直接制作在 CPU 芯片内,速度较快,几乎与 CPU 处于同一个量级。在 CPU 和主存之间设置 cache,总是把主存中被频繁访问的活跃程序块和数据块复制到 cache 中。由于程序访问的局部性,大多数情况下,CPU 可以直接从 cache 中直接取得指令和数据,而不必访问慢速的主存。
    在 CPU 执行程序过程中,需要从主存取指令或写数据时,先检查 cache 中有没有要访问的信息,若有,就直接在 cache 中读写,而不用访问主存储器。若没有,再从主存中把当前访问信息所在的一个一个主存块复制到 cache 中。因此,cache 中的内容是主存中部分内容的副本。
    cache参考

C++中的内存泄漏是怎么发生的?
如何避免C++中发生内存泄漏?
C++语言中的继承体系。
C++语言中的多态。

多态

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。

在用父类指针调用函数时,实际调用的是指针指向的实际类型(子类)的成员函数。
当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定。

https://blog.csdn.net/wanghao109/article/details/11483373

确认进程启动过程中,动态库加载的时间与用户代码响应时间。如果是动态库加载时间过长,考虑进程的动态库优化;如果是用户代码响应时间过长,考虑代码优化。(就出现了一次)

虚函数

虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段中。当子类继承了父类的时候,也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
相关面试题
https://blog.csdn.net/weixin_36299192/article/details/88415452
https://juejin.cn/post/6844903991780835342

智能指针

C++14 智能指针unique_ptr、shared_ptr、weak_ptr
shared_ptr和unique_ptr之间的区别在于:shared_ptr是引用计数的智能指针,而unique_ptr不是。这意味着,可以有多个shared_ptr实例指向同一块动态分配的内存,当最后一个shared_ptr离开作用域时,才会释放这块内存。另一方面,unique_ptr意味着所有权。单个unique_ptr离开作用域时,会立即释放底层内存。
智能指针
智能指针可用于动态资源管理,资源分配即初始化,定义一个类来封装资源的分配和释放,在构造函数中完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
智能指针的原理:智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类是栈上的对象,智能指针指向堆上开辟的空间,函数结束时,栈上的函数会自动被释放,智能指针指向的内存也会随之消失,防止内存泄露。

STL容器

详情看另一篇博客(侯捷老师讲的太好了)

  1. STL中vector的实现原理
    vector采用的数据结构很简单:线性的连续空间。
    它以两个迭代器start和finish分别指向配置得来的连续空间中目前已经被使用的空间。迭代器end_of_storage指向整个连续空间的尾部。

http://c.biancheng.net/view/6901.html
2. 哈希
https://blog.csdn.net/fjhugjkdsd/article/details/108091424
3. unordered_map与unordered_set的底层实现
所有无序容器的底层实现都是采用的哈希表存储结构,用“链地址法”解决数据存储位置发生冲突的哈希表。
4. map和set

20道必须掌握的C++面试题

问13:指针和引用的区别?
答:

  1. 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用仅是个别名;

  2. 引用使用时无需解引用(*),指针需要解引用;

  3. 引用只能在定义时被初始化一次,之后不可变;指针可变;

  4. 引用没有 const,指针有 const;

  5. 引用不能为空,指针可以为空;

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

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

  8. 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

9.从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。

60道30K+C++工程师面试必问面试题

c++面试题大全

c++面试题总结

c++面试题总结一

c++面试题总结二

c++经典面试题

c++经典面试50题

你可能感兴趣的:(工作搬砖,python,面试,数据结构)