自学内容网 自学内容网

[C语言]第十三节 指针一基础知识到高级技巧的全景探索

目录

13.1. 内存和地址

13.1.1. 内存和地址

13.2. 指针变量和地址

13.2.1 取地址操作符(&)

13.1.2 指针变量和解引⽤操作符(*)

13.1.3 指针变量的⼤⼩

13.3. 指针变量类型

13.3.1 指针的解引⽤

13.3.2 指针+-整数

13.3.3 void* 指针

13.4. const修饰指针

13.4.1 const修饰变量

13.4.2 const修饰指针变量

13.5. 指针运算

13.5.1 指针+- 整数

5.2 指针-指针

13.5.3 指针的关系运算

13.6. 野指针

13.6.1 野指针成因

12.6.2 如何规避野指针

13.7. assert断⾔

断言的工作原理:

使用 assert() 的好处:

如何禁用 assert():

assert() 的缺点:

13.8. 指针的使⽤和传址调⽤

13.8.1 strlen的模拟实现

13.8.2传值调用和传址调用。

13.9. 数组名的理解

13.9.1 数组名的理解

13.10. 使⽤指针访问数组

13.11. ⼀维数组传参的本质

13.12. 冒泡排序

13.13. ⼆级指针

13.14. 指针数组

13.15. 指针数组模拟⼆维数组

13.16. 字符指针变量

13.16.1. 字符指针变量

13.17. 数组指针变量

13.17.1 数组指针变量是什么?

13.18. ⼆维数组传参的本质

13.19. 函数指针变量

13.19.1 函数指针变量的创建

13.19.1 函数指针变量的使⽤

13.20. 函数指针数组

13.21. 转移表

13.22. 回调函数是什么?

13.23. qsort使⽤举例

13.23.1 使⽤qsort函数排序整型数据

13.24. qsort函数的模拟实现

3. bubble 函数


13.1. 内存和地址

13.1.1. 内存和地址

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

 

有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。⽣活中,每个房间有了房间号,就能提⾼效率,能快速的找到房间。 如果把上⾯的例⼦对照到计算中,⼜是怎么样呢?
我们知道计算上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数 据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这些内存空间如何⾼ 效的管理呢?
其实也是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。
计算机中常⻅的单位(补充): ⼀个⽐特位可以存储⼀个2进制的位1或者0
bit - ⽐特位
byte - 字节
KB
MB
GB
TB
PB
1b yte = 8b it
1 KB = 1024b yte
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB
1 PB = 1024 TB
其中,每个内存单元,相当于⼀个学⽣宿舍,⼀ 个⼈字节空间⾥⾯能放8个⽐特位,就好⽐
住的⼋⼈间,每个⼈是⼀个⽐特位。 每个内存单元也都有⼀个编号(这个编号就相当
于宿舍房间的⻔牌号),有了这个内存单元的编 号,CPU就可以快速找到⼀个内存空间。 ⽣活中我们把⻔牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起
了新的名字叫:指针。 所以我们可以理解为:
内存单元的编号 == 地址 == 指针
 2. 如何理解编址
   CPU访问内存中的某个字节空间,必须知道这个 字节空间在内存的什么位置,⽽因为内存中字节 很多,所以需要给内存进⾏编址(就如同宿舍很多,需要给宿舍编号⼀样)。 计算机中的编址,并不是把每个字节的地址记录 下来,⽽是通过硬件设计完成的。 钢琴、吉他 上⾯没有写上“都瑞咪发嗦啦”这样 的信息,但演奏者照样能够准确找到每⼀个琴弦
的每⼀个位置,这是为何?因为制造商已经在乐 器硬件层⾯上设计好了,并且所有的演奏者都知
道。本质是⼀种约定出来的共识! 硬件编址也是如此
⾸先,必须理解,计算机内是有很多的硬件单元,⽽硬件单元是要互相协同⼯作的。所谓的协
同,⾄少相互之间要能够进⾏数据传递。 但是硬件与硬件之间是互相独⽴的,那么如何通
信呢?答案很简单,⽤"线"连起来。 ⽽CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也⽤线连起来。 不过,我们今天关⼼⼀组线,叫做地址总线。
我们可以简单理解,32位机器有32根地址总线, 每根线只有两态,表⽰0,1【电脉冲有⽆】,那么
⼀根线,就能表⽰2种含义,2根线就能表⽰4种含 义,依次类推。32根地址线,就能表⽰2^32种含 义,每⼀种含义都代表⼀个地址。 地址信息被下达给内存,在内存上,就可以找到
该地址对应的数据,将数据在通过数据总线传⼊   CPU内寄存器。

13.2. 指针变量和地址

13.2.1 取地址操作符(&)

理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:
#include <stdio.h>
int main()
{
    int a = 10;
    return 0;
}
⽐如,上述的代码就是创建了整型变量a,内存中
申请4个字节,⽤于存放整数10,其中每个字节都
有地址,上图中4个字节的地址分别是:
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73
那我们如何能得到a的地址呢? 这⾥就得学习⼀个操作符(&)-取地址操作符
#include <stdio.h>
int main()
{
    int a = 10;
    &a;//取出a的地址
    printf("%p\n", &a);
    return 0;
}

按照上述的图,会打印处理:006FFD70 &a取出的是a所占4个字节中地址较⼩的字节的地 址。 变量在内存中的存储 虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可 ⾏的。

13.1.2 指针变量和解引⽤操作符(*)

1 指针变量
那我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要 存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中。
⽐如:
#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;//取出a的地址并存储到指针变量pa中

    return 0;
}

指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。

2.  如何拆解指针类型
我们看到pa的类型是 int* ,该如何理解指针的类型呢?
int a = 10;
int * pa = &a;
这⾥pa左边写的是 int* * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)
类型的对象。
char ch = 'w';
pc = &ch;
.3 解引⽤操作符
我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。
C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。
#include <stdio.h>
int main()
{
    int a = 100;
    int* pa = &a;
    *pa = 0;
    return 0;
}

   上⾯代码中第7⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0. 

13.1.3 指针变量的⼤⼩

   32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后 是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4 个字节才能存储。 如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变的⼤⼩就是8个字节。
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
    printf("%zd\n", sizeof(char *));
    printf("%zd\n", sizeof(short *));
    printf("%zd\n", sizeof(int *));
    printf("%zd\n", sizeof(double *));
    return 0;
}

X86环境输出结果                                                    X64环境输出结果

                                       

结论:
32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。

13.3. 指针变量类型

13.3.1 指针的解引⽤

//代码1
#include <stdio.h>
int main()
{
    int n = 0x11223344;
    int *pi = &n;
    *pi = 0;
    return 0;
}

//代码2
#include <stdio.h>
int main()
{
    int n = 0x11223344;
    char *pc = (char *)&n;
    *pc = 0;
    return 0;
}
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。

13.3.2 指针+-整数


#include <stdio.h>
int main()
{
    int n = 10;
    char *pc = (char*)&n;
    int *pi = &n;

    printf("%p\n", &n);
    printf("%p\n", pc);
    printf("%p\n", pc+1);
    printf("%p\n", pi);
    printf("%p\n", pi+1);
    return 0;
}
代码运⾏的结果如下:
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

13.3.3 void* 指针

  在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 ⾏指针的+-整数和解引⽤的运算。
举例:
#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    char* pc = &a;
    return 0;
}
结果:,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警 ,是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。
VS2022编译的结果 使⽤void*类型的指针接收地址:
结果
可以看到, void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,在《深⼊理解指针(4)》中我们会讲解。

13.4. const修饰指针

13.4.1 const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
#include <stdio.h>
int main()
{
    int m = 0;
    m = 20;//m是可以修改的
    const int n = 0;
    n = 20;//n是不能被修改的
    return 0;
}
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我 们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。
#include <stdio.h>
int main()
{
    const int n = 0;
    printf("n = %d\n", n);
    int*p = &n;
    *p = 20;
    printf("n = %d\n", n);
    return 0;
}
输出结果:
我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了 不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让 p拿到n的地址也不能修改n,

13.4.2 const修饰指针变量

#include <stdio.h>

// 代码1
void test1()
{
    int n = 10;
    int m = 20;
    int* p = &n;
    printf("Before: n = %d\n", n);  // 打印 n 的初始值
    *p = 20;  // 修改 n 的值
    printf("After *p = 20: n = %d\n", n);  // 打印修改后的 n
    p = &m;   // p 指向 m,但不影响 n
    printf("After p = &m: n = %d\n", n);  // n 不受影响
}

// 代码2
void test2()
{
    int n = 10;
    int m = 20;
    const int* p = &n;
    printf("Before: n = %d\n", n);  // 打印 n 的初始值
    // *p = 20;  // 错误,不能修改 *p 指向的内容
    p = &m;   // p 指向 m,但不影响 n
    printf("After p = &m: n = %d\n", n);  // n 不受影响
}

// 代码3
void test3()
{
    int n = 10;
    int m = 20;
    int* const p = &n;
    printf("Before: n = %d\n", n);  // 打印 n 的初始值
    *p = 20;  // 修改 n 的值
    printf("After *p = 20: n = %d\n", n);  // 打印修改后的 n
    // p = &m;   // 错误,p 是常量指针,不能重新赋值
    printf("After p = &m (not executed): n = %d\n", n);  // n 不受影响
}

// 代码4
void test4()
{
    int n = 10;
    int m = 20;
    int const* const p = &n;
    printf("Before: n = %d\n", n);  // 打印 n 的初始值
    // *p = 20;  // 错误,p 指向的内容是常量,不能修改
    // p = &m;   // 错误,p 是常量指针,不能重新赋值
    printf("After (no operations on n): n = %d\n", n);  // n 不受影响
}

int main()
{
    // 测试无 const 修饰的情况
    printf("Test 1:\n");
    test1();
    
    // 测试 const 放在 * 的左边情况
    printf("\nTest 2:\n");
    test2();
    
    // 测试 const 放在 * 的右边情况
    printf("\nTest 3:\n");
    test3();
    
    // 测试 * 的左右两边都有 const
    printf("\nTest 4:\n");
    test4();

    return 0;
}

代码1

代码2

代码3

代码4

  • test2():由于 pconst int*,不能修改 *p 指向的值。p 可以重新指向 m,但 n 的值不变。
  • test3():由于 p 是常量指针,不能重新指向 m。但是,*p = 20 会修改 n 的值为 20
  • test4():由于 p 是常量指针且指向常量数据,既不能修改 p 指向的内容,也不能修改指针本身。n 保持不变。
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本⾝的内容可变。
const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。

13.5. 指针运算

指针的基本运算有三种,分别是:
指针+- 整数
指针-指针
指针的关系运算

13.5.1 指针+- 整数

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

int arr[10] = {1,2,3,4,5,6,7,8,9,10};

​​​​​​​int arr[10] = {1,2,3,4,5,6,7,8,9,10};
数组元素和下标
#include <stdio.h>
//指针+- 整数
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = &arr[0];
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(i=0; i<sz; i++)
    {
        printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
    }
    return 0;
}

5.2 指针-指针

#include <stdio.h>

// 自定义字符串长度计算函数
int my_strlen(char *s)
{
    char *p = s;      // 声明一个指针 p,并初始化为指向字符串 s 的起始位置
    while (*p != '\0') // 遍历字符串,直到遇到字符串结束符 '\0'
        p++;           // p 向后移动,指向下一个字符
    return p - s;      // 返回指针 p 和指针 s 之间的差值,即字符串的长度
}

int main()
{
    printf("%d\n", my_strlen("abc"));  // 输出 "abc" 的长度
    return 0;
}

my_strlen("abc") 会返回 3,因为 "abc" 是由 3 个字符组成的字符串。

13.5.3 指针的关系运算

#include <stdio.h>

int main()
{
    // 定义一个包含 10 个整数的数组,并初始化
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 定义一个指针 p,指向数组 arr 的第一个元素
    int *p = &arr[0];
    
    // 计算数组元素的个数
    int sz = sizeof(arr) / sizeof(arr[0]);
    
    // 使用指针遍历数组,直到 p 超过数组的有效范围
    while (p < arr + sz) // 比较指针 p 和数组的结束位置 arr + sz
    {
        // 输出当前指针 p 所指向的数组元素
        printf("%d ", *p);
        
        // 移动指针 p,指向下一个数组元素
        p++;
    }
    
    // 输出一个换行符以便于美观
    printf("\n");
    
    return 0;
}

13.6. 野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

13.6.1 野指针成因

1. 指针未初始化
#include <stdio.h>
int main()
{
    int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
    return 0;
}
2. 指针越界访问
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = &arr[0];
    int i = 0;
    for(i=0; i<=11; i++)
    {
    //当指针指向的范围超出数组arr的范围时,p就是野指针
         *(p++) = i;
    }
    return 0;
}
3. 指针指向的空间释放 ​​​​​​​
#include <stdio.h>
int* test()
{
    int n = 100;
    return &n;
}
int main()
{
    int*p = test();
    printf("%d\n", *p);
    return 0;
}

12.6.2 如何规避野指针

.1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
初始化如下:
#include <stdio.h>
int main()
{
    int num = 10;
    int*p1 = #
    int*p2 = NULL;

    return 0;
}
2 ⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

下面做个比喻:指针就像一条狗,它可以指向一块内存区域,就像狗可以找到自己的栓住的地方。但如果我们不再需要这个指针去访问内存区域时,最好把它设置为 NULL(就像把狗栓到树上)。为什么呢?

想象一下,如果我们不把狗栓起来,它就变成了一只“野狗”,自由地乱跑。没有人控制它,这样就会很危险,因为它可能随时跑到不该去的地方,或者出事。同样,指针如果没有被正确地设置为 NULL,它也变成了一个“野指针”,它指向的地方可能已经不再有效,访问它会导致程序崩溃或者产生不可预测的错误。

所以, NULL指针 就像是我们把那只野狗栓住了——只要指针是 NULL,我们就不会去访问它。这样就能避免错误。但是,虽然我们把指针设置为 NULL 了,使用指针时还是需要小心。我们不能盲目地去“挑逗”指针(访问它),而是应该在使用之前,先检查一下指针是否为 NULL,确保它不会指向一个无效的地方。如果指针是 NULL,那就说明它已经被“栓起来了”,我们就不去用它;如果不是 NULL,那我们就可以安全地使用它。

4 避免返回局部变量的地址
如造成野指针的第3个例⼦,不要返回局部变量的地址

13.7. assert断⾔

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL);

这行代码的意思是在程序运行到这儿时,检查指针 p 是否为 NULL。如果 p 不为 NULL,程序会继续执行。如果 pNULL,程序会报错并停止执行,同时打印错误信息,告诉你哪里出错了,在哪个文件和哪一行。

断言的工作原理:

  • assert() 宏接受一个表达式作为参数。如果表达式为真(即非零),assert() 不做任何事情,程序继续执行。
  • 如果表达式为假(即为零),assert() 会报错。错误信息会输出到标准错误流 stderr,并且显示出错误的表达式、文件名和行号。

使用 assert() 的好处:

  • 自动定位错误:它不仅会输出错误信息,还会显示出错的文件和行号,方便你快速定位问题。
  • 灵活启用或禁用:你可以轻松地控制是否使用断言。例如,在调试时开启断言,而在正式发布版本时禁用它,避免影响程序性能。

如何禁用 assert()

如果你确定程序没有问题,不想让断言继续运行,可以通过定义一个宏 NDEBUG 来禁用断言:

#define NDEBUG

#include <assert.h>

这样,编译器就会忽略掉所有的 assert() 语句,程序继续正常运行。如果之后需要调试,发现问题,可以把 #define NDEBUG 注释掉,重新编译,断言功能就会重新启用。

assert() 的缺点:

  • 增加运行时间:因为引入了额外的检查,assert() 会使程序的运行时间稍微增加。
  • 调试阶段使用:一般情况下,我们在调试阶段使用 assert() 来帮助找出潜在的问题,而在发布版本中禁用它,以避免影响性能。在集成开发环境(IDE)如 Visual Studio 中,发布版本(Release)默认会优化掉这些断言。

13.8. 指针的使⽤和传址调⽤

13.8.1 strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。
函数原型如下:
size_t strlen ( const char * str );
参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直 到 \0 就停⽌。
参考代码如下:
int my_strlen(const char * str)
{
    int count = 0;
    assert(str);
    while(*str)
     {
         count++;
         str++;
     }
     return count;
}
int main()
{
    int len = my_strlen("abcdef");
    printf("%d\n", len);
    return 0;
}

13.8.2传值调用和传址调用。

在编程中,我们经常会遇到需要在一个函数中修改另一个函数变量的情况。要想实现这种功能,我们需要理解两种函数调用方式:传值调用传址调用

1. 假设我们写一个函数来交换两个整数的值,使用传值调用时,代码如下:

#include <stdio.h>

void Swap1(int x, int y) {
    int tmp = x;
    x = y;
    y = tmp;
}

int main() {
    int a = 0, 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;
}
结果分析:

执行时,我们发现 ab 的值并没有交换。为什么呢?

原因:在 Swap1 函数中,ab 的值被传递给了 xy,但是 xy 是局部变量,它们并没有改变 ab 本身。因为在传值调用中,函数内部的修改不会影响外部变量

2. 传值调用的总结:

在传值调用中,当传递变量时,函数会创建新的变量(形参)来接收值,这些变量是独立的。对形参的修改不会影响实参。

3. 传址调用的解决方案:

为了能够在 Swap 函数中直接修改 ab,我们可以使用传址调用。传址调用是通过传递变量的地址,让函数内部操作的就是原始的变量,而不是它们的副本。

下面是使用传址调用修改后的代码:

#include <stdio.h>

void Swap2(int* px, int* py) {
    int tmp = 0;
    tmp = *px;  // 解引用px,获取a的值
    *px = *py;  // 将b的值赋给a
    *py = tmp;  // 将a的原值赋给b
}

int main() {
    int a = 0, b = 0;
    scanf("%d %d", &a, &b);
    printf("交换前:a=%d b=%d\n", a, b);
    Swap2(&a, &b);  // 传递a和b的地址
    printf("交换后:a=%d b=%d\n", a, b);
    return 0;
}
结果分析:

这次我们成功地交换了 ab 的值。为什么成功呢?

因为我们在 Swap2 函数中传递了 ab 的地址(通过 &a&b),在函数内部通过解引用(*px*py)直接修改了原始变量的值。

4. 传址调用的总结:

传址调用通过传递变量的地址,可以让函数直接修改主调函数中的变量。这种方式适用于需要在函数内部修改外部变量的情况。

13.9. 数组名的理解

13.9.1 数组名的理解

数组名实际上就是数组第一个元素的地址,类似于一个指针。比如:

int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = &arr[0];

使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且
是数组⾸元素的地址,做个测试。
#include <stdio.h>
int main()
{
     int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
     printf("&arr[0] = %p\n", &arr[0]);
     printf("arr = %p\n", arr);
     return 0;
}

arr 变量中,它存储的就是 arr[0] 的地址。你可能会好奇:为什么 sizeof(arr) 输出的是整个数组的大小,而不是指针的大小?这是因为在 sizeof(arr) 中,arr 代表的是整个数组,而不是单个元素的地址。如果你用 &arr 来打印,&arr 是整个数组的地址,而不是数组第一个元素的地址。这就是为什么 &arrarr 不完全一样的原因。

数组名如果是数组⾸元素的地址,那下⾯的代码怎么理解呢?

#include <stdio.h>
int main()
{
     int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
     printf("%d\n", sizeof(arr));
     return 0;
}
输出的结果是:40,如果arr是数组⾸元素的地址,那输出应该的应该是4/8才对。
其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:
sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩, 单位是字节
&数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的)
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
#include <stdio.h>
int main()
{
     int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
     printf("&arr[0] = %p\n", &arr[0]);
     printf("arr = %p\n", arr);
     printf("&arr = %p\n", &arr);
     return 0;
}

 

三个打印结果⼀模⼀样,那arr和&arr有啥区别呢? 

#include <stdio.h>
int main()
{
     int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
     printf("&arr[0] = %p\n", &arr[0]);
     printf("&arr[0]+1 = %p\n", &arr[0]+1);
     printf("arr = %p\n", arr);
     printf("arr+1 = %p\n", arr+1);
     printf("&arr = %p\n", &arr);
     printf("&arr+1 = %p\n", &arr+1);
     return 0;
}

 

&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是
⾸元素的地址,+1就是跳过⼀个元素。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。

13.10. 使⽤指针访问数组

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    int* p = arr;

    // 输入
    for(i = 0; i < sz; i++)
    {
        scanf("%d", p + i);
    }

    // 输出
    for(i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));
    }
    return 0;
}

将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i) ,同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移 量求出元素的地址,然后解引⽤来访问的。

13.11. ⼀维数组传参的本质

#include <stdio.h>
void test(int arr[])
{
    int sz2 = sizeof(arr)/sizeof(arr[0]);
    printf("sz2 = %d\n", sz2);
}

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int sz1 = sizeof(arr)/sizeof(arr[0]);
    printf("sz1 = %d\n", sz1);
    test(arr);
    return 0;
}

在函数内部是没有正确获得数组的元素个数。 这就要学习数组传参的本质,数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址。 所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函 数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
void test(int arr[])//参数写成数组形式,本质上还是指针
{

     printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
     printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{
     int arr[10] = {1,2,3,4,5,6,7,8,9,10};
     test(arr);
     return 0;
}
总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

13.12. 冒泡排序

// 方法1
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
    int i = 0;
    for(i = 0; i < sz - 1; i++)
    {
        int j = 0;
        for(j = 0; j < sz - i - 1; j++)
        {
            if(arr[j] > arr[j + 1])
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

int main()
{
    int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz);
    int i = 0;
    for(i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}
// 方法2 - 优化
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
    int i = 0;
    for(i = 0; i < sz - 1; i++)
    {
        int flag = 1;  // 假设这一趟已经有序了
        int j = 0;
        for(j = 0; j < sz - i - 1; j++)
        {
            if(arr[j] > arr[j + 1])
            {
                flag = 0; // 发生交换就说明无序
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
        if(flag == 1)  // 这一趟没交换就说明已经有序,后续无序排序了
            break;
    }
}

int main()
{
    int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz);
    int i = 0;
    for(i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

 

13.13. ⼆级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?

对于⼆级指针的运算有:
*ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa *ppa 其实访问的就是 pa .

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a .

13.14. 指针数组

指针数组是指针还是数组?
我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
那指针数组呢?是存放指针的数组。

 

指针数组的每个元素都是⽤来存放地址(指针)的。

13.15. 指针数组模拟⼆维数组

#include <stdio.h>
int main()
{
    int arr1[] = {1, 2, 3, 4, 5};
    int arr2[] = {2, 3, 4, 5, 6};
    int arr3[] = {3, 4, 5, 6, 7};

    int* parr[3] = {arr1, arr2, arr3};
    
    int i = 0, j = 0;
    for(i = 0; i < 3; i++)
    {
        for(j = 0; j < 5; j++)
        {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }

    return 0;
}

 

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数 组中的元素。 上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。

 

13.16. 字符指针变量

13.16.1. 字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char* ;
⼀般使⽤:
int main() {
    char ch = 'w';
    char *pc = &ch;  // pc 存储了 ch 的地址
    *pc = 'w';  // 通过 pc 修改 ch 的值
    return 0;
}
此外,也可以用来指向字符串:
int main() {
    const char* pstr = "hello bit.";  // pstr 存储的是字符串的首字符地址
    printf("%s\n", pstr);  // 输出 "hello bit."
    return 0;
}
代码 const char* pstr = "hello bit."; 特别容易以为是把字符串 hello bit 放 到字符指针 pstr ⾥了,但是本质是把字符串 hello bit. ⾸字符的地址放到了pstr中。

13.17. 数组指针变量

13.17.1 数组指针变量是什么?

指针数组是⼀种数组,数组中存放的是地址(指针)。 数组指针变量是指针变量?还是数组?
答案是:指针变量。
整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
下⾯代码哪个是数组指针变量?
int *p1[ 10 ];
int (*p2)[ 10 ];
答案是第二个
p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以
p是⼀个指针,指向⼀个数组,叫 数组指针。
这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。

数组指针变量怎么初始化 数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是之前学习的 &数组名 。

int arr[10] = {0};
&arr;//得到的就是数组的地址

如果要存放个数组的地址,就得存放在数组指针变量中,如下:

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

 

我们调试也能看到 &arr p 的类型是完全⼀致的。
int (*p) [ 10 ] = &arr;
|      |      |
|      |      |
|       |     p 指向数组的元素个数
|       p 是数组指针变量名
p 指向的数组的元素类型

 

13.18. ⼆维数组传参的本质

#include <stdio.h>
void test(int a[3][5], int r, int c) {
    int i, j;
    for (i = 0; i < r; i++) {
        for (j = 0; j < c; j++) {
            printf("%d ", *(*(p + i) + j));  // 通过指针访问二维数组元素
        }
        printf("\n");
    }
}

int main() {
    int arr[3][5] = {{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7}};
    test(arr, 3, 5);
    return 0;
}
这⾥实参是⼆维数组,形参也写成⼆维数组的形式,
⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维 数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

 ⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀ 维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类 型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀ ⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:

总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。 

 

13.19. 函数指针变量

13.19.1 函数指针变量的创建

什么是函数指针变量呢?
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
那么函数是否有地址呢?
 
#include <stdio.h>
void test()
{
    printf("hehe\n");
}
int main()
{
    printf("test: %p\n", test);
    printf("&test: %p\n", &test);
    return 0;
}

确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 & 函数名 的⽅ 式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针 ⾮常类似。如下:
void test()
{
     printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
     return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
int (*pf3) ( int x, int y)
|      |         ------------
|      |              |
|       |        pf3 指向函数的参数类型和个数的交代
|        函数指针变量名
pf3 指向函数的返回类型
int (*) ( int x, int y) //pf3 函数指针变量的类型

13.19.1 函数指针变量的使⽤

#include <stdio.h>

int Add(int x, int y) {
    return x + y;
}

int main() {
    int (*pf3)(int, int) = Add;
    
    printf("%d\n", (*pf3)(2, 3));  // 使用函数指针调用函数
    printf("%d\n", pf3(3, 5));     // 直接使用函数指针调用函数
    return 0;
}

13.20. 函数指针数组

数组是⼀个存放相同类型数据的存储空间,⽐如:

int *arr[10];
//数组的每个元素是int*

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

int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];

 答案是:parr1

parr1 先和 [] 结合,说明 parr1是数组,数组的内容是 int (*)() 类型的函数指针。

13.21. 转移表

函数指针数组的⽤途:转移表

举例:计算器的⼀般实现:
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int sub(int a, int b) {
    return a - b;
}

int mul(int a, int b) {
    return a * b;
}

int div(int a, int b) {
    return a / b;
}

int main() {
    int x, y, input, ret;
    int (*operations[4])(int, int) = {add, sub, mul, div};

    do {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
        printf(" 3:mul 4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);

        if (input > 0 && input < 5) {
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = operations[input - 1](x, y);  // 调用相应的函数
            printf("ret = %d\n", ret);
        } else if (input == 0) {
            printf("退出程序\n");
        } else {
            printf("选择错误\n");
        }
    } while (input);
    
    return 0;
}

13.22. 回调函数是什么?

   回调函数就是⼀个通过函数指针调⽤的函数。
如果把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数
时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条 件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。
    红⾊框中的代码,只有调⽤函数的逻辑是有差异的,我们可以把调⽤的函数的地址以参数的形式 传递过去,使⽤函数指针接收,函数指针指向什么函数就调⽤什么函数,这⾥其实使⽤的就是回调函 数的功能
                                              
使用回调函数改造前sh
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;

    do {
        printf("******************\n");
        printf(" 1:add \n");
        printf(" 2:sub \n");
        printf(" 3:mul \n");
        printf(" 4:div \n");
        printf("******************\n");
        printf("请选择:");
        scanf("%d", &input);

        switch (input) {
            case 1:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = add(x, y);
                printf("ret = %d\n", ret);
                break;
            case 2:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = sub(x, y);
                printf("ret = %d\n", ret);
                break;
            case 3:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = mul(x, y);
                printf("ret = %d\n", ret);
                break;
            case 4:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = div(x, y);
                printf("ret = %d\n", ret);
                break;
            case 0:
                printf("退出程序\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    } while (input);

    return 0;
}

使用回调函数改造后 

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}

void calc(int (*pf)(int, int))
{
    int ret = 0;
    int x, y;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);
    printf("ret = %d\n", ret);
}

int main()
{
    int input = 1;
    do {
        printf("******************\n");
        printf(" 1:add \n");
        printf(" 2:sub \n");
        printf(" 3:mul \n");
        printf(" 4:div \n");
        printf("******************\n");
        printf("请选择:");
        scanf("%d", &input);

        switch (input) {
            case 1:
                calc(add);
                break;
            case 2:
                calc(sub);
                break;
            case 3:
                calc(mul);
                break;
            case 4:
                calc(div);
                break;
            case 0:
                printf("退出程序\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    } while (input);

    return 0;
}

13.23. qsort使⽤举例

13.23.1 使⽤qsort函数排序整型数据

#include <stdio.h>
#include <stdlib.h>

// 比较函数,用于 qsort
int int_cmp(const void * p1, const void * p2)
{
    return (*(int *)p1 - *(int *)p2);
}

int main()
{
    int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
    int i = 0;
    
    // 使用 qsort 排序
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);

    // 输出排序后的数组
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

13.24. qsort函数的模拟实现

使⽤回调函数,模拟实现qsort(采⽤冒泡的⽅式)。
注意:这⾥第⼀次使⽤ void* 的指针,讲解 void* 的作⽤。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义学生结构体
struct Stu
{
    char name[20];  // 名字
    int age;        // 年龄
};

// 按照年龄比较的函数
int cmp_stu_by_age(const void* e1, const void* e2)
{
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

// 按照名字比较的函数
int cmp_stu_by_name(const void* e1, const void* e2)
{
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void test2()
{
    struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
    int sz = sizeof(s) / sizeof(s[0]);
    
    // 使用 qsort 按照年龄排序
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);

    // 输出排序后的结果
    for (int i = 0; i < sz; i++)
    {
        printf("%s %d\n", s[i].name, s[i].age);
    }
}

void test3()
{
    struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
    int sz = sizeof(s) / sizeof(s[0]);

    // 使用 qsort 按照名字排序
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);

    // 输出排序后的结果
    for (int i = 0; i < sz; i++)
    {
        printf("%s %d\n", s[i].name, s[i].age);
    }
}

int main()
{
    test2();
    test3();
    return 0;
}

1. int_cmp 函数

这个函数是用来比较两个整数的大小的。由于 qsort(以及模拟实现)需要一个通用的比较函数,因此它接收的是 void* 类型的指针(可以指向任何类型的数据)。在 int_cmp 函数中,通过强制类型转换把 void* 转换为 int*,然后解引用获取整数值并进行比较。返回值是:

  • 正数:p1 大于 p2
  • 负数:p1 小于 p2
  • 0:p1 等于 p2

2. _swap 函数

 

这个函数实现了交换两个内存块的数据。由于我们使用 void* 指针来表示任意类型的数据,因此我们需要通过 char* 类型来进行字节级别的操作。size 参数表示要交换的数据的大小(以字节为单位)。交换过程是:

  1. p1p2 指向的内存位置逐字节交换数据。
  2. 使用 char 类型来遍历内存块,这样可以处理任意类型的数据。

3. bubble 函数

这个函数模拟了 qsort 的排序逻辑,使用了冒泡排序算法。具体来说:

  • base 是指向待排序数组的指针。
  • count 是数组中的元素个数。
  • size 是每个元素的大小(以字节为单位)。
  • cmp 是一个回调函数,用来比较数组中两个元素的大小。

冒泡排序的基本思想是,通过多次比较相邻元素并交换它们,直到数组排好序。具体步骤:

  1. 外层循环控制排序的轮数,从第一个元素到倒数第二个元素。
  2. 内层循环依次比较相邻的元素,如果前一个元素比后一个大,就交换它们的位置。
  3. 交换是通过调用 _swap 函数来实现的,传递的是两个元素的位置和元素大小。

main 函数中:

  1. 定义了一个整数数组 arr,包含了未排序的数字。
  2. 调用 bubble 函数对数组进行排序:
    • arr 是待排序的数组。
    • sizeof(arr) / sizeof(arr[0]) 计算出数组中元素的个数。
    • sizeof(int) 传递给 bubble 函数,表示每个数组元素的大小(以字节为单位)。
    • int_cmp 是用于比较数组元素的函数。
  3. 排序完成后,通过 for 循环遍历并打印数组中的元素。

代码执行流程总结

  • 首先定义一个未排序的数组 arr
  • 使用冒泡排序算法(通过回调函数 int_cmp 比较元素)对数组进行排序。
  • 最后,打印排序后的数组

原文地址:https://blog.csdn.net/cheng222777/article/details/143892612

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!