前言

面试总结第八波,面试了腾讯、百度、阿里、虎牙直播等几个公司,然后总结了这一波面经,主要针对前面总结的那些,在面试时,有些被问到了,所以进行的一个查漏补缺总结。

1、C++

unique_ptr的用途

特性总结:1)默认情况下,占用内存大小和raw指针一样(除非指定了用户自定义deleter);2)运行过程中unique_ptr消耗资源和raw指针一样;3)unique指针只可以进行转移操作,不能拷贝、赋值。所以unique指针作为函数入参数类型的时候,函数的调用方必须使用转移语义;4)允许在定义unique指针的时候,指定用户自定义的指针销毁函数(在指针析构的时候会回溯);5)从一个unique指针转换成shared指针很容易

使用场景:1)用作工厂函数的返回类型

unique_ptr无拷贝构造函数,仅能通过move进行转移;只有在计数归零的时候,shared_ptr才会真正释放所占用的堆内存空间;weak_ptr可以指向shared_ptr指针的对象内存,却并不拥有该内存,weak_ptr成员函数lock(),则可以返回指向内存的一个shared_ptr对象,且在所指对象内存已经无效的时候,返回指针空值(nullptr)。

智能指针和普通指针混合使用

当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。

如果传入形参在前面没定义,那函数结束后就会销毁不能再用,如果先定义了这个数据再传入函数,函数结束了也不影响这个数据。

1)普通指针转化成智能指针:shared_ptr<int> p(iPtr);

2)智能指针转化成普通指针:int *iPtr=p.get();

注意:

1)普通指针转智能指针

void f(shared_ptr<int> ptr){ //值传递,增加引用计数//do something
}//销毁ptr,减少引用计数

一直使用智能指针可解决问题。

2)智能指针转普通指针

auto p=make_shared<int>(42);
int *iPtr=p.get();
{shared_ptr<int>(iPtr);
}
int value=*p; //Error! 内存释放

p与iPtr指向了相同的内存,然而通过get方法后,将内存管理权转移给普通指针。iPtr传递给里面程序块的临时智能指针后,引用计数为1,随后出了作用域,减少为0,释放内存。

使用shared_ptr<int>(智能指针)时需要格外注意,因为这个不会增加智能指针的计数器,但是在离开作用域后会减少计数器。

vector

1)当容量不足时,即finish==end_of_storage,调用vector的成员函数insert_aux(end(), x)函数进行扩容,复制。

2)vector的效率低原因:当vector预留空间不足时,要重新分配内存,并且拷贝当前已有的所有元素到新的内存区域。

3)解决效率低方法:可以预先估计元素个数,用reserve函数进行预留空间的分配。

map/set区别

都是关联式容器

set:以红黑树作为底层容器;所得元素只有key没有value,value就是key;不允许出现键值重复;所有的元素都会被自动排序;不能通过迭代器来改变set的值,因为set的值就是键。

map:以红黑树作为底层容器;所有元素都是以键+值存在;不允许键重复;所有元素是通过键自动排序的;map的键是不能修改的,但是其键对应的值是可以修改的。

inline

1)使用了inline关键字的函数只是用户希望它成为内联函数,但编译器有权忽略这个请求;

2)关键字inline必须与函数定义体放在一起才能使函数称为内联,仅将inline放在函数声明前面不起任何作用;

3)inline函数可以定义在源文件中,但多个源文件中同名inline函数的实现必须相同,一般把inline函数定义放在头文件中更加合适;

4)类中的成员函数,默认都是inline的。

内联函数就是将很简单的函数“内嵌”到调用他的程序代码中,为了避免原本函数调用时的时空开销(保护现场,恢复现场)。

常量表达式:允许一些计算发生在编译时,即发生在代码编译而不是运行的时候,优化:假如有些事情可以在编译时做,它将只做一次,而不是每次程序运行时都计算。

constexpr函数限制:1)函数中只有一个return语句;2)函数必须返回值;3)在使用前必须已有定义;4)return返回语句表达式不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式。

常量表达式的构造函数:1)函数体必须为空;2)初始化列表只能由常量表达式来赋值

多线程

原子类型和原子操作

原子操作:多个线程访问同一个资源时,有且仅有一个线程对资源进行操作。可以通过加锁保证。

C++新增了原子类型:atomic_llong、atomic_int等,它们都是用atomic<T>模板定义的,例如std::atomic_llong就是用std::atomic<long long>来定义的。C++11中将原子操作定义为atmoic模板类的成员函数,包括大多数类型的操作,比如读写、交换等。对于内置类型,主要通过重载全局操作符来实现。 operator+=()函数会产生一条特殊的以lock为前缀的x86_64指令,用于控制总线及实现x86_64平台上的原子性加法。

atomic_flag一种简单的原子布尔类型,只支持两种操作:test_and_set和clear

使用ATOMIC_FLAG_INIT宏初始化,可以保证该对象创建处于clear状态。

CPU指令是多线程不可再分的最小单位,如果我们有办法将代码语句和指令对应起来,就不需要引入互斥锁从而提高性能?而这个对应关系就是所谓的原子操作,C++11中的atomic中有两种做法:1)模拟,对于一个atomic<T>类型,可以给他附带一个mutex,操作时lock/unlock一下,这种在多线程下进行访问,必然会导致线程阻塞;2)有相应的CPU层级的对应,这就是一个标准的lock-free类型。

例如:在执行自增操作的时候,在xaddl指令前多了一个lock前缀,而CPU对这个lock指令的支持就是所谓的底层硬件支持,增加了这个前缀后,保证了load-add-store步骤的不可分割性。

lock指令的实现

CPU在执行任务的时候并不是直接从内存中加载数据,而是会先把数据加载到L1和L2的cache中(典型的是两层缓存,甚至更多),然后再从cache中读取数据进行运算。

现在计算机通常都是多核处理器,每个内核都对应一个独立的L1层缓存,多核之间的缓存数据同步是CPU框架设计的重要部分,MESI是比较常用的多核缓存同步方案。当我们在单线程内执行atomic++操作,自然不会发生多核之间数据不同步的问题,但是我们在多线程多核的情况下,cpu是如何保证lock特性呢?

以intel x86架构的cpu为例:lock前缀实现原子性的两种方式:1)锁bus:性能消耗大,在intel 486处理器上用此种方式实现;2)锁cache:在现代处理器上使用此种方式,但是无法锁定cache的时候(如果锁驻留在不可缓存的内存种,或者锁超出了划分cache line的cache body),任然会去锁定总线。

extern

1)声明外部变量

各个文件定义的全局变量是互相透明的,在链接时,要将各个文件的内容合为一体,因此某些文件中定义的全局变量名相同的话,在这个时候就会出现错误,也就是会出现重定义的错误。extern的原理很简单,就是告诉编译器,现在编译的文件中,这个标识符虽然没有在本文中定义,但是在别的文件中定义的全局变量。

2)在C++文件中调用C定义的变量

因为C++中新增了诸如重载新特性,所以全局变量和函数名编译后的命名方式有很大区别,使用extern "C"{ int iRI;}告诉编译器,iRI是使用C方式编译的。

3)C++调用C定义的function

static:1)函数内;2)模块内;3)类中变量;4)类中函数

const:1)阻止一个变量被改变;2)声明常量指针和指针常量;3)修饰形参,表明该输入参数在函数内部不能改变其值;4)修饰类的成员函数,是指不能修改类的成员变量;5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

STL的map和unordered_map

1)构造函数: unordered_map需要hash函数,比如取模函数;map只需要比较函数

2)存储结构:unordered_map采用hash表存储;map一般采用红黑树实现

3)查找时间复杂度:unordered_map是O(1);map是log(n)

4)插入时间复杂度:unordered_map取决于哈希函数,平均大概O(c);map是log(n)

5)是否有序:unordered_map无序;map按照key有序

使用场景:

unordered_map查找速度比map块,而且查找速度基本和数据量大小,属于常数级别,但内存消耗大。

vector中的emplace_back为何优于push_back

如果一个对象有显示的构造函数,则使用emplace_back会省去调用本类的构造函数;emplace_back("yyqx"),push_back(CText("yyqx"));省去了调用CText进行构造。

添加一个元素到结束容器,该元件是构成在就地,即没有复制或移动操作进行。push_back会先构造一个临时对象,再使用拷贝构造函数进行拷贝。

emplace_back(vec.back()):会出现问题,因为emplace_back可能会造成迭代器失效。

string

string s1="asdf";   //使用带参的构造函数
string s2=s1; //使用拷贝构造函数   "asdf"在内存中有一份
string s3=string("sdfgh");  //使用带参的构造函数
string s4;  //执行了无参的构造函数
s4=s1;    //执行了=赋值操作

引用和指针的区别

本质:引用是别名,指针是地址

1)从现象上看,指针在运行时可改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。即指针可以被重新赋值以指向另一个不同的对象,但是引用总指向在初始化时被指定的对象,以后不能改变,但是指定对象内容可以改变;

2)从内存上分配看,程序为指针变量分配内存空间,而不用为引用分配内存区域,引用声明时必须初始化,从而指向一个已经存在的对象,引用不能指向空值;

3)sizeof(引用类型)的结果是被引用对象的大小,而不是引用本身的大小。

左值引用:从一个变量取得地址,然后赋值给引用变量

右值引用:会对变量进行一个拷贝在临时变量中,这个临时变量没有名字而已,它的生命周期和函数栈帧是一致的,也可以说是临时变量和它的引用具有相同的生命周期。

const int &i=10,内部和右值引用没有什么区别。

能将右值引用赋值给左值引用。

把局部变量改变为静态变量后改变了它的存储方式即改变了它的生存期,static局部变量只被初始化一次;把全局变量改为静态变量后是改变了它的作用域,限制了它使用的范围。

hello.c——预编译器(hello.i)——编译器(hello.s)——汇编器(hello.o)——链接器(可执行文件)

符号表

存在于系统中,如常数表、变量名表、数组名表、过程名表、标号表等,统称为符号表。

符号表属性:符号名、符号类型、符号存储类别、符号的作用域及可视性、符号变量的存储分配信息

拷贝构造函数为什么使用引用类型?

因为如果不使用引用传参,那么在传参时,会进行调用拷贝构造函数,那么又会触发拷贝构造函数,就这下永远的递归下去。所以拷贝构造函数使用引用类型不是为了减少一次内存拷贝,而是避免拷贝构造函数无限递归下去。导致栈溢出。

赋值运算符重载函数需要避免自赋值

因为可能导致内存泄漏,使用悬挂指针。仅仅内容相同的赋值不是自赋值。

对于移动构造函数,抛出异常比较危险,因为可能移动语义还没有完成,就抛出了异常,从而导致一些指针称为悬挂指针。使用noexcept关键字即可,这样当抛出异常时,程序就会被std::terminate()终止。

强类型枚举

enum class M_Type::char{value1,value2};

1)枚举常量不会暴露在外层作用域中;2)枚举值不会被隐式转换成整数,无法和整数值比较;3)枚举类型所使用的类型默认为int类型,也可以指定其他类型

虚函数表和vptr指针

虚表指针的初始化,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初始化父类的虚表指针,该虚表指针指向父类的虚表,当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。

C++编译器在编译的时候,发现这个函数是虚函数,这个时候C++就会采用晚绑定技术,也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型来确定调用的哪一个函数。

虚表是和类对应的,虚表指针是和对象对应的。

虚函数表是由编译器自动生成与维护的,存储类成员函数指针的数据结构。虚函数表属于类,类的所有对象共享这个类的虚函数表。虚函数表由编译器在编译时生成,保存在可执行文件的.rdata只读数据段。

每个对象都有一个指向虚函数表的指针,vptr指针,C++编译器不需要区分子类或者父类对象,只需要在base指针中,找到vptr指针即可。vptr一般作为类对象的第一个成员。

多重继承,会有多个虚函数表。

override关键字:确保该函数为虚函数并覆盖来自基类的虚函数,如果基类无此函数,或基类中函数并不是虚函数,编译器会给出相关错误信息。

静态联编:在编译的时候就确定了函数的地址,然后call就调用了;

动态联编:首先需要取到对象的首地址,然后解引用取到虚函数表的首地址,再加上偏移量才能找到要调的虚函数,然后call调用。

子类和父类的析构函数名字不一样,但作为虚函数,也可以构成重写,因为编译器对析构函数的名字做了特殊处理,在内部函数名是一样的。

RTTI机制

RTTI:提供了运行时确定对象类型的方法。typeid函数返回的是一个结构体或者类,然后,再调用这个返回结构体或类的name成员函数。

dynamic_cast主要用于在多态的时候,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针(引用)转换为派生类指针(引用)。

构造函数不能是虚函数:虚函数对应一个虚函数表,虚函数的调用是通过虚函数指针指向虚函数表进行调用,该指针存放在对象的内存空间中,但对象还没实例化,还没有内存空间,即没有虚函数指针。

类的非静态成员函数调用时,编译器会传入一个“隐藏”的参数,这个参数就是通常我们说的“this”指针,它的值就是对象的地址。后来把虚表的地址存在对象的起始地址,即对象的第一个数据成员,也就是它的虚表指针。

构造函数调用过程细分:1)进入到构造函数体之前,在这个阶段如果存在虚函数的话,虚表指针被初始化,如果存在构造函数的初始化列表的话,初始化列表也会被执行;2)进入到构造函数体内,这一阶段是我们通常意义上说的构造函数。

结构体声明只声明一个结构体“看起来是什么样子的”,所以不会在内存中创建成员变量。只有通过定义该结构体类型的变量来实例化结构体,才有地方存储初始值。

栈上的变量是在程序运行时分配内存的,但分配的大小多少是确定的,而这个“大小多少”是在编译时确定的,不是在运行时。堆是应用程序在运行的时候请求操作系统分配给自己内存,是由操作系统管理的内存分配,编译器并不知道要从堆里分配多少内存空间。

静态存储:分配要求在编译时就能知道所有变量的存储要求;栈式存储:分配要求在运行时必须知道所有的存储要求;堆式存储:分配要求在编译时或运行时都无法确定存储要求的数据结构。

C++中内存分区

内存分为5个区:堆、栈、自由存储区、全局/静态存储区、常量存储区

栈:由编译器在需要的时候分配,在不需要的时候自动清除的变量存储区。里面的变量通常是局部变量、函数参数等;

自由存储区:由new分配的内存块,释放编译器不管,由我们的应用程序去控制。一般一个new就要对应一个delete。如果程序员没有释放,那么在程序结束后,操作系统会自动释放。(无论你是怎么分配的,也无论你是分配在堆还是栈上面,很明显,它都是属于进程的,当程序退出时候,进程就不存在了,进程所占用的所有资源,操作系统都会收回的);

堆:由malloc等分配内存块,由free来释放;

全局/静态存储区:由编译器在编译阶段就分配好了内存,程序结束时,由操作系统回收;

常量存储区:比较特殊的存储区,存放的是常量。

堆和栈

int *p=new int[5];  在程序中先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中。

静态数据成员:在程序一开始就必须存在,因为函数在程序运行中被调用,所以静态数据不能在任何函数内分配空间和初始化。类声明只是声明一个类的“尺寸和规格”,并不进行实际的内存分配,所以在类声明中写成定义是错的,它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。

全局变量、文件域的静态变量和类的成员变量是在main执行之前的静态初始化过程中分配内存并初始化的;局部静态变量是在第一次使用时分配内存并初始化。

STL allocator将两阶段操作区分开来

内存配置由alloc::allocate 负责,对象构造由alloc::construct()负责;内存释放由alloc::deallocate()负责,对象析构操作由::destroy()负责。

针对内存碎片问题,SGI设计了两层的配置器,第一级配置器和第二级配置器,SGI版STL提供了一层更高级的封装,定义了一个simple_alloc类,无论是用哪一级

第一级配置器:直接调用malloc和free来配置释放内存

第二级配置器:根据情况来判定,如果配置区块大于128bytes,说明足够大,调用第一级配置器,而小于128bytes,则采用复杂内存池来管理。(维护16个自由链表,负责16种小型区块的配置能力,内存池以malloc配置而得,如果内存不够,转调用第一级配置器)。如果自由链表有,则直接取走,不然则需要装填自由链表。释放操作:大于128,直接调用第一级空间配置器收回,小于等于128,则有自由链表收回。

自由链表:指针数组,类似hash表,它的数组大小为16,每个数组元素代表所挂的区块大小。同时我们还有一个被称为内存池地方,以start_free和end_free记录其大小,用于保存未被挂在自由链表的区块,它和自由链表构成了伙伴系统。

如果自由链表对应的位置没有所需的内存块,使用Refill函数实现:默认获取20的新节点,然后返回一块给调用者。其他挂在自由链表中。

系统会自动将n字节扩展到8的倍数,用户需要n字节,且自由链表中没有,因此系统会向内存池申请nobjs*n大小的内存块,默认nobjs=20。如果内存池大于nobjs*n,那么直接从内存池中取出;如果内存池小于nobjs*n,但是比一块大小n要大,那么此时将内存最大可分配的块数给自由链表,并且更新nobjs为最大分配块数x;如果内存池连一个区块的大小n都无法提供,那么首先将内存池残余的零头给挂在自由链表上,然后向系统heap申请空间,申请成功则返回,申请失败则到自己的自由中看看还有没有可用区块,如果连自由链表都没了最后会调用一级配置器。

优点:1)避免频繁调用malloc,free开辟释放小块内存带来的性能效率的低下;2)内存碎片问题,导致不连续内存不可用的浪费。

缺点:1)内存碎片的问题,自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费,以空间换时间。2)似乎没有释放自由链表所挂区块的函数,由于配置器的所有方法,成员都是静态的,那么他们就是存放在静态区,释放时机就是程序结束,这样子会导致自由链表一直占用内存,自己进程可以用,其他进程却用不了。

内存泄漏

1)内存泄漏指是在程序里动态申请的内存在使用完后,没有进行释放,导致这部分内存没有被系统回收,久而久之,可能导致程序内存不断增大,系统内存不足。。。引发一系列灾难性后果。

2)检测内存泄漏:VS下使用CRT,在程序前加上:#define CRTDBG_MAP_ALLOC,在程序最后加上:_CrtDumpMemoryLeaks(); 程序运行后在下面的窗口中可以看到内存泄漏的信息,{65}代表了第65次内存分配操作发生了泄漏,所以根据这个信息,可以定位到内存泄漏的位置,可以添加如下代码:_CrtSetBreakAlloc(65)。

linux系统下内存泄漏的检测方法:valgrind   编译:g++ -g -o test test.cpp;使用:valgrind --tool=memcheck ./test

指针函数和函数指针

指针函数:本质是一个函数,函数返回类型是某一类型的指针

函数指针:指向函数的指针变量,即本质是一个指针变量。int (*fun)(int x);可以通过它来调用函数。

后面俩字是本质。

traits特性萃取技术

在STL中,算法和容器是分开的,所以算法的实现并不知道自己被传进来什么,萃取器相当于在接口的实现之间加一层封装,来隐藏一些细节并写主调用合适的方法。

traits一方面,在面对不同的输入类时,能找到合适的返回类型;原始指针无法定义型别,需通过类模板的偏特化得到型别,而迭代器能定义自己的型别。常用迭代器型别:迭代器所指对象的型别、两个迭代器之间距离,即容量、用来指向迭代器所指之物、解引用时获取左值。

一个空类默认产生哪些类成员函数?

默认产生:构造函数、拷贝构造函数、析构函数、赋值运算符、取地址运算符、常量取地址运算符。

C++11:默认移动构造函数A(A&&)、移动赋值运算符A& operator=(const A&&)。

C++类型转换

1)static_cast:静态类型转换,即在编译期间即可确定的类型转换,能代替C风格的类型转换

2)dynamic_cast:可以在执行期决定真正的类型。主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。提供了类型安全检查。被转换对象T1必须是多态类型,即T1必须公有继承其他类,或者T1拥有虚函数(继承或自定义)。对指针进行转换,失败返回NULL,成功返回正常cast后的对象指针;对引用进行转换,失败抛出异常,成功返回正常cast后的对象引用。

A *a=new B;
a->aa();
B *b=dynamic_cast<B*>(a);  //父类转换成子类
b->aa();/*
B *b=new B;
b->aa();
A *a=dynamic_cast<A*>(b); //子类转换成父类
a->aa();
*/

3)const_cast:去掉const属性转换,const_cast<目标类型>,目标类型只能是指针或者引用

4)reinterpret_cast:重新解释类型转换,相当于强制类型转换

hash表中当某一个链表太长时,会把链表改成红黑树结构。

指针和数组的区别

指针:是一个变量,存放的是其他变量在内存中的地址;同类型指针变量可以相互赋值;

赋值、存储方式、sizeof、初始化

32位数中1的个数

查表法利用空间换事件,将32位拆成4个8位数字,分别进行4次查表操作,然后将结果相加。

STL容器是否是线程安全的?

1)多个读者是安全的;2)对不同容器的多个写入者也是安全的。

需要锁定:1)每次调用容器的成员函数的期间需要锁定;2)每个容器返回迭代器的生存期需要锁定;3)每个容器在调用算法的执行期需要锁定。

处理Hash冲突

(1)再散列法:1)线性探测再散列;2)二次探测再散列。(2)拉链法

在跨平台进行指针传递时,应该用int类型还是long类型

结论:应该用long类型

int类型的长度为4个字节,而long类型是不定的,和操作系统位数保持一致(32位操作系统的long长度为32bit,4个字节,64位操作系统的long长度为64bit,8个字节)。如果用int类型传递64位操作系统的指针,会把高四位地址截断,导致错误。而long型,由于其字节长度和操作系统位数保持一致,因此不会产生地址截断的问题。

2、操作系统

在同一台主机上使用socket通信会不会经过网卡?

结论:不走网卡,不走物理设备,但走虚拟设备,loopback device环回。

本机的报文的路径:应用层——>socket接口——>传输层——>网络层——>back to传输层——>back to socket接口——>传回应用程序。

测试:可以在不用网络的情况下,进行测试。

在网络层,会在路由表查询路由,路由表初始化时会保存主机路由,查询后发现不用转发就不用走中断,不用发送给链路层,不用发送给网络设备(网卡)。像网卡发送接收报文一样,走相同的接收流程,只不过net device是loopback device,最后发送回应用程序。

epoll

1)支持一个进程打开大数目的socket描述符:1GB内存大约是10万作用,和系统内存关系很大;

2)IO效率不随FD数目增加而线性下降

3)使用mmap加速内核与用户空间的消息传递

poll

int poll(struct pollfd *fds,nfds_t nfds,int timeout)

fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd{fd 文件描述符、events等待事件读写、revents实际发生事件}

nfds:指定第一个参数数组元素个数

mmap应用

1)malloc分配内存;2)epoll模型中用于内核态和用户态通信。

CPU上下文切换

上下文切换是指CPU从一个进程或线程切换到另一个进程或线程。上下文是指某一个时间点CPU寄存器和程序计数器的内容。寄存器是CPU内部的数量较少但是速度很快的内存。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。程序计数器

进程切换与线程切换的代价比较

进程切换分两步:

1)切换页目录以使用新的地址空间;2)切换内核和硬件上下文。

切换的性能消耗:1)线程上下文切换和进程上下文切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成。内核的这种切换过程伴随的最显著性能损耗是将寄存器中的内容切换出。2)另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了,还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

为什么虚拟地址切换很慢
现在我们已经知道了进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB,Translation Lookaside Buffer,我们不需要关心这个名字只需要知道TLB本质上就是一个cache,是用来加速页表查找的。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。

int recv(socket s,char *buf,int len,int flags);

接收端套接字描述符、缓冲区用来存储recv函数接收到的数据、buf的长度、0

返回值:<0 出错、=0连接关闭、>0接收到数据大小。若没有数据,则recv会一直等。

阻塞模式下recv会一直阻塞直到接收到数据,非阻塞模式下如果没有数据就会返回,不会阻塞着读,因此需要循环读取。

int send(socket s,char* buf,int len,int flags)

返回值:<0 出错、=0 连接关闭、>0表示发送的字节数

ps -aux 查看进程PID

lsof -p 1430:可以获取打开的文件信息,类型、大小、节点信息等

ll proc/1430/fd:查看打开的文件描述符

netstat -nap | grep PID

Makefile和Cmake的联系和区别

CMake:是一种跨平台编译工具,比make更高级,使用起来更方便。CMake主要是编写CMakeList.txt文件,然后用cmake指令将CMakeList.txt文件转化成make所需要的makefile文件,最后用make命令编译源代码生成可执行程序。

自旋锁:当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断该锁是否能被成功获取,直到获取到锁才会退出循环。

优点:自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是活跃的,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。

3、计算机网络

流量控制

1)定义:TCP根据接收端缓冲区的大小,来决定发送数据的快慢,这个机制叫做流量控制。

2)原因:任何一个接受端接受的能力都是有限的,如果接受缓冲区满了,而发送端依然发送数据,就会导致丢包,而引起超时重传。这对资源也是一种浪费。

3)实现:接收端每次都会将自己的缓冲区大小放入到TCP报头的“滑动窗口”字段,通过ACK确认应答的来通知发送端。如果发送端得知接收端的缓冲区很小,就会减慢发送的频率。如果得知缓冲区满了,发送端就不再发送数据了,但是会定期发送一个零窗口探测数据段,来得知是否可以接受数据了。

什么时候开始三次握手?

客户端进程connect(),服务端进行了listen()时,进行三次握手。

accept函数会从已经建立连接的队列中取出第一个连接,并创建一个新的socket,新的socket的类型和地址参数要和原来那个的指定的socket的地址一样,并且还要为新的socket分配文件描述符。默认会阻塞进程。

四次挥手只进行了两次会怎么办?

四次挥手服务器先关闭,客户端不关闭,继续发送数据,因为对方关闭(相当于管道中对方的读端口关闭写端口写满缓冲区就会触发SIGPIPE信号,操作系统会强制关闭写端),客户端继续写的话,会触发SIGPIPE信号,操作系统会强制关闭客户端。

time_wait

产生原因:1)为实现TCP全双工连接的可靠释放,确保对方收到最后的ACK,不然会超时重传FIN;2)为使旧的数据包在网络因过期而消失,假设当前有一条TCP连接,因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接,TCP协议栈无法区分前后两条TCP连接是不同的,前一条TCP连接会被当做当前TCP连接的正常数据接收并向上传递至应用层,从而导致数据错乱进而导致各种无法预知的诡异现象。

避免time_wait:服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于time_wait状态时可以重用端口。例如,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项可以避免time_wait状态。

编辑内核文件/etc/sysctl.conf,加入以下内容:

net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间

TCP/IP中close_wait状态和time_wait状态

close_wait:说明套接字是被动关闭的,且还没有发FIN给对方,那么可能是在关闭连接之前还有许多数据要发送或者其他事要做,导致没有发这个FIN,发送完了自然就要通过系统调用发FIN了,这个场景并不是我们提到的持续的close_wait状态,这个在受控范围。

默认会至少维持2个小时,消耗大量资源,可通过修改TCP/IP的参数,来缩短这个时间,修改tcp_keepalive_*系列参数有助于解决这个问题。

粘包问题

只有TCP协议中才会发生粘包问题,TCP粘包是指发送方发送的若干包数据到接收方时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

原因:1)发送方,TCP默认使用Nagle算法,这个算法主要做两件事:1.只有上一个岑组得到确认,才会发送下一个分组;2.收集多个小分组,在一个确认到达时一起发送。2)接收方,TCP接收到的分组保存至接收缓冲区里,然后应用程序主动从缓存里读收到的分组,这样一来,如果TCP接收到分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。

解决:通过发送长度或者格式化数据

物理层:通过光纤、电缆、双绞线等把两台计算机连接起来,然后在计算机之间传送0,1这样的电信号;

数据链路层:工作在物理层之上,负责给这些0,1制定传送规则,然后另一方再按照相应的规则来进行解读。根据以太网协议,把一组电信号构成一个数据包,称为“帧”。每一个帧由标头(说明数据,例如发送者或者接收者等信息)长度固定为18个字节和数据(不是固定的,64~1518个字节)两部分组成。

广播与ARP协议:计算机A知道计算机B的MAC地址,可是计算机A无法知道计算机B是分布在哪边路线上的,实际上,计算机A是通过广播的方式把数据发送给计算机B,在同一个子网中,计算机A要向计算机B发送一个数据包,这个数据包包含接收者的MAC地址。这个时候同一个子网中的计算机C,D也会收到这个数据包,然后收到这个数据包的计算机,会把数据包的MAC地址取出来,与自身的MAC地址对比,如果两者相同,则接收这个数据包,否则就丢弃这个数据包。这种发送方式称为广播。

网络层:IP协议帮助我们区分MAC地址是否处于同一个子网中。IP地址由网络部分+主机部分。把子网掩码和IP相与,若得到的一样,则处于同一个子网中。假如两台计算机的IP不是处于同一个子网中,这个时候,我们就会把数据包发送给网关,然后网关让我们进行转发。

根据MAC地址发送数据和根据IP地址询问MAC地址都是通过广播的形式发送,区分:在询问MAC地址的数据包中,在对方的MAC地址这一栏中,填的是一个特殊的MAC地址,其他计算机看到这个特殊的MAC地址之后,就能知道广播想干嘛了。

DNS服务器:通过DNS服务器来对域名进行解析,得到对方的IP

传输层:虽然已经把数据成功从计算机A传送到计算机B,可是计算机B里面有各种各样的应用程序,计算机通过端口进行解决的,传输层的功能就是建立端口到端口的通信。

应用层:虽然已经收到了传输层传来的数据,可是这些数据有html格式的,有mp4格式的,各种各样的,应用层就是指定这些数据的格式规则,收到后才能进行解读。

伙伴系统:伙伴系统从物理连续的大小固定的段上进行分配,假设内存段的大小最初为256KB,内核请求21KB,最初,这个段分为两个伙伴,称为AL和AR,每个的大小都为128KB;这两个伙伴之一进一步分成两个64KB的伙伴,即BL和BR,然而,从21KB开始的下一个大的2的幂是32KB,因此BL或BR再次划分为两个32KB的伙伴CL和CR,因此,其中一个32KB的段可用满足21KB请求,当释放已分配的CL内存时,系统可以将CL和CR合并成64KB的段,然后继续合并,最终可以得到原来的256KB段。

缺点:由于圆整到下一个2的幂,很可能造成分配段内的碎片。

slab分配:每个slab由一个或多个物理连续的页面组成,每个cache由一个或多个slab组成,每个内核数据结构都有一个cache。例如,用于表示进程描述符、文件对象、信号量等的数据结构都有各自单独的cache,每个cache含有内核数据结构的对象实例称为object。例如,信号量cache有信号量对象。

优点:1)没有因碎片而引起的内存浪费。每个内核数据结构都有关联的cache,每个cache都由一个或多个slab组成,而slab按所表示对象的大小来分块。因此当内核请求对象内存时,slab分配器可以返回刚好表示对象的所需内存;2)可以快速满足内存请求。由于对象已经预先创建,因此可以从cache中快速分配。再者,当内核用完对象并释放它时,它被标记为空闲并返回到cache,从而立即可用于后续的内核请求。

http常见状态码

1xx:代表消息,一般告诉客户端,请求已经收到了,正在处理

2xx:代表请求成功,一般是请求收到,请求已经处理完成等信息

3xx:代表重定向到其他地方,他让客户端在发起一个请求,以完成整个处理

304:每个资源请求完成后,通常会被缓存在客户端,并会记录资源的有效时间和修改时间。当客户再次请求该资源,客户端首先从缓存中查找该资源。如果该资源存在,并且在有效期,则不请求服务器,就不会产生对应的请求数据包。如果不在有效期,客户端会请求服务器,重新获取,服务器会判断修改时间,如果没有修改过,就会返回状态码304,告诉客户端该资源仍然有效,客户端会直接使用缓存的资源。客户端和服务器端只需要传输很少的数据量来做文件的校验,如果文件没有修改,则不需要返回全局的数据。

4xx:代表处理错误,责任在客户端,如客户端请求一个不存在的资源、客户端未被授权、禁止访问等

5xx:处理发生错误,责任在服务端,如服务端抛出异常,路由出错、HTTP版本不支持等

Linux系统中,可以用来查找可执行文件是:whereis、locate、which、type、find

进程由执行——>阻塞:I/O请求

UDP:无连接的,面向消息的,不会使用块的合并优化算法,由于UDP支持一对多的模式,所以接收端的套接字缓冲区采用链式结构来记录每一个到达的UDP包,在每个UDP包中就有消息头(消息来源地址,端口等信息),这样对于接收端来说,就容易进行区分处理。即面向消息的通信是有消息保护边界的。

4、算法

发红包

int price=rand()%(money/count*2)+1

money=money-price;

count-=1;

大数int求平局值,可能爆int

用到贪心算法:Dijkstra、Kruskal、Prim

5、高并发服务器

web服务器在web页面处理上的步骤:

1)web浏览器向一个特定的服务器发出web页面请求;

2)web服务器收到web页面请求后,寻找所请求的web页面,并将所有请求的web页面传送给web浏览器;

3)web浏览器接收到所请求的web页面内容,并将它显示出来。

影响web页面访问的影响因素会有这几个

1)web服务器磁盘性能:提升服务器磁盘访问性能,也即通常所说的I/O性能;

2)web服务器与应用服务器交互的性能

3)应用服务器处理动态内容的性能,或者说动态内容应用处理性能:加快动态内容的处理性能;尽可能多地使用静态内容,这样web服务器就可以无需请求应用服务器,直接将web内容发给浏览器,这里可以入手的方案又有:1.动态内容缓存、2.动态内容静态化;

4)客户端与web服务器的连接速度,即网络传输性能:增加宽带,包括服务器和客户端两边的internet连接宽带;

5)web浏览器解释和渲染web内容的性能

6)web访问并发性能:多态服务器负载均衡同时处理大量的并发访问;

6、数据库

索引B+树

1)叶子节点包含全部关键字以及指向相应记录的指针,而且叶节点的关键字按大小顺序排列,相邻叶节点用指针链接;

2)非叶节点仅存储其子树最大(或最小)关键字,可以看成是索引。

优点:

1)B+树更适合外部存储,由于根节点不存放真正数据(只存放其子树的最大或最小的关键字,作为索引),一个节点可以存储更多的关键字,每个节点能索引的范围更大更精确,也意味着B+树单次磁盘IO的信息量大于B树,I/O的次数相对减少

2)MySql是一种关系型数据库,区间访问是常见的一种情况,B+树叶节点增加的链指针,加强了区间访问性,可使用在区间查询的场景;

事务四大特性:原子性、一致性、隔离性、持久性

事务隔离级别:读未提交、读已提交、可重复读

索引:1)MyISAM使用B+树作为索引结构,叶节点的data域存放的是数据记录的地址,即MyISAM索引文件和数据文件是分离的,MyISAM的索引文件仅仅保存记录的地址。2)InnoDB索引使用B+树作为索引,叶节点的data域存放的就是数据记录。这个索引的key是数据表的主键。

MySQL数据库的四类索引:普通索引、唯一索引(允许有空值)、主键索引(不允许空值)、组合索引

索引生效条件:最左前缀匹配原则

数据库三大范式:1)字段具有原子性,不可再分解;2)非主键字段不能出现部分依赖主键;3)非主键字段不能出现传递依赖。

1、MySQL的主从复制

三个线程:主binlog线程、从IO线程、从SQL执行线程

日志:主bin-log日志、从relay log日志

2、MyISAM和InnoDB区别

MyISAM:不支持事务、支持表级锁、不支持MVCC、不支持外键、支持全文索引。MyISAM内部维护一个计算器,可以直调取。MyISAM的索引和数据是分开的,并且索引是有压缩的,内存使用率就对应提高不少。能加载更多索引,而InnoDB是索引和数据是紧密捆绑,没有使用压缩从而造成InnoDB比MyISAM体积庞大不少。

InnoDB:支持事务、支持行级别锁、支持外键、不支持全文索引。

select count(*):MyISAM更快,因为MyISAM内部维护了一个计数器,存储了表的总行数,每次新增一行,这个计数器就加1,可以直接调取

select:InnoDB在select时,要缓存数据库和索引块,而MyISAM只缓存索引块,这中间还有换进换出的减少;InnoDB寻址要映射到块,再到行,而MyISAM记录的直接是文件的OFFSET,定位比InnoDB快;InnoDB还需要维护MVCC一致。

堆表:数据插入时存储位置是随机的,主要是数据库内部块的空闲情况决定,获取数据是按照命中率计算,全表扫描时不见得先插入的数据先查到;

索引组织表:数据存储是把表按照索引的方式存储的,数据是有序的,数据的位置是预先定好的,与插入的顺序没有关系。

索引表的查询效率比堆表高(相当于查询索引效率),插入数据的速度比堆表慢。

3、事务4种隔离级别

读未提交:脏读意味着在事务A中,事务B虽然没有提交,但它任何一条数据变化,在事务A中都可以看到

读已提交:不可重复读意味着在同一个事务中执行完全相同的select语句时可能看到不一样的结果。

可重复读:是MySQL的默认事务隔离级别,可能出现幻读,即一个事务在进行插入数据后,另一个事务先select后,并未发现这条数据,然后第一个事务进行了提交,然后第二个事务进行插入同样的事务,发现插入不成功,即出现幻觉。

可串行化:它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简而言之,它是再每个读的数据行加上共享锁。

4、对于MyISAM表会把自增主键的最大ID记录到数据文件里,重启MySQL自增主键最大ID也不会丢失;如果是InnoDB,只是把自增主键最大ID记录到内存中,所以重启数据库或者是对表进行OPTIMIZE操作,都会导致最大ID丢失。

5、MySQL为什么用自增列作为主键

如果表使用自增索引,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。

如果使用非自增主键,由于每次插入主键的值近似于随机,因此每次新的记录都要被插入到现有索引页的中间某个位置,此时MySQL不得不为了将新纪录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上都回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。

6、为什么使用数据索引能提高效率

1)数据索引的存储是有序的;2)在有序的情况下,通过索引查询一个数据是无需遍历索引记录的;3)极端情况下,数据索引的查询效率为二分法查询效率,趋近于log2(N)

7、什么情况下应不建或少建索引

1)表记录太少;2)经常插入、删除、修改的表;3)数据重复且分布平均的表字段;4)经常和主字段一块查询但主字段索引值比较多的表字段

表分区:根据一定规则,将数据库中的一张表分解成多个更小的,容易管理的部分。从逻辑上看,只有一张表,但是底层却是由多个物理分区组成。

分表:指的是通过一定规则,将一张表分解成多张不同的表。

分表与分区的区别:分区从逻辑上来讲只有一张表,而分表则是将一张表分解成多张表。

表分区好处:1)高效利用多个硬件设备;2)可以存储更多数据;3)分区表更容易维护,例如批量删除大量数据可以清楚整个分区;

分区表的限制因素:1)一个表最多只能有1024个分区;2)分区字段要么不包含主键或者索引列,要么包含全部主键和索引列;3)分区表无法使用外键约束;4)MySQL的分区适用于一个表所有数据和索引,不能只对表数据分区而不对索引分区。

行级锁定的优点:1)当在许多线程中访问不同的行时减少锁定冲突;2)回滚时只有少量的更改;3)可以长时间锁定单一的行。

行级锁定的缺点:1)比页级或表级锁定占用更多的内存;2)当在表的大部分中使用时,比页级或表级锁定速度慢,因为必须获取更多的锁;3)如果在大部分数据上经常进行GROUP BY操作或者必须经常扫描整个表,比其他锁定明显慢很多;

加表锁:Lock tables db1 read local;

MySQL优化

1)开启查询缓存,优化查询;修改配置文件,vi /etc/my.cnf,在[mysqld]中添加:query_cache_size=20M,query_cache_type=ON

2)explain你的select语句,可以帮忙分析查询语句或是表结构的性能瓶颈。还会显示索引主键被如何利用的,你的数据表是如何被搜索和排序的;

3)当只要一条语句时使用limit 1,MySQL数据库引擎会在找到一条数据后停止搜索;

4)为搜索字段建索引;

5)当知道这些字段是有限而且固定的,那么应该使用ENUM,而不是VARCHAR;

6)选择正确的存储引擎。

key:是数据库的物理结构,包含两层意义和作用,一是约束(偏重于约束和规范数据库的结构完整性),二是索引(辅助查询用的),包括primary key,unique key

index:是数据库物理结构,它只是辅助查询的,它创建时会在另外的表空间(mysql的innodb表空间)以一个类似目录的结构存储。索引要分类的话,分为前缀索引、全文本索引等。

show processlist:Id user host db command time state info

iotop:查看当前系统进程的磁盘读写情况

top:cpu和内存占用资源情况

netstat -lnp:根据网络连接情况,最后一栏显示PID/Program name

netstat -an:打印网络连接状况

幻读:一个事务进行了插入数据,还没提交,另一个事务在查询时,没有查到该记录,然后进行插入同样的数据,但在这之前,前一个事务进行了提交,然后后面这个事务就不能插入成功了,出现了幻觉。

MySQL主要的两种搜索引擎有MyISAM和InnoDB,前者大多索引的结构为B-tree,而后者只有主键索引B+tree,非主键索引也用B-tree,所以应该默认的索引结构是B-tree。

在MySQL中只有Memory引擎显式支持哈希索引。

哈希索引只包含索引值和行指针,不存储字段值。

MyISAM支持空间索引。

空间索引还是用where,全文索引查询用的是match agatinst

索引一定要用顺序I/O

web开发常见问题(SQL注入、XSS攻击、CSRF攻击)

SQL注入:发生在应用程序之数据库层的安全漏洞。在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了检查,那么这些注入进去的指令就会被数据库服务器误认为是正常的SQL指令而运行,因此遭到破坏或是入侵。

XSS攻击:类似于SQL注入,通过插入恶意脚本,实现对用户

【C++后台开发面经】面试总结第八波:整个知识的查漏补缺相关推荐

  1. Java 面试知识大全总结:程序员面试的必备,想拿offer来查漏补缺

    如果你参加了很多公司的面试,但都没有拿到心目中理想的offer,又或者拿到offer的概率特别低,原因很简单,你可能在技术知识点上有漏洞. Java面试,是对技术知识栈的梳理.考核.复盘 每一次Jav ...

  2. 参考别人的面试总结:linux C/C++服务器后台开发面试题总结

    参考别人的面试总结:linux C/C++服务器后台开发面试题总结 参考博客:http://www.cnblogs.com/nancymake/p/6516933.html 基础语言知识方面: 1.使 ...

  3. c++后台开发项目_[c/c++后台开发面经系列]4 Zoom面经(含答案)

    嘻嘻,祝小仙女们女神节快乐! 看完别忘点个在看哦 此系列内容以及观点仅个人感受,不妥之处直接私我!目的为了大家能更好的知道面试题难度以及如何准备,希望能让大家少浪费时间寻找资料,多点时间学点干货!因为 ...

  4. ​[c/c++后台开发面经系列]4 Zoom面经(含答案)

    嘻嘻,祝小仙女们女神节快乐! 看完别忘点个在看哦 此系列内容以及观点仅个人感受,不妥之处直接私我!目的为了大家能更好的知道面试题难度以及如何准备,希望能让大家少浪费时间寻找资料,多点时间学点干货!因为 ...

  5. Java后台开发面经汇总

    Java后台开发面经汇总 2021 + 面试复习 (持续更新,与君共勉) 写在前面: 内容颇多,博主不断追更,根据下方"文章目录"可对所需内容快速跳转

  6. php开发面试题---面试常用英语(你能介绍你自己吗?)

    php开发面试题---面试常用英语(你能介绍你自己吗?) 一.总结 一句话总结: Could you please describe yourself? 1.为什么觉得自己适合这份工作? Why do ...

  7. 前端面试查漏补缺--(一) 防抖和节流

    前言 本系列最开始是为了自己面试准备的.后来发现整理越来越多,差不多有十二万字符,最后决定还是分享出来给大家. 为了分享整理出来,花费了自己大量的时间,起码是只自己用的三倍时间.如果喜欢的话,欢迎收藏 ...

  8. 前端面试查漏补缺--(二) 垃圾回收机制

    前言 本系列最开始是为了自己面试准备的.后来发现整理越来越多,差不多有十二万字符,最后决定还是分享出来给大家. 为了分享整理出来,花费了自己大量的时间,起码是只自己用的三倍时间.如果喜欢的话,欢迎收藏 ...

  9. 前端面试查漏补缺--(三) 跨域及常见解决办法

    前言 本系列最开始是为了自己面试准备的.后来发现整理越来越多,差不多有十二万字符,最后决定还是分享出来给大家. 为了分享整理出来,花费了自己大量的时间,起码是只自己用的三倍时间.如果喜欢的话,欢迎收藏 ...

最新文章

  1. Java基于自定义注解的面向切面的实现
  2. 计算机无法搜索本地文件,电脑本地磁盘无法搜索文件。怎么处理?
  3. 【数据结构与算法】之单向循环链表的创建/遍历/插⼊/查找/删除算法实现
  4. mysql中定时任务_mysql中定时任务的用法
  5. grep、sed命令使用记录
  6. Linux系统管理第六周作业【Linux微职位】
  7. MySQL之Lock探索(二)
  8. sicp 3.9题解答
  9. 单片机矩阵键盘扫描程序c语言,51单片机矩阵键盘扫描程序详解
  10. 用实例配置 linux squid 代理服务器
  11. Therefore, hence, so, then, thus
  12. Photo Album: 2008年5月-三亚爱琴海岸康年度假村-day1
  13. 我来说说我给宝宝买保险的事
  14. 直属上司,才是你最该管的人
  15. 第1章 人工智能时代,人人都应该学会利用AI这个工具 / 1-6 Pandas、Numpy、Matplotlib实操
  16. 如何破坏双亲委派模型
  17. 作用域符号与成员访问符号_如何购买表情符号域
  18. 云计算学习路线教程大纲课堂笔记:构建企业级WIKI及工单系统
  19. 谷歌浏览器部分网站不能保存密码
  20. 杭州有什么「鲜为人知但很有意思」的景点?

热门文章

  1. XenAPP6.5安装于部署(五)---Citrix AppCenter配置,发布应用
  2. 大二学长写给计算机专业大一学生们
  3. 三步快速免费开发工业手机APP,远程监控西门子300PLC数据
  4. PHP APP端支付宝支付
  5. 保持左右脑平衡的方法
  6. C# 调用WinRar执行rar、zip压缩的代码
  7. freeswitch延迟呼叫
  8. 微型计算机状态标志,微机原理--指令系统--判断状态标志位!尤其是OF和CF!!...
  9. poj2331 (IDA*)
  10. VBA----FileSystemObject