老汤回味——C语言指针,数组及动态内存分配

了解过C语言的人,都会知道C语言有一种独特的变量,那就是指针。指针是一把双刃剑,它为我们提供了操作内存的手段,但同时不恰当的使用,会为我们带来很多麻烦。


如何理解指针呢?如果把一个个内存单元看做一个个房间,指针就好像一个个指示牌(内存地址),通过指示牌,我们也可以找到房间(内存单元),查看房间里面的东西。


比如现在有一个整型变量a,我们可以理解为一个门牌号为a的房间,我需要拿到这个房间的指示牌,只需要下面的语句

int a = 0;

int *ptr_a = &a;


这样,a的内存地址就被赋值给了一个名称为ptr_a的整型指针变量。如果我们想查看ptr_a这个指示牌指示的房间中的内容,可以使用下面的语句

printf("*ptr_a = %d\n", *ptr_a);


如果想看ptr_a中保存的a变量的内存地址,可以这样打印:

printf("ptr_a = %p\n", ptr_a);


既然我们的指针保存了内存地址,那么我们就可以直接通过指针来修改内存地址对应位置的内容了,如:

*ptr_a = 10;

printf("new *ptr_a = %d, a = %d\n", *ptr_a, a);


输出应该为

new *ptr_a = 10, a = 10


可见,我们通过修改ptr_a指向的内存中的内容,就改变了a变量的值(a变量代表内存中的内容)。所以,可以简单的说,*ptr_a和a是等价的。


现在我们基本了解了指针是个什么东西,总结一下:

  1. 指针保存了其他变量的内存地址;

  2. 通过*操作符,我们可以利用指针变量保存的内存地址查看和修改该地址处的内存中存放的内容;

  3. 指针变量是有类型的,如整型指针,浮点型指针等等。


指针可以进行加减运算,也可以使用++和--运算符,加i会找到当前指针位置后第i个位置的内存地址,同理,减i就是找到当前指针位置前第i个位置的内存地址。


为了说明上面的问题,我们先来了解一下C语言的数组,C语言的数组是相同类型的一组变量的集合,在内存上,这些变量的内存地址是连续的。下面是一些类型的数组:

int arr [5] = {1, 2, 3, 4, 5};

char str[5] = "abc\n";

float float_arr[3] = {1.1, 2.2, 3.3};


可以看到,数组定义的基本形式是

类型 数组名[数组大小] = {初始值};


初始值是可选的,另外,上面的例子可以看到,C语言的字符串实际上就是char数组,不过有一点需要说明,上面的abc\n总共4个字符,但是我们却分配大小为5,这是因为C语言会自动在字符串后添加一个\0,代表字符串的结束,我们应该为它预留一个位置。


数组名是数组的首地址,如果我们要访问整型数组第3个数组元素,它的索引为2(索引从0开始计),可以这样写:

arr[2] = 30;


这样,第三个整型元素就被改为了30。因为数组的名称代表了数组的首地址,我们还可以把数组名直接被赋值给一个指针变量,如:

int *int_arr = arr;

printf("arr[0] = %d\n", *int_arr);


好了,有了现在的知识,回到上面的问题,我们改变了arr[2]为30,但是怎么查看呢?有两种方法,一种借助于指针,一种使用[]去索引

printf("*(int_arr + 2) = %d, *(arr + 2) = %d, arr[2] = %d\n", *(int_arr + 2), *(arr + 2), arr[2]);


前两种方法都是借助于指针,而第三种使用了索引。指针的加减运算跳过多少地址,是按照你的类型决定的,比如int占用4个字节,则int类型的指针加1,就是跳过4个地址,找到了下一个int存放的位置。


此时的你可能会感觉C语言指针真是一个好东西,灵活方便。现在就来看看使用指针的危险之处,我们在使用指针时,一定要注意这些问题:

  1. 指针没有初始化时,是一个随机值,直接使用它就是去修改了一块不知是什么地址处的内存的内容,数据也许很重要,篡改会导致其他程序的崩溃,或者系统的崩溃,所以使用指针前切记要进行初始化;

  2. 指针有空类型,为NULL,如果直接使用NULL,会导致程序崩溃,所以我们在使用指针前,一定要记得进行判空,尤其是作为函数(将来会介绍函数)返回值的指针,我们更要多加小心;

  3. 如果使用指针指向了数组,千万不可以出现越界访问,也就是访问了超出数组第一个或者最后一个元素地址位置的其他内存,危害同第一条。


上面列举的问题,编译器不会报错,但是运行时会出现问题,如果代码规模较大,排查错误会更加费劲。常见的情况是程序莫名其妙崩掉了,或者更可怕的是另一段代码,无论语法还是逻辑貌似都没有错误,但是却执行错误,那很可能是这段代码的内存数据被某一个指针篡改了。


了解了指针,再来说说C语言内存分配的方式。我们之前使用的方式,都是静态分配内存,比如上面大小为5的整型数组,进程在启动时,就会为我们分配这块内存空间,直到进程结束。但是,如果这是一块很大的内存,比如1G,那么我们采用这种方式,进程就会占用过多的内存,C语言为我们提供了一种方式,叫动态内存分配,也就是说,在需要的时候我们向系统申请一块内存,当我们不需要的时候,再还给系统,这样,我们的内存利用率会更高,流动性会更好。


下面是一个例子,演示了动态内存分配:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
	int size = 10;
	int len = size * sizeof(int);
	int *arr = (int *)malloc(len);
	if (NULL == arr) {
		printf("malloc error\n");
		return 1;
	}

	memset(arr, 0, len);
	for (int i = 0; i < size; i++) {
		arr[i] = i;
	}


	for (int i = 0; i < size; i++) {
		printf("arr[%d] = %d\n", i, arr[i]);
	}
	free(arr);

	return 0;
}

使用stdlib.h下的malloc函数进行动态内存分配,这里分配了10 *sizeof(int)个字节大小的内存空间给arr,实际上就是10个int型的空间大小,之后使用string.h中的memset将分配的内存中的脏数据清除,malloc只负责分配空间,但是不负责清除之前这块内存中存在的数据。然后我们对arr进行了赋值并打印了数组的内容,最后使用stdlib.h下的free释放内存。


free的调用是至关重要的,如果我们不调用free,内存会保留至进程结束,如果我们定义一个循环,循环中每次malloc一块内存,但是我们忘记了free,很有可能会导致我们内存被耗尽,这就是所谓的C语言的内存泄漏问题,就好比身体一直在慢性出血,短时间发现不了,但是程序长久运行下去,最后只会消耗殆尽。因此,对于需要24小时不间断运行的程序,一般要进行长期拷机测试,或者使用一些内存泄漏检测工具对代码进行细致的检查,C语言程序不应该出现任何一处内存泄漏。


从今天的内容大家应该有所体会,C语言非常灵活,但是我们在使用中要关注很多问题,需要我们更加细致的对待我们的程序,所以C语言的开发难度更大,开发时长更长,除了关注业务问题,代码本身的质量保证也是一项重要的工作。但是,我们为什么还要使用C语言呢?个人认为有三方面原因:

  1. 硬件开发和系统开发工作比较底层;

  2. C语言程序运行效率很高;

  3. 使用C语言开发核心模块,更有利于保护公司的核心技术。

本次及之前老汤回味C语言部分的代码已经上传至github


猜你喜欢

转载自blog.csdn.net/yjp19871013/article/details/80546792