【C语言】第九章-指针进阶

第九章 指针进阶

  在初阶篇已经讨论过C语言中最为重要也是极容易出错的指针,在进阶篇哦我们还要继续深入探讨指针,学习种类更加多样更加灵活,更加变态的指针。

字符指针

  字符指针我们所要知道的就是这种指针可以只想单个字符,也可以指向整个字符串,而实际使用中,用来指向字符串的情况更为常见,同时也更容易出错。因为有不少同学往往把字符指针指向一个字符串的情况理解成了将字符串存储进了整个指针中,与字符数组搞混。因此我们可以看以下这个例子做以区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
int main()
{
char str1[] = "HelloWorld";
char str2[] = "HelloWorld";
char* str3 = "HelloWorld";
char* str4 = "HelloWorld";
if(str1 == str2)
{
printf("str1 和 str2 相同!\n");
}
else
{
printf("str1 和 str2 不相同!\n");
}
if(str3 == str4)
{
printf("str3 和 str4 相同!\n");
}
else
{
printf("str3 和 str4 不相同!\n");
}
}

  输出结果:

1
2
3
[misaki@localhost test]$ ./Main
str1 和 str2 不相同!
str3 和 str4 相同!

  由此可见相对于字符数组str1str2是由两块完全不同的内存空间进行存储,因此他它们的首元素地址并不相同,而对于完全相同的字面量字符串,当有不同的指针指向他们的时候,系统不会再另开辟新空间进行存储,而是会让不同的指针指向同一块字面量字符串存储的内存地址。这是字符数组与字符指针的区别之一。

指针数组和数组指针

  在C语言中,这两种指针是十分容易混淆的,在不同的C语言系列教材中,这两种指针在不同的情境下往往也被冠以其它的名字,例如在多为数组中出现的行指针,但其根源终究不过是这两种指针。

指针数组

  指针数组较为浅显易懂,通过名字也应该能想到,这是一种数组,而数组元素类型是指针。定义语法如下:

        typeName* arrayName[N];

  例:

        int* ptrArray[10] = {NULL};

  这个例子我定义了一个大小为10元素类型为int*型的指针数组。这样的例子看上去很容易理解,但有了这个基础我们就可以定义出以下这几种更有实际用途的指针数组。

1
2
3
4
5
6
char* array[4];
//字符指针数组,里面的每个元素都是一个字符指针,可以用来指向字符或者字符串
//这种指向方式更为简单节省空间,而当我们使用二维数组存储多个字符串往往需要花费更多空间
char** array[5];
//二级字符指针数组,里面的每个元素都是一个指向字符指针的指针,这种用法很不多见,也较为复杂
//但是在我们想要指向多个字符指针数组时就派的上用场。

数组指针

  数组指针相对于指针数组来说较为复杂。数组指针从名字上看不过是一个数组的指针罢了,确实如此。但是定义起来较为难以理解。

        typeName (*arrayName)[N];

  例:

        int (*array)[10];

  当我们定义了一个数组指针后,我们可以将任意一个数组取地址,然后赋值给它。

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int array[4] = {0};
int (*arrayPtr)[4] = &array;//注意数组指针定义时[]中的值需与目标数组的长度相同
printf("%p\t%p\n", &array, arrayPtr);
}

  输出:

1
2
[misaki@localhost test]$ ./Main
0x7ffdf4f3c8e0 0x7ffdf4f3c8e0

  通过以上例子我们是否可以联想到我们平时使用的二位数组本身就是一个数组指针呢?

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int array[3][4] = {0};
int (*ptr)[4] = array;
printf("%p\t%p\n", array, ptr);
}

  输出:

1
2
[misaki@localhost test]$ ./Main
0x7fffc874b160 0x7fffc874b160

  由此可见二维数组本身就是一个数组指针,用来指向内部的各个一维数组。

  同时在这里还需强调的一点是,又是尽管指针的值相同,也就是说指向同一块内存地址,却有可能指针类型完全不同。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
int arr[3][4] = {0};
printf("%p\n", arr);//二维数组首地址,一维数组指针int (*)[4]
printf("%p\n", &arr);//二维数组指针int (*)[3][4]
printf("%p\n", arr[0]);//一维数组首地址,int型指针int*
printf("%p\n", &arr[0][0]);//二维数组第一个元素的地址,int型指针int*
}

  输出:

1
2
3
4
5
[misaki@localhost test]$ ./Main
0x7ffca85b9440
0x7ffca85b9440
0x7ffca85b9440
0x7ffca85b9440

  由以上的代码可以看出尽管几个指针类型不尽相同,但它们都指向同一块内存空间,因此我们在给指针间进行赋值时,一定要清楚各个指针的类型,在这里十分容易出错。

延伸

  有了数组指针与指针数组的基础,我们即可将他们合并得出更多更加复杂的指针,例如数组指针数组:

        int (*ptr[10])[4];

  以上这行代码表示定义了一个长度为10的数组,数组内每一个元素都是一个数组指针,其中每个指针指向长度为4的int型数组。有了数组指针数组,那么是否就有指针数组指针呢?是的,得益于C语言中灵活多变的语法,这些都可以得到实现,但是难度较高也并不经常使用,在此不再讨论。

指针与数组传参

  在C语言中,我们都知道对于函数传递参数都是进行副本传参,也就是将实参克隆一份变为形参再进行传递,由此形参的改变并不会影响到实参。但是当我们传递指针的时候就可以对原本指针所指向的变量进行改变。而当我们想要把数组传递进函数中时,无论我们以何种语法进行传递,对于编译器来说也都会自动帮我们将数组转换为指针继续宁传入,以此来节省开销。

一维数组传参

  A:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void Print(int* arr, int len)
{
for(int i = 0; i < len; i++)
{
printf("%d\n", arr[i]);
}
}
int main()
{
int arr[4] = {1, 2, 3, 4};
Print(arr, 4);
}

  B:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>  
void Print(int arr[], int len)
{
for(int i = 0; i < len; i++)
{
printf("%d\n", arr[i]);
}
}
int main()
{
int arr[4] = {1, 2, 3, 4};
Print(arr, 4);
}

  对于一维数组来说,我们可以直接使用指针的方式进行传入,也可以使用数组作为形参进行传入,并且以数组作为形参的时候可以直接忽略数组的长度,因为无论如何编译器都会将其转换为指针,这里的长度也就不需要了,因此为了更好的表示数组的长度,我们也必须将数组的长度作为参数也传入函数。

二维数组传参

  A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
void Print(int arr[][4], int len)
{
for(int i = 0; i < len; i++)
{
for(int j = 0 ; j < 4; j++)
{
printf("%d\t", arr[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);
}

  B:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
void Print(int (*arr)[4], int len)
{
for(int i = 0; i < len; i++)
{
for(int j = 0 ; j < 4; j++)
{
printf("%d\t", arr[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);
}

  对于二维数组来说,也同样的有两种方法,一种是以数组的方式,另一种是以与之相同类型的数组指针的方式,不过这里不同的是,二维数组的低维度不可省略,而高维度可以省略。

  对于数组传参不光是一维和二维数组,几乎所有类型的数组都适用同样的原理。

函数指针

  函数指针是指针中较为重要也是比较特殊的类别,并且在实际使用中也是极为常见的类型。

函数指针

  函数指针定义:

        void (*FunName)();

  函数指针定义起来语法十分简单,而我们想要函数指针指向某个函数则只需要让它指向和它返回值类型以及参数完全相同的函数即可。当他进行指向后我们就可以使用函数指针直接进行函数的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
void Print(int (*arr)[4], int len)
{
for(int i = 0; i < len; i++)
{
for(int j = 0 ; j < 4; j++)
{
printf("%d\t", arr[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);
void (*ptr)(int (*arr)[4], int len) = Print;
ptr(arr, 3);
(*ptr)(arr, 3);
}

  输出:

1
2
3
4
5
6
7
8
9
10
[misaki@localhost test]$ ./Main
1 2 3 4
5 6 7 8
9 10 11 12
1 2 3 4
5 6 7 8
9 10 11 12
1 2 3 4
5 6 7 8
9 10 11 12

  在以上的例子中我们使用了一个ptr函数指针指向了Print()函数,注意在给函数指针赋值时,函数名就是函数的地址了,而在使用函数指针进行函数调用时,则可以直接对指针像函数呢样传参即可,当然也可以先解引用再传参,都是可以的。

函数指针数组

  接下来我们将函数指针与我们之前说过的指针数组相结合,形成函数指针数组,这在实际开发中往往可以大大提高执行效率。

  与数组指针数组类似有:

        
int (*arr[10])(参数)

  这就是一个函数指针数组的标准定义式了,以下我举个小小例子将其带入应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
int Plus(int num1, int num2)
{
return num1 + num2;
}
int Min(int num1, int num2)
{
return num1 - num2;
}
int main()
{
int (*arr[2])(int, int) = {
Plus,
Min
};
int choice = 0, num1, num2;
printf("执行加法请输入1,执行减法请输入2:");
scanf("%d", &choice);
if(choice == 0)
{
return 0;
}
printf("请输入操作数:");
scanf("%d%d", &num1, &num2);
printf("%d\n", arr[choice - 1](num1, num2));
}

  这里我简单举了个以加减法为主的小计算器的例子,在最后我们使用函数指针数组进行函数调用以此减少判断的开销,或许这个例子不够明显,但在我们有很多函数要进行调用时,结果就会更加一目了然了。

回调函数

  回调函数说简单些就是我们将函数指针作为参数传给了另外一个函数,这个函数指针所指向的函数往往不是实现者所写的,因此更不会由实现者进行调用,而是由使用者根据使用情况写出的自定义函数,这个函数在特定情况下进行调用。这就是回调函数,因此这样的函数在编写外部接口时经常被用到。接下来我i我们以一个qsort函数为例编写一个回调函数并进行使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
int cmp(const void* p1, const void* p2)//自己所编写的函数
{
return (*(int*)p1 > *(int*)p2);
}
int main()
{
int arr[] = {11, 22, 99, 34, 454, 32, 0};
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), cmp);//传入函数中成为回调函数
for(int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d\n", arr[i]);
}
}

  输出:

1
2
3
4
5
6
7
8
[misaki@localhost test]$ ./Main
0
11
22
32
34
99
454

  从以上这个例子即可看出回调函数的威力,我们可以根据使用我们自己所写的函数改变排序规则。因此回调函数也是实际使用中十分重要的一环。

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助