天天看点

C/C++程序的内存区域划分

很多刚接触C/C++的朋友,很难深入的理解指针,以及内存管理的特性。

与基于.NET 的托管C++和JAVA不同的是,C/C++将内存管理的过程,都交给了程序员,虽然这给编码带来了更加复杂的一个过程,但是直接操作内存,具有极高的执行效率。然而,与此同时,带来的也是各种安全隐患。如我们使用数组时经常出现的数组越界问题,指针被“野”掉的问题。

数组越界:

int main()
{
    int arr[] = {  };
    arr[] = ;
}
           

VC调试下,出现Crash

C/C++程序的内存区域划分

像这样的语句,便是最经典的越界问题,因为对标准数组而言,下标从0开始,并且截止于length - 1。当然,在程序中,即时是新手,也不一定会出现上述显式的越界错误,更多的,是出现在循环嵌套,以及各种条件控制中,对下标的难以控制,而造成越界错误等,如下标小于0。

而像野指针,自然也经常出现在程序中:

int *p;
*p = ;
/*Like this occasion: the pointer is used without being initialized*/
           
C/C++程序的内存区域划分

上面的情况,说明了指针被“野”而产生的错误,由此看来,指针在给我们带来方便的同时,一样的造成了各种异常错误的潜在威胁。

那么我们在使用指针的时候,便非常有必要注意,防止越界问题,和野指针问题。

在程序运行期间,操作系统会为每个程序分配内存。由于数据的性质不同,每个程序都会有不同的内存区域,并且其生命周期也是不同的。

每个程序有如下几个内存区域:堆栈区(栈区), 堆区, 全局区,常量区和代码区。

下面分别介绍几个区域的区别:

堆栈区(栈区)Stack

栈区变量存在的特性,主要在于其生命周期较短,并且其内存,由编译器管理,并由编译器自动释放。如我们手动声明的局部变量,函数的形参等,都是作为栈区变量存在。

如:

int main()
{
    int a = ;//局部变量a,只在main函数及以下部分可见
    int func(int, int);// 函数代码存入代码区
    char str[] = "China"; //字符常量,存入常量区
    static int b = ;// 静态变量,存入全局区
    return ;
}

int func(int a, int b) // 产生a,b两个栈区变量,声明周期只存在于func内
{
    return a - b;//return 语句结束,函数结束,a,b都将被释放
}
           

如上述代码,所有的变量,均被分配在栈区,并且由编译器负责释放,当变量的生命周期结束,内存空间便不再有效。同样的,数组空间也被归还操作系统,有可能被其他进程占用,这也就是为什么在子函数中,不推荐返回数组的原因之一。如下将会产生警告(VC):

C/C++程序的内存区域划分
int main()
{
    int *getArray();
    int *arr = getArray();
    std::cout << arr[];
}

int *getArray()
{
    int _arr[] = {  };
    return _arr;
}
           

堆区 Heap

而堆区不一样,其生命周期取决于程序员,若程序员不对该内存进行释放,那么它将一直存在,知道程序结束。于是堆区内存,是程序员真正有权限大规模控制的内存区域。如:

/*Initial an array ranged by input.*/
int main()
{
    int size;
    int *arr = NULL;
    printf("Please input the size of the array: ");
    size = scanf("&d", &size);
    arr = (int *)malloc(sizeof(int) * size);
    if(!arr)
    {
        printf("Failed allocated!\n");
    }
    else
    {
        // TODO: Insert code to use this memory area.
    }
    // After used, the memory should be released
    // Call it to free
    free(arr);
    arr = NULL;
    // If you don't free it, it will not free until this program ended.
}
           

因此,堆区内存,尽量使用后立即释放且必须释放,若不释放,它将一直占据内存,并直到程序的结束。且释放内存后,尽量将该指针置空,防止野指针问题。

全局区 Static

全局区,拥有较久的生命周期,该段内存由系统管理。全局区分为两种情况,存放静态变量,存放全局变量,且未初始化的变量与初始化后的变量,存在于两个不同的区域。这也就是static变量为何只被一次初始化的原因。并且,在任何地方,对static变量的修改,都是生效的,因为其只有在程序结束后,系统进行内存释放。

常量区

常量区,一般用于存放字符串等一些字面常量。与宏定义不同,宏定义不能算作常量,而应该叫做“符号常量”,重点在于符号两个字,#define命令产生的一切符号,都是要在编译阶段,进行宏展开的,这些符号常量,会被一次替换成为真正的数据。

代码区

如字面意思,该区域,只用于存放函数体的二进制代码而已。

在一个C/C++程序中,都大致分为这5个区域,对内存进行管理。那么我们在使用指针时,便需要尤其注意栈区内存和堆区内存的区别,因为其生命周期的不同,在使用指针时,便尤为重要。同时,清楚这些,才能真正理解,为何数字交换的函数,如若不借助指针,将无法完成交换的原因。

下列代码,将无法实现交换:

#include <stdio.h>
int main()
{
    void swap(int, int); //Statement of function
    int a = , b = ;
    printf("The original data is : a = %d, b = %d\n", a, b);
    swap(a, b);
    printf("The data after swapped is: a = %d, b = %d\n", a, b);
    return ;
}
void swap(int a, int b)
{
    int t = a;
    a = b;
    b = t;
}
           

执行结果:

C/C++程序的内存区域划分

原因在于,在swap函数中,a,b都是形式参数,因此属于栈内存,而且该两个变量,与main中的a,b变量,属于不同的内存地址,如果仅仅靠交换栈区变量值,是无法完成交换的,而且函数在进行调用时,永远是传值调用,函数调用会将实参进行一份拷贝,并传入形式参数的栈区变量,指针也如此,传递指针时,也是将指针拷贝,进行传递。

最后说几句

尽管静态变量,用起来貌似很方便,因为其一直存在。但是一个程序中,是要极力避免使用过多的静态变量和全局变量的,因为其内存不被释放,持久占据内存,这对内存使用率是不高的,而且过度使用全局变量会造成变量的命名冲突问题。

内存管理,对于C/C++程序来说,算是精髓之处,如果能够充分理解,将对指针的使用,有很大的帮助。

继续阅读