指针是什么?
指针理解的要点:
1.指针是内存中一个最小单元的编号,也就是地址。
2.平时口语中所说的指针,通常指的是指针变量,用来存放内存地址的变量。
3.内存是电脑上的存储设备,一般为4/8/16G。程序运行的时候会加载到内存中,会使用内存空间
总结:指针就是地址,口语中说的指针通常指的是指针变量
图形理解:
指针变量
我们可以通过&(取地址操作符)取出变量的起始地址,并把这个地址存放在一个变量中,这个变量就是指针变量。
问题①:一个小的内存单元是多大?
问题②:如何编址?
C语言规定一个字节对应一个地址;对于32位机器·,假设有32根地址线,每根地址线寻址的时候产生高电平(高电压)或者低电平(低电压),在计算机用0或1表示。
那么32根地址线产生的地址如下:
这里有2的32次方个地址,每个地址标识一个字节,我们可以给(2 ^ 32 Byte== 2 ^ 32/1024KB== 2 ^ 32/1024/1024MB == 2 ^ 32/1024/1024/1024 ==4GB) 4G的空间进行编址。
同样的原理,那64位机器,对应64根地址线,能编址16GB的空间。
总结:
①在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就是4个字节。
②在64位机器上有64个地址线,地址得用8个字节的空间来存储,这时一个指针变量的大小就是8个字节。
③指针变量是用来存放地址的,一个地址标示一个内存单元。
④指针变量在32位平台是4个字节,在64位平台是8个字节。
⑤在vs编译器下x86是32位机器的平台,x64是64位机器的平台。
我们都知道变量有不同的类型,整型、浮点型等。那指针变量有没有类型呢·?
准确来说:有的。
当有这样的代码:
int num=0;
p=#
要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?
char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;
这里我们可以看到,指针的定义方式:type(类型)+*。
char * 类型的指针就是为了存放char类型变量的地址。
short * 类型的指针就是为了存放short类型变量的地址。
int * 类型的指针就是为了存放int类型变量的地址。
eg:
#include
int main()
{
int n = 10;
char* pc = (char*)&n;//把a的地址(int*类型的指针),强制转换为char*类型,char*类型只能访问一个字节。
int* pi = &n;//int*类型可以访问4个字节
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc + 1);
printf("%p\n", pi);
printf("%p\n", pi + 1);
//%p是以十六进制的形式打印地址
return 0;
}
运行结果如下:
总结: 我们在使用中期望访问几个字节的内存,然后决定用什么样的指针类型去访问,指针类型决定了指针向前或者向后走一步有多大(距离)。
eg:
#include
int main()
{
int n = 0x11223344;//0x表示十六进制的数
char* pc = (char*)&n;
int* pi = &n;//重点观察在调试过程中内存的变化
*pc = 0; //访问1个字节
*pi = 0; //访问4个字节
return 0;
}
*pc=0执行后:
*pi=0执行后:
总结: 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。比如:char * 的指针解引用只能访问一个字节,而int * 的指针的解引用能访问四个字节。
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
#include
int main()
{
int* p;//局部变量指针未初始化,里面的值为随机值
*p = 20;
return 0;
}
注意: 我们在定义指针变量的时候应该进行初始化,即令指针变量等于空指针或者某个变量的地址(int *p=NULL或 int *p=&a)。
#include
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*(p + i) = i;
//当指针指向的范围超出数组arr的范围时,p就是野指针
}
return 0;
}
程序运行的结果:
注意: 我们存在指针变量的程序时,应该注意指针越界。
这里指的动态内存函数malloc、calloc、realloc在使用的时候,需要开辟内存空间,函数返回的值用指针接收,使用完之后,应释放掉指针指向的空间,并把指针置为空指针。在这里我们只需要简单了解一下即可,在后面的C语言进阶栏目中我们将详细介绍。
#include
int main()
{
int arr[10] = { 0 };
int* ptr = NULL;
ptr = (int*)malloc(10 * sizeof(int));
if (NULL != ptr)
{
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr+ i) = 0;
}
}
free(ptr);//释放掉ptr所指向的动态内存空间
ptr = NULL;//指针置空
return 0;
}
eg1:
#include
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("a=%d", *p);
return 0;
}
上面的代码中存在错误,函数test使用完之后,该函数开辟的空间就销毁了,即还给操作系统了。在主函数main中再用int *p来接收a的地址,并把用printf函数打印出来是不合法的。那为什么还能把a=10打印出来呢?这是因为操作系统虽然回收了test函数开辟的空间,但是并没有去使用它,恰好该地址存放的值还是10。
那我们稍微改动一下代码看看结果又如何?
#include
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
int i = 20;
printf("%d\n", i);
printf("a=%d", *p);
return 0;
}
代码执行结果如下:
我们看到曾经a的地址里面现在是一个随机值,这是因为操作系统为int i=20开辟了空间,让曾经a的地址中的值被覆盖掉了。这是有关函数栈帧的内容,感兴趣的可以看一下我的这篇博客:函数栈帧的创建和销毁
eg2:
#include
int main()
{
int* p = NULL;
int a = 10;
p = &a;
if (p != NULL)//判断p是否为空指针
{
*p = 20;
}
return 0;
}
#include
#include
int main()
{
int* p = NULL;
int a = 10;
p = &a;
assert(p);
//断言assert中可以放一个表达式
//表达式的结果为假,就报错,如果为真啥事都不发生
//空指针NULL的本质是0
*p = 20;
return 0;
}
eg:
#define N 5
#include
int main()
{
float values[N] = { 0 };
float* vp = NULL;
for (vp = &values; vp < &values[N];)
{
*vp++ = 1;//后置++,先使用,再+1
//等价于*vp=1;vp++;
}
vp = &values;
int i = 0;
for (i = 0; i < N; i++)
{
printf("%.1f ", *(vp + i));
}
return 0;
}
总结: 指针加减整数n,指针向前或者向后跳过n个该类型的字节数,指向另一块空间。
eg:
//用指针-指针模拟strlen函数
#include
int my_strlen(char* p)
{
char* start = p;
while (*p)
{
p++;
}
return p - start;
}
int main()
{
char arr[] = "abcdef";
int ret= my_strlen(arr);
printf("%d", ret);
return 0;
}
代码执行结果如下:
总结: 在两个指针指向同一块空间的前提下,指针减指针得到的是两个指针之间的元素个数;如果两个指针不是指向同一块空间,指针减指针得到的结果无实际意义。
eg:
#define N 5
#include
int main()
{
int arr[N] = { 1,2,3,4,5 };
int* p = NULL;
for (p = &arr[5]; p > &arr[0];)
{
*--p = 0;//等价于p--;*p
}
p = arr;
int i = 0;
for (i = 0; i < N; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
#define N 5
#include
int main()
{
int arr[N] = { 1,2,3,4,5 };
int* p = NULL;
for (p = &arr[N-1]; p >= &arr[0];p--)
{
*p=0;
}
p = arr;
int i = 0;
for (i = 0; i < N; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
简化代码原理:
简化代码运行结果:
简化代码中,虽然容易让人理解并且在绝大部分编译器上是可以顺利完成任务的,然而我们应该避免这样写,因为标准并不保证它可行。就是你觉得它可行,它不一定合法(符合语法)。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
eg1:
#include
int main()
{
int arr[6] = { 1,2,3,4,5,6 };
printf("arr=%p\n", arr);
printf("&arr[0]=%p\n", &arr[0]);
return 0;
}
代码运行结果为:
①从上面的运行结果可以看出,一维数组的数组名表示数组首元素的地址,但有两种特殊情况如下:
1.sizeof(数组名),这里的数组名指的是整个数组,计算的是整个数组的大小,单位是字节。
2.&数组名,这里的数组名表示整个数组的地址,即取出的是数组的地址。
②指针和数组是不同的对象,指针是一种变量,可以存放地址,大小4/8个字节;数组是一组相同类型元素的集合,可以放多个元素,大小取决于元素个数和元素的类型。
③地址可以存放在指针变量中,可以通过指针变量访问数组。
eg2:
#include
int main()
{
int arr[6] = { 1,2,3,4,5,6 };
int i = 0;
int* p = arr;
printf("%d\n", arr[i]);
printf("%d\n", *(arr + i));
printf("%d\n", *(i + arr));
printf("%d\n", i[arr]);
printf("%d\n",*(p+i));
printf("%d\n", p[i]);
return 0;
}
代码运行结果:
④C语言中[ ]是一个双目操作符,i和arr是两个操作数,可以像a+b=b+a一样执行的加法交换律,再从编译器的执行代码的结果,可知arr[i]<=> * (arr+i)<=> * (i+arr)<=>i[arr]。
eg3:
#include
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("&arr[%d]=%p <===> p+%d=%p\n", i, &arr[i], i, p + i);
}
return 0;
}
代码运行结果:
⑤代码中p+i计算的是数组下标为i的地址,我们可以通过指针来访问数组。
eg4:
#include
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//存放一维数组首元素地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ",*(p+i));
}
return 0;
}
二级指针是什么呢?指针变量也是变量,是变量就有地址,那么二级指针就是用来存放指针变量(一级指针变量)的。
eg:
#include
int main()
{
int a = 10;
int* pa = &a;//一级指针
//pa旁边的*说明变量pa是指针,前面的int说明该指针指向的变量是int类型
int** ppa = &pa;//二级指针
//ppa旁边的* 说明变量ppa是指针,前面的int*说明该指针指向的变量是int*类型
int*** pppa = &ppa;//三级指针
//ppa旁边的* 说明变量pppa是指针,前面的int**说明该指针指向的变量是int**类型
return 0;
二级指针的解引用运算
- *ppa通过ppa中的地址进行解引用,这样找到的是pa,*ppa访问的就是pa。
- **pa先通过*ppa找到pa,然后对pa进行解引用操作,*pa访问就是a。
#include
int main()
{
int a = 20;
int* pa = &a;
int** ppa = &pa;
//*ppa<==>pa,**pa<==>*pa<==>a
**ppa = 10;
printf("%d\n", **ppa);
printf("%d\n", a);
return 0;
}
指针数组是指针还是数组呢?
那我们类比之前学过的char arr[ ]字符数组—用来存放字符的数组;
int arr[ ]整型数组—用来存放整型的数组;
顾名思义,指针数组就是用来存放指针的数组。
指针数组的形式:
#include
int main()
{
int* arr[] = {NULL};
//arr[]说明arr为数组,int*则说明数组每个元素类型是int*类型
}
为了更好的理解指针数组,下面我们使用一维指针数组,模拟实现二维数组。
#include
int main()
{
//模拟一个三行四列的二维数组
int a[] = { 1,2,3,4 };
int b[] = { 5,6,7,8 };
int c[] = { 9,10,11,12 };
int* arr[3] = { a,b,c };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}return 0;
}
代码运行结果:
图形理解:
注意:如果没有把某变量的地址存在指针变量中,则该变量的地址不会占用内存空间(即变量才会占内存空间),就比如int a=10,a的地址本身就存在了,但是a变量的地址没有(int* p=&a)放到指针变量中,所以变量a的地址不会占用内存空间;变量名本身不占内存空间,我们命名变量是给程序员看的,而编译器在执行过程中拿到的是该变量的地址,而不是变量名本身。
本章我们学习了
①什么是指针?
②指针和指针类型
③野指针含义、成因及规避方法
④指针的运算
⑤指针和数组的关系
⑥什么是二级指针?
⑦什么是指针数组?
希望对大家初识指针有些许帮助!若有不对的地方,欢迎大家指正!最后感谢大家的阅读!