c++

static的做用html

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

c++中的智能指针

​ 其实就是一个类,当销毁指向的内存时,能够不用手动free内存,它会自动释放内存空间。c++

  • auto_ptr面试

  • unique_ptr数据库

  • shared_ptrwindows

    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;
    }

    img

    针对 capacity 这个属性,STL 中的其余容器,如 list map set deque,因为这些容器的内存是散列分布的,所以不会发生相似 realloc() 的调用状况,所以咱们能够认为 capacity 属性针对这些容器是没有意义的,所以设计时这些容器没有该属性。

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

系统IO模型

+ 阻塞IO(Blocking IO)

img

​ 在这个例子中,咱们会经过UDP而不是TCP来举例,由于对于UDP来讲,等待数据就绪这一步更加直观:要不就是收到了一个数据报,要不就是没收到一个数据报.可是对于TCP来讲,还有不少额外的变量.

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

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

  • 非阻塞IO(Nonblocking IO)

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

    img

    在上面的三个recvfrom操做中,因为数据并无就绪,因此内核返回了一个EWOULDBLOCK错误.在第四个recvfrom中,数据已经就绪了,而且已经被拷贝到咱们的应用程序的缓冲区了,内核返回一个OK,而后咱们的应用程序处理这些数据.

    咱们能够看到,在这种模型中,咱们须要使用轮询的方式来肯定数据究竟是否就绪.尽管这会浪费CPU时间,可是仍然是比较常见的模型,通常是在系统函数中用到.

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

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

    img

    从上图中,咱们能够看到,咱们会阻塞在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信号通知咱们.过程以下:

    img

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

  • 异步IO(Asynchronous IO)

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

    img

    img

美团面试题

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

相关文章
相关标签/搜索