C和C++笔记:动态内存管理

4.1 C内存管理:

C标准内存管理函数:

(1).malloc(size_t size):分配size个字节,并返回一个指向分配的内存的指针。分配的内存未被初始化为一个已知值。

(2).aligned_alloc(size_t alignment, size_t size):为一个对象分配size个字节的空间,此对象的对齐方式是alignment指定的。alignment的值必须是实现支持的一种有效的对齐方式,size的值必须是alignment的整数倍,否则行为就是未定义的。此函数返回一个指向分配的空间的指针,如果分配失败,则返回一个空指针。

(3).realloc(void* p, size_t size):将p所指向的内存块的大小改为size个字节。新大小和旧大小中较小的值那部分内存所包含的内容不变,新分配的内存未作初始化,因此将有不确定的值。如果内存请求不能被成功分配,那么旧的对象保持不变,而且没有值被改变。如果p是一个空指针,则该调用等价于malloc(size);如果size等于0,则该调用等价于free(p),但这种释放内存的用法应该避免。

(4).calloc(size_t nmemb, size_t size):为数组分配内存,(该数组共有nmemb个元素,每个元素的大小为size个字节)并返回一个指向所分配的内存的指针。所分配的内存的内容全部被设置为0。

内存分配函数返回一个指向分配的内存的指针,这块内存是按照任何对象类型恰当地对齐的,如果请求失败,则返回一个空指针。连续调用内存分配函数分配的存储空间的顺序和邻接是不确定的。所分配的对象的生存期从分配开始,到释放时结束。返回的指针指向所分配的空间的起始地址(最低字节地址)。

free(void* p):释放由p指向的内存空间,这个p必须是先前通过调用aligned_alloc、malloc、calloc或realloc返回的。如果引用的内存不是被这些函数之一分配的或free(p)此前已经被调用过,将会导致未定义行为。如果p是一个空指针,则不执行任何操作。

由C内存分配函数分配的对象有分配存储期限。存储期限是一个对象的属性,它定义了包含该对象存储的最低潜在生存期。这些对象的生存期并不限于创建它的范围内,因此,如果在一个函数内调用malloc,那么在该函数返回后,已分配的内存仍然存在。

对齐:完整的对象类型有对齐(alignment)要求,这种要求对可以分配该类型对象的地址施加限制。对齐是实现定义的整数值,它表示可以在一个连续的地址之间分配给指定的对象的字节数量。对象类型规定了每一个该类型对象的对齐要求。消除对齐要求,往往需要生成代码进行跨字边界域访问,或从更慢的奇数地址访问,从而减慢内存访问。

完整的对象(complete object):对象中可以包含其它对象,被包含的对象称为子对象(subobject)。子对象可以是成员子对象、基类子对象,或数组元素。如果一个对象不是任何其它对象的子对象,那么它被称为一个完整的对象。

对齐有一个从较弱的对齐到较强的对象(或更严格的对齐)的顺序。越严格的对齐,其对齐值越大。一个地址满足某一种对齐要求,也满足任何更弱的有效对齐要求。char、signed char、unsigned char类型的对齐要求最弱。对齐表示为size_t类型的值。每个有效对齐值都是2的一个非负整数幂。有效的对齐包括基本类型的对齐,加上额外的一组可选的实现定义的值。

基本对齐(fundamental alignment)小于或等于在所有上下文中由编译器支持的最大对齐。max_align_t类型的对齐与在所有上下文中由编译器支持的对齐大小相同。扩展对齐(extended alignment)大于max_align_t类型的对齐。一个具有扩展对齐要求的类型也称为超对齐(overaligned)类型。每个超对齐类型,要么是一个结构或联合类型,其中一个成员已应用扩展对齐,要么它包含这样的类型。如果实现支持,可以使用aligned_alloc函数分配比正常更严格的对齐的内存。如果一个程序要求比alignof(max_align_t)更大的对齐,那么这个程序是不可移植的,因为对超对齐类型的支持是可选的。

void test_aligned_alloc()
{
const int arr_size = 11;
// 分配16字节对齐的数据
#ifdef _MSC_VER
float* array = (float*)_aligned_malloc(16, arr_size * sizeof(float));
#else
float* array = (float*)aligned_alloc(16, arr_size * sizeof(float));
#endif
auto addr = std::addressof(array);
fprintf(stdout, "pointer addr: %p ", addr);

fprintf(stdout, "char alignment: %d, float alignment: %d, max_align_t alignment: %d ",
alignof(char), alignof(float), alignof(max_align_t));
}
在C标准中引入_Alignas关键字和aligned_alloc函数的主要理由是支持单指令多数据(SIMD)计算。

4.2 常见的C内存管理错误:常见的与内存管理相关的编程缺陷包括:初始化错误、未检查返回值、对空指针或无效指针解引用、引用已释放的内存、对同一块内存释放多次、内存泄漏和零长度分配。

初始化错误:由malloc函数返回的空间中的值是不确定的。一个常见的错误是不正确地假设malloc把分配的内存的所有位都初始化为零。

// 读取未初始化的内存
void test_memory_init_error()
{
// 初始化大的内存块可能会降低性能并且不总是必要的.
// C标准委员会决定不需要malloc来初始化这个内存,而把这个决定留给程序员
int n = 5;
int* y = static_cast<int*>(malloc(n * sizeof(int)));
int A[] = {1, 2, 3, 4, 5};

for (int i = 0; i < n; ++i) {
y[i] += A[i];
}

std::for_each(y, y+n, [](int v) { fprintf(stdout, "value: %d ", v); });
free(y);
}
不要假定内存分配函数初始化内存。不要引用未初始化的内存。

清除或覆写内存通常是通过调用C标准的memset函数来完成的。遗憾的是,如果不在写后访问内存,编译器优化可能会默默地删除对memset函数的调用。

未检查返回值:内存分配函数的返回值表示分配失败或成功。如果请求的内存分配失败,那么aligned_alloc、calloc、malloc和realloc函数返回空指针。

// 检查malloc的返回值
int* test_memory_return_value()
{
// 如果不能分配请求的空间,那么C内存分配函数将返回一个空指针
int n = 5;
int* ptr = static_cast<int*>(malloc(sizeof(int) * n));
if (ptr != nullptr) {
memset(ptr, 0, sizeof(int) * n);
} else {
fprintf(stderr, "fail to malloc ");
return nullptr;
}

return ptr;
}
Null或无效指针解引用:用一元操作符”*”解引用的指针的无效值包括:空指针、未按照指向的对象类型正确对齐的地址、生存期结束后的对象的地址。

空指针的解引用通常会导致段错误,但并非总是如此。许多嵌入式系统有映射到地址0处的寄存器,因此覆写它们会产生不可预知的后果。在某些情况下,解引用空指针会导致任意代码的执行。

引用已释放内存:除非指向某块内存的指针已设置为NULL或以其它方式被覆写,否则就有可能访问已被释放的内存。

// 引用已释放内存
void test_memory_reference_free()
{
int* x = static_cast<int*>(malloc(sizeof(int)));
*x = 100;
free(x);
// 从已被释放的内存读取是未定义的行为
fprintf(stderr, "x: %d ", *x);
// 写入已经被释放的内存位置,也不大可能导致内存故障,但可能会导致一些严重的问题
*x = -100;
fprintf(stderr, "x: %d ", *x);
}
从已被释放的内存读取是未定义的行为,但在没有内存故障时几乎总能成功,因为释放的内存是被内存管理器回收的。然而,并不保证内存的内容没有被篡改过。虽然free函数调用通常不会擦除内存,但内存管理器可能使用这个空间的一部分来管理释放或未分配的内存。如果内存块已被重新分配,那么其内容可能已经被全部替换。其结果是,这些错误可能检测不出来,因为内存中的内容可能会在测试过程中被保留,但在运行过程中被修改。

写入已经被释放的内存位置,也不太可能导致内存故障,但可能会导致一些严重的问题。如果该块内存已被重新分配,程序员就可以覆写此内存,一个内存块是专门(dedicated)为一个特定的变量分配的,但在现实中,它是被共享(shared)的。在这种情况下,该变量中包含最后一次写入的任何数据。如果那块内存没有被重新分配,那么写入已释放的块可能会覆写并损坏内存管理器所使用的数据结构。

多次释放内存:最常见的场景是两次释放(double-free)。这个错误是危险的,因为它会以一种不会立即显现的方式破坏内存管理器中的数据结构。

// 多次释放内存
void test_memory_multi_free()
{
int* x = static_cast<int*>(malloc(sizeof(int)));
free(x);
// 多次释放相同的内存会导致可以利用的漏洞
free(x);
}
内存泄漏:当动态分配的内存不再需要后却没有被释放时,就会发生内存泄漏。

零长度分配:C标准规定:如果所要求的空间大小是零,其行为是实现定义的:要么返回一个空指针,要么除了不得使用返回的指针来访问对象以外,行为与大小仿佛是某个非零值。

// 零长度分配:不要执行零长度分配
void test_memory_0_byte_malloc()
{
char* p1 = static_cast<char*>(malloc(0));
fprintf(stderr, "p1 pointer: %p ", std::addressof(p1)); // 是不确定的
free(p1);

p1 = nullptr;
char* p2 = static_cast<char*>(realloc(p1, 0));
fprintf(stderr, "p2 pointer: %p ", std::addressof(p2)); // 是不确定的
free(p2);

int nsize = 10;
char* p3 = static_cast<char*>(malloc(nsize));
char* p4 = nullptr;
// 永远不要分配0个字节
if ((nsize == 0) || (p4 = static_cast<char*>(realloc(p3, nsize))) == nullptr) {
free(p3);
p3 = nullptr;
return;
}

p3 = p4;
free(p3);
}
此外,要求分配0字节时,成功调用内存分配函数分配的存储量是不确定的。在内存分配函数返回一个非空指针的情况下,读取或写入分配的内存区域将导致未定义的行为。通常情况下,指针指向一个完全由控制结构组成的零长度的内存块。覆写这些控制结构损害内存所使用的数据结构。

realloc函数将释放旧对象,并返回一个指针,它指向一个具有指定大小的新对象。然而,如果不能为新对象分配内存,那么它就不释放旧对象,而且旧对象的值是不变的。正如malloc(0),realloc(p, 0)的行为是实现定义的。

不要执行零长度分配。

4.3 C++的动态内存管理:在C++中,使用new表达式分配内存并使用delete表达式释放内存。C++的new表达式分配足够的内存来保存所请求类型的对象,并可以初始化所分配的内存中的对象。

new表达式是构造一个对象的唯一方法,因为不可能显示地调用构造函数。分配的对象类型必须是一个完整的对象类型,并且不可以(例如)是一个抽象类类型或一个抽象类数组。对于非数组对象,new表达式返回一个指向所创建的对象的指针;对于数组,它返回一个指向数组初始元素的指针。new表达式分配的对象有动态存储期限(dynamic storage duration)。存储期限定义了该对象包含的存储的生存期。使用动态存储的对象的生存期,不局限于创建该对象所在的范围。

如果提供了初始化参数(即类的构造函数的参数,或原始整数类型的合法值),那么由new操作符所分配的内存被初始化。只有一个空的new-initializer()存在时,”普通的旧数据”(POD)类型的对象是new默认初始化(清零)的。这包括所有的内置类型。

void test_memory_new_init()
{
// 包括所有的内置类型
int* i1 = new int(); // 已初始化
int* i2 = new int; // 未初始化
fprintf(stdout, "i1: %d, i2: %d ", *i1, *i2);

// 就地new没有实际分配内存,所以该内存不应该被释放
int* i3 = new (i1) int;
fprintf(stdout, "i3: %d ", *i3);

delete i1;
delete i2;

// 通常情况下,分配函数无法分配存储时抛出一个异常表示失败
int* p1 = nullptr;
try {
p1 = new int;
} catch (std::bad_alloc) {
fprintf(stderr, "fail to new ");
return;
}
delete p1;

// 用std::nothrow参数调用new,当分配失败时,分配函数不会抛出一个异常,它将返回一个空指针
int* p2 = new(std::nothrow) int;
if (p2 == nullptr) {
fprintf(stderr, "fail to new ");
return;
}
delete p2;
}
就地new(placement new)是另一种形式的new表达式,它允许一个对象在任意内存位置构建。就地new需要在指定的位置有足够的内存可用。因为就地new没有实际分配内存,所以该内存不应该被释放。

分配函数:必须是一个类的成员函数或全局函数,不能在全局范围以外的命名空间范围中声明,并且不能把它在全局范围内声明为静态的。分配函数的返回类型是void*。

分配函数试图分配所请求的存储量。如果分配成功,则它返回存储块的起始地址,该块的长度(以字节为单位)至少为所要求的大小。分配函数返回的分配的存储空间内容没有任何限制。连续调用分配函数分配的存储的顺序、连续性、初始值都是不确定的。返回的指针是适当地对齐的,以便它可以被转换为任何具有基本对齐要求的完整对象类型的指针,并在之后用于访问所分配的存储中的对象或数组(直到调用一个相应的释放函数显示释放这块存储)。即使所请求的空间大小为零,请求也可能会失败。如果请求成功,那么返回值是一个非空指针值。如果一个指针是大小为零的请求返回的,那么对它解引用的效果是未定义的。C++在发起一个为零的请求时行为与C不同,它返回一个非空指针。

通常情况下,分配函数无法分配存储时抛出一个异常表示失败,这个异常将匹配类型为std::bad_alloc的异常处理器。

如果用std::nothrow参数调用new,当分配失败时,分配函数不会抛出一个异常。相反,它将返回一个空指针。

当一个异常被抛出时,运行时机制首先在当前范围内搜索合适的处理器。如果当前范围内没有这样的处理程序存在,那么控制权将由当前范围转移到调用链中的一个更高的块。这个过程一直持续,直到找到一个合适的处理程序为止。如果在任何级别中都没有处理程序捕获该异常,那么std::terminate函数被自动调用。默认情况下,terminate调用标准C库函数abort,它会突然退出程序。当abort被调用时,没有正常的程序终止函数调用发生,这意味着全局和静态对象的析构函数不执行。

在C++中,处理分配和分配失败的标准惯用法是资源获取初始化(Resource Acquisition Is Initializatiton, RAII)。RAII运用C++的对象生存期概念控制程序资源,如内存、文件句柄、网络连接、审计跟踪等。要保持对资源的跟踪,只要创建一个对象,并把资源的生存期关联到对象的生存期即可。这使你可以使用C++对象管理设施来管理资源。其最简单的形式是,创建一个对象,它在构造函数获得资源,并在其析构函数中释放资源。

class intHandle {
public:
explicit intHandle(int* anInt) : i_(anInt) {} // 获取资源
~intHandle() { delete i_; } // 释放资源

intHandle& operator=(const int i)
{
*i_ = i;
return *this;
}

int* get() { return i_; } // 访问资源

private:
intHandle(const intHandle&) = delete;
intHandle& operator=(const intHandle&) = delete;
int* i_;
};

// 资源获取初始化(Resource Acquisition Is Initialization, RAII)
void test_memory_arii()
{
intHandle ih(new int);
ih = 5;
fprintf(stdout, "value: %d ", *ih.get());

// 使用std::unique_ptr能完成同样的事情,而且更简单
std::unique_ptr<int> ip(new int);
*ip = 5;
fprintf(stdout, "value: %d ", *ip.get());
}
如果发生下列任何情况,new表达式抛出std::bad_array_new_length异常,以报告无效的数组长度:(1).数组的长度为负;(2).新数组的总大小超过实现定义的最大值;(3).在一个大括号初始化列表中的初始值设定子句数量超过要初始化的元素数量(即声明的数组的大小)。只有数组的第一个维度可能会产生这个异常,除第一个维度外的维度都是常量表达式,它们在编译时检查。

// 抛出std::bad_array_new_length的三种情况
void test_memory_bad_array_new_length()
{
try {
int negative = -1;
new int[negative]; // 大小为负
} catch(const std::bad_array_new_length& e) {
fprintf(stderr, "1: %s ", e.what());
}

try {
int small = 1;
new int[small]{1, 2, 3}; // 过多的初始化值设定
} catch(const std::bad_array_new_length& e) {
fprintf(stderr, "2: %s ", e.what());
}

try {
int large = INT_MAX;
new int[large][1000000]; // 过大
} catch(const std::bad_alloc& e) {
fprintf(stderr, "3: %s ", e.what());
}
}
释放函数:是类的成员函数或全局函数,在一个全局范围以外的命名空间范围声明释放函数或全局范围内声明静态的释放函数都是不正确的。每个释放函数都返回void,并且它的第一个参数是void*。对于这些函数的两个参数形式,第一个参数是一个指向需要释放的内存块的指针,第二个参数是要释放的字节数。这种形式可能被用于从基类中删除一个派生类对象。提供给一个释放函数的第一个参数的值可以是一个空指针值,如果是这样的话,并且如果释放函数是标准库提供的,那么该调用没有任何作用。

如果提供给标准库中的一个释放函数的参数是一个指针,且它不是空指针值,那么释放函数释放该指针所引用的存储,引用指向已释放存储(deallocated storage)的任何部分的所有指针无效。使用无效的指针值(包括将它传递给一个释放函数)产生的影响是未定义的。

垃圾回收:在C++中,垃圾回收(自动回收不再被引用的内存区域)是可选的,也就是说,一个垃圾回收器(Garbage Collector, GC)不是必须的。一个垃圾回收器必须能够识别动态分配的对象的指针,以便它可以确定哪些对象可达(reachable),不应该被回收,哪些对象不可达(unreachable),并可以回收。

4.4 常见的C++内存管理错误:常见的与内存管理相关的编程缺陷,包括未能正确处理分配失败、解引用空指针、写入已经释放的内存、对相同的内存释放多次、不当配对的内存管理函数、未区分标量和数组,以及分配函数使用不当。

未能正确检查分配失败:new表达式要么成功要么抛出一个异常。new操作符的nothrow形式在失败时返回一个空指针,而不是抛出一个异常。

// 未能正确检查分配失败
void test_memory_new_wrong_usage()
{
// new表达式,要么成功,要么抛出一个异常
// 意味着,if条件永远为真,而else子句永远不会被执行
int* ip = new int;
if (ip) { // 条件总是为真

} else {
// 将永远不执行
}
delete ip;

// new操作符的nothrow形式在失败时返回一个空指针,而不是抛出一个异常
int* p2 = new(std::nothrow)int;
if (p2) {
delete p2;
} else {
fprintf(stderr, "fail to new ");
}
}
不正确配对的内存管理函数:使用new和delete而不是原始的内存分配和释放。C内存释放函数std::free不应该被用于由C++内存分配函数分配的资源,C++内存释放操作符和函数也不应该被用于由C内存分配函数分配的资源。C++的内存分配和释放函数分配和释放内存的方式可能不同于C内存分配和释放函数分配和释放内存的方式。因此,在同一资源上混合调用C++内存分配和释放函数及C内存分配和释放函数是未定义的行为,并可能会产生灾难性的后果。

class Widget {};

// 不正确配对的内存管理函数
void test_memory_new_delete_unpaired()
{
int* ip = new int(12);
free(ip); // 错误,应使用delete ip

int* ip2 = static_cast<int*>(malloc(sizeof(int)));
*ip2 = 12;
delete ip2; // 错误,应使用free(ip2)

// new和delete操作符用于分配和释放单个对象
Widget* w = new Widget();
delete w;

// new[]和delete[]操作符用于分配和释放数组
Widget* w2 = new Widget[10];
delete [] w2;

// operator new()分配原始内存,但不调用构造函数
std::string* sp = static_cast<std::string*>(operator new(sizeof(std::string)));
//delete sp; // 错误
operator delete (sp); // 正确
}
在C++的new表达式分配的对象上调用free,因为free不会调用对象的析构函数。这样的调用可能会导致内存泄漏、不释放锁或其它问题,因为析构函数负责释放对象所使用的资源。

new和delete操作符用于分配和释放单个对象;new[]和delete[]操作符用于分配和释放数组。

当分配单个对象时,先调用operator new()函数来分配对象的存储空间,然后调用其构造函数来初始化它。当一个对象被删除时,首先调用它的析构函数,然后调用相应的operator delete()函数来释放该对象所占用的内存。当分配一个对象数组时,先调用operator new[]()对整个数组分配存储空间,随后调用对象的构造函数来初始化数组中的每个元素。当删除一个对象数组时,首先调用数组中的每个对象的析构函数,然后调用operator delete[]()释放整个数组所占用的内存。要将operator delete()与operator new()一起使用,并将operator delete[]()与operator new[]()一起使用。如果试图使用operator delete()删除整个数组,只有数组的第一个元素所占用的内存将被释放,一个明显的内存泄漏可能会导致被利用。

new和operator new():可以直接调用operator new()分配原始内存,但不调用构造函数。

函数operator new()、operator new[]()、operator delete()和operator delete[]()都可以被定义为成员函数。它们是隐藏继承的或命名空间范围中同名函数的静态成员函数。与其它内存管理函数一样,重要的是让它们正确配对。如果对象所使用的内存不是通过调用operator new()获得的,同时对其使用operator delete(),就可能发生内存损坏。

多次释放内存:智能指针是一个类类型(class type),它具有重载的”->”和”*”操作符以表现得像指针。比起原始指针,智能指针往往是一种更安全的选择,因为它们可以提供原始指针中不存在的增强行为,如垃圾回收、检查空,而且防止使用在特定情况下不合适或危险的原始指针操作(如指针算术和指针复制)。引用计数智能指针对它们所引用的对象的引用计数进行维护。当引用计数为零时,该对象就被销毁。

释放函数抛出一个异常:如果释放函数通过抛出一个异常终止,那么该行为是未定义的。释放函数,包括全局的operator delete()函数,它的数组形式与其用户定义的重载,经常在销毁类类型的对象时被调用,其中包括作为某个异常结果的栈解开。允许栈解开期间抛出异常导致逃避了调用std::terminate,而导致std::abort函数调用的默认效果。这种情况可能被利用为一种拒绝服务攻击的机会。因此,释放函数必须避免抛出异常。

4.5 内存管理器:既管理已分配的内存,也管理已释放的内存。在大多数操作系统中,包括POSIX系统和Windows,内存管理器作为客户进程的一部分运行。分配给客户进程的内存,以及供内部使用而分配的内存,全部位于客户进程的可寻址内存空间内。操作系统通常提供内存管理器作为它的一部分(通常是libc的一部分)。在较不常见的情况下,编译器也可以提供替代的内存管理器。内存管理器可以被静态链接在可执行文件中,也可以在运行时确定。

4.6 Doug Lea的内存分配器:GNU C库和大多数Linux版本(例如Red Hat、Debian)都是将Doug Lea的malloc实现(dlmalloc)作为malloc的默认原生版本。Doug Lea独立地发布了dlmalloc,其他一些人对其作了修改并用作GNU libc的分配器。

动态分配的内存也可能遭遇缓冲区溢出。例如,缓冲区溢出可被用于破坏内存管理器所使用的数据结构从而能够执行任意的代码。

解链(unlink)技术:最早由Solar Designer提出。unlink技术被用于利用缓冲区溢出来操纵内存块的边界标志,以欺骗unlink()宏向任意位置写入4字节数据。

4.7 双重释放漏洞:Doug Lea的malloc还易于导致双重释放漏洞。这种类型的漏洞是由于对同一块内存释放两次造成的(在这两次释放之间没有对内存进行重新分配)。要成功地利用双重释放漏洞,有两个条件必须满足:被释放的内存块必须在内存中独立存在(也就是说,其相邻的内存块必须是已分配的,这样就不会发生合并操作了),并且该内存所被放入的筐(在dlmalloc中,空闲块被组织成环形双链表,或筐(bin))必须为空。

写入已释放的内存:一个常见的安全缺陷。

RtlHeap:并非只有使用dlmalloc开发的应用程序才可能存在基于堆的漏洞。使用微软RtlHeap开发的应用程序在内存管理API被误用时也有可能被利用。与大多数软件一样,RtlHeap也在不断地进化,不同的Windows版本通常都有不同的RtlHeap实现,它们的行为稍有不同。

4.8 缓解策略:有很多缓解措施可以用来消除或减少基于堆的漏洞。

空指针:一个明显的可以减少C和C++程序中漏洞数量的技术就是在指针所引用的内存被释放后,将此指针设置为NULL。空悬指针(执行已释放内存的指针)可能导致赋写已释放内存和双重释放漏洞。将指针置为NULL后,任何企图解引用该指针的操作都会导致致命的错误,这样就增加了在实现和测试过程中发现问题的几率。并且,如果指针被设置为NULL,内存可以被”释放”多次而不会导致不良后果。

一致的内存管理约定:(1).使用同样的模式分配和释放内存;(2).在同一个模块中,在同一个抽象层次中分配和释放内存;(3).让分配和释放配对。

随机化:传统的malloc函数调用返回的内存分配地址在很大程度上是可预测的。通过让内存管理程序返回的内存块地址随机化,可以使对基于堆的漏洞利用变得更加困难。

运行时分析工具:Valgrind、Purify、Insure++、Application Verifier。

GitHub:https://github.com/fengbingchun/Messy_Test
入门C语言,学习资料及视频在这里:

C语言编程基础
http://www.makeru.com.cn/live/1758_311.html?s=143793
C语言 数组和字符串
http://www.makeru.com.cn/video/2238_12037.html?s=143793
C语言 指针专题一
http://www.makeru.com.cn/video/2239_12043.html?s=143793
C语言可控制led灯
http://www.makeru.com.cn/live/1392_304.html?s=143793

原文地址:https://www.cnblogs.com/jinwenyi/p/13274086.html