史上最强C语言教程—-指针(进阶部分1)
目录
1. 字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char*
一般使用:
int main() { char ch = 'w'; char* pc = &ch; *pc = 'w'; return 0; }
还有一种使用方式如下:
int main() { const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗? printf("%sn", pstr); return 0; }
代码 const char* pstr = "hello bit.";
特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是本质上是把字符串 hello bit. 首字符的地址放到了pstr中。
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。
那就有可这样的面试题:
#include <stdio.h> int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char* str3 = "hello bit."; const char* str4 = "hello bit."; if (str1 == str2) printf("str1 and str2 are samen"); else printf("str1 and str2 are not samen"); if (str3 == str4) printf("str3 and str4 are samen"); else printf("str3 and str4 are not samen"); return 0; }
问:上述代码的输出结果是什么?
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当 几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化 不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
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* arr[] = { arr1,arr2,arr3 };//指针数组,每个元素都是指针(数组名代表首元素的地址) for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) { printf("%d ", *(arr[i] + j)); //上面的这一行也可以用下面这一行来代替 //printf("%d ", arr[i][j]); } printf("n"); } return 0; }
通过上面的这种方法可以实现遍历数组的元素!当然,其实这也是二维数组的本质所在,后面还会进行相关的讲解,相信大家在后面会对这些有一个更深入的理解,此处只是起到一个抛砖引玉的作用!
3.数组指针
3.1数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。 下面代码哪个是数组指针?
int *p1[10]; int (*p2)[10]; //p1, p2分别是什么?
解释:
int *p1[10];//此处就不再过多解释了,因为这就是我们上面刚才讲过的指针数组 int (*p)[10]; //解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。 //这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
3.2 &数组名VS数组名
对于下面的数组:
int arr[10];
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。 那&arr数组名到底是啥?
我们看一段代码:
#include <stdio.h> int main() { int arr[10] = { 0 }; printf("%pn", arr); printf("%pn", &arr); return 0; }
运行截图:
可见数组名和&数组名打印的地址是一样的。
难道两个是一样的吗? 我们再看一段代码:
#include <stdio.h> int main() { int arr[10] = { 0 }; printf("arr = %pn", arr); printf("&arr= %pn", &arr); printf("arr+1 = %pn", arr + 1); printf("&arr+1= %pn", &arr + 1); return 0; }
运行截图:
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40
3.3 数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
#include<stdio.h> int main() { int arr[10] = { 1,2,3,4,5 }; int(*ptr)[10] = &arr; for (int i = 0; i < 5; i++) { printf("%d ", *((*ptr) + i)); /*printf("%d ",(*ptr)[i]);*/ //上面的代码可以由下面的一行代码代替,因为(*ptr)就代表数组名,同时也代表着首元素的地址 } return 0; }
当然,上面的运用只是一种简单的运用,下面结合二维数组来给大家进行展示一下相关的运用!
#include<stdio.h> void print(int(*p)[5], int x, int y) { for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { printf("%d ", *(*(p + i) + j)); //printf("%d ", (*(p + i))[j]); //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} }; print(arr, 3, 5); return 0; }
大家应该会觉得相对来说不太好理解,下面我将简单给大家进行解释一下!
问题1:为什么在print函数那的形参会是数组指针的形式?
答:因为数组名代表首元素的地址,我们都知道,整型的一维数组的元素是整型,实际上我们在求数组元素类型时常常会发生类似降维一样的情况,即此处由一维降到了点,那么我们来进行类推,二维数组也应该降成一维数组,而数组名我们在前面已经了解到了就是首元素的地址,而数组的地址的类型就是数组指针!
总结:我们在看待二维数组的时候,要把它看成是由一维数组组成的,即二维数组的每个元素就是一维数组,我们在将这个结论扩展到多维时也同样适用,比如三维数组的元素就是二维数组,四维数组的元素就是三维数组,那么其数组名的意义我们就相应的能够了解到了,
问题2:为什么上面的四种形式能够进行互换?
答:首先大家先看第一个为什么行,p代表的是第一行,p+i就是第i行,我们对其进行解引用,就是拿到的是这一行的元素,实际上在此处就是代表的是一维数组的数组名,而数组名又是数组的首地址,将其加上i后就是一维数组第i个元素的地址,再对其进行解引用,我们就得到了第i行第j列的元素。
为了帮助大家理解上面的等价替换,下面会给大家举个例子!
#include<stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; for (int i = 0; i < 10; i++) { printf("%d ", p[i]); //printf("%d ", arr[i]); //printf("%d ", *(p + i)); //printf("%d ", *(arr + i)); } return 0; }
p[i] == arr[i] == *(p+i) == *(arr + i)
上面的四种形式其实是等效的!
下面我们进行类比一下,其实我们就能明白上面的四种形式为什么会相同!
*(*(p + i) + j)) == (*(p + i))[j]) == p[i][j]) == *(p[i]+j))
其实这个地方也不难,只是由一维扩展到了二维!
4. 数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1 一维传参
4.1.1. 一维数组传参
一维数组传参有三种方法,下面给大家列举出来
#include<stdio.h> void print(int arr[5])//方法一 //void print(int *arr)方法二 //void print(int arr[])方法三 { } int main() { int arr[5] = { 1,2,3,4,5 }; print(arr); return 0; }
注意:其实这三种传参方式本身并没有什么区别,其实无论是上面的哪一种方式进行传参,其本质上都是通过指针的方法进行传参,就像方法二一样,所以在方法一中的数字,写什么都是可以的,并没有任何的问题,在后续的使用上也并没有任何的区别。
4.1.2 一维指针数组传参
#include<stdio.h> void print(int* arr[5])//方法一(当然,括号中的5也可以不写,也可以随便写一个数) //void print(int **arr)方法二(因为数组的元素是指针,而数组名代表首元素的地址,指针的地址就是二级指针) { } int main() { int* arr[5]; print(arr); return 0; }
4.2 二维数组传参
#include<stdio.h> void print(int arr[2][3])//方法一(中规中矩的二维数组传参,此处一定要注意,行可以省略掉,但是列一定不能省略,同时需要注意,行可以随便写,但是列一定要与原来的数组保持一致,至于为什么,看下一种方法,即本质就能明白,因为列就是数组类型的一部分) //void print(int (*arr)[3])方法二(也就是二维数组传参的本质所在) { } int main() { int arr[2][3] = { {1,2,3},{4,5,6} }; print(arr); return 0; }
4.3 一级指针传参
#include <stdio.h> void print(int* p, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%dn", *(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; }
思考:
当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
void test1(int* p) {} //test1函数能接收什么参数? /* int a = 1; int *p = &a; int arr[] = {1,2,3,4,5}; test1(&a);//可以 test1(p);//可以 test1(arr);//可以 */ void test2(char* p) {} //test2函数能接收什么参数? /* char ch = 'w'; char *p = &ch; char*arr[] = "abcde"; test2(&ch);//可以 test2(p);//可以 test2(arr);//可以 */
4.4 二级指针传参
#include<stdio.h> void print(int** pp) { } int main() { int a = 10; int* p = &a; int** pp = &p; print(&p); print(pp); return 0; }
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
首先可以比较清楚的了解到上面的这两种传参方式肯定是没有问题的,但是除了上面这两种之外还有别的传参方式!
#include<stdio.h> void print(int** pp) { } int main() { int* p[5]; print(p); //p是指针数组,数组的每一个元素都是指针,而我们传的是指针数组的数组名,即指针数组的首元素的地址 //指针数组的数组的首元素的地址即指针的地址,其类型就是二级指针 return 0; }
5. 函数指针
首先看一段代码:
#include <stdio.h> void test() { printf("hehen"); } int main() { printf("%pn", test); printf("%pn", &test); return 0; }
下面是程序的运行结果:
输出的是两个地址,这两个地址是 test 函数的地址。
这个地方相信大家就会想问了,那么这两个代表的意义时候完全相同呢?还是说像数组一样,数组名和&数组名代表不同的含义呢,这个地方就给大家说明白,函数名和&函数名代表着相同的含义,表示的都是函数的地址,其数值表现形式也都是函数的地址,两者没有任何的区别,在使用上也没有任何的区别!
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
void test() { printf("hehen"); } //下面pfun1和pfun2哪个有能力存放test函数的地址? void (*pfun1)(); void* pfun2();
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
在这个地方相信大家还是不怎么理解,这里给大家进行解释一下,我们知道,对于数组来说,我们把在函数定义时的语句的变量名去掉就能得到定义的变量的类型,对于pfun1来说,我们将变量名去掉后,剩余的部分是void (*)(),如果我们将pfun2去掉之后,剩余的部分是 void *pfun()。
好像这两个乍一看并没有太大的区别,在它们进行定义时的唯一的区别就是pfun1比pfun2多了一个(),我们清楚,()的优先级是大于*的,那么在*pfun1左右加上括号之后,*就将与pfun1变量名进行结合,这就说明了pfun1是一个指针变量而去掉变量名之后,就是一个函数,这就说明了pfun1是一个指向函数的指针变量,所以能够存储函数的地址。
接下来带大家来看一下,pfun2到底是一个什么!因为pfun2的左侧的操作符是*,而右侧的操作符是(),很明显,()的优先级比*要高,所以pfun2先与()进行结合,构成函数,而没有形成指针变量。void*是函数pfun2的返回类型,此处我们就可以进行下结论了,即pfun2是一个函数名,函数的返回类型是void *类型。
这个地方相信已经给大家讲明白了!其实这些在清楚的了解了那些操作符的优先级和结合性顺序之后也并不难理解!
既然我们已经理解了函数指针的相关知识,我们就先简单的运用一下吧!
#include<stdio.h> int add(int a, int b) { return a + b; } int main() { int (*padd)(int a, int b) = add; int sum = (*padd)(3, 5);//方法一 //int sum = padd(3, 5);方法二 printf("%d ", sum); return 0; }
那么我们该如何取理解上面的两种调用方法呢?其实也不难,我们取理解一下函数名的概念就能比较轻松的理解上面的两种调用方法,函数名和&函数名代表的含义是相同的,都是表示的是函数的地址,下面我会再给出一段代码来帮助大家进行理解!
#include<stdio.h> int add(int x,int y) { return x + y; } int main() { int (*p)(int x, int y); p = add;//当然,此处也可以写成p = &add //从上面这段代码中其实就可以明白,其实p和add是几乎完全相同的,所以它们的用法也是差不多完全相同的 int sum = p(3, 4); //上面这一行可以用下面的三种形式进行代替 //int sum = (*p)(3, 4);方法一 //int sum = add(3, 4);方法二 //int sum = (*(&add))(3, 4);方法三 return 0; }
当然,如果你不相信的话,可以自己在编译器上试一试就ok!