C语言学习中的小收获(指针)

目录

1、初识指针:

关于指针:

关于字符指针:char*

关于const:

1. 常量指针 形如:const int*p

2. 指针常量  形如:int*const p

3、常量指针常量 形如:const char* coonst p 

关于内存分配:

2、指针类型

有关野指针:

3、指针运算

1、指针-指针得到中间的元素个数

关于字符串的拷贝:

关于assert()

2、指针+-整数:就是指向+-整数运算后的地址,一般也可用来遍历数组元素或对其赋值等

3、指针的关系运算:

4、指针与数组

5、字符串与指针

关于自符串与指针:

关于字符串处理库函数

6、二级指针

 7、指针数组(存放指针的数组,该数组每个元素都是指针)

关于指针数组:

关于指针数组得初始化:

 8、数组指针(指向数组的指针) 形如:int(*p)[10]  

关于&数组名 和 数组名

数组指针的运用:

9、数组传参、指针传参

关于二维数组传参

关于指针传参:

关于一级指针传参:

二级指针传参:

 10、函数指针

函数指针:指向函数的指针

关于函数指针数组:

关于指向函数指针数组的指针:

 11、回调函数


1、初识指针:

关于指针:

a. 指针是内存中一个最小单元的编号,也就是地址。

b. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量 

c.指针变量:我们可以通过&(取地址操作符)取出变量的内存其实是地址,把地址可以存放到一个变量中,这个变量就是指针变量

d.指针可以减但不能加,可以减去两个相同类型的指针变量,表示两个指针指向的内存位置之间隔了多少个元素(注意是元素个数,而不是字节数)。

d.存放在指针的值都被当成地址处理(指针是拿来存放地址的),地址是唯一标示一块地址空间的。一个字节对应一个地址,然后指针的大小在32位平台是4个字节,在64位平台是8个字节的空间来存储。C语言学习中的小收获(指针)_第1张图片

 如图指针大小相同都为4个字节,和不同平台有关。

关于字符指针:char*

int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";//这里被赋值的实质是,首字符h的地址放到指针str3中
	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;
}

C语言学习中的小收获(指针)_第2张图片

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

如下图所示,当我用字符串常量去赋值时出现问题,解决办法是只要在前面加个const就好了C语言学习中的小收获(指针)_第3张图片而实际了解到一些相关内容:

在一个双引号" "内的字符序列或者转义字符序列称为字符串常量。如下的“sunday”,这些字符串常量是不能改变的,而当你尝试改变用指针指向的内容也是错误的,例如:

const char*p=“sunday”;
*p="we";//是错误的,不可以更改

因为“sunday”是字符串常量,它的存储位置属于只读,而指针p只是指向这个储存空间,而对其内容无法进行修改,如果修改的话其行为是位定义的。但是我们基于对其储存位置的理解,可以发现,如果我们把该值放到一个可修改的空间就可以了,所以我们可以如下:

C语言学习中的小收获(指针)_第4张图片

如果在上图中没有写那个if判断语句,判断p指针是否为空的话,会出现下面的警告提示
警告    C6387    “p”可能是“0”: 这不符合函数“strcpy”的规范。   
和上面有类似的更改效果如下:

C语言学习中的小收获(指针)_第5张图片

 如上图,可以将"we"赋值给p;看上面的监视窗口我们可以发现,指针p存放的是首字符s的地址,而当代码运行完p="we"后,如下面的监控窗口所示,指针p存放的地址发生了改变,这时我们可以说我们并没有改变原先指向地址里存放的值,而是把字符串we存放的新的地址赋值给了pC语言学习中的小收获(指针)_第6张图片

补充:但如果是创建一个数组char a[]=“sunday”,这样赋值的就是一个字符数组,字符数组a在栈区申请一个空间,存放从只读区复制过来的“sunday”,所以我们可以修改a中内容。

关于const:

1. 常量指针 形如:const int*p

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

C语言学习中的小收获(指针)_第7张图片

2. 指针常量  形如:int*const p

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

C语言学习中的小收获(指针)_第8张图片

如上图所示,我们不可以修改指针p1指向的地址,因为const修饰的是常量,指针的指向不可以改变,但指针指向地址所存的内容可以改变,如下图:将a地址中的r改为cC语言学习中的小收获(指针)_第9张图片

  

3、常量指针常量 形如:const char* coonst p 

指向的地址和指向对象的内容都不可以被修改

关于内存分配:

1、内存分配的类型:

有了解到原来在C/C++中内存分为5个区,分别为栈区、堆区、全局/静态存储区、常量储存区、代码区。

静态内存分配:编译时分配。包括全局、静态全局、静态局部三种变量

动态内存分配:运行时分匹配。包括栈、局部变量、堆(C语言中用到的变量被动态的分配在内存中。

(动态内存分配可以用如下函数,malloc(上述有个示例用到了)、calloc、realloc、free函数)

 关于相关函数和内存分配,有从这个链接了解更多:C语言动态内存分配详解_WE-ubytt的博客-CSDN博客_动态内存分配

零碎的:

关于栈区的默认使用:

先使用高地址处空间再使用低地址处的空间(从上到下)

在栈区中存放数组时,数组随着下标的增长,地址是由低到高变化(从下到上)

2、变量的内存分配

栈区:指那些由编译器在需要的时候分配,不需要时自动清除的变量所在的储存区,如函数执行时,函数的形参以及函数内的局部变量分配在栈区,函数运行结束后,形参和局部变量去栈(自动释放)。


堆区:指哪些由程序员手动分配释放的储存区,如果程序员不释放这块内存,内存将一直被占用,直到程序运行结束由系统自动收回,c语言中使用malloc,free申请和释放空间。


静态存储区:全局变量和静态变量的储存是放在一块的,其中初始化的全局变量和静态变量在一个区域,这块空间当程序运行结束后由系统释放


常量存储区:常量字符串就是储存在这里的,如“ABC”字符串就储存在常量区,储存在常量区的只读不可写。const修饰的全局变量也储存在常量区,const修饰的局部变量依然在栈上。

程序代码区:存放源程序的二进制代码。

在这方面我倒不是很了解,只是上述示例中存在内存位置问题,这里简单补充一下。

然后又了解到一些零散知识:

代码一般经过的四个步骤

预处理: 展开头文件/宏替换/去掉注释/条件编译(输出的是.i文件)

编译 :检查语法,生成汇编 (输出的是.s文件)

汇编 :汇编代码转换机器码 (输出的是.o文件)

链接 :链接到一起生成可执行程序(输出的是.out文件)

2、指针类型

char*,int*,short*,long*,float*,double*,其中不同类型对应存储不同类型变量的地址,如char*类型指针存放char类型变量的地址(所以char*的char是变量的类型)

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

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0;   //pc为char*类型,只能访问一个字节,故在内存监控中可以看到第一个字节变为0
	*pi = 0;   //pi为int*类型,可访问四个字节,在内存监控中可以看到该数值为0(四个字节都为0)
	return 0;
}

有关野指针:

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

首先我们要知道局部变量不初始化的话,默认会是随机值,所以当我们创建了一个指针变量却没有初始化的话,它也会任意指向一个地址,这样很不可控,极易对内存数据造成影响。如下代码存在的形成野指针成因:

//int* remind()
//{
//	int a = 1; //局部变量,一旦出了函数将会销毁该变量
//	return &a;
//}
//int main()
//{
//	  int a = 7;
//	  int* p;//1、指针未初始化,默认为随机值
//	  *p = 1;//这里的p为野指针,会指定随机一个地址赋值,不太可控
//
//	  int a[3] = { 0 };
//	  int* p = a;
//	  int i = 0;
//	  for (i = 0; i < 7; i++ )
//		*(p++) = 1;//2、指针越界访问,随着循环的继续会超过数组的大小
//
//	int* p = remind(); //3、指针指向已被释放的空间
//	*p = 0; //虽然有返回地址,但因为该变量已被销毁,空间被收回,该地址已经不是原来变量的地址了
//	return 0;
//}

如果想规避野指针,则可以将指针指向空间释放即:eg:int*p=NULL;

具体如下:

1. 指针初始化

2. 小心指针越界

3. 指针指向空间释放即使置NULL

4. 避免返回局部变量的地址

5. 指针使用之前检查有效性

3、指针运算

1、指针-指针得到中间的元素个数

如下代码

int count(char* shu)
{
	char* start = shu;
	char* end = shu;
	while (*shu++ != '\0')//判断条件也可写成*end!='\0'
		end++;
	return end - start;//指针-指针
	//该效果等同于如下代码:
	//int shu1[16]={0};
	//printf("%d",&shu[15]-&shu[0]);
} //记得数组前要加&,才代表指针(进行地址的相减)否则则是数组(元素值得相减)
int main()
{
	char shu[] = "today is sunday!";
	int a = count(shu);
	int shu1[16] = { 0 };
	printf("%d\n",&shu1[15]-&shu1[0]);
	printf("%d\n", a);
	return 0;
}
函数也可写成:记得最后一个元素地址-首地址的逻辑即可
int count(char* shu)
{
	//char* start = shu;
	char* end = shu;
	while (*end != '\0')
		end++;
	return end - shu;//start;

关于字符串的拷贝:

这里引入了之前没使用过的assert(断言),在完善copy函数时也涉及到函数类型的设置问题和函数参数是否出错的问题,当我们传送给函数地址时,如发生传输地址与应修改内容的地址互换的情况,可在前面加个const修饰,则代表被拷贝的对象内容无法被修改,

 char* copy(char* a,const char* b)
{
	// char* c = NULL;
	 assert(a != NULL);
	 assert(b != NULL);//记得加#include 
	 //assert(c != NULL);
	// 该效果等同于:
	// if (c==NULL)
	// {
	//	 puts("a=NULL");
	//	 abort();
	//}
	
	 char* adress = a;
	while (*a++ = *b++)
	{
		;
	}//当跳出循环时,指针a指向的位置是'\0'的位置,
	return adress;//所以要创建一个指针变量储存首地址
}
int main()
{
	char shu[] = "what day is it today?";
	char shu11[] = "tomorrow";
	printf("%s %s\n", copy(shu, shu11), shu11);
	return 0;
}

上图代码的函数的返回值是指针adress,它复制了传入的形参的a所储存的数组首元素的值,这就意味着,函数返回的是指向复制后的字符串的第一个字符的指针

关于assert()

如下两种书写方式,但其实感觉如果直接用if(a!=NULL&&b!=NULL)再写后面函数拷贝实现的代码,也会有类似的效果(I think)

 C语言学习中的小收获(指针)_第10张图片

 assert()宏接受一个整型表达式作为参数。如果表达式求值为假,assert()宏就在标准错误流(stderr) 中写入一条错误信息, 并调用abort()函数终止程序。因此如下也能完成上述功能(abort()函数的原型在stdlib.h头文件中):C语言学习中的小收获(指针)_第11张图片

了解到断言只用于测试阶段,不能用release版本使用,若要实现在这个版本使用,则要进行一些此博客没有提及的操作(hehe)

补充:

a、后面了解到原来也可以这样写assert(a)当a为空指针时括号里的值为0(0为假)也会报错,来检查指针的有效性。

b、使用assert的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。(一般不推荐)可用if。

2、指针+-整数:就是指向+-整数运算后的地址,一般也可用来遍历数组元素或对其赋值等

其实感觉和指针关系运算里的差不多,那换一个++的代码吧

int main()
{
	int shu[5] = { 1,2,3,4,5 };
	int* p=shu;
	while(p<&shu[5])	
	{
		*p++ = 9;   
	}
	for (p = &shu[0]; p< &shu[5];p++)
	{
	printf("%d ", *p);
	}
	return 0;
}

3、指针的关系运算:

int main()
{
	int shu[5] = { 1,2,3,4,5};
	int* p;
	for (p = &shu[5]; p > &shu[0];)或者可以写成for (p = &shu[5]; p > &shu[0];--p)
	{                                           {
		*--p = 0;                                    *p=0;            
	}                                           }
	return 0;
}

但是要注意有标准规定:

允许指向数组元素的指针(p2)与指向数组最后一个元素后面的那个内存位置的指针(p3)比较,但是不允许p2与指向第一个元素之前的那个内存位置的指针(p1)进行比较。

 p1(··········p2······)p3

补充:数组a的元素个数为n时,构成数组a的元素是a[0]到a[n-1],但是,指向数组元素的指针则可以是&a[0]到a[n],共n+1个,因为在对遍历数组元素的结束条件(是否到达了末尾)进行判定时,如果可以利用指向末尾元素后一个元素的话将会非常方便。例如上述代码从a[5],开始对数组进行遍历。

其先将a[5]的地址赋值给指针,再利用该值大于&a[0]作为判定条件进行遍历,符合此条件时,从一开始自减1使指针指向a[4]的地址,并将其元素改为0,再往复判断条件并赋值,直到指针指向a[0],跳出循环,完成遍历数组并赋值。

4、指针与数组

除了可以改变数组下标访问数组元素还可以用对应的指针形式去访问

int my_strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
		p++;
	return p - s;
}
int main()
{
	char b[12] ="hhhhgggggR";
	char* p = b;
	char* s = b;
	int a= my_strlen(s);
	printf("%d", a);
    printf("%p %p %p %p\n",&b[9], &p[9], p + 9, b + 9);//打印地址结果相同
	printf("%c %c %c %c", b[9],p[9],*(p+9),*(b+9));//打印元素结果相同
}

C语言学习中的小收获(指针)_第12张图片
如上代码为例,我们可以得出:

 &b[i]=p+i=&p[i]=b+i  所表示的地址相同且与i一一对应

(这些表达式都是指向各元素的指针)

 b[i]=p[i]=*(p+i)=*(b+i)  所表示的数组元素相同且i一一对应

(都是访问各元素的表达式)

可以发现数组和指针都可以通过下标运算符[],去访问各个元素

5、字符串与指针

关于自符串与指针:

a、用指针实现数组时,该指针被初始化为指向保存在内存中的字符串的第一个字符的指针(字符首元素地址赋值给指针)

b、可以为指向字符串字面量(中的字符)的指针赋上指向别的字符串字面量(中的字符)指针,赋值后,指针指向新的字符串字面量(中的字符)

c、用指针实现字符串数组————指针数组

eg:char*p[]={"today","is","sunday"};

各数组元素p[0],p[1],p[2]的初始值分别是指向各字符串字面量的首字符”t“、”i“、”s“,的指针,这里和下面提及的数组指针在二维数组上的运用是一样的道理(这里和二维数组不同的是二维数组所有元素连续排列,而指针数组无法保证字符串排列的顺序和连续性)如下图:

C语言学习中的小收获(指针)_第13张图片

int main()
{
	int i = 0;
	char shu[][7] = { "today","is","sunday" };
	const char* p[] = { "today","is","sunday" };
	for (i = 0; i < 3; i++)
	{
		printf("%s ", p[i]);
	}	
	puts("\n");
	for (i = 0; i < 3; i++)
	{
		printf("%s ", shu[i]);
	}
	return 0;
}

C语言学习中的小收获(指针)_第14张图片

发现用%s格式打印字符串时,不需要用两个for循环,原因在于p[i]根据i值变化,指向每个元素的首字符,然后可以很好的打印出整个字符,shu[i]也是一样的效果,那如果是%c呢?如果有解引操作呢?如下面提及的指针数组的示例

关于字符串处理库函数

字符串处理所需的库函数主要由头文件提供

求字符串长度:strlen函数,返回不包括null字符在内的字符串

strlen函数原型  size_t strlen (const char* s)

复制字符串:strcpy、strncpy函数

strcpy原型:char*strcpy(char*s1,const char* s2)

返回s1的值。(若s1、s2指向的空间重叠,则作未定义处理)

strncpy函数:char*strncpy(char*s1,const char*s2,size_t n)

在strcpy效果的基础上,若s2的长度大于等于n,则复制到第n个字符为止,否则用null字符填充剩余部分。(同上未定义处理和返回值)

连接字符串:strcat、strncat

strcat函数原型:char*strcat(char*s1,const char*s2)

将s2指向的字符串连接到s1指向的数组末尾(未定义处理情况和返回值同上)

strncat函数:char*strncat(char*s1,const char*s2,size_t  n)

在strcat效果的基础上,若s2的长度大于n则截断超出部分。(同上)

(size_t是无符号的长整型,常用于sizeof的返回值)

学习指针时大概认识了一下相关函数,知道其形参和返回值类型是指针形式,详情请参考如下链接:

C语言----详解字符串相关的库函数(建议收藏)_梦の澜的博客-CSDN博客_c语言字符串函数库

6、二级指针

指针变量也是变量,是变量就有地址,而指针的地址就存储在二级指针,二级指针地址存在三级指针中·······,不难发现对应指针的地址需要创建对应的指针才能接收

int main()
{
	int i = 123;
	int* a= &i;
	int** b = &a;//int**b创建的是二级指针只能用一级指针赋值
	int*** c = &b;//和上面一样原理,该变量只能存放二级指针地址
	printf("%d %d %d\n", *a, **b, ***c);//运行结果相同
	printf("%p %p %p\n", a, *b,**c);//同上
	printf("%p %p\n", b, *c);//同上
}

C语言学习中的小收获(指针)_第15张图片

 我们也可以对多个*进行处理便于理解,如:int***c,将三个*间隔成int**  *c,则分开来看*c我们可以更好的理解成c为指针,指向的对象类型为int**(在如上代码即为二级指针b),而二级指针也是一样int* *b,*b说明b为指针,int*说明b指针指向一个一级指针int*a。

 7、指针数组(存放指针的数组,该数组每个元素都是指针)

关于指针数组:

指针是形容,修饰数组,所以实质是数组(同样的数组指针则实质是指指针,用数组来形容)

我们可以知道:                                              

整型数组 --- 存放整型     int a[2] ={1,4} 

字符数组--存放字符      char b[3] = "abc"

 指针数组---存放指针     int* c[3]={&a,&b,&c}

由此可以得出在创建类型的时候,按照存放对象的类型创建即可。

记得使用指针数组元素时要解引操作(eg:*c[2],和指针一样去看待)但如果是字符指针数组的话,如下解引后则是不同的

int main()
{
	int i = 0;
	char shu[][7] = { "today","is","sunday" };
	const char* p[] = { "today","is","sunday" };
	for (i = 0; i < 3; i++)
	{
		printf("%c %c ", *p[i],*(*(p+i)));
	}	
	puts("\n");
	for (i = 0; i < 3; i++)
	{
		printf("%c", *shu[i]);
	}
	return 0;
}

因为p[i]和shu[i]指向的是每个字符数组的首字符(即每个开头字符的首地址),在解引操作后,就只能打印出首字符(格式符为%c,打印单个字符),

C语言学习中的小收获(指针)_第16张图片

 

如下的监视窗口所示这是i=0时各表达式的值与地址,不难看出,解引和没解引前所指向内容的不同,详情可看:%s、%c、字符常量、字符串常量,const char*,指针数组,字符串数组总结_小哇123的博客-CSDN博客

C语言学习中的小收获(指针)_第17张图片

来区别一下不同的指针数组:例如

int* arr1[10]; //整形指针的数组

char *arr2[4]; //一级字符指针的数组

char **arr3[5];//二级字符指针的数组

关于指针数组得初始化:

如图所示,当我们取数组地址去初始化指针数组时出现报错,由此我们可以知道&a(取数组地址)的类型是int(*)[4](为数组指针),而int*p[2]中的表示p是一个由2个int*型指针元素组成的数组,所以要对作为元素之一的&a进行类型转换(像b一样这样表示)

C语言学习中的小收获(指针)_第18张图片

 

但是因为我们&a取的是整个数组的地址,所以的要进行类型转换,若换成数组名a呢?

C语言学习中的小收获(指针)_第19张图片

 我们可以看到,把数组名a、b初始化指针数组p时,可以发现。我们不需要再进行类型转换,原因和下面关于&数组名 和 数组名这一内容类似,数组名代表首元素地址,当其作为元素赋值给p数组时,该实质相当于一个一维指针(存储一个变量地址)因此它的类型就是int*,和上面相比不需要进行转换),如图代码结果在解引后也只能打印a、b数组的首元素。

 8、数组指针(指向数组的指针) 形如:int(*p)[10]  

int (*p)[10];

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

//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组(但我们一般不写成 int (*p)[10]=&shu,而是写成int*p=&shu)

//p的类型是int(*)[10],一般去掉名字后就是类型

例如:

int main()
{
	int shu[4] = { 1,2,3,4 };
	int(*p)[4] = &shu;
	printf("%d %d\n", (*p)[2],*(*p+2));
 //*p存储着数组地址(*p=shu),*p解引得到数组地址,再解引得到数组元素
	/*但一般都这样写:
	int*p=shu; 
    //指针p存储着地址,
	printf("%d %d\n",*(p+2),p[2]);*/
	return 0;
}

上图结果都为3,数组中第三个元素

C语言学习中的小收获(指针)_第20张图片

 由上图**p可知,int(*p)[4] = &shu;这样定义使得p与二级指针一样去操作。

int main()
{
	char shu1[] = "we!";
	char* shu[2] = { &shu1[0],&shu1[1]};
//该效果等同于:
//char a='w'; char b='e'; char c='!';
//char* shu[2]={&a,&b};
	char* (*p)[2] = &shu; //该数组指针指向对象类型为char*[2](字符型指针数组)
	printf("%c %c\n", *(*p)[0], *(*p)[1]);
//对数组指针(*p)[i](存储着指针数组的地址)用*解引得到指针数组的元素
	return 0;
}

图中两种赋值方式均可,如下图输出结果得效果,输出前两个元素w e

主要记得那个对数组指针得解引操作

C语言学习中的小收获(指针)_第21张图片

补充: int(*p[10])[5]:p是一个数组,该数组有10个元素,每个元素是一个数组指针,该数组指针指向一个含有5个元素的数组,每个元素都是int。

关于&数组名 和 数组名

实际上:形如int arr[10]={0};我们要了解得是:

a、&arr 表示的是数组的地址,而不是数组首元素的地址。

b、其中 &arr 的类型是: int(*)[10] ,是一种数组指针类型

c、数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值为(数组个数×4),而arr代表首地址,arr+1,代表跳过一个数组元素,相对arr的差值为4

C语言学习中的小收获(指针)_第22张图片

 

根据以上代码,我们除了上面所述得大小不同外,还要知道更多的是:

我们知道shu代表数组首地址,这个时候指向的是数组首元素(即为整型11),所以根据指向的是一个整型,我们应该用int *p(整型指针去接收)。同理,&shu代表是区整个数组的地址,所以指向的是整个数组(该数组类型为int[3]),所以我们应该创建int(*p1)[3]这一数组指针去接收。

在解引方面,*p则是直接对首元素地址解引,即得到首元素11,*p1则是对数组地址解引得到数组地址,**p1则是对首地址解引得首元素。

数组指针的运用:

一般情况下数组指针至少用到二维数组以上才方便一些

void print(int(*p)[5], int x, int y)
{
	int i;
	int j=0;
	for (i = 0; i < x; i++)
	{
		for (j = 0; j < y; j++)
		{
			printf("%d  %d  %d  %d\n", *(*(p + i) + j),(*(p+i))[j],*(p[i]+j),p[i][j]);
			
		}
	}
}
int main()
{
	int shu[3][5] = { {1,2,3,4,5},{6,7,8,9,0},{1,1,1,5,7} };
	print(shu,3,5);
	return 0;
}
 

C语言学习中的小收获(指针)_第23张图片

 我们可以发现上面四种表示方式的输出结果都是一样的,由此可以发现在二维数组中,p指向的地址其实是第一行的地址(我们把第一行看成一个元素,第二行,第三行也是),指向的是以{1,6,1}为元素的数组地址,而首地址就是以第一行为首元素的地址,而该元素又是一个由5个整型数据形成的一维数组,

第一行        1,2,3,4,5   

第二行        6,7,8,9,0

第三行        1,1,1,5,7

通过下图我们可以发现*p,*(p+1),*(p+2),代表指向第一行,第二行,第三行的地址,所以当我们解引操作就可以得到这个数组元素(即每行的首元素)

C语言学习中的小收获(指针)_第24张图片

补充:虽然函数中有解引二级指针,但是不能用二级指针做形参接收二维数组的地址。

9、数组传参、指针传参

关于二维数组传参

在传二维数组传参时,二维数组数组名表示的是第一行,首元素的地址是第一行的地址,第一行是一维数组,所以首元素地址是就是一个一维数组,所以在设置形参接收时,不能以int*shu这样的整型指针存放(因为这样表示指针shu指向一个整型,我们需要的是指向一个数组),也不能用int**shu这样的二级指针存放(因为二级指针存放一级指针变量的地址,我们的数组元素是int型),而是用形似int(*shu)[10]这样的数组指针(指针shu指向一个由10个int型元素组成的数组)

其实我们要记得: 二维数组的数组名表示首元素的地址,而二维数组首元素是第一行!

其中对于int(*shu)[10]我们要理解的是它是怎样得到的:

int[10]:说明了指针指向的数组类型,是由一个包含10个元素的数组,每个元素为int型

同理,如下指向指针数组的数组指针也是一样的

我们要接收的是形如int*shu[2][2] 的指针数组

数组类型为int*[2][2],加上对二维数组传参的理解,我们在[]里填的应该是列数,加上我们是要用一个指针去接收实参,所以可以确定(*p),在把int*[2]数组类型(确定行数后指向的一维数组类型)补上即形成了int*(*p)[2]

void print(int*(*p)[2])
{
	int i = 0;
	int j = 0;
	printf("%d %d\n", ***p, ***(p + 1));
	puts("\n");
	for (i = 0; i < 2; i++)
	{
		for (j = 0; j < 2; j++)
		{
			printf("%d %d ", **(*(p + i) + j), **(p[i] + j));
		}
		puts("\n");
	}
}
int main()
{
	int shu11[4] = { 11,33,77,99 };
	int* shu[2][2] = { {&shu11[0],&shu11[1]},{&shu11[2],&shu11[3]} };
		print(shu);
		return 0;
}

C语言学习中的小收获(指针)_第25张图片

关于指针传参:

关于一级指针传参:

(int shu[11]={0}; int*p=shu; )

print(p):当用指针传参时:

void print(int*p):直接创建一个指针变量当做形参接收

print(shu):当用数组传参时

void print(int*p):同上

void print(int shu[]):创建一个数组(本质为指针)当作形参接收

二级指针传参:

( int a=10;  int b=11;   int*p=&a ;   int**ppa=&p; int*shu[2]={&a,&b})

void print(int**p):当用二级指针当作形参时,可以有以下几种传参形式:

print(ppa):其中ppa为二级指针

print(&p):其中p为一级指针

print(shu):其中shu为(存放一级指针的数组)指针数组(创建int*shu[]当作形参接收也可)

(传一个存有一级指针这样数组的数组名,数组名表示首元素地址,所以一级指针的地址恰好在二级指针里)

 10、函数指针

函数指针:指向函数的指针

a、&函数名 和 函数名都是函数的地址(它们等价,要和数组名区分开来)

例如:

int factorial(int*a)//该函数实现1!+2!+······a!
{
	int i, j;
	int sum, temp;
	for (i = 1,sum=0; i <= *a; i++)
	{
		for (j = 1,temp=1; j <= i; j++)
		{
			temp *=j;	
		}
		sum += temp;
	}
	return sum;
	
}
int main()
{
	int a=0;
	int(*p)(int*) = factorial;//不带&取函数地址
	int(*p1)(int*) = &factorial;//带&取函数地址
	scanf("%d", &a);
	printf("%p %p\n", factorial, &factorial);//用二者形式等价,所以给p和p1初始化时,下面的结果相同
    printf("%p %p\n", *p, *p1);//p和p1效果一样,都是指向factorial函数的地址
    printf("%p %p\n", p, p1);//效果同上
    printf("%d\n", factorial(&a));//平时调用函数的写法
	printf("%d %d\n", (*p)(&a),(*p1)(&a));//用函数指针指向函数进行替换
	printf("%d %d\n", p(&a),p1(&a));//用*和不用*效果相同
	return 0;
}

 如下图结果所示:当指向函数的指针,*p和p(解引和不解引)效果相同(这时的*没有实际意义),是否用&取函数地址也相同,在用函数指针(如果用*p形式)去调用函数时记得要()把*p括起来,否则p会和()结合。(括号优先级更高)C语言学习中的小收获(指针)_第26张图片

 

b、创建形如:int(*p)(int,int)的函数指针接收函数地址

(p的类型名为int(*)(int,int))

  开头的int:是函数返回类型

(int,int):这里则是函数形参的类型

如上代码:创建int(*p)(int*,int*)函数指针

//代码1 (*(void (*)())0)();

(调用0地址处的函数,该函数无参,返回类型是void)

可通过以下步骤解读

1、void(*)()  :函数指针类型

2、(void(*)())0 :对0进行强制类型转换,被解释为一个函数

3、*(void(*)())0 :对0地址进行了解引用操作

4、(*(void(*)())0)() :调用0地址处的函数(最外面的()即是表示传参,只不过这里不用)

//代码2 void (*signal(int , void(*)(int)))(int);

1、signal和()先结合,说明signal是函数名

2、signal函数的第一个参数类型是int,第二个参数的类型是函数指针

(该函数指针,指向一个参数为int,返回类型是void的函数)

3、signal函数的返回类型也是一个函数指针

(该函数指针,指向一个参数为int,返回类型是void的函数)

综上:signal是一个函数声明

如果写成如下方式(语法不支持)可能会更好理解:

void(*)(int)    signal(int,void(*)(int))
————————————    —————— ———————————————— 
函数返回类型      函数名     函数参数类型

或者用typedef重命名:

eg:typedef  void(*)int    hans 

则代码2可改写成 hans  signal(int,hans)

(这样感觉可读性更强)

补充:关于define和typedef定义后创建的变量形式区别

#define int_p  int* 若创建变量ab,则

int_p a,b;  (等价于int *a;int b)

typedef int*   int_p a,b; (等价于int*a;int*b)

而typedef定义之后是一种独立类型

但define 本质是进行文本替换,下面的例子可以更深刻的意识到:

#include
#define n 2
#define m n+1
#define num  (m+1)*m/2
int main()
{
   printf("%d\n",num);
   return 0;
}

错误版本:先算出 n=2,m=n+1=3,m+1=4;
然后带入公式(m+1)*m/2= 4*3/2 = 12/2=6
因为define定义实际是一种文本替换
所以正确应该这样:
(m+1)*m/2=(n+1+1)*n+1/2=4*2+1/2=8.5,
打印结果为8

关于函数指针数组

形如:void (*shu[5])(const char* str),

一个含有5个这样参数类型为const char* str,返回类型为void的函数指针元素的数组

简单来说就是一个数组,每一个数组元素都是指向函数的指针(存放函数指针的数组)

运用场景如下:(实现加减乘除)

void manu()
{
	printf("********************************\n");
	printf("******1、加法    2、减法********\n");
	printf("********************************\n"); 
	printf("******3、乘法    4、除法********\n");
	printf("********************************\n");
	printf("**********0、退出 **************\n");

}
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 dev(int a, int b)
{
	return a/b;
}
int main()
{
	int a = 0;
	int b = 0;
	int input = 0;
	int(*p[5])(int, int) = {NULL,add,sub,mul,dev};//函数指针数组
	do {
		manu();
		printf("请选择:");
    	scanf("%d", &input);
		if (input>0 && input <= 4)
		{
			printf("请输入两个数:");
			scanf("%d %d", &a, &b);
			printf("%d\n", p[input](a, b));
		}
		else if (input == 0)
		{
			printf("退出程序\n");
			break;
		}
		else
		{
			printf("输入错误,请再次选择\n");
		}
		
	} while (input);
	
	return 0;
}

关于指向函数指针数组的指针

形如void (*(*shu)[5])(const char*),

指向一个含有5个参数类型为const char* ,返回类型为void的函数指针数组的指针

简单来说就是一个指针,指向的是一个数组,每一个数组元素都是函数指针

(指向函数指针的数组的指针)

int(*p)(int,int)=add;//函数指针
int(*p1[5])(int,int)={NULL,add,sub,mul,dev};//函数指针数组
int(*(*p)[5]))(int,int)=&p1;//指向函数指针数组的指针

C语言学习中的小收获(指针)_第27张图片

 

 如上运用

 11、回调函数

(回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。)好绕的概念,反正发现不是很分明的关系,

(以下注释为另外一种方式其中cala和cala1函数实现效果相同)

void manu()
{
	printf("********************************\n");
	printf("******1、加法    2、减法********\n");
	printf("********************************\n");
	printf("******3、乘法    4、除法********\n");
	printf("********************************\n");
	printf("**********0、退出 **************\n");

}
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 dev(int a, int b)
{
	return a / b;
}
void cala(int(*(*p)[5])(int , int ),int input)//
{//创建指向函数指针数组的指针接收数组地址
    int a = 0;
    int b = 0;
	printf("请输入两个数:");
	scanf("%d %d", &a, &b);
	printf("%d\n",(*p)[input](a, b));
}
//void cala1(int(*p)(int, int)) 创建函数指针接收函数地址
//{
//  int a = 0;
//  int b = 0;
//	printf("请输入两个数:");
//	scanf("%d %d", &a, &b);
//	printf("%d\n", p(a, b));
//}
int main()
{
    int input = 0;
	int(*p1[5])(int, int) = { NULL,add,sub,mul,dev };
	do {
		manu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			cala(&p1,input);
			//cala1(add);
			break;
		case 2:
			cala(&p1,input);
			//cala1(sub);
			break;
		case 3:
			cala(&p1,input);
			//cala1(mul);
			break;
		case 4:
			cala(&p1,input);
			//cala1(dev);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("输入错误,请再次选择\n");
		}
	} while (input);

	return 0;
}

如上图创建了cala()函数与创建的函数指针数组的指针(cala函数中)或是创建的函数指针(cala1函数中)之间的调用关系,如果没有创建cala或是cala1函数(即函数中的代码都会在case中重复),那么在switch函数中每种情况会变得很冗长。

通过函数指针调用各种各样的函数来实现函数调用机制

qsort函数原型:

void qsort{(void*base, //base中存放的是待排序数据中第一个对象的地址
            size_t num,//排序数据元素的个数
            size_t size,//排序数据中一个元素的大小,单位是字节
            int(*compar)(const void*,const void*)//compar是用来比较待排序数据中的2个元素的函数,参数存放的是两个元素的地址
           };int表示返回一个整型,大于0则表示第一个元素大于第二个元素,=0,则表示第一个元素等于第二个元素,<0则表示第一个元素小于第二个元素

下面模拟qsort实现一个冒泡排序的通用算法:

compar(const void* x, const void* y)
{
	return (*(int*)x - *(int*)y);//若是想要得到逆序,则可以交换相减顺序
}
void swap(void* x, void* y,int size)
{
	int i = 0;
	for (i = 0; i < size; i++)//根据类型大小,一个字节一个字节进行交换
	{
		char temp = *((char*)x + i);//类型的强制转换再解引,+i达到很好的逐字节交换效果
		*((char*)x+i )= *((char*)y+i);
		*((char*)y+i) = temp;
	}//这里的解引操作很容易搞错,记得是设置char型的临时变量来放置,达到交换的目的
	

} //参数很多都是void型,即使是已经知道要排列的是整型数据,但在设计函数时为了通用性,还是进行强制类型转换和编写比较参数大小的不同函数更实际的多。
void bubble(void* shu, int num, int size, int(*compar)(const void* ,const void* ))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < num; i++)
	{
		for (j = 0; j < num - i - 1; j++)
		{
			if (compar((char*)shu+j*size,(char*)shu+(j+1)*size)> 0)
			{
				swap((char*)shu + j * size, (char*)shu + (j + 1) * size, size);

			}//通过转换成char*型(可以进行一个字节一个字节的访问)+j很好的通过遍历来访问和排序
		}
		
	}
}

int main()
{
	int i = 0;
	int shu[] = {11,33,77,3,5,1,11,99,79,31,57 };
	int num = sizeof(shu) / sizeof(shu[0]);
	//qsort(shu,num, sizeof(shu[0]),compar);
	//具体的qsort函数的实现,我们用冒泡的思想去模拟一下:
	bubble(shu,num , sizeof(shu[0]), compar);
	for (i = 0; i < num; i++)
	{
		printf("%d ", shu[i]);
	}
	return 0;
}

详情请参考:(觉得从以下可以了解的比较清楚些)

【C语言进阶】使用回调函数,模拟实现qsort_吃不胖的熊猫的博客-CSDN博客

使用回调函数,模拟实现qsort(采用冒泡的方式)_天青i的博客-CSDN博客_qsort函数

你可能感兴趣的:(c语言,学习)