C 语言指针学习

89 阅读14分钟

指针

  • 内存定义

内存是电脑上重要的寄存器,计算机中所有运行的程序都是在内存中进行的。为了有效的使用内存,就把内存分成一个个小的内存单元,每个内存单元的大小是一个字节。 为了能有效访问到内存的每个单元,就给内存单元进行了编号,这些编号称为内存单元的地址。 一个内存单元存放一个字节

 int a = 10;
    // pa  还指向指针变量,pa 存的是 a 的地址
    int *pa = &a;
    *pa = 20; //通过 pa 里面的地址,找到 a

  • 指针定义
1、指针就是个变量,用来存放地址,地址唯一标识一块内存空间
2、指针的大小是固定 4/8 个字节(32位平台/64位平台)
3、指针是有类型的,指针的类型决定了指针的 +- 整数的步长,指针解引用
操作时的权限
4、指针的运算
  • 指针的大小是固定的,指针是用来存地址的,指针需要多大空间,取决于地址的存储需要多大的空间,指针在 32 位机器: 32 bit(4个字节), 在 64 位机器: 64 bit(8个字节)

  • 指针类型

意义: 1、指令类型决定了解引用的权限有多大: 是修改 4 个字节还是 8 个字节 2、指针类型决定了,指针每走一步,走多远(步长),int 型+1,跳过的是 4 个字节, char 型+1,跳过的是 1 个字节,

以下代码,使用 int * pc 修改 a 的值时,一次能修改 4 个字节,能把 a 的值变为 0;但是使用 char *pc, 只能修改一个字节,a 的值变成 0x11223300, 对应的 int 值为 287453952

    int a = 0x11223344;
    int * pc = &a;
    char *pc = &a;
    //这里 pc 定义的 char* , 只能修改一个字节 ,变成 11 22 33 00,对应的十进制数为 287453952
    *pc = 0;
    printf("a:%d\n", a);

image.png

  • 野指针

场景1: 非法访问内存



    int *p; // p 是一个局部的指针变量,局部变量不初始化的话,默认是随机值
    // 非法访问内存
    *p = 20;

场景2: 指针越界

image.png

3、指针指向已经被释放的空间

int *test()
{
    int a = 10;
    return &a;
}

/**
 * 访问已经被释放的空间
 */
void accessFreeedMemory()
{
    // 执行完这行代码后,a 就已经被系统回收了
    int *p = test();
    //此时通过 *p 访问 a 属于野指针
    *p = 20;
}
  • 如何避免野指针
1、指针初始化
2、小心指针越界
3、指针指向空间释放,及时设置为 NULL
4、指针使用之前检查有效性
  • 指针运算

指针 - 指针, 得到的是两个之间直接元素的个数(指针相减的前提是,两个指针指向同一块内存空间)

  • 数组名在什么情况下不被视为指向起始元素的指针
1、作为 sizeof 运算符的操作数出现时:sizeof(数组名)不会生成指向起始元素的指针长度,而是生成数组整体的长度
2、作为取值运算符 &的操作数出现时: &数组名 不是指向起始元素的指针的指针,而是指向数组整体的指针
  • 指针运算符和下标运算符
指针 p 指向数组中的元素 e 时,
指向元素 e 后第 i 个元素的 *(p + i),可以写为 p[i]
指向元素 e 前第 i 个元素的 *(p - i),可以写为 p[-i]

  • 指针关系运算符
方式一、
float values[N_VALUES];
float *vp;
int len = sizeof(values) / sizeof(values[0]);
    printf("从前往后遍历赋值\n");

for (vp = &values[0]; vp < &values[N_VALUES];)
{
    *vp++ = 1;
}
    
方式二、从后往前遍历赋值
 for (vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}



方式 三: 
/**
     * 这种方式在绝大部分编译器上可以正常运行,然后还是应该避免这么写,因为标准并不保证它可行
     * C 语言标准规定:
     * 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置
     * 的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行
     * 比较
     */
  for (vp = &values[N_VALUES -1]; vp > &values[0]; vp--)
{
    *vp = 0;
}

  • 二级指针
 int a = 10;
    // pa 是指针变量,一级指针
    int *pa = &a;
    // ppa 是二级指针变量:  pa 也是个变量,&pa取出 pa 在内存中的起始地址
    int **ppa = &pa;
    //对二级指针解引用
    **ppa = 20;
  • 指针数组
 //整型指针数组: 存放整型指针
int * piArra[5];
// 字符指针数组: 存放字符指针
char * pcArra[5];
//二级字符指针数组
char ** arr[5]; 

image.png

  • 字符数组
   // 本质上是把 "hello world"这个字符串的首字符的地址存放到了 pc 中
    char *ps = "hello world";
     //ps  的值是常量字符串,常量值是不能改的,因此下面的代码会报错
      *ps = 'w';
    
    
    // 输出: h *pc 是对指针 pc 进行解引用操作,得到的是指针指向的第一个字符的值,即字符串 "hello world" 的第一个字符 'h'printf("%c\n", *ps);
    // %s,它用于打印以 null 字符('\0')结尾的字符串。 pc 是一个指向字符串 "hello world" 的指针,没有进行解引用操作。
    // 因此,这行代码打印的是指针 pc 指向的整个字符串,即 "hello world"printf("%s\n", ps);
  • 字符数组比较

以下代码执行结果是: str1 and str2 are not the same str3 and str4 are the same


  //str1 和 str2 是两个字符数组,它们分别被初始化为字符串 "hello world"。在C语言中,当使用双引号初始化字符数组时,每个数组都会在内存中占据不同的位置,即使它们的内容相同。因此,str1 和 str2 实际上是指向不同内存地址的两个指针
    char str1[] = "hello world";
    char str2[] = "hello world";

    //str3 和 str4 是两个指向字符串常量的指针,它们都指向同一个字符串字面量 "hello world"。在C语言中,所有的字符串字面量通常存储在程序的只读数据段中,并且相同的字符串字面量会指向相同的内存地址。
    // 加上 const 的原因是从语法层面不让修改,因为常量字符串本身时不能修改的
    const char *str3 = "hello world";
    const char *str4 = "hello world";

    if (str1 == str2)
    {
        printf("str1 and str2 are the same\n");
    }
    else
    {
        printf("str1 and str2 are not the same\n");
    }


    if (str3 == str4)
    {
        printf("str3 and str4 are the same\n");
    }
    else
    {
        printf("str3 and str4 are not the same\n");
    }
  • 指针数组 指针数组是一个存放指针的数组
int * arr[] ; //整型指针数组,数组中存放的是地址(指针)
char * arr[];  //一级字符指针数组
char ** arr[];  //二级字符指针数组


  • 数组指针 数组指针是指针,指针指向的是数组的地址。
int arr[10] = {1, 2, 3, 4, 5};
// parr是,一个数组指针,存放数组地址的指针
int(*parr)[10] = &arr;


 double *d[5];
//pd 是一个数组指针 ,
//1、 *首先和 pd 结合说明是一个指针,
//2、指针指向的是数组,数组中的每个元素为 double *, 因此变成 double *(*pd)
//3、数组有5个元素,因此后面添加 [5],
double *(*pd)[5] = &d;

数组名是首元素地址,但有 2 个例外:

1sizeof(数组名): 数组名表示的是整个数组,计算的是整个数组的大小,单位是字节
2、&数组名: 数组名表示整个数组,取出的是整个数组的地址
  • 数组指针的使用 数组指针一般用于二维数组

  • 二维数组

二维数组的数组名表示首元素地址,二维数组的首元素是第一行


/**
 * 方式一、常规方式打印二维数组
 */
void printErweiArray1(int arr[3][5], int row, int column)
{
    printf("printErweiArray1 start --->\n");
    for (size_t i = 0; i < row; i++)
    {
        for (size_t j = 0; j < column; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}


/**
 * 方式二、 数组指针方式打印
 * p 是一个指针,指向的是某一行,每行有5个元素
 */
void printErweiArray2(int (*p)[5], int row, int column)
{
     printf("printErweiArray2 start --->\n");
    for (size_t i = 0; i < row; i++)
    {
        for (size_t j = 0; j < column; j++)
        {
            // *(p + i) 是定位到某一行,也即是该行的首地址, + j 是定位到该行的 第 j 个元素地址, 最后加 * 解引用,得到第 i 行第 j 列的值
            printf("%d ", *(*(p + i) + j));
        }
        printf("\n");
    }
}


 int arr[3][5] = {{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7}};
    //arr数组名是首元素地址
    printErweiArray1(arr, 3, 5);
  • 解释以下指针定义
int arr[] ; //整型数组
int *parr[5]; //整型指针数组, 是数组,数组里面存的元素类型是 整型指针
int  (*parr2) [5] // 数组指针,指针指向一个数组,数组有 10 个元素,每个元素的类型是 int

int  (*parr3 [3])[5] //parr3 是一个存放数组指针的 数组,该数组能够存放 10 个数组指针,每个数组指针能够指向一个数组,数组有5个元素,每个元素是 int 类型,
  • 数组参数

  • 指针参数

  • 用指针打印数组


/**
 * 通过指针打印数组
 */
void printArrayByPoint()
{
    printf("printArrayByPoint start--->");
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    // p为数组首元素地址
    int *p = arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (size_t i = 0; i < sz; i++)
    {
        // p + i 是第 i 个元素的地址
        printf("%d ", *(p + i));
    }
    printf("\n");
}

void printArrayByPoint2()
{
    printf("printArrayByPoint2 start--->");

    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int sz = sizeof(arr) / sizeof(arr[0]);
    // p为数组首元素地址
    int *p = arr;
    // 最后一个元素地址
    int *pend = arr + sz - 1;
    while (p <= pend)
    {
        printf("%d ", *p);
        p++;
    }
    printf("\n");
}
  • 字符串翻转
/**
 * 字符串翻转,流入 abcdef 变成 fedcba
 */
void reverse(char *str)
{
    assert(str);
    int len = strlen(str);
    char *left = str;
    char *right = str + len -1;
    while (left < right)
    {
       int temp = *left;
       *left = * right;
       *right = temp;
       left++;
       right--;
    }
}

 char str[] = "abcdef";
    //这种写法不行,因为 abcdef 是常量字符串不允许修改
    // char *str = "abcdef";
    reverse(str);
    printf("reverse arr:%s\n",str);

  • 指针类型
一级指针
int *p - 整型指针,指向整型的指针
char *pc -字符指针-指向字符的指针
coid *pv -无类型的指针


二级指针
char **p 
int ** p

数组指针
int (*p)[4];  
int arr[10] = 0;
int (*parr)[10] = &arr; //取出数组的地址, paar 是指向数组的指针-存放的是数组的地址


函数指针
指向函数的指针(存放函数地址的指针)
  • 数组
一维数组
二维数组
指针数组 

int* arr[4]-存放指针的数组
  • 函数指针 函数名就是函数地址,因此 &add 和 add 的值是一样的。
int add(int x, int y)
{
    return x + y;
}

void testStr(char *str){
    printf("%s\n",str);
}



  // pa 是函数指针, 参数是 int,int, 返回值是 int
    int (*pa)(int, int) = &add;
    int result = pa(2, 3);
    //result 和 result2 的值是一样的,所以 pa 前面的 * 其实是无用的,只是为了方便理解
    int result2 = (*pa)(2, 3);

    printf("result:%d,result2:%d \n", result, result2);
    // pa 是函数指针,参数是 char *, 返回值是 void
    void (*pt)(char *) = &testStr;
    pt("hello");

  • 有趣的代码
代码1:
 (*(void (*)())0) ();
 
//调用 0 地址处的函数
//该函数无参,返回值是 void
1void(*)() -函数指针类型
2、(void(*)())0 -对 0 进行强制转换,被解释为一个函数地址
3、*(void(*)())0 -对 0地址进行解引用
4、(*(void(*)())0)() -调用 0 地址处的函数

 
代码2:

void (*signal(int, void(*)(int))) (int);

1、signal 和 ()先结合,说明 signal 是函数名
2、signal 函数的第一个参数类型是 int,第二个参数的类型是函数指针,该函数指针,指向一个参数为 int,返回类型是 void 的函数
3、signal 函数的返回类型也是一个函数指针, 该函数指针,指向一个参数为 int,返回类型是 void 的函数

可用如下方式简化:

typedef void (*pfun_t)(int);
pfun_t signal(int,pfun_t);

  • 函数指针数组
int add(int x, int y)
{
    return x + y;
}

int sub(int x, int y)
{
    return x - y;
}

int mul(int x, int y)
{
    return x * y;
}

int division(int x, int y)
{
    return x / y;
}


/**
 * 函数指针数组
 */
void calcByFuncPointerArray()
{
    int input = 0;
    do
    {
        int (*parr[5])(int, int) = {NULL, add, sub, mul, division};

        menu();
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:>");
        scanf("%d", &input);
        if (input >= 1 && input <= 4)
        {

            printf("请输入2个操作数>:");
            scanf("%d %d", &x, &y);

            ret = parr[input](x, y);
            printf("ret: %d \n", ret);
        }
        else if (input == 0)
        {
            printf("退出程序\n");
            break;
        }
        else
        {
            printf("选择错误,请重新输入\n");
        }

    } while (input);
}

  • "指向函数指针数组" 的指针

指向函数指针数组的指针,是一个指针, 指针指向一个数组,数组的元素都是 函数指针

整形数组
int arr[5];
int (*p1)[5] = &arr;

整形指针数组
int * arr[5];
//p2 是指向 ”整数指针数组“ 的指针
int* (*p2)[5] = &arr;

函数指针数组
int (*p)(int,int)  //函数指针
int (*p2[4])(int,int) //函数指针的数组
//*p3  说明是指针,和  【4】结合,说明指向的是数组,数组中的元素类型是函数指针
//p3 就是一个指向 ”函数指针数组“ 的指针
int (* (*p3)[4])(int,int) = &p2 //取出的是函数指针数组的地址




void test(const char *str)
{
    printf("%s\n", str);
}

/**
 * 函数指针:指向函数指针数组的 指针
 */
void pointToFuncPointerArray()
{
    // pfun 是指向函数的指针,称为 "函数指针"
    void (*pfun)(const char *) = test;
    // pfunArr为函数指针数组,  初始化所有元素为NULL
    void (*pfunArr[5])(const char *) = {0};
    pfunArr[0] = test;
    
    // 指向 ”函数指针数组“的 指针: ppfunArr
    void (*(*ppfunArr)[5])(const char *) = &pfunArr;
    (*ppfunArr[0])("Hello, World!");
}

  • 回调函数

回调函数是一个通过函数指针调用的函数,如果你把函数指针(地址)作为参数传递给另一个函数, 当这个指针被用来调用其所指向的函数时,我们称这为回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用,用于对该事件或条件进行响应。

int calc(int (*p)(int, int))
{
    int x = 0;
    int y = 0;
    printf("请输入2个操作数>:");
    scanf("%d %d", &x, &y);
    //通过函数指针调用函数,这种机制称为回调函数
    int ret = p(x, y);
    printf("结果: %d \n", ret);
    return ret;
}

void calcTest()
{
    int input = 0;
    do
    {
        menu();
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:>");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            //把函数的地址 add 传给 calc 
            ret = calc(add);

            break;
        case 2:
            ret = calc(sub);
            break;
        case 3:
            ret = calc(mul);
            break;
        case 4:
            ret = calc(division);
            break;
        case 5:
            printf("输入错误,请重新选择:\n");

            break;
        case 0:
            printf("退出");
            break;

        default:
            break;
        }

    } while (input);
}

  • qsort 排序

int cmp_int(const void *e1, const void *e2)
{
    // 先对 e1 使用 int * 强制换成整型指针,然后再 * 解引用得到值
    int e1Value = *(int *)e1;
    int e2Value = *(int *)e2;
    return e1Value - e2Value;
}

 int arr2[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
 //对整型数组排序
 qsort(arr2, sz, sizeof(arr2[0]), cmp_int);


int sortByAge(const void *e1, const void *e2)
{
    stu stu1 = *(stu *)e1;
    stu stu2 = *(stu *)e2;
    return stu1.age - stu2.age;
}

int sortByName(const void *e1, const void *e2)
{
    stu stu1 = *(stu *)e1;
    stu stu2 = *(stu *)e2;
    return strcmp(stu1.name, stu2.name);
}

   //对结构体排序
 stu list[3] = {{"zhangsan", 28}, {"lisi", 18}, {"wangwu", 10}};
    int sz = sizeof(list) / sizeof(list[0]);
    printf("按年龄排序:\n");

    qsort(list, sz, sizeof(list[0]), sortByAge);
    for (size_t i = 0; i < sz; i++)
    {
        printf("%s %d\n", list[i].name, list[i].age);
    }
    printf("按名字排序:\n");
    qsort(list, sz, sizeof(list[0]), sortByName);
    for (size_t i = 0; i < sz; i++)
    {
        printf("%s %d\n", list[i].name, list[i].age);
    }

  • 自定义冒泡排序
int cmp_int(const void *e1, const void *e2)
{
    // 先对 e1 使用 int * 强制换成整型指针,然后再 * 解引用得到值
    int e1Value = *(int *)e1;
    int e2Value = *(int *)e2;
    return e1Value - e2Value;
}

void swap(char *buf1, char *buf2, int width)
{
    for (size_t i = 0; i < width; i++)
    {
        char temp = *buf1;
        *buf1 = *buf2;
        *buf2 = temp;

        buf1++;
        buf2++;
    }
}

// 模仿 qsort 实现冒泡排序的通用算法

void my_qsort(void *base, int sz, int width, int (*cmp)(const void *e1, const void *e2))
{
    for (size_t i = 0; i < sz; i++)
    {
        for (size_t j = 0; j < (sz - 1 - i); j++)
        {
            // 找出下标 j 和 下标 j+1 的地址
            // j的位置: (char*)base + j * wdith,  j +1 的位置: (char*)base + ( j+1) * wdith
            char *jAddress = (char *)base + j * width;
            char *nextAddress = (char *)base + (j + 1) * width;
            if (cmp(jAddress, nextAddress))
            {
                // 交互元素
                swap(jAddress, nextAddress, width);
            }
        }
    }
}

int main(int argc, char const *argv[])
{
    int arr[] = {9, 8, 7, 6, 5};
    int sz = sizeof(arr) / sizeof(arr[0]);
    my_qsort(arr, sz, sizeof(arr[0]), cmp_int);
    for (size_t i = 0; i < sz; i++)
    {
        printf("%d ",arr[i]);
    }
    
    return 0;
}



  • 存放任意类型指针值
void *  可以存放任意类型指针值

  • 数组名的意义 1、sizeof(数组名),这里的数组名表示的是整个数组,计算的是整个数组的大小 2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址 3、除此之外所有的数组名都表示首元素的地址

  • 预处理