目录
一、指针是什么?
1.1 指针
指针理解的两个要点:
- 指针是内存中一个最小单元的编号,也就是地址。
- 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
要理解指针,我们就要先理解内存:内存是一块存储区域,为了更大化的利用空间,我们把内存划分为一个个很小的内存单元,我们会给每一个内存单元编号,这个编号也就是地址即指针。
1.2 指针变量
1.2.1 指针变量是什么?
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放在一个变量中,这个变量就是指针变量。
创建变量的本质就是向内存中申请一块空间。
我们接下来来看一段代码:
int main()
{
int a = 10;
char ch;
int* pa = &a;
char* pc = &ch;
printf("%p\n", &a);
return 0;
}
创建了一个int类型的变量,在内存空间里面会开辟四个字节的空间,创建一个char类型的变量,在内存空间里面会开辟一个一个字节的空间。
总的来说:指针变量就是用来存放地址的变量,在上述代码中pa存储a的地址,pa就是一个指针变量,我们需要注意的是:存放在指针中的值都被当成地址处理。
1.2.2 指针变量的大小
要知道指针变量的大小,我们就要知道在内存中是如何编址的。
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)。
那么32根地址线产生的地址就会是:
0000000000000000000000000000000000000000000000000000000000000001
......
1111111111111111111111111111111111111
这里就会有2的 32次方个地址。
每个地址标识一个字节,那么我们就可以给(2^32byte == 2^32/1024KB ==2^32/1024/1024MB == 2^32/1024/1024/1024GB == 4GB)4G的空间进行编址。
同样的方法,如果是64位的机器上,有64个地址线,也可以计算出能够给多大的空间进行编址。
- 在32位的机器上,地址是32个0或者1组成的二进制序列,即32个bite位,需要4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
- 在64位的机器上,地址是64个0或者1组成的二进制序列,即64个bite位,需要8个字节的空间来存储,所以一个指针变量的大小就应该是8个字节。
总结:
- 指针变量是用来存放地址的,地址是唯一标识一个内存单元的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
二、指针和指针类型
变量有不同的类型,整型,浮点型等,那么指针有没有不同的类型呢?答案是有。
指针的类型是怎么定义的呢?
我们想要创建一个int类型的变量a,想要存储a的地址,这时我们就需要一个指针来存储a的地址,这个指针变量是什么类型的呢?
存储int类型的变量a的地址,指针类型是int*类型。
同理,如果我们要存储一个char类型的变量b的地址,这时我们就可以写成:char* pb = &b;
在讲解指针变量的大小的时候,我们已经说明指针变量在32位的平台上占4个字节,在64位的平台上占8个字节,既然他们在内存中所占的字节大小相同,那么设置不同指针类型的意义在哪里?有以下两个意义。
2.1 指针类型的意义
2.1.1 指针类型决定了访问空间的能力
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
}
我们可以看出在pa(存储a的地址的变量)的类型是int*时,在对pa解引用操作时,我们会访问4个字节的空间。
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;
}
如果使用char*类型的指针pa来存储int类型的a的地址,在对pa进行解引用的时候只能访问一个字节的空间。
我们发现int* 变为 char* 类型时,指针的操作能力就变化了。
指针类型决定了访问空间的能力,决定了指针解引用操作时,访问几个字节。
char * 类型的指针解引用访问一个字节的空间
int * 类型的指针解引用访问四个字节的空间
double * 类型的指针解引用访问八个字节的空间
2.1.2 指针类型决定了+1时跳过几个字节
指针类型决定了+1时跳过几个字节,即指针类型决定了指针的步长(向前或向后走一步都多大的距离)。
int* 类型的指针+1,意思是跳过一个整形,也就是4个字节。
char* 类型的指针+1,意思是跳过一个字符,也就是1个字节。
double* 类型的指针+1,意思是跳过一个double类型,也就是8个字节。
三、野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
即指针指向的空间不是自己的。
3.1 野指针的成因
3.1.1 指针没有初始化
在c语言中,局部变量都是在栈区创建的,如果局部变量没有初始化,会默认为随机值。
下面我们来举一个野指针的例子:
int main()
{
int* pa; //局部变量指针未初始化,默认为随机值,而这个随机值会被当做地址
*pa = 20;
return 0;
}
在上述的代码中pa就是一个野指针,pa属于局部变量,局部变量没有初始化,就会默认为随机值,而默认的这个随机值会被当做一个地址,这个地址所维护的空间不属于我们当前的程序,会出现问题。
3.1.2 指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++)
{
*(p++) = i;
}
return 0;
}
上述代码中数组arr的下标应该是从0-9,而在访问的时候数组访问了0-11的下标,属于越界行为,当指针指向的范围超出数组arr的范围时,p就是野指针。
3.1.3 指针指向的空间释放
int* text()
{
int num = 100;
return #
}
int main()
{
int* p = text();
*p = 200;
return 0;
}
在上述代码中,p就是野指针。
3.2 如何规避野指针
-
指针初始化
int main() { int a = 0; int** pa = &a; //明确初始化 int b = 0; int* pb = NULL; //当不知道pb赋什么值时,用空指针NULL初始化 return 0; }
-
小心指针越界
-
指针指向空间释放,及时置NULL
-
避免返回局部变量的地址
-
指针使用之前检查有效性
int main() { int* p = NULL; if (p != NULL) { printf("%d\n", *p); } return 0; }
检查是否为空指针。
四、指针运算
4.1 指针 +- 整数
我们已经解释过指针+1的情况,同理int *类型的指针+-n就是跳过n个4个字节的空间。
int main()
{
double arr[5] = { 0 };
double* p = arr;
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%lf", *(p + i));
}
return 0;
}
上述代码中p+1就相当于跳过一个double类型的空间,即8个字节的空间。
4.2 指针 - 指针
指针-指针的前提条件是,指针指向的是同一块连续的空间。
指针-指针的结果是什么呢?
我们用代码来观察一下:
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);
printf("%d\n", &arr[0] - &arr[9]);
return 0;
}
我们发现指针-指针得到的(绝对值)是指针与指针之间元素的个数。
在这里,我们可以想到strlen函数实现的另一种方式:
int my_strlen(char* p)
{
char* str = p;
while (*p != '\0')
{
p++;
}
return p - str;
}
int main()
{
char arr[] = "abcde";
int ret = my_strlen(arr);
printf("%d\n", ret);
return 0;
}
4.3 指针的关系运算
#define N_VALUES 5
float values[N_VALUES];
float* vp;
int main()
{
for (vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
return 0;
}
将代码简化:
#define N_VALUES 5
float values[N_VALUES];
float* vp;
int main()
{
for (vp = &values[N_VALUES]; vp >= &values[0];vp--)
{
*vp = 0;
}
return 0;
}
在大多数的编译器中是可以完成任务的,但是我们应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
五、指针与数组
首先我们需要知道:
- 数组和指针不是一个东西,数组能够存放一组相同类型的数,连续的空间,数组的大小取决于元素的个数,指针是一个变量,是用来存放地址的变量。
- 数组与指针的联系:数组名就是数组首元素的地址,交给一个指针变量后,可以通过指针来访问数组。
在接下来的代码中,我们使用指针来访问数组的每一个元素:
int main()
{
int arr[10] = { 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++);
}
return 0;
}
六、二级指针
指针变量也是变量,是变量就会有地址,那指针变量的地址存放在哪里?
这就是二级指针。
int main()
{
int a = 0;
int* p = &a; //p就是指针变量,一级指针变量
int** pp = &p; //pp就是二级指针
return 0;
}
对于二级指针的运算有:
- *pp通过对pp中的地址解引用,找到的是p,*pp访问的就是p。
- **pp先通过*pp找到p,再对p解引用操作,**pp找到的就是a。
七、指针数组
指针数组是存放指针的数组。
指针数组有什么用法:
7.1 存储一组相同类型元素的地址
int main()
{
int a = 0;
int b = 10;
int c = 20;
int d = 30;
int e = 38;
int* arr[] = { &a,&b,&c,&d,&e };
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", *(arr[i]));
}
return 0;
}
7.2 用一维数组模拟一个二维数组
#include<stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int arr4[] = { 4,5,6,7,8 };
int* arr[5] = { &arr1,&arr2,&arr3,&arr4 };
int i = 0;
for (i = 0; i < 4; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
//printf("%d ",*(*(arr+i)+j));
}
printf("\n");
}
return 0;
}