C语言 内存管理的细枝末节

1、 一些细枝末节的概念 - 作用域、链接、存储时期

1. 作用域 (scope)

作用域限定了程序中可以访问一个标识符一个或多个区域. C语言中,一个变量的作用域可以是代码块作用域,函数原型作用域或者文件作用域等。

一个代码块是包含在花括号{}之间的一段代码。在代码块中定义的变量具有代码块作用域 (block scope),从该变量被定义的地方到代码块结束均可见。函数的形式参量尽管在函数的开始花括号前被定义, 同样也具有代码块作用域。若在内层代码块中定义了一个与外部代码块同名变量,那么在内层代码块中会使用内层定义的变量而非外层变量,这称为内层定义隐藏(hide)了外层定义, 但内层定义的变量只在内层代码块有效,当程序运行离开内层代码块时, 外部变量重新恢复使用.

举个栗子(仅仅做一下演示):
void fun(int arg0,int arg1){  // 代码块作用域 

    int sum = arg0 + arg1;
    printf("%d\n",sum) ;

    { // 内层代码块 
        // 作用域在内层代码块里 
        int sum = 0;  // 隐藏了上面的sum ,  
        printf("%d\n",sum) ;

    }

    printf("%d\n",sum) ;  // 最上面的sum恢复可见 
}

在所有函数之外定义的变量具有文件作用域 (file scope). 具有文件作用域的变量从它定义处到定义它的文件结尾处都是可见的,通常称作全局变量。

函数原型作用域 (function prototype scope) 适用于函数原型中使用的变量名,函数原型作用域从变量定义处一直到函数声明的末尾.编译器处理函数原型的时候,只关心变量的类型,变量的名称相对无关紧要,即使函数原型的参数名称与函数定于的参数名称不同。不过需要注意的是,函数原型中的变量不可重名。
对于下面函数原型:

void fun(int a, int b, int c); //a,b,c的作用域在这条声明语句,且不可出现重名 

还有一种特殊的情况,在C99中出现了变长数组,此时参数变量名则很有必要

void val(int rows, int cols, int arr[rows][cols]); // rows和cols分别是arr的行列

2. 链接 (linkage)

标识符的链接属性决定了如何处理在不同文件中、同文件的不同位置出现的标识符。C 变量可能具有下列链接之一: 外部链接 (external linkage), 内部链接(internal linkage), 空链接 (no linkage).

  • 具有代码块作用域或者函数原型作用域的变量具有空链接, 意味着它们由其定义所在的代码块或函数原型所私有.
  • 具有文件作用域的变量可能有内部链接或者外部链接,这取决于变量是否被static修饰符所修饰, 被static修饰的变量具有内部链接,没有的变量则具有外部链接。
  • 一个具有外部链接的变量可以在程序的多文件的任何地方使用. 一个具有内部链接的变量只可以在程序的一个文件的任何地方使用。

如下示例:

int i = 1; // 具有外部链接的静态变量(全局变量)
static int gi; // 具有内部链接的静态变量

void test()
{
    int i = 10; //空链接
    printf("%d,%d", gi, i);
}

3. 存储时期 (storage duration)

在 C语言中,变量的存储时期分为静态存储时期 (static storage duration)和自动存储时期 (automatic storage duration),变量可能会具有两种存储时期中的某一种。 如果一个变量具有静态存储时期, 它在程序执行期间将一直存在。

说明

  • 具有代码块作用域的变量一般都具有自动存储时期. 在程序运行某代码块时,将为其中定义的变量分配内存;当代码块执行完成后, 分配的内存将被释放,以供他用 (局部变量(非静态)类型的)。

  • 具有文件作用域的变量具有静态存储时期。对于具有文件作用域的变量, 修饰符 static 表明链接类型而非存储时期。

static的含义
一个使用 static 声明了的文件作用域变量具有内部链接, 而所有的文件作用域变量, 无论它具有内部链接, 还是具有外部链接, 都具有静态存偹时期.而对于一个局部变量,使用static修饰则改变了其存储时期(从自动改为静态)

5 种存储类

C 语言使用作用域、链接和存储时期定义了 5 种存储类: 自动、寄存器、 具有代码块作用域的静态、具有外部链接的静态和具有内部链接的静态。

如下列出:

存储类作用域链接存储时期声明方式
自动类型代码块自动代码块内(可省略auto)
寄存器类型代码块自动代码块内(使用register)
代码块作用域的静态代码块静态代码块内(使用static)
具有外部链接的静态文件外部链接静态所有函数之外(全局)
具有内部链接的静态文件内部链接静态所有函数之外(使用static)

2、 存储类变量

2.1 auto自动变量

一般情况下代码块内部定义的变量都是自动变量。当然也可以显示的使用auto关键字来声明,关键字 auto 称为存储类说明符 (storage class specifier)。

特点

  • 自动变量在存储时期存储在栈上,自动分配自动释放内存空间。自动变量不会自动初始化,所以应该显式的为自动变量初始化。不可将全局变量声明为auto。

  • 自动变量具有代码块作用域可空链接。这意味着只能在定义变量的代码块中才能直接访问它(通过指针可间接访问)。因而不同代码块之间的变量可重名,相互无影响。

例:

void fun(register int x)
{
    auto int i; // 自动变量,auto可省略,不会自动初始化为0
    static int j; // 非自动变量,静态存储,自动初始化为0
}

2.2 register寄存器变量

通常变量存在于内存之中,如果能把变量存放到CPU的寄存器里,则可比普通变量更快地被访问和操作,代码执行效率会更高。若没有手动初始化, 它的值是不确定的。

一般,寄存器变量存放在一个寄存器而不是内存中, 所以无法获得寄存器变量的地址。除了存储的位置不同,寄存器变量和普通变量有很多共同之处 , 它们都有代码块作用域,空链接和自动存储时期。使用存储类说明符 register 来声明寄存器变量。

如下把i声明为寄存器变量:

register int i;

注意

  • 使用register声明一个变量为寄存器变量只是对编译器的一个请求, 而非一条直接的命令。

  • 编译器会在你的请求和可用寄存器的个数或可用高速内存的数量之间做权衡,所以声明为寄存器变量的变量不一定会被存储在寄存器中,若请求失败则该变量成为一个普通的自动变量,但是仍然不可对它使用地址运算符。

  • 在C语言中,对寄存器变量区地址是被禁止的,在CPP中,C++编译器对register变量做了优化,检测到使用地址运算符,就不会把它放到寄存器,可以取地址。

将函数的形式参数声明为寄存器变量是合法的。如下

void fun(register int i)
{
}

说明:虽然将 i 存储到寄存器会提高访问速度,但是要将 i 拷贝到寄存器也需要花费额外的时间,所以,可以在两者之前作协调,以达到最好性能。

2.3 代码块作用域的静态变量

创建具有代码块作用域, 兼具静态存储时期的局部变量,它和自动变量具有相同的作用域,但在函数调用完毕后, 它们在内存中并不会被销毁,并且只能被这个代码块内部访问。 因此它们具有代码块作用域、空连接和静态存储时期,在函数再次调用时,访问该变量时,该变量保存的值仍然是上次调用完毕后的值。

在代码块中使用static来创建,如下

void getNum()
{
    static int i; // 创建代码块作用域的静态变量, 会被自动初始化为0
    printf("i=%d\n", i);
    i = 10;
    printf("i=%d\n", i);
}

注意:代码块作用域的静态变量在创建后,会被自动初始化为0 。

创建代码块作用域的静态变量时不可用变量名来初始化:

int b = 20;
static int i =b; // error C2099: 初始值设定项不是常量

实参总是在栈中传参给函数,所以不推荐将函数形式参数声明为静态(尽管在vs2015上编译通过)

void getNum(static int a)  // warning C4042: “a”: 有坏的存储类
{
}

2.4 具有内部链接的静态变量 — 全局静态变量

这种存储类的变量具有静态存储时期、 文件作用域和内部链接。具有内部链接的静态变量在程序执行期间一直存在,但只能在被定义这个变量的文件中访问。该类型变量在未手动初始化时会自动初始化为0,若要为该变量初始化则必须用常量表达式。

在所有函数外,做如下声明

static int gi; // 声明一个具有内部链接的静态变量,会自动初始化

2.5 具有外部链接的静态变量 — 全局变量

具有外部链接的静态变量具有文件作用域、 外部连接和静态存储时期,亦可称为全局变量

全局变量的存储方式和具有内部链接的静态变量相同,但可以被多个文件访问。与自动变量不同的是,全局变量在未手动初始化时会自动初始化为0,若要为全局变量初始化则必须用常量表达式。

这一类型有时被称为外部存储类 (external storage class), 该类型的变量被称为外部变量 (external variable)。外部变量的作用域 : 从声明处开始到定义它的文件结尾为止。若要访问一个其他文件定义的具有外部链接的静态变量(外部变量),则需要使用extern关键字来声明它,如下:

int i = 1; // 定义并初始化一个具有外部链接的静态变量(全局变量)
extern int j; // 声明一个外部变量,j在其他文件被定义

需要注意的是,当局部出现重名变量时,无法直接引用全局变量。简言之, 在程序执行代码块时, 代码块作用域的变量隐藏了具有文件作用域的同名变量。

int i = 1; // 定义并初始化一个具有外部链接的静态变量(全局变量)
void test()
{
    int i = 10; //局部变量会隐藏定义的全局变量,在该代码块访问的i均是局部i
}

3、 存储类与函数

在C语言中函数默认都是全局函数(或外部函数),可以在其他文件中被调用,因此若其他文件出现了同名的函数则会产生重定义错误。使用static可以规避这个错误,它可将一个函数作用域限制在某个文件而无法被其他文件所访问。

通常使用关键字 extern 声明在其它文件中定义的函数,这一习惯做法主要是为了使程序更清晰, 因为除非函数声明使用了关键字 static, 否则就为它是 extern 的

使用关键字static可以将函数声明为静态函数,但是此函数将无法在其他文件中被调用,作用域被限定在当前文件,因此可在其它文件中定义具有相同名称但不同功能的函数

如下:

static int get()  // 作用域为当前文件,其他文件不可访问
{
     printf("call get");
    int i = 10;
    return i++; // 返回i的拷贝
}

C99 增加了第三种存储类的函数 - 内联函数


4、 内存四区

C语言程序运行时,操作系统会为其分配内存空间,这段空间主要分为四个区域,分别是栈区、堆区、静态区和代码区,此所谓“内存四区”。

用一张图展示如下:
这里写图片描述

特点:
• Code Area:程序代码指令、常量字符串,只可读
• Static Area:存放全局变量/常量、静态变量/常量
• Heap:由程序员控制,使用malloc/free来操作
• Stack:预先设定大小,自动分配与释放

4.1 代码区

代码区(code area),程序被操作系统加载到内存的时候,所有的可执行代码都加载到代码区,也叫代码段,这块内存是不可以在运行期间修改的。

4.2 静态区

静态区(static area),所有的全局变量/常量以及程序中的静态变量/常量都存储到该区,该区在程序结束后由操作系统释放。 在程序载入内存时被加载进内存。

4.3 栈区

4.3.1 栈区的特点

  • 栈stack是一种先进后出的内存结构,所有的自动变量,函数的形参都是由编译器自动放出栈中,当一个自动变量超出其作用域时,自动从栈中弹出。

  • 对于自动变量,什么时候入栈,什么时候出栈,是不需要程序控制的,由C语言编译器实现。

  • 栈不会很大,一般都是以K为单位的。虽然栈区的容量小,但由于是系统自动分配的,同时栈区的内存空间是连续的,不会产生内存碎片,因此栈区中数据的执行速度很快。假如申请的内存空间超过栈区的剩余空间,那么系统会提示栈溢出,因此,别指望栈区能存储较多的数据。

4.3.2 栈的实现原理

栈顶从高地址向低地址方向增长,存储非静态局部变量、函数参数、返回地址等。插入时从尾部插入元素,取出元素也从尾部取出元素,此所谓后进先出(LIFO)。

图解如下:
这里写图片描述

栈的实际大小往往与操作系统以及开发环境有密切关系。

4.3.3 栈溢出

当栈空间已满,但仍然往栈内存压入变量,造成栈发生溢出的现象就叫栈溢出

4.4 堆区

堆(heap)也是一种数据结构,和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序,它的空间不连续,空间的申请与释放需要手动完成。

特点

  • 堆内存的容量要远远大于栈,但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成,而我们常常忘了释放,这往往是程序出现内存泄漏的根源。若不手动释放,程序结束时可能由操作系统回收(程序不正常结束则回收不了)。

  • 堆是不连续的内存区域,各块区域由链表将它们串联起来。该区域一般由程序员分配或释放,堆区的上限是由系统中有效的虚拟内存来定的,因此获得的空间较大,而且获得空间的方式也比较灵活。

  • 虽然堆区的空间较大,但堆区的内存空间不是连续的,容易产生内存碎片,因此堆中数据的执行速度比较慢。


5、 常用内存函数

5.1 memset

函数原型void *memset(void *s, int ch, size_t n);

函数解释:将s中前n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
作用 :在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
**头文件:**memory.h中。( 头文件memory.h为内存操作函数头文件)
参数
1) 第一个参数是内存的首地址,
2) 第二个参数是要设置的字符,
3) 第三个参数是整数,从首地址开始设置多少个字节为第二个参数

memset多用于清空字符串 用法如下:

 memset(str,’\0’,strlen(str));

5.2 memcpy

函数原型 void *memcpy(void *destin, const void *source, size_t n);

函数解释: 从源source所指的内存地址的起始位置开始拷贝n个字节到目标destin所指的内存地址的起始位置中
说明
1.source和destin所指的内存区域可能重叠,但是如果source和destin所指的内存区域重叠,那么这个函数并不能够确保source所在重叠区域在拷贝之前不被覆盖。而使用memmove可以用来处理重叠区域。函数返回指向destin的指针.

2.如果目标数组destin本身已有数据,执行memcpy()后,将覆盖原有数据(最多覆盖n)。如果要追加数据,则每次执行memcpy后,要将目标数组地址增加到你要追加数据的地址。

注意:source和destin都不一定是数组,任意的可读写的空间均可。与 strcpy() 不同的是,memcpy() 会完整的复制 n 个字节,不会因为遇到“\0”而提前结束。

头文件 memory.h

示例:

void main()
{
    char str1[20] = "hello world";
    char str2[20] = {0}; // 初始化
    memcpy(str2, str1, strlen(str1)); // 将str1的内容拷贝到str2,拷贝 strlen(str1)个长度

    printf("%s\n", str2);

    system("pause");
}

5.3 memccpy

函数原型 void *memccpy(void *dest, void *src, unsigned char c, unsigned int count);

函数说明 由src所指内存区域复制不多于count个字节到dest所指内存区域,如果遇到字符c则停止复制。
返回值:如果c没有被复制,则返回NULL,否则返回字符c 后面紧挨一个字符位置的指针(此指针是指向dest中的某个位置)。
区别于memcpy
memccpy与memcpy都是内存拷贝函数,但是memccpy会遇到指定字符终止。如果不存在指定字符,则结果memccpy与memcpy相同

memccpy前面加下划线是为了符合新的C语言标准。

示例:

void main()
{
    char str1[20] = "hello world";
    char str2[20] = { 0 }; // 初始化
    char* p = (char*) _memccpy(str2, str1,'w', strlen(str1)); // 将str1的内容拷贝到str2,拷贝 strlen(str1)个长度,遇到字符w则停止

    printf("%s\n", str2);
    if (p!=NULL)    // 若p不为NULL,p则指向目的str2中的指定字符w后一位的指针
    {
        *p = 'o';   //  将字符w后一位赋值为字符o
        printf("%s\n", str2);
    }

    system("pause");
}

输出:

hello w
hello wo

5.4 memmove

原型:void memmove( void dest, const void* src, size_t count );

功能:由src所指内存区域复制count个字节到dest所指内存区域。
头文件:string.h
说明:如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,但复制后src内容会被更改。但是当目标区域与源区域没有重叠则和memcpy函数功能相同。

示例1

void main()
{
    char str1[20] = "hello world";
    char str2[20] = { 0 }; // 初始化
    memmove(str2, str1, strlen(str1)); // 没有发生重叠的情况,与memcpy功能相同
    printf("%s\n", str2);

    system("pause");
}

示例2

void main()
{
    char str1[20] = "hello world";
    char str2[20] = { 0 }; // 初始化
//  memmove(str2, str1, strlen(str1)); // // 没有发生重叠的情况,与memcpy功能相同
//  memmove(str1, str1 + 3, 6); // 重叠情况1,源的内容始终在目的后,使用memcpy也不会出现拷贝出错
    memcpy(str1+3, str1, 6); // 重叠情况2,源的内容在前面,目的在后,需要使用memmove
    printf("%s\n", str1);

}

Linux平台,第二种重叠情况使用memcpy,打印发现拷贝失误

helhelhe ld

Windows平台 vs2015打印如下,发现第二种重叠情况使用memcpy依然拷贝正常(奇怪之处,希望知道的朋友不吝赐教~)

helhello ld

5.5 memchr

函数原型extern void *memchr(const void *buf, int ch, size_t count);

功能:从buf所指内存区域的前count个字节查找字符ch。
说明:当第一次遇到字符ch时停止查找。如果成功,返回指向字符ch的指针;否则返回NULL。

功能使用较简单就不做演示了~。

5.6 memcmp

函数原型 int memcmp(const void *buf1, const void *buf2, unsigned int count);

功能: 比较内存区域buf1和buf2的前count个字节。
头文件: string.h 或 memory.h
返回值

当buf1<buf2时,返回值-1
当buf1==buf2时,返回值=0
当buf1>buf2时,返回值1

说明:该函数是按字节比较的。

示例

void main()
{
    char* str1 = "abcd";
    char* str2 = "accd";
    int res = memcmp(str1, str2, strlen(str1)); // 比较str1和str2前 strlen(str1)个

    if (res)    // 非0
    {
        printf("不等");
    }
    else
    {
        printf("相等");
    }
    system("pause");
}

5.7 memicmp

函数原型extern int memicmp(void *buf1, void *buf2, unsigned int count);

功能:比较内存区域buf1和buf2的前count个字节但不区分字母的大小写。
说明:memicmp同memcmp的唯一区别是memicmp不区分大小写字母。
返回值

当buf1<buf2时,返回值<0
当buf1=buf2时,返回值=0
当buf1>buf2时,返回值>0

参数:前两个参数为要比较的两个参数的首地址,第三个参数为要比较的长度


6、 动态内存分配

对于上面的5种存储类,在决定了使用哪一存储类之后, 就自动决定了作用域、链接和存储时期.并且要服从预先的内存管理规则. 然而,还可以使用库函数来分配和管理内存(通常是堆内存),这个选择给程序员更多灵活来使用内存。

动态内存分配,是指用户可以在程序运行期间根据需要申请或释放内存,大小也完全可控。动态分配不像数组内存那样需要预先分配空间,而是由系统根据程序需要动态分配,大小完全按照用户的要求来,当使用完毕后,用户还可释放所申请的动态内存,由系统回收,以备他用。

对于动态分配的内存,虽然使用灵活,我们使用的时候也需谨慎。对分配的内存进行操作时不要越过边界,否则可能会造成非法访问等异常。而对于已经释放过的内存不要试图再进行访问,也不要再进行释放,最好将指针那片内存空间的指针赋值为NULL。

动态分配内存不是总会成功,若申请内存空间得不到满足,则会导致失败,相关函数会返回一个NULL指针,因此判空是很有必要的。

6.1 malloc 函数

malloc函数是C标准库中提供的函数,用以动态申请内存,malloc()函数的原型为:

void *malloc( unsigned int size );

参数

  • 1) 参数size是个无符号整型数,用户由此控制申请内存的大小,执行成功时,系统会为程序开辟一块大小为size个内存字节的区域,并将该区域的首地址返回,用户可利用该地址管理并使用该块内存,如果申请失败(比如内存大小不够用),返回空指针NULL,此时需作判空处理。

  • 2) malloc()函数返回类型是void*,用其返回值对其他类型指针赋值时,须进行显式转换。size仅仅是申请字节的大小,并不管申请的内存块中存储的数据类型,因此,申请内存的长度须由程序员通过“长度×sizeof(类型)”的方式给出,举例来说:
    int* p=(int*) malloc(10* sizeof(int) ); // 申请了10*4个字节的内存

注意

1)  使用malloc 函数进行分配内存时可能失败而返回NULL,因此有必要作判空处理 
2)  对分配的内存进行操作时不要越过边界,否则可能会造成非法访问等异常

6.2 free 函数

free函数是C标准库中提供的函数,用以释放内存。函数原型如下

void  free(void* _Block);

通过malloc等函数在堆上申请的内存,最终需要通过free函数来释放内存,例如free(p)。当向free传递NULL指针时,无任何效果(free(NULL);)

注意

1)  不要释放非动态分配的内存。
2)  不要在已经释放的内存区域继续使用它
3)  不要重复释放已经被释放的内存
4)  在一次释放完毕后,最好将指向那片内存空间的指针赋值为NULL

6.3 calloc函数

calloc函数与malloc函数作用大致相同,都用于动态申请内存。不同的是,calloc函数会将申请的内存自动清零。函数原型如下

void*  calloc(size_t _Count, size_t _Size);  

参数
_Count表示要申请多少个,_Size表示要申请的每个大小为多大。如果分配内存需要初始化,则会带来便利,但程序若是只是将数值存入到数组中,则使用calloc时的初始化则只是浪费时间。

注意事项与malloc 同。

6.4 realloc函数

realloc函数用于重新分配(用malloc或者calloc函数)在堆中已分配好的内存空间的大小。函数原型如下:

void* realloc(void*  _Block,size_t _Size ); 

参数
第一个参数 p是之前用malloc或者calloc分配的内存首地址,_NewSize为重新分配内存的大小,单位:字节。
说明
使用此函数可以扩大或缩小分配的内存空间,这取决于你传入的_Block* _Size大小与原来的内存大小。有如下情况:

  • (1) 当_Block* _Size 大于原分配的内存大小时,若能够有足够内存空间分配,则会直接分配到当前内存的后面,若原先的内存无法拓展以满足要求,则会重新申请一块内存,并把原有内存的内容拷贝到新的内存区域,此时指向内存首地址的指针发生变化,应该用realloc函数返回的新指针,原内存区域被释放。

  • (2) 当_Block* _Size 小于原分配的内存大小时,则会直接从原有内存的某个部分截断,丢弃大于申请的大小的部分,保留的部分仍然是原有的内容。

  • (3) 当realloc申请内存失败,则会返回NULL,因此判空是有必要的。

注意事项

1)  成功返回新分配的堆内存地址,失败返回NULL.
2)  如果参数p等于NULL,那么realloc与malloc功能一致
3)  realloc函数不会对新分配的内存进行初始化

6.5 内存泄漏

当我们动态分配的内存不再使用时,我们应该释放它,以便可以被重新分配并使用。若分配的内存在我们使用完后不需要时没有被释放,则会引起内存泄漏(memory leak)。内存泄漏可能会导致可用内存越来越少,使得程序运行变慢,甚至是系统崩溃。因此防止内存泄漏是极其重要

6.6 使用示例

// 检查分配内存是否成功
void checkMemoryState(void* p)
{
    if (p == NULL)
    {
        printf("\n分配内存出错\n");
        exit(0);
    }
}

void main()
{
    int *a = (int *)malloc(sizeof(int) * 10);
    checkMemoryState(a);  // 检查分配内存是否成功
    for (int i = 0; i < 10; i++)
    {
        a[i] = i+1;  // 初始化
    }

    for (int i = 0; i < 10; i++)
    {
        printf("%d\t", a[i]);  // 打印
    }

    a = realloc(a, 15*sizeof(int));  // realloc不会对新分配的内存进行初始化
    checkMemoryState(a);
    memset(a + 10, 0, sizeof(int)*5);  // 对后申请的5个元素进行初始化

    for (int i = 0; i < 15; i++)
    {
        printf("%d\t", a[i]);  // 打印
    }
    free(a);  // 释放内存
    // free(a);  // 不要重复释放已经被释放的内存
    a=NULL; // 已经释放的将指针置为NULL
    //free(NULL); // 传递空时,无任何效果
    system("pause");
}

7、 总结

栈与堆的使用

  • 通常,可能会不清楚是选择在栈上还是在堆上存储数据,一般会综合考虑栈和堆的特性。栈内存一般相对较少,但是自动管理,速度快,而堆内存相对来说空间较大,但是由于空间的碎片化等会导致效率相对低下,并且需要手动申请手动释放。

  • 如果需要的内存空间不太大,并且对效率有一定的要求,则可以选择栈;如果需求空间很大,则可以选择堆,虽然相对低效,需要手动管理,但是对内存的管理相对灵活。

  • 还有很特殊的情况,比如在函数中返回一个占用较多内存的数据(比如说结构体),如果将它存储于栈上,则只能选择返回它的副本(若返回它的地址,则在函数调用完毕后,它占用的内存将会被释放,再通过地址访问它则会失败),然而返回一个占用很多内存的数据是较为浪费时间和效率的。这时可以将数据存储于堆上,使函数返回指向堆内存的指针,即是在函数调用结束后,这片内存依然存在,不过在使用完后记得释放这片内存空间。

存储类变量的使用示例

  • 对于存储类生成的变量使用,默认是使用自动类型,但是有时候可按需求来实施。比如当你定义一个变量,希望它能在其他文件中可以被访问,则不能将它们定义成静态的。如果你希望能够避免多文件变量的重定义情况,你可以将变量定义为内部链接的静态变量(全局静态)来限制作用域为当前文件,甚至定义为空链接的静态变量(局部静态)来限制作用域为某个代码块。

  • 当你希望一个常用的变量能获得更加快速的访问与操作,你可以尝试使用register来修饰它,寄希望它成为寄存器变量,但不是总能满足请求。

  • 当你希望在每次调用函数时,某个变量的值仍然是上次调用保存的值,可以使用static修饰局部变量,比如想达到计数的目的。

存储类在函数中的使用

  • 如果你希望把一个函数的作用域限制在某个文件,则可以使用static来修饰它(静态函数),可以达到多文件函数重名而不会产生重定义的情况。若是功能函数需要被其他文件调用,则不能使用static修饰。

默认存储类型

  • 变量的默认存储类型取决于它在代码中声明的位置。只要是在所有函数之外声明的变量,都具有静态存储时期,不可手动改变为auto;只要是在代码块中声明的变量,默认都具有自动存储时期,可以选择性的使用static来改变其存储时期为静态的。

作用域、链接和存储时期

  • 用于存储程序数据的内存可用存储时期、 作用域和链接来表征.。 存储时期可以是静态的、 自动的和动态分配的。如果是静态的, 内存在程序开始执行时被分配, 且在程序运行时一直存在. 如果是自动的, 变量所用内存在程序执行该变量被定义的代码块时开始分配, 在代码块执行完毕后该变量所占用的内存被自动释放。如果是动态分配的内存, 需要手动申请手动释放。

  • 从作用域的角度来看,它描述了程序中的数据在各个部分的可见性。在所有函数外定义的变量具有文件作用域、静态存储时期, 且对该变量声明之后定义的全部函数可见,若对文件作用域的变量使用static修饰,则会将该变量的作用域限制在当前文件,反之则可被多文件访问。在代码块中定义的变量或作为函数形参的变量,具有代码块作用域,在该代码块及其子代码块中可见(可被访问)。

  • 从链接的角度看,它描述了程序的某个单元定义的变量可被链接到的地方区域。对于一个具有外部链接的变量,它可以被多个文件所访问;具有内部链接的变量,作用域被限制在当前定义的文件;对于空链接的变量,作用域被限制在某个代码块。

  • 从存储时期来看,对于自动存储时期的变量,在程序执行相关代码时加载入内存,在执行代码块完毕后,会自动被释放;对于静态存储时期的变量,它在程序执行期间将一直存在。

理解static关键字

  • 在不同的语言中,static关键字有着不同的含义。在C语言中,在不同的上下文环境中,static关键字有着不同的含义。当static用于代码块内部的变量声明的时候,它作用于变量的存储时期(从自动改变为静态),而对变量的链接和作用域没有影响,这种方式声明的变量在程序执行前创建并在程序运行时一直存在。当static用于文件作用域的变量声明或函数的定义时,它修改的是链接属性(从外部链接改为内部链接),而对作用域和存储时期没有影响,这种方式定义的变量或函数只能在当前文件被访问。

内存特性

  • 理解静态内存、自动内存以及动态分配的内存的特性与区别。 一般,所需静态内存的数量在编译时就确定了, 静态数据在程序被载入内存时就被载入了内存。 在程序运行时,当执行自动变量定义的代码块时,自动为自动变量分配和释放内存。 因此, 自动变量使用的内存数量在程序运行时会不断变化,但是这个过程是自动管理的。动态分配的内存也会增加和减少, 但这个过程是由程序员来通过调用函数控制的, 而非自动发生的。

动态内存分配

  • 对于动态内存分配,当我们手动申请的内存空间在使用完毕后,一定要记得使用free进行释放,否则会导致内存泄漏。同时需要注意的是,对于动态分配的内存,不要试图做越界操作;已经释放的内存不可再次使用,也不可再通过free来释放;在一次释放完毕后,最好将指向那片内存空间的指针赋值为NULL,因为free一个NULL指针是无效果的。对于不是动态分配的内存,不可进行手动释放(比如释放一个在栈上创建的数组),否则会导致程序崩溃。

参考:《C与指针》、《C Primer plus 第五版》


很多细枝末节,如有错误,欢迎指正~

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值