1.指针是什么?
指针是内存中一个最小单元的编号,也就是地址
平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
我们通过图片来理解这句话:
总结:指针就是地址,口语中说的指针通常指的是指针变量
我们再来了解一下指针变量的概念:
我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量。
看代码:
#include <stdio.h> int main() { int a = 10;//在内存中开辟一块空间 int *p = &a;//使用&操作符取出变量a的地址。 //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。 return 0; }
总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于 32 位的机器,假设有 32 根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是( 1 或者 0 );
那么 32 根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就有 2 的 32 次方个地址。
每个地址标识一个字节,那我们就可以给 ( 2^32Byte == 2^32/1024KB ==
2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB ) 4G 的空间进行编址。
同样的方法,那 64 位机器,就是给 64根地址线,就是2^32*4GB的空间
在 32 位的机器上,地址是 32 个 0 或者 1 组成二进制序列,那地址就得用 4 个字节的空间来存储,所以一个指针变量的大小就应该是 4 个字节。
那如果在 64 位机器上,如果有 64 个地址线,那一个指针变量的大小是 8 个字节,才能存放一个地址
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
2.指针和指针类型
指针的定义方式是:type + *,例如:
int* 类型的指针是为了存放 int 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
char* 类型的指针是为了存放 char 类型变量的地址。
double*类型的指针是为了存放 duoble 类型变量的地址。
那么指针类型的意义是什么呢?
2.1指针的解引用
下面我们有图有真相:
通过两图对比我们发现:指针类型的决定了:指针在进行解引用操作时访问几个字节(权限)。
int* 类型的指针在解引用时访问4个字节;
shot* 类型的指针在解引用时访问2个字节;
char* 类型的指针在解引用时访问1个字节;
double* 类型的指针在解引用时访问4个字节;
2.2指针±整数
这里举了指针+整数的栗子,- 整数也是一样的,只不过是往低地址走。
在这里的指针类型决定了指针的步长(向前/向后走一步有多大距离)。
int*指针 +1,意思是跳过一个整型的大小,也就是向后走4个字节;
short*指针 +1,意思是跳过一个短整型的大小,也就是向后走2个字节;
char*指针 +1,意思是跳过一个字符的大小,也就是向后走1个字节;
double*指针 +1,意思是跳过一个浮点型的大小,也就是向后走8个字节;
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
指针的类型决定了指针向前或者向后走一步有多大(距离)。
3.野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.1野指针成因
1.指针未初始化
#include <stdio.h> int main() { int* p;//局部变量指针未初始化,默认为随机值 *p = 20;//此时p改动的不是想要的目标地址 return 0; }
2.指针越界访问
#include<stdio.h> int main() { int arr[10] = { 0 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; for (i = 0; i <= sz; i++)//当i=sz时发生数组越界访问 { *p = i; p++; } return 0; }
3.指针指向的空间释放
#include<stdio.h> int* test() { int num = 100; return # } int main() { int* p = test(); *p = 200; return 0; } //此时p为野指针,因为函数调用后空间会被系统回收,此时num的地址被回收,p改动的不是想要的目标地址
3.2如何规避野指针
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量(栈空间)的地址
5. 指针使用之前检查有效性
指针初始化
#include<stdio.h> int main() { int a = 10; int* pa = &a;//明确初始化 //NULL--0,就是初始化指针的 int* p = NULL; return 0; }
检查指针有效性
#include<stdio.h> int main() { int a = 10; int* p = NULL; //*p = 20;//无法改变 //空指针的指针变量是不能改值的 if (p != NULL)//验证 { printf("%d\n", *p); } return 0; }
4.指针运算
4.1指针±整数
#define N_VALUES 5 float values[N_VALUES]; float* vp; //指针±整数;指针的关系运算 int main() { for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--) { *vp = 1; } return 0; }
图解:
4.2指针-指针
我们来举个栗子,用指针-指针的方法模拟实现库函数strlen
//模拟实现strlrn #include<stdio.h> int my_strlen(char* arr) { char* start = arr; while (*arr) { arr++; } return arr - start; } int main() { char arr[] = "cjcwqr"; int len = my_strlen(arr); printf("%d\n", len); return 0; }
注意:
1.两个指针相减的前提是:指针指向同一块儿连续的空间
2.指针类型不同不能相减
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 - 1]; vp >= &values[0]; vp--) { *vp = 0; } return 0; }
图解:
两段代码一个是访问数组前的地址,一个是访问后的地址,实际上都是可以顺利执行功能的,但我们还是应该避免第二种写法,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,
但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
5.指针和数组
我们知道了数组名就是数组首元素的地址(两种特殊情况),那我们就可以使用指针来访问一个数组,比如:
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* pa = arr; int i = 0; for (i = 0; i < 10; i++) { //方法一 //printf("%d ", *(pa + i)); //方法二 printf("%d ", *pa); pa++; } return 0; }//输出1 2 3 4 5 6 7 8 9 10
6.二级指针
我们还是通过图解来观察理解二级指针:
前面我们知道了指针变量是用来存放指针的地址的,那么指针变量的地址应该存到哪里呢?
这就需要用到二级指针了
通过**pp对pp中的地址解引用,找到的是p,**pp访问的就是p,再通过*p对p中的地址解引用,找到的就是a ,*p访问的就是a。
7.指针数组
首先我来问个问题,指针数组是指针呢还是数组呢?
指针数组是数组,是个存放指针的数组。
我们通过用指针数组来模拟二维数组来对其进行讲解 :
//用一维数组模拟一个二维数组 #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[] = { 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("\n"); //} //方法二 for (i = 0; i < 4; i++) { int j = 0; for (j = 0; j < 5; j++) { printf("%d ",arr[i][j]); } printf("\n"); } return 0; }
我们知道数组名就是首元素的地址(两个例外) ,这里创建的指针数组int* arr[ ]存放的就是每个数组首元素的地址,我们进行循环首先通过arr[i]找到每个数组,再通过arr[i][j]来访问数组中的每个元素,另一种方法也是同理,先通过*(arr+i)找到每个数组,再通过*(*(arr + i) + j)来访问数组中的每个元素,以达到实现模拟二维数组的效果。
好了以上就是今天的全部内容了,对友友们有帮助的话不妨三连加关注走一波,后期会持续更新C语言干货!