指针详解与应用

  注意:本章内容很多,请耐心的看下去

       指针的大名大家可能听说过,也有可能没有听过,不管有没有听过我们一起来看看这一部分的内容,指针的内容很丰富同时有一定的难度,正因为它有难度,指针的重要性也是其他东西不可比拟的。

       在本章我们会了解内存和地址,指针变量和const与assert关键字,指针运算等内容,同时我们还会了解到一维与二维数组的本质,字符指针变量,数组指针变量,函数指针变量等一些指针变量类型,同时还会学习到冒泡排序与回调函数的内容。

1.内存和地址

在讲内存和地址之前,我们想有个⽣活中的案例:
假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩,如果想找到你,就得挨个房⼦去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
⼀楼: 101 102 103. ..
⼆楼: 201 202 203. ...
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
那在计算机中是如何处理的呢?
计算机为了方便管理内存, 把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。

这里大家可能不知道字节是多大,那大家一定知道GB与MB的关系

1GB=1024MB

1KB = 1024byte(字节)

1byte = 8bit(比特)

其中,每个内存单元,相当于⼀个学⽣宿舍,⼀个⼈字节空间⾥⾯能放8个⽐特位,就好⽐同学们
住的⼋⼈间,每个⼈是⼀个⽐特位。每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的⻔牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。
⽣活中我们把⻔牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起
了新的名字叫:指针。
所以我们可以理解为:
内存单元的编号 == 地址 == 指针

2.指针变量

   了解了地址后,我们就来认识指针变量。

1.取地址操作符(&)

在C语⾔中创建变量其实就是向内存申请空间,如图

上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10,其中每个字节都有地址,那我们如何能得到a的地址呢?
这时候就要用到&操作符了,我们来看看下面这段代码
我们发现 &a取出的是a所占4个字节中地址较⼩的字节的地 址。
一个整形变量4个字节,应该有四个地址,为什么只打印一个地址呢?
虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可⾏的。

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

1.指针变量

那知道了怎么取地址,那么怎么储存呢?

这时候指针变量就来了。

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;//取出a的地址并存储到指针变量pa中
	return 0;
}

指针变量也是⼀种变量 ,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
那我们如何理解指针变量呢?
pa左边写的是 int* * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)类型的对象。
同样如果我们要储存一个char类型变量的地址,只需要char *(name)=储存的的值。
2.解引⽤操作符

了解了如何储存地址那我们该如何去用呢?

这时候就需要解引用操作符*

#include <stdio.h>

int main()
{
	int a = 100;
	int* pa = &a;
	*pa = 0;
	return 0;
}
上⾯代码中就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间;所以*pa = 0,这个操作符是把a改成了0.
这里我们就可以知道 *pa就是a变量,而pa则是a变量的地址
有人肯定在想,这⾥如果⽬的就是把a改成0的话,写成 a = 0; 不就完了,为啥⾮要使⽤指针呢?
其实这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活

3.指针变量大小

我们知道每个变量类型大小都不一样,例如int类型4个字节大小,char类型1个字节大小,那指针变量多大呢?

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

3.指针变量类型的意义

1.指针的解引⽤

指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各种各样的指针类型呢?
我们来看看下面这两段代码
#include <stdio.h>
int main()
{
	int n = 0x11223344;
	int* pi = &n;
	*pi = 0;
	return 0;
}

#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* 的指针的解引⽤就能访问四个字节

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个字节。
这就是指针变量的类型差异带来的变化。
结论: 指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

3.void* 指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。
如图
在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告,是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。
这⾥我们可以看到, void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。
那么 void* 类型的指针到底有什么⽤呢?
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果,使得⼀个函数来处理多种类型的数据。

4.const修饰指针

1.const修饰变量

变量是可以修改的,但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。
我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?

2.const修饰指针变量

我们来看看代码

5.指针运算

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

1.指针+- 整数应用

我们在上面知道指针+-整数代表跳过多少字节,而字节数是根据指针类型来跳的,下面我们就来看看实际应用的场景。

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
这个代码就很好利用指针+-整数来访问数组的内容,同时我们要使用指针+-整数 ,前提是数据是要在内存中连续存放的

2.指针-指针

那指针-指针代表什么呢?

我们发现指针-指针代表这两个指针间的元素个数,而使用指针-指针的前提是它们指向的是同一块空间。

3.指针的关系运算

关系运算表示就是比较大小,而指针比较的就是地址

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;
}

函数是在内存中暂时开辟的一块空间,函数走完后,内存会返回给操作系统,此时*p相当于未初始化

2.如何规避野指针

1.指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
#include <stdio.h>
int main()
{
	int num = 10;
	int* p1 = &num;
	int* p2 = NULL;

	return 0;
}
2.⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问,这个就需要自己去判断了。
3.对指针及时置NULL
#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
	int* p = &arr[0];
	for (int i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//下次使⽤的时候,判断p不为NULL的时候再使⽤
	//...
	p = &arr[0];//重新让p获得地址
	if (p != NULL) //判断
	{
		//...
	}
	return 0;
}
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL

7.assert断言

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
#include<assert.h>
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和
出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移
除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。
assert() 的缺点是因为引入了额外的检查,增加了程序的运⾏时间。

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;
}

同时我们也可以利用指针-指针的思路

//指针-指针
#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;
}

2.传值调⽤和传址调⽤

我们学习了指针的知识,可能有人会觉得没什么用,那我们来做一道题

eg:写⼀个函数,交换两个整型变量的值:
有人看到这道题后可能会写出下面这段代码:
#include <stdio.h>
void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
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);
	return 0;
}

我们运行一下

我们发现为什么没有交换?

我们发现在main函数内部,创建了a和b,Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址和y的地址与a和b不同,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* px, int* py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
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);
	return 0;
}

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤

9.数组名的实质

我们知道利用指针可以来访问数组,那么数组名的地址是什么呢?

#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[0] 的⽅式拿到了数组第⼀个元素的地址。
我们发现数组名和数组⾸元素的地址打印出的结果⼀模⼀样,数组名就 是数组⾸元素(第⼀个元素)的地址。
接下来我们看看下面这个代码
#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 操作是跳过整个数组的。

10.使⽤指针访问数组

利用指针访问数组,我们在指针+-整数就已经了解过了,我们这里再深入一下

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);
		//scanf("%d", arr+i);//也可以这样写
	}
	//输出
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}
我们知道数组名arr是数组⾸元素的地址,可以赋值给p,其实数组名arr和p在这⾥是等价的。那我们可以使⽤arr[i]可以访问数组的元素,同时也可以利用p[i]访问数组
#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);
		//scanf("%d", arr+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),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。

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));//计算⼀个指针变量的⼤⼩
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	test(arr);
	return 0;
}

⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

12.冒泡排序

我们了解了指针的内容后,我们就可以认识一下冒泡排序,排序大家一定不陌生,就是把无序的数,排列有序的数,当然排序的方法很多,但是这里只会详细解释冒泡排序。

首先我们要先了解冒泡排序的核⼼思想:两两相邻的元素进⾏⽐较

每一趟下来会把最大的数放在后面,下面我们来实现一下。

对数据排序,我们首先就要知道数据的个数

int main()
{
	int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	return 0;
}

之后我们可以把排序的过程写成一个函数在里面实现,同时最后打印出来看看

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);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

接下来我们实现排序

我们知道冒泡排序的核⼼思想是两两相邻的元素进⾏⽐较,每一趟下来会把最大的数放在后面,我们先来实现一趟的程序

for (j = 0; j < sz-1 ; j++)
{
	if (arr[j] > arr[j + 1])
	{
		
		int tmp = arr[j];
		arr[j] = arr[j + 1];
		arr[j + 1] = tmp;
	}
}

之后我们可以用循环的嵌套来实现整个排序

	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;
			}
		}
		
	}

由于每趟下来,大的就在后面,后面就会有序,而前面还是无序的,因此 j < sz - 1 - i。

我们发现不论数据是否无序都会循环很多遍,有没有方法当数据有序是,停止循环?

其实很简单,多设置一个变量来判断

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;
	}
}

最后我们把代码整合一下

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

13.⼆级指针

学习了一级指针,我们来认识一下二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?
答案是是⼆级指针。
对于二级指针的运算和一级指针的运算差不多
int main()
{
	int a = 10;
	int* p = &a;
	int** pa = &p;
	printf("%p\n",p);
	printf("%p\n",*pa);
	printf("%d\n",**pa);
}

*pa 通过对pa中的地址进⾏解引⽤,这样找到的是 *pa 其实访问的就是 p.

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

14.指针数组

是存放指针的数组,不是指针

同理,指针数组存储的是指针

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数组中
	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]);
		}
		printf("\n");
	}
	return 0;
}

parr中存放的数组是arr1,arr2,arr3,的首元素地址,也就是存放着这三个数组的指针

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

16.字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char* 
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

同时字符指针还有一种使用方法

int main()
{
	const char* pstr = "hello bit.";
	printf("%s\n", pstr);
	return 0;
}
其中代码 const char* pstr = "hello bit."; 特别容易让人以为是把字符串 hello bit 放到字符指针 pstr ⾥了,但是本质是把字符串 hello bit. ⾸字符的地址放到了pstr中。
我们来看看下面一个例题
#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";
	const char* str4 = "hello bit.";
	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");

	return 0;
}
这⾥str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

17.数组指针变量

1.数组指针和指针数组区别

 数组指针变量就是一种指针变量。

 指针数组是存储指针的数组

我们已经熟悉:
整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量是:存放的应该是数组的地址,能够指向数组的指针变量。
语法:
int  (*p) [ 10 ];
|       |        |
|       |        |
|       |       p 指向数组的元素个数
|       p 是数组指针变量名
p 指向的数组的元素类型
p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。
这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
由于数组指针和指针数组容易弄混,大家一定要注意。

2.数组指针变量初始化

数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的 &数组名 int arr[ 10 ] = { 0 };
&arr; // 得到的就是数组的地址
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int (*p)[ 10 ] = &arr;
&arr p 的类型是完全⼀致的。

18.⼆维数组传参的本质

有了数组指针的理解,我们就能够认识⼆维数组传参的本质了。
之前我们有⼀个⼆维数组的需要传参给⼀个函数的时候,给的是这样的:
#include <stdio.h>
void test(int a[3][5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", a[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] 。那就意味着⼆维数组传参本质上也是 传递了地址,传递的是第⼀⾏这个⼀维数组的地址,所以参数我们也可以写成:
#include <stdio.h>
void test(int(*p)[5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", a[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;
}
⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式

19.函数指针变量

1.函数指针变量的创建

我们认识了数组指针,字符指针,同样我们来认识一个指针,叫函数指针。

根据我们所学的知识,我们可以推出函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。

那有人会有一个问题,函数也有地址吗?我们来看看下面这段代码

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

其实函数也是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的⽅式获得函数的地址。对于函数,&函数名和函数名都是函数的地址,不存在说什么首元素地址

而函数指针的用处就是存放函数的地址,接下来我们看看函数指针的基本形式

int (*pf3) ( int x, int y)
|       | 
|       |               |
|       |              pf3 指向函数的参数类型和个数的交代
|        函数指针变量名
pf3指向函数的返回类型
int (*) ( int x, int y) //pf3 函数指针变量的类型
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写上或者省略都是可以的

2.函数指针变量的使⽤

接下来我们来通过函数指针来调用函数
#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;
}

3.例题

接下来我们来看看下面这两段代码

(*(void (*)())0)();

当我们拿到这个代码的时候怎么分析呢?先从内到外,我们发现void(* )()是函数指针类型,只不过指向的是空,(void(* )())是强制类型转换,接下来我们就知道,调用0地址处的函数,调用的函数,参数是无参,返回类型是void

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

4.typedef关键字

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。

例如, unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:
typedef unsigned int uint;

//unsigned int 重命名为uit

当然我们也可以给指针类型重命名

⽐如,将 int* 重命名为 ptr_t ,这样写:

 typedef int*   ptr_t;
但是对于数组指针和函数指针稍微有点区别
⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边

同样函数指针也是一样的

typedef void(*pfun_t)(int);//新的类型名必须在*的右边

命名知道了怎么使用呢?其实很简单,我们可以直接把pfun_t当做一种变量类型直接使用就可以

int Add(int a)
{
	return a;
}
int main()
{
	int ret= 0;
	scanf("%d", &ret);
	pfun_t p = &Add;
	printf("%d", p(ret));
	return 0;
}

20.函数指针数组

根据名字我们就知道,函数指针数组就是储存函数指针的数组

1.语法:

int  (*parr1[3]) ( );

储存int (*)() 类型的函数指针。

#include<stdio.h>
int Add(int a, int b)
{
	return a + b;
}
int Sud(int a, int b)
{
	return a - b;
}
int main()
{
	int (*a)(int, int)=Add;
	int (*b)(int, int)=Sud;
	int (*p[2])(int, int) = { Add,Sud };
	return 0;
}

2.转移表

我们知道了函数指针数组后,我们就可以来实现转移表,那什么是转移表呢?我们可以理解为是一个简易计算器,接下来我们就实现一下

#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;
	int(*p[5])(int x, int y) = { 0, 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 <= 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;
}

这样一个简易的计算机就实现完成了。

21.回调函数

回调函数就是⼀个通过函数指针调⽤的函数。

如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。

大家这里可能会有一些不明白的地方,没关系,下面有关qsort函数我会带大家去理解。

22.qsort函数

1.使用方法与介绍

qsort函数的作用是对数据排序,这个数据是void类型,也就是说可以排序任何一种类型

对于qsort我们首先来看看它的参数

在这里我们只讨论qsort排序整型

#include <stdio.h>
//qosrt函数的使⽤者得实现⼀个⽐较函数
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;
	int(*cmp)(int, int) = int_cmp;
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), cmp);
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

在这里我们就可以发现,qsort中cmp为函数指针作为参数,而cmp则调用了int_cmp函数,而int_cmp函数就是回调函数。

2.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;
	my_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;
}

接下来我们就开始实现qrsrt函数

实现函数我们首先了解函数参数,qsort函数的参数我们已经知道了

void my_qsort(void *base, int count , int size, int(*cmp )(void *, void *))

我们是利用冒泡排序完成的,冒泡排序在上面我们已经了解过两两相邻的元素进⾏⽐较

void bubble(void* base, int count, int size, int(*cmp)(void*e1, void*e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; j++)
		{
			;
		}
	}
}

下面就是最重要的部分交换了

有人可能会这样写

void bubble(void* base, int count, int size, int(*cmp)(void*e1, void*e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; j++)
		{
			if(base[j]>base[j+1])
                 //交换
		}
	}
}

这样是不对的,我们在这里虽然是用整形数据测试,如果是结构体类型的话,就不能直接比较大小,那我们怎么比较呢,这时候第四个参数就有用处了,cmp就是来比较的

void bubble(void* base, int count, int size, int(*cmp)(void*e1, void*e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; j++)
		{
			if(cmp()>0)
             //交换
		}
	}
}

那cmp里的参数是什么呢?是我们要比较两个数的地址

那怎么得到这两个数的地址呢,有人可能会这样写base+j,但是base是void指针类型不能这样加减整数,那我们加上一个强制类型转换呢?(int*)base+j,这样是不对的,我们在这里虽然是用整形数据测试,如果是结构体类型的话,就不能直接这样。

那如何做呢?这个方法很巧妙我们把base强制转换成char*类型,之后加上j*size(一个元素大小),我们就可以得到了

void bubble(void* base, int count, int size, int(*cmp)(void*e1, void*e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; j++)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				//交换
			}
		}
	}
}

判断条件知道了,接下来就是交换了

对于交换,我们可以写个函数来解决,而这个函数我们需要把size传进去,因为交换元素是char*类型,每一次交换都只交换一个字节大小,因此要传入元素大小保证数据交换正确。

void _swap(void* p1, void* p2, int size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}
void bubble(void* base, int count, int size, int(*cmp)(void*e1, void*e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; j++)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}

交换完成后,我们整个程序的模拟实现就结束了。

#include<stdio.h>
void _swap(void* p1, void* p2, int size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}
void bubble(void* base, int count, int size, int(*cmp)(void*e1, void*e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < count - 1; i++)
	{
		for (j = 0; j < count - i - 1; 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 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;
	bubble(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;
}

23.sizeof和strlen的对⽐

1.sizeof

在学习操作符的时候,我们学习了 sizeof sizeof 计算变量所占内存内存空间⼤⼩的,单位是
字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的⼤⼩。
sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。
int main()
{
	int a = 10;
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(int));
	return 0;
}

​​​​​​

2.strlen

strlen 是C语⾔库函数,功能是求字符串⻓度。

size_t strlen ( const char * str );
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。
int main()
{
	char arr1[3] = { 'a', 'b', 'c' };
	char arr2[] = "abc";
	printf("%d\n", strlen(arr1));
	printf("%d\n", strlen(arr2));

	printf("%d\n", sizeof(arr1));
	printf("%d\n", sizeof(arr1));
	return 0;
}

这里第一个结果不一定是35,因为arr1没有\0,因此strlen越界访问,直到\0停下计算

3.sizeof 和 strlen的对⽐

sizeof
1. sizeof是操作符
2. sizeof计算操作数所占内存的⼤⼩, 单位是字节
3. 不关注内存中存放什么数据
strlen
1. strlen是库函数,使⽤需要包含头⽂件 string.h
2. srtlen是求字符串⻓度的,统计的是 \0 之前字符的隔个数
3. 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可 能会越界

4.例题

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(a + 0));
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a + 1));
	printf("%d\n", sizeof(a[1]));
	printf("%d\n", sizeof(&a));
	printf("%d\n", sizeof(*&a));
	printf("%d\n", sizeof(&a + 1));
	printf("%d\n", sizeof(&a[0]));
	printf("%d\n", sizeof(&a[0] + 1));
	return 0;
}
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr + 0));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr[1]));
	printf("%d\n", sizeof(&arr));
	printf("%d\n", sizeof(&arr + 1));
	printf("%d\n", sizeof(&arr[0] + 1));
	return 0;
}

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", strlen(arr));
	printf("%d\n", strlen(arr + 0));
	printf("%d\n", strlen(*arr));
	printf("%d\n", strlen(arr[1]));
	printf("%d\n", strlen(&arr));
	printf("%d\n", strlen(&arr + 1));
	printf("%d\n", strlen(&arr[0] + 1));
	return 0;
}

int main()
{
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr + 0));
	printf("%d\n", sizeof(*arr));
	printf("%d\n", sizeof(arr[1]));
	printf("%d\n", sizeof(&arr));
	printf("%d\n", sizeof(&arr + 1));
	printf("%d\n", sizeof(&arr[0] + 1));
	return 0;
}

int main()
{
	char arr[] = "abcdef";
	printf("%d\n", strlen(arr));
	printf("%d\n", strlen(arr + 0));
	printf("%d\n", strlen(*arr));
	printf("%d\n", strlen(arr[1]));
	printf("%d\n", strlen(&arr));
	printf("%d\n", strlen(&arr + 1));
	printf("%d\n", strlen(&arr[0] + 1));
	return 0;
}

int main()
{
	char* p = "abcdef";
	printf("%d\n", sizeof(p));
	printf("%d\n", sizeof(p + 1));
	printf("%d\n", sizeof(*p));
	printf("%d\n", sizeof(p[0]));
	printf("%d\n", sizeof(&p));
	printf("%d\n", sizeof(&p + 1));
	printf("%d\n", sizeof(&p[0] + 1));
	return 0;
}

int main()
{
	char* p = "abcdef";
	printf("%d\n", strlen(p));
	printf("%d\n", strlen(p + 1));
	printf("%d\n", strlen(*p));
	printf("%d\n", strlen(p[0]));
	printf("%d\n", strlen(&p));
	printf("%d\n", strlen(&p + 1));
	printf("%d\n", strlen(&p[0] + 1));
	return 0;
}

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(a[0][0]));
	printf("%d\n", sizeof(a[0]));
	printf("%d\n", sizeof(a[0] + 1));
	printf("%d\n", sizeof(*(a[0] + 1)));
	printf("%d\n", sizeof(a + 1));
	printf("%d\n", sizeof(*(a + 1)));
	printf("%d\n", sizeof(&a[0] + 1));
	printf("%d\n", sizeof(*(&a[0] + 1)));
	printf("%d\n", sizeof(*a));
	printf("%d\n", sizeof(a[3]));
	return 0;
}

24.结语

我不知道有多少人能看到这里,希望不是直接翻到这里,不过到此指针的内容就结束了,内容很多,而且有点东西去消化的时间还很长,但是大家能看到这,非常感谢,最后,一起加油!!!

​​​​​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值