学习

c++内存管理RAII

RAII(Resource Acquisition Is Initialization)资源获取即初始化。代表使用局部对象来管理资源的技术。

这里的资源主要指操作系统中有限的东西,例如内存、网络套接字等,局部对象指的存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。

资源的使用一般经历3个步骤:获取资源、使用资源、销毁资源。但是销毁资源经常忘记,需要自动销毁。RAII给出的方案是利用C++语言局部对象自动销毁的特性来控制资源的生命周期,有效防止资源泄露和程序异常。

智能指针是RAII机制的一种典型应用。通过封装原生指针,智能指针能自动管理内存的生命周期,从而避免野指针。

c++构造函数

  • C++的构造函数不是线程安全的
  • 构造函数是对类的数据成员进行初始化和分配内存,一个C++的空类的默认的成员函数有:默认构造函数和拷贝构造函数、析构函数、赋值函数(赋值运算符)、取值函数。构造函数可以被重载,可以多个可以带参数。析构函数只有一个,不能被重载,不能带参数。

C++拷贝构造函数

拷贝构造函数和赋值函数容易混淆。拷贝构造函数是在对象被创建时调用,而赋值函数只能被已经存在了的对象调用。

string c = a;//调用了拷贝构造函数,最好写成c(a)

c = b; //调用了赋值函数

当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象

A a;

A b(a); // 拷贝构造

A b = a; // 拷贝构造

C++中三种对象需要复制,此时拷贝构造函数会被调用:

  1. 一个对象以值传递的方式传入函数体
  2. 一个对象以值传递的方式从函数返回
  3. 一个对象需要通过另外一个对象进行初始化

系统提供的默认拷贝构造函数是内存拷贝即浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,此时需要手动重载拷贝构造函数,实现深拷贝。

  • 浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容。(指针虽然复制了,但是所指向的空间内容没有复制,而是由两个对象共用)
  • 深拷贝:复制这个对象的时候为新对象制作了外部对象的独立复制。

拷贝构造函数重载如下:

A(const A &other) : m_i(other.m_i) {}

C++赋值函数

赋值运算符的重载声明如下:

A& operator = (const A& other)

拷贝构造函数和赋值函数区别:

  1. 拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
  2. 一般来说,在数据成员包含指针对象的时候,需要考虑两种不同的处理需求,一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象。
  3. 拷贝构造函数首先是一个构造函数,它调用的时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配,要先把内存释放掉,而且还要检查一下两个对象是否为同一个对象,如果是,不做任何操作,直接返回。

C++进程

进程控制块PCB:本质上是一个结构体,包含了进程相关的一系列信息:

  • 进程id:每一个进程的唯一标识,类型pid_t,其实是int
  • 进程的状态:开始、就绪、阻塞、运行、终止
  • 进程切换所需要保存的和恢复的现场信息:其实就是寄存器里的值
  • 描述虚拟地址空间的信息
  • 描述当前控制终端的信息
  • 当前工作目录
  • umask掩码:保护文件权限
  • 文件描述符表
  • 信号相关信息
  • 会话和进程组:守护进程会用到
  • 用户ID和组ID
  • 进程的资源上线

进程控制

fork函数:创建一个子进程,失败返回-1,成功父进程返回子进程的pid,子进程返回0

  • fork之后的父子进程关系,实际上是子进程把父进程的内容拷贝了一份。遵从“读时共享,写时复制”,意思是当父子进程有写的操作时,就会单独给子进程映射一块物理内存,防止父子进程操作同一个数据而导致数据错误。调用fork函数的进程将根据当前进程的内容复制得到一个子进程,那么当前进程就会成为新进程的父进程,新进程的内容与父进程的内容几乎一致。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
  • 需要fork进程的场景:
  • 在服务器程序中,一个请求到来后,为了避免服务器阻塞,fork出一个子进程处理请求,父进程仍然继续等待请求的到来,但是开销会稍大
  • 一个进程要执行一个不同的程序,例如shell端执行一条命令,实际上是bash调用fork之后在执行exec函数
  • 一个子进程同一时刻最多只有一个父进程。fork之后,父进程和子进程谁先执行完全由调度器决定。
  • fork之后父子进程的异同:
  • 相同:全局变量、data、bss、.txt、堆栈、环境变量、用户ID、当前工作目录......
  • 不同:进程ID、fork的返回值、父进程ID、进程运行的时间、定时器、子进程不继承父进程设置的文件锁......
  • 个人理解:fork之后,产生的两个进程都会执行fork后面的代码
  • fork调用失败的原因可能是
  • 系统进程太多,内存空间不足
  • 实际用户的进程数超过了限制

进程退出

  • return、exit和_exit之间的区别:只有在main函数中的return才能起到退出进程的作用,子函数中return不能退出进程
  • 使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数直接终止进程,不会做任何收尾工作。

进程等待的必要性

  • 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏
  • 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程
  • 对于一个进程来说,最关心自己的就是其父亲进程,因为父进程需要知道自己派给子进程的任务完成的如何
  • 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息

线程同步的4种方式

在并发的情况下,指令执行的先后顺序由内核决定。同一个线程的内部,指令按照先后顺序执行,但不同线程之间的指令很难说清楚哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件,在这样的状况下,计算机的结果很难预知,所以应该尽量避免竞争条件的形成。

C++线程创建

c++线程创建用到#include <thread>

thread t1(run, 1); //第一个参数为函数,第二个参数为函数参数,如果函数没有参数,thread对象便没有第二个参数,同理若函数有两个参数,thread变量声明中便有三个参数。

多线程同步

对于多线程的程序来说,同步指的是在一定的时间内只允许某一个线程访问某个资源,具体可以使用一下四种方式实现:

  1. 互斥锁mutex
  2. 最常见的线程同步方式,它是一种特殊的变量,它有 lock 和 unlock 两种状态,一旦获取,就会上锁,且只能由该线程解锁,期间,其他线程无法获取。在使用同一个资源前加锁,使用后解锁,即可实现线程同步,需要注意的是,如果加锁后不解锁,会造成死锁
  3. 优点:使用简单
  4. 缺点:重复锁定和解锁,每次都会检查共享数据结构,浪费时间和资源;频繁查询的效率非常低
  5. #include <mutex> 声明mutex变量和lock_guard变量 =>mutex m;lock_guard<mutex> l(m);lock_guard优势是实现简单、使用方便,适用于大多数场景,但存在的问题是使用场景过于简单,无法处理一些精细操作。此时便需要使用unique_lock。unique_lock<mutex> l(m);
  6. 条件变量condition
  7. 当线程在等待某些满足条件时使线程进入睡眠状态,一旦条件满足,就唤醒,这样不会占用宝贵的互斥对象锁,实现高效。条件变量允许线程阻塞并等待另一个线程发送信号,一般和互斥锁一起使用。条件变量被用来阻塞一个线程,当条件不满足时线程会解开互斥锁,并等待条件发生变化。一旦其他线程改变了条件变量,将通知相应的阻塞线程,这些线程重新锁定互斥锁,然后执行后续代码,最后在解开互斥锁。
  8. 读写锁reader-writer lock
  9. 也称为共享-独占锁,一般用在读和写的次数有很大不同的场合。即对某些资源的访问会出现两种情况,一种是访问的排他性,需要独占,称之为写操作;还有就是访问可以共享,称之为读操作。
  10. 最适用于对数据结构的读操作次数多余写操作次数的场合
  11. 读写锁有以下几种状态:
  12. 写锁定的状态,在解锁前,所有试图加锁的线程都会阻塞
  13. 读锁定的状态,所有试图以读模式加锁的线程都可以得到访问权,但是以写模式加锁的线程会阻塞
  14. 读写锁处于读模式的锁(未加锁)状态时,有另外的线程试图以写模式加锁,则读写锁会阻塞读模式的加锁请求,这样避免了读模式锁长期占用导致写模式锁长期阻塞的情况
  15. 处理这种问题一般有两种常见的策略:
  16. 强读者同步:总是给读者更高的优先权,只要没有写操作,读者就可以获取访问权,比如图书馆查询系统
  17. 强写者同步:读者只能等到写者结束之后才能执行,比如航班订票系统查看最新的信息记录
  18. 信号量semphore
  19. 信号量和互斥锁的区别在于:互斥锁只允许一个线程进入临界区,信号量允许多个线程同时进入临界区。即互斥锁使用对同一个资源的互斥的方式达到线程同步的目的,信号量可以同步多个资源以达到线程同步(每个进程中访问临界资源的那段程序称为临界区(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入)
全部评论

相关推荐

异步编程是一种编程模式,用于处理可能会花费较长时间的操作,而不会阻塞其他代码的执行。在同步编程中,代码会按照顺序一步一步执行,每个操作的完成都会等待前一个操作完成后才继续执行。这样的执行方式可能会导致程序在等待某些操作完成时出现阻塞,影响用户界面的响应性或导致整个程序的执行速度变慢。异步编程通过将长时间运行的操作(如网络请求、文件读取、数据库查询等)放置在后台,不会阻塞主线程的执行。在进行异步编程时,可以在发起异步操作后继续执行后续的代码,而不需要等待异步操作完成。当异步操作完成时,系统会通知相应的回调函数或执行注册的事件处理程序,以便使用异步操作的结果继续处理。常见的异步编程模式和技术包括:https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mg58Em&amp;uuid=aa2d7fa706914dfc9afef6476efb3004回调(Callback):&nbsp;将一个函数作为参数传递给异步操作,并在操作完成时调用该函数。这是一种传统的异步编程模式,但它容易造成回调地狱(callback&nbsp;hell)的问题,即多层嵌套的回调函数,难以维护和阅读。Promise:&nbsp;Promise&nbsp;是一种表示异步操作的对象,可以在异步操作完成后进行处理。使用&nbsp;Promise,可以链式地调用&nbsp;then()&nbsp;方法来处理成功的结果,以及&nbsp;catch()&nbsp;方法来处理失败的情况,避免了回调地狱问题。Async/await:&nbsp;Async/await&nbsp;是基于&nbsp;Promise&nbsp;的语法糖,它提供了更加简洁和易读的方式来处理异步操作。通过使用&nbsp;async&nbsp;关键字声明一个函数,并在其中使用&nbsp;await&nbsp;关键字等待异步操作的结果,可以以同步的方式编写异步代码。
点赞 评论 收藏
转发
点赞 1 评论
分享
牛客网
牛客企业服务