初识C语言指针
前言
相信大家肯定都听说过“学不好指针就学不好C语言”类似的话,当然,我也同样认可这句话。指针是C语言中最基础、最重要的概念之一,正是因为指针的存在,才使得C语言成为一门强大的、高效的、灵活的编程语言。指针的存在使得C语言可以进行复杂的内存操作,能够更好地控制程序的行为,同时也能够实现高效的数据结构和算法。
一、指针是什么?
简单的来说,指针就是内存中每一个最小单位空间的编号,也就是我们常说的地址。那该怎样去理解这些呢?
如上图,我们可以将内存分成许多个小空间,每个空间的大小为一个字节(最小单位空间),我们可以给每一个空间一个不同的编号,而这个编号就是地址。当然,在计算机中,这些内存的编号并不是简单的1,2,3……而是由32或64个二进制序列表示的。那为什么还会有32与64之分呢?
原因地址的二进制序列的产生是通过计算机的地址线产生的。列如对于32位机器共有32根地址线,在通电时每根地址线都会产生低电平或高电平(低电平为0,高电平为1)。因此就可以产生2的32次方个地址(指针):
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…………
11111111 11111111 11111111 11111111
因此每个指针都为32个比特位也就是(32/8=)4个字节大小,又因为每一个地址代表的一个字节的空间,因此计算机就可以给(2^32/1024/1024/1024=)4GB的空间来进行编址。 因此,如果在64位机器上,每个指针大小就为(64/8=)8的字节大小。
最后要注意的是,为了便于表达地址,我们系统常用十六进制来表示指针(地址),如下图:
代码表示如下:
#include <stdio.h>
int main()
{
int a = 5;
int* p = &a; //此处对于变量a,要取出它的地址需要使用&操作符
//a变量占用四个字节,但这里是将四个字节中第一个字节的地址(低地址那一段)放在p中
printf("%p", p); //打印地址需要用%p
return 0;
}
二、指针与指针类型
众所周知,变量有不同的类型如:整型,浮点型等。而指针有没有类型呢?答案是有的。就比如:一个整型变量为a,要将&a(a的地址)放在指针变量p中,那该怎么写呢?又或是字符型变量又该怎么去写呢?
int a = 0;
char b = 0;
short c = 0;
int * pa = &a ;
char * pb = &b;
short * pc = &c;
这里我们可以看到定义指针的方式为:type+*(中间空格可有可无)。并且
int类型的变量要存放在int*类型的指针变量中;
char类型的变量要存放在char*类型的指针变量中;
short类型的变量要存放在short*类型的指针变量中……`
这时,肯定就有人问定义这些指针类型的意义是什么,又或者说,不都是用来表示地址的,为什么要定义怎么多类型呢?接下来我将用代码来解释:
首先我们可以通过观察数据在内存中的变化来解释,如下图:
通过上面两张图不难发现,int*/类型的指针访问的是四个字节,而char*类型的指针访问的是一个字节。因此我们就可以得出这样一个结论:指针类型可以决定指针解引用时访问多少个字节,type *p中p解引用时访问的字节数就等于sizeof(type)。
其次,我们在来观察以下这段代码:
#include <stdio.h>
int main()
{
char st = 'a';
int a = 5;
char* p1 = &st;
int* p2 = &a;
printf("%p\n", p1);
printf("%p\n", p1 + 1);
printf("%p\n", p2);
printf("%p\n", p2 + 1);
return 0;
}
结果如下:
由此可见不同类型的指针变量加一之后其值是不同的,并且通过观察也不难发现type* p的指针变量加一之后跳过了sizeof(type)个字节大小的地址。因此我们就可以得出另外一个结论:指针的类型决定了指针向前或向后“走一步”的距离大小。
三、指针运算
1.指针±整数
指针的运算一般与数组相结合。在数组中,假设指针变量p表示数组首元素地址,这p+n则表示第n个元素的地址,就如下代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int* parr = arr; //数组名为数组元素首地址,此处即为arr[0]的地址
for (i = 0; i < 10; i++)
{
arr[i] = i; //给arr的每个元素赋值
}
for (; parr < &arr[10]; parr++)
{
printf("%d ", *parr); //parr++之后就指向了下一个元素的地址,因此经过i次循环之后*parr=arr[i]
}
return 0;
}
图解如下:
2.指针-指针
要进行指针减指针的运算的前提是两指针必须指向的是同一块空间(数组)。其次,两指针相减得到的是两指针之间的元素数,如:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int* parr = arr; //数组名为数组元素首地址,此处即为arr[0]的地址
int* parr2 = &arr[5];
printf("%d", parr2 - parr); //arr[0]到arr[5]之间共有5的元素
printf("%d", parr - parr2);
return 0;
}
另外要注意的是,只有指针-指针的运算,没有指针+指针的运算。
3.指针的关系运算
地址是有大小的,指针的关系运算就是比较指针的大小。列如:
#include <stdio.h>
int main()
{
int arr[5] = { 0 };
int* arrp = &arr[0];
for (arrp = &arr[0]; arrp < &arr[5]; arrp++)
{
printf("%p\n", arrp);
}
return 0;
}
此外,有这样一项特殊的规定:**允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。**因此,我们再写代码时要防止出现类似&(arr[0]-1)的代码。
四、野指针
野指针的概念
野指针就是指针指向的位置是不可知的、随机的、不正确的、没有明确限制的。
野指针的成因
1.指针未初始化
如:
#include <stdio.h>
int main()
{
int* p; //指针未初始化
*p = 5;
printf("%d", *p);
return 0;
}
运行结果:
此时程序运行的结果就会报错。
2.指针越界访问
如:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr; //数组名为数组首元素(arr[0])地址
int i = 0;
for (i = 0; i < 12; i++)
{
*(p++) = i;
}
return 0;
}
运行结果为:
此时代码也会报错。原因就是当i=10,11时,进入循环使得arr[i]越界了,又因为p指向的是arr[i]的地址,这就导致了产生了越界访问,就使得p变成了野指针。
3.指针指向的空间释放
如:
#include <stdio.h>
int* test()
{
int a = 5;
return &a;
}
int main()
{
int* p = test();
printf("%p\n", p);
printf("%d\n", *p);
return 0;
}
运行结果为:
此处会发现*p的值并不是我们所想的5。原因是在每一个函数被调用时,操作系统都会开辟一片新的内存空间来供这个代码存储,函数被调用完之后,都会将自己所占用的那片内存都还给操作系统,从而导致a的地址虽然传过去了,但是a的值发生了改变。并且此时p的指向是不确定的,所以此时的p就属于野指针。
如何规避野指针
1.指针初始化
在创建变量时,我们都知道要初始化。同理,我们在创建指针变量时也要进行初始化。如果可以明确指针指向的是谁的地址则至今对其初始化,若不能明确则可将指针暂时初始化成NULL(必须大写)。如:
{
int a=0;
int* pa=&a;
int* pb=NULL;
return 0;
}
2.小心指针越界访问
使用指针时要确保指针不会进行越界访问。
3.指针指向空间释放及时置NULL
当指针指向的空间释放时,该指针的指向就是不可知的,因此该指针成为了野指针。所以对于指向的空间释放后的指针,我们要及时的置NULL,重新对其初始化。
4.避免返回局部变量的地址
因为局部变量在其作用域之外是会被销毁的,这就导致了原来指向该局部变量所占用的空间释放了,就导致了原来指向该局部变量的指针成为了野指针。
5.指针使用之前检查其有效性
可以在使用指针前通过某些代码来检验该指针是否可以被使用。