前言
指针是c语言的精华,我们之前已经学习了c语言的初阶指针,对初阶指针有了一个大概的了解:
1.指针就是一个变量,是用来存放地址的,一个地址唯一标识一块内存空间,可以通过这个地址来访问这个对应内存空间的数据 2.指针的大小是固定的4/8个字节(32位平台下是4个字节,64位平台下是8个字节。同时我们平时通常看到的是x86,其实就是32位平台,只是以前的叫法,被沿用至今。x64就是64位平台,可千万不能以为x86就是86位平台) 3.指针的类型决定了指针+1/-1时指针往前跳几个字节,即指针的步长,以及指针解引用时的权限 4.指针的运算
接下来就是我对指针进阶的学习。这里对指针的研究的广度和深度将会更进一步。
1.字符指针
字符指针的一般使用方法是
#include<stdio.h>
int main()
{
char ch = 'a';
char* pc = &ch;
return 0;
}
而还有一种使用方式是:
int main()
{
const char* pch = "hello world";
printf("%s", ch);
return 0;
}
这时可能第一次见到的同学会比较诧异,为什么可以把字符串赋给一个字符指针呢? 其实这样使用在语法上是支持的,它的意思其实是把一个常量字符串的首字符’h’的地址存放到指针变量pch里面。 它的打印结果是就是:
这里的是一个常量字符串,那么一般在声明这样的一个字符串指针时,我们要在指针变量前面加上一个const修饰,表示指向的是一个常量。
从这个知识点可以往后衍伸。 C会把常量字符存储到一个单独的内存空间,当几个指针。指向同一个字符串的时候,他们实际上会指向同一块内存空间。但是相同的字符串去初始化不同的数组时,就会开辟出不同的内存空间。这句话理解可能会比较抽象,我们通过一段代码来解释一下:
#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.指针数组
指针数组我们之前初阶的时候就已经介绍过了,这里再重新提一嘴。 指针数组其实就是一个数组。一个什么数组呢?一个存放指针的数组。
它的声明方式:
int* arr1[10];
char *arr2[4];
char **arr3[5];
3.数组指针
3.1数组指针的定义
数组指针是数组还是指针?我们从名字就可以看出来。 草莓味饼干是草莓还是饼干?答案是饼干。 那么数组指针就是指针。一个什么指针?一个指向数组的指针
声明方式: 我们先看一段代码:
int *p1[10];
int (*p2)[10];
p1是指针数组。p2是数组指针。 为什么呢? 解释:*的优先级比[]更高,我们看p1,p1先和[]结合,那么它是一个数组。再与’*‘结合,说明它是一个存放指针变量的数组。 p2先和*结合,说明p2是一个指针,再与[]结合,说明它是一个指向数组的指针变量。
3.2数组名vs&数组名
我们来看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
运行结果如下: 打印结果一样,那么我们是不是就可以认为数组名等于&数组名了呢? 其实不是的。 我们再看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
打印结果如下:
通过上面的代码我们可以发现,虽然直接打印arr的地址和&arr的地址是一样的,但是它们+1的结果截然不同,arr+1跳过了1个整型,而&arr+1跳过了10个整型,即一个数组的长度。 实际上。arr是首元素的地址,而&arr是整个数组的地址。那么这个地址的类型是什么?怎么接收呢?
本例中,&arr的类型是:int(*)[10],是一种数组指针类型。 数组的地址+1,跳过了整个数组的大小,所以&arr+1相对于&arr的差值是40.
3.3数组指针的使用
当我们想传入一个数组到函数里去的时候,这时候数组指针就起效果了。 看以下代码: #include <stdio.h> void print_arr1(int arr[3][5], int row, int col) { int i = 0; for(i=0; i<row; i++) { for(j=0; j<col; j++) { printf("%d ", arr[i][j]); }
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
void print_arr2(int (*arr)[5], int row, int col) {
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
print_arr2(arr, 3, 5);
return 0;
}
我们可以看到,当我们想传入一个二维数组的时候,我们知道数组名是首元素地址,而二维数组的首元素却是一个数组,这样,我们函数在接收数组指针时就需要数组指针类型来接收。这就是数组指针的作用。
4.数组参数、指针参数
我们经常遇到这样一个问题,当我们想把数组或者指针传给函数时,函数的参数我们应该如何设计呢?
4.1一维数组传参
当我们要传入一维数组时,我们的函数的参数应该怎么设置呢?这里我已经进行了一个汇总
#include <stdio.h>
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int* arr)
{}
void test2(int* arr[20])
{}
void test2(int** arr)
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
4.2二维数组传参
void test(int arr[3][5])
{}
void test(int arr[][5])
{}
void test(int* arr)
{}
void test(int* arr[5])
{}
void test(int(*arr)[5])
{}
void test(int** arr)
{}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
同样的,我也用代码对它进行了归纳总结。一定要搞清楚它当中的逻辑。为什么可以,为什么不可以,从它传入数据的类型的本质出发。
4.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]);
print(p, sz);
return 0; }
4.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;
}
5.函数指针
老惯例,我们先来看代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
这段代码的打印结果是一致的,函数不需要区分这个。因为区分了也没有什么意义。重点我们放在后面
函数指针的变量初始化:
void (*pfun1)();
pfun1先与*结合,表示pfun1是一个指针,一个什么指针呢?一个指向返回值为void,没有参数的函数。
6.函数指针数组
函数指针数组是什么?是数组,什么数组?存放函数指针的数组,而函数指针前面已经介绍了。 函数指针数组的定义:
int (*parr1[10])(); int parr210; int ()() parr3[10];
这个其实是不需要去记忆的,只需要按照以下思路:
首先找到你的变量名:以parr1为例,看它左右,左边有一个*,右边有一个括号,我们知道括号的优先级比*高,所以我们可以先知道它是一个数组。是一个什么数组,它左边有一个*号,那么我们知道它是一个指针数组。同时后面跟着括号,以及前面有一个int,我们知道它是一个存放没有参数的返回类型是int的函数的指针的数组。
它的作用:转移表。 仍然通过一段代码来理解:
#include <stdio.h>
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 x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div };
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0;
}
那么如何理解转移表的含义呢。 我们可以看到,如果我们不适用函数指针数组的话,我们的代码将是一段非常冗余繁琐的代码,其原因是swtich-case语句的不断重复,将不同情况下的函数调用情况给罗列出来。而使用了函数指针数组的话,就可以代替掉switch-case语句从而实现不同情况下将不同函数调出不同函数。 从而我们将这一功能抽象出来形成了一个名词:转移表。 由此可见,许多时候我们理解一个名词的定义时不要去死扣它的字面意思,字面意思往往非常晦涩和抽象,我们应该更多的去结合它的实际应用,把我们自身和发明这个名词的人摆在一个位置,这样我们就能更加深刻理解它发明这个名词的心理活动。
7.指向函数数组的指针
这其实就是不断的套娃,当你有一个数组或者是函数或者是什么新的类型数据的时候,它都会在内存开辟一个空间,从而会拥有一个地址,而这时你就可以发明一个新的数据类型去存储这个新的地址。这样就可以不断的套娃,你又可以发明一个存储这些新地址的数组。又可以发明一个指向这个数组的指针。所以这个指向函数数组的指针我就不过多赘述的。
其本质就是:
它是一个指针。指向的是一个数组。数组里存放的函数的地址。
往往我们从后往前读就能得到它的本质。
|