很多刚接触C/C++的朋友,很难深入的理解指针,以及内存管理的特性。
与基于.NET 的托管C++和JAVA不同的是,C/C++将内存管理的过程,都交给了程序员,虽然这给编码带来了更加复杂的一个过程,但是直接操作内存,具有极高的执行效率。然而,与此同时,带来的也是各种安全隐患。如我们使用数组时经常出现的数组越界问题,指针被“野”掉的问题。
数组越界:
int main()
{
int arr[100] = { 0 };
arr[100] = 0;
}
VC调试下,出现Crash
像这样的语句,便是最经典的越界问题,因为对标准数组而言,下标从0开始,并且截止于length - 1。当然,在程序中,即时是新手,也不一定会出现上述显式的越界错误,更多的,是出现在循环嵌套,以及各种条件控制中,对下标的难以控制,而造成越界错误等,如下标小于0。
而像野指针,自然也经常出现在程序中:
int *p;
*p = 100;
/*Like this occasion: the pointer is used without being initialized*/
上面的情况,说明了指针被“野”而产生的错误,由此看来,指针在给我们带来方便的同时,一样的造成了各种异常错误的潜在威胁。
那么我们在使用指针的时候,便非常有必要注意,防止越界问题,和野指针问题。
在程序运行期间,操作系统会为每个程序分配内存。由于数据的性质不同,每个程序都会有不同的内存区域,并且其生命周期也是不同的。
每个程序有如下几个内存区域:堆栈区(栈区), 堆区, 全局区,常量区和代码区。
下面分别介绍几个区域的区别:
堆栈区(栈区)Stack
栈区变量存在的特性,主要在于其生命周期较短,并且其内存,由编译器管理,并由编译器自动释放。如我们手动声明的局部变量,函数的形参等,都是作为栈区变量存在。
如:
int main()
{
int a = 100;//局部变量a,只在main函数及以下部分可见
int func(int, int);// 函数代码存入代码区
char str[100] = "China"; //字符常量,存入常量区
static int b = 100;// 静态变量,存入全局区
return 0;
}
int func(int a, int b) // 产生a,b两个栈区变量,声明周期只存在于func内
{
return a - b;//return 语句结束,函数结束,a,b都将被释放
}
如上述代码,所有的变量,均被分配在栈区,并且由编译器负责释放,当变量的生命周期结束,内存空间便不再有效。同样的,数组空间也被归还操作系统,有可能被其他进程占用,这也就是为什么在子函数中,不推荐返回数组的原因之一。如下将会产生警告(VC):
int main()
{
int *getArray();
int *arr = getArray();
std::cout << arr[9];
}
int *getArray()
{
int _arr[10] = { 0 };
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
int main()
{
void swap(int, int); //Statement of function
int a = 6, b = 5;
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 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
原因在于,在swap函数中,a,b都是形式参数,因此属于栈内存,而且该两个变量,与main中的a,b变量,属于不同的内存地址,如果仅仅靠交换栈区变量值,是无法完成交换的,而且函数在进行调用时,永远是传值调用,函数调用会将实参进行一份拷贝,并传入形式参数的栈区变量,指针也如此,传递指针时,也是将指针拷贝,进行传递。
最后说几句
尽管静态变量,用起来貌似很方便,因为其一直存在。但是一个程序中,是要极力避免使用过多的静态变量和全局变量的,因为其内存不被释放,持久占据内存,这对内存使用率是不高的,而且过度使用全局变量会造成变量的命名冲突问题。
内存管理,对于C/C++程序来说,算是精髓之处,如果能够充分理解,将对指针的使用,有很大的帮助。