【C语言】指针进阶(超详细)【C语言】指针进阶(超详细)

简介: 【C语言】指针进阶(超详细)

指针的进阶

本章重点

  1. 字符指针
  2. 数组指针
  3. 指针数组
  4. 数组传参和指针传参
  5. 函数指针
  6. 函数指针数组
  7. 指向函数指针数组的指针
  8. 回调函数

指针的了解

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间
  2. 指针的大小固定是4/8个字节(32位平台/64位平台)
  3. 指针是有类型的,指针的类型决定了指针±整数的步长,指针解引用操作的时候的权限
  4. 指针的运算

字符指针

一般使用:

int main() {
    char ch = 'w';
    char* pc = &ch;
    *pc = 'l';
    return 0;
}

还有一种使用方法:

#include <stdio.h>
int main() {
    const char* pstr = "hello world!";
    printf("%s\n", pstr);
    return 0;
}

这里特别容易认为是把字符串放到字符指针里,但是本质是把首字符的地址放进去了.

这里就有一道这样的面试题:

#include <stdio.h>
int main() {
    char str1[] = "hello world!";
    char str2[] = "hello world!";
    const char* str3 = "hello world!";
    const char* str4 = "hello world!";
    if(str1 == str2) {
        printf("str1 and str2 are same\n");
    }else {
        printf("str1 and str2 are not same\n");
    }
    if(str3 == str4) {
        printf("str3 and str4 are same\n");
    }else {
        printf("str3 and str4 are not same\n");
    }
}

注意 判断字符串的相等用的是strcmp函数,==是判断地址是否相等

这里的str1和str2指向的是同一个常量字符串.C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存.但是用相同的字符串去初始化不同的数组的时候就会开辟出不同的内存块.

指针数组

指针数组是一个存放指针的数组

int* arr1[10]; //整型指针的数组
char* arr2[10]; // 一级字符指针的数组
char** arr3[10]; // 二级字符指针的数组

数组指针

数组指针的定义

数组指针是指针?还是数组?

当然是一个指针了,我们已经熟悉

整型指针:int* pint能够指向整型数据的指针

浮点型指针:float* pf能够指向浮点型数据的指针

那个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

p2是一个数组指针,p2先和*结合,说明p2是一个指针变量,然后指向是一个大小为10个整型的数组,所以p2是一个指针,指向一个数组,叫做数组指针

&数组名VS数组名

数组名表示数组首元素的地址.

那么&数组名到底是什么?

#include <stdio.h>
int main() {
    int arr[10] = {0};
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}

可见数组名和&数组名打印的地址是一样的,真的一样吗?我们在看一段代码

#include <stdio.h>
int main() {
    int arr[10] = {0};
    printf("arr = %p\n", arr);
    printf("&arr = %p\n", &arr);
    printf("arr + 1 = %p\n", arr + 1);
    printf("&arr + 1 = %p\n", &arr + 1);
    return 0;
}

实际上&arr表示的是数组的地址而不是数组首元素的地址,数组的地址加1跳过的是整个数组的大小,所以&arr+1相对于arr+1的差值是40

数组名不是首元素地址的两个例外:
1. sizeof(arr)
2. &arr,这两个例子代表都是整个数组

数组指针的使用

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col) {
    for(int i = 0; i < row; i ++) {
        for(int j = 0; j < col; j ++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
void print_arr2(int (*arr)[5], int row, int col) {
    for(int i = 0; i < row; i ++) {
        for(int j = 0; j < col; j ++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main() {
    int arr[3][5] = {1,2,3,4,5,6,7,8,9};
    print_arr1(arr, 3, 5);
    print_arr2(arr, 3, 5);
    //数组名arr表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里可以传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    return 0;
}

我们来看看下面代码的意思

int arr[5]; // 存放整型的数组
int *parr1[10];// 指针数组
int (*parr2)[10];// 数组指针
int (*parr3[10])[5];//存放数组指针的一个数组

数组参数,指针参数

一维数组传参

#include <stdio.h>
void test(int arr[]){}//ok?
void test(int arr[10]){} // ok?
void test(int* arr){} //ok ?
void test2(int* arr[20]){} //ok ?
void test2(int** arr)// ok?
int main() {
    int arr[10] = {0};
    int* arr2[20] = {0};
    test(arr);
    test2(arr2);
}

这个地方自己看看就知道怎么回事了

二维数组传参

#include <stdio.h>
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}
int main() {
    int arr[3][5];
    test(arr);
}

二维数组的数组名代表的是第一行元素的地址,所以要用数组指针来接收

一级指针传参

当一个函数的参数是一级指针的时候,函数能接收什么参数?

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

二级指针传参

当函数的参数是二级指针的时候,可以接收什么样的参数?

void test(char **p) {}
int main()
{
    char c = 'b';
    char*pc = &c;
    char**ppc = &pc;
    char* arr[10];
    test(&pc);
    test(ppc);
    test(arr);
    return 0; 
}

函数指针

先看一段代码:

#include <stdio.h>
void test() {
    printf("hello world!");
}
int main() {
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

运行结果是:

输出的是两个地址,这两个地址是test函数的地址,那我们函数的地址要怎么保存起来?

void (*pfun1)();

pfun1可以存放,pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回类型是void

阅读两段有趣的代码

(   *(   void (*)()   )   0   )();
这段代码的意思是把0当成一个函数的地址,转换为一个无参,void返回值的函数指针.
void (*signal(int , void(*)(int)))(int);
这是一个函数声明
第一个参数是int类型的,第二个参数是函数指针类型的,
该函数的返回类型是函数指针类型
我们可以看到这个语句难以阅读,并且最不易阅读的是void(*)(int),所以我们只需要将它重定义
(

一般的重定义都是这个语法,但是这里不能这样,必须将重定义的放进里面变成这样

最后简化为这样,这里重定义的pf_t是一个返回类型.

函数指针数组

把函数的地址放到一个数组中,那么这个数组就叫函数指针数组那么是怎么定义的呢?

int (*pf[10])()

这个函数指针数组的主要用途是来存放函数的地址,来达到简化代码的效果.

使用方法的文章

指向函数指针数组的指针

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

void test(const char* str) {
    printf("%s\n", str);
}
int main()
{
//函数指针pfun
    void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
    void (*pfunArr[5])(const char* str);
    pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
    void (*(*ppfunArr)[5])(const char*) = &pfunArr;
    return 0;
}

回调函数

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


首先了解一下qsort函数的使用,

我们可以看到这个函数一共有四个参数,但是每个参数是什么意思呢?

首先看base,指针指向要排序数组的第一个元素,转换为void指针

num指的是数组中元素的个数,(是一个无符号整形类型)

size关于数组中每一个元素的字节数

compare指向比较两个元素的函数指针,这个函数是两个void*类型的指针.

接下来我们进行一个简单的使用,用来排序一个数组

#include <stdio.h>
#include <stdlib.h>
int cmp(const void* e1, const void* e2) {
    return (*(int*) e1 - *(int*) e2);
}
void print(int arr[], int sz) {
    for(int i = 0; i < sz; i ++) {
        printf("%d ", arr[i]);
    }
}
int main() {
    int arr[] = { 1,3,4,3,0,8,8,3,6 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), cmp);
    print(arr, sz);
}

接下来我们看看如何对结构体数组进行排序

#include <stdio.h>
#include <stdlib.h>
#include <cstring>
struct S {
    char name[200];
    int age;
};
int cmp(const void* e1, const void* e2) {
    return strcmp(((struct S*)e1) -> name, ((struct S*)e2) -> name);
}
void print(struct S s[], int sz) {
    for(int i = 0; i < sz; i ++) {
        printf("%s %d\n", s[i].name, s[i].age);
    }
}
int main() {
    struct S s[] = {{"zhangsan", 20}, {"lisi 18"}, {"wangwu, 30"}};
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), cmp);
    print(s, sz);
}

最后我们使用回调函数,来模拟实现一个qsort函数.这是我们自己写代码来实现的qsort函数,可以交换任意类型的数组.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu{
    char name[20];
    int age;
};
void Swap(char* buf1, char* buf2, int width) {
    for(int i = 0; i < width; i ++) {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1 ++;
        buf2 ++;
    }
}
void bubble_qsort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2)) {
    for(int i = 0; i < sz - 1; i ++) {
        //一趟冒泡排序的过程
        for(int j = 0; j < sz - 1 - i; j ++) {
            if(cmp((char*)base+j*width, (char*)base+(j+1)*width)>0) {
                Swap((char*)base+j*width, (char*)base+(j+1)*width, width);
            }
        }
    }
}
int cmp_stu_by_name(const void* e1, const void* e2) {
    return strcmp(((struct stu*)e1) -> name, ((struct stu*)e2) -> name);
}
void print(struct stu s[], int sz) {
    for(int i = 0; i < sz; i ++) {
        printf("%s %d\n", s[i].name, s[i].age);
    }
}
void test() {
    struct stu s[] = { {"zhangsan", 29}, {"lisi", 30}, {"wangwu", 18} };
    int sz = sizeof(s) / sizeof(s[0]);
    bubble_qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
    print(s, sz);
}
int main() {
    test();
}

函数中很巧妙地地方


1.无论传入任何类型的数组,我们都把他强转为char类型的指针,然后我们通过通过乘以每个元素的长度来访问每个元素,这样就可以访问数组中的每个元素.

2.交换的时候,我们按照一个字节一个字节的交换,这样就可以实现交换.这里的cmp_stu_by_name就是一个回调函数.

总结

前面已经说过数组名的意义,这里再总结一遍

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
  2. &数组名,这里的数组名表示的是整个数组, 取出的是整个数组的地址
  3. 除此之外的所有的数组名都表示首元素的地址.
相关文章
|
3月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
69 0
|
1月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
109 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
1月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
93 9
|
1月前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
53 7
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
223 13
|
2月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
79 11
|
2月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
2月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
180 3
|
2月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
2月前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
57 1

热门文章

最新文章