【C语言】指针详解

1. 基本指针类型说明

int p; //这是一个普通的整型变量

int * p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针

int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组

int * p[3]; //首先从P 处开始,先与[]结合,因为其优先级比* 高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组

int (*p)[3]; //首先从P 处开始,先与 *结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针

int * *p; //首先从P 开始,先与 *结合,说是P 是一个指针,然后再与 *结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.

int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据

Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针

int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的 *结合,说明函数返回的是一个指针,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与 *结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

2. 什么是指针?

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。

  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。

  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。

  4. 指针的运算。

总结:指针就是地址,口语中说的指针通常指的是指针变量

指针变量

我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个

变量就是指针变量

总结:

指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

那这里的问题是:

一个小的单元到底是多大?(1个字节)

如何编址?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);

那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111

这里就有2的32次方个地址。

每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==

232/1024/1024MB==232/1024/1024/1024GB == 4GB) 4G的空闲进行编址。

同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。

这里我们就明白:

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以

一个指针变量的大小就应该是4个字节。

那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结:

指针是用来存放地址的,地址是唯一标示一块地址空间的。

指针的大小在32位平台是4个字节,在64位平台是8个字节

3.指针和指针类型

这里我们在讨论一下:指针的类型

我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?

准确的说:有的。

指针类型的意义:指针类型决定了指针进行解引用操作时,能够访问空间的大小

int * p;* p可以访问4个字节

char * p;* p可以访问1个字节

double * p;* p可以访问8个字节

1. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

一般使用:

int main()
{
    char ch = 'w';
    char *p = &ch;
    const char* p2="abcd";//"abcd"是字符串常量,不可改变,所以一般加个const保护
    *p = 'w';
    return 0; 
}

C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。

总结:指针的类型决定了指针向前或者向后走一步有多大(指针的步长)
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

4. 如何规避野指针

  1. 指针初始化

  2. 小心指针越界

  3. 指针指向空间释放即使置NULL

  4. 避免返回局部变量的地址

  5. 指针使用之前检查有效性

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

5. 指针运算

1.指针± 整数

#include<stdio.h>

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

2.指针-指针
得到的是两个地址之间的元素个数

注意:这两个指针应该指向同一个地址空间

int my_strlen(char *s) 

 {

   char *p = s;

    while(*p != '\0' )

        p++;

    return p-s; 

}

3.指针的关系运算

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

代码简化, 这将代码修改如下:

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) {
    *vp = 0; 
}

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证

它可行。

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

6. 数组指针

数组指针是指向数组的指针,可以存放数组的地址。

如何定义一个数组指针

int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

1. &数组名和数组名

#include <stdio.h>
int main()
{
    int arr[10] = {1,23,4,5};
 printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    printf("%d\n", *arr);
    //输出结果
    return 0; }

结论:

数组名是数组首元素的地址。(有两个例外)

如果数组名是首元素地址,那么:

int arr[10] = {0};
printf("%d\n", sizeof(arr));

为什么输出的结果是:40?

补充:

  1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
  1. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。

int a[10] = { 0 };
	printf("&a[0] = %p\n", &a[0]);//数组首元素的地址
	printf("&a[0] + 1 = %p\n", &a[0] + 1);//跳过首元素指向下一个元素的地址
	
	printf("a = %p\n", a);//数组首元素的地址
	printf("a + 1 = %p\n", a + 1);//跳过首元素指向下一个元素的地址
	printf("&a = %p\n", &a);	//整个数组的地址,数组开始的地址
	printf("&a + 1 = %p\n", &a + 1);	//跳过整个数组的地址

结果

运行结果

&a[0] 	  = 0000008FFC13F688
&a[0] + 1 = 0000008FFC13F68C
a 		  = 0000008FFC13F688
a + 1 	  = 0000008FFC13F68C
&a 		  = 0000008FFC13F688
&a + 1	  = 0000008FFC13F6B0

2. 数组指针的使用

数组指针指向的是数组,数组指针中存放的是数组的地址。

#define _CRT_SECURE_NO_WARNINGS  1
#pragma warning(disable:6031)

#include <stdio.h>

void print(int(*p)[4], int x, int y)
{
	for (int i = 0; i < x; i++)
	{
		for (int j = 0; j < y; j++)
		{//p[i]=*(p+i);
			printf("%d ", p[i][j]);	
			printf("%d ", *(p[i] + j));
			printf("%d ", *(*(p + i) + j));
			printf("%d ", (*(p + i))[j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,8,7,6} };
	print(arr, 3, 4);

	return 0;
}

运行结果:
在这里插入图片描述

下面定义的类型是:

int arr[5];				//arr1是一个包含5个元素的整形数组
int *parr1[10];			//parr1是一个包含10个元素的数组,数组中的元素为int *类型
int (*parr2)[10];		//parr2是一个指针,指向10个元素的整形数组,parr2是数组指针
int (*parr3[10])[5];	//parr3是一个数组,数组有10个元素,每个元素是一个指向5个整形元素的数组指针

7 . 指针数组

指针数组是存放指针的数组。

数组我们已经知道整形数组,字符数组。

int arr1[5];
char arr2[6];

那指针数组是怎样的?

int* arr3[5];//是什么?

arr3是一个数组,有五个元素,每个元素是一个整形指针。

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

8. 数组参数、指针参数

1. 一维数组传参

#include <stdio.h>
//方法1
void test(int arr[])
{}
//方法2
void test(int arr[10])
{}
//方法3
void test(int *arr)
{}
//方法4
void test2(int *arr[20])
{}
//方法5
void test2(int **arr)
{}
int main()
{
 	int arr[10] = {0};
 	int *arr2[20] = {0};
 	test(arr);
 	test2(arr2);
    return 0;
}

2.二维数组传参

//方法1
void test(int arr[3][5])
{}
//方法2
void test(int arr[][5])
{}
/*总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
这样才方便运算。*/
//方法3
void test(int (*arr)[5])
{}
int main()
{
 int arr[3][5] = { 0 };
 test(arr);
}

3. 一级指针传参

#include <stdio.h>
void print(int *p, int sz) {
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0; }

当一个函数的参数部分为一级指针的时候,函数能接收一个指针或者变量的地址

4. 二级指针传参

#include <stdio.h>
void test(int** ptr) {
 printf("num = %d\n", **ptr); 
}
int main()
{
 int n = 10;
 int*p = &n;
 int **pp = &p;
 test(pp);
 test(&p);
 return 0; }

当一个函数的参数部分为二级指针的时候,函数能接收一个二级指针变量或者一级指针变量的地址或者一个指针数组

9. 函数指针

函数指针是存放函数地址的指针

例子:

#include <stdio.h>
void test()
{
 printf("Hello World!\n");
}
int main()
{
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0; 
}

运行结果:
在这里插入图片描述

根据上面的结果可以得出

&函数名 和 函数名 都是函数的地址

可以通过以下方式保存起来

void test()
{
 printf("hehe\n");
}
int Add(int x,int y)
{
    return x + y;
}
void (*pfun1)();
int (*pfun2)(int x,int y);

pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void

pfun2先和*结合,说明pfun2是指针,指针指向的是一个函数,指向的函数参数为int x,int y,返回值类型为int(x,y)可省略

接下来让我们分析两段代码:

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

//代码1:

void(*p)( ) 是一个函数指针,p是函数名,

所以 void( * )( ) 是一个函数指针类型

所以( void( * )( ) )0 是将0强制转换成一个函数地址

*( void ( * )( ) )0 解引用就能够找到这个函数

( *( void ( * )( ) )0 )( )调用这个函数,即调用地址为0参数为无参,返回类型为void的函数

总结:

void( * )( ) 是一个函数指针类型,对0进行强制类型转换,转换完后0就是一个函数地址,

*( void ( * )( ) )0 解引用后就可以找到这个函数,最后我们再调用这个函数( *( void ( * )( ) )0 )( ),不需要传参,因为指向的函数是无参的,

总而言之这其实是一次函数调用,即调用地址为0,参数为无参,返回类型为void的函数。

//代码2

根据代码1的分析,我们能够很快得出

signal是一个函数声明,有两个参数第一个参数是int第二个参数是函数指针,指向的参数是int,返回类型是void

将函数名和参数去掉之后剩下的就是返回值类型->signal(int , void(*)(int))

去掉后剩下void ( * )(int),所以void ( * )(int)是这个函数的返回值类型,为函数指针

总结:

signal是一个函数声明,有两个参数第一个参数是int第二个参数是函数指针,指向的参数是int,返回类型是void

signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回类型是void

代码二太复杂,进行如下简化:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

现在有以下代码:

在这里插入图片描述

所以其实函数指针在使用时可以解引用,也可以不解引用直接使用,所以我们也可以讲函数指针理解为对函数的重命名

10. 函数指针数组

把函数的地址存到一个数组中,那这个数组就叫函数指针数组

定义方式:

int ( *parr1[10] )( );

parr1 先和 [] 结合,说明 parr1是数组,数组的内容是 int (*)() 类型的函数指针。

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

例子(计算器):

#define _CRT_SECURE_NO_WARNINGS  1
#pragma warning(disable:6031)

#include <stdio.h>
#include <string.h>

void menu()
{
	printf("***************************\n");
	printf("**  1.add         2.sub  **\n");
	printf("**  3.mul         4.div  **\n");
	printf("**         0.exit        **\n");
	printf("***************************\n");
}
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;

}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int (*pfArr[5])(int x, int y) = { 0,Add,Sub ,Mul ,Div };//函数指针数组 - 转移表
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input >=1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			int ret = pfArr[input](x, y);
			printf("%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出\n");
		}
		else
		{
			printf("选择错误\n");
		}
		
	} while (input);

	return 0;
}

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

指向函数指针数组的指针是一个指针

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

例子

void test(const char* str) {
 printf("%s\n", str);
}
int main()
{
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //ppfunArr是一个指向函数指针数组的指针
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 //ppfunArr 是一个数组指针,指向的元素有五个
 //指向的数组的每个元素的类型是一个函数指针 void(*)(const char*)
 return 0; 
}

12. 回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应

void* 的作用

void * 可以接受任意类型的地址

void * 不能进行解引用操作

void * 不能进行±整数的操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Genius-Sue

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

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

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

打赏作者

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

抵扣说明:

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

余额充值