指针问题的核心就是认识到:指针的地址和指针的内容是两码事。它们同时归属于一个对象,却有着完全不同内涵。只要深刻把握了这一点,小心考察每一个已使用指针。犯错的可能性就会大幅度降低。
在使用链表时,很容易在指针上出问题。尽管问题的实质是指针的指向,但是用结构体指针来说明这一问题更容易理解,且容易实践。
- 首先,需要简单地解释一下指针是什么。
简单的代码诠释:
int a = 0;
int *p = &a;
printf("%p -> %p --> %d\n",&p,p,*p);
输出:
000000000064fe10 -> 000000000064fe1c --> 0
这说明,指针p本身就有一个64位地址:000000000064fe10
其内存储了一个64位的数字,而这个数字代表了a的地址。其本质上还是一个64位数。从这里,指针的双重属性—地址和内容就体现出来了。
那么空指针NULL
是什么呢?
p = NULL;
printf("%p",p);
输出:0000000000000000
。
一般来说,NULL
是不可访问的。因为操作系统把自己运行内存映射到低内存区,不允许用户态进程访问。
- 接下来定义一个结构体:
首先来看结构体的正常声明:
typedef struct Node{
int data;
struct Node *next;
} Node;
typedef struct Node *Linklist;
那么如果我们直接声明一个结构体指针会怎样呢?
Linklist p;
if(p == NULL) printf("1");
else printf("0");
输出0
,也就是说,这个指针指向的内存地址不是NULL
.
但有意思的是,它并不能访问:
printf("%d",p->data);
printf("%p",p->next);
这是因为,整个build过程已经将cpp程序划分好了。这一块就是给你的。所以已声明的指针p非空(分配的这一部分空间有内容, 其内容被诠释为地址),但是其指向的内容未必能够恰好在分配cpp内存之中。所以访问会出现段错误。或者没报错,而是不输出就停止。
一个解决办法是定义一个Node类型,然后取其地址赋给指针p。也就是把已分配在内存空间中的Node的地址交给指针p,这样访问p的内容时,就不会出现异常。
Linklist p;
Node nd;
nd.data = 1;
nd.next = NULL;
p = &nd;
printf("%d\n",p->data);
printf("%p\n",p->next);
输出:
1
0000000000000000
这样的分配基本都是在栈上分配, 如果涉及到函数的调用则最好不要使用这种方式. 如果是比较重要的变量, 全局声明则比较合适. 但一般来说, 下面这种方式会更好一点.
另外一个方法就是利用动态分配的地址空间,堆。
再看一个封装的函数:
Linklist lalloc(void)//给出内存
{
return (Linklist)malloc(sizeof(Node));
}
问题的核心就来了,我们需要对malloc函数有一个实际认知,否则便会陷入唯心主义的藩篱。ctrl点击进入stdlib.h.
void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
void __cdecl free(void *_Memory);
void *__cdecl malloc(size_t _Size);<----
void *__cdecl realloc(void *_Memory,size_t _NewSize);
_CRTIMP void *__cdecl _recalloc(void *_Memory,size_t _Count,size_t _Size);
__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。
以上内容来源于百度百科。看看即可,不必深究。
查看其函数声明后,我们知道,malloc()传回一个指向void类型的指针。进行(Linklist)
强制类型转换后,变成一个我们需要的合适类型的指针。
也就是说,我们绕过了对程序一次性分配的过程。而是在程序运行的过程中,在堆中申请一块内存,又释放一块内存。这样做的好处是,随用随取,不用则free。
这样做的坏处是:我们只能通过指针来访问我们申请的内存空间。
这样一来,一切的问题就得到的解释。
由于链表的可变长度性,一次性分配内存不过是生成一个“另类的数组”。所以动态内存堆的使用才使得链表有了真正的可变长度性。
但是这种申请分配的机制就决定了链表访问内存的麻烦,他必须一步步按顺序去访问,而不能像数组一样直接访问。这也是由堆的性质决定的。而且一旦丢失了指针,那么访问就变得不再可能,所以链表的链接是一个十分重要的问题。另外,由于堆的地址空间并非有限,所以在程序运行过程中,不能只malloc()
而不free()
。
所以,每次lalloc()
后,我们就得到了一个指向堆中内存地址的指针。这个指针的内容存放于堆中,而其地址则要看上下文才能看到。