在这里插入图片描述

1.指针是什么?

指针是内存中一个最小单元的编号 ,也就是地址
平时口语中说的指针其实本质上是指针变量,是用来存放内存地址的变量

  • 内存
    在这里插入图片描述

一个字节为一个内存单元,我们可以通过编号找到内存 ,编号在生活中叫地址,对应我们c语言其实就是指针

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

那我们&a是取出的是哪一个字节的地址?取出的是第一个字节的地址(较小的地址)
在这里插入图片描述

  • 内存单元是如何进行编号?

在32位机器上,地址就是32个0或1组成二进制序列,那地址就得用用4个字节的空间进行储存 ,也就是说32位机器,指针变量大小就是4个字节
在64位机器上,地址就是64个0或1组成二进制序列,那地址就得用用8个字节的空间进行储存 ,也就是说64位机器,指针变量大小就是8个字节

2.指针和指针类型

int main()
{
	int a = 0x11223344;
	char* pc = (char*)&a;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		*pc = 1;
		pc++;
	}
	return 0;
}
  • 指针类型的不同意义:指针的不同类型提供了不同的视角去观看和访问内存

指针的不同类型决定了,指针在解引用的时候一次性访问几个字节,决定了访问权限的大小
如果是char *的指针,解引用访问一个字节
如果是int *的指针,解引用访问四个字节

int main()
{
	int a = 0x11223344;
	int* pa = &a;
	char* pc = &a;

	printf("%p\n", pa);
	printf("%p\n", pa+1);

	printf("%p\n", pc);
	printf("%p\n", pc+1);

	return 0;
}

在这里插入图片描述

指针类型决定了指针+1跳过几个字节(步长)
如果是字符指针 ,+1跳过一个字节
如果是整形指针 ,+1跳过四个字节

2.1指针的解引用

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0; 
	*pi = 0; 
	return 0;
}

3.野指针

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

3.1野指针成因

指针未初始化

#include <stdio.h>
int main()
{
int *p;
  *p = 20;
return 0;
}

局部变量指针未初始化,默认为随机值

指针越界访问

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

当指针指向的范围超过数组arr的范围 , p 就是野指针

指针指向的空间释放

int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int *p = test();
	printf("hehe\n");

	printf("%d\n", *p);

	return 0;
}

3.2如何规避野指针

  • 指针初始化
int main()
{
	int  a = 10;
	int* pa = &a;
	return 0;
}

如果实在不知道指针指向哪里 也需要初始化

#include <stdio.h>
int main()
{ 
	int* p = NULL;
	if (p != NULL)
	{

	}
	return 0;
}
  • 使用assert(断言)

assert中可以放一个表达式,如果表达式为假就报错,如果为真就什么也不发生
举个例子

 void  * my_strcpy(char* dest, const char* src)
{
	 assert(dest && src);//断言指针的有效性
	 //assert(dest!= NULL)
	 //assert(src!= NULL)
	 while (*dest++ = *src++)
	 {
		 ;
	 }
 }
int main()
{
	char arr1[] = "hello";
	char arr2[20] = { 0 };
	my_strcpy(arr2, arr1);
	printf("%s\n", arr2);
	return 0;
}
  • 小心指针越界
  • 指针指向空间释放,及时设置NULL
  • 避免返回局部变量的地址
  • 指针使用之前检查有效性

4.指针运算

4.1指针± 整数

4.2指针-指针

前提 :两个指针要指向同一块空间
指针-指针的绝对值得到的是两个指针之间的个数

int main()
{
	char ch[5] = {0};
	int arr[10] = { 0 };
	printf("%d\n", &arr[0] - &arr[9]);
	return 0;
}

在这里插入图片描述

4.3指针的关系运算

标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
也就是说可以允许向后找到越界的地址和数组某个元素进行比较,但是不允许向前越界找到的地址和数组某个元素进行比较。这里的越界是指跳出了自己的范围,并没有非法访问越界之后的指针。这里只是比较大小。

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES]; vp > &values[0];)
{
  *--vp = 0;
}

在这里插入图片描述

5.指针和数组

指针和数组是不同的对象
指针是一种变量,用来存放地址,大小为4/8个字节。
数组是一组相同类型元素的集合,是可以存放多个元素的,大小取决于元素个数和元素类型。
数组的数组名是数组首元素的地址,地址是可以放在指针变量中,可以通过指针访问数组。

6.二级指针

int main()
{
	int a = 10;
	int* pa = &a;
	int* * ppa = &pa;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

pa是一级指针变量 ,ppa 是二级指针变量。 a 开辟了一块内存,用来存放数据 ,数据为10。 a的地址是0x0012ff40。 pa里面存放a的地址

7.指针数组

存放指针的数组
使用一维数组模拟二维数组

int main()
{
	int a[] = { 1,2,3,4 };
	int b[] = { 2,3,4,5, };
	int c[] = { 3,4,5,6 };
	int* arr[] = { a , b ,c };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("%d", arr[i][j] );
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述

关于arr [i] [ j] 的解释: arr[ i] 当i 为 0 , 1 ,2 时 arr [0] 访问的是a数组首元素地址 arr[1] 访问的是b数组首元素的地址 arr[ 2] 访问的是c数组首元素的地址
在这里插入图片描述
*(arr [0] + j ) 当j是 0 , 1,2,3 时 ,就可以等价于 arr[0] [j ]


8.字符指针

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

还有一种表现形式

int main()
{
const char* ps = "hello bit";//常量字符串是不能被修改
	return 0;
}

本质上是把“hello bit“这个字符串的首字符的地址存储到ps中


下面看一下一道来自《剑指offer》的题

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

结果:str1和str2不相同 ,str3和str4相同 那为什么会这样子呢?
因为str1 和str2 指向不同的地址(这里不是比较两个字符串的内容)
hello bit 是常量字符串 ,在内存中只会存一份
str3 和str4 都存放了h的地址,所以str3和str4相同


在这里插入图片描述

9.指针数组

指针数组是一个存放指针的数组

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

指针数组的应用场景

int main()
{
	int a[5] = { 1,2,3,4,5 };
	int b[] = { 2,3,4,5,6 };
	int c[] = { 3,4,5,6,7 };
	int* arr[3] = {a,b,c};
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			//printf("%d ", *(arr[i] + j));
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}


模拟了一个二维数组 ,但是其实并不是二维数组 ,因为二维数组每一行在内存中是连续存放的

10.数组指针

能够指向数组的指针
存放的是数组的地址

int main()
{
	int arr[10] = { 1,2,3,4,5 };
	int(*parr)[10] = &arr;
	return 0;
}

&arr 取出的是数组的地址
parr是一个数组指针,其中存放的是数组的地址

  • 数组传参,数组接收

void print1(int arr[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 ", arr[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 } };
	print1(arr, 3, 5);
	return 0;
}
  • 用数组指针传参
void print2(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 } };
	print2(arr, 3, 5);//arr数组名,表示数组首元素的地址
	return 0;
}

arr数组名 ,表示数组首元素的地址
二维数组的数组名表示首元素地址 , 二维数组的首元素是第一行

在这里插入图片描述

10.1&数组名VS数组名

数组名是数组首元素的地址

但是有2个例外:

  1. sizeof(数组名) - 数组名表示整个数组,计算的是整个数组大小,单位是字节
  2. &数组名 - 数组名表示整个数组,取出的是整个数组的地址
int main()
{
	int arr[10] = { 1,2,3,4,5 };
	printf("%p\n", arr);
	printf("%p\n", &arr);

	return 0;
}

在这里插入图片描述

数组名代表首元素地址
&arr 取出的是数组的地址,数组也有起始位置,虽然值相同 ,但是类型不一样


在这里插入图片描述


int main()
{
	int arr[10] = {0};

	int* p1 = arr;
	int (*p2)[10] = &arr;

	printf("%p\n", p1);
	printf("%p\n", p1+1);

	printf("%p\n", p2);
	printf("%p\n", p2+1);


	return 0;
}

p1是整形指针 ,整形指针+1 跳过4个字节
p2是数组指针 ,数组指针+1跳过一个数组


既然知道了数组指针和指针数组 ,那我们看一下下面的代码:

int arr[5]; // 1
int *parr1[10]; //2 
int (*parr2)[10]; // 3
int (*parr3[10])[5]; // 4

1 整型数组
2 整形指针的数组
3 数组指针 , 该指针指向一个数组 ,数组10个元素 ,每个元素的类型是int
4 parr3 是一个存放数组指针的数组 , 该数组能够存放10个数组指针 ,每个指针能够指向一个数组 ,数组有5个元素 ,每个元素的类型是int

11.数组参数、指针参数

11.1 一维数组传参

#include <stdio.h>
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[10] = { 0 };
    int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

11.2二维数组传参

void test(int arr[3][5]) //正确
{
}
void test(int arr[][])//err
{
}
void test(int arr[][5])//正确
{
}
void test(int* arr)//err
{
}
void test(int* arr[5])//err
{
}
void test(int(*arr)[5])//正确
{
}
void test(int** arr)//err
{
}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

11.3一级指针传参

void print(int* ptr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(ptr + i));
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//p是一级指针
	print(p, sz);
	return 0;
}

指针传参,形参用一级指针变量接收

void test(char* p)
{
}
int main()
{
	char ch = 'w';
	test(&ch);//char*
	return 0;
}
void test(char* p)
{
}
int main()
{
	char ch = 'w';
	char* p1 = &ch;
	test(p1);//char*
	return 0;
}

11.4二级指针传参

void test(int** p2)
{
	**p2 = 20;
}
int main()
{
	int a = 10;
	int* pa = &a;//pa是一级指针
	int** ppa = &pa;//ppa是二级指针
	//把二级指针进行传参呢?
	test(ppa);
	printf("%d\n", a);
	return 0;
}

二级指针传参 ,形参用二级指针接收

void test(int** p2)
{
	**p2 = 20;
}
int main()
{
	int a = 10;
	int* pa = &a;//pa是一级指针
	//把二级指针进行传参呢?
	test(&pa); //传一级指针变量的地址
	printf("%d\n", a);
	return 0;
}

传一级指针变量的地址 ,形参用二级指针接收

void test( int **p2)
{

}
int main()
{
	int* arr[10] = { 0 };
	test(arr);//传存放一级指针的数组
	return 0;

}

整形指针数组 ,每个元素都是int * , 数组名相当于首元素地址 ,首元素是int * , int *的地址就是 int **

12.函数指针

指向函数的指针
存放函数地址的指针
&函数名 ,取到的就是函数的地址

 int Add(int x, int y)
{
	 return x + y;
}
int main()
{
	int  (*pf) (int, int) = &Add;
		return 0;
}

pf就是函数指针变量

 int Add(int x, int y)
{
	 return x + y;
}


 int main()
{
	int (*pf)(int, int) = &Add;

	int (*pf)(int, int) = Add;//Add === pf
	int ret = (*pf)(3, 5);//1
	int ret = pf(3, 5);//2
	int ret = Add(3, 5);//3
	 
	int ret = * pf(3, 5);//err

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

1 2 3 得到的结果都是一样的


看两段来自《c陷阱和缺陷》的代码

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

代码一:

首先 代码中将0强制类型转换为类型为 void (*)() 的函数指针
然后去调用0地址处的函数

代码二:

该代码是一次函数的声明
声明的函数名字叫signal
signal函数的参数有2个,第一个是int类型,第二个是函数指针类型 (该函数指针能够指向的那个函数的参数是int,返回类型是void)
signal函数的返回类型是一个函数指针,该函数指针能够指向的那个函数的参数是int,返回类型是void

13.函数指针数组

存放函数指针的数组

函数指针数组的应用 :转移表

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

int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
void menu()
{
	printf("**************************\n");
	printf("***** 1.add   2.sub  ***&*\n");
	printf("***** 3 mul   4.div  *****\n");
	printf("******  0 .exit   ********\n");
 }
int main()
{
	int input = 0;
	do
	{
		
		menu();
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);
		int (*pfArr[5])(int, int) = { NULL ,Add,Sub ,Mul ,Div };
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数\n");
			scanf("%d %d", &x, &y);
		int ret = (*pfArr[input] )(x, y);
		printf("%d", ret);

		}
		else if (input == 0)
		{
			printf("退出程序\n");
			break;
		}
		else
		{
			printf("选择错误 ,请重新选择\n");
		}
	} while (input);
	return 0;
}

14.指向函数指针数组的指针

指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针


int main()
{
	int (* p1)(int, int);
	int (* p2[4])(int, int);
	 int ( * (*p3)[4])(int ,int )=&p2;
	return 0;
}

p1是函数指针
p2是函数指针的数组
&p2 取出的是函数指针数组的地址
p3 就是一个指向【函数指针的数组】的指针

15.回调函数

在这里插入图片描述

void qsort(void* base, 
			size_t num, 
			size_t size,
			int (*cmp)(const void* e1, const void* e2)
			);

base中存放的是待排序数据中第一个对象的地址
size_t num 排序数据元素的个数
size_t_size 排序数据中一个元素的大小 ,单位是字节
int (cmp)(const void e1, const void* e2) 是用来比较待排序数据中2个元素的函数

15.1 整形数据的排序

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int cmp_int( const void* e1 ,const void* e2)
{
	return  * (int *)e1 - * (int *)e2 ;
 }
int main()
{
	int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr, sz);

	return 0;
}

15.2 冒泡排序的通用算法

int cmp_int( const void* e1 ,const void* e2)
{
	return  * (int *)e1 - * (int *)e2 ;
 }
void Swap( char *buf1 ,char * buf2 ,int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf2++;
		buf1++;
	}
}
void bubble_sort(void * base,int sz , int width,int (*cmp)(const void *e1, const void *e2)	)
{
	//躺数
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			//比较
			if (cmp(    (char*)base+j*width   ,  (char *)base+(j+1)*width   ) > 0)
				//交换
			{
				Swap(   (char*)base + j * width, (char*)base + (j + 1) * width ,width  );
			}
		}
	}
  }


void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
		int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr, sz);
	return 0;
}

void* 是无具体类型的指针 ,解引用不知道访问几个字节 ,+1 也不知道跳过几个字节,强制类型转换为char* 就是一个很好的解决办法

在这里插入图片描述

如果你觉得这篇文章对你有帮助,不妨动动手指给点赞收藏加转发,给鄃鳕一个大大的关注
你们的每一次支持都将转化为我前进的动力!!!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐