C语言指针详解


指针是C语言中十分重要的概念,掌握指针是掌握C语言的基础,本篇将为大家详细介绍C语言中的指针。

内存和地址

内存

我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。内存在使用时会被划分为一个个的内存单元,每个内存单元的大小取1个字节。每个内存单元就是1byte,每个字节单元可以存放8个比特位,每个内存单元也都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。
在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。
所以我们可以理解为:
内存单元的编号=地址=指针

地址

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址。计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
CPU和内存之间有大量的数据交互,所以,两者必须用线连起来,我们现在主要关注地址总线
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有无】,那么⼀根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据再通过数据总线传入CPU内寄存器。

指针变量和地址

取地址操作符&

理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间。
比如

#include <stdio.h>
int main()
{
	int a = 10;
	return 0;
}

上述的代码就是创建了整型变量a,内存中申请4个字节,用于存放整数10,其中每个字节都有地址。
那我们如何能得到a的地址呢?这里就得学习⼀个操作符&—取地址操作符。

#include <stdio.h>
int main()
{
	int a = 10;
	&a;//取出a的地址
	printf("%p\n", &a);//%p为打印地址(指针)的占位符
	return 0;
}

&a取出的是a所占4个字节中地址较小的字节的地址,虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。

指针变量和解引用操作符*

指针变量

我们通过取地址操作符&拿到的地址是⼀个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中。
比如:

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

指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址

如何拆解指针类型

pa的类型是int* ,我们该如何理解指针的类型呢?

int a = 10;
int * pa = &a;

这里pa左边写的是类型的对象int**是在说明pa是指针变量,而前面的int 是在说明pa指向的是整型类型的对象。
那么可以类比,如果我们有⼀个char类型的变量ch,可以这样放在指针变量中:

char ch = 'w';
char * pc = &ch;

解引用操作符

我们将地址保存起来,未来是要使用的,那怎么使用呢?
在现实生活中,我们使用地址要找到⼀个房间,在房间里可以拿去或者存放物品。C语言中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习⼀个操作符叫解引用操作符*

# include <stdio.h>
int main()
{
	int a = 0;
	int* pa = &a;
	*pa = 0;
	return 0;
}

上⾯代码中就使用了解引用操作符,*pa 的意思就是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa=0,这个操作符是把a改成了0。

指针变量的大小

前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。
总结:
32位平台下地址是32个bit位,指针变量大小是4个字节;
64位平台下地址是64个bit位,指针变量大小是8个字节;
注意指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的平台下,大小都是相同的。

指针变量类型的意义

指针变量的大小和类型无关,只要是指针变量,在同⼀个平台下,大小都是⼀样的,为什么还要有各种各样的指针类型呢?我们继续往下学习。

指针的解引用

对比,下面2段代码,主要在调试时观察内存的变化。

在这里插入图片描述
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。
比如:char* 的指针解引用就只能访问⼀个字节,而int* 的指针的解引用就能访问四个字节。

指针 ± 整数

先看⼀段代码,调试观察地址的变化。

#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个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也以-1。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。

void*指针

在指针类型中有⼀种特殊的类型是void * 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void* 类型的指针不能直接进行指针的±整数和解引用的运算。
那么void*类型的指针到底有什么用呢?
⼀般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得⼀个函数可以处理多种类型的数据。

const修饰指针

const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作用。

#include <stdio.h>
int main()
{
	int m = 0;
	m = 20;//m是可以修改的
	const int n = 0;
	n = 20;//n是不能被修改的
	return 0;
}

上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我
们在代码中对n进行修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们绕过n,使用n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。

#include <stdio.h>
int main()
{
	const int n = 0;
	printf("n = %d\n", n);
	int*p = &n;
	*p = 20;
	printf("n = %d\n", n);
	return 0;
}

输出结果:
在这里插入图片描述
我们可以看到这里n确实修改了,但是我们还是要思考⼀下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?

const修饰指针变量

⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的。
我们看下面代码,来分析具体分析⼀下:

 #include <stdio.h>
 //代码1 - 测试⽆const修饰的情况
 void test1()
{
	int n = 10;
	int m = 20;
	int *p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
//代码2 - 测试const放在*左边的情况
 void test1()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
//代码3 - 测试const放在*右边的情况
 void test1()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
//代码4 - 测试const放在*两边的情况
 void test1()
{
	int n = 10;
	int m = 20;
	int* const * p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
int main()
{
	//测试⽆const修饰的情况
	test1();
	//测试const放在*左边的情况
	test2();
	//测试const放在*右边的情况
	test3();
	//测试const放在*两边的情况
	test4();
	return 0;
}

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

指针运算

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

指针 + -整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸瓜就能找到后面的所有元素。

 #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]);
 	for(i=0; i<sz; i++)
 	{
 	 	printf("%d ", *(p+i));//p+i 就是指针 +- 整数
 	}
 	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;
}

可以看出,指针 - 指针的结果是两个地址间的元素个数,元素类型和指针指向的数据类型相同。

指针的关系运算

 //指针的关系运算
#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;
}

野指针

野指针成因

指针未初始化

#include <stdio.h>
 int main()
 {        
	int *p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
 }

指针越界访问

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

指针指向的空间释放

#include <stdio.h>
 int* test()
 {
	 int n = 100;
	 return &n;
 }
 int main()
 {
	 int*p = test();
	 printf("%d\n", *p);
	 return 0;
 }

如何规避野指针

  1. 初始化指针
    如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。
    NULL是C语言中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
    初始化如下:
#include <stdio.h>
 int main()
 {
	 int num = 10;
	 int*p1 = &num;
	 int*p2 = NULL;
	 return 0;
 }
  1. 小心指针越界
    ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
  2. 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性指针变量不再使用时,及时置NULL,指针使用之前检查有效性
    当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
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;
}
  1. 避免返回局部变量的地址
    如造成野指针的第3个例⼦,不要返回局部变量的地址。

assert断言

assert.h头文件定义了宏assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运⾏。这个宏常常被称为“断言”。

assert(p != NULL);

代码在程序运行到这一行语句时,验证变量p是否等于NULL。如果确实不等于则继续运行,否则就会终止运行,并且给出报错信息提示。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零),assert() 不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

指针的使用和传值调用

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

传值调用和传址调用

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤

数组名的理解

首先请看代码:

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

这里我们使用&arr[0]的方式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址,我们来做个测试。

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

输出结果(为了方便展示,选用x86环境):
在这里插入图片描述
我们发现数组名和数组⾸元素的地址打印出的结果一模一样,数组名就是数组首元素(第⼀个元素)的地址
那么下面的代码该如何理解呢?

 #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才对。
其实数组名就是数组首元素(第⼀个元素)的地址是对的,但是有两个例外:

  1. sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
    除此之外,任何地方使用数组名,数组名都表示首元素的地址。
    再看下面代码:
 #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操作是跳过整个数组的。
到这里大家应该搞清楚数组名的意义了。
总结:数组名是数组首元素的地址,但是有2个例外

使用指针访问数组

有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。

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

这个代码搞明摆后,我们再试一下,如果我们再分析⼀下,数组名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]));//将*(p+i)换成p[i]
 	}
 	return 0;
 }

*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i]是等价于*(p+i)的。
同理arr[i]应该等价于*(arr+i)数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的

一维数组传参的本质

数组是可以传递给函数的,现在我们讨论⼀下数组传参的本质。
首先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,函数内部求数组的元素个数吗?

#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) 计算的是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行比较。

//⽅法1 
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
	 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;
		 		}
		 	 }
		 }
}

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

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

二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址也可以存放在指针变量中,存在指针的指针变量就是二级指针。
二级指针的定义:

#include<stdio.h>
int main()
{
	int a = 10;
	int * pa = &a;
	int ** ppa = &pa;
	return 0;
}

对于⼆级指针的运算有:

  1. *ppa通过对ppa中的地址进行解引用,这样找到的是pa*ppa访问的就是pa
 int b = 20;
 *ppa = &b;//等价于pa = &b;
  1. **ppa先通过*ppa找到pa,再对pa进行解引用操作,找到a
**ppa = 30;
//等价于 *pa = 30;
//等价于 a = 30;

指针数组

指针数组是数组,数组中的元素是指针

指针数组模拟⼆维数组

指针数组中的每个元素都是指针,如果这里的每个指针都是一个数组的首元素的地址,那么这个指针数组就可以看作是二维数组。

#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]就是整型一维数组中的元素。上述的代码模拟出⼆维数组的效果,实际上并非完全是⼆维数组,因为每一行并非是连续的。
parr数组的画图演示

字符指针变量

在指针的类型中,指向字符的指针类型为字符指针char*
一般这样使用:

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

还有⼀种使用方式如下:

int main()
 {
 	const char* pstr = "abcdef.";//这里用char*来接受一个字符串
 	printf("%s\n", pstr);
 	return 0;
 }

const char* pstr = "abcdef."这句代码并不是把字符串abcdef.放到字符指针pstr中,而是把字符串abcdef.的首元素地址放进了pstr中。
《剑指offer》中收录了⼀道和字符串相关的笔试题:

#include <stdio.h>
 int main()
 {
	 char str1[] = "hello world";
	 char str2[] = "hello world";
	 const char *str3 = "hello world";
	 const char *str4 = "hello world";
	 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相同。

数组指针变量

数组指针变量是什么

数组指针变量是指针变量,存放的是数组的地址,能够指向数组。
数组指针变量int (*p)[10]:p先和*结合,说明p是一个指针变量,然后指向的是⼀个大小为10个整型的数组。所以p是⼀个指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于*的,所以必须加上()来保证p先和*结合
对比数组指针和指针数组的定义:

int *p1[10];//p1是一个指针数组,数组包含十个元素,每个元素是指针。
int (*p2)[10];//p2是一个数组指针变量,指向一个数组,数组包含十个元素,每个元素的类型是int

数组指针变量的初始化

数组指针变量是用来存放数组地址的,想获取数组的地址就要使用上面提到的&数组名

int arr[10] = {0};
int(*p)[10] = &arr;

在这里插入图片描述
调试也能看到p&arr的类型是完全相同的
数组指针类型解析:

int  (*p)  [10] = &arr;
 |    |     |    
 |    |     |
 |    |     p指向数组的元素个数——10
 |    p是数组指针变量名
 p指向的数组的元素类型——int[10]

二维数组传参的本质

了解数组指针后,就可以理解⼆维数组传参的本质了。
使用二维数组传参,代码如下:

#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 ", *(*(p+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;
 }

总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式

函数指针变量

函数指针变量是用来存放函数地址的,未来通过地址能够调用函数。

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

打印结果为:在这里插入图片描述
打印出来了地址,说明函数是有地址的,函数名就是函数的地址,也可以通过&函数名来获得函数的地址。
如果我们要将函数的地址存放起来,就需要创建函数指针变量,函数指针变量的写法其实和数组指针非常类似。如下:

 void test()
 {
 	printf("hello world\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;

函数指针类型解析:

 int   (*pf3)   (int, int)
  |       |          |
  |       |          |
  |       |          pf3指向函数的参数类型和个数
  |       函数指针变量名
  pf3指向函数的返回类型
int (*) (int x, int y)//pf3函数指针变量的类型

函数指针变量的使用

通过函数指针调用指针指向的函数。

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

输出结果:

5
8

typedef关键字

typedef是用来类型重命名的,可以将复杂的类型,简单化。比如:

typedef unsigned int uint;
//将unsigned重命名为unit
typedef int* ptr_t;
//将int*重命名为ptr_t

但是对于数组指针和函数指针稍微有点区别:

//新的类型名必须在*的右边
typedef int(*parr_t)[5];
//将int(*)[5]重命名为parr_t
typedef void(*pfun_t)(int);
//将void(*)(int)重命名为pfun_t

函数指针数组

把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组。
函数指针数组的定义:

int (*arr[]) (int, int);

arr先和[]结合,表示arr是数组,数组中元素的数据类型为int (*) (int, int)

回调函数

回调函数就是⼀个通过函数指针调用的函数。
如果把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
例如,使用回调函数模拟实现qsort函数(采用冒泡排序)

struct grade 
{
	char* name;
	int grade;
};
int com_int(const void* p1,const void* p2)
{
	return *((int*)p1) - *((int*)p2);
}
int com_char(const void* p1,const void* p2)
{
	return *((char*)p1) - *((char*)p2);
}
int com_struct_by_grade(const void* p1,const void* p2)
{
	return ((struct grade*)p1)->grade - ((struct grade*)p2)->grade;
}
int com_struct_by_name(const void* p1,const void* p2)
{
	return strcmp(((struct grade*)p1)->name , ((struct grade*)p2)->name);
}
//模仿qsort的功能实现一个通用的冒泡排序
void my_qsort(const void* p, size_t sz, size_t width, int(*com)(void* p1, void* p2))
{
	for (int i = 0; i < sz - 1; i++)
	{
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (com((char*)p + j * width, (char*)p + (j + 1) * width )> 0)
			{
				for (int k = 0; k < width; k++)
				{
					char tmp = *((char*)p + j * width + k);
					*((char*)p + j * width + k) = *((char*)p + (j + 1)* width + k);
					*((char*)p + (j + 1)*width + k) = tmp;
				}
			}
		}
	}
}
int main()
{
	int arr1[] = { 2,5,3,2,4,6,5,4,8,1,9 };
	char arr2[] = { '3','a','!','%' };
	struct grade arr3[] = { {"zhangsan",68} ,{"lisi",90} ,{"wangwu",53} };
	size_t sz1 = sizeof(arr1) / sizeof(arr1[0]);
	size_t sz2 = sizeof(arr2) / sizeof(arr2[0]);
	size_t sz3 = sizeof(arr3) / sizeof(arr3[0]);
	my_qsort(arr1, sz1, sizeof(int), com_int);
	my_qsort(arr2, sz2, sizeof(char), com_char);
	my_qsort(arr3, sz3, sizeof(arr3[0]), com_struct_by_grade);
	my_qsort(arr3, sz3, sizeof(arr3[0]), com_struct_by_name);
	return 0;
}
  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值