进一步理解指针:灵活性和动态内存分配

指针内涵

指针的作用

指针通常有两种常见的用途,第一种是给变量起个别名,用来指代某个变量;第二种是操纵动态的内存空间,根据需要进行扩大或者缩小。

1
2
3
4
5
int i = 10;
int *p = &i;
*p = 8;
printf("i: %d, *p:%d", i, *p);
// i: 8, *p:8

修改了*p的同时,i的值也被修改了,可以操作指针修改指针指向的变量,要注意的是赋予指针的值一定是个地址。在C的函数中,如果你想交换入参的值,就不得不传入指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}

int a = 1, b = 2;
printf("a:%d,b:%d\n",a,b);
swap(&a,&b);
printf("a:%d,b:%d\n",a,b);

// a:1,b:2
// a:2,b:1

指针的作用就是指向内存当中某个地址的值,并且可以对该值进行修改。与一般变量存在本质差别的是,指针存储的是一个地址值,而别的一般变量存储的是特定类型的值。

第一种用途的指针只要注意解引用符号基本不会出现太多的问题,因为不涉及到堆内存的空间分配。动态分配则不同,需要主动释放空间,下文会细说,这里暂且不表。

指针的类型

1
2
3
4
5
6
7
8
9
int *p1;
float *p2;
double *p3;
printf("sizeof p1 %d\n", sizeof(p1));
printf("sizeof p2 %d\n", sizeof(p2));
printf("sizeof p3 %d\n", sizeof(p3));
// sizeof p1 8
// sizeof p2 8
// sizeof p3 8

这里我为了方便而没有给指针赋初值,具有一定的风险

指针的类型并不决定指针的大小,一般来说它的大小随着操作系统固定的。指针的类型是为了标识指针所指向的区域存储的值类型,当你对指针进行加法操作的时候,类型的差别就开始显露了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int *a = NULL;
int arr[] = {1, 2, 3};
a = arr;

double *b = NULL;
double arrf[] = {1.1, 1.2, 1.3};
b = arrf;

printf("%p\n", a);
printf("%p\n", a + 1);
printf("%p\n", a + 2);

printf("%p\n", b);
printf("%p\n", b + 1);
printf("%p\n", b + 2);

// 000000000061FE04
// 000000000061FE08
// 000000000061FE0C
// 000000000061FDE0
// 000000000061FDE8
// 000000000061FDF0

看来编译器会根据指针的类型去计算对应地址的起始位置,当出现加减操作时,根据指针的类型大小进行计算

万能指针

既然指针的类型并不会对指针产生本质的影响。你也可以定义指针的类型为void,这样的指针叫做万能指针,它可以指向任意类型的值,不过在用之前记住指向区域的值类型,不然真的很容易出错。

1
2
3
4
void *p = (void *)malloc(sizeof(int));
*(int *)p = 12;
printf("%d %p", *(int *)p, p);
// 12 00000000001D1400

这里给p分配了int类型大小的空间,然后初始化指向值。每次在解引用指针时需要进行强制转化,不然无法编译通过。

数组和指针

数组是内存中连续的相同类型值的集合,数组的标志是[],这个在别的语言里面似乎也是这样,好像是约定俗成的。
之所以会把数组和指针搞混,可能是因为数组可以和指针进行相似的操作,下面两种访问数组的结果是一致的。

1
2
3
4
5
6
int arr[] = {1, 2, 3};
printf("%d\n",*(arr + 0));
printf("%d\n",*(arr + 1));

printf("%d\n",arr[0]);
printf("%d\n",arr[1]);

可以总结为,a[n] = *(a + n) 这对指针也适用。而且指针不止可以指向单个值,还可以指向连续的多个值,并且可以动态地进行扩大或者缩小,这就突显了指针与数组的最大区别——灵活性。
数组很不灵活,你只能初始化的时候固定数组的大小,然后就无法随心所欲地改变数组大小了。arr就只能表示数组的首地址,如果是指针的话还可以修改指向的地址。所以指针的功能更为强大,它甚至可以直接替代数组做到相同的效果。

动态内存分配

在编程的时候我们可以将内存空间视为三类,栈内存、堆内存和静态内存,这边主要介绍前两者。栈内存主要存储局部变量,比如函数域内定义的变量,特点是会在程序执行离开域之后变量会自动被释放,想栈一样后进先出;堆内存则不会收到函数域或者是块区域的影响,只会在程序退出之后清理。通过调用内存分配函数,我们可以将堆内存分配好的地址赋予指针,并且可以同时分配超过两个以上。

1
2
3
4
5
6
7
int *p = (int *)malloc(3 * sizeof(int));
p[0] = 0;
p[1] = 1;
p[2] = 2;
free(p);
p = NULL;

以上为一个指针分配了三个连续的空间,存储值为int类型,使用起来就像数组一样。最后用完不要忘了使用free()释放对应的内存空间。由于内存空间被释放,指针p的就指向了未定义的地址,所以最好将指针赋值空地址,避免直接使用导致的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int **p = (int **)malloc(3 * sizeof(int *));
*p = (int *)malloc(sizeof(int));
*(p + 1) = (int *)malloc(sizeof(int));
*(p + 2) = (int *)malloc(sizeof(int));

**p = 0;
**(p + 1) = 1;
**(p + 2) = 2;

for (size_t i = 0; i < 3; i++) {
free(p[i]);
}
free(p);
p = NULL;

这里演示了二级指针的内存分配情况,三级四级也是同理,只不过二级以上的指针用起来会很繁琐,也很容易出错,所以实际当中很少会用到(我从来没在项目代码中见过)。需要注意的是,free()需要由内而外释放。

错误例子

以下的代码是错误的案例,演示了为啥一不小心会出现内存泄露。

1
2
3
4
5
6
7
8
/* wrong code */
void alloc_int_w(int *p, int data) {
  p = (int *)malloc(sizeof(int));
  *p = data;
}

int *p = NULL;
alloc_int_w(p,10);

这里给*p分配了空间,这个函数的本意是想输入一个指针,然后给这个指针分配空间并初始化。但是没有注意*p是个形参,导致指针切换了指向,最后函数结束时形参自动释放,p指向的空间未及时释放,导致内存泄露。
正确的例子如下

1
2
3
4
5
6
7
8
/* right code */
void alloc_int(int **p, int data) {
  *p = (int *)malloc(sizeof(int));
  **p = data;
}

int *p = NULL;
void alloc_int(&p, 10);

进一步理解指针:灵活性和动态内存分配
http://www.sjblg.com/pointer-2/
作者
Jay Shen
许可协议