C++进阶——内存管理(二)
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的原理:
- 调用void * operator new (size_t size)函数申请T类型大小的堆内存空间
- 调用 T 的构造构造函数,完成对象的构造
以下表达式中delete 做了两件事为:将指针p指向的对象析构,然后释放内存
delete p;
delete的原理:
- 调用对象的析构函数,完成对象中的资源释放
- 调用void operator delete (size_t size) 函数释放对象的空间
2. new [ ] 与 delete[ ]
这是侯杰大佬C++内存管理课程中的文档(其中涉及到delete[] 的使用,对于析构无实在操作时,[]的使用有或者无都是无影响的,但是对于object的对象,必须要使用 [ ] ):
对于对象数组的内存布局(如:new T[3]; )不同于单一的对象内存布局,更确切的说,对象数据的内存通常还包含”数据大小的记录“,以便delete知道需要调用多少次析构函数,它的布局可能是这样:
new T[N]的原理:
- 调用 void * operator new [](sizeof(T)*N + 4)函数,在内部实际调用void * operator new (size_t size)函数完成N个独享的空间申请
- 将空间的前四个字节填充对象的个数
- 在申请的空间上执行N次构造函数
delete[] 的原理:
- 从一个对象数组的前四个字节中获取对象的个数N
- 调用N个对象的析构函数,释放资源
- 调用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++进阶——内存管理(二)相关推荐
- 操作系统概念学习笔记 16 内存管理(二) 段页
操作系统概念学习笔记 16 内存管理 (二) 分页(paging) 分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的.分页避免了将不同大小的内存块匹配到交换空间上(前面叙述的内存管 ...
- C++进阶——内存管理(一)
[导语] 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不 ...
- Linux任督二脉之内存管理(二) PPT
五节课的第二节课-内存的动态申请和释放 * slab.kmalloc/kfree./proc/slabinfo和slabtop * 用户空间malloc/free与内核之间的关系 * mallopt ...
- Linux内存管理二(页表)
1.综述 用来将虚拟地址空间映射到物理地址空间的数据结构称为页表,即页表用于建立用户进程的虚拟地址空间和系统物理内存(内存.页帧)之间的关联 实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空 ...
- 高端内存映射之kmap持久内核映射--Linux内存管理(二十)
日期 内核版本 架构 作者 GitHub CSDN 2016-09-29 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理 在内 ...
- BOOST内存管理(二) --- boost::pool
Boost库的pool提供了一个内存池分配器,用于管理在一个独立的.大的分配空间里的动态内存分配.Boost库的pool主要适用于快速分配同样大小的内存块,尤其是反复分配和释放同样大小的内存块的情况. ...
- 内存管理(二) - MRC关键字解读
本篇主要学习以下几个知识点 alloc/reatin/release/dealloc 理解 autorelease 理解 autorelease GUN 实现 autorelease 苹果 实现 原文 ...
- iphone内存管理(二)
(2)尽量避免使用autorelease 虽然autorelease非常简单有用,但是在iphone上一定要谨慎使用,毕竟iphone内存相当有限.autorelease可能会导致直接的隐型内存泄露. ...
- matlab内存管理(二)
转自:http://hi.baidu.com/bi%CB%AB%C9%FA%BB%A8/blog/item/5ab86c38ac2f45e715cecbab.html 1,确保内存的连续性 Matla ...
最新文章
- 飞书在线文档 美誉度国内最佳!一起来围观~
- 论坛报名 | 李开复张亚勤陆奇共论AI时代的创业
- ipad html 自定义裁剪,canvas裁剪clip()函数的具体使用
- 自动驾驶技术的终局,可能将无限期推迟来到
- ecshop管理找不到index.php,前台出现找不到这样的目录,打不开某文件的提示
- 使用jquery.qrcode生成二维码(转)
- QString包含中文时与char *转换
- glyphicons-halflings-regular.woff2 文件 404
- 阿里云王伟民:数据库的策略与思考
- [zhuan]asp.net程序性能优化的七个方面 (c#(或vb.net)程序改进)
- linux卸载java rpm_详解Linux中查看jdk安装目录、Linux卸载jdk、rpm命令、rm命令参数...
- 弹性法计算方法的mck法_经济学原理中讲到的中点法计算需求弹性是怎么回事
- 读书笔记——《灰度决策:如何处理复杂、棘手、高风险的难题》
- 有哪些适合中小商户的仓库管理软件,走一波
- xp系统进不去2008服务器共享,xp系统设置访问Server 2008R2的共享不输入密码的方法...
- 期货基础知识(竞价,定价,保证金计算)
- 无法回避的现实问题:“亲对象”也要明算账?
- 基于Java的qq截图工具(毕业设计含源码)
- 从源码角度上探索AdapterViewFlipper怎么实现广告栏的垂直自动滚动
- 51单片机和315M无线发射模块编码与解码