深入理解指针(速通版)

首先感谢大家对我的支持,大家的支持是我持续更新的动力哦!

1.地址与内存

在讲内存和地址之前,我们想有个⽣活中的案例:

    假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩, 如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:

      一楼:101,102,103...

      二楼:201,202,203...

      三楼:301,302,302...

...........

有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。

地址就如同房间号一般,可以快速找到我们希望找到的内容。每个内存单元也都有⼀个编号(这个编号就相当 于宿舍房间的⻔牌号),有了这个内存单元的编 号,CPU就可以快速找到⼀个内存空间。我们也可以这么理解:
内存单元的编号 == 地址 == 指针

2.指针变量和地址

   

int * p = &a;  //p是一个变量(指针变量)
 注:“*”是说明p是指针变量;“int”在说明p指向的对象是int类型。

我们现在在引入一个操作符:解引用操作符(间接访问操作符)‘*’。

int a = 10;
int * p = &a;
//*p = 10 <--> *&a = 10

小知识:指针变量的大小与(地址总线根数)有关,与(地址对象的类型)无关。

故:32位平台下地址是32个bit位,指针变量大小为4个字节(X86)                                                       64位平台下地址是64个bit位,指针变量大小为8个字节(X64)

3.指针类型变量的意义

指针类型 决定了指针进行解引用操作符的时候访问几个字节,也就是决定了指针的访问权限。

char* --->  指向字符的指针

short*--->  指向短整型的指针

int*    --->  指向整型的指针

float* --->  指向单精度浮点型的指针

void* --->  无具体类型的指针(泛型指针)<多用于函数参数的部分>

int a = 10;
void* p = &a;
*p = 20;//error
p = p+1;//error

 故,对void*指针,可以用其存放地址,但不能够进行运算。

4.const修饰指针

举个例子:

               const int a = 10;//a具有了常属性(不能被修改了)

                 <a为常变量,其本质还是变量>

const修饰符有两个位置可以使用,分别是解引用符‘*’的前与后,如下:

一、

      int constr *p = &a;

       p = &b;//OK

       *p=100;//ERROR

     const修饰指针变量时, 放在*的左边,限制的是指针指向的内容,不能通过指针来修改,但可以修改指针变量本身;

二、

       int * const p = &a;

       p = &b;//ERROR

       *p = 100;//OK

     const修饰指针变量时, 放在*的右边,限制的是指针变量本身,其不能再指向其他变量,可通过指针变量修改指针变量指向的内容。

       

5.指针的运算

  a.指针-指针:得到的为两个指针间的元素个数。(指针必须指向同一个数组)

  b.指针+-数字:因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

6.野指针

  a.指针指向的位置是不可知的;

  b.数组越界;

  c.指针指向的空间释放。

这三点是野指针发生的条件。那我们就应当晓得解决办法,如下:

1.明确知道指针应该指向哪里,就给初始化一个明确的地址;                                                          2.如果现在还不知道应该指向哪里,那就初始化为NULL。

7.assert断言

 大致用法

#include<assert.h>  //使用assert函数必须添加的头文件

#define NDEBUG // 关闭assert断言

assert( 表达式  ); //只有表达式成立时,程序才可以运行,若不符合表达式,程序将终止运行

8.指针的传值调用和传址调用

一、strlen函数的功能与模拟实现

     1.库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。                               【其头文件为: #include <string.h>】

     2.模拟实现(a.指针减指针型      b.计数器型       c.递归型 )

a.指针减指针型

#include <stdio.h>
#include <assert.h>

  size_t my_strlen(const char* arr)
{
    assert(arr != NULL);    //判断数组arr是否是空指针
    char* ret = arr;             //记录数组arr
    while (*arr++ != '\0')    
    {
        ;
    }
    arr -= 1;                      //记录数组arr的最后一个元素的地址位置
    return arr - ret;            //数组末地址减去数组首地址,为数组元素个数
}

int main()
{
    char arr[] = "abccdef";
    int ret = my_strlen(arr);
    printf("%d", ret);
    return 0;
}

b.计数器型 

size_t my_strlen(const char* arr)
{
    assert(arr);
    int count = 0;           // 设置计数器
    while (*arr != '\0')
    {
        count++;             //计算数组arr中元素个数
        arr++;
    }
    return count;
}

c.递归型

size_t my_strlen(const char* arr)
{
    assert(arr);
    char* ret = arr;
    if (*arr != '\0')
    {
        return 1 + my_strlen(++arr);        //每次递归时加1,并减少数组arr中1个元素个数
    }
    else {
        return 0;
    }
}

二、传址调用

    我们以交换数字举例

#include <stdio.h>
void Swap2(int*px, int*py)
{
 int tmp = 0;
 tmp = *px;
 *px = *py;
 *py = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(&a, &b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

首先看输出结果:

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传 递给了函数,这种函数调⽤⽅式叫:传址调⽤。

传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。

三、传值调用

#include <stdio.h>
void Swap1(int x, int y)
{
 int tmp = x;
 x = y;
 y = tmp;
}
int main()
{
 int a = 0;
 int b = 0;
 scanf("%d %d", &a, &b);
 printf("交换前:a=%d b=%d\n", a, b);
 Swap1(a, b);
 printf("交换后:a=%d b=%d\n", a, b);
 return 0;
}

运行结果

我们发现在main函数内部,创建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤ Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是 x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤ 的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这 种叫传值调⽤。

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。

9.数组名的理解

数组名就是数组⾸元素(第⼀个元素)的地址。

但是有两个例外:

 • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表示整个数组,计算的是整个数组的大小, 单位是字节

• &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的) 除此之外,任何地⽅使⽤数组名,数组名都表示⾸元素的地址。

10.冒泡排序

冒泡排序的核心思想就是:两两相邻元素进行比较。

11.二级指针

eg;

     int a = 10;

     int *p = &a;         // p是1级指针

     int **pp = &p;     // pp是2级指针

解析 int * * pp = &p;

  "int *"在说明pp执行的p的类型为"int *";

""*pp"中的‘*’说明pp是指针变量。 

12.指针数组与数组指针

一、举例区分

   int * p1[10]; // p1是指针数组  ———存放指针的数组;

   int  (*p2)[10]; //p2是指针变量,指向的是数组 ————数组指针。

二、用指针数组模拟二维数组

    eg:

#include <stdio.h>

int main() 

{

   int arr1[ ] = { 1, 2, 3, 4, 5 };

   int arr2[ ] = { 2, 3, 4, 5, 6 };

   int * arr[ ] = { arr1, arr2 }; // 指针数组arr 可模拟二维数组;

    // 打印数组

    int i = 0;

    int j = 0;

    for( i = 0 ; i < 2 ; i++ )

    {

            for( j = 0 ; j < 5 ; j++ )

                {

                        printf( "%d", arr[i][j] ); // arr[i][j]等价于*( *( arr * i) + j );

                }

    }

   return 0;

}

三、数组的地址要存放在数组指针变量当中

        int (*p) [10] = &arr;

四、顺序打印正整数的每一位(递归法)

#include <stdio.h>

void Print(int n)
{
    if (n > 9)
    {
        Print(n / 10);
        printf("%d ", n % 10);
    }
    else {
        printf("%d ", n % 10);
    }

}

int main()
{
    //顺序打印正整数的每一位
    int i = 0;
    scanf("%d", &i);
    Print(i);
    return 0;
}
 

13.字符数组变量

         char arr [10] = "abcdef";

类型1.

        char * p1 = arr ; <p1为首元素地址>

        * p1 = 'w' ;

        printf("%s", p1);  // "wbcdef";<%s打印字符串时,需要提供起始地址>

类型2.

        (const) char * p2 = "abcdef"; // 常量字符串 ; <p2为首字符地址>

        * p2 = 'w';  // ERROR;

由类型1得,字符数组内容可变;

由类型2得,常量字符串不可被修改。

14.二维数组传参的本质(真正原理)

#include <stdio.h>

void print ( int (*arr)[5] , int r, int c)

{

        int i = 0;

        int j = 0;

        for( i = 0;i < r; i++ )

                {

                        for( j = 0; j < c; j++ )

                                {

                                        printf( "%d ",*(*(arr + i) + j )  );

                                }

                        printf("\n");

                }

int main()

{

        int arr[5][2] = {{1,2},{2,3},{3,4},{4,5},{5,6}};

        print( arr, 5, 2);

        return 0;

}

15.函数指针变量

一、函数指针变量的解析

 

二、函数指针变量的使用

 eg:

int Add ( int x, int y)

{

        return x + y;

}

int main( )
{

        int (*pf)(int, int ) = &Add;

        printf("%d\n", (*pf3)(2, 3));        // 5

        printf("%d\n", pf3(3, 5));            // 8

        return 0;

}

三、有趣的代码(能力拔高)

       ( *(void   (* )(  )  )  0  ) (  );

解析:

        1.void (* )(  ) ——>函数指针类型;

        2.( void (* )(  ) ) ——> 强制类型转换;

        3.*( void (* )(  ) ) 0 ——>将0强制类型转换为“void (* )(  )”的函数指针类型;

        4.其意味着假设0地址处放着无参,返回类型是void的函数。

16.typedef关键字

举例运用

   1.

        typedef int* pint_t   //  之后可以用“pint_t”来替代“int*”;

    2.

        typedef int (*pf_t) [5] // "pf_t"  <==> "int (* )[5]"; 

 

注意(typedef 与 宏定义的区别):

int main()

{

        int p1,p2,p3,p4;

        typedef int* ptr_t;

        #define PTR_T int*;

        ptr_t      p1,p2;  // p1,p2均为指针变量;

        PTR_T  p3,p4;  // p3是指针变量,p4是整型变量。<替换后:int* p3,p4;>

}

17.函数指针数组(多用于回调函数)

数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组, 比如:

int *arr[10];

//数组的每个元素是int*

那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[3])();

int *parr2[3]();

int (*)() parr3[3]; 

答案是:parr1

 parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢?

是 int (* )(  ) 类型的函数指针 。

如果大家看到这里,我相信大家一定对指针有了很深刻的认识。在之后,博主会更新有关函数的系统知识,希望大家关注博主,之后的内容可是干货满满哦!

    别忘了一键三连哦,大家的支持是博主持续更新的动力!!!

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值