目录
前言
我们在之前知道指针就是一个变量,用来存放地址,地址唯一标识一块内存空间,指针的大小是固定的,32位平台是4个字节,64位是8个字节,指针还是有类型的,指针的类型决定了指针的+ - 整数的步长,指针解引用操作的时候的权限。指针还是有运算的,指针加减整数,指针减去指针,指针的关系运算,忘了的可以去之前的文章复习一下。
接着我们进行指针的深入学习。
1、字符指针
在指针的类型中我们知道有一种指针类型为字符指针char*
一般都是这样的:
cpp
nt main()
{
char a = 'w';
char* b = &a;
return 0;
}
这里b就是一个字符指针变量。还有一种是这样的:
cpp
int main()
{
const char* pc = "hello";
printf("%s\n", pc);
return 0;
}
给定一个常量字符串hello,这里创建了一个pc的指针变量,相当于指向字符串首字母,可以理解成字符数组,这里打印字符串知道起始位置就可以直到识别到\0,之后才会结束打印。
2、指针数组
指针数组是一个存放指针(地址)的数组。
cpp
int main()
{
const char* arr[4] = { "abc","edf","ghi","mno" };//存放字符指针的数组
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%s\n", arr[i]);
}
return 0;
}
这里给定了四个元素的char类型的字符指针,通过字符数组来存上这些指针(地址),当我们用循环来访问每一个元素地址的时候,就访问了这些常量字符串。
我们也可以仿照二维数组,来用这个指针数组来实现一个二维数组:
cpp
int main()
{
int arr1[4] = { 1,2,3,4 };
int arr2[4] = { 2,3,4,5 };
int arr3[4] = { 6,7,8,9 };
int arr4[4] = { 0,0,0,0 };
int* arr[4] = { arr1,arr2,arr3,arr4 };
int i = 0;
for (i = 0; i < 4; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);//或者 *(arr[i] + j );
}
printf("\n");
}
return 0;
}
使用指针数组的好处是可以灵活地管理内存,只需分配和释放指针所指向的对象的内存空间,而不需要对整个数组进行操作。此外,指针数组还可以方便地传递和操作指针,提高程序的效率。
注意,使用指针数组时需要注意指针的有效性,即确保指针指向的对象在使用时是有效的,否则可能导致程序出错。
3、数组指针
我们知道指针有字符指针,指向字符的指针(char*),整形指针,指向整形的指针(int*),浮点型指针,指向浮点型的指针(float*)(double*)。
3.1数组指针
顾名思义,数组指针就是存放数组地址的指针,指向数组的指针。
但数组指针怎么来写呢:
cpp
int arr[10];
int (*pa)[10] = &arr;
这里说明pa是一个指针,因为[ ] 的优先级要高于 * 号的优先级,所以要加上一个( )来保证p先和* 结合,这个指针指向一个有10个整形元素的数组,用来接收arr数组的地址。
3.2&数组名VS数组名
对于这两种数组的区分是啥?
cpp
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
return 0;
}
这里通过一个十个元素的数组来进行区分,我们发现,这里打印出来的地址都一样,因为都是数组的首地址。这里并不能看出来什么,于是我们将每一个后都加上一个1,如果有不一样的效果那么加上1后的效果也不一样。
cpp
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n", arr+1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0]+1);
printf("%p\n", &arr);
printf("%p\n", &arr+1);
return 0;
}
这里就可以看出来了,十六进制中,第一个和第二个出现的情况都一样,都是多了四个字节,也就是一个整形的大小,而第三个,相差了40个字节,也就是差了一组的大小。他们的类型也不一样,第一个和第二个是整形指针(int *),而第三个的类型是数组指针,也就是(int( * ) [10] )。
所以,数组名是数组首元素的地址,拿到的是第一个元素的地址,而取数组名,拿到的是数组的地址。数组首元素的地址和数组的地址从值的角度来看是一样的,但是意义是不一样的。我们从上述代码就可以看出来。
3.3数组指针的使用
针对数组指针和数组名的了解,我们可以以简单的用数组指针来访问一个二维数组:
cpp
#include<stdio.h>
void print(int(*p)[4], int a, int b)
{
int i = 0;
for (i = 0; i < a; i++)
{
int k = 0;
for (k = 0; k < b; k++)
{
printf("%d ", (*(p + i))[k]);//等同于p[i][j]
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
print(arr, 3, 4);
return 0;
}
取到地址后,就通过每行+第几列遍历来进行访问数据打印数据,最后打印出这个二维数组。
学过后可以看看下面的代码代表什么意思?
cpp
int(*arr[10])[5];
这里是arr这个数组的里面存着的是每个元素的类型是:int( * ) [5]的数组指针类型。arr3里每一个元素存着的就是一个五个int类型元素的数组的地址(也就是指向)。
4、数组参数、指针参数
4.1一维数组传参
cpp
void test(int arr[]){}
void test(int arr[10]){}
void test(int *arr){}
void test2(int *arr[20]){}
void test2(int **arr[20]) {};
int main()
{
int arr[10] = { 0 };
int* arr1[20] = { 0 };
test(arr);
test2(arr1);
return 0;
}
可以通过上述代码中的方式来传入一维数组的参数,其中二级指针是因为在主函数中要传入的是一级指针,所以传入的参数要用二级指针来获取这个一级指针的地址。
4.2二维数组传参
二维数组在传参数的时候也是一样的道理,大差不差:
cpp
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
我们要把一个二维数组传入test这个函数中,可以这么做:
cpp
void test(int arr[3][5]) {}
void test(int arr[][5]){}
通过传入二维数组,但这里要注意,二维数组行可以省略,但是列不可以省略。
二维数组传参形参的部分可以写成指针吗?以下有几种形式,我们来进行分析:
cpp
void test(int *arr) {}
这样不可以,因为数组名表示首元素地址,而这里是一个二维数组,所以这里写成一个整形一维数组的地址肯定不行。
cpp
void test(int* arr[5]){}
这里传入的是一个数组但不是二维数组,可以存指针但又不是一个指针,这里传参要么是数组,要么是一个指针,所以这个完全不搭边。
cpp
void test(int(*arr)[5]){}
这个是传入的是一个数组指针,也就是传入的是第一行五个整形元素的数组的地址,而刚刚好二维数组一行是五个元素,所以这个可以。
cpp
void test(int **arr){}
二级指针是用来接收一级指针的地址的,这里需要传入的是第一行的地址,是一个数组的地址,不能用二级指针来作为参数传入。
4.3一级指针传参
cpp
void test(int *p)
{}
这里传入可以直接传入一个一级指针或者整形变量的地址
cpp
int a=10;
int *p=&a;
test(&a);
test(p);
这里也可以传入数组,因为传入的是首元素的地址
cpp
int arr[10];
test(arr);
4.4二级指针传参
二级指针传参形参的部分就用二级指针来接收就可以。
cpp
void test(int **a)
{ }
int main()
{
int n=10;
int *p=&n;
int **pp=&p;
test(pp);
test(&p);
return 0;
}
如果我们发现函数的参数是二级指针,完全就可以传入一个二级指针的变量,也可以传入一个一级指针的地址,也可以传入一个指针数组的数组名。
4.5总结
传过去的是一级指针,就用一级指针来接收就行,传过去的是二级指针,就用二级指针就行。
5、函数指针
函数指针,顾名思义,就是指向函数的指针。
cpp
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pc)(int, int) = &Add;
printf("%p", pc);
return 0;
}
这里写出一个加法的简单函数,通过函数指针来接收这个加法函数的地址,之后打印这个函数指针。&函数名和函数名都是函数的地址,所以取不取地址符号都可以。
函数指针的写法和数组指针非常非常的类型:
函数指针:
int ( *pc )( int , int )
函数返回类型+指针变量+(参数类型)
数组指针:
int (*pc)[ 10 ]
数组元素类型+指针变量+数组元素个数(可以不写但要写[ ] )
注意优先级括号问题
我们可不可以通过函数指针来调用函数呢?
cpp
( *pc )( 2 , 3);
pc( 2 , 3);
对pc解引用,也就是找到了函数,之后进行传参,这样就可以实现函数的调用。不加解引用也可以,因为pc就相当于Add。用第一行来写是为了方便理解,就是为了摆设,告诉这是一个指针解引用。如果要是写这个*,一定要加上括号。
5.1思考
接下来看两段代码:
cpp
( *(void(*)()) 0 ) ();
这段代码出自《C陷阱和缺陷》。将0强制类型转换成一个函数指针,这个函数的参数没有,返回类型为void,这里就是把0地址处的函数调用了一下,调用的时候后面传参,因为函数没有参数,所以直接加个括号无参。
该代码是一次函数的调用,调用0地址处的一个函数。
接下来再看一个:
cpp
void (*signal(int,void(*)(int)))(int)
该代码是一次函数的声明,声明的函数名字叫signal,signal函数的参数有两个,一个参数是int,第二个参数是一个函数指针类型,该函数指针指向的参数是int,返回类型是void,signal函数的返回类型是一个函数指针,该函数指针能够指向的那个函数的参数是int,返回类型是void。
可以把这个代码简化一下:
cpp
typedef void(*pc)(int)
pc signal(int,pc)
这里说一下,typedef重命名,只有对指针的时候才会把类型名放在 * 旁边,像之前的
cpp
typedef unsigned int unit;
就正常写就可以。
总结
本篇文章讲了一些不同类型指针的使用和写法,指针在编程中是非常实用的工具,可以帮助我们更好地管理内存、构建和操作数据结构,提高程序的性能和效率。熟练地使用指针可以让我们编写出更加灵活、高效和可靠的程序。下一篇继续学习。