十八、指针
作用 : 指针可以指向内存的每一块地址,加快运行速度,可以是程序简洁,节省空间。
指针 : 表示每个字节的编号,也称为地址
指针变量 : 存储指针的一个容器
18.1 指针的定义与占用空间
指针的定义:
格式: 存储类型 数据类型 *指针变量名
解析:
存储类型 : auto\static\extern\const\volatile\register
数据类型 : 基类型、构造类型、指针类型、空类型
* : 指针的标志
指针变量名 : 满足命名规范
int *a; //指针类型:int * int:该指针变量指向地址所对应值的数据类型
//int:表示接下来指针的偏移量字节大小
float *b;
double *c;
char *d;
void *e; //空指针,可以指针任意数据类型的地址,但是在使用时需要强制转换
指针的字节:
指针的大小与类型无关,只与操作系统的位数有关:
64位操作系统,指针占8字节
32位操作系统,指针占4字节
16位操作系统,指针占2字节
解释:
因为指针变量里面保存的是地址,
例如32位系统,它的寻址能力大小是2^32,
即,32位系统中,地址用32个bit位表示,
而32个bit位就是四个字节(4*8=32)
同理,16位的操作系统与64位操作系统也是这么计算
18.2 指针的定义、初始化及使用
指针的定义与初始化:
1.指针指向变量的地址,建议类型一致,如果不一致类似小端存储
格式1:
int a=100;//0x20-0x23
int *p=&a;//0x20
格式2:
int a;
int *p;
p=&a;
2.已经初始化的指针可以为另一个指针初始化
int a;
int *p=&a;
int *q=p;
3.野指针 : 未初始化的指针直接使用
野指针:段错误,轻则计算机混乱,甚至死机
int *p;//没有初始化指针,计算机会随机指向一块地址
*p=10; 段错误
printf("*p=%d\n",*p);
4.当定义一个指针不明确指针那个地址,指向NULL
int *p=NULL;
NULL:表示0或者0号地址,地址存在,地址很小,小到不可以使用
常见的使用方式:作为返回值
5.错误初始化
int *p=100; 错误 指针只能指向地址
int a;int *p=a;
int *p;//野指针
int a=100;
*p=a;//修改失败,段错误
指针的使用:
* : 乘法、标识符,取值
& : 取地址,逻辑与,按位与
*和&互为逆运算
注意:int a=10; 输出(&(*a))不合法,不能对值再取值
带*表示值、不带*表示地址
总结:
int a=10;
int *p=&a;
值: a--->*p --->*&a --->*(&a)
地址 : &a--->p--->&*p --->&(*p)
18.3 指针的运算
- 算数运算
- 关系运算
- 赋值运算
18.3.1 算数运算
int a=100;
int *p=&a;
a : 表示100的值
*p : 表示100的值
p : 表示a的地址
&a : 表示a的地址
&p : 表示指针p的地址
+ | p+n | 向高地址方向偏移n个数据类型字节大小 |
---|---|---|
*p+n | 取值,对值加n | |
*(p+n) | 向高地址方向偏移n个数据类型字节大小,在取值 | |
&p+n | 向高地址方向偏移n个8字节大小 | |
&a+n | 向高地址方向偏移n个数据类型字节大小 | |
- | p-n | 向低地址方向偏移n个数据类型字节大小 |
*p-n | 取值,对值减n | |
*(p-n) | 向低地址方向偏移n个数据类型字节大小,在取值 | |
&p-n | 向低地址方向偏移n个8字节大小 | |
&a-n | 向低地址方向偏移n个数据类型字节大小 | |
++ | &a++ | 报错,操作数不是变量 |
int a; int *p=&a | ++p | 前缀运算,先偏移,后运算,向高地址方向偏移一个数据类型字节大小 |
p++ | 后缀运算,先运算,后偏移,向高地址方向偏移一个数据类型字节大小 | |
(*p)++ | 先取值,后缀运算,先运算值,后对值自增 | |
++*p | 先取值,前缀运算,先对值自增后运算 | |
*p++ | 先执行p++,但是后缀运算,先取值,后向高地址方向偏移一个数据类型字节大小 | |
*(p++) | 先执行p++,但是后缀运算,先取值,后向高地址方向偏移一个数据类型字节大小 | |
- - |
部分解释:
&p+n : 取 p 的地址,相当于对 p 升维了,而一个p占8字节,所以就是向高地址挪n个8字节。
其他的,如&p-n以及对 &a 的操作也是类似的原理
&a++ : 虽然 ++ 在右,本应先执行,但是因为 ++ 在右是先运算再自增,所以是先对 a 取地址,取地址的结果是个地址,地址是常量,所以会报错,++的操作数需要是变量
++p : 其值为 p 指向地址的下一个数据类型字节的地址;
先让 p 的地址自增,然后再进行操作,所以 p 指向原来地址往后 一个数据类型 的字节的地址
p++ : 其值为 p 指向的地址;
先进行操作,然后再对 p 自增,最后 p 指向原来地址往后 一个数据类型 的字节的地址
(*p)++ : 先对 p 进行解引用,然后得到的值 再与++ 结合,所以值为 p 指向的地址存放的数据,但是后面会再进行自增,自增后不会赋值给 等号 的左值;传到 等号 的左值的结果是p指向的地址上的值;传到 等号 的左值的结果是p指向的地址上的值
++*p : 因为是从右向左结合的,所以也是先取值再对值进行自增,这个是能够将自增的值赋给 等号 的左值;其传给 等号 左值的结果是p指向的地址上的值
*p++ : 因为从右向左结合,但是 ++ 是先运算再自增,所以这个的值为 p 指向的地址上的值,但是 p 在最后又会指向 往后一个数据类型所占字节长度 的地址,传到 等号 的左值的结果是p指向的地址上的值+1
*(p++) : 虽然也是从右向左而且加括号,但是也是因为 ++ 是先运算再自增,所以这个是的值是p指向地址上的值,之后 p 会指向往后一个数据类型字节长度的地址;其传给 等号 左值的结果是p指向的地址上的值
18.3.2 关系运算
- >
- >=
- <
- <=
- ==
- !=
指针多用来指向连续的存储空间
int a;
int b;
int *p=&a,*q=&b;
> | 大于 | p>q |
---|---|---|
>= | 大于等于 | p>=q |
< | 小于 | p<q |
<= | 小于等于 | p<=q |
== | 判断相等 | p==q |
!= | 判断不相等 | p!=q |
18.3.3 赋值运算
- =
- +=
- -=
= | int *p=&a | 等号两端必须一致,地址赋值指针,值赋值变量 |
---|---|---|
+= | p+=n | 向高地址方向偏移n个数据类型字节大小 |
-= | p-=n | 向低地址方向偏移n个数据类型字节大小 |
18.4 函数参数的值传递与地址传递
18.4.1 值传递
1. 传递的是值
2. 实参与形参所代表的存储空间不一样
3. 形参的改变不会影响到实参
4. 单向传递
18.4.2 地址传递
1. 传递的是地址
2. 实参与形参所代表的存储空间一样,或者说所指向的空间一样
3. 形参的改变能够影响到实参
下面这几个表格图,里面的地址是直接向下拉生成的,真正十六进制表示并不是这样,做个参考看看就行,别较真
18.5 指针和一维数组
数组名表示数组的首地址,也就是第一个元素的地址,数组名表示常量,
数组名不可以自增或自减【arr++\arr--\--arr\++arr\arr+=1】
int arr[]={11,22,33,44};
int *p = arr;
值等价:arr[i] -->p[i] -->*(arr+i) --->*(p+i) -->*(&arr[0]+i) --->*(&p[0]+i) --->*p++
地址等价:&arr[i] -->&p[i] -->arr+i --->p+i -->&arr[0]+i --->&p[0]+i --->p++
18.6 二维数组和指针
int *p=arr;//arr偏移12字节 p:偏移4
二维数组指针定义:数组指针
int arr[2][5]={0}
int (*p)[5]=arr; //arr:行偏移20字节 p行偏移20字节
int arr[2][3]={1,2,3,4,5,6};
int (*p)[3]=arr;
18.6.1 二维数组的指针解引用
int arr[2][3]={1,2,3,4,5,6};
int (*p)[3]=arr;
值 : arr[i][i] --->p[i][j] --->*(*(arr+i)+j) -->*(*(p+i)+j)--->
*(p[i]+j)-->*(arr[i]+j)--->*(*(&arr[0]+i)+j)-->*(*(&p[0]+i)+j)
地址 : &arr[i][i] --->&p[i][j] --->*(arr+i)+j-->*(p+i)+j--->
p[i]+j-->arr[i]+j--->*(&arr[0]+i)+j-->*(&p[0]+i)+j
18.7 数组指针
本质上还是一个指针,指向数组一整行的地址。
作用:数组指针主要用来指向二维数组,多用于二维数组传参以及返回二维数组的地址。
int arr[2][3]={1,2,3,4,5,6};
int (*p)[3]=arr;
定义格式:数据类型 (*指针变量名)[常量表达式]
优先级: () > [] >*
int arr[2][3];
int (*p)[3]=arr;
数据类型:基本类型、构造类型、指针、空
():不可以省略
*: 表示指针
[]: 表示数组
常量表达式:必须和二维数组列数保持一致
示例:
int a[2];
int *p1=a;
int a[2][3];
int (*p)[3]=a;
int b[2][3][4];
int (*q)[3][4]=b;
18.8 一维字符数组和指针
char str[10];
char *p=str;
- 指针指向字符数组的地址
可以通过指针修改字符数组的内容
不同字符数组存储的内容一样,他们的地址也不一样
- 指针指向字符串常量的地址
多个指针指向同一内容的字符串常量时,这些指针指向的地址一样
指针指向字符串常量时,不能通过指针修改字符串常量,因为字符串常量存储在只读数据区
18.9 指针数组
- 本质上是一个数组,数组中的元素为同一类型的指针。
定义格式 : 数据类型 *数组变量名[常量表达式]
优先级 : []>*
数据类型 : 基本类型、构造类型、空类型、指针类型
* : 表示指针
数组变量名 : 满足命名规范
[] : 表示数组
常量表达式 : 表示指针的个数
示例:
int a1,a2,a3;
int *a[3]={&a1,&a2,&a3}; // 数组 a 中有三个元素,每个元素都是整型指针
字符指针数组
1. 字符指针数组存储多个字符数组变量的地址
int main(int argc, const char *argv[])
{
// char str[3][10]={"123","asdfgh","0"};
//二维数组存储多个字符串,浪费空间
#if 0
char a[]="123";
char b[]="asdfgh";
char c[]="0";
// printf("%s",a);
char *p[3]={a,b,c};
// p[0] p[1] p[2]
// *p *(p+1) *(p+2)
for(int i=0;i<3;i++)
{
printf("%s\n",*(p+i));
}
**(p+1)='A';
puts(*(p+1));
}
2. 字符指针数组存储多个字符串常量的地址
char *p[3]={"123","ASDFGH","!@#$"};
for(int i=0;i<3;i++)
{
printf("%s\n",*(p+i));
}
**(p+1)='a';//修改只读区的内容,段错误
puts(*(p+1));
- main函数中的参数
-
- int argc; // 参数个数,./a.out也算上
-
- const char *argv[]; // 参数字符串,传的内容,用指针字符数组接收
- const char *argv[]; // 参数字符串,传的内容,用指针字符数组接收
18.10 指针函数
- 本质还是一个函数,只是修改了返回值的形式,返回一个地址。
定义格式:
数据类型 *函数名(参数列表)
{
函数体;
return 地址;
}
不可以返回局部变量的地址:
局部变量调用函数申请空间,函数调用结束空间释放,
一旦返回局部变量的地址就会变为野指针
解决方案:
转换为全局变量
数组做参数
指向堆区的空间
//不可以返回局部变量的地址:局部变量调用函数申请空间
//函数调用结束空间释放,一旦返回局部变量的地址
//就会变为野指针
int * fun()
{
int a=100;
int b=10;
//返回a、b
int arr[2];//从定义开始到本函数结束 0x10
arr[0]=a;arr[1]=b;
return arr;//返回 数组的地址 int * 0x10
}
//解决方式1:数组转换为全局变量
int arr[2];//全局变量18--36
int * fun1()
{
int a=100;
int b=10;
//返回a、b
arr[0]=a;arr[1]=b;
return arr;//返回 数组的地址 int * 0x10
}
//解决方式2:数组做参数
int * fun2(int *arr)//0x55
{
int a=100;
int b=10;
//返回a、b
*arr=a;*(arr+1)=b;
return arr;//返回 数组的地址 int * 0x55
}
//解决方式3:指向堆区的空间
//堆区:程序员手动申请malloc,手动释放free
int * fun3()//0x55
{
int a=100;
int b=10;
//返回a、b
int *p=(int *)malloc(8);//返回堆区申请的首地址
*p=a;
*(p+1)=b;
return p;//返回 数组的地址 int * 0x55
}
int main(int argc, const char *argv[])
{
// int *p=fun();//0x10
// int *p=fun1();
// int arr[2];//arr:内存在main函数 0x55
// int *p=fun2(arr);//0x55
int *p=fun3();
printf("*p=%d\n",*p);
printf("*(p+1)=%d\n",*(p+1));
return 0;
}
//解决方式4:static修饰局部变量延长证明周期
//static 修饰的局部变量 在静态区的data段
int * fun3()//0x55
{
int a=100;
int b=10;
//返回a、b
static int arr[2];
arr[0]=a;arr[1]=b;
return arr;//返回 数组的地址 int * 0x55
}
int main(int argc, const char *argv[])
{
// int *p=fun();//0x10
// int *p=fun1();
// int arr[2];//arr:内存在main函数 0x55
// int *p=fun2(arr);//0x55
int *p=fun3();
printf("*p=%d\n",*p);
printf("*(p+1)=%d\n",*(p+1));
return 0;
}
18.11 函数指针
- 本质上还是一个指针,指向了函数的首地址,即函数名。
- 可以指向同一类的函数,即返回值和参数列表相同
- 函数地址上的内容是函数的地址,函数地址的地址也是函数的地址
- 可以通过对函数取地址调用函数,也可以通过对函数解引用调用函数
只要返回值形式和参数列表符合指针中定义的格式,那么这个函数指针就可以进行指向。
格式:
返回值类型 (*函数指针名)(函数的形参列表);
示例:
int (*p)(int , int ); //定义了一个函数指针,可以指向返回值为 int 形参列表为 (int, int)的函数
int (*p1)(int, int) = my_add; //函数名就是函数的地址
int (*p2)(int, int) = my_sub;
当函数指针指向函数后,通过函数指针也可以调用函数,
初始化时也可以用 NULL 初始化
- 函数指针:多用于回调函数
- 回调:函数名作实参,函数指针作形参
回调过程如下:
回调函数的应用场景:
较大的工程,当某一功能不明确具体实现,但是明确返回值类型和参数类型时,可以先定义一个函数指针,放在该位置,等到功能具体实现时,把函数传进调用位置。
- 代码示例:
#include <stdio.h>
int add(int a,int b)
{
return a+b;
}
int sub(int a,int b)
{
return a-b;
}
//这个函数指针指向的是:返回类型为int,参数为两个int的函数,具体需要看传过来的函数名是啥
int jisuan(int a,int b,int (*p)(int,int))
{
return p(a,b);
}
int main(int argc, const char *argv[])
{
int num1 = 11, num2 = 2;
int m = 20, n = 10;
//函数指针指向了加法函数
int ret = jisuan(num1,num2,add)
printf("%d\n",ret);
//函数指针指向了减法函数
int ret1 = jisuan(num1,num2,sub);
printf("%d\n",ret);
//定义了一个函数指针p,指向返回值类型为int,参数为两个int的函数,指向了add函数
int (*p)(int ,int)=add;
int ret = jisuan(num1,num2,p); //调用计算函数,传了一个指向add函数的函数指针 98
int ret1 = jisuan(num1,num2,sub); //82
printf("%d\n",ret);
return 0;
}
18.12 函数指针数组
- 本质是一个数组,数组中每个元素都是一个函数指针
格式:
返回值类型 (*数组名[长度])(形参列表);
例如:
int (*s[2])(int,int);
//定义了一个函数指针数组
//数组名叫做 s ,数组中有俩元素
//每个元素都是一个能指向 (返回值为 int ,形参列表为(int, int))的函数指针
//给数组元素赋值:
s[0] = my_add;
s[1] = my_sub;
//使用数组元素调用函数
printf("%d\n",s[0](20,10));//30
printf("%d\n",s[0](20,10));//10
18.13 指向函数指针数组的指针
- 本质是个指针,指向了函数指针数组的首地址
格式:
返回值类型 (*(*数组名))(形参列表);
int (*p1)(int, int) = my_add;//函数指针
int (*s[3])(int, int);//函数指针数组
//定义一个指针,指向函数指针数组
int (*(*p))(int, int) = s;//s是上面定义的函数指针数组名
//通过指针调用函数
printf("%d\n",p[0](20,10));//30
printf("%d\n",p[0](20,10));//10
18.14 多级指针
- 指向指针的指针,多级指针里面存放的是上一级指针的地址
格式:
存储类型 数据类型 **指针名; // 是几级指针就几个星号 "*"
示例:
int a=1; // 变量
int *p=&a; // 一级指针指向变量的地址
int **pp=&p; // 二级指针指向一级指针的地址
int ***ppp=&pp; // 三级指针指向二级指针的地址
.
.
.
还可以继续套娃...
使用时按需要逐级解引用就可以了,解引用别忘了是加星号 "*"
18.15 通用类型指针
- 就是空指针类型
格式:
void *指针变量名
特点:可以指向任何类型的地址,但是在使用是必须类型强转。
代码示例:
int main(int argc, const char *argv[])
{
int a=100;
void *p=&a;
printf("*p=%d\n",*(int *)p);
return 0;
}
作业 1
解析程序,回答输出结果
解读程序1:值传递和地址传递
void fun(int a,int b)
{
int t=a;a=b;b=t;
printf("a=%d b=%d\n",a,b);
}
int main()
{
int a=10,b=100;
fun(a,b);
printf("a=%d b=%d\n",a,b);
return 0;
}
解读程序2:值传递和地址传递
void fun(int *a,int *b)
{
int t=*a;*a=*b;*b=t;
printf("*a=%d *b=%d\n",*a,*b);
}
int main()
{
int a=10,b=100;
fun(&a,&b);
printf("a=%d b=%d\n",a,b);
return 0;
}
解读程序3:值传递和地址传递
void fun(int *a,int *b)
{
int *t=a;a=b;b=t;
printf("*a=%d *b=%d\n",*a,*b);
}
int main()
{
int a=10,b=100;
fun(&a,&b);
printf("a=%d b=%d\n",a,b);
return 0;
}
下面是我做的:
// 解读程序1:值传递和地址传递
void fun(int a, int b) // 采用了值传递的方式
{
// 使用了三杯水交换的方式,
// 然而并没有什么卵用
// 因为都是局部变量
int t = a;a = b;b = t;
// 输出:100 10
printf("a=%d b=%d\n", a, b);
}
int main()
{
//定义a、b并初始化
int a = 10, b = 100;
//调用函数,并传入值
fun(a, b);
//输出:10 100
printf("a=%d b=%d\n", a, b);
return 0;
}
// 解读程序2:值传递和地址传递
void fun(int *a, int *b) // 采用地址传递的方式,用指针变量进行接收
{
//采用三杯水交换,
//因为通过 解引用 交换
//所以能够做到将实参改变
int t = *a;*a = *b;*b = t;
//输出:100 10
printf("*a=%d *b=%d\n", *a, *b);
}
int main()
{
//定义a、b并初始化
int a = 10, b = 100;
//调用函数,并传入地址
fun(&a, &b);
//输出:100 10
printf("a=%d b=%d\n", a, b);
return 0;
}
// 解读程序3:值传递和地址传递
void fun(int *a, int *b)// 采用地址传递的方式,用指针变量进行接收
{
//采用三杯水交换,
//因为通过 交换地址
//但 a 与 b 都是局部变量,
//它们的地址也是新生成的
//它们的值才是实参a与b的地址
//所以不能将实参改变
int *t = a;a = b;b = t;
//输出:100 10
printf("*a=%d *b=%d\n", *a, *b);
printf("a = %p\n",&a);
printf("b = %p\n",&b);
}
int main()
{
//定义a、b并初始化
int a = 10, b = 100;
printf("a = %p\n",&a);
printf("b = %p\n",&b);
//调用函数,并传入地址
fun(&a, &b);
//输出:10 100
printf("a=%d b=%d\n", a, b);
printf("a = %p\n",&a);
printf("b = %p\n",&b);
return 0;
}
作业 2
1.通过指针实现杨辉三角
void YangHui(int n,int (*p)[n])
2.通过字符指针实现字符串连接
void my_strcat(char *dest,char *src)
下面是我写的,老样子,代码在后
1.
2.
代码:
1.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void my_strcat(char *dest, const char *src);
int main(int argc, const char *argv[])
{
char s1[32] = {"hello"};
char s2[32] = "world";
my_strcat(s1, s2);
puts(s1);
return 0;
}
void my_strcat(char *dest, const char *src)
{
char *p = dest;
while (*p)
p++;
int i = 0;
while (*(src + i))
{
*p = *(src + i);
p++;
i++;
}
*p = *(src + i);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NUM 10
void yanghui(int n, int (*a)[n]);
void print_arr(int n, int (*a)[n]);
int main(int argc, const char *argv[])
{
int a[NUM][NUM] = {1};
yanghui(NUM, a);
print_arr(NUM, a);
return 0;
}
void yanghui(int n, int (*a)[n])
{
int(*p)[n] = a;
for (int i = 1; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (0 == j)
{
*(*(p + i) + j) = *(*(p + i - 1) + j);
}
else
{
*(*(p + i) + j) = *(*(p + i - 1) + j) + *(*(p + i - 1) + j - 1);
}
}
}
}
void print_arr(int n, int (*a)[n])
{
int(*p)[n] = a;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (0 == *(*(p + i) + j))
{
printf("\t");
}
else
{
printf("%d\t", *(*(p + i) + j));
}
}
printf("\n");
}
printf("\n");
}
一个图放不下,超过5MB了