嵌入式养成计划-8-C语言:指针、函数参数、多级指针、指针数组、数组指针、指针函数、函数指针、通用类型指针(void *)

十八、指针

作用		:	指针可以指向内存的每一块地址,加快运行速度,可以是程序简洁,节省空间。
指针		:	表示每个字节的编号,也称为地址
指针变量	:	存储指针的一个容器

在这里插入图片描述

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[]; // 参数字符串,传的内容,用指针字符数组接收
      在这里插入图片描述

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了
思维导图2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhk___

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值