指针的含义
变量与地址
C++变量存储分为两个步骤,首先申请相对大小的内存,然后对该内存进行赋值。比如int num;
,首先申请8个字节的内存,然后进行num=1;
,对num进行赋值操作。这两步可以同时进行int num=8;
,声明变量、申请内存和赋值一步到位。但是从上一讲中可以得知,这种方法是自动存储,内存在编译阶段申请,无法修改内存地址。但是在OOP编程中我们更倾向于动态存储,可以动态的更换变量的地址,动态的调整内存大小,这个时候我们就需要直接获取到内存的地址。
&num,取地址符,获取到num内存地址
获取到一个内存地址之后,我们可以修改变量所指向的内存,赋给一个新的内存地址,这样就可以动态修改内存大小。
指针的引用
通过&
取到内存地址后,使用指针进行地址引用,*
关键字就是用来解析地址的。
int num = 1; # 申请8字节内存
int *p; # 申请一个指针
p = # # 通过取地址符获取地址,赋值给p
std::cout << *p; # 通过*解析到该内存地址的具体值
所以指针其实就是在内存地址前面加了一个*
,这个符号可以获取到该地址所代表的具体值。在C++中,指针的声明后并不申请内存(实际会申请2字节的地址,但这里可以忽略),指针本身是不占内存的,指针的作用,就指向另一个已经存在的内存地址。所以指针的声明有如下两种方式:
// 方式一
int num = 1; # 先申请一个整型内存
int* p = # # 将num的地址给p,*p就可以获取到num的值
// 方式二
int* p; # 创建一个指针
p = new int; # 通过new动态申请一个新的整型内存地址
切记,因为指针本身是不指向任何内存地址的,所以在没有为指针分配内存地址之前,不可以对其进行赋值操作,不然会发生内存错误,十分危险的行为!
指针的运用
常规声明与new声明
指针的声明格式是typename* name = new typename
,如果想要声明数组指针的时候,则用typename* name = new typename[num]
,这两种方式的本质都在于new
申请了一块内存地址,再用这个指针指向该地址。
而常规声明则例如:
int a = 1;
int* p = &a;
这两种方法的根本区别在于,new
方法可以动态的申请,实时的申请新空间,但是常规声明还是不够灵活,不过还是要相对来看。
数组指针
int* ps = new int[10];
int nums[10];
上述两种方案都可以创建一个长度为10的整型数组,二者除了内存申请方式不同外还有什么不同呢?首先我们指定int nums[10]
,就是在在内存里申请了80字节的内存空间,通过下标对应每8个字节的内存。
反观int* ps = new int[10];
做了什么呢?我们知道*ps
也可以通过下标进行赋值和取值,但是如果我们直接输出*ps
我们会发现,我们可以输出第一个元素的值,对*ps
赋值的话,也是对第一个元素进行赋值。
这个时候我们发现了,*ps
其实是指向数组的第一个元素地址的,而new
申请的内存在堆上是连续的,所以当我们对ps+1
时,指针向后移动了8个字节,正好就指向了第二个元素的地址,然后再进行ps-1
,指向就向前移动了8个字节,又指向了第一个元素的地址。这下子,我们就明白了,其实数组指针的取值可以用以下公式表示:
ps[num] <==> *(ps+num)
这就体现了为什么C++的运行速度如此之快,因为它是直接在内存上、字节级的进行操作。但是也要注意,因为直接操作到内存,一定不要忘记在结束的时候使用delete
释放内存。
特别注意
当我们使用数组指针的时候,我们再看看上面的公式,我们会发现即使我们的下标是负数,或者超出了申请的大小,我们依旧可以正常操作。
ps[-1] or ps[10000]
这是因为C++是不会检查你的下标是否合法,它只会老老实实的做*(ps+num)
的操作,所以一旦你下标溢出了,其实你会对堆上的其他内存做修改,这就是最常见的内存溢出错误,这是特别危险的操作,因为你也不知道其他内存上面存储着什么数据,所以C++的指针是双刃剑,一定要好好使用。