指针的概念
指针是用来存放地址的变量,存放的地址标识唯一一块内存空间。系统中的内存就像带编号的宿舍,想找到这个宿舍的位置,就要知道宿舍门牌号,程序中定义一个变量,再进行编译时就会给该变量在内存中分配一个地址,这个地址就是一个宿舍,通过访问这个宿舍就可以找到想要找到的数据,指针就是这个宿舍的门牌号。
当然,这个指针也是一个变量,叫做指针变量,只不过这个变量里存放的是另一个变量的门牌号,即我们所需要的变量的地址。
指针的使用
指针变量的形式和赋值
int a = 0; int* b = 0; b = &a;
&取地址操作符,取出a的地址赋给b,此时b就是a的指针。
指针变量的类型为 类型*变量名。(*表示该变量位指针变量)
在定义一个指针变量一定要记得为他赋值,不然就变成了一个野指针,指向随机的数,贸然使用就会引起错误。
赋值有两种方法
可以定义时同时赋值
int a; int *p = &a;
也可以先定义,后赋值
int a; int *p; p=&a;
一定要注意哇,p是指针变量
错误赋值:
int *p;
p=1000;
这个语句p就指向了内存编号为1000的位置,而内存编号为1000的地址存放的是什么我们不得而知,在初始化时要明确指向的地址是我们所知道的,不可以乱修改指针变量的值。
指针变量的引用
引用指针变量是对变量的间接访问的一种形式
*指针变量
解引用可间接访问指针变量所指向的变量。扩展一个内容,指针可以修改const修改的变量,如何解决这个漏洞呢?看这里
字符指针
字符指针的类型为 char*
来看如下代码
int main() { const char* pstr = "hello world"; printf("%s\n", pstr); return 0; }
运行后如下
很容易让我们错误的理解是把字符串hello world放在了字符指针里了,但本质是把字符串hello world的首字符的地址放进了pstr中。
调试观察验证
接下来再看一题
int main() { char str1[] = "hello world"; char str2[] = "hello world"; const char* str3 = "hello world"; const char* str4 = "hello world"; if (str1 == str2) { printf("str1 and str2 are same\n"); } else printf("str1 and str2 are not same\n"); if (str3 == str4) { printf("str1 and str2 are same\n"); } else printf("str1 and str2 are not same\n"); return 0; }
运行结果如图
str3和str4指向的是同一个常量字符串,C/C++会把常量字符串存储到单独的内存区域,不管有几个指针如果指向了同一个常量字符串,那么他们会指向同一块空间,但是用相同的字符串常量去初始化不同的数组的时候就会开辟出不同的内存块,所以运行结果前两个不同,后两个相同。
指针加减法和类型关系
我们都知道整型变量在内存中占4个字节,short短整型在内存中占两个字节,指针的自加自减不是加一减一这么简单,通过一个例子来说明。
发现不同了吗,int类型的指针++后地址增加了4个字节,而端正型short++后增加2个字节。
可以控制指针类型,char类型要走16步的,用int只需要走4步,可以快速快速得到想要的地址的变量。
如:
在字符数组中直接用整形指针,++后跳过四个字节,得到u。
数组和指针
数组的数组名为数组首元素地址,如果我们把首元素地址赋给一个指针变量,就可以通过这个指针变量来找到这个数组的每个元素。
一维数组和指针
当定义一个一维数组时,系统就会在内存中为该数组分配一个储存空间,这个数组的名称就是数组在内存的首地址,如果把该地址赋给一个指针变量,那么这个指针就指向了这个数组,
例如
int*p;
int a[10];
p=a;
也可以这样:
int *p;
int a[10];
p=&a[0];
都是将数组首元素地址赋给一个指针变量。
int main() { int* p = NULL; int arr[10] = { 0,1,2,3,4,5,6,7,8,9 }; p = arr; for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } printf("\n"); for (int i = 0; i < 10; i++) { printf("%d ",*p++); } return 0; }
通过解引用和++操作可以找到下一位的地址,从而访问数组中的每个元素。
他们对应的关系如图所示;
二维数组和指针
首先要了解二维数组
创建一个二维数组a[3][5]。
默认数组名a为第一行的地址,a+1就是第二行的地址。a[0]+n表示第0行第n+1列的元素的地址。a[0]+1就是第一行第二列的元素的(地址)解引用后才是数组中所存放的元素。
a[[0][0]既可以看作数组零行零列的元素,a[m][n]就表示数组m+1行n+1列的元素。(数组的计数是从零开始,而我们所说的行和列是从1开始)
举一个例子我们来观察数组元素在内存中的位置
int main() { int a[3][5]; int i; int j; for (int i = 0; i < 3; i++) { for (j = 0; j < 5; j++) { scanf("%d", a[i] + j); } printf("\n"); } //打印数组 printf("the array is:\n"); for (int i = 0; i < 3; i++) { for (j = 0; j < 5; j++) { printf("%d ", a[i] + j); } printf("\n");//打印每一列的元素后换行 } return 0; }
在这里我们打印的是数组元素的地址。可以发现他们在内存中是连续存储的。
只观察每一行第一个元素的地址也可以看出来。
//打印数组 printf("the array is:\n"); for (int i = 0; i < 3; i++) { printf("%d ", a + i); printf("\n");//打印每一列的元素后换行 }
此时a表示第一行的数组的地址,依次加 i 后找到二维数组的确定的每一行元素的地址。因为每一行有五个int类型的数,所以依次相差20。
对比两种写法
*(*(a+n)+m)表示第n+1行第m+1列元素。
*(a[n]+m)也可以表示第n+1行第m+1列元素。
(1)a+n表示第n+1行的地址,解引用找到第n+1行的首元素地址,加上m后偏移到第n行的第m个位置,再次解引用就得到了第n+1行第m+1列的元素。
(2)a[n]表示的也是第n+1行的地址,加上m解引用后也找到了第n+1行第m+1列的元素。
字符串与指针
利用指针指向一个字符串。
要注意的是,我们用hello world为其赋值,并不是将整个字符串放进string中,只是把字符串的第一个字符的地址赋给指针变量string,由这个指针变量就可以找到这个字符串的所有内容。
上述代码就是把h的地址存放在了指针变量中。
字符指针初始化
char *string="hello world";
这里字符指针的用法和指针差别不大,但知道了字符串首个字符的地址,利用%s可以将整个字符串打印出来。因为是char类型,所以每个符号在内存中占用一个字节。
有一题值得我们看一看
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <assert.h> int main() { char str1[] = "hello world"; char str2[] = "hello world"; const char* str3 = "hello world"; const char* str4 = "hello world"; 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; }
运行结果如何呢?
str1和str2虽然是两个内容一样的字符数组,但编译器在执行时创建了两块区域,分别存放这两个数组中的元素,用相同常量字符串初始化不同的数组时,通常要开辟出不同的内存块。
而str3和str4是两个指向同一字符串的指针,因为指向的内容相同,所以只会创建单独的区域存放这串字符,将两个指针同时指向这块空间。
指针数组
像整型数组中存放的是整型变量,字符数组里存放的是字符变量一样,指针数组是存放指针变量的数组,由于指针也是变量,当然可以把它们像变量一样存储到数组里,数组的定义是数据类型加数组名加元素个数;
int* arr1[10];
char* arr2[10];
char**arr3[10];//二级字符指针的数组
指针数组的作用就是将数组和指针结合使用。
例如
name[0]里存放的字符类型指针指向首元素地址。利用%s即可打印该字符串。
二级指针
二级指针就是指向指针的指针,指针可以指向各种类型的数据,当然也可以指向指针变量,取出一个指针变量的地址,赋给另一个指针变量,那么这个指针就称作二级指针。
如下图
二级指针的定义如下
类型标识符 **指针变量名
例如:int **p;
利用一个不是很恰当的例子来说明一下二级指针的用法
找出数组中的偶数且保存其个数
这里把数组a的首元素地址赋给指针变量p1,再将指针变量的地址赋给p2,通过双指针变量p2来完成相关操作(直接用p1就可以,这里麻烦一点是为了更好的体现出二级指针的讲解),我们来一层一层的分析,首先*p2的含义,*p2是指针变量p1所存的东西,也就是数组a的首地址,要想取出数组内的元素,就必须在*p2前再加上一个解引用操作符,在每次循环过程中++i就可以依次找到数组的各个元素。
数组指针
上边我们讲了指针数组,是存放指针的数组,数组指针,顾名思义是指向数组的指针。
下边哪个是数组指针呢?
int*p1[10];
int (*p1)[10];
第一个是指针数组,第二个才是数组指针。
解释如下
优先性的问题,不加括号的话,p和 [ ] 先结合,变成一个数组,int*表示数组内存放的数据类型,故为指针数组,如果加上括号,p与*先结合,说明p是一个指针变量,然后指向的是一个大小为10的整型数组。所以p是一个指针,指向一个数组的指针。([ ]的优先级高于*)。
&数组名VS数组名
对于数组名的理解,通常我们会认为就是数组首元素的地址,但也有两个例外。除了下边要讲的&数组名,还有就是sizeof (数组名)
例如
sizeof(数组名)这里的数组名不再是首元素地址,而是整个数组。
举出一例 int arr[10];
对于该数组,我们知道arr是数组名,&arr是数组的首元素地址。这两个有区别吗?
打印出来后发现他们两个一模一样,真的一样吗?
将其各进行加1
比较后我们可以发现,其实&arr和arr虽然值是一样的,但意义却并不一样。
实际上&arr表示的是数组的地址,而不是数组首元素的地址,&arr的类型其实就是刚刚介绍的数组指针,所以&arr+1就跳过整个数组,相差的值为40。
除去以上两个例外,其他的数组名表示的都是数组首元素地址。
来看一看这几串代码的含义
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
第一个是指针数组,第二个是数组指针,第三个呢
*与parr3[10]结合,前边说明他是一个数组指针,后边与[5]结合,说明是一个装着数组指针的数组。
函数指针
指针指向的内容是函数,那么这个指针就可以叫做函数指针,函数也是有地址的。
函数指针的形式
返回类型(*函数名)(参数);
void (*ptest)();
其中*先于ptest结合,说明ptest是指针,指针指向的是一个函数,指向的函数没有参数,返回类型为空。
函数指针数组
类比一下
int *arr[10];
以上是一个整型指针数组,数组的每个元素都是整形指针。
只要把函数的地址存放到一个数组中,这个数组就叫做函数指针数组,如何定义呢?
例如:
int (*pf[10])(int int)
前边说过[]的优先级大于*的优先级,pf先和[]结合,说明pf是一个数组,再与*号结合,说明他是一个指针,这个函数指针数组内的函数指针的参数为两个int型,函数的返回值为Int型。
如何使用函数指针数组呢?
举一个例子对比一下
用代码写一个简易的计算器
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.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; } int main() { int a = 0; int b = 0; int input = 0; int ret = 0; do { printf("#####################"); printf("####1,add 2,sub#####"); printf("####3,mul 4,div#####"); printf("####0,exit###########"); printf("请输入要使用的算法:\n"); scanf("%d", &input); switch (input) { case 1: scanf("%d %d", &a, &b); printf("%d\n", add(a, b)); break; case 2: scanf("%d %d", &a, &b); printf("%d\n", sub(a, b)); break; case 3: scanf("%d %d", &a, &b); printf("%d\n", mul(a, b)); break; case 4: scanf("%d %d", &a, &b); printf("%d\n", div(a, b)); break; default: printf("输入错误\n"); } } while (input); return 0; }
如果使用函数调用的方法,就要用到switch case语句,明显代码行长,且比较麻烦,接下来借助函数指针数组的方法进行调用,看代码
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.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; } int main() { int a = 0; int b = 0; int input = 0; int ret = 0; do { printf("#####################\n"); printf("####1,add 2,sub#####\n"); printf("####3,mul 4,div#####\n"); printf("####0,exit###########\n"); printf("请输入要使用的算法:\n"); //int (*pf1)(int, int) = add;//函数指针变量 //int (*pf2)(int, int) = sub;//四个函数传入的类型及返回值都一样 //int (*pf3)(int, int) = mul;//何不把他变成一个指针数组? //int (*pf4)(int, int) = div; int (*p[5])(int x,int y) = { 0,add,sub,mul,Div}; scanf("%d", &input); if ((input <= 4 && input >= 1)) { printf("输入操作数\n"); scanf("%d %d", &a, &b); ret = (*p[input])(a,b); } else { printf("输入有误\n"); } printf("%d\n", ret); } while (input); return 0; }
相对于前边所写的代码明显不那么赘余,观赏性更高。