指针
在了解什么是指针之前,我们需要先搞清楚数据在内存中是如何存储的,又是如何读取的。
如果在程序中定义一个变量,在编译的时候就给这个变量分配内存单元。系统根据程序中定义的变量类型,会分配一定长度的空间。例如:C++编译系统一般为整型变量分配4个字节,对单精度浮点型变量分配4个字节,对字符型变量分配1个字节。内存区的每一个字节都有一个编号,这就是地址,它可以类比我们旅馆中的房间号,在地址所标识的内存单元中存放数据,这相当于旅馆中各个房间中居住旅客一样。
注意:地址和内容(值)是两个概念,如下图:
可以看到程序已定义了变量i,j,k,编译的时候系统分配2000-2003这4个字节给变量i,分配2004-2007这4个字节给j,分配2008-2009给变量k。在程序中我们一般是通过变量名来对内存单元进行存取操作,其实程序在经过编译以后已经将变量名转换为变量的地址,对变量值的存取都是通过地址进行的。
当我们执行cout<;语句的时候,即获取
变量i
的值,根据变量名与地址的对应关系(在编译的时候确定),找到变量i
的地址2000
,然后从2000开始的4个字节
中取出数据,即变量的值为3,并输出。
当我们执行cin>>i;
语句的时候,即变量i
赋值的时候,在执行的时候,就把值送到地址为2000
开始的整型存储单元中。如果执行语句k=i+j;那么从2000字节开始的整型变量存储单元中取出i的值3,从2004字节开始的变量存储单元中取出j的值6,将它们进行相加,结果9送到k所占用的2008字节开始的整型存储单元中。
上面的这种按变量地址存储变量值的方式称为直接存取方式。我们还可以使用另外一种存储方法,间接存取方式,即将变量i的地址存放到另一个变量中。
看到这里,那么什么是指针呢?
其实地址就是指针,一个变量的地址称为该变量的指针。因为通过地址能找变量单元,因此可以说,地址指向该变量单元。所以将地址形象化地称为“指针”,意思是通过它能找到以它为地址的内存单元。
变量与指针
变量的指针其实就是变量的地址,用来存放变量地址的变量是指针变量。有没有感觉有点绕,嘿嘿。指针变量是一种特殊的变量,用它来指向另外一个变量,“*”表示指向
,比如:pointer是一个指针变量,而*pointer表示pointer所指向的变量,也就是说*pointer也代表一个变量。
定义指针变量
定义指针变量的形式:基类型 *指针变量名;
C++规定所有变量在使用前必须先定义,即指定其类型。在编译时按变量类型分配存储空间。在Visual C++中,为每个指针变量分配4个字节的存储空间。对指针变量必须定义为指针类型,如:
int i,j; //定义整型的变量i,j
int *pointer1,*pointer2; //定义指针变量pointer1,pointer2
第二行开头的int是指:所定义的指针变量是指向整型数据的指针变量,或者说pointer1和pointer2只能存储整型数据的地址,而不能存放其他数据类型的地址。
正如上面代码所示,pointer1和pointer2可以指向整型数据i和j,而不能指向浮点数。int是指针变量的基类型,所谓基类型就是该指针变量指向的变量的类型。
注意:变量名是pointer1和pointer2而不是*pointer1和*pointer2,*表示该变量为指针变量
那么怎样使一个指针变量指向另一个变量呢?简单,只需要把被指向的变量的地址赋给指针变量即可。
pointer1 = &i; // 表示将变量i的地址存放到指针变量pointer1中
pointer2 = &j; // 表示将变量j的地址存放到指针变量pointer2中
通过上面赋值操作,pointer1指向了变量i,pointer2指向了变量j。
定义变量时注意几点:
1、在定义指针变量时必须指定基类型
我们知道,不同类型的数据在计算机系统中的存储方式和所占的字节数是不相同的。因此,如果想通过指针引用一个变量,只知道地址(如:2000)是不够的,因为无法判断是从地址为2000的一个字节取出字符数据,还是从2000-2003四个字节中取出int型数据,所以必须知道其类型,只有知道了数据类型,才能按存储单元的长度以及数据的存储形式正确的读取数据。
其实一个变量的指针包括两个方面的含有:
- 一是以存储单元编号表示的地址(如:2000)
- 一是它指向的存储单元的数据类型(如:int,float等),即基类型
2、怎么表示指针类型
比如:指向整型数据的指针类型表示为int *,读作指向int的指针
3、一个指针变量只能指向同一个类型的变量,不能一会指向整型变量,一会指向单精度变量
引用指针变量
与指针变量相关的运算符:
1)&:取地址运算符
2)*:指针运算符(或称间接访问运算符)
如:&a表示变量a的地址,*p表示变量p所指向的存储单元
例:通过指针变量访问整型变量
int main(int argc, char const *argv[])
{
int a,b; // 定义整型变量a,b
int *pointer1, *pointer2; // 定义pointer1,pointer2为(int *)型变量,即指向整型数据的指针变量
a = 100; b = 200; // 对a,b赋值
pointer1 = &a; // 把变量a的地址赋给pointer1
pointer2 = &b; // 把变量b的地址赋给pointer2
cout<
结果:
100 200
100 200
&和*运算符说明:
1、如果已经执行了“pointer1 = &a;”语句,那么&*pointer1的含义是什么?
&和*两个运算符的优先级别相同,是按照自右而左的方向结合,因此先进行*pointer1运算,那么结果就是变量a,然后再执行&运算,即取变量a的地址。因此&*pointer1与&a相同,都是变量a的地址。
2、*&a的含义是什么?
先进行&a运算,得到a的地址,再进行*运算,即&a所指向的变量。所以*&a和*pointer1的作用一样,都等价于变量a.
例:输入a和b两个整数,按先大后小的顺序输出a和b(用指针变量处理)
int main(int argc, char const *argv[])
{
int *p1, *p2, *p, a, b;
cout<<"请输入两个整数:"<>a>>b;
p1 = &a;
p2 = &b;
if (a>a>>b;
p1 = &a;
p2 = &b;
if (a
指针变量p已经指向数组中的第一个元素了,p+1表示指向同一数组中的下一个元素(并不是将p的值简单加1),上面代码已经告诉我们了。
简单分析:
1、数组元素是整型,每个元素占4个字节,当执行p+1;语句的时候,意味着使p的值(即当前地址)加4个字节,以指向下一个元素。p+1
的实际地址是p+1*d
,d
是数组元素所占的字节数。
2、p+i
和a+i
其实是一样的,都是a[i]
的地址,或者说,它们都指向数组a
的第i个元素
。a是数组名指向数组的首元素地址,指针p也是指向数组的首元素地址,那么对地址进行相同操作结果当然相同。
3、*(p+i)
或者*(a+i)
是p+i
或a+i
所指向的数组元素,即a[i]。例如上面我们为*(p+1)
进行赋值,其实就是为a[1]进行赋值操作。因而*(p+i)
,*(a+i)
,a[i]
这三者是等价的。
4、指向数组元素的指针变量也可以带下标,如:p[i]与*(p+i)等价
例:有一个整型数组a,有10个元素,输出数组的所有元素。
下标法
int main(int argc, char const *argv[])
{
int a[10];
cout<<"请输入10个数:"<>a[i];
}
cout<<"输出:"<
指针法:就是将a[i]改为*(a+i)
for (int i = 0; i < 10; ++i)
{
cout<<*(a+i)<<" ";
}
用指针变量指向数组
int main(int argc, char const *argv[])
{
int a[10];
int *p;
p = a;
cout<<"请输入10个数:"<>*(p+i);
}
cout<<"输出:"<
指针变量做函数形参
数组名代表数组首元素的地址,用数组名做函数的参数,传递的是数组首元素的地址,既然是地址,那么同样可以使用指针变量做函数形参。
函数实参与形参的结合有四种形式:
实参 | 形参 |
---|---|
数组名 | 数组名 |
数组名 | 指针变量 |
指针变量 | 数组名 |
指针变量 | 指针变量 |
函数与指针
指针变量也可以指向一个函数,一个函数在编译时被分配给一个入口地址,这个函数入口地址就称为函数的指针。可以用一个指针变量指向函数,然后通过该指针变量调用此函数。
比如:求a和b中的大者,一般情况下我们会这么写:
int main(int argc, char const *argv[])
{
int max(int x, int y);
int a,b,m;
cin>>a>>b;
m = max(a,b);
cout<<"max= "< y ? x: y;
}
但是我们也可以用一个指针变量指向max函数,然后通过该指针变量调用函数。定义指向max函数的指针变量的方法是:
int (*p)(int,int);
int:指针变量p指向的函数的类型
p:是指向函数的指针变量
(int,int):p所指向的函数中的形参的类型
指向函数的指针变量的一般形式为:函数类型(*变量名)(函数列表)
int main(int argc, char const *argv[])
{
int max(int x, int y); // 函数声明
int (*p)(int, int); // 定义指向函数的指针变量p
int a,b,m;
p = max; // 让p指向函数max,表示将max的入口地址赋给p
cin>>a>>b;
m = p(a,b);
cout<<"max= "<
注意:在定义指向函数的指针变量p时,(*p)两侧的括号不能省略,表示p先与*结合,它是指针变量,然后再与后面的()结合,表示此指针指向函数,函数的返回值是整型。如果写出了“int *p(int, int);”,由于()的优先级高于*,它就成了声明一个函数,这个函数的返回值是指向整型变量的指针,返回指针值的函数也简称为指针函数。
返回指针值的函数
前面已经提到了返回指针值的函数为指针函数。定义指针函数的形式为:类型名 *函数名(参数列表);,例如:
int *a(int x, int y);
a是函数名,调用它以后能够得到一个指向整型数据的指针(地址)。x,y是函数的形参。在a的两侧分别是*和()运算符,由于()优先级高于*,因此a先与()结合表示为函数。函数前面的*表示此函数是指针型函数(函数值是指针),int为返回的指针指向的整型变量。
指针数组
如果一个数组,其中的元素全部为指针类型数据,那么该数组就是指针数组。也就是说,指针数组中的每一个元素都相当于一个指针变量,它的值是地址。
一维数组的定义形式为:类型名 *数组名[数组长度];,例如:
int *p[4];
C中的 NULL 指针
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。NULL 指针是一个定义在标准库中的值为零的常量
#include
int main ()
{
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
ptr 的地址是 0x0
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西
如需检查一个空指针,可以使用 if 语句,如下所示:
if(ptr) /* 如果 p 非空,则完成 */
if(!ptr) /* 如果 p 为空,则完成 */
指向指针的指针
指向指针的指针,简称为指向指针的指针,也可以理解为二级指针。
const指针
可以指定指针变量是一个常量,或者指定指针变量指向的对象是一个常量。有以下几种情况
指向常量的指针变量
定义这种指针变量的一般形式为:const 类型名 * 指针变量名;
不允许通过指针变量改变它指向的对象的值,例如:
int a = 12, b = 15;
const int *p = &a; //定义p为指向整型变量a的const指针变量
*p = 15; //试图通过p改变它指向的对象a的值,非法
上面定义了p为(const int *)型的指针变量,并使其指向变量a,不能通过p来改变a的值,但是指针变量p的值(即p的指向)是可以改变的。例如:
p = &b; //p改为指向b是合法的
不要以为只要定义了(const int *)型指针变量就能保证其所指向的对象的值无法改变。例如:
a = 10;
所以用指向常量的指针变量只是限制了通过指针变量改变它指向的对象的值。如果想保证a的值始终不变,应当把a定义为常变量:
const int a = 16;
这样p就成为了指向常变量的指针变量,无论用直接访问方式还是间接访问方式都无法改变a的值
常指针
指定指针变量的值是常量,即指针变量的指向不能改变。
int a = 4, b= 5;
int * const p = &a; // 指定p只能指向变量a
p = &b; // 试图改变p的指向,不合法
定义这种指针变量的一般形式是:类型名 *const 指针变量名;
- 这种指针变量称为常指针变量,简称常指针,即指针值不能改变
- 必须在定义时初始化,指定其指向
- 指针变量的指向p不能改变,但指针变量的指向变量的值可以改变
*p = 10;
- 注意const和位置。const在后面
指向常量的常指针
把以上两种叠加在一起,就是指向常量的常指针变量。即指针变量指向一个固定对象,该对象的值不能改变(指不能通过指针变量改变该对象的值)
int a = 4, b= 5;
const int * const p = &a; // 指定p只能指向变量a
p = &b; // 试图改变p的指向,不合法
*p = 10; // 试图改变p的值,不合法
a = 10; // 直接改变a的值,合法
定义这种指针变量的一般形式为:const 基本类型名 * const 指针变量名
void指针类型
可以定义一个基类型为void
的指针变量(即(void *)型变量),它不能指向任何类型的数据。注意:不要把指向void类型
理解为能指向“任何类型”的数据,而应该理解为“指向空类型”或“不确定的类型”的数据
如果指针变量不指定一个确定的数据类型,它是无法访问任何一个具体的数据的,它只通过了一个地址。在c中用malloc函数开辟动态存储空间,函数的返回值是该空间的起始地址,由于该空间尚未使用,其中没有数据,谈不上指向什么类型的数据,故返回一个void *型的指针,表示它不指向确定的具有类型的数据。
显然这种指针是过渡型的,它必须转换为指定一个确定的数据类型的数据,才能访问实际存在的数据,否则它是没有任何用处的,在实际使用该指针变量时,要对它进行类型转换,使之适合于被赋值的变量的类型。
int a = 3; // 定义a为整型变量
int *p1 = &a; // p1指向int型变量
char *p2 = "new"; // p2指向char型变量
void *p3; // p3为无类型指针变量
p3 = (void *)p1; // 将p1的值转换为void *类型,然后赋值给p3
//cout << *p3 << endl; //p3不能指向确定类型的变量,*p3非法
cout << *(int *)p3 << endl; // 把p3的值转换为(int *)型,可以指向指针变量a
p2 = (char *)p3; // 将p3的值转换为char *类型,然后赋值给p2,输出3
printf("%d", *p2); //合法,输出a的值
可以把非void型的指针付给void型指针变量,但是不能把void型指针直接付给非void型指针变量,必须先进行强转换
引用
对于一个数据可以建立一个“引用”,它的作用是为一个变量起一个别名。这是C++对C的一个重要扩充。
假如有一个变量a,想给它起一个别名b,可以这样写:
int a; // 定义a为整型变量
int &b = a; // 声明b是a的引用
以上声明了b是a的引用,即b是a的别名
,经过这样的声明后,使用a或b的作用相同,都代表同一变量。可以这样理解引用,通过b可以引用a。声明变量b为引用,并不需要另外开辟内存单元来存放b的值,b和a占内存的同一个存储单元,它们具有同一地址。即使变量b具有变量a的地址,如果a的值是20,那么b的值也是20
注意:
- 在上面的声明中,&是引用声明符,并不代表地址,不要理解为把a的值付给b的地址。在数据类型名后面出现的&声明符是引用声明符,在其他场合出现的都是地址符,如:
char c;
char &d = c; // 此处的&是引用声明g符
int *p = &a; // 此处的&是地址符
引用不是一种独立的数据类型,对引用只有声明,没有定义。必须先定义一个变量,然后声明对该变量建立一个引用(别名)
声明一个引用时,必须同时使之初始化,即声明它代表哪一个变量。
在声明一个引用后,不能再使之作为另一变量的引用。比如:声明变量b是变量a的引用后,在其有效的范围内,b始终与其代表的变量a相联系,不能再作为其他变量的引用(别名)。下面的用法不对:
int a1, a2;
int &b = a1; // 声明b是a1的引用
int &b = a2; // 试图使b又变成a2的别名,不合法
- 不能建立引用数组。如:
int a[5];
int &b[5] = a; //错误,不能建立引用数组
int &b = a[0]; //错误,不能作为数组元素的别名
- 不能建立引用的引用。如:
int a = 3;
int &b = a; // 声明b是a的别名,正确
int &c = b; //试图建立引用的引用,错误
- 可以取引用的地址。如已声明b是a的引用,则&b就是变量a的地址&a
int *p;
*p = &b;
引用作为函数参数
C++之所以增加引用机制,主要是把它作为函数参数,以扩充函数传递数据的功能。
函数参数传递有两种情况
1、将变量名作为实参和形参。这时传给形参的是变量的值,传递是单向的。如果在执行函数形参的值发生变化,并不传回给实参。因此在调用函数时,形参和实参不是同一个存储单元。
要求将变量i和j的值互换。下面的程序无法实现
int main(int argc, const char * argv[]) {
void swap(int , int); // 函数声明
int i = 3, j = 5;
swap(i, j); // 调用函数swap
cout << i << "" << j << endl; // i和j的值并未互换
return 0;
}
void swap(int a, int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
为了解决这个问题,采用传递变量地址的方法
2、传递变量的地址。形参是指针变量,实参是一个变量的地址,调用函数时,形参(指针变量)得到实参变量的地址,因此指向实参变量单元。
int main(int argc, const char * argv[]) {
void swap(int *, int *); // 函数声明
int i = 3, j = 5;
swap(&i, &j); // 调用函数swap
cout << i << "" << j << endl; // 5 3
return 0;
}
void swap(int *p1, int *p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
其实上面这种虚实结合的方法仍然是“值传递”方式,只是实参的值是变量的地址而已。通过形参指针变量访问主函数中的变量(i和j),并改变它们的值
3、以引用作为形参,在虚实结合时建立变量的引用,使形参名作为实参的“引用”。即形参成为实参的引用
int main(int argc, const char * argv[]) {
void swap(int &, int &); // 函数声明
int i = 3, j = 5;
swap(i, j); // 实参为整型变量
cout << i << " " << j << endl; // 5 3
return 0;
}
void swap(int &a, int &b) // 行参是引用
{
int temp;
temp = a;
a = b;
b = temp;
}
在定义swap函数声明行参时,指定a和b是整型变量的引用。注意:此处&a不是a的地址,而是指a是一个整型变量的引用(别名),&是引用声明符。由于是形参,不必对它初始化,即未指定它们是哪个变量的别名。
当main函数调用swap函数时,进行虚实结合,把实参变量i和j的地址传给形参a和b。这样i和a的地址相同,二者是同一变量,即a成为i的别名,同理b成为j的别名。那么在swap函数中使a和b的值对换,显然i和j的值同时也改变了。这就是地址传递方式