C语言--深入理解指针

指针

Part 1

内存和地址

内存单元的编号 = 指针 =地址 (指针就是地址 地址就是指针 )
就如字面意思 可以理解为 指向某一个空间的箭头 通过指针就能找到指向的内容 从而对他进行操作

1.1.2 指针变量和地址

(1) 取地址操作符(&)

C语言中 创建变量就是像内存申请空间如
在这里插入图片描述
可以看到 &a 就是所以 p确实是存放了a的地址
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/28d6eeb2421c409e8a063acddff3ded8.png在这里插入图片描述

虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可 ⾏的。
p会指向a的低地址处 然后顺着找到a

(2)指针变量和解引⽤操作符(*)

如 int a=0;我们要把a的地址存起来 那就需要一个相应的指针变量去存放a的地址
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
不同类型的变量都有相应类型的指针变量去对应 如 int a=0;那几需要int* p=&a;去存放a的地址
就是在所存变量的类型前面加一个*
在这里插入图片描述
*** 自定义类型也没有区别 如 有一个结构体变量 那么就要用一个结构体指针去存储他的地址
通过* 操作符就可以找到指向的变量 并对其进行操作
在这里插入图片描述
其实指针变量和其他变量也没有太大区别 只是存储的内容是地址 因为存的是别人的地址 所以*(解引用)就可以找到

(3)指针变量的大小

简单概括 : 32位平台 —4个字节 64位平台—8个字节 所有的指针都是一样的 *所有 所有 所有 重要的事情说3遍
详细:32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4 个字节才能存储。 如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。 同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变的⼤⼩就是8个字节
在这里插入图片描述
在这里插入图片描述
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的

(4)指针变量的意义

指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
如下图
在这里插入图片描述
在这里插入图片描述

(5)指针的运算

1.指针加减整数

在这里插入图片描述
在这里插入图片描述
指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化

2.指针-指针

注意看 没有指针+指针 只有-
如图
在这里插入图片描述
所以 指针-指针的结果是 他们之间的元素个数 注意 是分正负的 低地址的指针-高地址的指针结果为负
只有指向同一区域且类型完全相同的指针才能够相减 所以一般可以用在数组

3.指针的关系运算

指针也分大小 指向低地址的指针比指向高地址的指针的值要小

(6)void* 类型指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 ⾏指针的±整数和解引⽤的运算。
void* 类型的指针可以存放任意类型的地址 但是他不能解引用 也不能进行加减等任何运算 因为他解引用访问的空间大小是不确定的 不像int一次访问4个字节
⼀般 void
类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址

3.const修饰指针

const a=5;意味着把a变量加了一个常属性 a的值是不能够被修改的 (本质a还是一个变量 只是有了常属性)
同样的 const修饰指针也是同理 代表指针不能被修改 但const修饰指针有两种类型
1.const在左侧
如 const int
p;或 int const* p; 两者是一样的 在的左侧代表这修饰的是p 所以p是不能够被改变的
但 p是可以改变的 —也就是p可以改变指向
2. const 在
右侧
如 int * const p;代表这const修饰的是p 而不是p 所以 p是不能改变的—不能改变指向 但p 不受const影响
3. 的左侧右侧都有const
如 const int
const p;可以看到 const既修饰了p的修饰了p 所以 p和p都不能改变

(7)野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
在我们的程序中 要坚决避免出现野指针
野指针的成因分为几种
1.指针未初始化
在这里插入图片描述
2.指针越界访问
如图
在这里插入图片描述
3.指针指向已被释放的空间
在这里插入图片描述
总的来说 如果指针没有明确的指向 那他就是野指针

如何避免野指针?

1.初始化指针
通常情况下 没有明确的指向 可以把指针初始化位NULL 即指针指向空
在这里插入图片描述
2.避免指针越界
3.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
4. 避免返回局部变量的地址

(7)

assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为“断⾔”。
如 assert(p != NULL);
意思是 如果p不为空 那么什么都不发生 程序正常往下走
assert括号内的条件 是程序继续进行的条件 只有满足括号中的条件 程序才会继续进行
不满足时程序会报错
在这里插入图片描述

可以看到 如果assert成功断言 那么他会报错并告诉你是在哪里出错
所以 assert可以多多使用 可以及时的发现错误并改正

(函数的传值调用和传址调用)

在这里插入图片描述
可以看到 如果函数传递的是参数的地址 那么就可以在函数中改变参数
所以 如果想通过函数改变参数的内容 那么就可以利用传址调用
典型的就是swap交换函数
如果传值调用----------实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

Part 2

⽬录

  1. 数组名的理解
  2. 使⽤指针访问数组
  3. ⼀维数组传参的本质
  4. 冒泡排序
  5. ⼆级指针
  6. 指针数组
  7. 指针数组模拟⼆维数组

(1)数组名的理解

int arr[10]={0};
数组名代表数组首元素的地址 所以 arr指向的是arr[0]的地址
在这里插入图片描述
但 有两种情况 数组名代表的是整个数组的地址 (只有这两种情况 ,其他时候都代表的是首元素的地址)
1.&arr 取出的是整个数组的地址
2.sizeof(arr)计算的是整个数组的大小
如图
在这里插入图片描述

(2)使用指针访问数组

如图
在这里插入图片描述

可以看到 指针可以通过一下方式访问并操作数组

(3) 一维数组传参的本质

在这里插入图片描述
ps:我觉得 文字都放到图片里 大家会看的更方便看 所以 就试试
⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

(4)冒泡排序

冒泡排序的核⼼思想就是:两两相邻的元素进⾏⽐较
在这里插入图片描述
可以对这个bubblesort进行一个小优化 为——>

void BubbleSort(int* arr, size_t sz)
{
	for (int i = 0; i < sz - 1; ++i)
	{
	       int exchange =0//0代表下一趟没有交换 1代表有交换
		for (int j = 0; j < sz - 1 - i; ++j)
		{
			if (arr[j] > arr[j + 1])
			{
				swap(&arr[j], &arr[j + 1]);
				exchange=1;
			}
		}if(exchange==0)//意味着这一趟没有进行任何交换 也就是已经排序完了 那么就可以提前出去了
		{
		break;
		}		
	}
}

(5)二级指针

有两个**的指针 就是二级指针 但 无论多少级的指针 含义都和一级指针一样
在这里插入图片描述

(6)指针数组

是存放指针的数组 (如果是 数组指针 那就是指向数组的指针)
同样的 存放int的指针就用 int [ ] 类型的数组
在这里插入图片描述

(7)指针数组模拟二维数组

如图
在这里插入图片描述
因为可以像二维数组一样访问 所以就可以模拟二维数组

part 3

⽬录

  1. 字符指针变量
  2. 数组指针变量
  3. ⼆维数组传参的本质
  4. 函数指针变量
  5. 函数指针数组
  6. 转移表

(1)字符指针变量–指向字符的指针

1.指向单个字符

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

2.指向字符串`

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

pstr并没有存放整个字符串 而是存放他的首个字符的地址 也就是h的地址 再通过h的地址就可以找到整个字符串
在这里插入图片描述

上⾯代码的意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中

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

在这里插入图片描述
在这里插入图片描述

(2)数组指针----是指针 指向数组的指针

eg:
int arr[3] 要存放arr整个数组的地址
就这样表示
int (p)[3]=&arr;
p被括号括起来 代表p是指针 后遍的[3]代表指向的是数组 数组中有三个元素 每个元素的类型是int
若有 int
arr[3] ;
要存放arr的地址 就要这样表示
int
*(*p)[3]
比较
int *p1[10];-----p没有用括号括起来 那么p优先与[ ]结合 代表p是数组 数组中有10个元素 每个元素类型为int
int (*p2)[10];-----如上
在这里插入图片描述

(3) 二维数组传参本质

在这里插入图片描述
⼆维数组传参本质上也是传递了地址,传递的是第⼀ ⾏这个⼀维数组的地址

⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式

(4) 函数指针变量—指向函数的指针变量(存放函数的地址)

1.函数指针变量的创建
#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("test: %p\n", test);
 printf("&test: %p\n", &test);
 return 0;
 }

在这里插入图片描述
可以看到 函数的名字就是函数的地址 &test与test是一样的
存放函数的地址 就要用到函数指针 比如要存放test的地址 就要这样定义指针
void(*p)()=test;
或者 void(*p)()=&test;
*p用括号括起来 代表p是指针 旁边的括号代表指向的是函数 括号内容为空代表函数没有参数 最后左侧的void代表 函数的返回类型是void
再比如 有一个 int Add(int a, int b);这样的函数 用p存放 他的地址 就是这样表示
int (*p)(int ,int )=Add;//这里 函数括号里的参数 只需要写出类型就可以 加上变量也可以–>
int (*p)(int a,int b)=Add;

在这里插入图片描述

2. 函数指针变量的使⽤
#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)(2, 3));
   printf("%d\n", (***pf3)(2, 3));
   ***//无论pf3加不加* 都是一样的效果*** 
 printf("%d\n", pf3(3, 5));
 return 0; }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ps:typedef 关键字

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了 —>typedef unsigned int uint;

(5) 函数指针数组—是数组 存放的是函数指针

在这里插入图片描述

(6) 转移表

eg:计算器的实现
在这里插入图片描述

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(*calc[5])(int ,int) = {0,Add,Sub,Mul,Div};
	printf("1.加法\n");
	printf("2.减法\n");
	printf("3.乘法\n");
	printf("4.除法\n");
	int input = 0;
	int x = 0, y = 0;
	scanf("%d", &input);
	scanf("%d%d" ,&x,&y);
	printf("%d \n", calc[input](x, y));

	
	return 0;
}


可以看到 这样使用函数指针数组 就可以简化很多重复的代码

Part 4

⽬录

  1. 回调函数是什么?
  2. qsort使⽤举例
  3. qsort函数的模拟实现

(1) 回调函数是什么

回调函数就是⼀个通过函数指针调⽤的函数。
使用函数指针调用了一个函数 那么这个函数就叫回调函数
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数 时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条 件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。
在这里插入图片描述

(2)qsort使用

使用cplusplus查看可以发现
在这里插入图片描述
qsort函数
返回类型 : 为void
参数一 void* base 是要排序的元素起始位置的地址
参数二 size_t num 是要排序多少个元素
参数三 size_t size; 是每个排序元素的大小
参数四 int(compar)(const void, void* ) 是个函数指针 返回类型int 参数为 const void* 和void*
这个函数指针 是接收 比较函数的地址 这个比较要我们自己实现
在这里插入图片描述
这个比较函数 返回1代表 参数一大于参数二 0是相等
比如要比较 int a ,和int b
要这样定义

int compar(const void* p1,void* p2)
{
 return *(int*)p1 - *(int*)p2;
}
int cmp_int(const void* p1,const* p2)
{
  return *(int*)p1-*(int*)p2;
}

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

在这里插入图片描述
可以看到 排序的结果默认是从小到大 但只要把比较函数中 return (int)p1-(int)p2;改为(int*)p2);就会变成从大到小的顺序
qsort的速度非常块 性能很高 不然怎么叫 qsort呢?

(3)qsort模拟实现

目前学过 bubblesort的冒泡排序 所以我们可以用bubblesort模拟实现这个可以排序任意类型元素的函数
和qsort一样 bubblesort改为
void BubbleSort(void* base,size_t num,size_t width,int(compar)(const void* ,void*));
注意 第三个参数改为 了 width 代表着每个元素的宽度 因为是接收任意类型的参数 所以不知道大小 用width表示宽度 以便于之后的操作
eg:

在这里插入代码片#include"slist.h"

void Swap(char* p1, char* p2,size_t width)
{
	for (int i = 0; i < width; ++i)
	{
		char p3 = *p1;
		*p1 = *p2;
		*p2 = p3;
		++p1;
		++p2;
	}
}

int cmp(void* p1, void* p2)
{
	return (*(int*)p1) - (*(int*)p2);
}

void BubbleSort(void* base, size_t num, size_t width, int(*cmp)(void*, void*))
{
	for (int i = 0; i < num - 1; ++i)
	{
		for (int j = 0; j < num - 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 PrintArr(int* arr, size_t sz)
{
	for (int i = 0; i < sz; ++i)
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	int arr[] = { 1,2,34,54,3,543,6,45,7,65,7,65,34,243, };
	size_t sz = sizeof(arr) / sizeof(int);
	BubbleSort(arr, sz,sizeof(int),cmp);
	PrintArr(arr, sz);

	return 0;
}

Part 5

⽬录

  1. sizeof和strlen的对⽐
  2. 数组和指针笔试题解析
  3. 指针运算笔试题解析

(1) sizeof和strlen的对⽐

sizeof 计算的是变量(或类型)所占内存空间的大小 单位是字节
sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。

#inculde <stdio.h>
int main()
{
 int a = 10;
 printf("%d\n", sizeof(a));
 printf("%d\n", sizeof a);
 printf("%d\n", sizeof(int));
 
 return 0; }

strlen功能是求字符串的长度 不算’\0’并且遇到’\0’就会停止
函数原型如下
size_t strlen ( const char * str );
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数
strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。

#include <stdio.h>
int main()
{
 char arr1[3] = {'a', 'b', 'c'};
 char arr2[] = "abc";
 printf("%d\n", strlen(arr1));
 printf("%d\n", strlen(arr2));
 
 printf("%d\n", sizeof(arr1));
 printf("%d\n", sizeof(arr1));
 return 0;
 }

在这里插入图片描述
在这里插入图片描述

(2)数组和指针笔试题解析

1.⼀维数组

int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));

在这里插入图片描述

2. 字符数组

在这里插入图片描述

1 char arr[] = {'a','b','c','d','e','f'};
2 printf("%d\n", sizeof(arr));
3 printf("%d\n", sizeof(arr+0));
4 printf("%d\n", sizeof(*arr));
5 printf("%d\n", sizeof(arr[1]));
6 printf("%d\n", sizeof(&arr));
7 printf("%d\n", sizeof(&arr+1));
8 printf("%d\n", sizeof(&arr[0]+1));
//代码2 
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));

在这里插入图片描述
在这里插入图片描述

3.二维数组

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值