指针的特点与细述,含抽象指针详解及qsort使用

 
本人能力有限,难免有叙述错误或者不详细之处!希望读者在阅读时可以反馈一下错误以及不够好的地方!感激不尽!

目录

指针是什么?

创建一个指针

指针变量类型是什么?

指针运算

二级指针

其他类型的指针

字符指针

数组指针

 数组名和&数组名:

指针数组

数组传参和指针传参

一维数组传参

二维数组传参

函数指针

抽象函数指针细解

再来一个抽象的

qsort的使用


指针在C语言中是非常重要的概念,相应的,它也在许多人的C语言学习过程中成为了最大的守门员之一,其抽象的概念以及格式经常让人难以理解,不过只要掌握其本质以及其他情况下的样子其实也就那么回事,那么我们将从其本质出发一点点的去了解这个玩意儿。

指针是什么?

我们把内存单元的编号称为地址,地址也叫指针,指针其实就是地址,地址就是编号,指针就是内存单元的编号。

这样子讲其实比较抽象,我们举例来理解。

我们都知道内存是存放数据的地方,编译器会根据我们创建变量的时候在内存里开辟一段空间用于存放数据,至于开辟的空间大小是多少,每段空间存放什么类型的数据当然是由程序员说的算。

假如我们创建了一个数组,元素类型是int,那么在内存的空间中,它的存储是这样的

我们可以看到,整个内存中并不是上来就在内存的开头或者结尾处为变量开辟空间,就好像一帮人住进了一栋大楼,但你根本不知道他们到底住在那一层。编译器也是一样,这个时候地址就像门牌号一样,可以帮助你找到这些人,这就是地址


(题外话) 经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

在32位的机器上,假设有32根地址线,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址

那么:指针的大小在32位平台是4个字节,在64位平台是8个字节


是我们总不能在编译的时候一个个的去找这个变量的地址吧,所以我们就可以用取地址操作符&来取出某个变量的地址。

可是你取出来的地址该如何存放呢?这个时候,指针的本质就非常明显了。

指针,本质上是存放地址的一个变量,是一个保存地址的容器!而地址是唯一标示一块地址空间

我们口头上称为指针,但其真名为指针变量。存放在指针中的值都被当成地址处理

注意!口语所说的指针,通常指的是指针变量,用于存放指针的容器。

创建一个指针

#include <stdio.h>
int main()
{
int a = 10;    //在内存中开辟一块空间
int *p = &a;    //这里我们对变量a,取出它的地址,可以使用&操作符。

//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。
return 0;
}

这段代码看上去还是很简单的,我们创建了一个指针,解引用这个指针,把a的地址塞了进去,听上去好像这就是全部了,这前面的int是不是有些多余?

其实它非常重要,它是指针变量的类型

接下来就是指针变量的类型的问题

指针变量类型是什么?

 指针变量类型:创建一个指针时,需要指定其类型,也就是其地址指向的变量类型是什么。

比如我想要存放一个int类型变量的地址,那么我就要在创建这个指针的时候也指定这个指针是int类型的。

那我创建的时候如果不是对应类型的时候会发生什么?它们到底有啥区别?


在X86即32位的条件下创建的指针变量所占的字节都是4个字节,不论是int也好,char也好都是4个,指针变量的类型则决定了它们在解引用操作的时候到底访问几个字节。

int*类型的指针变量访问4个字节,char*类型的变量访问1个字节。


 我们将一个变量的类型按照如下方式打印

 原因如下:

int main()
{
	int n = 10;
	char* pc = (char*)&n;//将n的地址强制类型转换为字符指针并放入pc中
	int* pi = &n;//将n的地址存放入整型类型的指针
	printf("%p\n", &n);//将变量n的地址打印出来
	printf("%p\n", pc);//打印pc指针所指向的地址
	printf("%p\n", pc + 1);//pc指针加一,因为是字符类型指针,向后访问一个字节
	printf("%p\n", pi);//打印pi指针所指向的地址
	printf("%p\n", pi + 1);//pi指针加一,因为是整型类型指针,向后访问四个字节
	return 0;
}


总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。

当然,以上只是当指针加减整数的时候的情况,相应的,当解引用一个指针的时候其变量类型也会对其访问的字节数量有影响。

int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	
	*pc = 0; 
	*pi = 0; 


	return 0;
}

我们创建一个方便我们在调试时于内存中观察的数,在解引用的时候为其赋值为0,其情况如下:

我们查询pc的地址观察其内存状态:

 我们查询pi的地址观察其内存状态:

 我们可以发现,其解引用的时候访问的字节数量也是和变量类型相等的,char*类型访问并改变了1个字节,int*类型则访问并改变了4个字节。

总而言之:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

指针运算

 *vp ++ = 0(加减整数)

先为*vp赋值为0,然后再自增1


指针-指针的绝对值得到的是指针和指针之间的元素个数

比如&arr【9】- &arr【0】= 9

&arr【0】-&arr【9】= -9

注意!不是所有的指针都能加减!指向同一空间的两个指针才能相减

指针+指针是没有意义的,好比日期+日期,它的本质是地址。

指针可以向后越界的地址进行比较,但是不能向前比较

二级指针

创建:int ** ppa = &pa;

二级指针是用来存放一级指针变量的地址!而不是地址的地址!

int main()
{
	int a = 10;
	int* pa = &a;//pa是一个一级指针变量
	int** ppa = &pa;//ppa则是一个二级指针变量

}

想通过ppa访问到a需要解引用两次

也就是**ppa =20;才可以把a的值修改

int *pa,*表明pa是一个指针,而int则表示这个指针所指向的变量类型是int类型的

同样,int **ppa,第一个*表示这个ppa是一个指针,而前面的int*则表明这个指针指向的玩意儿是一个int*类型的指针变量

其他类型的指针

接下来则是介绍三种比较抽象的指针,由于它们的创建形式以及变化较多经常叫人摸不着头脑,我们会借由一些例子更好的去理解它们。
1. 字符指针
2. 数组指针
3. 指针数组
4. 函数指针

字符指针

 字符指针的创建:

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

 另一种创建方法:

int main()
{
const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}

其实这个时候有一个疑问,我们存入的字符串究竟把啥存进去了?是整个字符串的地址吗?

我们看看这段代码

const cahr* p1="abcdefg";
const cahr* p2="abcdefg";

char arr1 [] = "abcdefg";
char arr2 [] = "abcdefg";

请问上面的指针与指针比较,数组与数组比较是否相等?

答案:p1 = p2 ,arr 1 != arr 2

解释:

因为指针只取走当前字符串第一个字符的地址,而且是由const修饰的,整个字符串存储与只读区域之中所以p1p2存放的都是指向第一个字符a的地址,所以相等

而数组则是在初始化的时候开辟了两个空间,哪怕内容相等也指向的不是一样的东西,数组名指向的是首地址,比较起来是不相等的。但是比较内容物是相等的

总结:字符串存放入字符指针时,存放的其实是首字符的地址,这一点和数组有些相像。

数组指针

 我们知道其实数组名就是其首地址,但是有些情况下我们还是需要数组的地址,要存放数组的地址我们就需要一个指针变量,这个就是数组指针

 数组指针的创建:

int main()
{
	int(*p)[10];

	return 0;
}

翻译为:名称为p的指针变量指向了一个类型是整型且大小容量为10的一个数组。

解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。

这里要注意:[] 的优先级要高于 * 号的,所以必须加上()来保证p先和*结合

这个数组指针到底是什么?有什么用呢?在了解之前,我们可以先了解一下&数组名的概念

 数组名和&数组名:

我们知道数组名其实是首元素地址,那么当我们取数组名的地址的时候会产生什么不同吗,它有没有什么其他的意义?

我们先看看这两的地址是否是一样的:

 看上去他俩都是指向同一个地址,但是其实是有一定区别的,我们运行以下代码来看看它们的区别。

 在各自+1之后,其地址却不一样了。

解释:

我们知道指针变量决定了+-一个整数的时候其跨度的大小,在这我们发现&arr+1跨过了40个字节大小的长度,那么我们可以得出结论:&arr的本质其实是一个数组指针,也就是int (*) [10]

总结:arr指的是数组首元素的地址,而&arr指的是整个数组的地址,同理数组指针指向的是整个数组的地址

指针数组

这玩意容易和上面的数组指针弄混,不过其实还是很容易理解的,指针数组,顾名思义,专门存放指针用的数组。

指针数组的创建:

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

这两个玩意其实还是比较容易弄混的,我们来尝试辨识一下以下的几个指针,看看它们是什么

int  arr[5]         //整型数组
int* parr [10]      //存放整型指针变量的数组
int  (*parr2)[10]   //指向一个容量为10的整型数组的数组指针
int  (*parr3[10])[5]//指向一个容量为5的存放数组指针的数组

数组传参和指针传参

一维数组传参

了解完上述的数组和一些指针之后,难免会涉及到使用的问题,也就是传参。

我们创建俩个数组,一个是整型数组,另一个是整型指针数组

int main()
{

    int arr[10] = { 0 };
    int* arr2[20] = { 0 };

    test(arr);
    test2(arr2);

}

 我们用不同样式的形参来尝试传参,分析其是否可行。

void test(int arr[])//一维数组接收一维数组,【】内元素个数没有要求,正确
{}
void test(int arr[10])//一维数组接收一维数组,【】内元素个数于实参相同,正确
{}
void test(int* arr)//整型指针变量接收首元素地址,正确
{}
void test2(int* arr[20])//指针数组接收指针数组,正确
{}
void test2(int** arr)//指针数组的首元素地址,那就是指向一个指针变量的地址
也就是二级指针,用二级指针接收,正确
{}

二维数组传参

我们创建一个二维数组

int main()
{
int arr[3][5] = {0};
test(arr);
}

我们用不同样式的形参来尝试传参,分析其是否可行。

void test(int arr[3][5])//二维数组传参二维数组接收,正确
{}
void test(int arr[][])//形参二维数组省略了行列的元素个数,无法传参,错误
{}
void test(int arr[][5])//形参二维数组省略了行的个数,列未省略,依然可以传参,正确
{}
void test(int *arr)//二维数组在传参时,传递的是第一行的一维数组的地址
整型指针变量无法接收,错误
{}
void test(int* arr[5])//这是一个容量为5的指针数组,无法接收一维数组地址,错误
{}
void test(int (*arr)[5])//这是一个数组指针,可以正确的对应上实参的一维数组地址,大小也
相等,可以正确传参,正确
{}
void test(int **arr)//二维指针无法接收一维数组的地址,错误
{}

总结:

二维数组在传递参数时,传递的不是首元素地址,而是第一行整个一维数组的地址,也就是一个数组指针。

二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
这样才方便运算。

函数指针

也是一种指针,是指向函数的指针

相应的我们应该了解函数名究竟是什么,如同数组名一样

我们用地址的方式打印函数名发现其获得的也是一个地址,这说明函数名指向了函数所在的地址

#include <stdio.h>
void test()
{
    printf("hehe\n");
}

int main()
{
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

 怎么使用函数指针?比如我希望将一个简单的加法函数的地址放到函数指针里头并且进行调用

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Add(int x, int y)
{
    return x + y;
}

int main()
{
    int x = 0;
    int y = 0;
    int ret = 0;
    scanf("%d %d", &x, &y);

    int (*pf)(int, int) = Add;//将Add函数的函数名存放入函数指针中

    ret =  (*pf)(x, y);//调用函数指针,调用时为其添加好所需实参

    printf("%d", ret);

    return 0;

}

解释:

int (*pf)(int, int) = Add; 这条代码就是将Add函数的地址存放到了函数指针里,其中(*pf)表示这个pf是一个指针变量,pf后面的小括号表示这存放的是一个函数地址,括号里则是存放的函数所需要对应的参数,int则表示这个函数的返回值类型是int。

为了更好的去理解函数指针,我们尝试理解一下它的抽象版本,在《C陷阱与缺陷》一书中,有如下一条代码。

抽象函数指针细解

int main ()
{
    (*(void (*)())0)();
    
    return 0;
}

解释:

这段代码看着很吓人,但其实还就是那个大肠包小肠,看作整体即可。

首先突破点在于这个0前面的这个括号,我们知道强制类型转换的格式是(类型)值。而刚才接触到的函数指针也是一种类型!只不过它稍微特殊一点,换了一只马甲!正常来讲应该长这样:

int(*)()

红括号就是强制类型转换的括号了,橙色的括号就表示这是个函数指针,但是里面不需要传参,int表示该指针指向的函数返回值是个int。

那么,当他套上个马甲时:void(*)()这样一看,这玩意可就清楚多了,是一个不需要传参,返回值类型为void的函数指针

那么,在其后头添个0?

void(*)()0

现在这段代码的意思就很清楚了,将0强制类型转换为一个不需要传参,返回值类型为void的地址

那外头的那个大肠呢?很简单!

我们在用指针调用Add函数的时候是这么写的:

ret = *pfx,y

我们不需要返回值,也不需要传参,这个指针变量是一个怪东西,照着整体颜色一一对应后如下:

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

这下,整个代码的真实形态也就非常明显了

总结以下就是:

1.将0强制类型转化为:无参,返回值类型为void的地址

2.调用0作为地址处的函数

再来一个抽象的

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

 解释:

还就那个俄罗斯套娃,只不过整个玩意乍一看有点难搞,不过我们依旧可以拆开分析:

首先,看向整个代码

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

无视蓝色部分得到:

void(*)(int)

这玩意就很眼熟,不就是个函数指针,传参类型是int返回值为void吗?

那么接下来就是里面的东西

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

看上去还是挺复杂啊,没事,无视掉绿色部分

无视后:signal()

这玩意不就是个函数吗?那么,里面的东西不就是参数了吗?

没错!逗号前传参类型是int,逗号后是void(*)(int),这玩意不就是个函数指针吗

那么现在我们知道一个函数需要传参,名称,还有返回值类型,前两个我们都找到啦,返回值类型呢?

我们看回去前面的紫色部分代码,这不就是整个函数的返回值吗?只不过返回的是一个函数指针而已!


那么,这条代码想表达的意思就非常简单明了了!我们先拿掉一部分来解释:

void(*signal(.....) )(int)

声明了一个名为signal的函数,它的返回值类型是一个函数指针,其中这个函数指针指向的是无返回值且只需要一个实参类型为int类型的函数,

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

而这个signal函数所需要的实参则是有两个:一个int类型的整数和一个无返回值,需要传递一个int类型的函数指针。

qsort的使用

 qsort是什么?

qsort其实就是quicksort的意思,其名称为快速排序,简称快排,如其名,我们可以借用这个函数来帮助我们对各种数据类型进行升序或者降序的排列。

我们在msdn上面可以找到qsort的使用方法,其所需的参数和返回值。

qsort所需变量的各类意义:

void*base:你要排序的数据的起始位置

size_t num:待排序的数据元素的个数

size_t width:待排序的数据元素的大小,单位是字节

int (cdecl*compare)(const void *eleml,const void *elem2)这个之后解释。

光说不练假把式,那么我们尝试使用快排来排序一段数组:

{8,7,6,5,4,3,2,1,0}

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

这是一段主代码,我们已经为qsort准备好了base num width 接下来差一个函数,我们在上面可以看到这个函数相当于操作方法,因为qsort可以为任意类型的数据进行排序,所以对于这个函数的创建和数据的接收就要使用void*类型的指针。

整个要求就是需要一个返回值类型为int,传参为两个void*类型的地址

在这之前,我们则需要知道void*是什么。

void*是什么?

在C语言里,传递一个数值的地址到指针变量里需要对应的数据类型,char*对应char类型的数据,一一对应,所以void*就是那个包容万物的指针变量,它来者不拒,存入任何类型的数据地址对它来讲都无所谓。但力量总是有代价的,它不能被解引用,也不能实现其他指针变量所可以进行的运算。

那它除了会包容不就是一个包袱吗?没法解引用我存来干什么?

它是不可以解引用,其他类型的可以呀!

我们直接强制类型转换再解引用!

*(int*)void*

这样void*里的数据就可以正常的被解引用了!

最后一块拼图compare函数:

那么传参问题被解决了就是compare函数的问题了。

compare函数在库函数里其实有一个要求:

值1值2时,返回>0的值

我们需要一个正序的数组,那么compare函数就可以这么写:

int compare(void* e1, void* e2)
{
	return(*(int*)e1 - *(int*)e2);
}

 总体代码如下:

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

int compare(void* e1, void* e2)
{
	return(*(int*)e1 - *(int*)e2);
}


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


	qsort(arr, sz, sizeof(arr[0]), compare);

	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	return 0;
}    

这样,我们就成功的使用快排排列了一个数组。

8道指针有关的题目

 一些需要更深入理解指针才比较好作对的题目,部分的题目都有注解,部分的题目有图解,可以尝试挑战以下,如果有看不懂的的地方可以随时留言。

1.


 

int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int* ptr = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(ptr - 1));
	return 0;
}
//程序的结果是什么?2,5

2.


struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;
假设p 的值为0x100000。 如下表表达式的值分别为多少?
已知,结构体Test类型的变量大小是20个字节
int main()
{
	printf("%p\n", p + 0x1);//相当于+1,一个结构体指针+1,跳过一个结构体的大小
	//0x10000 + 20 前面是16进制,这个20是10进制,其最终结果是0x100014
	printf("%p\n", (unsigned long)p + 0x1);
	//p的值被抓换成了无符号的整型,一个整型+上一个整型直接相加,此时p已经不再是
	//指针,所以单独+1即可,答案就是0x100001
	printf("%p\n", (unsigned int*)p + 0x1);
	//这个时候p被转化成了一个无符号整型指针,但它的本质依然是指针,+1那就跨越一个无符号整型大小也就是4个
	//字节答案就是0x100004
	return 0;
}

3.

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);
	int* ptr2 = (int*)((int)a + 1);
	printf("%x,%x", ptr1[-1], *ptr2);
	return 0;
}//4
 //第二个则是先将首元素的地址强制类型转换成了整型,这个时候这个地址+1的话就跳过一个字节,为什么?因为在原本的
 // 情况下,这个指针需要跳过一个类型的字节大小才能访问下一个数组的元素,这个时候相当于只让指针走了一个字节
 // 而1 2 3 4在内存中以16进制存储的时候是小端字节序存放,内存此时情况是:
 // 01 00 00 00 | 02 00 00 00 00 | 03 00 00 00 | 04 00 00 00 |
 // 原本ptr2指向01,向前一个字节,指向了00,这个时候又需要它读取四个字节的数据,小端字节序存放则需要倒置
 // 所以这个时候取出来的是02 00 00 00 
 //

 4.


#include <stdio.h>
int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };
	int* p;
	p = a[0];
	printf("%d", p[0]);
	return 0;
}
// 1
// 这里头有个陷阱,初始化的时候并不是花括号进行初始化,而是一个逗号表达式,这个时候整个数组的存放情况是这样的:
// 1 3
// 5 0
// 0 0
//a【0】取出了第一行数组的地址,p【0】就是取出了0 0 的数据

5.


int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
	return 0;
}//由于p这个数组指针的大小与a不相同,对于p来讲,它所访问到的数组元素的规律会有所不同:见图
 //由图可见,p-a,两个指针相减得到的是两个指针之间相隔的元素个数,但是其有正负之分,%d可以正常的打印出
 //-4,而%p则是以地址的样式打印,-4在内存中的存放是用补码的,即原码取反后+1所得,以有符号整数打印的时候才
 //换回原码,但现在以地址形式打印直接就把内存里的补码当地址打印了,得到的就是FF FF FF FF

 

 

 6.


int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}//10,5

 

 

 7.


int main()
{
	char* a[] = { "work","at","alibaba" };
	char** pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}//at

 

 8.


 

int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };
	char*** cpp = cp;
	printf("%s\n", **++cpp);//POINT
	printf("%s\n", *-- * ++cpp + 3);//ER
	printf("%s\n", *cpp[-2] + 3);//ST
	printf("%s\n", cpp[-1][-1] + 1);//EW
	return 0;
}

 

 

 

 

感谢阅读!希望能对你有些帮助!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值