初阶指针
1.前言
接下来我们就要开始学习期待已久的指针了。其实我们前面已经有初步了解了部分指针的知识,如数组首元素、&操作符、*操作符等,相信学完后,你一定会对指针有更深的认识。
2.指针的概念
- 在了解指针前,我们要了解下内存。
内存
内存是电脑的存储设备,内存的大小有4G、8G和16G。程序在运行时会加载到内存中,也会使用内存空间。
内存单元
在内存空间中,一个内存单元为一个字节,在每次将程序加载到内存当中时,每个内存单元都会有自己的编号,通过这个编号,我们就可以找到这块内存单元,而这个编号又叫做地址,地址在C语言中又叫做指针。
结论1
指针就是地址。
- 已知整形有四个字节,那么对一个整形进行&时,取出的是哪个内存单元的地址?
int a = 1;
int * pa = &a;//取出a的地址放到pa中,因为a是整形,所以指针类型是整形
a是整形变量,&a取出的是第一个字节的地址(较低的地址)。
或者我们可以调试查看下a和pa的地址是否相同,a的地址是第一个字节的地址,如果相同,说明pa是指向a的第一个字节的地址。
结论2
从上面我们对指针的叫法来看,指针也可以指指针变量。
3. 内存单元是如何编号的
对于32位机器上,有32根地址线,地址线需要通电(高电平和低电平),产生电信号,电信号可以转换成数字信号(1和0)。32根地址线产生的电信号可以作为内存单元的编号,一共可以形成232个编号,也就是有232个地址 。
问题
内存的大小是多少
一个地址管理一个内存单元,一共有232个内存单元,232个内存单元就是232个字节。根据1KB=1024byte,1MB=1024KB,1GB=1024MB,我们可以计算出内存大小是4G。
4.指针变量的大小
在32位机器上,由上面可知,地址由32根地址线产生的数字信号(二进制)组成,地址的大小是32个比特,所以指针变量存放地址需要4个字节。同理,64位机器上,指针变量的大小是8个字节。
5.指针类型
既然在32位机器上,指针的大小是4个字节,为什么还要有int*、char*、float*等不同类型 的指针?为什么不创建一个通用的类型?反正指针的大小是一样的。
其实指针变量是有意义的。
- 用不同类型的指针变量接受地址,分别解引用,并对其赋值,看看他们的值发生什么变化
int main()
{
int a = 0x12345678;//赋给a一个十六进制的值(305419896)
int* pa = &a;//创建一个整形指针,存放a的地址
*pa = 0;//对这个指针进行解引用,访问这个地址的内容,并把0赋给它
int b = 0x12345678;//赋给b一个十六进制的值(305419896)
char* pb = (char*)&b;//创建一个字符指针,存放a的地址
*pb = 0;//对这个指针进行解引用,访问这个地址的内容,并把0赋给它
printf("%d %d",a,b);
}
结果为0 305419776
注意
将不同类型的变量的地址放到指针变量中,需要强制类型转换成指针变量的类型。
我们能明白*pa=0,因对一个整形指针进行解引用能够访问到4个字节,但不清楚为什么 *pa=0,难道是对char类型的指针解引用访问到整形变量b的一个字节?我们可以再调试看看内存。
我们可以很清楚地看到 * pb=0后,只是对b的较低地址的那个字节进行赋值0,也就是说对char类型的指针变量进行解引用,访问较低地址的那个字节(一个字节)。
意义1
指针变量决定了指针进行解引用操作的时候,一次性访问几个字节,决定访问权限的大小。
char * 类型,解引用访问一个字节;short*类型,解引用访问两个字节;int * 类型,解引用访问四个字节。
2. 不同类型的指针加1,看看它们分别跳过几个字节
int a = 0x11223344;
int * pa = &a;
char * pb = &a;
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf('%p\n', pb);
printf("%p\n", pb + 1);
我们会发现pa和pb的值是相等的,pa+1跳过四个字节,pb+1跳过一个字节。就是这个指针变量本身是什么类型,对这个指针变量进行+n操作,就跳过sizeof(pa或pb)*n个字节。
意义2
指针类型决定指针的步伐(即指针+1跳过几个字节)。
6.野指针
野指针是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
6.1野指针的成因
- 指针未初始化
int main()
{
int*p;
*p = 1;
}
从函数栈帧可知,在一个函数内创建一个局部变量,局部变量不初始化,里面是随机值。我们没对p进行初始化,它就随意指向空间,对其进行解引用操作,就是随意访问那个地址,这是非法的,所以p是野指针。
- 指针越界访问
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int* p = arr;
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i = 0;i<=sz;i++)
{
printf("%d ",*p);
p++;
}
}
不难发现数组有10个元素,我们进行了11次的解引用和打印,当i = sz时,p对不属于数组的元素进行解引用,属于越界访问。所以p是野指针。
- 指针指向的空间释放
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d",*p);
return 0;
}
由函数栈帧可知,a是我们在函数内创建的变量,当函数调用结束返回时,这个变量就被销毁,而我们把他的地址返回,此时p接受的是已经被销毁的a的地址,这块空间早已不属于a。我们对p进行解引用就是非法访问,p就是野指针。就像你已经被酒店的房间退了,你还想进去里面睡觉。
但你会发现这个10竟然可以打印出来。这并不意味着你的代码就是正确的。这只是这片空间在函数销毁后未被改变(破坏)。不信,你在int *p = test后面重新调用一个函数,此时这片空间就被改变。
6.2如何规避野指针
- 指针的初始化
当我们不知道将指针初始化为什么时,我们可以把它置成空指针(NULL),NULL本质上是0,专门用来初始化指针的。
int *p = NULL;
但不能对指针进行解引用,因为0地址是用户不能使用的。所以使用前判断if(p!=NULL),再对它进行解引用。
既然NULL不能解引用(有危险),为什么还要赋NULL。因为不初始化更危险,解引用后会随意访问,但NULL相当于将它限制在0这个值,对其解引用只能访问0。
- 小心指针越界
- 指针指向空间释放,及时置NULL
- 避免返回局部变量的地址
- 使用指针之前检查有效性
7.指针运算
7.1指针±整数
在前面我们已经讲过,p+n就是跳过n*sizeof(指针类型)个字节,减法也相同。
#define N_VALUES 5
int main()
{
float values[N_VALUSE];
float *vp;
for(vp = &values[0];vp<&values[N_VALUES])
{
*vp++ = 0;//++优先级高于*操作符,但因为是后置++,
//所以先对vp进行解引用,再对vp++
}
}
7.2指针-指针
int arr[0]={0};
int* p0 = &arr[0];
int* p9 = &arr[9];
printf("%d",p9-p0);
结果为9。
|指针-指针|得到的是两个指针之间的个数,但有前提:两个指针指向同一块空间,并且类型要相同。
例子
写一个函数求字符串的长度。
int my_strlen(char *str)
{
char * start = str;
while(*str!='\0')
{
str++;
}
return str-start;
}
int main()
{
char arr[] = "abcdef";
int ret = my_strlen(arr);
printf("%d",ret);
}
结果为5。
7.3 指针的关系运算
其实就指针与指针的比较
for(vp = &values[N_VALUES;vp>&values[0];)
{
*--vp = 0;//先减减,再解引用
}
在这个例子中我们发现可以把数组最后元素后面的那个元素的地址放在指针中,但没对其进行解引用,因此是合法的。
规则
允许指向数组元素的指针与指针向最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
8.指针与数组
- 指针和数组是不同的对象
指针是一种变量,存放地址的,大小为4或8个字节。
数组是一组相同类型元素的集合,是可以放多个元素的,大小是取决于元素个数和元素的类型的。 - 数组的数组名是首元素的地址,地址是可以放在指针变量中,可以通过指针访问数组。
int main()
{
int arr[10] = {0};
int *p = arr;
int i =0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i = 0;i<sz;i++)
{
*p = i+1;
p++;
}
i = arr;
for(i = 0;i<sz;i++)
{
printf("%d ",*p);//*(p+i)==*arr[i] == *(arr+i)==*(i+arr)==&i[arr]
p++;
}
return 0 ;
}
因为[ ]是操作符,操作数i和arr是操作数,类似于a+b一样可以写成b+a,满足交换律,所以arr[i]可以写出i[arr]。
9.总结
今天学习了初阶指针的部分内容,介绍了指针概念、指针类型、内存、指针与数组等。