内存浅谈
说到指针就不得不提一下内存这个概念,指针和内存是密切关联的,掌握好指针的大前提,就是必须要理解得了内存。
内存与外存的区别:
①内存存储空间较小,外存存储空间比较大;
②内存访问速度比较快,外存访问速度比较慢;
③内存比较贵,外存比较便宜;
④内存上的数据,断电之后会丢失,外存不会;
理解内存就可以把内存想象成一个大走廊,走廊上有很多个房间,每个房间的大小就是一个字节(8个比特位),针对每个房间都给它编了号,从0开始,依次递增,用十六进制表示,而我们把这个房间的编号称为内存的地址。
对于访问内存地址,内存有一个重要的特点:随机访问能力;并且访问内存上的任意地址的数据,开销都非常小,一瞬间的事,而且对任意地址访问的开销都差不多。
指针是什么
简单来说,指针就是存储别的数据在内存中地址的变量,有了这个地址,我们可以利用一定的手段,来访问这个地址对应的数据;在32位系统上,地址是32个0或1组成的序列,所以要求用4个字节来存储,所以指针在32位机器上是4个字节的大小。 因此可以得到,在64位机器上,指针是8个字节大小的。
结合上面所说的内存的抽象概念,那么指针就可以想象成房卡,这个房卡就相当于是地址,有了房卡,就可以打开房间,访问里面的东西。
指针的使用
数据类型* 变量名=初始化;
我们可以使用上述方法来定义一个指针类型的变量,并且直接在其后赋值;
但是需要注意的是,我们不能随便将一个地址赋给指针变量,我们只能使用内存分配给我们的地址,否则就会出现未定义行为;
那么我们想要取到一个变量的地址,就要使用如下方法:
int num=0;
//取num的地址:&num
int* p=#
而想要利用指针来得到此地址中的值,就要采用如下方法:
int num=0; int* p=# //解引用:*p *p=20;
举例:
#define _CRT_SECURE_NO_WARNINGS //添加头文件 #include <stdio.h> #include <stdlib.h> //主函数,函数入口 int main() { int num = 10; //创建指针变量并让其指向num变量 int* p = # //打印指针p所指向的内存地址 printf("%d\n", p); //利用指针改变num的值 *p=20; //打印p所指向变量的值 printf("%d\n", *p); return 0; }
注:指针的使用一定要注意内存地址的合法性,我们每定义一个变量,系统都会分给我们一块内存空间以便我们存放数据,而这个数据的地址就是合法的;
除此之外,我们得到的地址就是非法的,非法地址就是我们并未创建过、未向系统申请过的内存地址;
因此我们随便向一个指针赋值,我们无法得知这个地址是否合法,此时访问指针属于未定义行为,这样会出现我们无法预知的错误,所以对于指针不能乱赋值,防止其指向非法的内存;
当我们在定义指针的时候,如果没有可以赋给的合法地址,那么我们可以暂时为其赋值:
数据类型* 变量名=NULL;
NULL是什么:内存中的非法地址特别多,反而合法的地址是少数,所以这么多的非法地址,挑出了一个作为典型代表,0号地址,对于0号地址,起了个特殊的名字:空指针,NULL在C中是一个宏定义,标准库已经定义好了的。
常见注意事项:
①不可将字面值常量的地址赋给指针,如int* p = &10这样的写法就是错的;
②不可给指针直接赋值内存地址,以免造成访问非法内存的错误;
③对于暂时不使用的指针将其值赋为NULL;防止成为野指针,出现未定义行为;
指针和数组名
数组名常被我们说成是数组首元素地址,数组名作为函数参数会隐式转化为指针;
我们也可以使用指针来访问数组下标,以此来访问数组的元素;
但是指针和数组名绝对是两个概念,他们是两个不同的数据类型,这点一定要明确
#define _CRT_SECURE_NO_WARNINGS//添加头文件
#include <stdio.h> #include <stdlib.h> //主函数,函数入口 int main() {int arr[] = { 1,2,3,4 }; int* p = arr;
//数组的大小 printf("%d\n",sizeof(arr)); //数组隐式转换为指针,参与运算 printf("%d\n", sizeof(arr + 0)); //一个和数组指向相同元素的指针 printf("%d\n", sizeof(p)); return 0; }
在这个例子中,我们很明显就能看出指针和数组的区别,当我们取数组的大小时,会进行计算得出数组所有元素大小之和,而我们取指针的大小时,永远都只会是4,尤其是我们在打印arr + 0的大小的时候由于数组名不能参与运算,于是数组名隐式转换为指针后才参与运算,从结果是4也能看得出来此时的arr + 0已经是一个指针了,哪怕它所指向的内存地址并没有发生改变。
#define _CRT_SECURE_NO_WARNINGS //添加头文件 #include <stdio.h> #include <stdlib.h> //主函数,函数入口 int main() { int arr[] = { 1,2,3,4 };
int* p=arr; printf("%d\n", *(p + 1)); printf("%d\n", arr[1]); return 0; }
除此之外,在上面的例子中,我们可以利用数组名和指针可以相互转换的特点以及指针可以参与运算的特点,利用指针来访问数组中的元素。
在这个例子中,我们可以看到两种取数组中元素的方式都可得到数组中的第二个元素,因此我们不难得出结论*(p + 1) == arr[1],即这两种方式是等价的。因此我们很多情况下可以利用指针灵活使用数组。
#define _CRT_SECURE_NO_WARNINGS //添加头文件 #include <stdio.h> #include <stdlib.h> //主函数,函数入口 int main() { int arr[] = { 1,2,3,4 }; int* p = arr + 1; //等价于*(p - 1); printf("%d\n", p[-1]); return 0; }
从上面的例子中:可以看出是对于一个指针,下标的方式依然适用,并且不同于数组,在指针的下表中甚至可以使用负数作为下标,只要这块地方是申请过的,是合法的就行。
指针运算
int num=0; //p=0x100 int* p=# //p=0x104 p++;
指针-指针:指针虽然不能相加,但是可以相减,指针相减计算的是两个指针间所偏移的元素个数。
#define _CRT_SECURE_NO_WARNINGS //添加头文件 #include <stdio.h> #include <stdlib.h> //主函数,函数入口 int main() { int arr[] = { 1,2,3,4 }; int* p1 = arr + 1; int* p2 = arr + 2; //得出p2与p1间相偏移了一个元素 printf("%d\n", p2 - p1); //向左偏移则为负数 printf("%d\n", p1 - p2); return 0; }
指针的关系运算:指针间也可也进行普通的关系比较,比如指针相等,指针大小等等,其中指针大小比较时,指针相比于另一个指针向右便宜则为大,向左则为小。
#define _CRT_SECURE_NO_WARNINGS //添加头文件 #include <stdio.h> #include <stdlib.h> //主函数,函数入口 int main() { int arr[] = { 1,2,3,4 }; int* p1 = arr + 1; int* p2 = arr + 1; int* p3 = arr + 2; //得出p2与p1间相偏移了一个元素 printf("%d\n", p1 == p2); //向左偏移则为负数 printf("%d\n", p3 > p2 ? 1 : -1); return 0; }
注:不过在此值得说明的是:只有在连续的一段内存上指针的相减以及指针的关系运算才有意义,否则都是无意义的。
指针数组/数组指针
指针数组:
//指针数组 数据类型* 数组名[5];
他的每一个元素都代表了一个对应数据类型的指针。
数组指针:
//数组指针
数据类型 (*数组名)[5];
它的每一个元素仍然是一个普通元素,不过可以使用指针来访问数组的元素,只不过这种方式很鸡肋,基本不用。
常量指针/指针常量
常量指针:
int num=0; const int* p=# //等价于: int const* p=#
对于修饰的变量无法进行更改,这样的指针在我们不想让指针修改一个值却又方便传入的时候经常使用。
上面两种写法等价,但是第二种不推荐,因为int*是一种类型,写在一起更好,所以尽量写第一种;
指针常量:
int num=0; int* const p=#
这种写法是指针自身的值无法改变,也就是说,不能指向其他变量了,无法更改指向;
浙公网安备 33010602011771号