C++语言指针的基础学习
[TOC]
概述
C++中除了指定变量名称来存取数据之外,在计算机的运行中也有针对内存地址存取的工具,即指针(Pointer)。指针本身是一种变量类型,存储的内容就是内存的地址。大家可以想象把身份证号码当成变量的地址,有了身份证号码,自然就可以知道该位人士的个人资料(变量内容)了。通过指针变量,程序就可以直接存取该指针变量所指向的地址内容。
在C++中,定义变量之后,系统就会开始为此变量分配内存空间,以供程序使用。当需要使用某个数据时,存取那一个地址的内存空间即可。如果我们要更清楚地了解变量所在内存的地址,可以通过&(取址运算符)来得到变量所在的地址,语法格式如下:
1 | &变量名称; |
1 | 01 |
指针变量的声明
所谓指针变量,就是一种用来存储内存地址的变量,当指针变量指向目标地址后,可以通过程序来移动指针(包括将指针变量值进行数值运算),取得该地址所代表的内存区块的数据值。
由于指针也是一种变量,命名规则与一般变量相同,因此当程序声明一个指针时,内存分配的情况与一般的变量声明相同。声明指针时,首先必须定义指针的数据类型,并在数据类型后加上“*”符号,再赋予指针名称,即可声明一个指针变量。特别补充一点,一旦确定指针所指向的数据类型,就不能再更改了。另外,指针变量也不能指向不同数据类型的指针变量。
指针变量声明方式如下:
数据类型* 指针名称; // 第一种声明方式
// 也可以如下方式声明:
数据类型 *指针名称; // 第二种声明方式,*字号位置不同
// 例如:
int* piVal;
以上声明的意义为一个指向int的指针类型变量,其名称为piVal。通常好的指针命名习惯会在变量名称前加上小写p。若是整数类型的指针,则可在变量名称前加上“pi”两个小写字母,“i”代表整数类型(int)。在此要再提醒大家一下,良好的命名规则对于程序日后的判读与维护会有莫大帮助。
由于指针属于系统底层的存取功能,因此通过指针可以存取内存中所指向的内存区内容。假如赋予指针错误的地址,而该地址又刚好是系统数据存储的内存区,此时若覆盖(override)该内存区的内容,很可能会造成系统不稳定或者宕机的情况。另外,如果声明指针时未指定初值,就经常会让指针指向未知的内存地址。
因此,在编写程序时,指针变量务必确实指向合法地址,才不会造成非预期的运行结果。所谓合法地址,通常是指系统分配给程序的地址,如程序已声明或定义的变量(或数组),然后将指针变量指向该变量的地址,也就是为指针设置初值。声明方式如下:
1 | 数据类型 *指针变量; |
或者也可以在指针变量声明时先设置初值为0(或是NULL),声明方式如下:
1 | 数据类型 *指针变量=0; |
第一种方式:
1 | int Value=10; |
1 | 01 #include <iostream> |
指针与数组
在C++中,我们知道当声明数组时会由系统分配一段连续的内存空间。事实上,“数组名”就是指向数组中第一个元素的内存地址,也可以代表该数组在内存中的起始地址。“下标值”其实就是其他元素相对于第一个元素的内存地址的“偏移量”(offset)。
因此,对于已定义好的数组,也可以直接使用数组名来进行指针加法运算,也就是数组名可以直接当成一种指针常数来用,并能使用指针的各种运算。例如,只要在数组名上加1,表示移动一个数组元素内存的偏移量。或者通过取址运算符“&”取得该数组元素的地址,并以指针方式直接存取数组内的元素值。两种语法如下:
1 | 数组名[下标值]= *数组名(+下标值) |
数组可以直接当成指针常数来用,而数组名地址则是数组第一个元素的地址。不过由于数组的地址是只读的,因此不能改变其值,这点是和指针变量最大的不同。例如:
1 | int arr[2][3], value=100; |
下面,我们看一个例子:
1 | 01 #include <iostream> |
动态分配功能
动态分配内存(Dynamic Allocation)是指当程序在运行时才提出分配内存的要求,主要目的是让内存运用更有弹性。从程序本身的角度来看,动态分配机制可以使数据声明的操作在程序运行时再做决定。
对于编写程序而言,通常声明变量都采用“静态分配”(Static Allocation)的方式,也就是所有变量声明必须在编译阶段完成,这也往往会造成某些不便之处。例如,许多程序员往往会苦恼该如何事先声明适当的数组大小,如果事先声明的长度过大,内存使用的效率就不高,声明得过小,则容易面临存储空间不足的问题。
动态分配与静态分配
这时如果通过动态分配方式,程序中不确定的使用空间(如数组长度)即可在程序运行时再按照用户的设置与需求适当分配所需要的内存空间。特别是内存容量不充足时,如果程序运行都以静态声明方式分配内存,很容易造成程序无法运行的窘境。虽然动态分配内存的方式比一般静态分配更具有弹性,不过还是有一些意想不到的缺点,例如动态分配内存后必须在程序结束前完成释放内存的操作。
如果程序运行期间分配的内存未释放,就会造成内存空间的浪费,形成所谓的内存泄漏(memory leak),这种情况对于需要一次使用海量内存的程序而言将有可能无法运行或导致系统运行越来越缓慢等情况发生。静态及动态分配内存两种方式的比较如表6-1所示。
动态分配变量
在C++中,可以分别使用new与delete运算符在程序运行期间动态分配与释放内存空间。其中,new运算符会根据所要求的内存大小在内存中分配足够的空间,并返回所分配内存的指针值,也就是内存地址。
由于使用new运算符所分配的内存空间在程序运行期间将会一直占据内存,因此当不再使用时必须使用delete运算符来释放内存空间。
接着就来介绍C++动态分配变量的方式,也就是在运行时按照数据类型来动态分配一个内存空间,并将分配的内存空间地址返回并赋值给指针变量。这个数据类型除了C++的基本数据类型外,也可以包括结构(structure)等自定义数据类型,声明格式如下:
1 | 数据类型 *指针变量=new数据类型(初值); |
声明完毕时,new运算符会向系统申请分配内存,如果分配成功就返回该内存的地址,如分配失败就返回NULL值。使用new运算符动态分配内存时,可同时设置其初值。若不设置初值,可将小括号省略,如下所示:
1 | int *p_I=new int(77); // 动态分配int数据类型,且*p_I=77 |
另外,因为使用new运算符分配的内存空间将会保留到程序运行结束时才归还给系统,所以当分配的内存不再使用时就要用delete运算符来释放该内存空间,否则分配的内存过多时将会影响到程序可用的内存空间,从而降低程序运行的效率。释放内存空间的语句如下:
1 | delete指针名称; |
使用delete运算符释放内存时,该指针变量所指的内存地址必须是原来new运算符所分配的地址,否则将会造成无法预期的运行结果,如下所示:
1 | delete指针名称; |
使用delete运算符释放内存时,该指针变量所指的内存地址必须是原来new运算符所分配的地址,否则将会造成无法预期的运行结果,如下所示:
1 | int *ptr=new int; // 分配内存,并赋值给*ptr指针变量 |
下面,我们看一个例子:
1 | 01 #include <iostream> |
- 面试题-简述指针常量与常量指针区别
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
举例如下:
1 | int main(){ |