1.指针是什么?

指针是最小内存单元(大小:1byte)的编号,即地址(把内存单元的编号称为地址,地址也叫指针)

口头语中说的指针通常指的是指针变量,用来存放地址的变量

int main()
{
    int a =  10;//a是整型变量,占4个字节的内存空间
    int* pa = &a;//pa是一个指针变量,用来存放地址的
                 //取地址取出的是起始字节的地址
    return 0;
}

总结:

  • 指针就是地址,口语中的指针通常指的是指针变量
  • 指针变量里面存放的是地址,而通过这个地址,就可以找到一个内存单元
  • 指针变量是用来存放地址的,地址是标识一块内存空间的值;指针的大小在32位平台是4个字节,在64位平台是8个字节

2.指针和指针类型

X86 32位的环境  X64 64位的环境

#include <stdio.h>
int main()
{
    char *pc = NULL;
    short *ps = NULL;
    int *pi = NULL;
    double *pd = NULL;
    //sizeof返回值类型是unsigned int,用%u打印比较合适
    //%zu打印最准确(为sizeof准备的打印格式)
    printf("%zu\n", sizeof(pc));
    printf("%zu\n", sizeof(ps));
    printf("%zu\n", sizeof(pi));
    printf("%zu\n", sizeof(pd));//都是4/8
    return 0;
}

发现指针大小和指针类型没有关系,只和编译环境是32/64位有关,那类型到底有什么用,为什么要有这么多指针类型?                         

指针类型的意义

  • 指针类型决定了指针进行解引用时有几个字节的访问权限
  • 指针+-整数一次跳过几个字节
int a = 0x11223344;
int* pa = &a;
*pa = 0;

一次改动改了4个字节

C语言初阶-指针_数组元素

int a = 0x11223344;
char* pc = (char*) &a;
//int*的地址强制类型转换char*就没有warning
//pc在x86环境下是4个字节可以存放下a的地址
*pc = 0;

pc有能力存放下a的地址  pc=&a 说明地址存进去了

C语言初阶-指针_数组元素_02

只改动了一个字节

C语言初阶-指针_数组_03

指针类型的意义1:指针变量的类型不决定指针变量所占大小,指针类型决定了指针变量在被解引用操作时访问几个字节,是整型指针int*解引用访问4个字节,是字符型指针char*解引用访问1个字节,double型的指针解引用访问8个字节

int a = 0x11223344;
//进制只是表示数字的不同形式
int* pa = &a;
char* pc = (char*)&a;

printf("pa=%p\n", pa);
printf("pa+1=%p\n", pa+1);

printf("pc=%p\n", pc);
printf("pc+1=%p\n", pc+1);

pa与pc都是取出a的起始字节的地址   pa+1跳过4个字节   pc+1跳过1个字节

C语言初阶-指针_指针变量_04

指针类型的意义2:指针的类型决定了指针变量+1-1操作时,跳过几个字节,决定了指针的步长,int*指针+1跳过4个字节  char*指针+1跳过1个字节

不同指针类型的指针变量解引用访问字节数和+1-1跳过字节数不同,访问内存更加方便  

//int*想访问一个字节强制类型转换为char*
int* pi = &a; 
pi+2; //跳过8个字节
char* pc = &a;
pc+2; //跳过2个字节
//+-后不仅可以跟1,只要是整数就可以

对于相同长度的类型可以进行混用吗? 不可以 类型是一种视角

int a = 0;
int* pi = &a;  //pi解引用访问4个字节 pi+1跳过4个字节
float* pf = &a;//pf解引用访问4个字节 pf+1跳过4个字节

*pi = 100;
*pf = 100.0;

int*和float*是不是可以通用?

不能,虽然这两个指针的权限都一样(解引用访问空间大小,+1-1跳过大小),但是站在pi角度,它认为它指向的内存的是整型数据(按整数的存储方式存进去),站在pf角度,它认为它指向的内存空间放的是浮点型数据(按浮点数的存储方式存进去),浮点数和整数在内存中的存储方式是有差异的,存进内存的值是有所差异的,所以不能混用

按整数的存储方式存进去

C语言初阶-指针_指针变量_05

按浮点数的存储方式存进去

C语言初阶-指针_数组元素_06

注意:

  • 指针变量的大小跟通过指针变量访问内存空间的大小没有关系,不是一回事,指针变量访问空间大小取决于类型。
  • 指针变量的大小(4/8取决于地址大小)和指针变量所指向的空间的大小(取决于类型)没有关系

3.野指针

指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)

3.1 野指针成因

1.指针未初始化

int* p;
//p没有初始化,就意味着没有明确的指向
//一个局部变量不初始化,放的是随机值:0xcccccccc
//这个地址是非法的,不属于我们自己的地址,通过地址找到的空间也不是我们的
*p = 10;
//非法访问内存  没有初始化进行解引用,这里的p就是野指针

2.指针越界访问

int arr[10] = {0};
//arr[i]访问数组元素  现在用指针来访问数组
int* p = arr;//arr数组名,首元素地址即&arr[0]
int i = 0;
for(i = 0; i <= 10; i++)
//循环11次 第11次访越界问了 此时的p进行解引用,p就是野指针
{//当指针指向的范围超出数组arr的范围时,p就是野指针
    *p = i;
    p++;
}

p指向数组的首元素,每+1跳过1个int,解引用也是访问一个整型

C语言初阶-指针_数组元素_07

3.指针指向的空间释放

int* test()
{
  int a = 10;
  return &a;
}
//a是局部变量,一出test()就会销毁(空间还给操作系统了,空间不属于当前程序,不能用)
//空间销毁了,这块空间(变量a的空间)不属于当前程序 
//调用结束后,当前空间还给操作系统,当前程序没有这块空间的使用权限
//内存中这块空间还存在
//p里面保存着a的地址,p有能力通过地址找到这块空间,但是p不能访问使用这块空间

int main()
{
   int *p = test();
   return;
}


3.2 如何规避野指针

1.创建指针变量要初始化明确值(知道应该赋的值就赋明确值),不知道初始化什么值就初始化为空指针:NULL(指针变量没有指向,是个空指针)

int a = 10;
int* p1 = &a;//初始化 赋明确值
*p1 = 20;//使用指针进行修改a

int* p2 = NULL;//不知道初始化什么值就初始化为空指针
//NULL 本质上是0  相当于int b=0;

//不是说NULL没有指向嘛,空指针可以用它吗
//*p2 = 100;//可以这样使用空指针吗  不可以 err
//0地址,没有指向有效空间  不能直接访问

if(p2!=NULL)//想使用p2 要先进行判断,不为NULL(有了有效指向),就可以使用
{//也可以写成if(p2)但是还是建议写成p!=NULL(不是野指针,指向有效空间)更加直观
  *p2 = 100;
}

//但是也不是决对安全的,避免不了因为局部变量的空间释放造成的野指针

不能直接使用空指针

C语言初阶-指针_数组_08

变量的空间销毁了为什么还会打印输出10?

此处判断失效,没有起到作用
p中存放了地址,肯定不为空指针 判断失效 不能避免野指针出现
p通过非法地址找到那块空间(空间不属于当前程序,但是还是存在的)时当前空间没有被覆盖(没有被使用)所以打印输出10
p访问非法地址指向的空间,打印结果是不一样的(随机的)

C语言初阶-指针_指针变量_09

p访问非法地址指向的空间,打印结果是随机的

C语言初阶-指针_数组元素_10

2.小心指针越界

3.指针变量指向的空间被释放,及时置NULL

4.避免返回局部变量的地址

5.指针使用前检查有效性

4.指针运算

指针+-整数 指针-指针 指针的关系运算 指针的解引用都是指针运算 

4.1 指针+-整数

C语言初阶-指针_指针变量_11

vp指向首元素,先*vp=0  再 vp+1往后移动

C语言初阶-指针_数组_12

//vp没有初始化是野指针   还没有使用 不会有问题
//vp=&values[0] 给指针赋值,指针有指向了,不再是野指针

*vp++=0;
//*vp=0;
//vp++;

*vp++;  
//表达式的值是vp的值,vp先进行解引用vp* 再进行vp++ (没有对指向对象++) 地址++
//等价于
*vp;
vp++;

(*vp)++;//先进行vp*找到vp指向的对象,对vp*(vp指向的对象)进行++

int arr[5] = {0};
int* p;
p < &arr[5];
//算不算数组下标越界,可以这样使用,虽然空间不属于此程序
//只是取地址比较 没有问题
//数组初始化
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for(i = 0; i < sz; i++)
{//数组下标写法:全初始化为1
  arr[i] = 1;
}
//指针写法
int* p = arr;//数组名:首元素地址
for(i = 0; i < sz; i++)
{//第一种写法
   *p = 1;
    p++;
 //第二种方法
 //*(p+i) = 1;
}

C语言初阶-指针_指针变量_13

4.2 指针-指针

指针-指针的绝对值得到的是指针之间元素的个数(不是空间相减,只是指针变量相减;&arr[i]是地址,地址是存放到指针变量的,理解为两个指针变量相减,而不是两个地址相减)

int arr[10] = {0};
printf("%d\n", &arr[9]-&arr[0]);//9
printf("%d\n", &arr[0]-&arr[9]);//-9

//分析:
//&arr[9] 和 &arr[0] 分别是指向第10个元素和第1个元素的指针
//int* p = &arr[0];//&arr[0]首元素地址
//也可以写成 
//int* p = arr;
//p+9 = &arr[9];
//所以 &arr[9]-&arr[0]=(p+9)-p=9
int arr[10] = {0};
char ch[5] = {0};
printf("%d\n", &arr[5]-&ch[0]);//err  没有意义

注意:

  1. 不是所有的指针都能相减,指向同一块空间的两个指针(相同类型)才能相减,这样才有意义
  2. 指针+指针(即地址+地址)没有意义
#include <stdio.h>
#include <string.h>
int main()
{//用库函数求字符串长度
    int len = strlen("abcdef");
    printf("%d\n", len);//6
    return 0;
}
#include <stdio.h>

/*int my_strlen(char* str)//方法1  //方法2递归
{//自定义求字符串长度的函数
    int count = 0;
    while(*str != '\0') 
    {
        count++;
        str++;
    } 
    return count;
}*/

//方法3:指针-指针
int my_strlen(char* str)
{//\0地址-首元素地址就是元素个数
    char* start = str;
    while(*str != '\0')
    {
         str++;//跳出循环时指向\0
    }
    return (str-start);
}

int main()
{
    int len = my_strlen("abcdef");
    //字符串传参时,传的是首字符的地址
    printf("%d\n", len);// 6
    return 0;
}

4.3 指针的关系运算

比较大小

C语言初阶-指针_数组_14

vp开始指向数组往后越界的元素,先vp--往前移动  再*vp=0

C语言初阶-指针_数组元素_15

C语言初阶-指针_数组_16

分析:
*--vp = 0;

vp = vp-1;
*vp = 0;

C语言初阶-指针_数组元素_17

分析:vp最后指向在数组首元素前面一个的元素处,但是要避免出现这种情况

C语言初阶-指针_指针变量_18

C语言初阶-指针_数组元素_19

允许p1和p2(向后越界)比较,不允许p1和p3(向前越界)比较,可以往后越界的指针与之比较,不允许与向前偏移的指针比较

5.指针和数组

//数组:一组相同类型元素的集合(一组数据,内存中连续开辟的一块空间)
//数组的大小取决于元素的个数,元素的类型 

//指针变量:存放地址的变量
//指针的大小4/8  取决于32位环境还是64位环境

//联系:数组名是首元素地址  通过指针来访问数组

int mian()
{
   int arr[10] = {0};
   //arr是首元素地址
   //&arr[0] 首元素地址
   int* p = arr;
   //指针变量p和数组arr就建立了联系 p存放了数组首元素地址 p指向了数组arr
   //通过指针来访问数组
   int sz = sizeof(arr) / sizeof(arr[0]);
   int i = 0;
   for(i = 0; i < sz; i++)
   {
       printf("%d", *(p+i));
       //p+i  p是整型指针,p+i跳过i个整型,指向下标为i的数组元素
       //*p p指向的数组元素 p+1  p指向第一个元素 
       //*(p+i) 跳过i个元素 进行解引用就是下标为i的数组元素
   }
   return 0;
}
#include <stdio.h>
int mian()
{
   int arr[10] = {0};
   int* p = arr;
   int sz = sizeof(arr) / sizeof(arr[0]);
   int i = 0;
   for(i = 0; i < sz; i++)
   {
       printf("%p--------%p\n", &arr[i], p+i);
       //&arr[i] 下标为i的元素地址  p放的是首元素地址 p+i跳过i个元素(整型)
   }
   
   return 0;
}

地址完全相同

C语言初阶-指针_指针变量_20

说明这个指针变量p指向数组的首元素时,p+i和&arr[i](下标为i的数组元素的地址)一样的,p+i,跳过i个整型,指向下标为i的数组元素,就是下标为i的数组元素的地址,即&arr[i]。

//打印数组元素 三种方式等价
printf("%d ", arr[i]);
printf("%d ", *(p+i));
//又因为int* p=arr; p中存放首元素地址arr
printf("%d ", *(arr+i));
//arr数组名表示首元素地址
//arr+i,跳过i个元素,指向下标为i的数组元素,就是下标为i的数组元素的地址

arr[i]与*(arr+i)等价,arr[i]本质上计算时是通过arr+i(下标为i的元素的地址)找到下标为i的数组元素,然后解引用。( arr[i]<=====>*(arr+i) )

//数组传参  
#include <stdio.h>
/*void test(int* p, int sz)
{   
    int i = 0;
    for(i = 0; i < sz; i++)
    {//指针形式
        printf("%d ", *(p+i));//p就是首元素地址
    }
}*/

void test(int arr[], int sz)
{   
    int i = 0;
    for(i = 0; i < sz; i++)
    {//数组形式
        printf("%d ", arr[i]);//arr[i]计算时还是*(arr+i) 
    }
}

int main()
{   
    int arr[10] = {0};
    test(arr,10);//传参传的是首元素地址
    return 0;
}

6.二级指针

#include <stdio.h>
int mian()
{
    int a = 10;
    int* pa = &a;//pa是一个指针变量,一级指针变量
    //一级指针变量:通过pa直接去找a,找一次就找到了,对pa进行一次解引用就找到a了
    //即*pa=a
    *pa = 20;
    printf("%d\n", a);//20
    
                     //pa也是变量,也是有自己的空间,也有地址
    int** ppa = &pa; //ppa是二级指针变量 存放ppa地址
                     //int* *ppa 或 int** ppa都可以  
                     //通过ppa找到变量a需要进行两次解引用 所以叫二级指针变量
                     //*ppa--->pa  *pa--->a 即 **ppa
    
    **ppa=20;        //**ppa与*pa等价
    printf("%d\n", a);//20
    //起始地址
    return 0;
}

*pa = a 通过修改*pa达到修改a的效果

C语言初阶-指针_数组_21

ppa存放指针变量pa的地址,是二级指针变量

C语言初阶-指针_数组_22

指针类型的拆解:

int* pa;  //*说明pa是指针 int说明pa所指向的对象的类型是int
int** ppa;//*(后面的*)说明ppa是指针 int*说明ppa所指向的对象pa的类型整体叫int*

总结:二级指针变量是用来存放一级指针变量的地址

7.指针数组

存放指针的数组就是指针数组

//一组相同类型的指针变量的集合 
#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    int c = 30;
    //多个整型变量 可以创建一个整型数组
    int arr[10];
    
    int* pa = &a;
    int* pb = &b;
    int* pc = &c;
    //多个整型指针 可以创建一个整型指针数组
    int* parr[10] = {&a,&b,&c};
    //int*数组元素是整型指针
    //parr存放指针的数组 指针数组
    //parr数组名,是首元素地址
    
    //访问指针数组
    int i = 0;
    for(i = 0; i < 3; i++)
    {
        printf("%d\n", *(parr[i]));
    }
    return 0;
}

可以用指针数组模拟出一个二维数组

int arr[3][4] = {1,2,3,4,2,3,4,5,3,4,5,6};
//1 2 3 4 
//2 3 4 5
//3 4 5 6
//访问二维数组  
int i = 0, j = 0;
for(i = 0; i < 3; i++)
{
    for(j = 0; j < 4; j++)
    {
        printf("%d\n", arr[i][j])
    }
    printf("\n");
}

模拟的二维数组

int arr1[4] = {1,2,3,4};
int arr2[4] = {2,3,4,5};
int arr3[4] = {3,4,5,6};
//关联3个数组:数组名 首元素地址

int* parr[3] = {arr1,arr2,arr3};//parr数组元素是首元素地址
                                //指向了每个数组(arr1 arr2 arr3)的首元素
int i = 0 , j = 0;
for(i = 0; i < 3; i++)
{
    for(j = 0; j < 4; j++)
    {
        printf("%d\n",parr[i][j]); 
    }
    printf("\n");
}
//parr[i]拿到的是数组arr1 arr2 arr3起始地址(首元素地址)
//也就是数组名:arr1 arr2 arr3   
//访问数组元素采用(arr[i]) 数组名[下标] 此处数组名是parr[i]下标用j
//即 parr[i][j]

C语言初阶-指针_数组_23

把三个一维数组关联起来, 达到二维数组效果

C语言初阶-指针_数组元素_24