引言:
“代码胜于雄辩。”——林纳斯·托瓦兹(Linus Torvalds)
初步介绍:
C语言中最难之一莫过于指针了,但是你看完我的博客,你会对指针有更深一步的了解。首先,我们都知道,指针就是一个变量,用来存放地址,指针就是地址,地址就是指针,地址是内存中的一块内存空间。
那么什么是指针变量呢?
我们可以用过&(取地址符号)取出变量的内存地址,将这个地址放到一个变量中,这个变量就是指针变量。
int main() {int a = 5;int* p = &a;//这里的p变量就是指针变量,a变量占用4个字节的空间,这里是将a的4个字节的第一个字节//的地址存放在p指针变量中return 0;}
所以,指针变量是用来存放地址的,地址是唯一标示一个内存单元的,同时我们要知道,指针的大小在32位平台是4个字节,在64位平台是8个字节。
为了在指针进阶我们有更好的理解,我们还需要知道指针的类型和指针的类型决定了指针的+-步长,防止有些人不知道,我们再来复习一遍。
什么是指针的类型呢?
变量有不同的类型,整型,浮点型,字符型等等,由于指针也是一个变量,所以指针也是有类型的。
char *pc = NULL;int *pi = NULL;short *ps = NULL;long *pl = NULL;float *pf = NULL;double *pd = NULL;
这里可以看到,指针的定义方式是: type + * 。
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
指针类型的意思就是决定指针+-的步长。
这里看到,不同的类型的指针向前或者向后走一步的步长是不一样的。
这里我们已经初步了解了指针的大致内容,接下来我们进入更高级的主题。
正文进入:
目录
引言:
“代码胜于雄辩。”——林纳斯·托瓦兹(Linus Torvalds)
初步介绍:
正文进入:
1.字符指针
2.指针数组
3.数组指针
3.1数组指针的定义
3.2&数组名和数组名的区别
3.3数组指针的使用
4.数组参数,指针参数
4.1一维数组传参
4.2二维数组传参
4.3一级指针传参
4.4二级指针传参
5.函数指针
6.函数指针数组
7.指向函数指针数组的指针
8.回调函数
9.指针和数组笔试题解析
10.指针笔试题
1.字符指针
//字符指针int main() {//通常字符指针前面用const修饰,后面的"abcdef"是常量字符串,不可以修改!//这里的p指针变量是后面字符串a的地址,是首元素地址const char* p = "abcdef";printf("%c\n", *p);//aprintf("%s\n", p);//abcdefreturn 0;}
这里很多人会误以为字符串abcdef的地址放到指针p变量,其实不是,本质是将字符串首元素的地址保存到p指针变量中,然后通过链式访问,打印出后面的字符串即可。
类似于这样->
经典面试题:
输出结果为:
分析此题:
在这里str3和str4指向的是同一个常量字符串,都放在静态区,所以指针指向同一块区域的相同的字符串,他们会指向同一块内存,所以str3和str4相同。但是str1和str2用相同的常量字符串去初始化不同的数组会开辟不用的内存块,所以str1和str2不同。
2.指针数组
指针数组,顾名思义,是一个数组,是一个存放指针的数组。
指针数组的定义:
int* arr1[10]; //整形指针的数组char *arr2[4]; //一级字符指针的数组char **arr3[5];//二级字符指针的数组
指针数组的应用:
//重点:指针数组 是数组存放指针的数组int main() {int arr1[5] = { 1,2,3,4,5 };int arr2[5] = { 2,3,4,5,6 };int arr3[5] = { 3,4,5,6,7 };int* arr[] = { arr1,arr2,arr3 };//int* p = arr;int i = 0;for (i = 0; i < 3; i++) {int j = 0;for (j = 0; j < 5; j++) {//printf("%d ", *(arr[i] + j));printf("%d ", arr[i][j]);}printf("\n");}return 0;}
这里我们定义一个指针数组用来保存三个数组的数组名,也就是数组首元素的地址,我们通过指针数组可以用多个一维数组来模拟实现二维数组。
3.数组指针
3.1数组指针的定义
数组指针是数组还是指针,是指针?
是指针。
int* p;//整型指针,指向整型数据的指针,存放整型变量地址
char* p;//字符指针,指向字符数据的指针,存放字符变量地址
所以数组指针就是指向数组的指针
//数组指针的定义int (*p)[10];
解释:
p和*结合,说明p是一个指针变量,指着指向一个大小为10个整型数据的数组,所以p是一个指针,指向一个数组,叫做数组指针。这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
3.2&数组名和数组名的区别
我们先看一段代码:
这里显示,对数组名加上取地址符号和不加取地址符号结果是一样的,那么这两个真的一样吗?
我们再来看一段代码:
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。
数组名该怎么理解呢?
通常情况下,我们说的数组名都是数组首元素的地址
但是有2个例外:
1. sizeof(数组名),这里的数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小。
2. &数组名,这里的数组名表示整个数组,&数组名,取出的是整个数组的地址。
3.3数组指针的使用
在一维数组的使用:
//这种写法不建议,只是举例演示数组指针void print(int(*p)[10], int sz) {int i = 0;for (i = 0; i < sz; i++) {//这里*p相当于是数组名,数组名又是首元素地址,是&arr[0]printf("%d ", *(*p + i));}}int main() {int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//写一个函数打印这个数组的内容int sz = sizeof(arr) / sizeof(arr[0]);print(&arr, sz);return 0;}
在二维数组的使用:
void print(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行的数组名,也是第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,} };//写一个函数,打印这个数组//利用数组指针//int(*p)[3][5];//在这里arr表示的是二维数组中第一个一维数组的首地址print(arr, 3, 5);return 0;}
这里函数传参我们传递的是二维数组的数组名,二维数组的数组名也就是第一行一维数组的数组名,所以相当于传递的是二维数组的第一个一维数组,由于传递的是整个数组,我们用数组指针来接收,这个数组指针保存的就是这个二维数组的第一个一维数组的地址,然后我们通过解引用操作,来拿到我们二维数组中的数据。这个例子中,指针变量p保存的就是第一个一维数组的地址,通过解引用后,拿到了第一个一维数组。
4.数组参数,指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1一维数组传参
传递一维数组的时候,既可以写成数组的形式,也可以写成指针的形式,本质都是把数组首元素的地址传递过去。
4.2二维数组传参
二维数组传参我们传递的是二维数组第一个一维数组的地址,数组的地址用数组指针来接收,或者以二维数组的形式来接收,但是只能省略行,不能省略列,因为二维数组在内存中是连续存储的,知道多少列,就知道有多少行。
4.3一级指针传参
一级指针传参,我们就用一级指针来接收就好了。
4.4二级指针传参
二级指针传参用二级指针来接收就好啦。
5.函数指针
数组指针是指向数组的指针,那么函数指针,顾名思义,接收指向函数的指针
函数指针的定义:
//数组指针:指向数组的地址//函数指针:指向函数的地址int Add(int x, int y) {return x + y;}int test(char* p) {}int main() {printf("%p\n", Add);printf("%p\n",&Add);//在这里,函数名就相当于函数的地址,在前面加取地址符号&与不加结果是相同的int(*p)(int, int) = Add;//这里p就是函数指针变量//int ret = (*p)(2, 3);//这里相当于调用Add函数,*p就是Add//int ret = Add(2, 3);//这里与上面式子效果相同,性质一样int ret = p(2, 3);//前面的*只是修饰作用,带不带都一样的int(*pt)(char*) = test;//pt就是函数指针变量//int(*)(char*)就是函数指针变量的类型return 0;}
int(*p)(int, int) = Add;就是函数指针的定义,首先指针变量p和*结合表示这是一个指针,后面是函数的参数,前面是函数的返回类型。int(*)(char*)就是函数指针变量p的类型。在这里,&函数名和函数名两个本质上是一样的,同时,我们利用函数指针来调用函数用*p和p都可以调用这个函数,此时的这个*仅仅只是修饰作用。
6.函数指针数组
既然学习了函数指针,那么存放函数指针的数组就是函数指针数组。
那么函数指针数组是如何定义的呢?我来举个例子。
//函数指针数组//这个数组里面存放是函数指针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;}int main() {int(*p1)(int, int) = Add;int(*p2)(int, int) = Sub;int(*p3)(int, int) = Mul;int(*p4)(int, int) = Div;//函数指针数组int(* p[4])(int, int) = {Add,Sub,Mul,Div};int i = 0;int ret = 0;for (i = 0; i < 4; i++) {//ret = (*p)[i](2, 3);//这里的数组指针前面不可以加*errret = p[i](2, 3);printf("%d\n", ret);}return 0;}
在这里我们定义了四个加减乘除的函数,我们将这四个函数的地址全部保存在不同的函数指针,然后再将这四个不同的函数指针全部放在函数指针数组里面,int(* p[4])(int, int) = {Add,Sub,Mul,Div};就是我们定义的函数指针数组,因为[]的优先级比*高,所以指针变量p先和[]结合,表示这是一个数组,int(* )(int, int) 就是p的类型。
7.指向函数指针数组的指针
顾名思义,就是再次指向函数指针数组的指针,指向函数指针数组的指针是一个指针
指针指向一个数组 ,数组的元素都是函数指针。
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;}int main() {//函数指针数组int(*parr[4])(int, int) = {Add,Sub,Mul,Div};//指向函数指针数组的指针//相当于把parr换成*p//这里的p是指向函数指针数组的指针//p存放的是整个数组的地址!!!int(*(*p)[4])(int, int) = &parr;//怎么使用呢?int i = 0;for (i = 0; i < 4; i++) {//这里对p指针变量解引用,相当于为函数指针数组的数组名int ret = (* p)[i](2, 3);printf("%d\n", ret);}return 0;}
8.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数
的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进
行响应。
简而言之,就是在一个函数去调用另一个函数,这里的另一个函数就是那个回调函数。
这里,这个test就是我们所说的那个回调函数。回调函数是应用在函数指针上面的。
在回调函数中,有一个十分重要的库函数qsort函数,是库函数里面的一个函数,能够将各种类型的数据按照升序排列,库函数里面是采用快速排序法实现,今天我们用冒泡排序法的方法模拟实现一下。
void qsort(void* base,//待排序数据的起始位置 size_t num,// 数组的元素个数 size_t width,//数组每个元素的字节大小,比如int就是4个字节int(*cmp)(const void* e1, const void* e2)//函数指针//这里使用qsort函数需要我们自定义一个比较函数cmp );//e1和e2是待比较的两个元素的地址
qsort函数重点就是第四个参数,函数指针,让使用者传递的是我们自己想要的比较函数,也是这里运用到了回调函数的使用。cmp参数是qsort函数排序的核心内容,它指向一个比较两个元素的函数,注意两个形参必须是const void *型,同时在调用cmp函数(cmp实质为函数指针,这里称它所指向的函数也为cmp)时,传入的实参也必须转换成const void *型。在cmp函数内部会将const void *型转换成实际类型。
//模拟实现qsort函数//形参依次为数组比较起始元素位置,数组元素数量,一个元素的字节大小,还有传递的函数指针,比较函数int cmp_arr(const void* e1,const void* e2) {return ( * (int*)e1 - *(int*)e2);}void swap(char* bul1,char* bul2 ,int width) {int i = 0;//交换//假设交换9 和 8//转化为char*类型9和8再内存中是这么储存的 09 00 00 00 08 00 00 00//一次交换一个字节 09和08交换 然后是00 和00 交换,.......依次交换,交换次数计算数组元素的字节大小for (i = 0; i < width; i++) {int tmp = *bul1;*bul1 = *bul2;*bul2 = tmp;bul1++;bul2++;}}void bubble_sort(void* base,int num,int width,int(*cmp)(const void* e1,const void* e2)) {int i = 0;for (i = 0; i < num - 1; i++) {int j = 0;for (j = 0; j 0) {//交换!!!!swap((char*)base + j * width, (char*)base + (j + 1) * width, width);}}}}int main() {int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr,sz,sizeof(arr[0]),cmp_arr);int i = 0;for (i = 0; i < sz; i++) {printf("%d ", arr[i]);}return 0;}
回调函数可以根据程序员想要不同的比较方式写不同的函数都可以传递进去,真正做到了高效率使用。
9.指针和数组笔试题解析
int main() {int a[4] = { 1,2,3,4 };//数组指针来保存整个数组的地址int(*p)[4] = &a;//sizeof是一个操作符,不是一个函数//计算的是对象所占内存空间的大小,单位是字节,返回类型是size_t//不在乎内存中放的是什么,只在乎所占内存空间大小printf("%d\n", sizeof(a)); //16 这里取的是整个数组的大小的字节,为4*4=16printf("%d\n", sizeof(a + 0)); //4/8这里的a+0表示是数组第一个元素的地址,地址在内存都是4或者8个字节printf("%d\n", sizeof(*a)); //4*a表示数组第一个元素printf("%d\n", sizeof(a + 1));//4/8这里的a+1表示是数组第二个元素的地址printf("%d\n", sizeof(a[1])); //4 这里a[1]表示为数组第二个元素printf("%d\n", sizeof(&a)); //4/8这是对整个数组取地址printf("%d\n", sizeof(*&a));//16对整个数组的地址解引用就是拿出来整个数组,整个数组的大小为16printf("%d\n", sizeof(&a + 1));//4/8这是对整个数组的末尾4后面的地址printf("%d\n", sizeof(&a[0]));//4/8这是对数组第一个元素取地址printf("%d\n", sizeof(&a[0] + 1));//4/8这是对数组第二个元素取地址//字符数组char arr[] = { 'a','b','c','d','e','f' };//这里面的数组只有6个字符,没有\0!printf("%d\n", sizeof(arr));//6数组里面有6个字符,一个字符占一个字节printf("%d\n", sizeof(arr + 0));//4/8这里arr+0代表字符'a'的地址printf("%d\n", sizeof(*arr));//1*arr代表字符'a'printf("%d\n", sizeof(arr[1]));//1这里arr[1]代表字符'b'printf("%d\n", sizeof(&arr));//4/8这里&arr是取整个字符数组的地址printf("%d\n", sizeof(&arr + 1));//4/8这里&arr+1取的是字符f后面的地址printf("%d\n", sizeof(&arr[0] + 1));//4/8这里代表字符'b'的地址//强调一下 strlen函数// 求的是字符串长度// 从给定的一个地址依次向后访问字符,统计\0之前字符出现的个数//strlen函数传参传递的是地址printf("%d\n", strlen(arr));//随机值因为没有\0。所以是随机值printf("%d\n", strlen(arr + 0));//随机值arr+0是首字符,往后找\0,由于没有\0,所以也是随机值printf("%d\n", strlen(*arr));// err这里传递的是字符a的ascll码值,编译器会默认传递地址进来,编译器会挂printf("%d\n", strlen(arr[1]));// err同理,这里传递的也是字符bprintf("%d\n", strlen(&arr));//随机值,这里传递的是整个数组的地址printf("%d\n", strlen(&arr + 1));//随机值这里传递的是数组末尾元素后一位的地址printf("%d\n", strlen(&arr[0] + 1));//随机值return 0;}
int main() {char arr[] = "abcdef";//这里的数组里面计算a b c d e f \0//有七个元素!printf("%d\n", sizeof(arr));// 7因为数组有7个元素,只有数组名是求的是整个字符数组的大小printf("%d\n", sizeof(arr + 0));//4/8这里的arr+0是字符'a'的地址printf("%d\n", sizeof(*arr));// 1 这里的*arr是字符元素'a'printf("%d\n", sizeof(arr[1]));//1这里的arr[1]是字符元素'b'printf("%d\n", sizeof(&arr));//4/8这里面放的是整个数组的地址printf("%d\n", sizeof(&arr + 1));//4/8 这里面放的是\0后一位的地址,+1跳过的是一整个数组printf("%d\n", sizeof(&arr[0] + 1));//4/8 这里面放的是字符b的地址printf("%d\n", strlen(arr)); // 6统计\0之前的个数printf("%d\n", strlen(arr + 0));//6 arr+0表示数组第一个元素地址printf("%d\n", strlen(*arr));//err *arr是字符a,这里传递的是字符a的ascll码值,编译器会默认传递地址进来,编译器会挂printf("%d\n", strlen(arr[1]));//err arr[1]是字符b,同理printf("%d\n", strlen(&arr));// 6 &arr是对整个数组取地址,但是地址和首元素地址一样,传递的参数一样printf("%d\n", strlen(&arr + 1));//随机值 这里传递的是\0后面一位的地址,不知道内存后面哪里出现\0printf("%d\n", strlen(&arr[0] + 1));//5 这里传递的是字符b的地址,依次往后5个到\0//一维数组的题目看完,看看指针类型的题目char* p = "abcdef";//这里的p指针变量保存的是字符串首个元素的地址printf("%d\n", sizeof(p));//4/8这里的p是指针,是地址printf("%d\n", sizeof(p + 1));// 1p+1是字符b,占一个字节printf("%d\n", sizeof(*p));//1*p是字符a,首元素,占一个字节printf("%d\n", sizeof(p[0]));//1 p[0]相当于*(p+0),是字符a,占一个字节printf("%d\n", sizeof(&p));//4/8对指针变量取地址,相当于二级指针,也是地址printf("%d\n", sizeof(&p + 1));//4/8这里是指跳过p之后的地址printf("%d\n", sizeof(&p[0] + 1));// 4/8这里是表示字符b的地址,&p[0]是‘a’的地址,&p[0]+1就是b的地址//二维数组int a[3][4] = { 0 };//二维数组的数组名是第一行数组的地址printf("%d\n", sizeof(a));//48这里的a是整个数组的大小,3*4*4printf("%d\n", sizeof(a[0][0]));//4第一行第一个元素的数据元素printf("%d\n", sizeof(a[0])); // 164*4arr[0]是第一行数组,这里计算的是第一行数组的地址printf("%d\n", sizeof(a[0] + 1));//4/8a[0]作为数组第一行的数组名,没有单独放在sizeof内部,也没有被取地址,//这里a[0]+1是第一行数组第二个元素的地址printf("%d\n", sizeof(*(a[0] + 1)));//4*(a[0]+1)是数组第一行第二个元素printf("%d\n", sizeof(a + 1));// 4/8 a是第一行数组地址,a+1是第二行地址printf("%d\n", sizeof(*(a + 1)));//16对第二行的地址解引用就是拿到第二行,第二行16个字节printf("%d\n", sizeof(&a[0] + 1));//4/8a[0]是第一行的数组名,&a[0]是取出第一行的地址,+1是取出第二行的地址printf("%d\n", sizeof(*(&a[0] + 1)));//16对第二行地址解引用拿到第二行整个数组printf("%d\n", sizeof(*a));//16a就是第一行数组的地址,解引用拿到第一行printf("%d\n", sizeof(a[3]));//16一行数组的大小// *a*(a+0)a[0]都表示为第一行数组名return 0;}
10.指针笔试题
// 例题一int main() {int a[5] = { 1, 2, 3, 4, 5 };int* ptr = (int*)(&a + 1);printf("%d,%d", *(a + 1), *(ptr - 1));//分析此题:&a+1是跳过一整个数组,是5后面的地址,将这个地址强制类型转换为int*类型,赋给ptr变量//*(a+1)是跳过一个,指向2的地址,然后解引用,打印出来是2//*(ptr-1)是向前移动一个位置,指向5的地址,然后解引用,打印出5return 0;}
//例题二int main(){int a[4] = { 1, 2, 3, 4 };int* ptr1 = (int*)(&a + 1);int* ptr2 = (int*)((int)a + 1);printf("%x,%x", ptr1[-1], *ptr2); // 4,20000000//分析此题//&a+1是跳过整个数组,也就是数组元素4后面一位的地址,将这个地址强制类型转换为int*类型,并传递给ptr1的这样一个指针变量// ptr[-1]是*(ptr-1),往前移动一位,指向4的地址,解引用后,打印4//(int)a + 1是将首元素地址强制类型转换为int类型//在内存中是这样存储的 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00//因为转换为int类型,所以+1跳过一个,指向第二个00 //所以*ptr2解引用后占四个字节为 00 00 00 02//又因为是%x打印,所以打印出来的结果是20000000return 0;}
//例题三#include int main(){int a[3][2] = { (0, 1), (2, 3), (4, 5) };int* p;p = a[0];printf("%d", p[0]);//分析此题://这题有坑,数组里面是逗号表达式,所以这个二维数组中只有三个元素,为1 3 5,其余元素均为0//p = a[0];是将数组第一行的地址传给p指针变量,p[0]=*(p+0),所以就是数组第一个元素//输出1return 0;
//例题四int main(){int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };int* ptr1 = (int*)(&aa + 1);//&aa+1是是跳过整个数组,是10后面一位的地址,传给指针变量ptr1int* ptr2 = (int*)(*(aa + 1));//aa是数组第一行的地址,+1表示是第二行的地址,解引用是数组名,也就是第二行首元素地址//将第二行首元素地址传给指针变量ptr2printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//ptr1-1是向前移动一位指向10地址,解引用为10,ptr2-1也是向前移动一位,跳到5的位置,解引用后为5return 0;}
//例题五#include int main(){//定义一个字符指针数组//这里a数组存的是每个字符串首字母的地址char* a[] = { "work","at","alibaba" };//a表示数组首元素地址的地址,是用二级指针来接收char** pa = a;//这里pa二级指针++,跳过一个char**,指向下一个//*p解引用后就是字符a的地址,也就是第二个字符串的首地址//然后通过链式访问,传递a的地址,向后一直打印出\0之前的字符pa++;printf("%s\n", *pa);//所以打印出的结果是:atreturn 0;}