C++ memory primitives

分配 释放 类型 可否重载
malloc free C函数 不可
new delete C++表达式 不可
::operator new ::operator delete C++函数
allocator<T>::allocate allocator<int>::deallocate C++标准库 可自由设计并以之搭配任何容器

基本的使用测试(vs2015):

#include <iostream>
#include <memory>
using namespace std;class Sample {
public:Sample() {}
};int main()
{void* p1 = malloc(512);free(p1);Sample* sample = new Sample();delete sample;void* p2 = ::operator new(512);//512字节::operator delete(p2);allocator<int> mAllocator = allocator<int>();int* p3 = mAllocator.allocate(5);mAllocator.deallocate(p3,5);return 0;}

new expression

1. new 与 delete

以下表达式,我们知道new 做了两件事:先是分配内存,然后调用构造函数;下面来看下他的执行过程

Sample* p = new Sample();

执行过程:

a. void mem = operator new( sizeof(Sample));
b. p = static_cast<Sample*>(mem);
c. p->Sample::Sample();

new T的原理:

  1. 调用void * operator new (size_t size)函数申请T类型大小的堆内存空间
  2. 调用 T 的构造构造函数,完成对象的构造 

以下表达式中delete 做了两件事为:将指针p指向的对象析构,然后释放内存

delete p;

delete的原理:

  1. 调用对象的析构函数,完成对象中的资源释放
  2. 调用void operator delete (size_t size) 函数释放对象的空间

2. new [ ] 与 delete[ ]

这是侯杰大佬C++内存管理课程中的文档(其中涉及到delete[] 的使用,对于析构无实在操作时,[]的使用有或者无都是无影响的,但是对于object的对象,必须要使用 [ ] ):

对于对象数组的内存布局(如:new T[3]; )不同于单一的对象内存布局,更确切的说,对象数据的内存通常还包含”数据大小的记录“,以便delete知道需要调用多少次析构函数,它的布局可能是这样:

new T[N]的原理:

  1. 调用 void * operator new [](sizeof(T)*N + 4)函数,在内部实际调用void * operator new (size_t size)函数完成N个独享的空间申请
  2. 将空间的前四个字节填充对象的个数
  3. 在申请的空间上执行N次构造函数

delete[] 的原理:

  1. 从一个对象数组的前四个字节中获取对象的个数N
  2. 调用N个对象的析构函数,释放资源
  3. 调用void operator delete[] (size_t size);函数释放N个对象的空间,函数内部通过调用void operator delete (size_t size) 函数释放单个对象的空间。

Operator new 函数和 Operator delete 函数

在上面new/delete、new[]/delete[]中使用到了operator new/operator delete、 operator new[] / operator delete[] 的相关操作,来看下内存分配的实现源码(gcc-4.9.0\gcc-4.9.0\libstdc++-v3\libsupc++),最终的实现都是利用malloc 和 free:

void *  operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
  void *p;

/* malloc (0) is unpredictable; avoid it.  */
  if (sz == 0)
    sz = 1;
  p = (void *) malloc (sz);
  while (p == 0)
    {
      new_handler handler = std::get_new_handler ();
      if (! handler)
    _GLIBCXX_THROW_OR_ABORT(bad_alloc());
      handler ();
      p = (void *) malloc (sz);
    }

return p;
}

void
operator delete(void* ptr) _GLIBCXX_USE_NOEXCEPT
{std::free(ptr);
}
void*
operator new[] (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{return ::operator new(sz);
}
void
operator delete[] (void *ptr) _GLIBCXX_USE_NOEXCEPT
{::operator delete (ptr);
}

placement new

定位new(placement new):表达式在已分配的原始内存中初始化一个对象,它与new的其他版本的不同之处在于,它不分配内存;它接收指向已分配但未构造内存的指针,并在该内存中初始化一个对象。

new { place_adress }  type
       new { place_adress }  type { initializer-list }

重载operator new、operator delete函数

以下测试程序(vs2015)只是简单的重载处理,并没有注重对内存的优化处理。

1. operator new、operator delete以及operator new[]、operator delete[] 可以在类中进行特定,接管自己内部的内存管理,这也是一种内存优化的方式。

#include <iostream>
#include <memory>
using namespace std;class Sample {
public:Sample() {cout << "Sample::Sample()" << endl;}~Sample() {cout << "Sample::~Sample()" << endl;}Sample(int i){cout << "Sample::Sample(int)" << endl;}//这里需要静态函数, 无法通过对象调用该函数static void *operator new(size_t size) {Sample *p = (Sample*)malloc(size);cout << "Sample new()" << endl;return p;}static void operator delete(void* pdead, size_t) {cout << "Sample delete()" << endl;free(pdead);}static void* operator new[](size_t size) {Sample* p = (Sample*)malloc(size);cout << "Sample new()" << endl;return p;}void operator delete[](void * pdead, size_t) {cout << "Sample delete()" << endl;free(pdead);}
};void local_operator_new_test()
{Sample* sample = new Sample();delete sample;Sample* arr = new Sample[5];delete [] arr;
}int main()
{local_operator_new_test();return 0;
}

执行结果如下,通过重载,可以更清楚的认识C++new 以及delete的工作原理:

2. 重载全局的operator new 、operator delete

对上面的测试程序进行修改,重载全局的operator new。。。等操作:

#include <iostream>
#include <memory>
using namespace std;/*
* 重载全局的operator new、 operator delete
*/void* myAlloc(size_t size) {return malloc(size);
}void myFree(void* ptr) {return free(ptr);
}void* operator new(size_t size) {cout << "global new()" << endl;return myAlloc(size);
}void* operator new[](size_t size) {cout << "global new[]()" << endl;return myAlloc(size);
}void operator delete(void* ptr) noexcept {cout << "global delete()" << endl;return myFree(ptr);
}void operator delete[](void* ptr) noexcept {cout << "global delete[]()" << endl;myFree(ptr);
}class Sample {
public:Sample() {cout << "Sample::Sample()" << endl;}~Sample() {cout << "Sample::~Sample()" << endl;}Sample(int i){cout << "Sample::Sample(int)" << endl;}//这里需要静态函数, 无法通过对象调用该函数static void *operator new(size_t size) {Sample *p = (Sample*)malloc(size);cout << "Sample new()" << endl;return p;}static void operator delete(void* pdead, size_t) {cout << "Sample delete()" << endl;free(pdead);}static void* operator new[](size_t size) {Sample* p = (Sample*)malloc(size);cout << "Sample new()" << endl;return p;}void operator delete[](void * pdead, size_t) {cout << "Sample delete()" << endl;free(pdead);}
};void new_primitives_test()
{void* p1 = malloc(512);free(p1);Sample* sample = new Sample();delete sample;void* p2 = ::operator new(512);//512字节::operator delete(p2);allocator<int> mAllocator = allocator<int>();int* p3 = mAllocator.allocate(5);mAllocator.deallocate(p3, 5);
}void local_operator_new_test()
{Sample* sample = new Sample();delete sample;Sample* arr = new Sample[5];delete [] arr;
}void global_operator_new_test()
{Sample* sample = ::new Sample();::delete sample;Sample* arr = ::new Sample[5];::delete[] arr;
}int main()
{//new_primitives_test();//local_operator_new_test();global_operator_new_test();return 0;}

输出结果:

placement new 的重载

  • 可以重载class member operator new(), 写出很多版本,前提是每个版本的声明都必须具有独特的参数列,其中第一参数必须是size_t, 其余参数以new指定的placement arguments为初值;出现于new()小括号内的便是所谓的placement arguments
  • 可以重载class member operator delete(), 但它们绝不会被delete调用。只有当new所谓的ctor抛出exception,才会调用这些重载版本的operator delete();(不同的版本实现可能不一样,也有可能并没执行)
  • operator new和placement new两个都是重载operator new()这个函数签名;但是operator new的参数是operator new(size_t size),而placement new的参数是operator new(size_t size, void* ptr);
  • new(p)就是placement new被称为定点new, 而功能是并不分配内存,只是在以分配原始内存上初始化对象。
#include <iostream>
using namespace std;class Bad {
};class Foo {public:Foo() {cout << "Foo:::Foo()" << endl;}Foo(int) {cout << "Foo::Foo(int)" << endl;throw Bad();}//一般的operator new()的重载void* operator new(size_t size) {cout << "operator new(size_t)" << endl;return malloc(size);}//标准库已提供的placement new()的重载//当ctor发出异常,下面对应的operator delete就会被调用(其作用就是释放相应的内存)void* operator new(size_t size, void * start) {cout << "operator nen(size_t, void*)" << endl;return start;}void* operator new(size_t size, long extra) {cout << "operator new(size_t, long)" << endl;return malloc(size + extra);}void* operator new(size_t size, long extra, char init) {cout << "operator new(size_t, long, char)" << endl;return malloc(size + extra);}//相应的delete重载函数void operator delete(void*, size_t size) {cout << "operator delete(void*, size_t)" << endl;}void operator delete(void*, void*) {cout << "operator delete(void*, void*)" << endl;}void operator delete(void*, long) {cout << "operator delete(void*, long)" << endl;}void operator delete(void*, long, char) {cout << "operator delete(void*, long, char)" << endl;}private:int m_i;
};int main(void)
{Foo start;   //对应new expressioncout << "-------------------------------------------------------" << endl;Foo* p1 = new Foo;  //对应operator new()+默认构造delete p1;cout << "-------------------------------------------------------" << endl;Foo* p2 = new (&start)Foo;   //对应operator new(size_t, void*)+默认构造delete p2;cout << "-------------------------------------------------------" << endl;Foo* p3 = new(100)Foo;  //对应operator new(size_t, long)+默认构造delete p3;cout << "-------------------------------------------------------" << endl;Foo* p4 = new(100, 'a')Foo();  //对应operator new(size_t, long, char)+默认构造delete p4;cout << "-------------------------------------------------------" << endl;//构造函数内部抛出异常,会调相应的operator delete()Foo* p5 = new(100)Foo(1);  //对应operator new(size_t, long)+Foo::Foo(int)delete p5;cout << "-------------------------------------------------------" << endl;Foo* p6 = new(100, 'a')Foo(1);  //对应operator new(size_t, long, char)+Foo::Foo(int)delete p6;cout << "-------------------------------------------------------" << endl;Foo* p7 = new(&start)Foo(1);  //对应operator new(size_t, char*)+Foo::Foo(int)delete p7;cout << "-------------------------------------------------------" << endl;Foo* p8 = new Foo(1);  //对应operator new(size_t) + Foo::Foo(int)delete p8;cout << "-------------------------------------------------------" << endl;return 0;
}

输出结果如下(很显然,在vs2015上在构造函数中抛出异常,并未执行delete操作):

对象内存池实例

下面将通过四个版本一步步优化来实现我们自己的内存分配器。

1. 对象内存池第一版

通过在类中重载operator new 和 operator delete 来对内存进行管理,在VS2015 上的测试结果来看,详细见代码注释:

#include <iostream>
using namespace std;/*******************************************************
*
* Screen
* 使用标准库的operator new 和 operator delete
* 来进行内存的分配与使用
*
*******************************************************/
class Screen {
public:Screen(long x) : i(x) {}~Screen() {}int get() { return i; }private:Screen* next;static Screen* freeStore;static const int screenChunk;
private:long i;
};/*******************************************************
*
* Screen_override
* 在类中重写operator new 和 operator delete
* 通过内存池的方式来进行内存的分配与管理
* 目的:
*    a. 减少malloc的使用次数,提高分配速度
*    b. 降低空间浪费率(cookie)
*
*******************************************************/
class Screen_override
{
public:Screen_override(int x) : i(x) {}~Screen_override() {}int get() { return i; }//重载static void* operator new(size_t);static void operator delete(void*, size_t);
private:Screen_override* next;static Screen_override* freeStore;static const int screenChunk;
private:int i;
};
Screen_override* Screen_override::freeStore = 0;  //头结点
const int Screen_override::screenChunk = 24;/*
*Screen_override 在类中重载 operator new
* 逻辑是当freeStore空的时候分配24个
* 当freeStore不空的时候,从Chunk中取next
*/
void *Screen_override::operator new(size_t size) {  //size为元素大小,一次分配24个对象的大小Screen_override* p;if (!freeStore) {//linked list 是空的,申请一大块size_t chunk = screenChunk* size;// chunk = 24 * 8 = 40freeStore = p = reinterpret_cast<Screen_override*>(new char[chunk]);//将这一块分割成片,当做linked list串接起来for (; p != &freeStore[screenChunk - 1]; ++p) {p->next = p + 1;}p->next = 0;}//让p指向链表的第一个元素p = freeStore;//freeStroe执行下一个位置未使用的位置,在delete 可以看到实际的使用技巧freeStore = freeStore->next;return p;
}/*
将deleted object 插回到free list前端
*/
void Screen_override::operator delete(void* p, size_t) {//将释放的p放到链表的头部,和operator new 对应(static_cast<Screen_override*>(p))->next = freeStore;freeStore = static_cast<Screen_override*>(p);//更新freeStroe的位置}//测试不重载operator new,分配内存带cookie
void Screen_Test() {cout << endl << "Screen_Test Screen cookie" << endl;cout << "size of Screen: " << sizeof(Screen) << endl;int const N = 100;Screen* p[N];for (int i = 0; i < N; i++) {p[i] = new Screen(i);}for (int i = 0; i < 10; i++) {cout << p[i] << endl;}for (int i = 0; i < N; i++) {delete p[i];}return;
}//重载operator new, 分配内存不带cookie
void Screen_Override_Test(void)
{cout << endl << "Screen_Override_Test cookie: " << endl;//16字节cout << "size of Screen_override: " << sizeof(Screen_override) << endl;int  const N = 100;Screen_override* p[N];for (int i = 0; i < N; i++) {p[i] = new Screen_override(i);}for (int i = 0; i < 10; i++) {cout << p[i] << endl;}for (int i = 0; i < N; i++) {delete p[i];}return;
}int main(void)
{cout << "Screen_Test: " << endl;Screen_Test();cout << "--------------------------------" << endl;cout << "Screen_Override_Test: " << endl;Screen_Override_Test();return 0;
}

输出结果如下,可以看出通过重写operator new 和 operator delete 来进行内存管理,内存是连续的,减少了malloc的调用次数,在vs2015上输出的测试结果中Screen并没有cookie它的sizeof(Screen 依旧是 8),在侯捷大佬的课程中在GCC上测试,是由明显的区别:

2. 对象内存池第二版

在这个版本中使用内存管理的一个技巧:Embedded Pointer,相比于第一个版本,此时的指针Airplane* next 并不会造成多占用4个字节的数据量:

union {  //8个字节 Embedded Pointer
        AirplaneRep rep;
        Airplane* next;//内嵌指针
    };

#include <iostream>
#include <algorithm>
#include <cstring>using namespace std;class Airplane
{
public:Airplane() {}~Airplane() {}private:struct AirplaneRep {//总共8个字节unsigned long miles;  //4字节char type;  //1字节};
private:union {  //8个字节 Embedded PointerAirplaneRep rep;Airplane* next;//内嵌指针};public:unsigned long getMiles() { return rep.miles; }char getType() { return rep.type; }void set(unsigned long m, char t) {rep.miles = m;rep.type = t;}
public:static void* operator new(size_t size);static void operator delete(void* deadObj, size_t size);
private:static const int BLOCK_SIZE;static Airplane* headOfFreeList;
};
Airplane* Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;//重载
void* Airplane::operator new(size_t size) {if (size != sizeof(Airplane)) {  //继承发生就会有问题return ::operator new(size);}Airplane* p = headOfFreeList;if (p) {headOfFreeList = p->next;}else {Airplane* newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane)));for (int i = 1; i < BLOCK_SIZE - 1; i++)newBlock[i].next = &newBlock[i + 1];newBlock[BLOCK_SIZE - 1].next = 0;p = newBlock;headOfFreeList = &newBlock[1];}return p;
}//重载
void Airplane::operator delete(void* deadObj, size_t size) {if (!deadObj) return;if (size != sizeof(Airplane)) {  //继承情况::operator delete(deadObj);return;}Airplane* carcass = static_cast<Airplane*>(deadObj);carcass->next = headOfFreeList;headOfFreeList = carcass;
}int main(void)
{cout << "sizeof(unsigned long): " << sizeof(unsigned long) << endl;cout << sizeof(Airplane) << endl; //8字节size_t const N = 100;Airplane* p[N];for (size_t i = 0; i < N; i++) {p[i] = new Airplane;}p[1]->set(1000, 'A');p[2]->set(2000, 'B');p[3]->set(3000, 'C');for (int i = 0; i < 10; i++) {cout << p[i] << endl;}for (size_t i = 0; i < N; i++) {delete p[i];}return 0;
}

输出结果:

3. 对象内存池第三版

第三个版本是将内存的管理分离出来,通过my_alloctor来管理:

#include <iostream>
#include <complex>
using namespace std;class my_allocator {
private:struct obj {struct obj* next;  //Embedded pointer};
public:my_allocator() {}~my_allocator() {}public:void* allocate(size_t);void deallocate(void*, size_t);private:obj* freeStore = nullptr;const int CHUNK = 5;
};void* my_allocator::allocate(size_t size) {obj* p;if (!freeStore) {freeStore = p = static_cast<obj*>(malloc(CHUNK * size));for (int i = 0; i < (CHUNK - 1); i++) {p->next = (obj*)((char*)p + size);p = p->next;}p->next = nullptr;}p = freeStore;freeStore = freeStore->next;return p;
}void my_allocator::deallocate(void* p, size_t) {(static_cast<obj*>(p))->next = freeStore;freeStore = static_cast<obj*>(p);
}class Foo {
public:long L;string str;static my_allocator myAlloc;public:Foo(long l) : L(l) {}static void* operator new(size_t size) {return myAlloc.allocate(size);}static void operator delete(void* dead, size_t size) {return myAlloc.deallocate(dead, size);}
};
my_allocator Foo::myAlloc;  //静态成员变量一定需要在类声明之外定义class Goo {
public:complex<double> c;string str;static my_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);}
};
my_allocator Goo::myAlloc;int main(void)
{cout << "sizeof(my_allocator)" << sizeof(my_allocator) << endl;cout << "sizeof(string)" << sizeof(string) << endl;Foo* p[100];cout << "sizeof(Foo): " << sizeof(Foo) << endl;for (int i = 0; i < 10; i++) {p[i] = new Foo(i);cout << p[i] << ' ' << p[i]->L << endl;}for (int i = 0; i < 10; i++) {delete p[i];}Goo *Gp[100];cout << "sizeof(Goo)" << sizeof(Goo) << endl;cout << "sizeof(complex<double>)" << sizeof(complex<double>) << endl;for (int i = 0; i < 10; i++) {Gp[i] = new Goo(complex<double>(i, i + 1));cout << Gp[i] << ' ' << Gp[i]->str << endl;}for (int i = 0; i < 10; i++) {delete p[i];}return 0;
}

输出结果:

4. 对象内存池的第四个版本

第四个版本是在第三个版本的基础上,将类中的operator new、operator delete 利用宏来快速替换:

#include <iostream>
#include <complex>
using namespace std;/*
* 定义二个宏,方便在类中使用,这是一个经常使用的方法
*/
#define DECLARE_POOL_ALLOC() \public: \static void* operator new(size_t size) {return myAlloc.allocate(size);}\static void operator delete(void* p) {return myAlloc.deallocate(p, 0);} \public:\static my_allocator myAlloc;#define IMPLEMENT_POOL_ALLOC(class_name) \my_allocator class_name::myAlloc/*
* 内存管理类
*/
class my_allocator {public:struct obj {struct obj* next;};public:my_allocator() {}~my_allocator() {}
public:void* allocate(size_t);void deallocate(void* p, size_t);//private:obj* freeStore = nullptr;int const CHUNK = 5;
};void* my_allocator::allocate(size_t size) {cout << "allocating..." << endl;obj* p;if (!freeStore) {freeStore = p = static_cast<obj*>(malloc(size * CHUNK));for (int i = 1; i < CHUNK - 1; i++) {p->next = (obj*)((char*)p + size);p = p->next;}p->next = nullptr;}p = freeStore;freeStore = freeStore->next;return p;
}void my_allocator::deallocate(void* p, size_t) {cout << "deallocate..." << endl;static_cast<obj*>(p)->next = freeStore;freeStore = static_cast<obj*>(p);
}/*
*利用宏快速的构造类
*/
class Foo {DECLARE_POOL_ALLOC()//一个宏替换operator new等的实现
public:long L;string str;
public:Foo(long _L) : L(_L) {}
};
IMPLEMENT_POOL_ALLOC(Foo);class Goo {DECLARE_POOL_ALLOC()
public:complex<double> c;std::string str;
public:Goo(const complex<double>& l) : c(l) {}
};
IMPLEMENT_POOL_ALLOC(Goo);int main(void)
{/** 简单的测试*/Foo* p[5];for (int i = 0; i < 5; i++) {p[i] = new Foo(static_cast<long>(i));cout << p[i] << endl;}for (int i = 0; i < 5; i++) {delete p[i];}return 0;
}

输出结果:

C++进阶——内存管理(二)相关推荐

  1. 操作系统概念学习笔记 16 内存管理(二) 段页

    操作系统概念学习笔记 16 内存管理 (二) 分页(paging) 分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的.分页避免了将不同大小的内存块匹配到交换空间上(前面叙述的内存管 ...

  2. C++进阶——内存管理(一)

    [导语] 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不 ...

  3. Linux任督二脉之内存管理(二) PPT

    五节课的第二节课-内存的动态申请和释放 * slab.kmalloc/kfree./proc/slabinfo和slabtop * 用户空间malloc/free与内核之间的关系 * mallopt ...

  4. Linux内存管理二(页表)

    1.综述 用来将虚拟地址空间映射到物理地址空间的数据结构称为页表,即页表用于建立用户进程的虚拟地址空间和系统物理内存(内存.页帧)之间的关联 实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空 ...

  5. 高端内存映射之kmap持久内核映射--Linux内存管理(二十)

    日期 内核版本 架构 作者 GitHub CSDN 2016-09-29 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理 在内 ...

  6. BOOST内存管理(二) --- boost::pool

    Boost库的pool提供了一个内存池分配器,用于管理在一个独立的.大的分配空间里的动态内存分配.Boost库的pool主要适用于快速分配同样大小的内存块,尤其是反复分配和释放同样大小的内存块的情况. ...

  7. 内存管理(二) - MRC关键字解读

    本篇主要学习以下几个知识点 alloc/reatin/release/dealloc 理解 autorelease 理解 autorelease GUN 实现 autorelease 苹果 实现 原文 ...

  8. iphone内存管理(二)

    (2)尽量避免使用autorelease 虽然autorelease非常简单有用,但是在iphone上一定要谨慎使用,毕竟iphone内存相当有限.autorelease可能会导致直接的隐型内存泄露. ...

  9. matlab内存管理(二)

    转自:http://hi.baidu.com/bi%CB%AB%C9%FA%BB%A8/blog/item/5ab86c38ac2f45e715cecbab.html 1,确保内存的连续性 Matla ...

最新文章

  1. 飞书在线文档 美誉度国内最佳!一起来围观~
  2. 论坛报名 | 李开复张亚勤陆奇共论AI时代的创业
  3. ipad html 自定义裁剪,canvas裁剪clip()函数的具体使用
  4. 自动驾驶技术的终局,可能将无限期推迟来到
  5. ecshop管理找不到index.php,前台出现找不到这样的目录,打不开某文件的提示
  6. 使用jquery.qrcode生成二维码(转)
  7. QString包含中文时与char *转换
  8. glyphicons-halflings-regular.woff2 文件 404
  9. 阿里云王伟民:数据库的策略与思考
  10. [zhuan]asp.net程序性能优化的七个方面 (c#(或vb.net)程序改进)
  11. linux卸载java rpm_详解Linux中查看jdk安装目录、Linux卸载jdk、rpm命令、rm命令参数...
  12. 弹性法计算方法的mck法_经济学原理中讲到的中点法计算需求弹性是怎么回事
  13. 读书笔记——《灰度决策:如何处理复杂、棘手、高风险的难题》
  14. 有哪些适合中小商户的仓库管理软件,走一波
  15. xp系统进不去2008服务器共享,xp系统设置访问Server 2008R2的共享不输入密码的方法...
  16. 期货基础知识(竞价,定价,保证金计算)
  17. 无法回避的现实问题:“亲对象”也要明算账?
  18. 基于Java的qq截图工具(毕业设计含源码)
  19. 从源码角度上探索AdapterViewFlipper怎么实现广告栏的垂直自动滚动
  20. 51单片机和315M无线发射模块编码与解码

热门文章

  1. grammarly 如何快速退款
  2. 数字藏品就是NFT吗?
  3. 如何用计算机提高英语水平,英语对计算机专业的重要性及如何提高英语水平
  4. 企业 百家号如何申请才是最划算的?
  5. matlab中字符串的替换,用for循环中的regexprep替换字符串? (MATLAB)
  6. 【笔记】《软件测试(第2版)》-周元哲
  7. 2021云南省高考成绩几点查询,2021云南高考成绩什么时候几点可以查
  8. 2021 最新Android知识体系
  9. 视频转换、视频压缩、录屏等工具合集:迅捷视频工具箱
  10. 邮箱大师添加outlook2010 方法 图文详解