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

万字长篇————C语言指针学习汇总

        经过一段时间的学习,我们已经接触到了C语言的很多知识了。不过目前我们接下来我们要接触C语言中一个最大的“门槛”:指针。

什么是指针?

        在介绍指针之前,我们首先要明白变量与地址之间的关系。

        举一个生活中的案例:一个宿舍楼内有一百个房间,你就身处其中一个房间内,你的朋友想来找你玩,但是这么多房间一个一个找很麻烦,效率很低。但是如果我们把每个房间编上号码,如101、102……,这样就方便你的朋友找到你了。

        而对应到C语言中,内存空间就是这个“宿舍楼”,而想要对内存进行高效的管理就需要“房间”来将内存划分为一个又一个的内存单元,就如同学生宿舍一样,每个内存单元的大小对应一个字节,有8个比特位。一个比特位只能存放一个1或0

        内存的储存关系对应关系如上。

        每个内存单元都有一个编号(相当于宿舍门牌号),有了这个编号就可以快速找到一个内存单元。生活中我们将门牌号称之为地址,C语言中我们也将内存单元对应的编号称之为地址,而在C语言中这个地址有一个新的名字:指针。因此有内存单元的编号==地址==指针

        接下来我们该如何理解编地呢?

        

        前面我们讲过变量创建的本质就是向内存申请空间,就会有属于自己的地址,而&符号就是取出变量所在地址的符号。

        

        

        如图所示的代码中,变量p与&a实际上是等价的,因此可以称p为指针变量,而int *是p的类型。我们可以得出以下结论

(1)指针就是地址

(2)指针变量是变量,是专门用来存放地址的变量

(3)存放在指针变量中的值,我们认为是地址。

指针的变量的意义

        指针的解引用

        如下图的代码中,指针变量为怕,而*p就是通过p所在的地址,找到指向的内存空间。实际上*p就是a。

        

        

指针的大小

        如下图的代码,打印出来的就是分别在X64(64位环境)和X86(32位环境)下指针变量的长度。

在X64环境下的结果

在X86环境下的结果

int 是4个字节、char是1个字节,但是由上图可知,在相同环境下int *与char *的大小相同。

        由上图可知,指针变量的长度与类型是无关的。在64位环境下指针变量长度为8;在32位环境下指针变量长度为4。

        既然如此,那不同类型的指针意义是什么呢?实际上指针的类型象征着对取地址权限的差异。如int * 可以取4个字节的地址,char * 只可以取出1个字节的地址,就像下面这张图一样。

指针+-整数

指针的类型决定了指针向后访问的“距离”有多大。

        

        

以上为代码展示,以下为原理概述。

void *  类型指针

        我们知道指针变量都有对应的类型,应用时必须使用对应的类型,使用错误的类型编译器就会报错,但是当对方向我们传递了一个未知类型的指针时我们应该怎么办呢?这时我们就需要用到void *这种特殊类型的指针。void*类型的指针没有具体的类型,又被称之为泛指针,可以用来接受任意类型的指针。但是void * 的指针无法应用于解引用和加减饭。

        void * 类型的指针通常应用于函数参数的部分,用来接收不同类型的指针,实现泛性编程的效果,使得一个函数可以处理多种数据。

指针的运算

指针与整数

        在之前的练习中,我们尝试过用正常循环的方式打印数组的元素,但是这一次我们将使用指针来打印数组的内容。

结果为:

        因为数组的元素是连续存放的,因此当我们得知第一个元素的地址的时候就可以顺藤摸瓜的知道所有的内容了。

指针与指针

        指针-指针 的前提:两个指针指向同一个内存空间

        指针-指针的结果:两个指针之间的元素个数。

结果为:

指针-指针的应用

结果为:·

扩充内容:size_t的概念与使用

在C语言中,size_t 是一种无符号整数类型,专门用于表示对象的大小(如内存块大小、数组元素数量等)。以下是关于 size_t 的关键点:


1. 定义与头文件

定义位置size_t 在标准库头文件 <stddef.h><stdio.h><stdlib.h> 等中通过 typedef 定义。

  • 底层类型:具体类型由编译器和平台决定,通常是:

    32位系统:unsigned int(4字节,0 ~ 4,294,967,295)
  • 64位系统:unsigned long 或 unsigned long long(8字节,0 ~ 18,446,744,073,709,551,615)

2. 为什么需要 size_t
  • size_t array_size = 10;
    int arr[array_size];
    for (size_t i = 0; i < array_size; i++) {
        // 安全遍历
    }内存安全的表示
    size_t 的宽度足够大,可以表示系统中可能的最大对象(与指针大小一致,如 sizeof(void*))。

  • 与 sizeof 运算符兼容sizeof 返回值的类型就是 size_t

  • 避免溢出:无符号特性确保非负值,适合内存相关的计算(如数组索引、内存分配)。


3. 典型使用场景
  • 内存操作函数:如 malloc(n * sizeof(int)) 的参数类型为 size_t

  • 字符串/数组长度:如 strlen() 返回 size_t

  • 循环计数器:遍历数组或容器时,使用 size_t 避免索引越界。

  • size_t array_size = 10;
    int arr[array_size];
    for (size_t i = 0; i < array_size; i++) {
        // 安全遍历
    }

size_t 是C语言中表示对象大小的无符号整数类型,具有平台无关性,确保内存相关操作的安全性。在涉及内存、数组、循环的场景中优先使用它,而非固定类型如 int 或 unsigned int


  • 4. 格式化输出

  • 格式说明符:使用 %zuz 表示 size_tu 表示无符号)。

  • size_t size = 100;
    printf("Size: %zu\n", size); // 正确
    // 错误示例:printf("%d", size);(可能导致未定义行为)

  • 5. 注意事项
  • 无符号回绕:当 size_t 减到 0 后再减 1 时,会回绕到最大值(需注意循环条件)。

  • 与有符号类型的比较:混合使用可能引发警告或逻辑错误(建议显式转换)。

总结

size_t 是C语言中表示对象大小的无符号整数类型,具有平台无关性,确保内存相关操作的安全性。在涉及内存、数组、循环的场景中优先使用它,而非固定类型如 int 或 unsigned int

指针的关系运算

const修饰指针

const修饰变量

        const表示常变量,即你无法改变的。在下图所示的代码中,a是无法直接改变的,在C语言中a被称之为常变量,意思就是a是变量但是无法被修改

但是可以通过以下的方式修改

结果为:

const修饰指针变量

        const可以放在*左边和右边。其代表的含义各不相同。

一、const  int *p

对于这种情况下,const限制的是*p,也就是说指针指向的内容无法通过改变p来修改了

二、int * const p

对于这种情况下,const限制的是变量p本身,p本身无法改变,但是p所指向的内容是可以通过p来改变的。

可以参考如下代码:

野指针

概念:指针指向的位置没有是不可知的(随机的、不正确的、没有限制的)。

每个程序运行后都会向内存申请空间程序中的指针,指向的内存不属于当前的程序,因此就会产生野指针。

野指针成因

        一、指针没有初始化

        

二、指针越界访问

        打印结果如下

当指针超出了数组的范围时候,会随机分配到内存的一个值

三、指针的空间被释放

        

如果你实在要返回n的指针,建议加上static修饰n

这样就能返回n的值了。

PS:这里做这项补充一下之前遗漏的知识点:static和extern

在C语言中,staticextern是两个用于控制变量或函数作用域链接属性的关键字,它们的核心区别如下:


1. static 关键字

用途

  • 限制作用域:将变量或函数的作用域限定在当前文件当前函数内。

  • 改变存储周期:局部变量声明为static时,生命周期延长至程序运行结束。

应用场景

1.静态局部变量(函数内部):

void counter() {
    static int count = 0; // 仅在函数内可见,但生命周期全局
    count++;
    printf("Count: %d\n", count);
}

特点

  • 每次函数调用时保留上一次的值。

  • 初始化仅执行一次

静态全局变量/函数(文件作用域):

static int hidden_var = 42; // 仅当前文件可见
static void internal_func() { ... } // 仅当前文件可调用

2. extern 关键字

用途

  • 声明外部符号:表示变量或函数定义在其他文件中。

  • 扩展作用域:允许跨文件访问全局变量或函数。

应用场景

  1. 引用其他文件的全局变量

    File1.c:int global_var = 100; // 定义全局变量
  2. File2.cextern void public_func(); // 声明外部函数

3. 核心区别对比

特性staticextern
作用域控制限制为当前文件或函数扩展为跨文件访问
存储周期影响可延长局部变量的生命周期不影响存储周期
是否分配存储空间定义时分配声明时不分配,依赖外部定义
多文件项目中的作用隐藏内部实现(信息隐藏)共享全局资源(接口暴露)
默认链接属性内部链接(仅当前文件)外部链接(跨文件可见)

4. 综合示例

场景:多文件项目中控制符号的可见性

FileA.c

static int private_var = 10;    // 仅FileA可见
int public_var = 20;            // 其他文件可通过extern访问

static void private_func() {    // 仅FileA可调用
    // ...
}

void public_func() {            // 其他文件可调用
    // ...
}

FileB.c

extern int public_var;          // 正确:引用FileA的public_var
extern void public_func();      // 正确:调用FileA的public_func

// extern int private_var;      // 错误:无法访问FileA的static变量
// extern void private_func();  // 错误:无法调用FileA的static函数

5. 常见问题

问题1:static全局变量能被其他文件访问吗?

  • 不能static全局变量仅在定义它的文件中可见,其他文件无法通过extern声明访问。

问题2:extern变量可以初始化吗?

  • 不可以extern仅用于声明,而非定义。初始化必须在定义时完成:

extern int var = 5; // 错误:extern声明不能初始化
int var = 5;        // 正确:定义并初始化

问题3:函数默认是static还是extern

  • 默认情况下,函数具有外部链接(相当于extern),除非显式声明为static

总结

  • static:用于隐藏持久化,限制作用域或延长生命周期。

  • extern:用于共享扩展,实现跨文件访问全局资源。

  • 关键设计原则

    • 使用static保护模块内部实现(封装)。

    • 使用extern暴露模块公共接口。

如何规避野指针

        一、指针初始化

        如果明确知道指针的指向哪里就直接赋值地址,如果不知道指针指向哪里,就给指针赋值NULL。

        NULL是C语言定义的一个标识常量,值为0,但是0也是一个地址。但是这个地址无法使用,读写该地址会报错。

        如下所示的代码中,指针变量*p表示传递的是一个野指针。

        二、小心指针越界

        一个程序向内存申请了多少空间,通过指针就只能访问多少空间,不能超出范围,超出了就是非法访问。

        三、避免返回局部变量的地址

        参考成因三图一

        四、当指针变量不再使用的时候,及时地将它们置NULL,指针使用前检查有效性

        当指针变量只想一块区域的时候,我们通过指针可以访问该区域,后期不再使用该指针的时候,我们可以将该指针置为NULL。因为约定俗成的规则就是:只要是NULL指针就不去访问。同时使用前可以判断指针是否为NULL。

        我们可以将指针视之为野狗,野狗放任不管的情况下十分危险的,所以我们可以找一棵树把野狗拴起来,,就相对安全了,给指针变量及时赋值为NULL就是类似于把野狗拴起来,也就是把野指针暂时管理起来。

        不过野狗即使拴起来也很危险,所以我们要绕着走不要挑逗他。对于指针来说也是如此,在使用前,我们也要判断指针是否为NULL,看看是不是被拴起来的野狗。如果是我们不能使用,如果不时我们再去使用。

示范案例:

        

assert断言

        <assert.h>头文件中定义了宏assert(),用于运行时确保程序符合指定条件,如果不符合终止程序,这个宏常常被称之为“断言”。
        

        当程序运行到该段代码时,验证变量p是否等于NULL,如果不等于NULL则程序正常运行,否则程序会 终止运行并给出错误信息。

        assert()宏需要接受一个表达式作为参数,如果表达式为真(返回值为非零),assert()不会产生任何作用,程序继续运行;如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式在内的文件名和行号。

        assert()的使用对程序员是相当友好的,使用assert()有几个好处:它不仅可以自动表示文件和出问题的行号,还有一种无需更改代码就能换开启或者关闭的assert的机制,如果已经确认程序没有问题,就不需要再做判断,就在#include<assert.h>的语句前定义一个宏NDEBUG。

        

        然后重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现了问题,可以移除或者注释掉   #define NDEBUG   这条指令重新编译这样就可以重启assert()这条语句了。

        缺点是:因为引入了额外的检查,增加了程序运行的时间。

        一般我们旨在Debug中shiyong,zai 在Release版本中选择禁用assert,在VS这样的集成开发环境中,在Release版本中,直接就优化掉了。这样在Debug版本中不影响程序员问题,在Release版本不影响用户的效率。

指针的使用和传址调用

        指针的传值和传址调用

        指针的内容写到这里已经不少了,那么实际应用中有什么问题非要用指针不可呢?

        举例:写一段代码实现两个数值之间的交换:

这个代码在上一期的操作符也写过几个,这里用指针在写一次。

算法一:要求有第三个变量

算法二:有溢出风险

算法三:按位异或算法

算法四:传址调用算法

在算法四中,当我们将exchange2函数注释后(即exchange1函数调用),结果为

这是为什么呢?

在exchange1 函数中,函数中定义的a、b与实际main()函数中的a、b并不一样。函数当中的a、b确实接受了main函数中的a、b的值,但是两组变量是独立的空间,无法互相影响。(如下所示)

exchange1函数使用时将变量本身传递给函数,这种调用方式称之为传值调用

结论:当实参传递给函数时形参会单独创立一个空间传递实参,对形参的修改是不影响实参的。

exchange2函数是将变量的地址直接传递给函数,称之为传址调用

        传址调用可以让函数与主函数之间建立真正的联系,在函数内部修改主函数的变量;在未来函数中只是需要主函数中的变量值来进行计算的就可以采用传值调用。如果函数内部要修改主函数内变量的值的,就需要传址调用。

数组名的理解

        数组名实际上就是地址,代码验证如下:

        但是有两个例外

1.sizeof(数组名):单独放数组名这个数组出了表示整个数组,计算的是整个数组的大小,单位是字节。

2.&数组名:这个数组名表示的整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)

实验验证如下:

整个数组的地址和数组首元素的地址是有区别的,具体体现在如下情况

可知,指针类型决定了+1跳过了多少距离。

指针访问数组

算法二与上相同

本质上,p[i]等价于*(p+i)。

在此处,*(p+i)等价于*(arr+i)等价于arr[i]等价于i[arr]。

结果如下:(直截取一部分)

                                                

        原理:[]只是一个下标引用操作符。

数组传参的本质

        之前我们都是函数外进行数组元素个数的计算的,今天我们来试试将数组交给函数,让函数帮我拿计算数组的元素个数。

错误示范:

        

        结果为:

        

        这是为什么呢?

        已知指针变量大小为8,则下列解析:

        由以上可知,本质上数组传参传递的是首元素的地址。

        因此实际上函数形参部分应该使用指针变量来接受首元素的地址。那么在函数内部我们用sizeof()计算的是一个地址的大小而不是数组的大小。正是因为函数参数的本质是地址,所以函数内部没有办法求数组元素个数。

        冒泡排序(重点!!!)

        冒泡排序:两两相邻的元素进行比较。

比如:9 8 7 6 5 4 3 2 1 0将其排成升序,即0 1 2 3 4 5 6 7 8 9 

      

代码如下:

主函数和打印函数

冒泡排序函数:

结果:

        

但是如果当数组中已经有一部分排列有序时候,就会多点计算量,如:

所以我们可以做如下的优化:

        这样效率就会更高。

二级指针

关于二级指针,可通过如下代码及其原理图理解。

        

        

        类比于一级指针,我们可以知道,对*ppa解引用得到的是pa的地址。*ppa访问的是pa

同理,如上图可知,**ppa通过*pa找到pa,然后通过对pa的解引用找到a。因此**ppa访问的就是a

                        

指针数组

        指针数组本质上仍然是数组。类比于字符数组char arr[]是用来存放字符元素的可知,指针数组是用来存放指针类型的的元素的。

        指针数组的每个元素是地址,又单独的指向一片区域。

        但是注意,指针数组的元素必须为同一类型的指针

        以下是指针数组代码的打印。

指针数组模拟二维数组

        在学习过指针数组之后,我们可以用指针数组来模拟一个二维数组。具体原理如下所示。

  有代码如下:

      

结果为:

        

拓展内容:

字符函数

strcpy 函数

  • 作用:将源字符串(包括终止的空字符 \0复制到目标字符数组中。

  • 示例

  • char dest[20];
    strcpy(dest, "Hello");  // dest 变为 "Hello\0"

  • 注意事项

    • 目标数组必须有足够的空间,否则会导致缓冲区溢出(安全隐患)。

    • 更安全的替代函数是 strncpy(可指定最大复制长度)。

strcat 函数

  • 作用:将源字符串追加到目标字符串的末尾(覆盖目标字符串的终止符 \0,并在新字符串末尾添加 \0)。

  • 示例

char dest[20] = "Hello";
strcat(dest, " World");  // dest 变为 "Hello World\0"

  • 注意事项

    • 目标数组必须足够大,且已有内容加上新内容后不溢出。

    • 更安全的替代函数是 strncat(可指定最大追加长度)。


总结

  • strcpy:复制字符串。

  • strcat:连接字符串。

strncpy 函数

  • 作用:将源字符串的前 n 个字符复制到目标字符数组中。如果源字符串长度不足 n,剩余部分用空字符(\0)填充。


3. strstr 函数


4. strcmp 函数


总结

函数作用关键点
strncpy安全复制字符串(限长度)需注意是否补 \0,防溢出
strdup动态复制字符串需手动 free,兼容性依赖环境
strstr查找子串位置区分大小写,返回指针或 NULL
strcmp按字典序比较字符串返回值为0、正、负,区分大小写

掌握这些函数可高效处理字符串操作,但需注意内存管理和边界条件。


如何比较字符串之间的大小

  • 语法

    char *strncpy(char *dest, const char *src, size_t n);
  • 示例

    char dest[10];
    strncpy(dest, "Hello World", sizeof(dest)); // 仅复制前9字符 + 末尾自动补'\0'
    // dest 内容为 "Hello Wo\0"
  • 注意事项

    • 如果 src 长度超过 n,目标字符串不会自动添加终止符 \0,需手动处理。

    • 常用于避免缓冲区溢出(比 strcpy 更安全)。


  • 2. strdup 函数

  • 作用:动态分配内存并复制字符串,返回指向新字符串的指针。需手动释放内存(free)。

  • 语法

    char *strdup(const char *s);
  • 示例

    char *copy = strdup("Hello");
    printf("%s\n", copy); // 输出 "Hello"
    free(copy); // 必须释放内存!
  • 注意事项

    • 非C标准库函数,属于POSIX标准(需包含 <string.h> 或 <stdlib.h>)。

    • 如果内存分配失败,返回 NULL,需检查返回值。

  • 作用:在字符串 haystack 中查找子串 needle 的首次出现位置,返回指向该位置的指针,未找到则返回 NULL

  • 语法

    char *strstr(const char *haystack, const char *needle);
  • 示例

    char *str = "Hello World";
    char *found = strstr(str, "World"); // found 指向 "World" 的起始位置
    if (found != NULL) {printf("Found at position: %ld\n", found - str); // 输出位置差
    }
  • 注意事项

    • 区分大小写(如需要不区分大小写,可用 strcasestr,但非标准函数)。

  • 作用:按字典顺序比较两个字符串,返回值为:

    • 0:两字符串相等。

    • <0str1 小于 str2(按ASCII值逐字符比较)。

    • >0str1 大于 str2

  • 语法

    int strcmp(const char *str1, const char *str2);
  • 示例

    if (strcmp("apple", "apple") == 0) {printf("Equal\n");    // 输出
    }
    if (strcmp("apple", "banana") < 0) {printf("apple < banana\n"); // 输出('a' ASCII值 < 'b')
    }
  • 注意事项

    • 比较时直到遇到 \0 或首个不同字符为止。

    • 安全版本 strncmp 可指定比较的最大长度。

在 C 语言中,字符串本质上是字符数组(以空字符 \0`` 结尾),因此**不能直接使用 == 运算符比较字符串是否相等**(==` 比较的是内存地址,而非内容)。以下是正确比较字符串的方法:


方法 1:使用 strcmp 函数

strcmp 是标准库函数(需包含 <string.h>),按字典序逐字符比较两个字符串,返回值为:

0:字符串内容完全相等

< 0:第一个字符串小于第二个字符串。

示例代码:

  • > 0:第一个字符串大于第二个字符串。#include <stdio.h>

  • #include <string.h>

    int main() {
        char str1[] = "Hello";
        char str2[] = "Hello";
        char str3[] = "World";

        if (strcmp(str1, str2) == 0) {
            printf("str1 和 str2 相等\n");  // 输出此句
        }

        if (strcmp(str1, str3) != 0) {
            printf("str1 和 str3 不相等\n");  // 输出此句
        }

        return 0;
    }

注意事项:

  • 比较时严格区分大小写(如 "hello" 和 "HELLO" 会被认为不相等)。

  • 若需要不区分大小写,可使用 strcasecmp(Linux/UNIX)或 _stricmp(Windows),但非标准函数。


方法 2:使用 strncmp 函数

strncmp 可指定比较的字符数,适用于:

  1. 只比较字符串的前 n 个字符。

  2. 防止未终止的字符串导致内存越界。

示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "HelloWorld";
    char str2[] = "HelloCat";

    // 只比较前5个字符
    if (strncmp(str1, str2, 5) == 0) {
        printf("前5个字符相等\n");  // 输出此句
    }

    return 0;
}

方法 3:手动逐字符比较

如果不使用库函数,可以手动遍历字符数组逐一比较:

示例代码:

#include <stdio.h>

int isEqual(const char *s1, const char *s2) {
    while (*s1 != '\0' && *s2 != '\0') {
        if (*s1 != *s2) {
            return 0;  // 发现不同字符,返回不相等
        }
        s1++;
        s2++;
    }
    // 检查是否同时到达末尾
    return (*s1 == '\0' && *s2 == '\0');
}

int main() {
    char str1[] = "Hello";
    char str2[] = "Hello";
    char str3[] = "Hell";

    if (isEqual(str1, str2)) {
        printf("str1 和 str2 相等\n");  // 输出此句
    }

    if (!isEqual(str1, str3)) {
        printf("str1 和 str3 不相等\n");  // 输出此句
    }

    return 0;
}

关键总结

方法适用场景注意事项
strcmp快速比较完整字符串区分大小写,依赖 \0 终止符
strncmp比较前 n 个字符或防止越界需明确指定长度
手动逐字符比较理解底层原理或特殊需求(如自定义规则)需自行处理边界条件(如 \0 和非等长情况)

常见错误

char *s1 = "abc";
char *s2 = "abc";
if (s1 == s2) {  // 错误!比较的是指针地址,而非内容
    printf("相等\n");
}

始终使用 strcmp 或 strncmp 比较字符串内容!

字符指针变量

        下图所示的是一个字符数组,数组是可以修改的。

        

        但是当你这么写的时候,

        和数组的区别是二图的写法下字符串不可以修改。

        且二图中的代码并不是把字符串赋值给了p,而是把字符串首字符的地址赋值给了p。

但是实际写的时候二图的写法不足够严谨,建议在char前面加上const修饰(二者含义没有变化,但是加上了更方便理解)

结果为

        为什么会这样呢?

        因为当我们创建str1和str2两个数组的时候,会分别的想内存申请两份空间(地址不同不能比),初始化为数组的时候会开辟出不同的字符串;而str3和str4创建的是同一个常量字符串,且常量字符串无法被修改(C\C++会把常量字符串储存到单独的内存区域内),当几个指针指向同一个内容的时候,实际上它们会指向同一块内存。因此只要内容一样,只存储一份就够了,大家共同使用就够了(节省内存)。

数组指针变量

        数组指针变量是什么?

        存放数组的指针,指向的是数组。

        int arr[10]={0}的地址是什么呢?答案是&arr。

        根据前面学到的知识,我们可知&数组名取出的就是数组的地址

&arr与p的类型完全一样

数组指针类型解析:

        

        二维数组传参的本质

        二维数组传参的时候形参是可以写成数组的。

        二维数组实际上是一维数组的数组。而且二维数组的数组如果表示数组首元素的地址,那么它就是第一行的地址。

        第一行的地址是一维数组的地址。

数组形式写法:

指针形式写法:

总结:二维数组传参时,形参可以写成数组也可以写成指针。

函数指针变量

        函数指针变量的创建:

        函数指针变量就是存储函数的地址,指向函数。

        函数名和&函数名都是函数的地址。数组名是首元素的地址,&数组名是数组的地址。

函数指针变量的创建:

上图所示的指针变量后的 int 后面也可以加上形参名字(但是没有实际意义)。

类比上图还有一个:

把指针变量的名字去掉。剩下的就是它的类型。

函数指针变量的调用

通过函数指针变量来调用函数:

结果为:

以下写法与上文等效:

有两段有意思的代码

以下两段代码均来自于书籍:《C语言陷阱与缺陷》

代码1:

类比以上还有一个

代码2:

代码解释:

1.上面的是一次函数声明

2.函数名叫:signal,有两个参数,第一个参数类型是int,第二个参数类型是函数指针void(*)(int)。

3.该函数指针所指向的函数参数为int,返回类型为void。

typedef关键字

        typedef是将复杂的类型重新命名的。通常使复杂的类型简单化。

如何定义两个指针呢?

函数指针的重新命名。

         

但是切记不要想当然的这样写:

类比以下,数组类型指针也是如此:

函数指针数组

        

函数指针数组的用途:

        做一个计算器程序实现加减乘除:(这里仅仅是为了演示,具体层序运行导致的数据丢失不精确等问题暂时不予以考虑,感兴趣者可自行修改)

转移表

初等算法:

定义函数

代码骨干:

这样一个初等的计算器代码就写好了。

        但是我们显然发现一个问题:当函数越来越多的时候,switch语句中case写的就越来越多,代码也越来越臃肿。

        因此可以用函数指针数组来改进(定义函数的内容与上一个算法相同)

效果与上图相同。

但是这种算法也有缺点:函数指针数组内的类型都被写死了,如果要改变很麻烦

回调函数

        利用函数指针,我们可以实现回调函数的效果。

        回调函数的概念

        通过函数指针调用的函数。

        如果你将一个函数的指针作为参数传递给另外一个函数,当这个函数被用来调用其所指向的函数时,被调用的函数就是回调函数。

        回调函数不是由该函数是现房直接调用的,而是在特定的事件或者条件发生的时候由另一方调用的,用于对该事件或条件进行响应。

        

        方案一是直接调用A函数,方案二是将A函数指针传递给B函数。

利用回调函数,针对上面计算器程序的算法一,我们可以做出如下的优化:

定义函数:

代码骨架:

回调函数的机制:

qsort的使用

        qsort是一个库函数。是用来排序的。基于快速排序算法实现的库函数,可以用来排序任意类型的数据.

        上图中我们所写的冒泡排序算法中,可以排整型浮点型数据,但是不能排结构体数据!即只能排列同一类型的数据。

(这里将冒泡排序放到这里再次复习一下)

        

        qsort的语法结构:

        当p1指向的元素大于p2指向的元素的时候返回一个大于0的数字。

        当p1指向的元素等于p2指向的元素的时候返回一个0

        当p1指向的元素小于p2指向的元素的时候返回一个小于0的数字。

        两个字符串比较大小不是比较长度,而是比较对应位置上字符的大小。比如在字符串“abcdefg”和字符串"abcqb"比较大小的时候,因为q的码值大于d,因此即使前面的字符串更长,前面的字符串后续的  f  比  b  更大,仍然是后面的字符串大于前面的字符串。

qsort函数排序代码示例:

头文件包含:

main函数主体:

qsort排列整型数据

qsort排列结构体数据:

模拟qsort函数对冒泡排序进行改造,使之实现对所有数据类型的排列

        关于原来的qsort函数语法结构的分析如下:

        因此,我们仿照qsort函数可以将冒泡排序的类型定义为如下

        在第四个参数中之所以设计为int,是因为两个函数在进行比较过后,只会返回大于0、小于0、等于0三项就足够了。e1 和 e2用void*是为了适应所有类型的数据而设计。加const修饰是因为仅仅只是为了比较大小而不会修改  e1  和  e2  的值,所以加上const来使得代码更加严谨。

        test2函数调用了bobbleSort_qsort函数,bobbleSort_qsort函数又将cmp_int函数指针作为第四个参数,在第一趟的时候,arr数组中1和3进入cmp_int函数,返回值为-2,不满足if语句的判断条件,因此flag仍然等于1,不进行数据交换,跳出内层循环。

        当数据为9和2的时候,满足内层循环if语句的判断条件,因此进入swap函数进行数据交换。

代码:

头文件:

后续刚需的函数:

模拟的冒泡排序函数:

实现函数:

结果:

结构体(本次没有放置打印函数)

        主函数:

以上就是本次指针的内容了。

感谢看到这里的读者朋友,看在如此大的内容量上可否给一个赞呢?感激不尽

但是关于指针方面的内容并没有就此结束。后续我们还会写一些经典的指针例题哦,敬请期待。

        

        

        

        

        

        

        

        

相关文章:

  • 数据库表设计五层分类系统表设计
  • 测试定时发布
  • How AI could empower any business - Andrew Ng
  • VueDOMPurifyHTML 防止 ​​XSS(跨站脚本攻击)​​ 风险
  • 组合数哭唧唧
  • C# 基类型和派生类型之间的转型
  • DDS信号发生器设计(Quartus)
  • 35. 搜索插入位置
  • python数据类型处理题,输出素数题
  • Flink SQL SavePoint最佳实践
  • 【项目(一)】-仿mudou库one thread oneloop式并发服务器实现
  • 【Qt】界面优化
  • 4.1.1 Redis相关命令详解及其原理
  • 深入解析分类模型评估指标:ROC曲线、AUC值、F1分数与分类报告
  • OCCT 入门(3)核心模块与架构
  • 游戏测试入门知识
  • matlab中进行海浪模型仿真
  • 利用pnpm patch命令实现依赖包热更新:精准打补丁指南
  • ARM Cortex汇编宏定义
  • 基于 PyGetWindow 获取窗口信息和控制窗口
  • 中方会否公布铁线礁的领海基线?外交部:中方执法活动旨在反制菲方侵权挑衅
  • 上海质子重离子医院已收治8000例患者,基本覆盖国内常见恶性肿瘤
  • 第一集丨《无尽的尽头》值得关注,《榜上佳婿》平平无奇
  • 从地下金库到地上IP,看海昏汉文化“最美变装”
  • 金隅集团:今年拿地将选择核心热门地块,稳健审慎投资
  • 政治局会议:积极维护多边主义,反对单边霸凌行径