当前位置: 首页 > news >正文

C语言基础(十)---指针基础

指针基础

一、预备知识

1、内存地址
  • 字节:字节是内存的容量单位,byte,1Byte = 8bits

  • 地址:系统为了便于区分每一个字节而对它们逐一进行编号(编号是唯一的),称之为内存地址,简称地址。举例:int a = 5;有四个编号,但只用关注第一个内存单元的编号,首地址的编号

2、基地址(首地址)
  • 单字节数据:对于单字节数据而言,其地址就是其字节编号。举例:char a = 'A';(它的首地址就是它完整的地址)

  • 多字节数据:对于多字节数据而言,其地址是所有字节中编号最小的那个,称为基地址(首地址)

3、取址符&(取地址符)
  • 每个变量都是一块内存,都可以通过取址符&来获取其地址。

  • 举例:

     int a = 100;
     printf("整型变量a的地址是:%p\n", &a);     //64位机是12位的十六进制
     ​
     char c = 'x';
     printf("%p", &c);
  • 注意:

    • 虽然不同的变量的尺寸是不同的,但是它们的地址的尺寸是一致的。

    • 不同的地址虽然形式上看起来是一样的,但由于它们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的

二、为什么要引入指针

  • 为我们函数修改实参提供支持(以实参的形式把值传递给形参,然后通过指针在函数内部对实参进行修改)

  • 为动态内存管理提供支持

  • 为动态数据结构(链表、队列等)提供支持

  • 为内存访问提供另一种途径

  • 指针就是地址

三、变量指针与指针变量

1、指针概念
(1)、内存单元与地址机制
  • 内存单元的划分

    • 系统将内存划分为连续的基本存储单元,每个存储单元的容量为1字节(8bits)

    • 每个内存单元拥有唯一编号,称为内存地址(十六进制表示)

  • 变量存储特性:

    • 变量根据数据类型占据不同数量的内存单元

      • char类型占1字节(1个单元)

      • int类型占4字节(4个单元)

      • double类型占8字节(8个单元)

    • 变量的基地址(首地址)是其首个内存单元的地址(首地址一般是这一组编号中最小的那个)

(2)、变量指针与指针变量
对比维度变量指针指针变量
本质内存地址(首地址)。变量指针其实就是变量的首地址存放地址的普通变量
操作符&(取址符)*(声明符,解引用符)。举例:如int* p;
代码示例&a(获取变量a的地址)int* p = &a;把a的地址存放到指针变量p里面
核心特性不可修改(地址由系统分配)可修改(修改指向)。举例:(p = &b;
(3)、指向

指针变量中存放谁的地址,就说明该指针变量指向了谁

(4)、指针的尺寸
系统类型指针尺寸地址位数十六进制显示长度
32位系统4字节(int)32bit8位(如:0x0804A000)
64位系统8字节(long)48bit12位(如0x7FFDEADBEEF)
(5)、指针的本质
  • 变量指针:数据的”门牌号“(&a)

  • 指针变量:存储门牌号的”笔记本“(int *p;)

  • 指向操作:通过门牌号访问数据(*p = &a)

 int a = 10;
 printf("%d\n", a);     //访问a的值
 ​
 int *p = &a;
 printf("%d\n", *p);     //访问p指向的a的值

注意:

Linux系统中打印地址时,最多显示12个十六进制数,为什么?

Linux64位操作系统中,一个内存地址占8个字节,一个字节8bit位,所以一个地址8*8=64bit 位, 每4个bit可以表示1个十六进制数; 64个bit位用十六进制表示最多有16个数值位;

系统为了寻址方便,默认当前计算机系统没必要寻址64bit为,只寻址了48个bit为,所以用12 个十六进制数表示一个地址

二进制: 0100 1010 十六进制: 0x4A 4*16+10 = 74

注意: 在Linux64位操作系统中,指针类型的变量占8个字节的内存空间 在Linux32位操作系统 中,指针类型的变量占4个字节的内存空间

2、内存数据的存取方式

在C语言中对内存数据(变量、数组元素等)的存取有2种方式:

(1)、直接存取
  • 通过基本数据类型的变量访问这个变量代表的内存空间的数据

  • 通过数组元素的引用,访问这个引用代表的内存空间的数据

     //基本数据类型变量
     int a = 10;     //存
     printf("%d\n", a);     //取
     ​
     //数组元素的访问
     int arr[] = {11, 12, 13};     //存
     arr[0] = 66;    //存
     printf("%d\n", arr[0]);     //取
(2)、间接存取
  • 通过指针变量,间接地访问内存中的数据。

  • *

    • 声明符:如果*前面有数据类型,,就是声明指针

       int *p
    • 解引用符:如果前面没有数据类型,读作解引用

    • 案例:

     int main(int argc, char *argv[]) {
         //定义一个普通变量
         int a = 3;
     ​
         //定义一个指针变量并赋值
         int *p = &a;     //int* p = &a;
     ​
         //访问变量a
         //直接访问
         printf("直接访问—%d\n", a);
     ​
         //访问变量a,通过指针变量p访问,间接访问
         printf("间接访问-%d\n", *p);     //*p叫做解引用
     ​
         //访问指针变量p的值,也就是访问变量a的地址
         printf("地址访问-%p, %p, %p\n", p, &a, &p);
     ​
         return 0;
     }

指针

一、变量指针域指针变量

1、指针变量的定义
  • 语法:

     数据类型 *变量列表;
  • 举例:

     int a;     //普通变量,拥有真实的数据存储空间
     ​
     int *a, *b;     //指针变量,无法存储数据,只能存储其他变量的地址

    指针变量的值只能是8/12位的十六进制整数。

  • 注意:

    1. 虽然定义指针变量*a,是在变量名前加上*,但是实际变量名依然为a,而不是*a

    2. 使用指针变量间接访问内存数据时,指针变量必须要明确的指向。(指向:指针变量存放谁的地址就指向谁)

    3. 如果想要借助指针变量间接访问指针变量保存的内存地址上的数据(内存地址对应的内存空间所存放的地址),可以使用指针变量前加*来间接返回访问。

      指针变量前加*,如果不带数据类型,就称之为对指针变量解引用

       int i = 5, *p;
       p = &i;     //将i的地址赋值给指针变量p
       ​
       //
       printf("%lx, %p, %p", p, p, &p);     //&p是p的地址,p是i的地址
       printf("%d\n", *p);     //访问的是变量i的值,5
       ​
       *p = 10;     //间接地给i赋值
       printf("%d, %d\n", *p, i);     //10,10 
    4. 指针变量只能指向同类型的变量,借助指针变量访问内存,一次访问的内存大小是取决于指针变量的类型

       int a = 10;
       int *p = &a;     //*p前面的类型是p指向的变量a的类型,这个类型要么完全一致,要么能够转换
    5. 指针变量在定义时可以初始化:这一点和普通变量是一样的

       int a = 5;
       int *p = &a;     //定义指针变量的同时进行初始化
       printf("%d\n", *p);
       ​
       int b;
       int *p1 = &b;     //指针初始化的时候,不需要关注指向的变量空间中是否有值
       ​
       printf("%d\n", *p1);     //随机值(正负数、0)
2、指针变量的使用
(1)、使用
  • 指针变量的赋值

     //方式一
     int a, *p;
     p = &a;     //先定义,后赋值
     ​
     //方式2
     int a, *p, *q = &a;     //定义并初始化
     p = q;     //其实就是变量的赋值操作,指针变量q的值赋值给了指针变量p,指针变量p和q同时指向a
  • 操作指针变量

     int a, *p, *q = &a;     //a先执行,所以q可以拿到a的地址
     ​
     p = q;     //将指针变量q的值赋值给指针变量p,此时p和q都指向了变量a
     printf("%p", p);      //访问的是指针变量P的值(也就是变量a的值)
     ​
     printf("%p", q);     //和上一条语句结果一样,访问的都是a的地址
  • 操作指针变量指向的值

     int a = 6, *q = &a, b = 25;     //一定要注意指针变量q的变量名就是q
     *q = 10;     //访问q指向的变量a的空间,其实就是间接给a赋值,此时a = 10
     printf("%d, %d\n", *q, a);     //10, 10
     ​
     q = &b;     //改变指向,指向b的.
     //一个指针变量只能同一时刻指向一个变量,但是一个变量可以同时被多个指针变量指向
     ​
     printf("%d, %d\n", *q, b);     //25, 25
     ​
     printf("%d, %d\n", *q, a);     //25, 10
(2)、两个指针运算符的使用
  • & : 取地址运算符。&a是变量a的地址。这个是变量指针。

  • * : 指针运算符、解引用符、间接访问运算符、取目标操作符。*p是指针变量p指向的对象的值。这个是指针变量。

(3)、案例
案例1
  • 需求:通过指针变量访问整型数据

  • 代码:

     #include <stdio.h>
     ​
     void main()
     {
         int a = 3, b = 4, *p1 = &a, *p2 = &b;
         
         printf("a = %d, b = %d\n", *p1, *p2);
     }
案例2
  • 需求:声明a,b两个变量,使用间接存取的方式实现数据的交换(只是交换了指针的指向)

  • 代码:

     //间接存取:实现a,b数据交换
     void main()
     {
         int a = 3, b = 4, *p = &a, *q = &b;
         
         printf("a = %d, b = %d\n", *p, *q);
         
         //方式一:直接操作指针
         //不会改变原始数据的值
         int *m = &a;
         p = q;
         q = m;
         
         printf("a = %d, b = %d\n", *p, *q);
         
         //方式2:间接操作指针指向的值(不推荐)
         //改变了原始数据的值
         int temp = *p;
         *p = *q;
         *q = temp;
         
         printf("a = %d, b = %d\n", *p, *q);
     }
  • 总结:

    • 方式一:此时改变的只是指针的指向,原始变量a,b中数据并没有发生改变

    • 方式二:此时a,b中存放的值也发生了改变

案例3
  • 需求:输入a,b两个整数,按先大后小的顺序输出a和b。

  • 代码:

     //间接存取:实现a,b数据交换
     void main()
     {
         int a = 3, b = 4, *p = &a, *q = &b;
         
         printf("a = %d, b = %d\n", *p, *q);
         
         if(a < b)
         {
             //方式一:指向改变
             //不会改变原始数据的值
             int *m = &a;
             p = q;
             q = m;
             
             /**(不推荐)
             //方式2:间接操作指针指向的值
             //改变了原始数据的值
             int temp = *p;
             *p = *q;
             *q = temp;
             */
         }
         
         printf("a = %d, b = %d\n", *p, *q);
     }
3、指针变量做函数参数

指针变量做函数参数往往传递的是变量的地址(基地址/首地址),借助于指针变量的间接访问是可以修改实参变量数据的。

案例:
  • 需求:有a,b两个变量,要求交换后输出(要求函数处理,用指针变量做函数的参数)

  • 方式1:交换指向(指针指向发生改变,指向对象不改变)

     //定义一个函数,实现两个数的交换
     void swap(int *p, int *q)
     {
         int *m;
         //以下写法,只会改变指向,不会改变指向对象的值
         m = p;
         p = q;
         q = m;
         
         printf("交换后:%d, %d\n", *p, *q);
     }
     ​
     int main()
     {
         int a = 3, b = 5;
         swap(&a, &b);
         printf("a = %d, b = %d\n", a, b);
     }

  • 方式2:交换数据(指针指向不发生变化,指针指向的对象的值发生变化)

     //定义一个函数,实现两个数的交换
     void swap(int *p, int *q)
     {
         int temp = *p;
         *p = *q;
         *q = temp;
         
         printf("交换后:%d, %d\n", *p, *q);
     }
     ​
     int main()
     {
         int a = 3, b = 5;
         swap(&a, &b);
         printf("a = %d, b = %d\n", a, b);
     }
4、指针变量指向数组元素
(1)、数组元素的指针
  • 数组的指针就是数组中的第一个元素的地址,也就是数组的首地址

  • 数组元素的指针是指数组的首地址。因此,同样可以用指针变量来指向数组或数组元素

  • 在C语言中,由于数组名代表数组的首地址,因此,数组名实际上也是指针。访问数组名就是访问数组首地址

     //创建一个数组
     int arr[] = {11, 12, 13};
     ​
     int *p1 = &arr[0];     //数组中第一个元素的地址
     int *p2 = arr;     //数组的首地址
     //以上两行代码所表示的位置是同一位置
     ​
     printf("%p, %p, %p\n", p1, p2, arr);     //显示的结果一样
  • 注意:虽然我们定义了一个指针变量接受了数组地址,但不能理解为指针变量指向了数组,而应该理解为指向了数组的首个元素。

(2)、指针的运算
  • 指针运算:前提是指针变量必须要指向数组的某个元素(指针运算只能发生在同一数组内)

序号指针运算说明
1自增:p++、++p、p+=1指向下一个元素的地址,适合在循环中
2自减:p--、--p、p-=1指向上一个元素的地址,适合在循环中
3加n个数:p+n.(这里相当于移动了n * sizeof(type)字节)后n个元素的(首)地址
4减n个数:p-n前n个元素的(首)地址
5指针相减:p1 - p2p1, p2之间相差几个元素
6指针比较:p1 < p2前面的指针小于后面的指针

说明:

  1. 如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的 下一个元素,p-1指向 同一数组中的上一个元素。即p+1或p-1也表示地址。但要注意的是,虽然指针变量p中存放的 是地址,但p+1并不表示该地址加1,而表示在原地址的基础上加了该数据类型所占的字节数d (d = sizeof(数据类型))

  2. 如果p原来指向a[0],执行++p后p的值改变了,在p的原值基础上加d,这样p就指向数组的 下一个元素a[1]。d是数组元素占的字节数。

  3. 如果p的初值为&a[0],则p+i 和a+i 就是数组元素a[i]的地址,或者说,它们指向a数组的第 i 个元素 。

  4. *(p+i)*(a+i)是p+i或a+i所指向的数组元素,即a[i]

  5. 如果指针变量p1和p2都指向同一数组,如执行p2-p1,结果是两个地址之差除以数组元素的 长度d。

 int arr[] = {11, 22, 33, 44, 55};
 ​
 int *p1 = arr + 4;     //arr[4]
 int *p2 = arr + 1;     //arr[1]
 ​
 printf("%ld\n", p1 - p2);     //4 - 1 = 3
(3)、案例:
  • 案例1

    • 需求:通过下标法和指针法遍历数组

    • 代码:

       #include <stdio.h>
       ​
       //下标法遍历数组(之前学的方法)
       void arr1(int arr[], int len)
       {
           //通过循环遍历
           for(int i = 0; i < len; i++)
           {
               printf("%-3d", arr[i]);
           }
           printf("\n");
       }
       ​
       //通过指针法遍历数组
       void arr2(int arr[], int len)
       {
           //建议不要直接操作数组,最好是在函数中创建一个指针变量用来接收形参
           int *p = arr;     //p存储的就是数组中第一个元素的地址
           //定义一个循环变量
           register int i = 0;     //将循环变量存储在寄存器,提高执行效率
           
           //通过循环遍历
           for(; i < len; i++)
           {
               printf("%-3d\n", *(arr + i));
           }
           
           printf("\n");
       }
       ​
       //通过指针法遍历数组
       void arr3(int arr[], int len)
       {
           //建议不要直接操作数组,最好是在函数中接收形参
           int *p = arr;     //p存储的就是数组中第一个元素的地址
           //定义一个循环变量
           register int i = 0;     //将循环变量存储在寄存器,提高执行效率
           
           //通过循环遍历
           for(; i < len; i++)
           {
               printf("%-3d\n", *p);
               p++;
           }
           
           printf("\n");
       }
       ​
       ​
       int main()
       {
           int array[] = {11, 22, 33, 44, 55};
           
           int len = sizeof(array) / sizeof(array[0]);
           
           arr1(array, len);
           arr2(array, len);
           arr3(array, len);
           
           return 0;
       }
  • 案例2:

    • 需求:推导以下代码的运行结果

    • 代码:

        #include <stdio.h>
        
        int arr2()
        {
            // 创建一个普通数组
            int arr[] = {11,22,33,44,55,66,77,88};
            
            int *p = arr;
            printf("%d\n", *p); // 11
            
            p++; 
            printf("%d\n", *p); // 22
            
            int x = *p++;  // 第一步:解引用p赋值给x,x = 22,第二步:p++,指针移动到了33这个元素
            printf("%d, %d\n", x, *p); // 22,33
            
            int y = *(++p); // 第一步:++p,指针移动到了44这个元素,第二步:对44这个地址解引用,得到44
            printf("%d, %d\n", y, *p);// 44,44
            
            (*p)++; 
            printf("%d\n",*p); // 45
        }
       ​
        int main(int argc,char *argv[])
        {
            arr2();
            return 0;
        }

      *p++:先解引用p,然后p这个指针自增

       int arr[] = {11, 22, 33}, *p = arr;
       int x = *p++;     //x = 11, *p = 22

      (*p)++:先解引用p,然后使用解引用出来的数据自增

       int arr[] = {11, 22, 33}, *p = arr;
       int x = (*p)++;     //x = 11, *p = 12
(四)、通过指针引用数组元素
  • 引用一个数组元素,可以用:

    1. 下标法:如arr[i]形式

    2. 指针法:如*(arr + i)*(p + i),其中arr是数组名,p是指向数组元素的指针变量,其初始值:p = arr;

  • 案例:

    • 需求:有一个整型数组arr,有10个元素,输出数组中全部的元素

    • 下标法:(通过改变下标输出所有的元素)

       #include <stdio.h>
       void main()
       {
           int arr[10];
           int i;
           
           //给数组元素赋值
           for(i = 0; i < 10; i++)
               scanf("%d",&arr[i]);
           // 遍历数组元素
           for(i = 0; i < 10; i++)
               printf("%-4d%",arr[i]);
           printf("\n");       
       }
    • 指针法(地址操作):(通过数组名计算数组元素的地址,找出数组元素值)

       #include <stdio.h>
       void main()
       {
           int arr[10];
           int i;
           // 给数组元素赋值
           for(i = 0; i < 10; i++)
               scanf("%d",&arr[i]);
           // 遍历数组元素
           for(i = 0; i < 10; i++)
                printf("%-4d%",*(arr + i));
           printf("\n");   
       }
    • 指针法(利用指针变量):(用指针变量指向数组元素)

       #include <stdio.h>
       void main()
       {
           int arr[10];
           int *p, i;
           // 给数组元素赋值
           for(i = 0; i < 10; i++)
               scanf("%d",&arr[i]);
           // 遍历数组元素
           for(p = arr; p < (arr + 10); p++)
               printf("%-4d",*p);
           printf("\n");   
       }
  • 注意:数组一旦创建,就无法改变其值(数组名),说的不是数组元素的值。数组名在内存中无法改变,不可对数组名重新赋值,不可改变数组的长度。

  • 以上三种写法比较:

    • 方式一和方式二执行效率相同。系统是将arr[i]转换为为*(arr + i)处理的,即先计算出地址,因此比较费时

    • 方式三比方式一二方法快。用指针变量直接指向数组元素,不必每次都重新计算地址。p++能大大提高执行效率

    • 用方式一比较直观,而用地址法或者指针变量的方法难以很快判断出当前处理的元素

使用指针变量指向数组元素时(上面第3种方法),注意以下几点:

  1. *(p--)相当于arr[i--],先*p,再p--

*(p++)相当于arr[i++],先*p,再p++ 完整写法

  1. *(--p)相当于arr[--i],先--p,再*

*(++p)相当于arr[++i],先++p,再*

  1. *p++相当于先*p,再p++简写

  2. (*p)++相当于先*p,再给*p进行++

具体关系参照下面的表格:

操作类型指针表达式数组下标等价执行顺序是否改变指针地址
前置自减+取值*(--p)arr[--i]1、指针前移;2、取新地址的值改变
前置自增+取值*(++p)arr[++i]1、指针后移;2、取新地址的值改变
后置自减+取值*(p--)arr[i--]1、取原地址的值;2、指针前移改变
后置自增+取值*(p++)arr[i++]1、取原地址的值;2、指针后移改变
后置自增(简写)*p++arr[i++]1、取原地址的值;2、指针后移改变
取值后值自增(*p)++arr[i]++1、取原地址的值;2、值+1不改变
(五)、数组名做函数参数
  • 表现形式:

    1. 形参和实参都是数组名

       void fun(int arr[], int len) {...}
       void main()
       {
           int arr[] = {11, 22, 33};
           fun(arr, sizeof(arr) / sizeof(arr[0]));
       }
    2. 实参用数组名,形参用指针变量

       void fun(int *p, int len) {...}
       void main()
       {
           int arr[] = {11, 22, 33};
           fun(arr, sizeof(arr) / sizeof(arr[0]));
       }
    3. 实参和形参都用指针变量

       void fun(int *p, int len) {...}
       void main()
       {
           int arr[] = {11, 22, 33};
           int *p = arr;
           fun(p, sizeof(arr) / sizeof(arr[0]));
       }
    4. 实参用指针变量,形参用数组名

       void fun(int arr[], int len) {...}
       void main()
       {
           int arr[] = {11, 22, 33};
           int *p = arr;
           fun(p, sizeof(arr) / sizeof(arr[0]));
       }
  • 案例:

    • 需求:将数组a中n个整数按相反顺序存放

    • 代码:

       //遍历数组
       void list(const int arr[], int len)     //加了const之后,arr就不可修改
       {
           for(int i = 0; i < len, i++)
               printf("%-3d", arr[i]);
           printf("\n");
       }
       ​
       //数组的反转:数组实现
       void inverse(int arr[], int len)
       {
           //将第i个和第len-i-1个进行对调
           
           //定义循环变量和临时变量
           register int i = 0, temp;
           
           for(; i < len / 2; i++)
           {
               temp = arr[i];
               arr[i] = arr[len - i - 1];
               arr[len - i - 1] = temp;
           }
       }
       ​
       //数组的反转:用指针实现
       void inverse_p(int *p, int len)     
       {
           //将第i个和第len-i-1个进行对调
           
           //形参不要直接操作,要用一个变量去接收一下
           int *i = p, *j = p + len - 1, temp;     //p+len-1等价于&p[len-1] 
           
           for(; i < j; i++, j--)
           {
               temp = *i;
               *i = *j;
               *j = temp;
           }
       }
       ​
       ​
       int main()
       {
           int arr[] = {11, 22, 33, 44, 55, 66};
           int len = sizeof(arr) / sizeof(arr[0]);
           int *p = arr;
           
           list(arr, len);
           
           inverse(arr, len);
           list(arr, len);
           
           
       }

http://www.dtcms.com/a/99975.html

相关文章:

  • C++学习之路:指针基础
  • GMap.NET + WPF:构建高性能 ADS-B 航空器追踪平台
  • 【Golang】第十弹----单元测试、go协程和管道
  • 《三极管侦探社:神秘信号放大案》
  • LPDDR(Low Power Double Data Rate)详解
  • J2EE框架技术 第四章 J2EE的IOC
  • 19840 Dijkstra求最短路2
  • 文件上传存储安全OSS 对象分站解析安全解码还原目录执行
  • React编程的核心概念:数据流与观察者模式
  • POSIX 和 System V IPC的区别
  • 微信小程序(下)
  • 02_MySQL安装及配置
  • 去中心化金融的基石——以太坊
  • OSPF协议(1)
  • 海洋大地测量基准与水下导航系列之七我国海洋水下定位装备发展现状(下)
  • 耘想WinNAS:重新定义Windows电脑的存储革命
  • 一文速通Python并行计算:05 Python多线程编程-线程的定时运行
  • 查看达梦数据库对象
  • 信号与系统(郑君里)第一章-绪论 1-24 课后习题解答
  • C++学习之Linux文件编译、调试及库制作
  • 【AI论文】LeX-Art:通过可扩展的高质量数据合成重新思考文本生成
  • 命令窗口tuna.tsinghua.edu.cn,清华镜像源坏了,如何换成阿里源
  • codeformer论文学习
  • 三、分类模块,通用组件顶部导航栏Navbar
  • AireOS WLC安装License报错
  • Pytorch中torch.nn的学习
  • ‌19.思科路由器:OSPF协议引入直连路由的实验研究
  • keil自学笔记3(按键)
  • sqli-labs靶场 less 11
  • Qt warning LNK4042: 对象被多次指定;已忽略多余的指定