首页 > 编程语言 > 详细

c++

时间:2020-03-21 03:40:42      阅读:40      评论:0      收藏:0      [点我收藏+]

static的作用

? static修饰变量只能在本范围内可见(由external变为internal,作用域和链接属性并没有改变):修饰全局变量只能在本cpp文件中可见,修饰局部变量只能在该代码块内可见。修饰类的静态成员在类的对象中共享这一份数据。

c++中的智能指针

? 其实就是一个类,当销毁指向的内存时,可以不用手动free内存,它会自动释放内存空间。

  • auto_ptr

    技术分享图片

  • unique_ptr

    技术分享图片

  • shared_ptr

    shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。

  • weak_ptr

    weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

在main()函数之前执行

? C++ 的全局对象的构造函数会在 main 函数之前先运行,其实在 c 语言里面很早就有啦,在 gcc 中可以使用 attribute 关键字指定如下(在编译器编译的时候就绝决定了)

map和set的实现,有什么区别

? 它们的底层都是用红黑树实现的,关于红黑树和avl树的区别(首先红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高!!!

  • 区别
    1. map是key-value键值对的形式存储,set是关键字的集合。
    2. set的迭代器是const的,不允许修改元素的值;map不是,允许修改value。
    3. map支持下标操作,set不支持

memset()函数

? extern void memset(void buffer, int c, int count) ;

    + buffer:指针或者数组
    + c:赋给buffer的值
    + count:buffer的长度

? 一般用来给一段内存空间全部设置为某个字符。

c++通讯

? 管道、系统IPC(信号、信号量、共享内存、消息队列)、套接字socket

多线程之间的通讯方式

互斥量、信号量、临界区

多线程之间的锁

每个进程的地址空间是独立的,位于一个进程的普通内存区域中的对象是无法被其它进程所访问的,能满足这一要求的内存区域是共享内存,因而同步对象要在进程的共享内存区域内创建。同步对象还可以放在文件中。同步对象可以比创建它的进程具有更长的生命周期。

    1. std::lock_guard

        2. std::unique_lock
                    3. std::condition_variable
  • 互斥锁(mutex)
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <mutex>
  
using std::thread;
using std::vector;
using std::cout;
using std::endl;
using std::mutex;
  
class Incrementer
{
  private:
    int counter;
    mutex m;
  
  public:
    Incrementer() : counter{0} { };
  
    void operator()()
    {
      for(int i = 0; i < 100000; i++)
      {
        this->m.lock();
        this->counter++;
        this->m.unlock();
      }
    }
  
    int getCounter() const
    {
      return this->counter;
    } 
};
  
int main()
{
  // Create the threads which will each do some counting
  vector<thread> threads;
  
  Incrementer counter;
  
  threads.push_back(thread(std::ref(counter)));
  threads.push_back(thread(std::ref(counter)));
  threads.push_back(thread(std::ref(counter)));
  
  for(auto &t : threads)
  {
    t.join();
  }
  
  cout << counter.getCounter() << endl;
  
  return 0;
}

运行结果

技术分享图片

修改其中代码

for(int i = 0; i < 100000; i++)
{
 this->m.lock();
 try
  {
   this->counter++;
   this->m.unlock();
  }
  catch(...)
  {
   this->m.unlock();
   throw;
  }
}
  • 条件锁(cond):允许线程以一种无竞争的方式等待某个条件的发生。当该条件没有发生时,线程会一直处于休眠状态。当被其它线程通知条件已经发生时,线程才会被唤醒从而继续向下执行。条件变量是比较底层的同步原语,直接使用的情况不多,往往用于实现高层之间的线程同步。使用条件变量的一个经典的例子就是线程池(Thread Pool)了。
#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
std::mutex       g_mutex;   // 用到的全局锁
std::condition_variable g_cond;   // 用到的条件变量
int g_i    = 0;
bool g_running = true;
void ThreadFunc(int n) {       // 线程执行函数
 for (int i = 0; i < n; ++i) {
  {
   std::lock_guard<std::mutex> lock(g_mutex);   // 加锁,离开{}作用域后锁释放
   ++g_i;
   std::cout << "plus g_i by func thread " << std::this_thread::get_id() << std::endl;
  }
 }
 std::unique_lock<std::mutex> lock(g_mutex);    // 加锁
 while (g_running) {
  std::cout << "wait for exit" << std::endl;
  g_cond.wait(lock);                // wait调用后,会先释放锁,之后进入等待状态;当其它进程调用通知激活后,会再次加锁
 }
 std::cout << "func thread exit" << std::endl;
}
int main() {
 int     n = 100;
 std::thread t1(ThreadFunc, n);    // 创建t1线程(func thread),t1会执行`ThreadFunc`中的指令
 for (int i = 0; i < n; ++i) {
  {
   std::lock_guard<std::mutex> lock(g_mutex);
   ++g_i;
   std::cout << "plus g_i by main thread " << std::this_thread::get_id() << std::endl;
  }
 }
 {
  std::lock_guard<std::mutex> lock(g_mutex);
  g_running = false;
  g_cond.notify_one();   // 通知其它线程
 }
 t1.join();     // 等待线程t1结束
 std::cout << "g_i = " << g_i << std::endl;
}
  1. 首先,这在一个局部作用域内, std::lock_guard 在构造时,会调用 g_mutex->lock() 方法;

  2. 局部作用域代码结束后, std:;lock_guard 的析构函数会被调用,函数中会调用 g_mutex->unlock() 方法。

  3. 当线程调用 g_cond.wait(lock) 前要先手动调用 lock->lock() ,这里是通过 std::unique_lock 的构造方法实现的;

  4. 当线程调用 g_cond.wait(lock) 进入等待后,会调用 lock->unlock() 方法,所以这也是前面构造lock时使用了 std::unique_lock ;

  5. 通知使用的 g_cond.notify_one() ,这个可以通知一个线程,另外还有 g_cond.notify_all() 用于通知所有线程;

  6. 线程收到通知的代码放在一个while循环中,这是为了防止APUE中提到的虚假通知。

  • 自旋锁

从 实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。所以,自旋锁一般用用多核的服务器。

int num = 0;
spin_mutex sm;

void thread_proc()
{

  for(int i = 0; i < 100000; ++i) {
    sm.lock();
    ++num;
    sm.unlock();
  }
}

int main()
{
  std::thread td1(thread_proc), td2(thread_proc);
  td1.join();
  td2.join();
  std::cout << num << std::endl;
  return 0;
}
  • 读写锁(rdlock)
  • 信号量(semophore):通过精心设计信号量的PV操作,可以实现很复杂的进程同步情况(例如经典的哲学家就餐问题和理发店问题)。而现实的程序设计中,却极少有人使用信号量。能用信号量解决的问题似乎总能用其它更清晰更简洁的设计手段去代替信号量。

windows系统中临界区(Critical Section)、事件对象(Event)

c++11新特性

  • nullptr

  • 类型推导 auto(不能用于推导数组类型,不能用于函数传参) 和decltype关键字

    有的时候我们只需要计算表达式得出的类型,不需要返回值

    auto x = 1;
    auto y = 2;
    decltype(x+y) z;

    拖尾返回类型、auto 与 decltype 配合,利用 auto 关键字将返回类型后置:

    template<typename T, typename U>
    auto add(T x, U y) -> decltype(x+y) {
        return x+y;
    }
  • 初始化列表

    struct A {
        int a;
        float b;
    };
    struct B {
    
        B(int _a, float _b): a(_a), b(_b) {}
    private:
        int a;
        float b;
    };
    
    A a {1, 1.1};    // 统一的初始化语法
    B b {2, 2.2};
  • Lambda表达式

    提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

    [ caputrue ] ( params ) opt -> ret { body; };
    • caputrue是捕获列表;
    • params是参数表;
    • opt是函数选项;mutable,exception,attribute。mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
      exception说明lambda表达式是否抛出异常以及何种异常。
      attribute用来声明属性。
    • ret是返回值类型(拖尾返回类型)
    • body是函数体

    捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。

    1. []不捕获任何变量
    2. [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
    3. [=]捕获外部作用域中的所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
    int a = 0;
    auto f = [=] { return a; };
    
    a+=1;
    
    cout << f() << endl;       //输出0
    
    int a = 0;
    auto f = [&a] { return a; };
    
    a+=1;
    
    cout << f() <<endl;       //输出1
    1. [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
    2. [bar]按值捕获bar变量,同时不捕获其他变量。
    3. [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量
    class A
    {
     public:
         int i_ = 0;
    
         void func(int x,int y){
             auto x1 = [] { return i_; };                   //error,没有捕获外部变量
             auto x2 = [=] { return i_ + x + y; };          //OK
             auto x3 = [&] { return i_ + x + y; };        //OK
             auto x4 = [this] { return i_; };               //OK
             auto x5 = [this] { return i_ + x + y; };       //error,没有捕获x,y
             auto x6 = [this, x, y] { return i_ + x + y; };     //OK
             auto x7 = [this] { return i_++; };             //OK
    };
    
    int a=0 , b=1;
    auto f1 = [] { return a; };                         //error,没有捕获外部变量    
    auto f2 = [&] { return a++ };                      //OK
    auto f3 = [=] { return a; };                        //OK
    auto f4 = [=] {return a++; };                       //error,a是以复制方式捕获的,无法修改
    auto f5 = [a] { return a+b; };                      //error,没有捕获变量b
    auto f6 = [a, &b] { return a + (b++); };                //OK
    auto f7 = [=, &b] { return a + (b++); };                //OK

    lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。

重载输入输出流

#include<iostream>
using namespace std;
class coord {
    int x, y;
public:
    coord(int i = 0, int j = 0)
    {
        x = i;
        y = j;
    }
    friend ostream& operator<<(ostream  &stream, coord &ob);//这里第二个参数采用了引用(&ob),
    //是为了减少调用的开销,使用引用参数只需把对象的地址传进来就可以了,而不需把每个域分量逐一传进来
    //而消耗内存和时间。所以不用普通的对象做参数,虽然结果一样。但是<<重载的函数返回值和第一个参数必须为输出流类ostream的的引用。
    friend istream& operator>>(istream &input, coord &ob);//这里的第二个参数必须为引用,目的是函数体对参数a的修改能影响实参,因为从输入
    //流输入的值要存入与a对应的实参中。注意重载输出<<时的作用并不是为了修改实参,此点不同。
};

ostream &  operator<<(ostream &stream, coord &ob)
{
    stream << ob.x << "," << ob.y << endl;//stream为ostream类的一个对象的引用,作为左操作数(cout也是一样,是C++中的两个流对象)
    return stream;
}

istream& operator>>(istream &input, coord &ob)
{
    cout << "Enter x and y value:";
    input >> ob.x;
    input >> ob.y;
    return input;
}

int main()
{
    coord a(55, 66), b(100, 220);
    cout << a << b;
    cin >> a;
    cin >> b;
    cout << a << b;
    return 0;
}

分析:上面输出重载函数的形参stream是ostream类对象的引用,返回值也是ostream类对象的引用。在main中cout<<a;cout是ostream类对象,a是coord类对象,所以可以把其理解为operator<<(cout,a);

#include <iostream>
using namespace std;
 
class Distance
{
    private:
        int feet;             // 0 到无穷
        int inches;           // 0 到 12
    public:
        // 所需的构造函数
        Distance(){
            feet = 0;
            inches = 0;
        }
        Distance(int f, int i){
            feet = f;
            inches = i;
        }
        ostream& operator<<( ostream & os)
        {
        os<<"英寸:"<<feet<<"\n英尺:"<<inches;
        return os;
    }
};
int main ()
{
    Distance d1(20,18);
    d1<<cout;//相当于d1.operator<<(cout)
}

C++ STL

  • 容器

    • vector向量:类似数组操作。

    • size 是当前 vector 容器真实占用的大小,也就是容器当前拥有多少个容器。

      capacity 是指在发生 realloc 前能允许的最大元素数,即预分配的内存空间。

      当然,这两个属性分别对应两个方法:resize()reserve()

      使用 resize() 容器内的对象内存空间是真正存在的。

      使用 reserve() 仅仅只是修改了 capacity 的值,容器内的对象并没有真实的内存空间(空间是"野"的)。

    #include <iostream>
    #include <vector>
    
    using std::vector;
    int main(void)
    {
        vector<int> v;
        std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
        v.reserve(10);
        std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
        v.resize(10);
        v.push_back(0);
        std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
    
        return 0;
    }

    技术分享图片

    针对 capacity 这个属性,STL 中的其他容器,如 list map set deque,由于这些容器的内存是散列分布的,因此不会发生类似 realloc() 的调用情况,因此我们可以认为 capacity 属性针对这些容器是没有意义的,因此设计时这些容器没有该属性。

    在 STL 中,拥有 capacity 属性的容器只有 vector 和 string。

系统IO模型

+ 阻塞IO(Blocking IO)

技术分享图片

? 在这个例子中,我们会通过UDP而不是TCP来举例,因为对于UDP来说,等待数据就绪这一步更加直观:要不就是收到了一个数据报,要不就是没收到一个数据报.但是对于TCP来说,还有很多额外的变量.

上图中的recvfrom是一个系统调用.当我们执行一次系统调用的时候,有一次从用户态到内核态的切换.

从上图中我们可以看到,进程调用recvfrom之后,这个系统调用并不会立即返回,它会等到数据报到达并且被拷贝到应用程序的缓冲区中,或者出现了一个错误,才会返回.我们称这个过程是阻塞的,应用程序只有在数据报被放入缓冲区之后,才能继续进行.

  • 非阻塞IO(Nonblocking IO)

    非阻塞IO和阻塞IO相对,它会告诉内核,"当我要你完成的IO操作不能完成时,不要让进程阻塞,你给我返回一个错误就行了".过程如下图所示:

    技术分享图片

    在上面的三个recvfrom操作中,由于数据并没有就绪,所以内核返回了一个EWOULDBLOCK错误.在第四个recvfrom中,数据已经就绪了,并且已经被拷贝到我们的应用程序的缓冲区了,内核返回一个OK,然后我们的应用程序处理这些数据.

    我们可以看到,在这种模型中,我们需要使用轮询的方式来确定数据到底是否就绪.尽管这会浪费CPU时间,但是仍然是比较常见的模型,一般是在系统函数中用到.

  • I/O复用(I/O Multiplexing)

    在I/O多路复用中,我们会调用select()或者poll(),并且阻塞在这两个系统调用上.而不是阻塞在recvfrom这个实际的IO操作的系统调用上.下面是I/O多路复用模型的过程图:

    技术分享图片

    从上图中,我们可以看到,我们会阻塞在select()这个系统调用上,并等待数据到达.当select()告诉我们数据到达时,再通过recvfrom系统调用将数据拷贝到应用程序的缓冲区.多了一次系统调用,确实是I/O多路复用模型的缺点.但是存在即合理,它也有优点.

    它的优点在于,select可以同时监听多个文件描述符,以及感兴趣的事件.所以,我们可以在一个线程中完成之前需要好多个线程才能完成的事情.

    比如,我们想要同时从一个接受来自Socket的数据,以及从文件中读数据.在阻塞IO模型中,我们会这么做:

    1.创建一个线程A,在其中创建一个Socket Server,并通过它的accept()方法,等待客户端的连接并处理数据
    2.创建一个线程B,在其中打开文件并且读数据.

    这就需要两个线程,对吧?

    而且我们又知道,线程之间的切换是有开销的,也是需要涉及到用户态到内核态的转换.

    而我们在I/O多路复用模型中,可以这样做:

      1.通过注册函数告诉系统,应用程序对于Socket的读事件以及文件的读事件感兴趣
      2.通过轮询调用select()方法,查看哪些我们感兴趣的事件已经发生了
      3.在同一个线程中,依次进行对应的操作

    我们可以看到,在这里我们只需要用一个线程就可以做到在阻塞IO中我们需要两个线程才能做到的事情.这就是I/O复用中的复用的含义.

  • 信号驱动IO(signal driven I/O)

    信号驱动IO使用信号量机制,它告诉内核,当文件描述符准备就绪时,通过SIGIO信号通知我们.过程如下:

    技术分享图片

    我们首先通过sigaction系统调用安装一个事件处理器.这个操作会立即返回.所以我们的应用程序会继续运行,而不会阻塞.当数据准备就绪时,内核会给我们的应用程序发出一个SIGIO信号,我们可以继续进行下面的处理:在信号处理器中,通过recvfrom系统调用将数据从内核缓冲区读取到应用程序缓冲区中,告诉应用程序从缓冲区读取数据并且处理.这种模型的优点是,在等待数据就绪时,应用程序并不会被阻塞.应用程序可以继续运行,只需要在数据就绪时,让时间处理器通知它即可.

  • 异步IO(Asynchronous IO)

    异步IO模型跟事件驱动IO模型类似,也是告诉内核,在一定情况下通知我们.但是它跟事件驱动IO模型不同的是,在事件驱动IO模型中,内核会在数据就绪,即数据被拷贝到内核缓冲区时,通知我们.而在异步IO中,内核会在整个操作都被完成,即数据从内核缓冲区拷贝到应用程序缓冲区时,通知我们.如下图所示:

    技术分享图片

    技术分享图片

美团面试题

  1. 如果线上某台虚机CPU Load过高,该如何快速排查原因?只介绍思路和涉及的Linux命令即可 。
  2. 请简要描述MySQL数据库联合索引的命中规则,可举例说明。
  3. 什么是分布式事务,分布式事务产生的原因是什么?分布式事务的解决方案有哪些?分别有哪些优缺点?
  4. 请描述https的请求过程。
  5. 什么是事务传播行为?你知道Spring事务中都有哪些传播类型吗?如何使用/指定传播类型?
  6. IO设计中Reactor 和 Proactor 区别。
  7. 技术分享图片
    技术分享图片
  8. 技术分享图片
  9. 技术分享图片
  10. 技术分享图片

c++

原文:https://www.cnblogs.com/ustc-BlueSky/p/12535622.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!