C语言学习之指针总结
文章目录
前言
C语言的灵魂在于指针,对于初学者来说,对这个灵魂了解的更大程度上体现在难度上,但是实际上也仅仅是由于不熟练,只要学完过后,看几个例子,跟着写几段代码,就会发现其实指针和数组,循环,分支语法之类的运用难度上差不了很多(虽然数组的本质就是指针)。所以,要对自己有信心!接下来正文开始。
一、指针是什么?
先给出结论: 指针==内存地址
我们经常听到“向内存申请空间”,“栈溢出”,“越界”等词,其实,这些词都是对C语言程序与内存空间之间的操作关系的描述。想要解释清楚这个概念,肯定是离不开内存空间的解释。下面给出一张图,让我们更形象的了解内存空间:

内存空间的最小操作单位是字节,我们常说的内存32G,64G,128G等等也说的是内存空间,将它们换算成字节,就是图中小格子的总数。每一个小格子都有自己的编号,只是有时候用十六进制表达,有时候用八进制表达,但本质都是一个独一无二的编号——地址,我们说指针指向哪里哪里,说的其实就是指向某个地址,所以说,指针==地址,是可以直接在内存空间上操作的灵魂!
二、指针变量是什么?
我们已经了解指针是什么,再根据C语言严谨的类型规则,很自然的会联想到指针用什么存放?,属于什么类型?接下来,让我们进一步了解存放指针的变量的类型:
还是先给结论:存放指针的也是变量——指针变量,指针的类型取决于指针指向的数据的类型,例如:指向整型数据的指针就叫做整型指针······
其实很好理解,我们知道像abcd这样的字符——有char等类型,123——有int等类型,那么内存地址(指针)这个特殊的数据,它不被任何一种已有类型所容纳,所以给它专门制定了新名字——指针变量。
而关于指针类型就更好理解了,指针指向一个地址,这个地址可以存放很多种类型的数据,我们不能把杂七杂八的类型一概而论,因此我也也把指针分出类型,这个类型就是取决于这个指针所制指向的数据类型。
指针变量类型和指针类型的关系
我们常听到的整型指针变量(int*),字符指针变量(char*),实际上指的是该指针变量的操作权限。
说人话:指针变量类型和指针类型就好比一辆自动化程度很高的车和一个人,指针变量类型决定这辆车一次可以开多远,指针类型就决定了这个人的承受能力,例如:整型指针管理包括首地址向后的4个字节,而整型指针变量就是启动一次开4字节的小车,如果小车和人的“运行里程”一致,那么这辆载人的车就可以很好的运转。
但是,一旦人和车的“运行里程”不一致了,就会出现一些意外的后果,就像人车“运行里程”不一样的话,总有一方要出事,具体问题这里不过多赘述,想了解的更多的话可以搜索专门讲解的博客
三、指针(变量)的分类
1.1 指针(变量)分类简介
指针的的种类很多,包括但不限于所有普通内置数据类型的指针,特殊的指针还有数组指针,函数指针,自定义类型指针(结构体,联合体,枚举),在文件操作里还会涉及到文件指针(这里预告一下,以后也会写关于C语言文件操作的博客),为防止文章太过臃肿,我们将用法趋同且相对更简单的普通内置数据类型(short,int ,long,char······等等)指针分为一组讲解。
2.1普通内置类型指针(变量)(非自定义)
这类指针是最基本的指针,用法简单,且可以说是一模一样。关于命名,一张图一目了然:

像其他long int等等的也是一样,总结就是:
指针的命名就是在类型后面+ ,即:类型名+*
就像前文所说,指针类型和指针变量类型犹如人和车,在非特别用法的情况下,统一很重要。它们的“运行里程”和名字里的内置类型一致:

2.2 void*指针(变量)
void类型,广泛用于泛型编程,它像一个变形金刚,可以接收任意类型“运行里程”的人(指针类型),再来通过强制转换适配这个人(指针类型)的“运行里程”。
例如:库函数qsort使用时,函数传参时就要用到void类型的指针变量来接收实参,这里给出作者用冒泡排序模拟qsort实现时写的代码,代码如下:
#include<string.h>
#include<stdio.h>
void swap( char*p1,char* p2 ,size_t width) {
//以字节为单位,一个一个字节交换
for (int i = 0; i < width; i++) {
char tmp = *(p1 + i);
*(p1 + i) = *(p2 + i);
*(p2 + i) = tmp;
}
}
int compare_int(const void* p1,const void* p2) {
return *((int*)p1) - *((int*)p2);
}
int compare_char(const void* p1, const void* p2) {
return *((char*)p1) - *((char*)p2);
}
void bubble_qsort(const void* p,const size_t sz,const size_t width,int (*cmp)(void*,void*) ) {
//外层表示循环层数
for (int i = 0; i < sz - 1; i++) {
//内层实现排序逻辑
for (int j=0; j < sz - 1; j++) {
//强转成char*指针,最小权限才能适应所有类型数据
if (cmp((char*)p + j * width, (char*)p + (j + 1) * width) > 0) {
//实现交换逻辑
swap((char*)p + j * width, (char*)p + (j + 1) * width,width);
}
}
}
}
int main() {
//整型类型数据模拟排序
int arr[] = { 8,6,9,5,3,4,2,1,7,0 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
bubble_qsort(arr,sz,sizeof(int),compare_int);
for (int i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
printf("\n");
//字符类型数据模拟排序
//不算\0
char arr1[] = "gdfebca";
size_t sz1 = strlen(arr1);
bubble_qsort(arr1, sz1, sizeof(char), compare_char);
for (int i = 0; i < sz1; i++) {
printf("%c ", arr1[i]);
}
printf("\n");
//算上\0 \0的ascll值是0,会排在最前面
bubble_qsort(arr1, sz1 + 1, sizeof(char), compare_char);
for (int i = 0; i < sz1 + 1; i++) {
printf("%c ", arr1[i]);
}
return 0;
}
2.3函数指针(变量)
函数去掉函数名,参数名也可以省略,但参数类型必须保留,就是该函数的类型,还是以上文冒泡排序模拟qsort实现的代码为例,
//这个函数用于完成凉凉比大小的逻辑
int compare_int(const void* p1,const void* p2) {
return *((int*)p1) - *((int*)p2);
}
这个函数类型就是: int ( void* , void* )
以这个函数为例,它的函数指针是:int (*name)( void* , void* )
name--是名字,可以自己命名
对于函数指针类型,也是一样的,
将函数指针去掉函数名就是函数指针类型:int (*)( void* , void* )
2.3.1 函数指针进阶实践——转移表实现计算器代码
通过函数指针的解引用,我们可以直接访问到该函数,根据这个特点,再利用数组的下标,如果我们将函数指针放在一个数组里面(函数指针数组),我们就可以0~n的简单下标转换快速的调用函数,这种方法就叫做转移表
接下来给出一个题目:用转移表实现有加减乘除和退出功能的计算器。
先给出我的方案:
#include<stdio.h>
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;
}
void menu() {
printf("****** 1.加 2.减 ******\n");
printf("****** 3.乘 4.除 ******\n");
printf("****** 0.exit ******\n");
printf("请输入: ");
}
//void calculator(int (*pf)(int, int)) {
// printf("请输入两个操作数(x y)\n ");
// int x, y;
// scanf("%d %d", &x, &y);
// int ret = pf(x, y);
// printf("结果是:%d\n", ret);
//}
int main() {
int taggle;
do {
menu();
scanf("%d", &taggle);
//这里就是函数指针数组,通过输入下标调用函数,即转移表
int (*pf[5])(int, int) = { 0,add,sub,mul,div };
//用于检查功能选择合法性
if (taggle >= 0 && taggle <= 4) {
switch (taggle) {
case 0:
printf("退出计算器\n");
break;
default:
printf("请输入两个操作数(x 和 y) \n");
int x, y;
scanf("%d %d", &x, &y);
int ret = pf[taggle](x, y);
printf("结果是:%d\n", ret);
break;
/* case 1:
calculator(add);
break;
case 2:
calculator(sub);
break;
case 3:
calculator(mul);
break;
case 4:
calculator(div);
break;*/
}
}
else
printf("功能选择错误,请重新输入\n");
} while (taggle);
return 0;
}
以上代码给出了两种实现方案,转移表没注释;另一种do···while语句列举多种情况的方案是比较容易想到的,但是不如转移表简介。两种方法都可以实现计算器功能,但是转移表版本,要深入了解。
2.4 数组指针(变量)
像函数一样,数组也有类型,并且数组类型获取方式和数组指针的样式也是一模一样:数组去掉数组名,剩下的就是数组类型
例如:
数组(包括内容): int arr1[3]={1,2,3}
数组(不看内容): int arr1[3]
数组类型(去掉数组名): int [3]
数组指针: int (*name)[3] //这里name和函数指针的name一样,只表示指针的名字
数组指针类型(数组指针去掉名字):int (*)[3]
2.4.1数组指针实践运用:用数组指针模拟二维数组
同样,了解数组指针后,
我们给出一个小小的实践运用:用数组指针模拟二维数组
#include <stdio.h>
int main() {
// 1. 定义原始二维数组(M行N列)
const int ROW = 3; // 行数
const int COL = 4; // 列数
int arr[ROW][COL] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 2. 定义数组指针,指向二维数组的首行(必须指定列数COL,与二维数组匹配)
int (*p)[COL] = arr; // 等价于 p = &arr[0](指向第0行的数组指针)
// 3. 三种等价访问方式(验证数组指针模拟效果)
printf("=== 数组指针模拟二维数组 ===\n");
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++) {
// 方式1:最直观,模拟二维数组语法 p[i][j]
printf("p[%d][%d] = %d\t", i, j, p[i][j]);
// 方式2:指针偏移 + 下标:*(p[i] + j)(p[i]是第i行的首地址)
// printf("*(p[%d]+%d) = %d\t", i, j, *(p[i] + j));
// 方式3:双重指针解引用:*(*(p+i) + j)(p+i指向第i行,解引用后为该行首地址)
// printf("*(*(p+%d)+%d) = %d\t", i, j, *(*(p + i) + j));
}
printf("\n");
}
return 0;
}
不过值得注意的是,通过数组指针模拟的二维数组,其元素在内存中的储存并不一定连续,而原始的二维数组在内存中是连续储存的,所以这样的方法并不完全等价与二维数组。
2.5 文件指针(变量)
文件指针就不像其他指针会涉及到类型,它只有一个固定的写法:FILE*name
其中,FILE* 就是文件指针的类型,FILE 是标准库预定义的结构体,封装了文件操作所需的底层信息。
关于文件指针的内容这里不过多赘述,后续更新的关于C语言文件操作的文章将会详细讲解。
总结
首先,感谢大家能看到这里!
然后是对文章内容的总结:这篇博客基本总结了C语言指针这块的大体框架,并给出了两个指针知识的运用实践和相关代码,但是由于篇幅原因,实际上关于指针还有很多细节没有说明,如果大家有兴趣可以去b站上进行学习,也可以去C/C++官方参考手册了解。
这里给出网站:C/C++参考手册(点击即可跳转)

更多推荐
所有评论(0)