C语言|指针进阶超详解

目录

一、什么是指针变量?
二、 字符指针
1.一般用法:指向字符变量
2.其它用法:指向字符串常量
3.字符串有关 常量区,栈区 创建区别
三、 指针数组
1.定义
2.运用
四、数组指针
1.预备知识
2..定义
五、 数组传参 和 指针传参
1.一维数组传参
2.二维数组传参
六、函数指针
1.定义
2.解引用调用函数指针
3.两个有趣的代码
七、函数指针数组
1.定义
八、指向函数指针数组的指针
1.定义
九、回调函数
1、定义

一、什么是指针变量? 地址:内存会被划分为小的内存单元,每个内存单元都有唯一的编号,这个编号就是地址,地址也叫指针。这个编号是一个数值。
不过我们平时所说的指针是指针变量。

1.指针就是一个变量,用来存放地址,地址唯一标识一块空间。
2.指针的大小4/8个字节(32位平台/64位平台)。
3.指针是有类型的,指针的类型决定了指针 + - 整数的步长,指针解引用操作时候的权限。
4.指针的 + - 运算是两个相同类型指针之间的元素个数。
由上述可以得到:内存编号 = 地址 = 指针
指针或者地址,要存储,就可以放到指针变量中去。
二、 字符指针 1.一般用法:指向字符变量 C语言|指针进阶超详解
文章图片

2.其它用法:指向字符串常量 C语言|指针进阶超详解
文章图片

如上所示的代码:p指向了一个常量字符串,我们需要先知道p是一个指针变量,它存放的内容一定是地址,大小一定为4/8个字节,这里的字符串已经超过了8个字节,所以一定不是存放的字符串,它存放的其实的这个字符串的首字符地址,也就是’a‘的地址,解释了为什么打印一个字符是 ’a‘。
那么我们可不可以修改其中的内容呢?
不可以
这里的 p 指针指向的是一个常量字符串,是不允许修改的,如果非要进行修改就会运行出错。程序运行后会直接挂掉。
C语言|指针进阶超详解
文章图片

事实上:这里的常量字符串是储存在内存的常量区中的(只读),在里面的数据是不允许修改的。所以为了明确它不能被修改,我们尽量在前面加上 const 关键字修饰一下。
C语言|指针进阶超详解
文章图片

这样如果我们不小心,将其修改了,编译器也会告诉我们报错的信息。
3.字符串有关 常量区,栈区 创建区别 我们可以看看一下一道列题
int main() { const char* p1 = "abcdef"; //p1 指向 'a' 地址 const char* p2 = "abcdef"; //p2 指向 'a' 地址 char arr1[] = "abcdef"; //arr1 指向 'a' 地址 char arr2[] = "abcdef"; //arr2 指向 'a' 地址//以下是为了判断是否两个地址是相等的 if (p1 == p2) { printf("p1 == p2\n"); } else { printf("p1 != p2\n"); } if (arr1 == arr2) { printf("arr1 == arr1\n"); } else { printf("arr1 != arr2\n"); } return 0; }

它的输出是什么呢?
C语言|指针进阶超详解
文章图片

我们可以直观的看到p1 和 p2 指向的地址是相同的,arr1 和 arr2 指向的地址是不相同的。
两者的区别就在于这里:
p1 和 p2 指向的是常量字符串,是储存在常量区的,而常量的数据是不能被修改的,所以同一个数据不会被多次创建,只会在常量区进行一次数据的创建,相当于p1 、 p2 指向的是同一个内容。
而arr1 和 arr2 是在栈区创建了两个不同的空间,其内容是用字符串来初始化的,所以两者的地址不同。

三、 指针数组 1.定义 定义:指针数组是一个数组,是用来存放指针的一个数组。
int main() { int* arr[10]; //整型指针数组 char* ch[10]; //字符指针数组 return 0; }

解释:int* arr[10]; arr先与[10]结合,说明arr是一个数组,剩下的int* 说明它其中的元素类型是int*.
其他类型的指针数组解释方法和定义方法类似。
2.运用
int main() { int arr1[] = {1,2,3,4}; int arr2[] = {1,2,3,4}; int arr3[] = {1,2,3,4}; int* arr[10] = { arr1, arr2, arr3 }; return 0; }

这样可以模拟出来一个二维数组。
四、数组指针 1.预备知识 首先需要明确知道数组首元素地址和整个数组地址概念的差别:
一般情况下数组名表示数组首元素地址,除了两种情况:数组名放在sizeof内部,或者&取地址数组名时,表示的是整个数组的地址。
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; printf("%p\n", arr + 0); //数组首元素地址 printf("%p\n", arr); //数组首元素地址 printf("%p\n",&arr); //整个数组的地址 return 0; }

C语言|指针进阶超详解
文章图片

我们发现:三者的数值是一样的。
C语言|指针进阶超详解
文章图片

我们对三者进行加一处理,发现数值出现了不同,前两者加一,是加了四个字节,而整个数组地址加一是跳过了 (010FFC74 - 010FFC4C = 40)个整型,就相当于这里的整个数组。从这里我们得出了他们之间的差距。
2..定义 定义:数组指针是一个指针,是用来指向一个数组的指针。
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; int(*p)[10] = &arr; //一个整型数组元素为10个的指针 return 0; }

解释:p 先和 * 结合,说明 p 是一个指针,然后再和[10]结合,说明 p 指向的是一个元素为10个的数组,int说明元素类型是整型。
这里 p 指针的类型是:int (*)[10]。

五、 数组传参 和 指针传参 1.一维数组传参 形参可以写成数组,也可以写成指针。
void print(int arr[], int sz)//形参为数组 { int i = 0; for (i = 0; i < sz; i++) { printf("%d ",arr[i]); } } void print1(int* arr, int sz)//形参为整型指针 { int i = 0; for (i = 0; i < sz; i++) { printf("%d ",arr[i]); } } void print3(int (*p)[10], int sz)//形参为数组指针 { int i = 0; for (i = 0; i < sz; i++) { printf("%d ",(*p)[i]); } } int main() { int arr[10] = { 0 }; int sz = sizeof(arr) / sizeof(arr[0]); print(arr, sz); return 0; }

2.二维数组传参 形参可以写成数组,也可以写成指针。需要知道二维数组的首元素地址是它的第一行的地址。
void print(int arr[3][5], int c, int r)//形参为数组 { for (int i = 0; i < c; i++) { for (int j = 0; j < r; j++) { printf("%d ",arr[i][j]); } printf("\n"); } } void print1(int (*arr)[5], int c, int r)//形参为首元素指针 { for (int i = 0; i < c; i++) { for (int j = 0; j < r; j++) { printf("%d ", *((*arr + i) + j)); } printf("\n"); } } void print(int (*p)[3][5], int c, int r)//形参为二维数组指针 {} int main() { int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} }; print(arr, 3, 5); return 0; }

注意:形参不能用二级指针,二级指针是一个指针变量的地址,而二维数组的元素是一行一行的,是一维数组,类型应该是一维数组指针的类型。
练习:
int* parr1[10]; //数组,每个元素的类型是int*int (*parr2)[10]; //指向数组的指针int (* parr3[10])[5]; //数组,每个元素的类型是:int (*p1)[5];


六、函数指针 1.定义 定义:指向函数的指针。
&函数名 == 函数名,函数的地址就是函数名。
void test(int a) {} int main() { void (*p1)(int) = test; void (*p2)(int) = &test; return 0; }

解释:p1 先和 * 结合,说明p1是一个指针,在和()结合,说明指向的是一个函数,void (int),声明函数的参数是int,返回类型是void
2.解引用调用函数指针 函数指针的解引用调用可以不写 * ,也可以不写 * 。
void test(int a) {} int main() { void (*p1)(int) = test; void (*p2)(int) = &test; (*p1)(2); p1(2); //这两种方式都可以调用函数 return 0; }

3.两个有趣的代码
int main() { //代码1 (*(void (*) ())0)(); //代码2 void (*signal(int, void(*) (int)))(int); return 0; }

代码1:
void (*)() 是一个函数指针类型;
(void (*)())0 相当于把 0 地址转化为了 这个函数的指针类型,就相当于再 0 地址处放了一个 该函数。
(* (void (*) (0))0)(); 相当于解引用0地址处的这个函数指针,然后进行传参。
代码2:
signal 先和 (int, void (*) (int))结合,说明他是一个函数,参数为int ,void (*)(int),现在我们明确了它的函数名和参数,还需要知道它的返回类型,先看一个函数,int Add(int),这个函数的函数名是Add,参数是int,把函数名和参数去掉后就是返回类型,同样,把这里的函数名和参数去掉,void (*)(int)这个就是它的返回类型。
方便理解我们可以简化一下代码2定义的函数
typedef void (*pf_t)(int); //重定义一下函数类型的名字void (*signal(int, void(*) (int)))(int); //修改后 pf_t signal(int, pf_t);

七、函数指针数组 1.定义 定义:是一个数组,存放函数指针的数组。
void test1(int a) {} void test2(int a) {} int main() { void (*p[2])(int) = { test1, test2 }; return 0; }

解释:p 先和 [2] 结合,说明 p 是一个数组,再和 * 结合,说明说明数组中每一个元素都是指针,再和()结合,说明指向的每一个元素都是函数类型的,去掉数组名p和[2]剩下void (*) (int) ,说明函数的返回类型是void,参数类型是int.
八、指向函数指针数组的指针 1.定义
定义:是指针,指向函数指针数组的指针。
void test() {} void test1() {}void test2() {} int main() { void (*pf[3])() = { test1, test2, test }; //函数指针数组 void (*(*p)[3])() = { &pf }; //函数指针数组指针 return 0; }

解释:(*p)说明p是指针,[]说明指向了一个数组,去掉函数名是void (*)(),说明指向的数组的元素类型是void (*)()类型函数的指针。
九、回调函数 1、定义
定义:通过函数指针来调用的函数
例如:将B函数的指针传给A函数,在A函数里面通过B函数的指针来调用B函数,那么这个B函数就是回调函数。
void B() { printf("hehe\n"); } void A(void (*pf)()) { pf(); //在A里面用其的函数指针来调用B,那么B就是一个回调函数 }int main() { A(B); //将B函数的地址传给A return 0; }

【C语言|指针进阶超详解】

    推荐阅读