c++笔试面试笔记

C++知识

c++11新特性

博客链接1
博客链接2

auto自动类型推导

auto实际上实在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。另外,似乎auto并不会影响编译速度,因为编译时本来也要右侧推导然后判断与左侧是否匹配
很有用的地方就是比如要声明的变量很复杂很长的时候,就可以直接用auto,这样代码简洁

decltype

decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型

#include <iostream>
using namespace std;
int main() {
	short a = 32670;
	short b = 32670;
	// 用decltype推演a+b的实际类型,作为定义c的类型 
	decltype(a + b) c;
	cout << typeid(c).name() << endl;	// int
	return 0;
}

nullptr 空指针

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,所以foo(NULL); 将会调用 foo(int), 这并不是程序员想要的行为,也违反了代码的直观性。0 的歧义在此处造成困扰。

C++11 引入了新的关键字来代表空指针常数:nullptr,将空指针和整数 0 的概念拆开。

nullptr 的类型为nullptr_t,能隐式转换为任何指针或是成员指针的类型,也能和它们进行相等或不等的比较。 而nullptr不能隐式转换为整数,也不能和整数做比较。

为了向下兼容,0 仍可代表空指

序列for循环

可以用于遍历数组,容器,string以及由begin和end函数定义的序列(即有Iterator的容器)

lambda表达式

于创建并定义匿名的函数对象,以简化编程工作

初始化列表

使用{}直接初始化vector、map这些

在引入C++11之前,只有数组能使用初始化列表,其他容器想要使用初始化列表,只能用以下方法:

int arr[3] = {1, 2, 3}
vector<int> v(arr, arr + 3);

或者for循环挨个赋值,比较麻烦,c++11之后就可以:

int arr[3]{1, 2, 3};
vector<int> iv{1, 2, 3};
map<int, string>{{1, "a"}, {2, "b"}};
string str{"Hello World"};

右值引用

类的新功能

  1. 新增了移动构造函数和移动赋值运算符重载
    在这里插入图片描述
  2. final与override关键字
  3. 禁止生成默认函数的关键字delete
  4. 强制生成默认函数的关键字default

可变参数模板

线程类

智能指针

c和c++区别

  1. c++是相当于是c的升级版,继承了c的一些之后,加入了一些新特性,还能兼容c
  2. c++最显著的区别就是引入了对象,c++是面向对象的,c是面向过程的
  3. 安全性方面,引用、智能指针、try-catch来改善安全性
  4. c++提高了可复用性,引入了模板概念,还开发了标准模板库stl
  5. c++对象的三大特性,封装、继承、多态
  6. c++的代码执行效率很高,仅仅比汇编慢10%-20%
  7. c++还在不断发展,一直有版本的更新
  8. c++支持函数重载,c不行

c不是c++的子集

c++面向对象

简述面向对象

  1. 面向对象是一种思想,就是把各种东西都看陈一个个对象,每个对象有他的属性和方法,比如把猫看成一个对象,猫有黑色白色,就是他属性,猫吃鱼这可以看成他的一个方法
  2. c是面向过程嘛,说白了就是就事论事,要做一个功能,从上往下写,数据、函数其实都是分开的,没有一个标准规则把他们联系起来
  3. c++面向对象了,就把每个业务都看成对象,通过标准的规则把他的数据、函数都打包起来,这样移植就很方便,要某个功能,把这个类以移植过去就好

对象的三大特性

封装
封装就是把数据和操作数据的方法都整合起来管理,外类外部提供访问的接口和权限,用类的人不需要管里面具体实现。权限有private、protectecd、public
继承

  1. 继承就是有的类可以看作其他类的父类,父类的功能它们都hi共有的,比如动物类有个属性是会动,那比如猫类、狗类,它们就可以自然继承这个会动这个属性,这样就可以避免重复的去写一些功能,提高代码的复用性。
  2. 继承有private继承、protect继承、public继承,父类的private成员都是不可被继承的。
    多态
    用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式,重写,重载。

虚函数

  1. 类中有一个vptr,在类空间起始位置,有虚函数的类才虚函数表指针
  2. vptr指向vtable,vtable属于类,实例化的对象都公用这个vtable,vtable中放的父类虚函数的地址
  3. 虚函数的实现过程:通过对象中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用
  4. 只要有虚函数,C++类都会存在这样的一张虚函数表,虚函数表存储在对象最开始的位置。虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。
  5. 父类会有一个vptr指向一个vtable,而子类重写虚函数之后,vtable中原来的就会被替换,这样父类中的vptr就可以指向子类中的具体实现函数。
class B
{
public:
    B() { printf("B()\n"); }
    virtual ~B() { printf("~B()\n"); }
private:
    int m_b;
};
 
class D : public B
{
public:
    D(){ printf("D()\n"); }
    ~D(){ printf("~D()\n"); }
private:
    int m_d;
};
 
int main()
{
    B* pB = new D();
    delete pB;
    return 0;
}

//执行结果
B()
D()
~D()
~B()

哪些函数可以做虚函数

  1. 只有类中的函数才能做虚函数
  2. 构造函数不能做虚函数:因为父类构造的顺序在子类之前,如果在子类中重写构造,那父类都还没有构造出来,就无法指向子类对象。
  3. 析构函数可以作为虚函数:使用父类指针指向子类对象时候,使用delete释放这个指针时候,他只会调用父类的析构,这就会导致我子类没有析构,出现内存泄漏,所以父类析构要写成虚函数。
    构造函数是用于创建对象的特殊函数,它在对象被创建时调用,用于对对象进行初始化操作。因为在调用构造函数时,对象还没有被构造出来,此时虚函数表也还没有生成,因此构造函数不能被定义为虚函数。

如果将构造函数定义为虚函数,就会存在一些问题,如:
虚函数表是在对象被构造时生成的,如果构造函数是虚函数,那么就需要在对象未构造完成时就生成虚函数表,这样会导致未定义的行为。

构造函数的任务是为对象进行初始化,如果构造函数是虚函数,子类必须先调用父类的构造函数,如果父类的构造函数是虚函数,那么子类无法在构造函数中正确地调用父类的构造函数,这样会导致对象的初始化出现问题。

因此,C++规定构造函数不能是虚函数。

  1. 静态函数不能是虚函数:类中的静态成员函数是该类所有对象所共有的,不受限于某个对象个体,因此不能作为虚函数,虚函数的目的就是为了重写之后,可以实现使用不同子类的功能,而静态函数,是共有的,所以是矛盾的

虚函数

参考博客链接

  1. 就是在利用类的多态性质的时候,可以设置一个基类,里面不写具体的功能实现,只作为一个通用接口,里面写一个虚函数,函数名前加上virtual,再用子类继承这个基类,在子类中重写这个虚函数,这样就可以让原来基类中的vfptr可以指向子类的vftable,使用的时候,通过基类指针或者引用来指向子类对象,这样就可以通过那个虚函数做的通用接口来实现不同功能,指向哪个子类就实现哪个子类的功能
  2. 如果基类有虚函数表,那么子类直接使用基类的虚函数表。且实例中虚函数表地址值存储顺序就是基类继承顺序。
  3. 虚函数地址按照其声明顺序放于虚函数表中。
  4. 父类的虚函数在子类的虚函数前面。
  5. 继承类新增的虚函数排在第一个虚函数表中,且在基类虚函数后面。
  6. 被重写的函数放到了虚函数表中原来父类虚函数的位置。即原来的虚函数的地址直接被覆盖。
  7. 子类重写父类的虚函数,只有按顺序第一个包含被重写的虚函数的地址的虚函数表中的原虚函数地址被覆盖,后面的虚函数表中如果也包含这个虚函数地址,那么就不是简简单单的覆盖,而是用更像是回调的方式调用重写后的函数。

哪些情况需要用初始化列表

  1. 类中有引用作为类成员的时候
  2. 类中有常量作为类成员的时候
  3. 子类继承了父类,并且父类中需要用有参构造
  4. 一个类B中有个类A作为成员,A需要有参构造

结构体和类的区别

  1. 主要区别就是默认的访问权限不一样,struct的默认权限是公有,class的默认权限是私有
  2. c++中结构体和类其实差距不大,除了上面那点,都可以通过struct做class也是ok的

运算符

  1. 算数运算符:+ - * / %
  2. 关系运算符:== != > < >= <=
  3. 逻辑运算符:&& || !
  4. 位运算符:& | ^ ~ << >>
  5. 赋值运算符:= += -= *= /= %= &= |= ^= ~= <<= >>=

数据类型

在这里插入图片描述

指针

数据访问的两种方式

直接访问:程序通过变量名来访问这块空间中的数据的访问方式。
间接访问:通过指针变量来访问它所存的变量的方式。

引用

int a=1;int &b=a;//自动转换为 int *const b=&a;
引用的本质是一个 指针常量(注意不是常量指针),指针常量的指向不可改而指向的值可以修改;这也解释了引用为什么能够修改原来变量的值,修改值的唯一方法即是修改该地址对应的值。
所有指针类型在32位操作系统下占4个字节内存,所以引用也占用4个字节内存。

函数对象(仿函数)

  1. 重载函数调用操作符的类,其对象称为函数对象。
  2. 函数对象使用重载的()时,行为类似函数调用,也叫仿函数。
  3. 函数对象(仿函数)是一个类,不是一个函数。
  4. 函数对象在使用的时候,可以像普通函数那样调用,可以有参数、有返回值。
  5. 函数对象超出普通函数的概念,函数对象可以有自己的状态。(可以有自己的一些成员属性记录它的状态)
  6. 函数对象可以作为参数传递
class FuncObjType{
public:
    FuncObjType(){num = 1;}
    int num;
    void operator()(){
        cout<<"Hello C++! "<< num ++ <<endl;
    }
};
int main(){
    FuncObjType f;
    f();//Hello C++! 1
    f();//Hello C++! 2
    f();//Hello C++! 3
    return 0;
}

auto

顶层const与底层const

顶层const是指 const修饰的是指针,即此指针是常量指针,一旦指向某个对象,则不能指向其他对象
底层const是指 const修饰的是指针所指向的对象为常量,不能通过指针去修改对象的值
在这里插入图片描述

auto规则

参考博客链接
规则1:声明为auto(不是auto&)的变量,忽视掉初始化表达式的顶层const。即对有const的普通类型(int 、double等)忽视const,对常量指针(顶层const)变为普通指针,对指向常量(底层const)的常量指针(顶层cosnt)变为指向常量的指针(底层const)。
规则2:声明为auto&的变量,保持初始化表达式的顶层const或volatile 属性。
规则3:若希望auto推导的是顶层const,加上const,即const auto。

在这里插入图片描述
auto的用途
在这里插入图片描述

四类cast转换

C++继承并扩展C语言的传统类型转换方式,提供了功能更加强大的转型机制(检查与风险)。
原来c语言中的强转有三个缺点:

  1. 没有从形式上体现转换功能和风险的不同,比如有的地方自动发生隐式转换
  2. 将多态基类指针强转成派生类指针时候不检查安全性,无法判断转换后的指针是否确实指向一个派生类对象
  3. 难以在程序中寻找到底什么地方进行了强制类型转换

const_cast

两个功能:去掉const属性和加上const属性

  1. 去除const属性
    在这里插入图片描述
    只能用在通类型的const和非const之间转换,比如只能把const char * 转成char *,不能说把const int * 转成char *
    在这里插入图片描述

  2. 加上const属性

	int* a = new int(1);
    *a = 2;
    cout<<a<<endl;//0x1d5fe70
	const int* b = const_cast<const int*>(a);
	*a = 3;
	// 	*b = 3;//常量不能修改
    cout<<a<<endl;//0x1d5fe70
    cout<<b<<endl;//0x1d5fe70

static_cast

在这里插入图片描述

  1. 下面两个效果等价,都实现了int 转 float,采用static在形式上能够明显的表明发生了强转
    在这里插入图片描述
  2. 适用于一些低风险的强转,用于可以发生隐式转换的条件下,对于有些原来用隐式转换不行的,static也不行:
    在这里插入图片描述
  3. 子类和父类之前强转,子类的通常比父类大,把父类指针强转子类指针的话,可能会它访问到一些不属于它的内容,虽然编译通过,所以是不安全的,而把子类指针强转父类指针的话,是安全的。
    在这里插入图片描述

dynamic_cast

在这里插入图片描述在这里插入图片描述

reinterpret_cast

在这里插入图片描述
reinterpret_cast是四种强制转换中功能最为强大的(最暴力,最底层,最不安全)。它的本质是编译器的指令。
就相当于c里面的显示强转,底层进行的是二进制的拷贝,很多能转,不管危险不危险,
但是,对于const的它还是不能转的
在这里插入图片描述

double a = 1.1;
char * c = reinterpret_cast<char*>(&a);
double* b = reinterpret_cast<double*>(c);
printf("%lf",*b);//1.1000000

c++模板template

封装继承多态

内存空间

在程序运行时,由于内存的管理方式是以为单位的,而且程序使用的地址都是虚拟地址,当程序要使用内存时,操作系统再把虚拟地址映射到真实的物理内存的地址上。所以在程序中,以虚拟地址来看,数据或代码是一块块地存在于内存中的,通常我们称其为一个段。而且代码和数据是分开存放的,即不储存于同于一个段中,而且各种数据也是分开存放在不同的段中的。

参考博客链接

参考博客连接

  1. 栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、const局部常量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
  3. 全局区(静态区static):存放全局变量、静态变量。程序结束后由系统释放,编译时候就确定大小。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
  4. 常量区(文字常量区):存放const全局常量,常量字符串,编译时候就确定大小,程序结束后有系统释放。
  5. 代码区:存放函数体(类成员函数和全局区)的二进制代码。
int global = 10;//全局变量
static int g_s = 10;//静态全局变量
const int g_c = 10;//全局常量

int main()
{
    cout<<"全局变量"<<&global<<endl;
    cout<<"静态全局变量"<<&g_s<<endl;
    cout<<"全局常量"<<&g_c<<endl;
    cout<<"----------------"<<endl;
    {
        cout<<"字符串常量"<<&"a"<<endl;
        int local = 10;//局部变量
        static int l_s = 10;//静态局部变量
        const int l_c = 10;//局部常量
        cout<<"局部变量"<<&local<<endl;
        cout<<"局部静态变量"<<&l_s<<endl;
        cout<<"局部常量"<<&l_c<<endl;
    }
    return 0;
}

全局变量0x602070
静态全局变量0x602074
全局常量0x400dc4
----------------
字符串常量0x400d95
局部变量0x7ffeae3b2bb4
局部静态变量0x602078
局部常量0x7ffeae3b2bb0

通过objdump -h命令可以查看一个.o文件(已编译成二进制文件但未链接)的各个段:
在这里插入图片描述

栈和堆

  1. 栈和堆的主要区别
    (1)管理方式不同:栈由编译器管理,堆由程序员管理
    (2)空间大小不同:栈空间较小,默认只有几M,Linux下默认只有8MB(通过ulimit -s查看),堆空间较大,可有好几个 G
    (3)效率不同:栈的内存分配运算内置于处理器的指令集,效率高,速度块,且不会产生内存碎片,堆开辟和释放是通过库函数实现,开辟过程中还需要去搜索合适的空闲区域,速度较低,会产生内存碎片
    (4)增长方向不同:栈的地址从高往低增长,堆的地址从低往高增长

开辟地址c和c++有什么区别

  1. c使用malloc,C++使用new,释放的时候,c使用free,c++使用delete
  2. malloc需要指定空间长度,new不需要指定长度(自动计算)
  3. new返回的就是与变量类型一致的指针,malloc返回的是void *,自己再强转
  4. new的安全性比malloc高
  5. new可以初始化(并且创建常量的时候必须初始化),malloc不可以初始化
  6. new如果出问题会放回异常,malloc是返回空
  7. new会调用构造,delete的时候调用析构,但是malloc和free就没有这个机制
  8. new是c++的关键字,malloc是c语言的库函数
  9. 堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。 基本上,所有的C++编译器默认使用堆来实现自由存储,序员也可以通过重载操作符,改用其他内存来实现自由存储

delete []a,这样来释放数组空间的时候,不需要指明数组长度,原因是因为new在开辟的时候就会记录下长度

//C语言:
int *a  =  (int *) malloc(sizeof(int)*10); //malloc开辟数组空间    
free(a);
a = NULL;//防止野指针
//C++:     
int *a = new int[10]; //new开辟一维数组空间    
delete [] a;
a = NULLL;//防止野指针

const int *a = new const int(10); //new创建常量的时候需要初始化
int* b = new int(1);//new可以初始化变量


//C++开辟一个10*10的二维数组
int **a = new int*[10];
for(int i = 0;i<10;i++)  a[i] = new int[10];
//释放二维数组时,先释放一维数组,再释放二维数组
for(int i=0;i<10;i++)  delete(a[i]);  //先释放一维数组
delete a;  //再释放二维数组

内存对齐

所谓的内存对齐,就是为了让内存存取更加有效率而采取的一种优化手段,对齐的结果是使得内存中数据的首地址是CPU单次获取数据大小的整数倍。
代码前添加关键字 #pragma pack(n) 即可,其中 n 是手动指定的内存对齐的字节数
① 若没有手动设置对齐值,则编译器默认使用成员变量中最大的类型的字节数作为对齐值;
② 若手动设置了对齐值,则编译器会在默认对齐值和手动设置的对齐值之间选择最小的那个作为最终对齐值。

char*和char[]

  1. char *
    字符串指针,指向一个字符串,字符串存在常量区,指针存在栈区,如果两个指针指向的字符串常量内容一样的话,那两个指针指向的都是同一个地方,比如
    char* str1 = “aaa”;
    char* str2 = “aaa”;
    str1和str2在栈上,“aaa”在常量区,str1和str2指向同一个地方
  2. char[]
    char str3[] = "aaa";
    char str4[] = "aaa";
    cout<<&str3<<endl;//0x7ffc82674b24
    cout<<&str4<<endl;//0x7ffc82674b20

str3和str4是字符数组,数组里面的aaa也是存在栈上的,str3代表‘a’

所以char* 和char[] 本质不是一样的

野指针

就是一个指针指向了一个位置不可知(随机、不正确的、没有明确限制的)的地址,或者指针再定义的时候未初始化

内存泄漏

就是new或者malloc一个内存空间之后,正常都是需要回收释放的,但是有可能还没到释放的时候,程序就return 或者break,导致没有执行到释放,这就导致了内存泄漏。

//1.执行流跳转导致没来得及释放
void test()
{
int *p = (int*)malloc(100);
//...
//...
return;   //在遇到return , break, goto,continue等语句的时候,就会引发执行流跳转
free(p);
}

智能指针

在这里插入图片描述
参考博客链接

shared_ptr(一种强引用指针)

  1. 多个shared_ptr可以指向同一处资源,当所有shared_ptr的count计数,该处资源才释放。它有某个对象的所有权(访问权,生命控制权) 即是 强引用,所以shared_ptr是一种强引用型指针
  2. 内部大概实现:每次复制,多一个共享同处资源的shared_ptr时,计数+1。每次释放shared_ptr时,计数-1。当shared计数为0时,则证明所有指向同一处资源的shared_ptr们全都释放了,则随即释放该资源
{  
    std::shared_ptr<Monster> monster1(new Monster());  //计数加到1
   do{
       std::shared_ptr<Monster> monster2 = monster1; 	//计数加到2  
    }while(0);          
  //该栈退出后,计数减为1,monster1指向的堆对象仍存在
  std::shared_ptr<Monster> monster3 = monster1;   	//计数加到2
}//该栈退出后,shared_ptr都释放了,计数减为0,它们指向的堆对象也能跟着释放.
  1. 最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。头文件和share_ptr相同,在memory中
//普通的内存分配
Buffer buf = new Buffer("auto free memory");
delete buf;//需要配合delete使用
//智能指针指向分配内存的对象
shared_ptr<buffer> buf = make_shared<Buffer>("auto free memory");
//在作用域外,自动释放内存​
  1. shared_ptr的指向数组,默认的shared_ptr删除器是不能释放数组的,需要自己指定删除器
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
std::shared_ptr<int> ptr3(new int[10]);//这种是合法的
//自己指定删除器
std::shared_ptr<int> p4(new int[10], [](int *p) { delete [] p;});//该指定删除器是以匿名函数lambda表达式呈现
  1. 他有个缺陷,就是可能出现循环引用,比如一个a指向b,b又指向a,这样在释放的时候,就谁也释放不掉

weak_ptr(一种弱引用指针)

可以用于解决shared_ptr的循环引用的问题

  1. weak_ptr是为了辅助shared_ptr的存在,它只提供了对管理对象的一个访问手段,同时也可以实时动态地知道指向的对象是否存活。可以从一个 shared_ptr 或者另一个 weak_ptr 对象构造而来
    (只有某个对象的访问权,而没有它的生命控制权 即是 弱引用,所以weak_ptr是一种弱引用型指针)
  2. 计数区域的释放却取决于shared计数和weak计数,当两者均为0时,才会释放计数区域
  3. 不具有普通指针的行为,没有重载 operator* 和 operator-> ,那么如何通过 weak_ptr 来间接访问资源呢?答案是在需要访问资源的时候 weak_ptr 为你生成一个shared_ptr,创建 shared_ptr 的方法就是 lock() 成员函数。

unique_ptr(一种强引用)

  1. 它持有对对象的独有权——两个unique_ptr 不能指向一个对象,即 unique_ptr 不共享它所管理的对象。
  2. 它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL)算法,可以通过移动构造交权力,在有的需要访问权限,但部控制它的时候,也可以通过get函数得到一个原始指针
std::unique_ptr<Monster> monster1(new Monster());//monster1 指向 一个怪物  
std::unique_ptr<Monster> monster2 = monster1;//Error!编译期出错,不允许复制指针指向同一个资源。 
std::unique_ptr<Monster> monster3 = std::move(monster1);//转移所有权给monster3.   

std::make_unique upw1(std::make_unique<Widget>());
//或者
std::unique_ptr<Widget> upw2(new Widget);
  1. unique_ptr指向数组
std::unique_ptr<int []> ptr1(new int[10]);
ptr1[9] = 9;
//unique_ptr需要确定删除器的类型,不能像shared_ptr那样直接指定删除器
std::unique_ptr<int, void(*)(int*)> ptr2(new int(1), [](int *p){delete p;}); //正确

总结

  1. 不要使用std::auto_ptr,这个已经被淘汰掉了

  2. 当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,请使用std::unique_ptr

  3. 当你需要一个共享资源所有权(访问权+生命控制权)的指针,请使用std::shared_ptr

  4. 当你需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr

  5. unique_ptr由于没有引用计数,性能较好于shared_ptr

  6. 推荐用法:一个shared_ptr和n个weak_ptr搭配使用 而不是n个shared_ptr。
    因为,逻辑上,大部分模型的生命在直观上总是受某一样东西直接控制而不是多样东西共同控制。
    程序上,能够完全避免生命周期互相控制引发的 循环引用问题。

智能指针线程安全

  1. shared_ptr的内部是封装了锁的,因为指向同一个对象的智能指针的引用计数指针是指向同一块资源的。实际上引用计数在shared_ptr底层中是以指针的形式实现的,所有的对象通过指针访问同一块空间,从而实现共享。所有智能指针在对引用计数加减的时候内部是有“加锁”和“释放锁”的操作。
    所以shared_ptr的引用计数时线程安全的(因为是原子操作)。

  2. 而shared_ptr管理对象时不一定是线程安全的。当不同的线程,操作同一个shared_ptr智能指针时,会发生线程不安全的情况。
    因为智能指针管理对象需要两步, 首先一个指针指向管理的对象, 然后一个指针控制引用计数。
    如果当指针指针的第一步完成了,第二步还没开始的时候CPU被占用,另一个线程也操作这个智能指针时就会出现线程安全问题。

对stl的了解, 常见容器的常用函数、底层实现、特点

  1. stl叫标准模板库,他开发出来的目的是为了提高代码的复用性,就是把一些通过的数据结构、算法按照标准封装好,大家一起用,不用每次用到这些功能都去重写。
  2. stl主要包括容器、算法、迭代器、仿函数、连接器、空间配置器。容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数。
  3. 其中关键的迭代器是容器和算法之间的重要桥梁(他就是提供的一种方法,用于能够依次访问容器中的元素,而无需暴露容器的内部表示方式)。
  4. 容器主要序列式容器(就是数据在容器中存储排列的顺序时和输入时候一样的)、,序列式容器比如vector、stack、list、queue、string。
  5. 关联式容器(通过key-value进行匹配,内部排列的顺序和初始存入时候可能是不一样的)关联式容器主要是set、map、unordered_set、unordered_map、multi_set、multi_map、multi_unordered_map、multi_unordered_set。

在这里插入图片描述

vector/list区别,优缺点

  1. vector是顺序存储的,连续数据在物理地址上也是连续的,list是链式存储的,连续的数据在物理地址上不一定连续,数据之间都是通过指针来联系的,list每个节点包含数据域,存当前节点的数据,还有指针域,指向他的后续节点的的指针
  2. vector他的好处是可以前后遍历都很方便,遍历速度也很快,缺点是插入、删除不方便,都需要移动元素
  3. list他的是链式的,插入、删除很方便,不需要移动其他元素,充分利用内存,缺点就是会更多空间,速度比vector慢
  4. list是双向循环链表
    在这里插入图片描述

一个结构体的大小 考察内存对齐

结构体是按照其中元素的最大尺寸来对齐的,比如结构体中有个double、有个char,那他的尺寸就是16字节,是最大的double的整数倍,比如有个double、两个int,他也是16字节,两个int共占8字节,这样实现对齐

深浅拷贝

  1. 浅拷贝就是把一个类的值复制给另一个类,不会新开辟空间,深拷贝就是不仅仅会复制类,还可以新开辟空间,比如一个类它的构造的时候,会开辟一个内存,它里面有个指针成员指向这个地址,当类创建了对象a和对象b,想把b拷贝给a的时候,使用浅拷贝就是是把b指针的地址值拷贝给了a中那个指针,这样他俩就指向通过一个地方,深拷贝不是,他不是拷贝这个地址值,而是新开一个地址,把这个地址内存中的数据拷过去到那里面去
  2. 在有开辟内存,在析构中释放内存的情况,需要用深拷贝,不然如果两个对象的里面的指针指向同一个地方,如果其中一个析构把空间释放了,还没有释放的那个可能就出问题

如何解决内存泄漏(我说了智能指针,RAII啥的)

单例模式(双重校验锁,问为什么要这么写,第一个判空可以去掉吗?为什么可以去掉?)

c语言和c++的const的区别

  1. c语言的const来修饰变量之后,相当于只是设定了不能通过变量名来修改值,通过直接操作地址还是可以修改值的
  2. c++中的const才是真正的常量,const修饰之后,会把变量的值放入符号表中,在用到的时候取得是符号表中的值,即使堆内存进行了操作,值也使用符号表中的值。
  3. 与C语言不同,C++中的const不是只读变量
    C++中的const是一个真正意义上的常量
    C++编译器可能会为const常量分配空间(当你去对他取地址的时候或者作为全局变量在其他文件中用到时候)
    C++完全兼容C语言中const常量的语法特性

宏和常量的不同

宏是文本替换,宏被预处理器处理,没有类型和作用域、以及类型的概念
常量被编译器处理,有类型和作用域、类型的概念

全局变量

  1. 全局变量存储在全局区,局部变量存在栈区,他在多个函数之间都可以访问使用
  2. 如在一个头文件里面声明了全局变量,那么包含这个头文件的文件都可以访问到
  3. 如果在一个源问价里面声明的,要在别的地方访问它,需要加上extern关键字

RAII

RAII,全称为Resource Acquisition Is Initialization,汉语是“资源获取即初始化”。简单说来就是,在资源获取的时候将其封装在某类的object中,利用"栈资源会在相应object的生命周期结束时自动销毁"来自动释放资源,即,构造函数创建时初始化获取资源,并将资源释放写在析构函数中。所以这个RAII其实就是和智能指针的实现是类似的。

左值、右值

  1. 什么是左值:左值可以在=号左边,能够取地址,有名字,非临时的

    在这里插入图片描述
  2. 什么是右值:只能放在=右边,不能够取地址,无名字,临时的
    (1)右值的分类
    ①将亡值(xvalue,eXpiring value):指生命期即将结束的值,一般是跟右值引用相关的表达式,这样表达式通常是将要被移动的对象,如返回类型为T&&的函数返回值(如std::move)、经类型转换为右值引用的对象(如static_cast< T&&>(obj))、xvalue类对象的成员访问表达式也是一个xvalue(如Test().memberdata,注意Test()是个临时对象)
    ②纯右值(prvalue, PureRvalue):按值返回的临时对象、运算表达式产生的临时变对象、原始字面量和lambda表达式等。
    在这里插入图片描述
  3. 左值和右值的区别
    ①左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
    ②右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

在这里插入图片描述

移动语义:在需要使用临时对象(通常是右值)来构造其他对象时候(比如函数返回值传递的时候),使用移动构造方式效率高,因为是右值,所以这个时候就需要用到右值引用了。主要通过 移动构造 和 移动拷贝构造 来实现

完美转发:就是在函数模板当中传参,可以把对象的左右值属性也原样的传递进去

左值引用、右值引用

  1. 右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化
  2. 左值引用是具名变量/对象的别名,右值引用是匿名变量/对象的别名。
  3. 左值和右值是独立于它的类型的,即左右值与类型没有直接关系,它们是表达式的属性。
  4. 具名的右值引用是左值,匿名的右值引用是右值。如Type&& t = 10中t是个具名变量(最简单的表达式),t 的类型是右值引用类型,但具有左值属性。而Type&& func()中的返回值(是个表达式)是右值引用类型,但具有右值属性(因为是个匿名对象)。
    在这里插入图片描述
#include <iostream>
#include <vector>
#include <typeinfo>
using namespace std;

void func(int& i){
    std::cout<< __PRETTY_FUNCTION__ <<" : " << typeid(i).name()<<std::endl;
}
void func(int&& i){
    std::cout << __PRETTY_FUNCTION__ << " : " << typeid(i).name() <<std::endl;
}
void func2(const int& i){
    std::cout << __PRETTY_FUNCTION__ << " : " << typeid(i).name() <<std::endl;
}
void l_func(int &i) {
      std::cout << __PRETTY_FUNCTION__ << " : " << typeid(i).name() <<std::endl;
}
void r_func(int&& i){
        std::cout << __PRETTY_FUNCTION__ << " : " << typeid(i).name() <<std::endl;
}
//__PRETTY_FUNCTION__是linux宏
template<class T>
void wrapper(T&& x){
    func(x);//没有完美转发,传入右值也会当作左值处理
    func(std::forward<T>(x));//forward完美转发,有条件转为右值引用
    func(std::move(x));//move无条件的转为右值引用
}
int main(){
    int a =10;
    const int c = 20;
    int &&m = 20;     //常量右值
    std::cout<< "##########左值与右值 Test##########"<<std::endl;
    func(a);            //非常量左值
    func(20);           //非常量右值

//     l_func(10);       //10不可以作为一个左值传递
//     r_func(a);        //a不可以作为一个右值传递
    
    std::cout<< "##########forward与move Test##########" <<std::endl;
    wrapper(a);         
    wrapper(20);        

    std::cout<< "##########常引用 Test##########" <<std::endl;
    //常引用万能类型,用于拷贝语义
    func2(a);           //非常量左值
    func2(c);           //常量左值
    const int& d = 20;  //常量右值
    func2(20);          //非常量右值
    func2(d);          //常量右值

    return 0;
}

移动构造

  1. 所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为己用”。
  2. 移动构造的用处:对于程序执行过程中产生的临时对象(通常是右值),往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
  3. 在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数
  4. 移动构造的实现简单来说就是,比如需要用b来构造a,以浅拷贝的方式把b中的指针赋值给a中指针,再把b中指针置空
  5. 如果使用左值初始化同类对象,但也想调用移动构造函数完成,有没有办法可以实现呢?
    可以使用 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。
  6. 这在stl的容器中就有应用
	// C++11中,移动构造函数也会默认提供,是第七个默认提供的函数。
	// 默认提供的移动构造函数实现方式跟之前的拷贝构造一致,
	// 只能提供浅拷贝,不做其他操作,所以如果需要移动构造函数,最好手写。
	// 移动构造函数,直接拿到指针的值,进行浅拷贝
	String(String && s) :
		m_str(s.m_str) 	{
		s.m_str = nullptr;
	}

在这里插入图片描述

完美转发

  1. 为什么需要完美转发?
    (1)在函数模板编程中,常有一种场景是使用模板参数去调用另一个函数(比如 f 调用 g ),这时候如果只提供值传递版本会显得效率太低。函数的参数一般会尽可能地设为引用类型,以避免对象拷贝带来的高昂开销
    (2)为了这个函数 f 既可以接受左值,又可以接受右值,这个时候适合使用万能引用,万能引用之后都变成了左值
    (3)如果函数 g 既提供了左值引用版本和右值引用版本,则最好的情况是函数 f 可以根据参数类型去调用相应版本的 g. 而完美转发正可以满足此要求.

  2. 万能引用与引用折叠

template<typename T>
void func(T&& value){}

int i = 1;
int &&k = i;
func(i);//实参为左值,故此时T将被推断为int&,value则为int&,如果我们把int &带入函数原型中,得到的将是func(int& &&value),在编译器内部将会对其进行”引用折叠“,最终结果为int&
func(1);//实参为右值,故此时T将被推断为int,value则为int&&,也就是一个右值引用
func(k);//虽然k是一个右值引用,但是k本身是一个左值(变量都是左值),所以此时T将被推断为int&,value也为int&

func中的形参其实是一个万能引用,当实参为左值时,T和value的类型均为左值引用;当实参为右值时,T为实参类型,value为右值引用

右值引用和右值引用叠加将得到右值引用;
右值引用和左值引用叠加将得到左值引用;
左值引用和左值引用叠加将得到左值引用.

forward

我们知道,实参的左值属性和右值属性实际上是保存在类型参数T中的。
forward的作用就是还原一个类型参数的左值,右值属性,我们传入一个左值,它将返回左值,传入一个右值,则返回一个右值。
当形参t的实参为左值时,forward还原实参原有属性,那么forward(t)的结果仍然为左值
当形参t的实参为右值时,forward还原实参原有属性,那么forward(t)的结果就是一个右值

i++和++i的区别

++i本质相当于

i = i + 1;
return i;

i++本质相当于

int tmp = i;
i = i + 1;
return tmp;

宏定义和宏函数

函数后面加入const

  1. 前面使用const 表示返回值为const

  2. 后面加 const表示函数不可以修改class的成员,不让这个成员函数修改其他成员的值

类的成员函数中,有一些是不改变类的成员变量的,也就是说,这些函数是"只读"函数。如果把不改变数据成员的函数都加上const关键字进行标识,显然,可提高程序的可读性。其实,它还能提高程序的可靠性,已定义成const的成员函数,一旦企图修改数据成员的值,则编译器按错误处理

内存管理

lambda表达式

lambda表达式类似于一个函数,可以写在一个函数内部,可以传入参数,可以通过参数捕获的方式获得函数内部的局部变量
在这里插入图片描述
lambda表达式式直接写在函数内部,作为函数可调用的对象传进去。
在这里插入图片描述
还有参数捕获的功能
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

    int m = 10;
    static int n = 10;
    int p = 10;
    
    auto a = [](int a, int b){return a + b;};
    auto b = [&]{return (++m) + n;};
    auto c = [&m, p]{return (++m) + p;};
    
    cout<<a(1, 2)<<endl;//3
    cout<<b()<<endl;// 21
    cout<<c()<<endl;//22
    cout<<m<<endl;//11

仿函数与lambda表达式

在这里插入图片描述

assert

assert是一个用于放置在debug版本的代码中,通过assert判断或者捕获一些非法情况,然后将报错信息打印到标准输出上,并终止程序
它只在debug版本中有效,release版本中失效。
这个功能可以启用也可以禁用,可以通过在文件头使用 #define NDEBUG。

namespace

  1. 避免,在大规模程序的设计中,以及在使用各种各样的 C++ 库时,这些标识符的命名发生冲突,标准 C++ 引入关键字 namespace(命名空间/名字空间/名称空间),可以更好地控制标识符的作用域。
  2. namespace 中可定义常量、变量、函数、结构体、枚举、类等
  3. namespace 只能在全局定义。
  4. namespace 支持嵌套定义。
  5. namespace 是开放的,可随时添加新的成员。
  6. namespace 关键字可以为已有空间名字增加别名
  7. 无名命名空间意味着命名空间中的符号只能在本文件中访问,相当于给符号增加了 static 修饰。推荐了解
//使用命名空间加 :: 在当前作用域下直接使用
{int a;
 std::cin>>a;
 std::cout<<a<<std::endl;
}
//使用using 指定命名空间下某些符号可以在当前作用域下可用
{int b;
 using std::cin;
 using std::cout;
 using std::endl;
 cin>>b;
 cout<<b<<endl;
}
//使用using namepace std,在当前作用域下可以使用std下面的 所有符号
{int b;
 using namespace std;
 cin>>b;
 cout<<b<<endl;
}

操作系统

多线程避免死锁一般不是通过架构设计嘛,怎么还用信号量

  1. 信号量帮助实现线程之间同步,只有线程之间同步做好了,有助于避免死锁
  2. 锁要记得成对使用,又上锁就要记得解锁
  3. 资源之间的逻辑尽量不要嵌套,嵌套复杂了的话也会让锁复杂,可能出现死锁
  4. 要注意一些异常,防止因为异常程序退出没有解锁

什么是死锁?产生死锁的原因是什么?

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。死锁产生的原因可归结为两点:
1.竞争资源;
2.进程间推进顺序非法

死锁的条件?

  1. 互斥,一个资源只能由一个进程占用。
  2. 请求和保持,进程请求资源被阻塞时,进程已经占用的资源不释放。
  3. 不被剥夺,进程已获取的资源不会被其他进程剥夺。
  4. 循环等待,多个进程形成首尾相接的循环等待状态。

多线程加锁的原则

  1. 多线程、进程共享资源时,一定要加锁:比如全局变量、静态变量、共享内存、文件等
  2. 锁的职责要尽量单一,锁的范围尽量小:每个锁只保护唯一一个资源,,一个锁同时保护很多类或者函数,容易让 逻辑复杂,还会导致线程执行效率降低,
  3. 须保证不同地方加锁顺序是一样的,不能出现循环等待解锁
  4. 可重入函数尽量只是用局部变量和函数参数,少用全局变量、静态变量
  5. 锁中尽量避免使用跳转语句:因为有的语句跳转了就有可能漏掉解锁

进程之间用到消息队列了吗

Linux系统中关于进程与线程

  1. 进程是处于执行期的程序以及相关资源的总称**——(对)**
  2. 内核调度的对象是线程,而不是进程**——(对)**
  3. 线程被视为一个与其他进程共享某些资源的进程**——(对)**
  4. 当进程处于TASK_UNINTERRUPTIBLE状态时可以被杀死**——(错)**
    TASK_UNINTERRUPTIBLE状态是一种不可中断的睡眠状态,不可以被信号打断,必须等到等待的条件满足时才被唤醒

关于进程内存空间描述

  1. 程序结束时,在堆上分配的没有释放的内存会造成泄漏**——(错)**
    程序结束时,内存都会被回收的,内存泄漏是指运行过程中开辟的内存本应该释放,但没有释放,当内存泄漏积累很多的时候,比如服务器运行很久,这样的情况下就很由危害

Linux内核

  1. Android、Ubuntu、RedHat都是linux内核,但是MacOS是UNIX内核

关于文件系统

  1. 删除一个文件,同时会删除与此文件对应的文件控制块**——(对)**
  2. 符号连接所连接的文件被删除后,符号连接也会消失**——(错)**
    符号连接其实就是软链接

操作系统分段机制

  1. 代码段存放可执行文件的操作指令,代码段是只读的,不可进行写操作**——(对)**
  2. 数据段存放程序中的静态变量和已初始化且不为零的全局变量**——(错)**
    没有初始化不为零这一说
  3. BSS段( Block Started By Symbol):存放未初始化的全局变量,在变量使用前由运行时初始化为零**——(对)**
  4. 分段是为了提高内存利用率,减少内存碎片**——(对)**

虚拟内存

  1. 虚拟内存是计算机系统内存管理的一种技术,使得应用程序认为它拥有连续可用的内存**——(对)**
  2. 实现进程地址空间隔离**——(对)**
  3. 虚拟内存和物理内存的映射通过页表(page table)来实现**——(对)**
  4. 虚拟内存使得多个应用程序之间切换会花费更少的时间**——(错)**
  5. 虚拟内存的概念,是为了满足物理内存不足的情况下,利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(swap space)——(对)
  6. 一个程序在运行的时候Linux会将其一部分暂时不用的数据放到交换区,以保证有充足的物理内存。如果在程序运行时,发现需要用的数据在磁盘,会先将它们读到内存。而当运行时发现内存不足了,又会将一部分数据写到磁盘(交换空间)——(对)
  7. 虚拟内存和物理内存的映射通过MMU来管理**——(对)**

什么情况下需要进行对现场的保存和恢复

  1. 保留现场: 通过push指令将寄存器中的值都压入到栈中
  2. 恢复现场: 通过pop指令将栈中的值赋值给寄存器中
  3. 比如递归的时候,当前层的一些计算结果,在进入下一层递归之前,需要存入栈中,这是现场的保护,等到下层递归返回的时候,又再把栈中数据取出来,这是现场恢复。
  4. 比如当前进程在执行过程中,突然某个信号来了,就需要切换到内核态去处理信号的动作,这个时候就需要把目前的现场保存起来,等信号动作结束返回用户程序时候,再接着往下执行,这时候需要现场恢复。
  5. 进程切换的时候,当前进程在执行过程中,时间片到了,就需要把目前的数据保存起来,等到下次轮到它的时候,在恢复继续执行

什么是线程池

  1. 因为在面向那种需要很多线程的高并发任务,但是呢每个线程任务时间又很短,以至于我创建和销毁的时间都可能大于任务执行时间,这样的情况下,要是采用频繁的切换线程的话,cpu的效率就很低,所以这个时候采用线程池,或者在需要反映迅速的任务的时候,或者突然某时候来了大量任务需求,那时候cpu又一下分不出那么多线程的时候。
  2. 线程池就是一系列线程组成的集合,我提前创建好一系列线程,安排一个任务队列,从任务队列中取任务出来交给线程去执行,这样cpu就不用频繁的创建、切换、销毁线程了。可以合理的控制线程数量,不至于让cpu浪费
  3. 线程池主要由任务队列和一系列线程组成

线程池是如何抢占资源的、会不会有线程抢不到资源

  1. 就是任务队列里面来了一个任务的时候,不能说多个线程一起上去执行这个任务,防止出现争抢,所以在一个线程去取任务的时候,要禁止其他线程也取这个任务。
  2. 为了维护线程之间的协调,需要一把互斥锁和一个条件变量:
    互斥锁用来保护任务队列的数据安全,即维护多线程从任务队列中pop任务时的互斥关系。
    条件变量用来维护多线程之间的同步关系,当任务队列为空时要求线程释放互斥锁并在条件变量下等待,这时任务队列中每插入一个任务就唤醒一个线程。
  3. 一个线程的他的主要任务就是,上锁->任务队列为空,等待条件变量->取任务->解锁->执行任务
  4. 任务生产者生产出任务之后,就会发送条件变量,线程检测到条件变量,就去取任务,如果生产者没有生产任务,它们就被条件变量阻塞着在等待
  5. 会,它们是等条件变量的,多个线程等一个任务的时候,就可能抢不到资源

线程和进程的区别

  1. 进程是操作系统的基本分配单元,每创建一个进程,它会获得一定的内存空间,cpu时间片,多个进程之间他们的资源是相对独立,进程之间通信相比线程代价大一些,需要使用进程间通信方式,创建进程速度比较慢
  2. 线程是操作系统调度和执行的基本单位,同一个进程下面的多个线程,它们之间的资源是共享的,可以很方面的就通过内存来共享,创建线程的速度也比创建进程的速度快的多,线程由进程创建

线程和进程切换的具体过程

进程的切换

涉及从用户态、内核态、用户态的过程,
切换过程中需要操作的信息(现场的保存与恢复)包括:进程ID,状态、优先级、权限、内存、栈、文件描述符、寄存器、IO状态
在这里插入图片描述

线程的切换

线程切换涉及用户态、内核态、用户态的过程
切换过程中需要操作的数据(现场的保存与恢复)比进程少一些,只需要:状态、优先级、栈、寄存器, 所以线程切换更快
在这里插入图片描述

  1. 当个cpu核同一个时刻只能在执行一个任务,多个进程和线程之间,都是采用时间片轮询的方式来切换的,当时间片很小的时候,宏观上看就是并行的
  2. 不同进程之间切换,主要通过一个进程控制块PCB来控制,是通过cpu的时间片轮循切换的,当前进程的时间片结束,就切换下一个进程
  3. 线程的切换也是类似,一个线程的时间片结束,就切换下一个,同一个进程下的线程切换的话 , 如果是不同进程间的线程切换,还会涉及进程切换

协程

大量的进程 / 线程出现了新的问题
(1)系统线程会占用非常多的内存空间
(2)过多的线程切换会占用大量的系统时间。
而协程刚好可以解决上述2个问题。

  1. 协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上
  2. 并且,协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
  3. 协程上下文切换只涉及CPU上下文切换,而所谓的CPU上下文切换是指少量寄存器(PC / SP / DX)的值修改,协程切换非常简单,就是把当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的 CPU 寄存器上就 ok 了。
    在这里插入图片描述

因此我们可以将线程分为 “内核态 “线程和” 用户态 “线程。
一个 “用户态线程” 必须要绑定一个 “内核态线程”,但是 CPU 并不知道有 “用户态线程” 的存在,它只知道它运行的是一个 “内核态线程”。

C++11的线程

  1. 线程的创建
	std::thread thread1(test,A());
    std::thread thread2([](int tmp){ printf("lambda\n"); }, 10);//使用lambda表达式作为线程执行函数
    std::thread thread3(std::bind(&test,A()));//使用std::function作为线程执行函数
  1. 线程的执行方式,join或者detach
  2. 向线程函数传递参数,需要注意的是线程默认是以拷贝的方式传递参数的,当期望传入一个引用时,要使用std::ref进行转换
  3. 线程是movable的,可以在函数内部或者外部进行传递
  4. 线程在运行过程中,如果需要停顿,可以用this_thread::sleep_for实现。
  5. 每个线程都一个标识,可以调用get_id获取。
  6. 线程同步——mutex
int num = 0;
std::mutex mutex;
void test(){
   for(int i = 0; i < 1000 ;i++){
       mutex.lock();
       num++;
       mutex.unlock();
   }
}
  1. 线程同步——条件变量
    条件变量是c++11提供的另外一种同步语义,它能阻塞一个或者多个线程,直到收到另外一个线程的发出的通知或者超时,才会唤醒当前阻塞的线程,条件变量需要和互斥量配合起来使用,c++11提供了两种条件变量:
    condition_variable : 只能配合std::mutex进行wait操作
    condition_variable_any : 可以和任意带有lock,unlock的锁进行配合使用,但是效率比condition_variable差一点
    条件变量使用过程如下:
    1.锁住mutex
    2.循环检查等待条件,若条件满足进入阻塞状态,释放mutex.不满足等待条件则继续往后执行
    3.收到某个线程的notify_one或者notify_all,检查等待条件是否成立,若不成立则尝试锁住mutex,锁住mutex后就往后执行,若等待条件成立则继续阻塞
  2. 异步操作
    C++11提供了std::future, std::promise, std::package_task可以很方便的进行异步操作,std::future用来获取异步操作的返回值,std::promise用来包装一个值,将数据和future绑定起来,方便线程赋值,std::package_task用来包装一个可调用对象,将函数和future绑定,以便异步调用
    由于std::thread会忽略线程执行函数的返回值,我们没有办法获取到异步执行的结果,这时std::future和std::promise就派上用场了,我们可以讲std::promise作为函数参数,函数执行完后将结果存到std::promise中.其他线程就可以通过std::futrue获取到

core dump报错

参考博客链接

  1. core dump就是程序异常退出的时候,操作系统将当时的内存信息保留下来,里面存了一些内存信息、寄存器、指针、内存管理信息。
  2. 以下列出几种信号的默认操作就会产生 core dump
    在这里插入图片描述
  3. 通过 ulimit -c unlimited 可以设置当前终端下,core文件大小无限制,也可以设置具体的大小

core文件怎么查

kill -9信号之后,会有core文件吗

不会

https://blog.csdn.net/u010783226/article/details/107028499

linux常用命令

原子操作

线程安全

进程间调度算法

深入的说一说线程和进程

算法和数据结构

排序

在这里插入图片描述

红黑树、B、B+树

数据结构也需要一起复习下

map和unordered_map的区别

  1. map底层是红黑树实现,所以是有序的,unordered_map底层是哈希表,是无序的
  2. 查找效率不同,map是logn,性能较稳定,unordered_map取决于哈希函数的冲突,大部分情况下接近于1,性能相对不稳定。
  3. 内存使用效率,map接近100%,利用率高,unordered_map有的空间是没用到的,不是100%
  4. map适用于:要求元素有序,对时间稳定性敏感的场景,unordered_map适用于要求查找速度块的场景,不需要有序的

二叉树的前后中序推导

计算机网络

poll和epoll的区别

  1. 两个都是IO复用的模式,基本原理都是,设置一个监听的角色负责监视着io缓冲区,有数据来了,通知用户,区别在于poll它只能告诉用户,我有多少个io由数据,并不能指明是哪些io缓冲区来了数据,还得靠用于取遍历找,epoll的话,它不但会通知来了数据,还会指出是哪里来了数据。
  2. 像那个快递的例子

请你回答一下epoll怎么实现的

  1. Linux epoll机制是通过红黑树和双向链表实现的。
  2. 首先通过epoll_create()系统调用在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。
  3. 然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示败。
  4. 最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。

其他项

学校专业课

  1. 研究生课程:机器人控制理论与技术、Intelligent Control & Application、嵌入式控制系统综合实验、优化理论、现代测量技术与误差分析
  2. 本科课程:微机原理、嵌入式、信息论、智能电网信息工程、电力系统、电力电子

debug 最久的经历?怎么解决的问题?

编程题

删除链表元素

反转列表

两个数组合并,再排序

字符串中按照字符出现频率进行排序

#include <string>
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;

bool camp(pair<char,int> a, pair<char,int> b){
    return (a.second > b.second)?true:false;
}

int main() {
    unordered_map<char, int> result;
    char input_char;
    while(cin>>input_char){
        if(result.find(input_char) == result.end()) result[input_char] = 1;
        else result[input_char] ++;
    }
    vector<pair<char, int>> res_vec(result.begin(), result.end());
    sort(res_vec.begin(), res_vec.end(), camp);
    for(auto iter = res_vec.begin(); iter != res_vec.end(); iter ++){
        for(int j = 0; j < (*iter).second; j ++){
            cout<<(*iter).first;
        }
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值