指针
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)数组名的理解
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)字符指针变量–指向字符的指针
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
⽬录
- 回调函数是什么?
- qsort使⽤举例
- 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
⽬录
- sizeof和strlen的对⽐
- 数组和指针笔试题解析
- 指针运算笔试题解析
(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.二维数组