目录
前言:
(整个涉及)
- 1. 字符指针
- 2. 指针数组
- 3. 数组指针
- 4. 数组传参和指针传参
- 5. 函数指针
- 6. 函数指针数组
- 7. 指向函数指针数组的指针
- 8. 回调函数
(本上半部分涉及)
- 1. 字符指针
- 2. 指针数组
- 3. 数组指针
- 4. 数组传参和指针传参
指针的基本概念:
- 指针就是个变量(指针变量),用来存放地址,地址唯一标识一块内存空间(内存会划分为小的内存单元,每个内存单元都有一个编号,这个编号就被称为地址,把地址也叫做指针(总的来说:内存编号 = 地址 = 指针)(指针或地址,要进行存储,就可以存放到指针比变量中(例如:int* p;)))。
- 指针的大小是固定的4/8个字节(32位平台/64位平台)(总的来说:多少位平台就决定了不同类型的指针只能具有一个固定的大小)。
- 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。(此条概念是极为重要的:这就是为什么同样大小指针变量前,必须需具备类型的意义)
- 指针的运算。
1. 字符指针
1.1 字符指针的一般使用
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'a';
return 0;
}
这里,就是将字符常量存储到开辟的char类型的空间ch中,然后将空间ch的第一个内存编号(地址)(char是一个字节,即是其本身的内存编号)放入为char*类型的pc中,即说明pc是指针变量,随后通过多指针变量pc的解引用(*)找到ch的空间,并将空间内容里的字符常量 ' w ' 更改为字符常量 ' a ' 。
1.2 字符指针的特殊使用
int main()
{
const char* p = "abcdef"; //这里是把一个字符串第一个字符的地址存入指针变量p中
printf("%s\n", p);
return 0;
}
从此运行代码,也可以证明指针变量p中存储就是字符串常量 " abcdef " 中首字符 ' a ' 的地址。而printf函数可以实现以传入地址进行向打印,然后打印到 ‘ \0 ’ 才停止,所以就可以打印出完正的字符串。(总的来说:字符串常量的存放是连续的,我们存放了第一个字符的地址,其实也就相当于存放了整个字符串常量的地址)
注意:
此处," abcdef "是字符串常量,在C语言中有规定,常量是无法进行修改的,于是:
这是一个错误的操作,会导致整个代码的崩溃,是一个非常危险的操作,于是为了预防此类错误,我们应该运用const进行修饰:
这样在后续的编写中就会进行警告(并且,也是无法编译过去的)。就可以进行一种保护,以防出现更改的情况。(并且,在有些编译器下,你不编译const就会报警告)
深入理解:
那就有一个问题了:这 " abcdef "是在哪里创建的?是内存本身就存在?还是因为我们这样写了,内存中才会出来这个字符串,并且让p指向它?
其实 " abcdef "是放到常量区的, 内存之中简易的来说,有栈区,堆区,静态区与常量区,而常量区是一经过创建就不能进行修改的,是只能进行读取的。
一道经典点的笔试题:
#include <stdio.h>
int main()
{
char str1[] = "abcdef";
char str2[] = "abcdef";
const char *str3 = "abcdef";
const char *str4 = "abcdef";
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;
}
可以试着做一做,看看答案是什么。
最终运行我们会发现,成功实现打印的就只有第一个if else语句的第二条与第二个if else语句的第一条。
str3 与 str4:
我们要知道,在前面我们已经说明 " abcdf "是常量字符串,是不能被修改的,并且其还存储于静态区中,于是对于编译器来说,无法被修改的两个完全相同的字符串常量,有没有必要用两个空间进行分别存储?答案当然是没有必要的,为节省空间编译器选择的是str3与str4,同时指向储存那条字符串常量的空间,于是本质上来说,str3 == str4。
str1 与 str2:
在str3 与 str4中是因为不能进行修改,并且完全相同,所以没有必要进行再次空间创建,然而我们要知道,str1 与 str2是用字符串 " abcdef "进行了初始化,其实可以被修改的,所以是要用不同空间进行存储(难道你能说,其由一个数据进行初始化,那后期的变化就必须一样吗?(存储到一个空间中,那他们就是同时显示那个空间里的内容))。
并且:
str1 与 str2所指向的内容是在栈区中的,str3 与 str4所指向的内容是在静态区的。
2. 指针数组
指针数组是一个存放指针的数组。
我们就来谈谈它所存在的意义:
如果我们需要运用大量的指针:
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* pa = &a;
int* pb = &b;
int* pc = &c;
return 0;
}
直接的书写就会使得代码显得极为的臃肿,这是极其不方便的,于是便有了指针数组:
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a,&b,&c };
return 0;
}
(空间是假设的,为了便于讲解与理解)
说白了,其就是数组,只是与普通数组的区别就是,这个数组的以元素就是指针。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a,&b,&c };
for (int i = 0; i < 3; i++)
{
printf("%d ", *(arr[i]));
}
printf("\n");
return 0;
}
需要注意的是 arr[ i ] 是地址,所以我们需要进行解引用。
不同样式的指针数组:
int* arr1[10]; //整形指针的数组
char* arr2[4]; //一级字符指针的数组
char** arr3[5];//二级字符指针的数组
//.......
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[3] = { arr1,arr2,arr3 };
return 0;
}
parr作为指针数组,就是将arr1的首元素地址存储在parr[0]中,arr2的首元素地址存储在parr[1]中,arr3的首元素地址存储在parr[2]中。其实,我们就可以发现:出了一个类二维数组。
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
便于理解(可以理解为):parr [ i ] [ j ] = ( * ( parr + i ) ) [ j ] = * ( parr [ i ] + j ) 。
(C语言在运行后,数组arr[ i ]会直接被转为:*(arr + i ))
3. 数组指针
数组指针是指针?还是数组?
是指针!(是一个能够指向数组的指针。)
int main()
{
int a = 10;
int* p = &a; //整型指针 - 指向整型的指针,存放整型变量的地址
char ch = 'a';
char* pc = &ch; //字符指针 - 指向字符的指针,存放字符变量的地址
return 0;
}
以普通的指针为例,那么这个所谓的数组指针,就是指向数组的指针,存放数组的地址。(数组指针的概念)
接下来,我们需要注意一个点:
#include <stdio.h>
int main()
{
int arr[3] = { 1,2,3 };
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%p\n", &arr[0]);
return 0;
}
虽然,三者最终取出来的地址是一样的,但是,却不是完全一样的意思,其中:arr,&arr[0]是仅仅是首元素的地址而已,而&arr是整个数组的地址,只不过,就算是整个数组,但也应该从首元素开始。
那就有一个问题了,我们知道 “ 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。” arr 与 &arr[0]是int*类型的指针,那&arr呢?于是便有了数组指针。
于是根据arr数组,是由3个int类型的元素组成,于是便有了数组指针:元素类型 (指针)[元素的个数]
根据操作符的属性:parr先与*结合,说明parr是指针,后*parr与[3]结合,说明*parr是一个元素有3个的数组的指针,而最前面的int,就说明指向的那个的具有3个元素的数组的每个元素的类型为int。所以parr就是指针,其指向了数组,其类型为int(*)[3]。
当数组存储的是指针的时候:
需要注意的点:
- 数组指针的方括号中,只能方常量表达式。
- 数组指针的方括号中的数字不能乱写,这代表了数组指针+-整数的步长,指针解引用操作的时候的权限。
- *parr相当于数组名,数组名又是首元素的地址,所以*parr就是&arr[0]。
数组指针的二维数组的运用:
这里就会提到为什么指针数组存储数组的地址时会很想二维数组。(二维数组的深层次理解)
首先我们来看看二维数组,例如:int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
二维数组的首元素的地址就是第一行的地址(本人是利用指针的方式进行理解的)
看不列,就看行。就将列忽略掉,这么不就是一个三个元素的指针数组?而且还是一个特别的指针数组,他是不同的数组却又是连续的,简直就是一个典型的指针数组!
干脆那就简单除暴一点:a = {1,2,3,4,5};b = {2,3,4,5,6};c = {3,4,5,6,7}。
那这样,这就是一个最最最普通的数组,那arr是首元素的地址,首元素是a,a又是{1,2,3,4,5},{1,2,3,4,5}又是一个数组,那它的地址是什么?是不是一二维数组第一行的地址?
//用于验证二维数组的数组名是否为第一行元素的地址
int main()
{
int arr[3][5] = { {1,2,3,4,5},{1024,3,4,5,6},{3,4,5,6,7} };
// ---- 为了便于观察
printf("%d\n", *(*(arr + 1)));
//*(arr + 1)跨过了5个int类型的空间
//* ( *(arr + 1) )对跨过了5个int类型的空间进行解引用
return 0;
}
这就说明:
二维数组名需要用一个函数指针来进行接收! (降维的操作)
void print2(int (*p)[5], int c, int r)
{
int i = 0;
for (i = 0; i < c; i++)
{
int j = 0;
for (j = 0; j < r; j++)
{
//p+i是指向第i行的
//*(p+i)相当于拿到了第i行,也相当于第i行的数组名
//数组名表示首元素的地址,*(p+i) 就是第i行第一个元素的地址
printf("%d ", *(*(p + i) + j));
//printf("%d ", p[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
//写一个函数,打印arr数组
print2(arr, 3, 5);
return 0;
}
注意:
当使用的是&arr时:
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
int (*ptr)[3][5] = &arr;
当数组指针与指针数组进行了结合:
与前面一个相似,只不过是将arr1,arr2,arr3中的各5个的int类型的变量,变为int*类型的指针变量。
注意:对于指针构建的理解方法
- 提取指针变量部分(是一个怎么样的指针变量)
- 提取指针变量部分后剩余的部分(指针变量具有怎样的一个类型)
例如:int* (*parr3[3])[5]。
- 提取指针变量部分:就是parr3[3],parr3与[]结合,说明是一个数组,然后方括号里面是3,说明是一个元素有三个的数组。
- 提取指针变量部分后剩余的部分:就是int*(*)[5],括号里的*说明parr3[3]是指针数组,而[5]说明其是一个指向数组的指针数组,而int说明被指向的指针都是int类型的元素。
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);
}
我们主要注意的是,在用数组进行接收的时候,其实[ ]里的数字没有什么意义 ,因为我们实参在进行传送的时候,根本不会创建一个新的数组,所以,对大小是没有必要的。
例如:
就入一个数组有10个元素,我接收的时候也就写5个,没有意义就是没有意义,就算是我写个1000,也一样。形参部分的大小是可以忽略的。
但是,这是十分不建议的,虽然也不是错的。
其实之所以有用数组接收的形式,是因为:C语言,为了使小白学的时候显得更加友好,毕竟以实参数组传出,再以形参数组进行接收,会十分易于理解,所以,对于如果已经已有一定水平的人来说,还是用指针接收好些。
4.2 二维数组传参
需要注意,对于行是没有什么意义的,但是列是不能被省略的,这也是C语言的语法规定。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//数组
void test(int arr[3][5]) //对
{}
void test(int arr[][]) //错
{}
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]);
//一级指针p,传给函数
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;
}
指针真的是C语言中最为奇妙的知识点,学好指针就能够很容易的更加深层次的理解。例如:这上半部分的指针数组与数组指针,我就感觉十分的奇妙,创建者们简直是一个天才。真想看看他们是怎么一步一步的创造出现在的程序语言。