文章目录

  • 第一讲:primitives
    • 四种内存分配与释放
    • 基本构件之 new/delete expression
      • 1、内存申请
      • 2、内存释放
      • 3、模拟编译器直接调用构造和析构函数
    • Array new
    • placement new
    • 重载
      • 1、C++内存分配的途径
      • 2、重载new 和 delete
      • 3、测试案例
    • pre-class allocator
    • static allocator
    • global allocator
    • new handler
    • =default和=delete

第一讲:primitives

四种内存分配与释放


在编程时可以通过上图的几种方法直接或间接地操作内存。下面将介绍四种C++内存操作方法:

1.::operator new()调用malloc,::operator delete()调用free

2.通常可以使用malloc和new来分配内存,当然也可以使用::operator new()(是一个全局函数)和分配器allocator来操作内存,下面将具体介绍这些函数的使用方法。对于不同的编译器,其allocate函数的接口也有所不同:

对于GNU C,不同版本又有所不同:

这张图中的__gnu_cxx::__pool_alloc().allocate()对应于上张图中的allocator().allocate()。

通过malloc和new分配内存、通过free和delete释放内存是十分常用的,通过::operator new操作内存比较少见,allocator分配器操作内存在STL源码中使用较多,对于不同的编译环境使用也有所不同。下面这个例子是基与VS2019环境做测试的

#include <iostream>
#include <complex>
#include <memory>//std::allocatorusing namespace std;
namespace jj01
{void test_primitives1(){cout << "\ntest_primitives().......... \n";void* p1 = malloc(512);//512 bytesfree(p1);complex<int>* p2 = new complex<int>;//one object一个单元delete p2;void* p3 = ::operator new(512);//512 bytes::operator delete(p3);//以下使用 C++ 標準庫提供的 allocators。//其接口雖有標準規格,但實現廠商並未完全遵守;下面三者形式略異。
#ifdef _MSC_VER//VC接口//以下兩函數都是 non-static,定要通過 object 調用。以下分配 3 個 ints.//allocator<int>()临时对象int* p4 = allocator<int>().allocate(3, (int*)0);p4[0] = 666;p4[1] = 999;p4[2] = 888;cout << "p4[0] = " << p4[0] << endl;cout << "p4[1] = " << p4[1] << endl;cout << "p4[2] = " << p4[2] << endl;//还内存的时候还要把当初分配内存大小表明,这种事情只有容器可以做到allocator<int>().deallocate(p4, 3);
#endif // _MSC_VER}
}int main(void)
{jj01::test_primitives1();return 0;
}

test_primitives()..........
p4[0] = 666
p4[1] = 999
p4[2] = 888

可见 int* p4 = allocator().allocate(3, (int*)0) 操作成功申请了三个int的空间。

基本构件之 new/delete expression

1、内存申请


上面这张图揭示了new操作背后编译器做的事:

1、第一步通过operator new()操作分配一个目标类型的内存大小,这里是Complex的大小;
2、第二步通过static_cast 将得到的内存块强制转换为目标类型指针,这里是Complex*
3、第三版调用目标类型的构造方法,但是需要注意的是,直接通过pc->Complex::Complex(1, 2)这样的方法调用构造函数只有编译器可以做,用户这样做将产生错误。

值得注意的是,operator new()操作的内部是调用了malloc()函数。

2、内存释放


同样地,delete操作第一步也是调用了对象的析构函数,然后再通过operator delete()函数释放内存,本质上也是调用了free函数。

3、模拟编译器直接调用构造和析构函数

#include <iostream>
#include <string>
#include <memory>//std::allocatorusing namespace std;
namespace jj02
{class A{public:int id;A():id(0) { cout << "default ctor. this=" << this << " id=" << id << endl; }A(int i):id(i) { cout << "ctor. this=" << this << " id=" << id << endl; }~A() { cout << "dtor. this=" << this << " id=" << id << endl; }};void test_call_all_ctor_directly(){cout << "\ntest_call_ctor_directly().......... \n";string* pstr = new string;cout << "str= " << *pstr << endl;//! pstr->string::string("jjhou");//[Error] 'class std::basic_string<char>' has no member named 'string'//! pstr->~string();   //crash -- 其語法語意都是正確的, crash 只因為上一行被 remark 起來嘛.cout << "str= " << *pstr << endl;A* pA = new A(1);cout << pA->id << endl;       //1pA->A::A(3);cout << pA->id << endl;//!  pA->A::A(3);                //in VC6 : ctor. this=000307A8 id=3//in GCC : [Error] cannot call constructor 'jj02::A::A' directlyA::A(5);//!    A::A(5);                    //in VC6 : ctor. this=0013FF60 id=5//         dtor. this=0013FF60//in GCC : [Error] cannot call constructor 'jj02::A::A' directly//         [Note] for a function-style cast, remove the redundant '::A'cout << pA->id << endl;       //in VC6 : 3//in GCC : 1delete pA;//simulate newvoid* p = ::operator new(sizeof(A));cout << "p=" << p << endl;    //p=000307A8pA = static_cast<A*>(p);pA->A::A(2);//! pA->A::A(2);             //in VC6 : ctor. this=000307A8 id=2//in GCC : [Error] cannot call constructor 'jj02::A::A' directlycout << pA->id << endl;     //in VC6 : 2//in GCC : 0//simulate deletepA->~A();//先析构::operator delete(pA);//free()再释放内存}
}int main(void)
{jj02::test_call_all_ctor_directly();return 0;
}

test_call_ctor_directly()..........
str=
str=
ctor. this=0149D1C8 id=1
1
ctor. this=0149D1C8 id=3
3
ctor. this=0118FB48 id=5
dtor. this=0118FB48 id=5
3
dtor. this=0149D1C8 id=3
p=0149FED0
ctor. this=0149FED0 id=2
2
dtor. this=0149FED0 id=2

Array new


上图主要展示的是关于new array内存分配的大致情况。

当new一个数组对象时(例如 new Complex[3],调用3次默认构造函数),编译器将分配一块内存,这块内存首部是关于对象内存分配的一些标记(包括内存的大小,这对于malloc是必要的)

然后下面会分配三个连续的对象内存,在使用delete释放内存时需要使用delete[]。如果不使用delete[],只是使用delete只会将分配的三块内存空间释放,但不会调用对象的析构函数,如果对象内部还使用了new指向其他空间,如果指向的该空间里的对象的析构函数没有意义,那么不会造成问题,如果有意义,那么由于该部分对象析构函数不会调用,那么将会导致内存泄漏。图中new string[3]便是一个例子,虽然str[0]、str[1]、str[2]被析构了,但只是调用了str[0]的析构函数,其他对象的析构函数不被调用,这里就会出问题。

下面将演示数组对象创建与析构过程:

#include <iostream>
#include <string>
#include <memory>//std::allocatorusing namespace std;
namespace jj03
{class A{public:int id;A():id(0) { cout << "default ctor. this=" << this << " id=" << id << endl; }A(int i):id(i) { cout << "ctor. this=" << this << " id=" << id << endl; }~A() { cout << "dtor. this=" << this << " id=" << id << endl; }};void test_array_new_and_placement_new(){cout << "\ntest_placement_new().......... \n";size_t size = 3;A* buf = new A[size];//default ctor 3 次. [0]先於[1]先於[2])//A必須有 default ctor, 否則 [Error] no matching function for call to 'jj02::A::A()'A* tmp = buf;cout << "buf=" << buf << "  tmp=" << tmp << endl;for (int i = 0; i < size; i++) {new (tmp++) A(i);//3次 ctor}cout << "buf=" << buf << "  tmp=" << tmp << endl;delete[] buf;//dtor three times (次序逆反, [2]先於[1]先於[0])}}int main(void)
{jj03::test_array_new_and_placement_new();return 0;
}

test_placement_new()..........
default ctor. this=0124D184 id=0
default ctor. this=0124D188 id=0
default ctor. this=0124D18C id=0
buf=0124D184  tmp=0124D184
ctor. this=0124D184 id=0
ctor. this=0124D188 id=1
ctor. this=0124D18C id=2
buf=0124D184  tmp=0124D190
dtor. this=0124D18C id=2
dtor. this=0124D188 id=1
dtor. this=0124D184 id=0

构造函数调用顺序是按照构建对象顺序来执行的,但是析构函数执行却相反。值得注意的是,在调用了delete的大括号代码段中,数组有三个元素,但最后只调用了第一个对象的析构函数。

接下来将更具体地展示new array对象的内存分配情况:

如果使用new分配十个内存的int,内存空间如上图所示,首先内存块会有一个头和尾,黄色部分为debug信息(分别为32字节和4字节),灰色部分才是真正使用到的内存,蓝色部分的12bytes是为了让该内存块以16字节对齐。在这个例子中delete pi和delete[] pi效果是一样的,因为int没有析构函数。但是下面的例子就不一样了:

上图通过new申请三个Demo空间大小,内存块使用了96byte,这里是这样计算得到的:黄色部分调试信息32 + 4 = 36byte;黄色部分下面的“3”用于标记实际分配给对象内存个数,这里是三个所以里面内容为3,消耗4byte;Demo内有三个int类型成员变量,一个Demo消耗内存3 * 4 = 12byte,由于有三个Demo,所以消耗了12 * 3 = 36byte空间;到目前为止消耗36 + 4 + 36 = 76byte,加上头尾cookie一共8byte一共消耗84byte,由于需要16位对齐,所以填充蓝色部分为12byte,一共消耗了84 + 12 = 96byte。这里释放内存时需要加上delete[],上面分配内存中有个标记“3”,所以编译器将释放三个Demo对象空间,如果不加就会报错。

placement new


1.new (buf)的buf之前就应该分配好

2.placement new就等同于调用构造函数

重载

1、C++内存分配的途径


如果是正常情况下,调用new之后走的是第二条路线,如果在类中重载了operator new(),那么走的是第一条路线,但最后还是要调用到系统的::operator new()函数,这在后续的例子中会体现。


对于GNU C,背后使用的allocate()函数最后也是调用了系统的::operator new()函数。

2、重载new 和 delete


上面这张图演示了如何重载系统的::operator new()函数,该方法最后也是模拟了系统的做法,效果和系统的方法一样,但一般不推荐重载::operator new()函数,因为它对全局有影响,如果使用不当将造成很大的问题。


1.如果是在类中重载operator new()方法,那么该方法有N多种形式,但必须保证函数参数列表第一个参数是size_t类型变量(需要知道大小);对于operator delete(),第一个参数必须是void* 类型(需要一个指针),第二个size_t是可选项,可以去掉。

2.一般情况下,operator new() 和operator delete()需要是static,因为一般是在创建对象的过程中才调用,这个时候还没有一个明确的对象,这样才可以不通过对象就调用起来

对于operator new[]和operator delete[]函数的重载,和前面类似。




3、测试案例

#include <cstddef>
#include <iostream>
#include <string>
using namespace std;namespace jj06
{class Foo{public:int _id;long _data;string _str;public:static void* operator new(size_t size);static void  operator delete(void* deadObject, size_t size);static void* operator new[](size_t size);static void  operator delete[](void* deadObject, size_t size);Foo() :_id(0) { cout << "default ctor. this=" << this << " id=" << _id << endl; };Foo(int i):_id(i) { cout << "ctor. this=" << this << " id=" << _id << endl; }//virtual~Foo() { cout << "dtor. this=" << this << " id=" << _id << endl; }//不加 virtual dtor, sizeof = 12, new Foo[5] => operator new[]() 的 size 參數是 64,//加了 virtual dtor, sizeof = 16, new Foo[5] => operator new[]() 的 size 參數是 84,//上述二例,多出來的 4 可能就是個 size_t 欄位用來放置 array size.};void* Foo::operator new(size_t size){Foo* p = (Foo*)malloc(size);cout << "Foo::operator new(), size=" << size << "\t  return: " << p << endl;return p;}void Foo::operator delete(void* pdead, size_t size){cout << "Foo::operator delete(), pdead= " << pdead << "  size= " << size << endl;free(pdead);}void* Foo::operator new[](size_t size) {Foo* p = (Foo*)malloc(size);  //crash, 問題可能出在這兒cout << "Foo::operator new[](), size=" << size << "\t  return: " << p << endl;return p;}void Foo::operator delete[](void* pdead, size_t size){cout << "Foo::operator delete[](), pdead= " << pdead << "  size= " << size << endl;free(pdead);}void test_overload_operator_new_and_array_new(){cout << "\ntest_overload_operator_new_and_array_new().......... \n";cout << "sizeof(Foo)= " << sizeof(Foo) << endl;{Foo* p = new Foo(7);delete p;Foo* pArray = new Foo[5];delete[] pArray;}{cout << "testing global expression ::new and ::new[] \n";// 這會繞過 overloaded new(), delete(), new[](), delete[]()// 但當然 ctor, dtor 都會被正常呼叫.Foo* p = ::new Foo(7);::delete p;Foo* pArray = ::new Foo[5];::delete[] pArray;}}
}int main(void)
{jj06::test_overload_operator_new_and_array_new();return 0;
}
Foo::operator new[](), size=184   return: 00F3FC10
default ctor. this=00F3FC14 id=0
default ctor. this=00F3FC38 id=0
default ctor. this=00F3FC5C id=0
default ctor. this=00F3FC80 id=0
default ctor. this=00F3FCA4 id=0
dtor. this=00F3FCA4 id=0
dtor. this=00F3FC80 id=0
dtor. this=00F3FC5C id=0
dtor. this=00F3FC38 id=0
dtor. this=00F3FC14 id=0
Foo::operator delete[](), pdead= 00F3FC10  size= 184
testing global expression ::new and ::new[]
ctor. this=00F3B2E8 id=7
dtor. this=00F3B2E8 id=7
default ctor. this=00F3FC14 id=0
default ctor. this=00F3FC38 id=0
default ctor. this=00F3FC5C id=0
default ctor. this=00F3FC80 id=0
default ctor. this=00F3FCA4 id=0
dtor. this=00F3FCA4 id=0
dtor. this=00F3FC80 id=0
dtor. this=00F3FC5C id=0
dtor. this=00F3FC38 id=0
dtor. this=00F3FC14 id=0
#include <vector>  //for test
#include <cstddef>
#include <iostream>
#include <string>
using namespace std;namespace jj07
{class Bad { };class Foo{public:Foo() { cout << "Foo::Foo()" << endl; }Foo(int) {cout << "Foo::Foo(int)" << endl;// throw Bad();}//(1) 這個就是一般的 operator new() 的重載void* operator new(size_t size){cout << "operator new(size_t size), size= " << size << endl;return malloc(size);}//(2) 這個就是標準庫已經提供的 placement new() 的重載 (形式)//    (所以我也模擬 standard placement new 的動作, just return ptr)void* operator new(size_t size, void* start){cout << "operator new(size_t size, void* start), size= " << size << "  start= " << start << endl;return start;}//(3) 這個才是嶄新的 placement newvoid* operator new(size_t size, long extra){cout << "operator new(size_t size, long extra)  " << size << ' ' << extra << endl;return malloc(size + extra);}//(4) 這又是一個 placement newvoid* operator new(size_t size, long extra, char init){cout << "operator new(size_t size, long extra, char init)  " << size << ' ' << extra << ' ' << init << endl;return malloc(size + extra);}//(5) 這又是一個 placement new, 但故意寫錯第一參數的 type (它必須是 size_t 以滿足正常的 operator new)//!      void* operator new(long extra, char init) { //[Error] 'operator new' takes type 'size_t' ('unsigned int') as first parameter [-fpermissive]//!       cout << "op-new(long,char)" << endl;//!      return malloc(extra);//!     }//以下是搭配上述 placement new 的各個 called placement delete.//當 ctor 發出異常,這兒對應的 operator (placement) delete 就會被喚起.//應該是要負責釋放其搭檔兄弟 (placement new) 分配所得的 memory.//(1) 這個就是一般的 operator delete() 的重載void operator delete(void*, size_t){cout << "operator delete(void*,size_t)  " << endl;}//(2) 這是對應上述的 (2)void operator delete(void*, void*){cout << "operator delete(void*,void*)  " << endl;}//(3) 這是對應上述的 (3)void operator delete(void*, long){cout << "operator delete(void*,long)  " << endl;}//(4) 這是對應上述的 (4)//如果沒有一一對應, 也不會有任何編譯報錯void operator delete(void*, long, char){cout << "operator delete(void*,long,char)  " << endl;}private:int m_i;};//-------------void test_overload_placement_new(){cout << "\n\n\ntest_overload_placement_new().......... \n";Foo start;  //Foo::FooFoo* p1 = new Foo;           //op-new(size_t)Foo* p2 = new (&start) Foo;  //op-new(size_t,void*)Foo* p3 = new (100) Foo;     //op-new(size_t,long)Foo* p4 = new (100, 'a') Foo; //op-new(size_t,long,char)Foo* p5 = new (100) Foo(1);     //op-new(size_t,long)  op-del(void*,long)Foo* p6 = new (100, 'a') Foo(1); //Foo* p7 = new (&start) Foo(1);  //Foo* p8 = new Foo(1);           ////VC6 warning C4291: 'void *__cdecl Foo::operator new(unsigned int)'//no matching operator delete found; memory will not be freed if//initialization throws an exception}
} //namespaceint main(void)
{jj07::test_overload_placement_new();return 0;
}

pre-class allocator

针对一个class写出他的内存管理

1.将一大块内存作为分割,降低malloc的调用次数(速度)

2.减少cookie的空间占用(空间)

每个对象以8byte对齐。内存池本质上是分配了一大块内存,然后将该内存分割为多个小块通过链表拼接起来,所以物理上不一定连续但是逻辑上是连续的。

static allocator



每一个class都有一个专门为自己服务的小型的allocator,这个allocator里面就有一个单向链表,里面每个区块就是Foo或Goo这种大小,这就是所谓的吧内存的事情抽出来分给allocator

#include <cstddef>
#include <iostream>
#include <complex>
using namespace std;namespace jj09
{class allocator{private:struct obj {struct obj* next;  //embedded pointer};public:void* allocate(size_t);void  deallocate(void*, size_t);void  check();private:obj* freeStore = nullptr;const int CHUNK = 5; //小一點方便觀察};void* allocator::allocate(size_t size){obj* p;if (!freeStore) {//linked list 是空的,所以攫取一大塊 memorysize_t chunk = CHUNK * size;freeStore = p = (obj*)malloc(chunk);//cout << "empty. malloc: " << chunk << "  " << p << endl;//將分配得來的一大塊當做 linked list 般小塊小塊串接起來for (int i = 0; i < (CHUNK - 1); ++i)  {  //沒寫很漂亮, 不是重點無所謂.p->next = (obj*)((char*)p + size);p = p->next;}p->next = nullptr;  //last}p = freeStore;freeStore = freeStore->next;//cout << "p= " << p << "  freeStore= " << freeStore << endl;return p;}void allocator::deallocate(void* p, size_t){//將 deleted object 收回插入 free list 前端((obj*)p)->next = freeStore;freeStore = (obj*)p;}void allocator::check(){obj* p = freeStore;int count = 0;while (p) {cout << p << endl;p = p->next;count++;}cout << count << endl;}//--------------class Foo {public:long L;string str;static allocator myAlloc;public:Foo(long l) : L(l) {  }static void* operator new(size_t size){ return myAlloc.allocate(size); }static void  operator delete(void* pdead, size_t size){return myAlloc.deallocate(pdead, size);}};allocator Foo::myAlloc;class Goo {public:complex<double> c;string str;static allocator myAlloc;public:Goo(const complex<double>& x) : c(x) {  }static void* operator new(size_t size){ return myAlloc.allocate(size); }static void  operator delete(void* pdead, size_t size){return myAlloc.deallocate(pdead, size);}};allocator Goo::myAlloc;//-------------void test_static_allocator_3(){cout << "\n\n\ntest_static_allocator().......... \n";{Foo* p[100];cout << "sizeof(Foo)= " << sizeof(Foo) << endl;for (int i = 0; i<23; ++i) {    //23,任意數, 隨意看看結果p[i] = new Foo(i);cout << p[i] << ' ' << p[i]->L << endl;}//Foo::myAlloc.check();for (int i = 0; i<23; ++i) {delete p[i];}//Foo::myAlloc.check();}{Goo* p[100];cout << "sizeof(Goo)= " << sizeof(Goo) << endl;for (int i = 0; i<17; ++i) { //17,任意數, 隨意看看結果p[i] = new Goo(complex<double>(i, i));cout << p[i] << ' ' << p[i]->c << endl;}//Goo::myAlloc.check();for (int i = 0; i<17; ++i) {delete p[i];}//Goo::myAlloc.check();}}
} //namespaceint main(void)
{jj09::test_static_allocator_3();return 0;
}

之前的几个版本都是在类的内部重载了operator new()和operator delete()函数这些版本都将分配内存的工作放在这些函数中,但现在的这个版本将这些分配内存的操作放在了allocator类中,这就渐渐接近了标准库的方法。从上面的代码中可以看到,两个类Foo和Goo中operator new()和operator delete()函数等很多部分代码类似,于是可以使用宏来将这些高度相似的代码提取出来,简化类的内部结构,但最后达到的结果是一样的:


global allocator

上面我们自己定义的分配器使用了一条链表来管理内存的,但标准库却用了多条链表来管理,这在后续会详细介绍:

new handler


如果用户调用new申请一块内存,如果由于系统原因或者申请内存过大导致申请失败,这时将抛出异常,在一些老的编译器中可能会直接返回0,可以参考上图右边代码,当无法分配内存时,operator new()函数内部将调用_calnewh()函数,这个函数通过左边的typedef传入,看程序员是否能自己写一个handler处理函数来处理该问题。一般有两个选择,让更多的Memory可用或者直接abort()或exit()。下面是测试的一个结果:

该部分中自定义了处理函数noMoreMemory()并通过set_new_handler来注册该处理函数,在BCB4编译器中会调用到自定义的noMoreMemory()函数,但在右边的dev c++中却没有调用,这个还要看平台。

=default和=delete


在C++里面,只有拷贝构造函数,拷贝复制函数,析构函数有默认版本

[侯捷C++](内存管理)相关推荐

  1. 侯捷 C++内存管理

    侯捷 C++内存管理课程目录: 第一讲:基础知识/工具 第二讲:malloc/free 第三讲:std::allocator 第四讲:other allocator 第五讲:loki::allocat ...

  2. 侯捷 C++内存管理 (一)

    本篇记录 <侯捷 C++内存管理 >,整理各节的要点,以备查阅 1.Overview 2.内存分配的每一层面 3.四个层面的基本用法 1).对比一下: 4.基本构件之一newdelete ...

  3. [侯捷 C++内存管理] 标准分配器实现

    [侯捷 C++内存管理] 标准分配器实现 文章目录 [侯捷 C++内存管理] 标准分配器实现 VC6 标准分配器之实现 VC6 malloc() VC6 allocator BC5 标准库分配器之实现 ...

  4. 侯捷C++视频资源全集 | 百度网盘下载

    之前给群里的小伙伴推荐了侯捷老师, 结果他学着学着发现b站侯捷老师的视频都被下掉了. 让我想起了我当年学c++的时候在b站看候捷老师视频的那些日子,每多看一点,就会多一点恍然大悟的感觉哈哈. 辛亏我的 ...

  5. 侯捷 C++系列课程视频 | 侯捷 C++ STL 视频

    侯捷C++课程视频课程一直都被看过的同学广为推荐,今天晚上发现 B 站关于侯捷老师的 C++ 课程视频几乎全部被下架了. 所以在网上找了下资源,找到了一套还算比较齐全的. 包含了 : 侯捷C++ 标准 ...

  6. 侯捷C++八部曲笔记(五、内存管理)

    侯捷C++八部曲笔记(五.内存管理) 表达式new.delete 基本用法 array new.array delete replacement new ----------------------- ...

  7. 【学习笔记】C++内存管理-侯捷

    文章目录 C++内存管理:从平地到万丈高楼 一.primitives-基础工具 1.内存分配的四个层面 2.new/delete表达式 2.1 new表达式 2.2 delete表达式 学习笔记源自博 ...

  8. 【C++内存管理侯捷】---学习笔记(下)malloc/free,loki allocator,other issue

    第三讲 malloc/free 3.1 VC6和VC10的malloc比较 malloc/free是C层面的函数 源代码是来自于VC6.0:因为比较复杂,因此以图为主,辅以部分源代码,理解其运行机制: ...

  9. C++__堆,栈与内存管理

    C++__堆,栈与内存管理 1.什么是栈,什么是堆 具体可以看这篇:转载[C]堆区和栈区的区别 2.静态变量,全局变量,堆,栈生命周期 3.new与delete的动作 4.动态分配的内存计算 参考:& ...

最新文章

  1. Splunk组件和架构详解
  2. ehcache_缓存基础
  3. rpm 安装ipython
  4. matPlotLib绘制决策树
  5. Date、DateFormat、Calendar日期类
  6. python打印99乘法表_Python 实例:九九乘法表
  7. linux oracle 01157,Oracle数据库启动时出现ORA-01157和ORA-01110问题
  8. 多丽丝·莱辛获今年诺贝尔文学奖
  9. html如何查看文档,查看文档
  10. Linux系统间文件双向同步搭建Unison版
  11. 深度学习算法 第四期
  12. 文件管理英文html,Directory Opus
  13. STM32工作笔记0037---STM32时钟系统精讲-M3
  14. 大数据可视化平台有什么优势
  15. oracle 能被2整除_整除专题基础篇 “刀法四式”
  16. 计算机excel表格教程高级筛选6,Excel高级筛选
  17. 手把手教你用python实现一个简单用户管理系统
  18. 慕课网-安卓攻城狮视频学习及练习(一)
  19. 如何在UNIX系统下操作软盘
  20. POJ-3426-0-1背包Charm Bracelet

热门文章

  1. apt 连接夜神模拟器,系统提示:nox_apt.exe已停止运行 的解决办法
  2. dll文件丢失怎么恢复?文件恢复妙招分享
  3. STM32单片机汽车儿童安全系统高温一氧化碳报警WIFI-APP报警
  4. rc4算法安全漏洞_(转)SSL/TLS 漏洞“受戒礼”,RC4算法关闭
  5. 用python绘制熊猫图案_利用Python进行数据分析_Pandas_绘图和可视化_Matplotlib
  6. 【kibana】 kibana报错内存溢出 CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
  7. 计算机系统实验:模型机(十六)另一篇?
  8. 关键词提取算法之RAKE
  9. 计算机组织与结构poc,CPU漏洞原理详解以及POC代码分享
  10. ERROR StreamingContext: Error starting the context, marking it as stopped