深入理解指针
- 指针
- 1. 内存与地址
- 1.1 认识内存
- 1.2理解编址
- 2.指针变量和地址
- 2.1 取地址操作符(&)
- 2.2 指针变量和解引用操作符(*)
- 2.2.1指针变量
- 2.2.2拆解指针类型
- 2.2.3解引用操作符
- 2.3指针变量的大小
- 3.指针变量类型的意义
- 3.1指针的解引用
- 3.2指针+-整数
- 3.2 `void *`指针
- 4.const修饰指针
- 4.1 `const`修饰指针
- 4.2 `const`修饰指针变量
- 5. 指针运算
- 5.1 指针+-整数
- 5.2 指针-指针
- 5.3 指针的关系运算
- 6. 野指针
- 6.1 野指针的成因
- 6.1.1指针未初始化
- 6.1.2 指针越界访问
- 6.1.3 指针指向的空间释放
- 6.2 避免野指针的方法
- 6.2.1指针初始化
- 6.2.2小心指针越界
- 6.2.3 指针变量不在使用时,及时置NULL,使用时检查有效性
- 6.2.4避免返回局部变量的地址
- 7. `assert`断言
- 8.指针的使用和传址调用
- 8.1 `strlen`的模拟实现
- 8.2 传值调用和传址调用
- 9. 数组名的理解
- 10.使用指针访问数组
- 11. 一维数组传参的本质
- 12.冒泡排序
- 12.二级指针
- 13. 指针数组
- 14.指针数组模拟二维数组
- 15. 字符指针变量
- 16. 数组指针变量
- 16.1 认识数组指针变量
- 16.2 数组指针变量的初始化
- 17.二维数组传参的本质
- 18. 函数指针变量
- 18.1函数指针变量的创建
- 18.2 函数指针变量的使用
- 18.3 `typedef`关键字
- 18.3.1 `typedef`与`define`定义的区别
- 18.4 函数指针数组
- 19.转移表
- 20. 回调函数
- 21.`qsort`的使用
- 21.1 使用`qsort`函数排序整型数据
- 21.2 使用`qsort`排序结构体数据
- 21.3`qsort`函数的模拟实现
- 22.对比`sizeof`和`strlen`
- 22.1 `sizeof`
- 22.2 `strlen`
- 22.3 `sizeof`和`strlen`的对比
指针
1. 内存与地址
1.1 认识内存
- 在日常生活中,假如有一栋宿舍楼,楼上有100个房间,但是没有房间号,恰好此时有朋友找我们,那么就需要一个房间一个房间的去找,但是如果我们根据楼层和楼层的房间情况,给每个房间都编上房间号,如:
一楼:101,102,103.......
二楼:201,201,203.......
三楼:301,302,303.......
......
此时我们有了房间号,如果朋友得到房间号,就能快速的找到我们。
- 其实指针同样可以这样理解。我知道计算机上的CPU在处理数据时,需要的数据时在内存中读取的,处理后的数据同时也会放回到内存中,我们平常说电脑的内存是16/32GB等,其实同样也是把内存划分为一个一个的内存单元,每个内存的单元的大小取1个字节。
- 其中,每个内存单元相当于一个宿舍,一个字节(一个宿舍)能存储8个比特位(8个人)。每个内存单元都有一个编号(相当于房间的门牌号,也就是我们平常说的,你是几零几宿舍的呀),有了这个内存单元的编号,CPU就能快速找到一个内存空间,以至于可以读取到数据。
- 生活中我们把门牌号叫做地址,在计算机中我们把内存单元的编号也成为地址。C语言中给地址起了新名字,叫做:指针。
内存单元的编号 == 地址 == 指针
1.2理解编址
- CPU访问内存的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中的字节有很多,所以需要给内存进行编址(就像给宿舍楼的房间编号一样)。
- 计算机中的编址,并不是把每个字节记录下来,而是通过硬件设计完成的。就像常见乐器上,明明没有“哆啦咪发”的字样也可以发声。因为这是大家约定俗成的一种规则或共识!
- 首先我们必须理解计算机内是有很多的硬件单元,而硬件单元之间是要互相协同工作的。所谓的协同共工作,至少需要能够进行数据的传递。
- 但是硬件与硬件之间是互相独立的,所以它们是被一天所谓的“线”连接起来的。而内存和CPU之间同样也有大量的数据交互,所以二者也是必须用“线”连起来的,这条线就叫做地址总线。
- 我们可以简单的理解,32位机器有32根地址总线,每一根线只有两种状态,分别表示0,1【电脉冲的有无】,那么一根线就能表示2种含义,2根就能表示4种,以此类推。32根地址线就能表示2…^32种含义,每种含义都代表一种地址。
- 地址信息被下达给内存,在内存上,就可以找到该地址对应的信息,让数据再通过数据总线传入CPU的内存寄存器中。
2.指针变量和地址
2.1 取地址操作符(&)
- 了解指针和内存的关系之后,回到C语言,再C语言中创建变量其实就是向内存申请空间,比如:
上述代码就是创建了整型变量a,内存中申请4个字节,用于存放整数10,其中每个字节都有地址,上图中的整型的4个字节都有地址。
#include <stdio.h>
int main()
{
int a = 100;
printf("a = %p",&a);
return 0;
}
虽然整型变量只占4个字节,但是我们其实只需要知道第一个字节,就能顺腾摸瓜访问到第4个字节;
2.2 指针变量和解引用操作符(*)
2.2.1指针变量
- 我们通过取地址操作符
&
拿到的是一个数值,比如:00CFFBF0
,这个数值有时候需要存放起来,方便我们后期使用,那么我们把它放在哪里呢?没错就是 指针变量 中。
例如:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并储存在指针变量pa中
return 0;
}
指针变量也是一种变量,这种变量就是用来存放地址的,存放地址在指针变量中的值就可以理解为指针;
2.2.2拆解指针类型
我们看刚刚的代码pa
的类型为int *
:
int a = 10;
int* pa = &a;
这里左边写的是int *
,
-
*
是在说明pa
是指针变量 -
int
是在说明pa
指向的是整型int
类型的对象
-
如果有一个
char
类型的变量ch
,ch
变量的地址同样的可以放在char *
类型的指针变量中里面。
2.2.3解引用操作符
- 我们把地址保存在指针变量中,那我们要怎么使用呢?在现实生活中,我们使用地址找到了房间,在房间里面可以拿或存放物品。
- C语言中也一样,我们拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,所以我们就需要用到一个操作符,解引用操作符
*
#include <stdio.h>
int main(){
int a = 100;
int* p = &a;
*p = 0;
return 0;
}
上述代码中就是用了就解引用操作符,*p
的意思就是通过p
中存放地址,找到指向的空间,所以*p
就是a
变量了,所以这里如果我们对*pa
进行赋值*pa = 0
,这样就能通过*
操作符把a
改成0
了;
2.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(32位)环境下输出的结果
- x64(64位)环境下输出的结果
结论
- 32位平台下地址是32个bit位,指针变量大小是4个字节
- 64位平台下地址是64个bit位,指针变量大小是8个字节
- 指针变量的大小与类型无关,只要是指针类型的变量,在相同的平台下,大小是相同的。
3.指针变量类型的意义
3.1指针的解引用
观察下面两段代码:
//代码一:
#include<stdio.h>
int main()
{
int n = 0x11223344;
int* p = &n;
*p = 0;
return 0;
}
//代码二:
#include<stdio.h>
int main()
{
int n = 0x11223344;
char* p = (char *)&n;
*p = 0;
return 0;
}
调试过程中我们可以看到,代码一会将n
的4
个字节全部改为0
,但是代码二只是将n
的第一个字节改为0
。
结论:
指针类型决定了,对指针解引用的时候就会有对应的权限大小(一次能操作几个字节)
例如:char*
的指针解引用就只能访问一个字节,而int *
的指针解引用就能访问4个字节
3.2指针±整数
观察以下代码:
#include<stdio.h>
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("&n = %p\n",&n);
printf("pc = %p\n", pc);
printf("pc + 1 = %p\n", pc+1);
printf("&pi = %p\n", pi);
printf("pi + 1 = %p\n", pi+1);
return 0;
}
我们可以看出,char *
类型的指针变量+1跳过1个字节,int *
类型的指针变量+1跳过4个字节。这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或向后走一步有多大(距离)。
3.2 void *
指针
- 在指针类型中有一种特殊的类型是
void *
类型的,我们可以理解为无具体指针类型(或者叫泛性指针类型),这种指针类型可以用来接受任意类型地址。但是有局限性,void *
类型的指针不能直接进行±整数和解引用的运算。
例如:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
上面代码中,将一个int
类型的变量的地址赋给了一个char*
类型的指针变量。编译器会给出警报如上图,因为类型不兼容。但是如果使用void *
类型就不会有这样的问题。
使用void *
类型的指针接收地址:
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
* pa = 10;
* pc = 0;
return 0;
}
这里我们可以看到,void *
类型的指针可以接收不用类型的指针,但是无法直接进行指针运算;
void *
的类型的指针可以运用到函数参数类型部分,用来接收不同类型的指针,这样的设计就可以实现泛型编程的效果。是一个函数可以出理多个数据,具体的运用可以看下面函数部分。
4.const修饰指针
4.1 const
修饰指针
- 我们把变量的地址传给一个指针变量,通过指针变量可以修改这个变量,但是如果我们希望一个变量加上一些限制,使之不能被修改,这时候就需要用到
const
例如:
#include<stdio.h>
int main()
{
int n = 0;
n = 20;//此时n是可以修改的
const int n = 0;
n = 20;//此时n不能被修改
return 0;
}
上述代码中的n
在被const
修饰后,n
就不能被修改,但是n
本质上是变量,只不过被const
修改后,在语法上加了限制,所以导致我们在代码中对n
进行修改就不符合语法规则,编译器就会报错,致使没法直接修改n
的值(在除C语言以外的其他语言可能不适用该语法规则)
但是如果我们绕开n
,使用n
的地址去修改n
的值就能做到;
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n",n);
int* p = &n;
*p = 520;
printf("n = %d\n",n);
return 0;
}
我么你在这里通过把n
的地址传给p
,用p
指针变量对n
进行修改,但是我们需要思考一下,我们用const
修饰n
不就是为了让n
的值不被修改,那我们用指针变量去修改了不就是变相的破坏了语法规则,打破了const
的限制,所以const
此时就涉及到了修饰指针变量的问题了。
4.2 const
修饰指针变量
#include <stdio.h>
int main()
{
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;
p = &m;
}
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;
p = &m;
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;
p = &m;
}
void test4()
{
int n = 10;
int m = 20;
const int* const p = &n;
*p = 20;
p = &m;
}
int main()
{
test1();
//无const修饰的情况
test2();
//const放在*的左边
test3();
//const放在*的右边
test4();
//从const放在*左右两边
}
return 0;
}
结论:
const
如果放在*
的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可以被修改;const
如果放在*
的右边,修饰的是指针变量本身,保证指针变量的内容(里面存储的地址)不能被修改,但是指针变量指向的内容可以被修改;
5. 指针运算
指针运算有三种基本运算方式:
- 指针±整数
- 指针-指针
- 指针的关系运算
5.1 指针±整数
因为数组在内存中是来连续存放的,只要我们知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素
#include <stdio.h>
//指针+-整数
int main()
{
int arr[] = { 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));
}
return 0;
}
5.2 指针-指针
- 前提是指针指向同一块空间
#include<stdio.h>
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
{
p++;
}
return p - s;//指针-指针
}
int main()
{
printf("%d\n",my_strlen("abc"));
return 0;
}
5.3 指针的关系运算
//指针的关系运算
#include<Stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz)
{
printf("%d ",*p);
p++;
}
return 0;
}
6. 野指针
- 野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
6.1 野指针的成因
6.1.1指针未初始化
#include<stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认是随机值
*p = 20;
return 0;
}
6.1.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;
}
6.1.3 指针指向的空间释放
#include <stdio.h>
int test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n",*p);
return 0;
}
6.2 避免野指针的方法
6.2.1指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给指针进行赋值NULL
,NULL
是C语言中定义的一个标识符,值是0
这个地址是无法使用的,读写该地址编译器会报错。
初始化如下:
#include <stdio.h>
int main()
{
int n = 10;
int* p1 = &n;
int* p2 = NULL;
return 0;
}
6.2.2小心指针越界
- 一个程序向内存申请了那些空间,通过指针也就能访问哪些空间,不能超过范围访问,超出了解释越界访问了。
6.2.3 指针变量不在使用时,及时置NULL,使用时检查有效性
- 当指针变量指向一块区域的空间时候,我们就可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们就可以把该指针置为空。我们可以理解为有一条约定俗成的规则:只要是
NULL
指针就不去访问,同时使用指针之前需要判断指针是否为NULL
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];sh
for (int i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使用的时候,判断p不为NULL的时候再使用
p = &arr[0];
if (p != NULL)
{b
........
}
return 0;
}
6.2.4避免返回局部变量的地址
- 不要返回局部变量的地址。
7. assert
断言
assert.h
头文件定义了宏assert()
,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏通常被称为“断言”
assert(p != NULL);
这段代码被程序执行时,是验证变量p
是否等于NULL
.如果不等于NULL
,程序继续运行,否则就会终止运行,并给出报错信息提示。
assert()
宏接受一个表达式作为参数。如果该表达式为真(返回值非零),assert
不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assert()
就会报错,在标准错误流stder
中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号,以便我们快速找到代码的问题所在。
assert
的优点:
- 能自动标识文件和出问题的行号
- 能通过特定的方式随时开启或关闭
assert
机制- 如果确定了程序没有问题,就可以在
#include <assert.h>
前面定义一个宏NDEBUG
- 如果确定了程序没有问题,就可以在
#define NDEBUG
#include <assert.h>
定义了以后,重新编译程序,编译器就会金庸文件中所有的assert()
语句。如果程序又出现问题,可以移除#define NDEBUG
指令(或者注释掉),再次编译就能再次启动assert()
语句找错误。
缺点:
- 引入了额外的的检查,增加了选择程序的时间。
一般情况下我们在Debug
中使用,在Release
版本中选择禁用assert
,在VS
这种集成开发环境中,Release
版本下这个功能直接就被优化掉了。这样在debug
版本中写代码就有利于程序员排查问题,在Release
版本中就不影响用户使用时的程序效率
8.指针的使用和传址调用
8.1 strlen
的模拟实现
- 库函数
strlen
的功能是求字符串长度,统计的时字符串中\0
之前的字符个数。
函数原型如下:
size_t strlen (const char * str)
参数接收一个字符串的起始地址,然后向后开始统计字符串中\0
之前的字符个数,最终返回长度。
如果要模拟实现只要从起始地址,然后开始向后逐个遍历字符,只要不是\0
字符,计数器就+1
,知道遇到\0
才停止
#include <stdio.h>
#include <assert.h>
int my_strlen(const char* str)
{
assert(str != NULL);
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n",len);
return 0;
}
8.2 传值调用和传址调用
我们先来写一个函数,交换两个变量:
#include <stdio.h>
void Swap1(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
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);
Swap1(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
函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式叫传值调用。
结论:
- 实参传递给形参的时候,形参会单独创建一份临时龙剑来接收实参,对形参的修改不会影响实参。
改进:
我们现在要解决的就是调用Swap
函数的时候,Swap
函数内部操作的就是main
函数中的a
和b
,直接将a
和b
的值交换了。那么就可以使用指针进行修改,在main
函数中将a
和b
的地址传递给Swap
函数,Swap
函数里边通过地址间接的操作main
函数中的a
和b
,这样就能达到交换的效果。
#include <stdio.h>
void Swap2(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a = 0;;
int b = 0;
scanf("%d%d", &a, &b);
printf("交换前:a = %d b = %d\n", a, b);
Swap2(&a, &b);
printf("交换后:a = %d b = %d\n", a, b);
Swap1(a, b);
return 0;
}
这里传递变量地址给函数的方式,我们叫做:传址调用
传值调用和传址调用:传址调用,可以让主函数和函数之间建立真正的联系,在函数内部可以修改主函数中的变量,所以只是需要主函数的变量值来实现计算时,就可以采用传值调用。如果函数内部要修改主函数中的变量值,就可以使用传址调用。
9. 数组名的理解
我们在使用指针访问数组内容时,有这样的代码;
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int* p = &arr[0];
这里我们使用&arr[0]
的方式拿到了数组的第一个元素的地址,但是其实数组名本来就市地址,而且是数组首元素的地址:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n",&arr[0]);
printf("arr = %p\n", arr);
return 0;
}
我们发现数组名和首元素地址打印出来的结果一模一样,数组名就是首元素地址(第一个元素)的地址。
但是并不是所有的数组名都表示首元素地址,存在两个特例,其一:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n",sizeof(arr));
return 0;
}
例外:
sizeof
(数组名):```sizeof``中单独存放数组名,这里的数组名表示真个数组,计算的是整个数组的大小;&
数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素地址是有区别的)
除此之外,任何地方使用数组名,数组名都是表示首元素地址。
&
数组名*从值上看,地址值与首元素地址相同,但&
***数组名***是整个数组的地址,是有区别的,我们看下面这个代码:
#include <stdio.h>
int main()
{
int arr[] = { 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;
}
可以看到,三个打印的结果是一模一样的,所以我们再看以下代码:
#include <stdio.h>
int main()
{
int arr[] = { 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
个字节,arrr
和arr+1
相差4
个字节,因为&arr[0]
和arr
都是首元素地址,+1
相当于跳过一个元素。
但是&arr
和&arr+1
相差40
个字节,这就是因为&arr
是整个数组的地址,+1
操作就是跳过整个数组。
10.使用指针访问数组
#include <stdio.h>
int main()
{
int arr[10] = { 10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]); // 计算数组的长度。sizeof(arr)返回整个数组的大小(字节),sizeof(arr[0])返回数组中单个元素的大小(字节)。两者相除即为数组的元素个数
int* p = arr; // 定义一个整数指针p,并使其指向数组arr的首地址
// 循环读取用户输入的10个整数,存储到数组arr中
for (i = 0; i < sz; i++)
{
scanf("%d",p+i); // 使用指针p+i来定位数组中当前要存储的位置,然后通过scanf函数读取用户输入的一个整数,并存储到该位置
}
// 循环输出数组arr中的所有整数
for (i = 0; i < sz; i++)
{
printf("%d ",*(p+i)); // 使用指针p+i来定位数组中当前要输出的整数,然后通过*操作符取得该整数的值,并使用printf函数输出
}
return 0;
}
- 数组就是数组,是一块连续的空间(数组的大小和数组的元素个数,以及元素类型都有关系)
- 指针变量就是指针变量,是一个变量(4/8个字节)
- 数组名是地址,是首元素地址
- 可以用指针来访问数组
- 上述代码中的
*(p+i)
可以与p[i]
替换
数组名arr
是首元素地址,可以赋值给p
,这里的数组名arr``和
p```是等价的。所以我们可以使用arr[i]
访问数组元素,同样的p[i]
也可以访问数组:
#include <stdio.h>
int main()
{
int arr[10] = { 10 };
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)
的。 - 那么数组元素的访问在编译器处理的时候,也可以转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问。
11. 一维数组传参的本质
- 我们知道数组可以传递给函数,之前我们实现了在函数外部计算元素个数,那如果我们把数组传递给函数后是否能在函数内部进行计算呢:
#include <stdio.h>
void test(int arr[])//数组传参可以写成数组形式,但本质上还是指针变量;
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n",sz2);//结果为1,得不到元素个数
}
int main()
{
int arr[] = { 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)
计算的就是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数参数的部分本质上是指针,所以在函数内部是没办法求数组元素的个数
形参的数组是不会单独创建数组空间的,所以可以数组大小省略不写。
#include<stdio.h>
void test(int arr[])//参数写成数组的形式,本质上还是指针,所以可以写成int* arr
{
printf("%d\n",sizeof(arr));
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
test(arr);
return 0;
}
总结:
- 一维数组传参,形参部分可以写成数组形式,也可以写成指针形式、
12.冒泡排序
- 冒泡排序的核心思想:两两相邻的元素进行比较
//方法一:
#include <stdio.h>
void bubble_sort(int* arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main()
{
int arr[] = { 7,3,2,5,6,8,9,1,4,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,sz);
for (int i = 0; i < sz; i++)
{
printf("%d ",*(arr+i));
}
return 0;
}
//方法二:优化:
#include <stdio.h>
void bubble_sort(int* arr, int sz)//参数接收数组元素个数
{
int i = 0;
for (i = 0; i < sz; i++)
{
int j = 0;
int flag = 1;//假设这一趟已经有序了
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
flag = 0;//发生交换就说明无序
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (flag == 1)//这一趟没交换就已经有序,后续则无需继续排序循环了
{
break;
}
}
}
int main()
{
int arr[] = { 7,3,2,5,6,8,9,1,4,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,sz);
for (int i = 0; i < sz; i++)
{
printf("%d ",*(arr+i));
}
return 0;
}
12.二级指针
指针变量也是变量,是变量就会有地址,那指针变量d地址存放的地方,我们就可以叫做二级指针
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
int** pa = &p;
return 0;
}
对于二级指针运算有:
*ppa
通过对ppa
中的地址进行解引用,这样找到的就是pa
,*ppa
其实访问的就是pa
:
int b = 10;
*ppa = &b;//等价于pa = &b;
**ppa
先通过*ppa
找到pa
,然后对pa
进行解引用操作:*pa
找到的就是a
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
13. 指针数组
- 指针数组是数组。
- 整型数组,存放的是整型的数组;字符数组,存放的是字符的数组,所以指针数组,存放指针的数组。
指针数组的每个元素都是用来存放(地址)指针的:
指针数组的每个元素是地址,有可以指向另一块区域
14.指针数组模拟二维数组
#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数组中
int* parr[3] = { arr1,arr2,arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ",parr[i][j]);//本质上是指针*(*(p+i)+j);
}
printf("\n");
}
return 0;
}
parr[i]
是啊访问parr
数组的元素,parr[i]
找到的数组元素指向了整型一维数组,parr[i][j]
就是整型一维数组中的元素;- 上述代码模拟出的只是二维数组的效果,实际上并不是二维数组,因为每一行并非是连续的。
15. 字符指针变量
- 在指针类型中我们知道有一种指针类型为字符指针
char*
;
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
另一种:
int main()
{
const char* str = "hello 优快云";
printf("%s\n",str):
return 0;
}
代码中的const char* str = "hello 优快云";
容易让人理解为是把字符串hello 优快云
放到字符指针str
中去了,但是本质上是把字符串hello 优快云
首字符的地址放到```str``中。
就是说把常量字符串的首字符h
的地址放到指针变量str
中了。
16. 数组指针变量
16.1 认识数组指针变量
数组指针变量是指针变量
- 整型指针变量:
int * ptr
存放的是整型变量,能够指向整型数据的指针 - 浮点型指针变量:
float * pf
存放的是浮点型变量的地址,能够指向浮点型的数据指针
int *p1[10];//p1是数组名,存放指针的数组;
int (*p1)[10];//p1是指针变量,指向的是数组
一下面的语句为例:
int (*p)[10]
p
先和*
结合,说明p
是一个指针变量,然后指向的是一个大小为10的整型数组,所以p
是一个指针,指向一个数组,所以叫做数组指针
-[]
方括号的优先级高于*
号,所以必须加上()
来保证p
先和*
结合
16.2 数组指针变量的初始化
数组指针变量是用来存放数组地址的,我们可以用&数组名
的方式或的数组的地址
int arr[10] = {0};
&arr;//得到的就是数组的地址
如果要存放数组的地址,就需要存放在数组指针变量中:
int (*p)[10] = &arr;
其中int (*)[10]
是数组指针的类型;
int (*p)[10] = &arr;
int
表示p
指向的是数组的元素的类型(*p)
表示p
是数组指针变量名[10]
表示p
指向数组的元素个数
17.二维数组传参的本质
- 过去我们有一个二维数组需要传递给函数时,我们是这样写的:
#include <stdio.h>
void test_arr(int arr[][5], int r, int z)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < z ;j++)
{
printf("%d ",arr[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(arr,3,5);
return 0;
}
这里的实参是二维数组,形参也是用二维数组的形式接收的
我们理解二维数组的时候,可以将二维数组的起始看作是每个元素是一维数组的数组,也就是说二维数组的每个元素是一个一维数组,那么二维数组的首元素就是第一行,是一个一维数组,如图:
所以,根据数组名是首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址,根据上面的代码例子,第一行的一维数组的类型是int (*) arr[5]
,所以第一行的地址的类型就是数组指针类型int (*) arr[5]
。那么这就意味着,二维数组传参的本质相当于就是传递了地址,传递的是第一行的这个一维数组的地址,所以形参也可以写成指针的形式。如下代码:
#include <stdio.h>
void test_arr(int(*arr)[5], int r, int z)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < z; j++)
{
printf("%d ", arr[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(arr, 3, 5);
return 0;
}
总结:二维数组传参,形参的部分可以写成数组,也可以写成指针的形式。
18. 函数指针变量
18.1函数指针变量的创建
根据前面介绍的整型指针,数组指针,我们可以联想类比得到结论:函数指针变量应该是用来存放函数地址的,可以通过地址来调用函数。那么我们怎样来获得函数的地址呢,我们可以看下面的代码:
#include <stdio.h>
void test()
{
printf("Hello! 优快云.");
}
int main()
{
printf("test: %p\n",test);
printf("&test: %p\n", &test);
return 0;
}
我们可以看到函数确实是有地址的,并且函数名就是函数的地址,当然我们也可以通过&数组名
的方式获得函数的地址。
所以如果我们要将函数的地址存放起来,那么就需要创建函数指针变量,函数指针变量的写法和数组指针很相似。如下代码:
#include <stdio.h>
void test()
{
printf("Hello! 优快云.");
}
int main()
{
void (*pf1)() = &test;
void (*pf2)() = test;
return 0;
}
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf3)(int x, int y) = Add;//x,y可以省略不写
int (*pf4)(int, int) = &Add;
return 0;
}
函数指针类型解析:
18.2 函数指针变量的使用
通过函数指针调用指针指向的函数:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf3)(int, int) = Add;
printf("%d\n",(*pf3)(1300,14));//函数指针调用
printf("%d\n", (pf3)(500, 20));//通过函数地址直接调用,无需解引用也可以,所以加不加*都可以解引用
return 0;
}
18.3 typedef
关键字
typedef
是用来类型重定义的,可以将复杂的类型简单化
例如,我们在敲代码过程中,觉得unsigned int
写起来不方便,如果写成uint
就很方便简洁,那么我们就可以使用:
typedef unsigned int uint;
//将unsigned int 重命名为uint;
如果是指针类型同样也可以,比如:将int
重命名为ptr
:
typedef int* ptr;
但是对于数组指针和函数指针就有一定区别:
typedef int(*ptr)[5];
//新的类型名ptr必须放在*的右边
函数指针类型的重命名也一样,比如,将void(*) (int)
类型重命名为ptr
:
typedef void(*ptr) (int);
//新的类型名必须放在*的右边
所以我们在重命名定义类型之后就可以使用新的类型名:
typedef void(*ptr)(int);
ptr signal(int,ptr);
18.3.1 typedef
与define
定义的区别
#define ptr_t int*;
ptr_t p1,p2;
用define
定义后使用,此时p1
指针变量,p2
是(整型)变量。此时我们可以直接把ptr_t
看作int *
,*
只和p1
结合构成指针变量,不与p2
结合。
typedef int* ptr_t;
ptr_t p3,p4;
用typedef
重定义之后,此时p3
和p4
都是指针变量。
18.4 函数指针数组
- 数组是一个存放相同类型数据的储存空间,如果我们把函数的地址放到数组中,那这个数组就叫做函数指针数组。
int (*ptr1[10]) ();
ptr1
先和[]
结合,说明ptr1
是数组,数组的内容就是int (*)()
类型的函数指针。
19.转移表
- 函数指针数组的用途:转移表。
一般实现:
#include <stdio.h>
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 Div(int x, int y)
{
return x / y;
}
void Menu()
{
printf("********************************\n");
printf("********1.Add 2.Sub********\n");
printf("********3.Mul 3.Div********\n");
printf("******** 0.exit ********\n");
printf("********************************\n");
}
int main()
{
int input = 1;
int ret = 0;
int x = 0;
int y = 0;
do {
Menu();
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 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 Div(int x, int y)
{
return x / y;
}
void Menu()
{
printf("********************************\n");
printf("********1.Add 2.Sub********\n");
printf("********3.Mul 3.Div********\n");
printf("******** 0.exit ********\n");
printf("********************************\n");
}
int main()
{
int input = 1;
int ret = 0;
int x = 0;
int y = 0;
int (*p[5])(int x, int y) = { 0,Add,Sub,Mul,Div };
do {
Menu();
printf("请选择:");
scanf("%d", &input);
if (input <= 4 && input >= 1)
{
printf("请输入两个操作数:");
scanf("%d%d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出程序!\n");
}
else
{
printf("选择错误!\n");
}
} while (input);
return 0;
}
20. 回调函数
- 回调函数就是一个通过函数指针调用的函数
如果把函数指针(地址)作为一个参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用,用于对该事件或条件进行响应。这种机制使得回调函数成为实现事件驱动编程、异步操作、以及模块间解耦等高级功能的关键技术之一。通过使用回调函数,我们可以构建出更加灵活、可维护且可扩展的程序结构。
下面是使用回调函数实现转移表代码:
#include <stdio.h>
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 Div(int x, int y)
{
return x / y;
}
void Menu()
{
printf("********************************\n");
printf("********1.Add 2.Sub********\n");
printf("********3.Mul 3.Div********\n");
printf("******** 0.exit ********\n");
printf("********************************\n");
}
void calc(int (*pf)(int , int ))
{
int ret = 0;
int x = 0;
int y = 0;
printf("请输入两个操作数:");
scanf("%d%d",&x,&y);
ret = pf(x, y);
printf("%d\n",ret);
}
int main()
{
int input = 1;
do {
Menu();
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;
}
21.qsort
的使用
21.1 使用qsort
函数排序整型数据
qsort
是 C 语言标准库<stdlib.h>
中提供的一个快速排序函数。它用于对数组进行排序。- 其函数原型通常如下:
void qsort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *));
base
:指向待排序的数组的第一个元素的指针。
num
:待排序数组的元素的个数。
size
:到排序数组中一个元素的大小(以字节为单位)。
compar
:指向比较函数的指针,该函数用于确定元素之间的顺序。
#include <stdio.h>
#include <stdlib.h>
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,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), int_cmp);
for (i = 0; i < sz; i++)
{
printf("%d ",*(arr + i));
}
return 0;
}
21.2 使用qsort
排序结构体数据
#include <stdio.h>
#include <stdlib.h>
struct Stu
{
char name[20];
int age;
};
int cmp_stu_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
void test()
{
struct Stu s[] = { {"zhangsan",24},{"lisi",25} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_age);
for (int i = 0; i < sz; i++)
{
printf("%s %d\n",s[i].name, s[i].age);
}
}
int main()
{
test();
return 0;
}
21.3qsort
函数的模拟实现
- 使用回调函数,模拟实现
qsort
(采用冒泡的方式)
#include <stdio.h>
// 比较函数,用于比较两个整数的大小
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1) - (*(int*)p2);
}
// 交换函数,用于交换两个内存块的内容
void swap(void* p1, void* p2, int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char temp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = temp;
}
}
// 冒泡排序函数,用于对任意类型的数组进行排序
void bubble_sort(void* base, int sz, int size, int(*cmp)(void*, void*))
{
int i = 0;
int j = 0;
for (i = 0; i < sz; i++)
{
for (j = 0; j < sz - 1 - i; j++)
{
// 如果前一个元素大于后一个元素,则交换它们的位置
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main()
{
int arr[] = { 1,3,5,7,9,2,4,6,8,10 }; // 初始化一个整数数组
int sz = sizeof(arr) / sizeof(arr[0]); // 计算数组的元素个数
// 调用冒泡排序函数对数组进行排序
bubble_sort(arr, sz, sizeof(arr[0]), int_cmp);
// 打印排序后的数组
for (int i = 0; i < sz; i++)
{
printf("%d ", *(arr+i));
}
return 0;
}
22.对比sizeof
和strlen
22.1 sizeof
- 我们知道
sizeof
是单目操作符,它是用来计算所占内存空间大小的,单位是字节,如果操作数是类型的话,那么计算的就是用类型创建的变量所占内存空间大小。sizeof()
括号内中有表达式,表达式不参加计算。所以总的来说sizeof
只关注占用内存空间的大小
例如:
#include <stdio.h>
int main()
{
int a = 10;
printf("%d\n",sizeof(a));
printf("%d\n", sizeof a);//a是变量,所以可以省略。侧面证明了sizeof不是函数,因为函数在调用时肯定会有括号
printf("%d\n", sizeof(int));
return 0;
}
22.2 strlen
strlen
是C语言库函数,功能是求字符串长度,使用时需要包含头文件<string.h>
。- 函数原型如下:
size_t strlen(const char * str)
strlen
统计的是从函数的参数str
中这个地址开始向后,\0
之前的字符串中字符的个数。
strlen
函数会一直向后找\0
字符,直到找到为止,所以可能会存在越界查找的情况。
#include <stdio.h>
int main()
{
char arr1[5] = { 'a','b','c','d','e' };
char arr2[] = "abcde";
printf("%d\n",strlen(arr1));//随机值
printf("%d\n",strlen(arr2));
printf("%zd\n",sizeof(arr1));
printf("%zd\n", sizeof(arr2));
return 0;
}
-
1.使用
strlen(arr1)
计算arr1
的长度。由于arr1
不是一个以空字符结尾的字符串,strlen
会继续读取内存,直到它偶然遇到一个空字符为止。这可能会导致程序读取不属于arr1
的内存区域,这是未定义行为,并且通常会导致程序输出一个随机的值。 -
2.使用
strlen(arr2)
计算arr2
的长度。由于arr2
是以空字符结尾的字符串,strlen
会正确地返回5
,这是字符串中字符的数量(不包括空字符)。 -
3.使用
sizeof(arr1)
获取arr1
的大小。由于arr1
被声明为包含5
个字符的数组,sizeof
会返回5
(在大多数系统上,一个字符占用1个字节)。 -
4.使用
sizeof(arr2)
获取arr2
的大小。arr2
的大小包括所有初始化的字符('a'
到'e'
)以及末尾的空字符'\0'
。因此,sizeof(arr2)
会返回6
。
22.3 sizeof
和strlen
的对比
sizeof | strlen |
---|---|
1.sizeof 是操作符 | 1.strlen 是库函数,使用时需要包含头文件<string.h> |
2.sizeof 计算的是操作数所占内存的大小,单位是字节 | 2.strlen 是求字符串长度的,统计的是\0 之前的字符个数 |
3.不管关注内存中存放的什么数据 | 3.关注内存中是否有\0 ,如果没有就会继续向后找,所以可能会越界 |