指针的概念
比尔盖茨曾经说过,640K ought to be enough for everybody,但从现在的发展看来,硬件设备的更新已经远远超越早期,计算机体系中的存储层次如下(Memory Hierarchy):
Remote secondary storage是云端存储,Local secondary storage是本地磁盘,Main memory是内存(内存数据断电会丢失),L1,L2 cache更加接近CPU,Register在CPU内部,可以直接操作数据;速度越来越快,但容量越来越小,成本越来越高;
- 内存由很多单元组成,这些内存单元用于存放各种类型的数据
- 机器对内存的每个单元进行编号,这个编号就是内存地址,地址决定了单元所在的位置
- C++的编译器让开发者通过自己定义名字映射到地址(指针),从而操作单元
对C++内存单元内容与地址的认识:
int a=112,b=-1;
float c=3.14;
int* d=&a;
float* e=&c;
d和e是两个指针变量,分别指向一个整型变量a和一个浮点型变量c所在地址;这些变量在内存中为:
当定义一个变量为指针类型时,就被称为指针变量,存储的值是地址。对类型T
,T*
表示指向一个T
型变量的指针。
通过指针访问其指向地址的过程称为间接访问(indirection)或者引用指针(dereferencing the point),访问通过单目操作符*
完成:
cout<<(*d)<<endl;
cout<<(*e)<<endl;
使用sizeof查看指针变量所占的字节数:
cout << sizeof(b) << endl; //4
cout << sizeof(e) << endl; //4
不管指针指向的变量类型是什么,指针所占字节数总是相同的,如果机器的地址总线是32位,则占4字节,如果机器地址总线是64位,则占8字节;当获取指针指向地址的值时,会先根据地址找到该地址对应的内容,编译器再根据定义指针时记录的类型解析得到被指向变量的正确值;
由此看出,一个变量必须含有三要素:
- 变量的类型
- 变量的地址
- 变量地址下的内容
左值与右值
在容器中提到过,字符串本质是数组,我可以定义指针变量指向该字符串的首字符:
char strHello[]={"helloworld"};
char* pStrhello="helloworld";
pStrhello=strHello; //指针变量的值允许改变
/*
strHello=pStrhello; //数组首地址不允许改变
*/
注意到,数组首地址strHello不可变,但strHello[index]即数组内容可以改变,指针变量pStrhello可以改变,而pStrhello[index]即所指地址的内容是否可变取决于该地址是否在存储空间的可变区域;
之前提到常量和变量,常量不可变是因为其保存在存储空间的不可变区域,变量可变是因为其保存在存储空间的可变区域;
回到异常的语句:strHello=pStrhello; //数组首地址不允许改变
,在visual studio中,会提示char strHello[11]
定义一个数组,表达式必须是可修改的左值;
概念
对于左值,一般说法,编译器为其单独分配了一块存储空间,可以取到其地址,比如常见的变量,左值可以放在赋值运算符左边;(也可以在赋值运算符右边)
右值指的是数据本身,不能取到其自身地址,右值只能在赋值运算符右边;
具体分析
左值最常见情况比如函数和数据成员名;
右值是没有标识符,不可以取地址的表达式,一般称为临时对象;
比如:a=b+c
&a是允许的,而&(b+c)不能通过编译,因为a是左值,(b+c)只是临时表达式(是右值);
对于异常的语句:strHello=pStrhello; //数组首地址不允许改变
,可以看出,严格来说,数组名strHello
并不单纯代表数组首地址,而是代表整体数组(a不等同于a[0]),正因为是一个整体,它将不能被取地址,所以是右值;即对于一个数组a,a是右值,而a[0](真正意义上的数组首地址)才是左值;
各种指针
一般指针,指针的数组,数组的指针
C++中有几种常见的较原始的指针:
- 一般类型指针
T*
,T是泛型,泛指任何一种类型;比如:
int i=4;
int* ip=&i;
double d=3.14;
double* dp=&d;
char c='a';
char* cp=&c;
- 指针的数组(array of pointers)与数组的指针(a pointer to an array):
T* t[m] //指针的数组:由指针变量组成的数组
T(*t)[m] //数组的指针:一个指向数组的指针
int* a[4]; //等价于 int* (a[4]), a是一个数组, 元素为4个int型的指针
int(*b)[4]; // ()的优先级高于[], b是一个指针, 指向含4个int型元素的数组
实例如下,注意回顾左值与右值中提到的数组名不单纯是数组首地址,而是代表数组这个容器整体:
int c[4]={0x80000000,0xFFFFFFFF,0x00000000,0x7FFFFFFF};
int* a[4] //指针的数组,array of pointers
int(*b)[4]; //数组的指针,a pointer to an array
b=&c; //注意数组的大小必须匹配
//把c中各元素的地址赋给a
for(unsigned int i=0;i<4;i++)
{
a[i]=&(c[i])
}
//机器内部数据是补码形式,补码0x80000000即对应-2147483648
cout<<(*a[0])<<endl; //-2147483648
//()优先级最高,先取指针b的所指地址的内容即数组c,再索引c的第4个元素
cout<<(*b)[3]<<endl; //2147483647
const与指针
pointer to const,const pointer与const pointer to const;
先看例子:
char strHello[]={"hello"};
// pointer to const
char const* pStr1="hello";
char str2Hello[] = { "hello" };
// const pointer
char* const pStr2 = str2Hello;
// const pointer to const
char const* const pStr3="hello";
pStr1=strHello; //没报错是因为此处编译器不检查strHello保存的内容是否为常量
//pStr2=strHello; pStr2不可改
//pStr3=strHello; pStr3不可改
当变量被const修饰时,系统会将变量存储于内存的不可变区域;
规则:判断const修饰的对象
- 1.先看左侧最近部分;比如
pStr1
,const左侧最近部分为char,代表char为常量,所以指针pStr1
的内容(地址)是变量,但地址对应的内容应当是常量(pointer to const);对于pStr2
,const左侧最近部分为char*,即修饰对象为字符型指针,所以pStr2
的内容不可变,指针不可变(const pointer);对于pStr3
,有第一个const左侧为char,第二个const左侧为指针的标志*,代表pStr3
的内容不可变,同时这个常量地址下的内容也是常量(const pointer to const) - 2.如果左侧没有,则看其右侧;比如例子中
char const* pStr1
实际上和const char *pStr1
是等效的;(注意const char *pStr1
不等于const char* pStr1
)
const与指针写法各异,理论上可以根据规则辨识其属于什么,但为了便于阅读,开发人员最好遵循以下规范进行声明:
//1.pointer to const:
const T *pname
//2.const pointer:
T *const pname
//3.const pointer to const
const T *const pname
关于const和指针在开发中的作用:比如有一块内存资源x,现在有两个指针,一个定义为pointer to const,即对于x为只读,另一个为const pointer,即固定了在内存中写的区域;
指向指针的指针
声明指针的指针,使用形式为T**
,比如:
int a=123;
int* b=&a;
int** c=&b; // 指针的指针
单目操作符*
具有从右向左的结合性,访问指针时,*c
得到其内容即b
,**c
则访问*b
(**c
的访问顺序为*(*c)
),即变量a
;
上面例子的各个变量如下:
表达式 值
a 123
b &a
*b a
c &b
*c b
**c a
野指针
首先明确,未初始化的指针是危险的:
int* a; //未初始化的指针
*a=12;
在定义指针a时并没有指明地址空间,但却在下一语句用单目运算符引用这个未知指向的指针,修改了未知地址下的内容;如果运气好的话,未知地址位于非法地址,程序出错自动终止;如果运气不好,未知地址是一个可访问地址,语句执行后无意识间修改了对应的内容,这样的错误将难以察觉,其造成的损失不可估量。
所以在使用指针时,应确保其被合理赋值;
NULL指针
NULL指针是一个特殊的指针变量,表示不指向任何区域,比如:
int* a=NULL;
NULL指针存在的意义就是为了给暂时不使用的指针一个安全的指向;
关于指针初始化,举例如下:
int a=123;
int* b=&a;
int** c=&b; // 指向指针的指针
int* pA=NULL;
pA=&a;
if(pA != NULL) // 判断指针是否为NULL
{
cout<<(*pA)<<endl;
}
pA=NULL;
杜绝野指针
野指针表示指向"垃圾内存"的指针,野指针并不是NULL指针,野指针有3种情况:
- 指针变量没有初始化
- 已经释放不用的指针没有置NULL:在堆空间中,通过malloc申请到一块区域,当区域无用后,该区域被free释放,此时指向该区域的指针也就是没有意义的,应该置NULL
- 指针操作超越了变量的作用范围