C语言指针要点解析

       很多想到C语言,就会认为这个语言最让人头疼的指针部分,所以本篇将对指针的重点疑点知识进行总结,适合遗忘时进行复习观看。

一、指针简单理解

在计算机中,每一个内存单元都有其自己的地址,所以在编写程序需要向内存进行申请或者访问空间时,就需要访问其对应的地址。为了更简单快捷的实现对地址的访问使用,C语言的指针变量由此产生。

int a= 5;
int* pa= &a;

 这里简单创建一个整型变量a,其值为5,然后用一个int*类型的指针变量存储a的地址,从而可以实现对a的地址访问和其他操作,这里的int和*各有含义,*代表这里的pa是指针变量,int代表这个指针变量pa中存储的是int类型的地址。

二、const修饰指针变量

• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。

• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。

具体例子解释下:

 //const放在*前面
 int n = 10;
 int m = 20;
 const int* p = &n;
 *p = 20;//不可以改变p指向的内容
 p = &m;//可以改变指针变量p中的内容(也就是p变量中所存储的地址)
//const放在*后面
 int n = 10;
 int m = 20;
 int const * const p = &n;
 *p = 20;//可以改变p指向的内容
 p = &m;//不可以改变指针变量p中的内容(也就是p中存储的地址)

 三、指针运算

(1)指针+-整数

以数组为例,数组在内存中是一块连续的空间,只要有了数组的首地址就可以依次找出后续的所有元素地址空间。

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

这里是通过*(p+i)来对数组各个元素进行解引用,其中的指针变量p代表的是数组arr的首元素地址,(p+i)就是从数组的首元素地址向后跳过i个int类型大小的空间,即从原位置向后走i个步长,这里的步长就是指针变量对应的原类型大小,int*指针对应的步长是4个字节(int类型大小),char*指针对应的步长是1个字节(char类型大小).......顾名思义,步长就是指针变量+-整数时,向前或者向后所走过的空间大小!

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

以这个自己实现的strlen()函数为例,其p-s的含义,就是用指向‘\0’的指针变量减去指向字符串首元素的指针变量,最后return的结果就是首元素到‘\0’前的元素个数。

(3)指针关系运算
//指针的关系运算
#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]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

这里的指针大小比较,实际上比较的就是指针变量所存储的地址的高低。

四、指针变量的类型分类

(1)常规类型

int*、double* 、char*......

char* str="abcdef";

常规类型中的字符型指针变量,要注意理解将一个字符串给给char*类型的指针,实际上是把该字符串的首字符的地址给了这个指针变量!

(2)void*

void*类型的指针可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 ⾏指针的+-整数和解引⽤的运算。⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。

int main()
{
	int a = 10;
	void* pa = &a;
	*(int*)pa = 5;
	printf("%d", a);//输出结果为5

	return 0;
}

这里只是示范一下void*的大概使用机制,并不是void*非要这么用,在函数参数部分才能体现其显著的效益。

(3)指针数组

顾名思义,指针数组实际上是一种数组的类型,其元素类型均为指针

分析一下指针数组的结构组成,这里的int*是指arr数组中的元素类型,[5]指的是数组的元素容量最大是5个元素。

具体示范使用:通过指针数组实现二维数组

#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[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。但是二维数组的空间是连续的,而我们这里将三个单独的数组通过指针数组拼在一起的操作只是实现了二维数组的效果,其在内核上与真正的二维数组是不同的!

(4)数组指针
int(*p)[5]

解释一下数组指针的构成,这里的(*p)说明p是一个指针变量,注意一定要加括号,[ ]的优先级高于*,不加括号的话就变成了指针数组,然后剩下的int  [5]指的是这个指针变量p指向的是一个类型为int,元素容量个数为5的数组。

但是什么是数组地址呢?我们印象中可能认为arr或者&arr[0]是数组的地址,实则不然,数组名arr和&arr[0]只是代表了数组首元素的地址,不能代表整个数组的地址,&arr才是真正的数组arr的地址,类型为int (* ) [ ],即:

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

int (*pa)[10]= &arr;//得到的是数组arr的地址

有了以上对于数组指针的理解之后,我们就可以对二维数组的机制有更深的理解了:

回想曾经,我们在写函数时,对于一维数组实参int arr[10],我们在函数部分的形参可以直接写和形参一样的形式,即:int arr[ ],其中括号内部可写可不写,从其本质出发,传参时我们写出数组名arr,也就是表示arr数组的首元素地址,其类型为int*,所以在定义函数形参部分可以直接写成int* arr。那么在二维数组函数传参这里同样有类似的情况:

void test(int a[3][5], int r, int c);

//...
int main()
{
    int arr[3][5]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    test(arr,2,3);
    return 0;
}

这里对于二维数组传参,同样可以直接在函数定义形参部分直接将二维数组全称都写上,但是传给函数的实参是arr,arr是一个二维数组的数组名,数组名是首元素地址,但是我们如何理解二维数组的首元素? 

一个形象但与本数组无关的图:

⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5]。就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,也就是int (*) [5],所以形参就可以同样写成这个类型,即:

void test(int (*a)[5], int r, int c);

//...
int main()
{
    int arr[3][5]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    test(arr,2,3);
    return 0;
}
(5)函数指针

事实上,函数也是有它自己的地址的,函数名即表示函数的地址,也可以用 &函数名 来表示函数的地址,既然有地址,那么我们就可以使用函数指针变量来存储函数的地址,并进行使用。

对函数指针的结构组成理解如下:

函数指针的基本使用方法如下:

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;
}
(6)函数指针数组

函数指针数组,当然就是一个数组,其中的元素类型均为函数指针喽!

int (*parr1[3]) ( );

其中parr1 先和 [ ] 结合,说明 parr1是数组,然后数组内的元素是 int (*)( ) 类型的函数指针。

用一个转移表来展示函数指针数组的使用场景:

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

五、qsort函数的实现

先看一下C语言中库函数中的qsort的参数类型:

void qsort(void* base, size_t num, size_t size,int (*compar)(const void*,const void*));

这里的参数分别为要排序数组的首地址,所需排序的元素个数,每个元素的字节大小以及一个返回类型为整型的比较函数。

qsort函数的使用方法不在多说,接下来我们使用掌握的指针知识来实现一下:

//比较函数
//由于要接收不同类型的数据,所以将p1和p2的类型设置为void*
//所以在比较时,需要先对其进行类型转换,在进行解引用,从而正常使用
int int_cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}

//交换函数
//这里同样是需要兼容交换不同类型的数据,所以这里直接将指针转换为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;
	}
}

//类冒泡排序
//依然是针对单个字节所进行操作
//这个(char*)base + j * size就代表从首地址向后移动j个对应类型字节大小
//也就是正常所写的arr[j]
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{
	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 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;
}

如果本文帮助您更好理解了指针的知识,麻烦给个三连吧!

  • 37
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值