1.指针是什么?
指针是什么?
指针的理解有两点:
1.指针是内存中一个最小单元的编号,也就是地址
2.平时口语中说的指针,通常是指针变量,用来存放内存地址的变量。
总结:指针就是地址,口语中说的指针通常指的是指针变量。
计算机中把内存分为一个一个的内存单元,每个内存单元的大小为一个字节,并对每个内存单元进行编号(也叫地址),方便管理和维护。
写C语言程序时,创建的变量、数组等都要在内存中开辟内存空间。
而要找到一个变量或者数组的地址,就要用取地址(&)操作符。
int a = 100; printf("%d\n", &a);
通过&(取地址操作符)取出变量的起始地址,把地址存放在一个变量中,这个变量就是指针变量。(存放在指针变量中的任何值都会被当成地址处理)
int*pa = &a;
这里的pa就是指针变量。在c语言中,编号 = 地址 = 指针
那么地址是怎么产生的?
在32位计算机中,有32根地址线,而每根地址线在寻址时都会产生高低电平的转换(即1/0的转换),那么32位地址线产生的地址就是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就有2^32个地址。
每个地址标识一个字节,那么我们就可以对(2^32Byte == 2^32/1024KB ==
2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB)4G的空间进行编址。
同样的方法也可以算一算64位机器能对多大空间进行编址。
在32位机器中,地址是32位0/1二进制序列,那地址就需要用4个字节的空间来存储,所以在32位机器中,指针变量的大小是4个字节。
在64位机器中,地址是64位0/1二进制序列,那地址就需要8个字节的空间来存储,多以在64位机器中,指针变量的大小是8个字节。
32位机器中,指针变量的大小:
64位机器中,指针变量的大小:
代码中的各个指针变量的类型不同,但是在同一机器中,不论是什么类型的指针变量,其大小都是相同的。
2.指针和指针类型
我们以前学的变量有整型,字符型等各种类型,那么指针有类型吗?
答案是肯定有的。
前面说过,同一机器中不论什么类型的指针变量,其大小都是相同的,那么我们在写代码时为什么还要分int*、char*、double*......呢?
指针类型的意义是什么呢?
2.1指针的解引用
下面我们来看一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int a = 0x11223344; int* pa = &a; *pa = 0; return 0; }
0x:代表16进制的数字。
调试并打开内存窗口可以看到:
红框中数值就是a在内存中的存储,用4个字节存储。 注意内存中的数字是倒着放的,这个原因以后会讲。
补充解释一下内存窗口个部分的含义:
当调试到*pa = 0;这一步时,可以发现内存中的4个字节空间中的数字全变为0。
那如果我们把指针的类型改为char型呢?
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int a = 0x11223344; char* pa = &a; *pa = 0; return 0; }
这时再打开内存窗口调试到*pa = 0;这一步会发现:
只有一个字节空间的数字变为0。
根据以上内容我们可以发现:int* 的指针解引用访问4个字节,char* 的指针解引用访问1个字节。
由此可得出结论:指针类型可以决定指针解引用的时候访问多少个字节(指针的权限)。
那么short* 的指针解引用访问2个字节,float* 的指针解引用访问4个字节,double* 的指针解引用访问8个字节。
我们在定义指针变量是用:type * p;其中 * 表明了p是指针变量。type表明了指针所指对象的类型,而且p解引用的时候访问的对象的大小是typeof(type)
2.2指针+/-整数
下面再看一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int a = 0x11223344; int* pa = &a; char* pc = &a; printf("%p\n", pa); printf("%p\n", pc); printf("%p\n", pa+1); printf("%p\n", pc+1); return 0; }
运行结果:
通过打印结果我们可以发现对存放地址的指针变量pa和pc同时进行加一操作,输出的地址pa跳过4个字节,pc跳过1个字节。
由此可得指针类型的第二个作用:指针类型决定指针+/-1操作时的步长
整型指针+1跳过4个字节,字符指针+1跳过1个字节。
总结:
指针类型有两个作用:
1.指针类型决定了指针解引用时访问的权限(即访问多少个字节)
2.指针类型决定了指针+/-整数操作时的步长
3.野指针
正常使用指针时,是如下写法:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int a = 10; int* pa = &a; *pa = 20; printf("%d\n", a); return 0; }
但是如果一不小心就会写成这样:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int* p; *p = 20; printf("%d\n", *p); return 0; }
这样写编译器就会报错,
这里p是局部变量,局部变量不初始化时,内容是随机值。
而这里的p也被称为野指针,由此可得野指针的概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.1野指针的成因
1.指针未初始化
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int* p; *p = 20; printf("%d\n", *p); return 0; }
2.指针越界访问
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int arr[10] = { 0 }; int* p = arr; int i = 0; for (i = 0; i <= 11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0; }
这里的数组里只有10个元素,实际上只需要10次就可以对数组的所有元素重新赋值,但是我们对*(p++)循环了12次,指针指向的范围超过了数组arr的范围,而一旦超出范围,p就是野指针了。
3.指针所指向的空间释放
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> vest() { int a = 110; return &a; } int main() { int* p = vest(); printf("%d\n", *p); return 0; }
我们来分析一下这段代码:
一进入主函数,我们就将vest()函数的返回值给指针p,接着进入vest()函数,给a赋值110,返回a的地址,那就是说现在p存放的是a的地址,然后我们经过*p找到存放a的空间中的值打印输出。这样感觉好像是对的,但真的对吗?
当然不对,因为代码中的a是局部变量,它在进入vest()函数的时候创建空间,但一旦出了vest()函数就销毁了,即就是空间释放了, 虽然a的地址传给了p,但是根据这个地址再也找不到a的空间了。
这时候p就成了野指针了。
3.2如何规避野指针
1.指针初始化
明确知道指针应该初始化为谁的地址,就直接初始化。
不知道指针初始化为什么值,就初始化为NULL
int main() { int a = 0; int* p = &a; int* ptr = NULL; return 0; }
像上述代码中的p就知道要初始化为p的地址,直接初始化就行,而ptr不知道要初始化为谁,就初始化为NULL(其实鼠标右击转到定义会发现NULL就是0)
2.小心指针越界
3.指针指向的空间释放,及时置NULL
置NULL就表示ptr是一个空指针,没有指向任何有效的空间,这个指针不能直接使用。而不置NULL,ptr直接就是野指针,野指针是非常危险的。
4.避免返回局部变量的地址
5.指针使用之前检查有效性
if (ptr != NULL) { //使用 }
判断是不是空指针,如果不是空指针就使用。
4.指针运算
4.1指针+/-整数
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int arr[10] = { 0 }; int* p = arr; int i = 0; for (i = 0; i < 10; i++) { *p = i; p++; } p = arr; for (i = 0; i < 10; i++) { printf("%d ", *(p+i)); } return 0; }
通过对指针加一实现对数组元素的重新赋值,通过对指针的解引用打印输出数组元素。
其实通过上述代码我们也可以发现,*(p+i)和arr[i]的作用是一样的,而*(p+i)也可以写成*(arr+i),由此可见,虽然我们在代码中写的是arr[i],但计算机内部运行时还是按照指针的规则来的。
根据加法交换律我们还可以知道:arr[i] = *(arr+i) = *(i+arr) = i[arr]
那在代码中能不能将arr[i]替换成 i[arr] 使用呢?
答案是可以的,可以自行尝试一下。
所以本质上 [ ] 是一个操作符,而arr和i只是它的两个操作数,像a+b和b+a等价一样,arr[i]和i[arr]也是等价的。
4.2指针-指针
看下面一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int arr[10] = { 0 }; printf("%d\n", &arr[9] - &arr[0]); return 0; }
两个地址(指针)相减的得到的值会是多少呢?
运行结果:
由此可见,指针-指针得到的数值的绝对值是两个指针之间的元素个数。
注意:指针-指针运算的前提条件是它们指向同一块空间。
像下面一段带码的写法就不对:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int arr[10] = { 0 }; char ch[5] = { 0 }; printf("%d\n", &ch[4] - &arr[0]); return 0; }
因为无法确定两指针是否在同一块空间。
下面来看一个指针-指针的实际应用:
前面我们写过一个求字符串长度的函数my_strlen(),如下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int my_strlen(char* arr) { if (*arr == '\0') return 0; else return 1+my_strlen(arr + 1); } int main() { char arr[] = "abcdef"; int i=my_strlen(arr); printf("%d\n", i); return 0; }
那么能否用指针-指针的运算来写一个求字符串长度的函数呢?
其实原理很简单,只需要用指向 \0 的指针减去指向数组第一个元素的指针,就能得到它们之间的元素个数,即字符串的长度。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int my_strlen(char* arr) { char* start = arr; while (*arr != '\0') { arr++; } return arr - start; } int main() { char arr[] = "abcdef"; int i = my_strlen(arr); printf("%d\n", i); return 0; }
4.3指针的关系运算
地址是有大小的,指针的关系运算就是比较指针的大小。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #define N_values 5 int main() { float values[N_values]; float* vp; for (vp = &values[N_values]; vp > &values[0];) { *--vp = 0; } return 0; }
这段代码实际作用是将数组values的值全部赋为0
具体步骤是先将数组下标为5的元素的地址给vp(假设下标为5的元素存在),然后将vp与数组第一个元素的地址比较,当vp大于数组第一个元素地址时,对vp先自减,然后解引用赋值为0,而for循环语句中没有调整的代码,继续比较判断,重复上述过程,直到vp = &values[0],结束循环,数组元素全部为0。
当然,这段代码简化后也可以写成如下形式:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #define N_values 5 int main() { float values[N_values]; float* vp; for (vp = &values[N_values - 1]; vp >= &values[0]; vp--) { *vp = 0; } /*int i = 0; for (i = 0; i < 5; i++) { printf("%d ", values[i]); }*/ return 0; }
具体步骤是,先将数组下标为4的元素的地址赋给vp,然后将vp与数组第一个元素的地址进行比较,当vp大于时,将下标为4的元素赋为0,接着vp自减,重复以上步骤,直到vp < &values[0],结束循环。
上面两种写法,第一种写法:比较时,指针vp第一步是和末尾元素的后面的那个内存位置的指针比较。第二种写法:比较时,最后一步是和首元素的前面的那个内存位置的指针比较。
这两种写法那种更好呢?
答案是第一种,第二种虽然在绝大多数编译器上能成功运行,但是标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
下面再来解释一下这个标准规定:
在上图中,红框表示的是数组的在内存中的存储,绿框是数组前后的内存空间,按照标准规定,指针在比较时,指针p能和指针p2进行比较,但是指针p不能和指针p1进行比较。
5.指针和数组
指针和数组之间是什么关系呢?
我们说指针变量就是指针变量,不是数组,指针变量的大小是4/8个字节,专门用来存放地址的。
数组就是数组,不是指针,数组是一块连续的空间,数组用来存放1个或多个相同类型的数据
但其实它们之间还是有联系的:数组名就是数组首元素的地址,所以数组名=地址=指针,当我们知道数组首元素的地址时,因为数组又是连续存放的,所以通过指针就可以遍历访问数组
下面我们打印一下数组元素的地址:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int arr[10] = { 0,1,2,3,4,5,6,7,8,9 }; int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; for (i = 0; i < sz; i++) { printf("%p\n", &arr[i]); } return 0; }
运行结果:
可以看到地址之间是每差4个连续存储的。
数组名是数组首元素的地址,我们就可以将数组名定义为指针p,然后通过对指针的加整数操作访问数组元素。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int arr[10] = { 0,1,2,3,4,5,6,7,8,9 }; int sz = sizeof(arr) / sizeof(arr[0]); int i = 0; int* p = arr;//数组名定义为指针。 for (i = 0; i < sz; i++) { printf("%p = %p\n",p+i, &arr[i]); } return 0; }
运行结果:
可以看到通过指针和数组名打印出来的地址是相同的。
也可以通过解引用的方式打印数组元素:
for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); }
6.二级指针
指针变量也是变量,那指针变量的地址存放在什么地方呢?
这就是二级指针了。
我们前面学的都是一级指针,
int a = 0; int* p = &a;//p是一级指针变量
上述代码中的p就是一级指针,一级指针也是指针变量,创建变量的时候也会在内存中开辟空间,是变量就有地址。打开监视窗口就可以明显看到:
指针变量p中存放的是a的地址,而p也是有地址的。
而要想访问指针变量p的空间就要用到二级指针,下面我们来看看二级指针是如何定义的:
int a = 0; int* p = &a;//p是一级指针变量 int** pp = &p;//pp是二级指针变量,二级指针变量就是用来存放一级指针变量的地址。
打开监视窗口:
二级指针和一级指针之间的关系如下图:
此时如果要通过二级指针改变变量a的值,要经过两次解引用:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int a = 0; int* p = &a;//p是一级指针变量 int** pp = &p;//pp是二级指针变量,二级指针变量就是用来存放一级指针变量的地址。 *(*pp) = 100; printf("%d\n", a); return 0; }
当然不带括号也可以写成 **pp
7.指针数组
指针数组是指针还是数组?
答案是数组。
我们以前学过整型数组是存放整型的数组,字符数组是存放字符的数组,那指针数组顾名思义就是存放指针的数组。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char arr1[] = "abcdef"; char arr2[] = "hello world"; char arr3[] = "chengyu"; char* parr[] = { arr1,arr2,arr3 }; return 0; }
上述代码想将数组1、2、3的内容存放在同一个数组内,我们可以将它们的数组名存放在一起,这样通过数组名就可以找到其内容。而数组名是地址(指针),所以数组parr的类型就是指针类型,数组parr也被称为指针数组。
而三个数组可以被当做是指针数组parr中的三个元素,我们要打印指针数组的内容,只需要将下标为0,1,2的元素打印,下标为0的元素存放的是数组名arr1,而数组名是首元素地址,通过首元素地址可以将arr1数组中的内容“abcdef”依次打印出来,arr2和arr3也一样。
下面我们可以将指针数组的内容打印出来:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char arr1[] = "abcdef"; char arr2[] = "hello world"; char arr3[] = "chengyu"; char* parr[] = { arr1,arr2,arr3 }; int i = 0; for (i = 0; i < 3; i++) { printf("%s\n", parr[i]); } return 0; }
运行结果:
那如果我们将arr1、arr2、arr3定义为整型数组还能不能用上述代码进行打印呢?
答案是不行的。
这时我们可以用指针模拟二维数组的方式打印出指针数组parr的内容:
代码实现:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int arr1[] = { 1,3,9,2,6 }; int arr2[] = { 2,4,0,1,5 }; int arr3[] = { 8,5,3,7,2 }; int* parr[] = { arr1,arr2,arr3 }; int i = 0; for (i = 0; i < 5; i++) { int j = 0; for (j = 0; j < 5; j++) { printf("%d ", parr[i][j]); } printf("\n"); } return 0; }
当然上述代码中的parr[ i ][ j ]也可以写成*(parr[ i ]+j)
运行结果:
注意这里是在模拟二维数组,它在内存中的存储和真实的二维数组完全不同
指针数组就讲到这,后面我们还会讲数组指针是什么?
今天就学到这里,未完待续。。。