指针的概念.docx
《指针的概念.docx》由会员分享,可在线阅读,更多相关《指针的概念.docx(16页珍藏版)》请在冰豆网上搜索。
![指针的概念.docx](https://file1.bdocx.com/fileroot1/2022-11/25/f898e196-75fc-47ed-b69e-9f21f61e02f7/f898e196-75fc-47ed-b69e-9f21f61e02f71.gif)
指针的概念
指针的概念
指针是C++所提供的一种颇具特色的数据类型,允许获取和直接操纵数据地址,实现动态存储分配。
掌握指针的应用,可以使程序简洁、紧凑、高效,并且能更有效地使用宝贵的内存空间。
指针是C和C++的精华所在,也是C和C++的一个十分重要的概念。
主要内容:
指针的概念;
指针数据对象的定义;
指针运算;
指针数据对象的引用;
利用指针实现动态存储分配(动态数组)。
重点:
指针的概念、动态存储分配。
一、指针的概念
1.什么叫指针
一个数据对象的内存地址称为该数据对象的指针。
指针可以表示简单变量、数组、数组元素、结构体甚至函数。
也即指针具有不同的类型,可以指向不同的数据存储体。
例如:
int*point1,a,b;
double*point2[20];
……
point1=&a;
point1 整型变量a
point2[0] 双精度形数组
Point1=&b;
Point1 整型变量b
图6.1 指针示意
注意:
指针中的内容是可以动态改变的,例如point1既可以指向变量a也可以指向变量b。
2.指针的作用
1)能实现复杂的数据结构,例如数组、链表、队列和堆栈等;
2)能方便地表示和处理字符串;
3)能方便地实现动态存储分配;
如果一个程序或者一个函数出现使用需要大存储量的数据对象,采用动态存储分配可以提高内存的使用率,也即这些数据一般用预先定义的指针变量来表示,当实际使用时才临时申请实际的存储空间,使用完毕立即释放。
指针变量所占的内存空间与所表示的数据对象的存储空间相比实在是微乎其微,因为它只是用来存放对应空间的首地址。
4)在函数之间进行数据的双向传递。
将形参定义成指针类型,对应的实参必须是某个数据对象的首地址,也即采用传地址的方式,这样就可以实现数据的双向传递。
3.指针类型
指针类型属于标准类型,其取值是所表示的数据对象的内存地址,所以其值域是内存地址集。
指针类型用来定义各种类型的指针变量,其语法如下:
<类型标识符>*
例如int*表示整型指针类型,char*表示字符指针类型,等等。
二、定义指针变量
同其他变量一样,指针变量也必须“先定义后使用”,可以在函数外部定义全局的指针变量也可以在函数内部定义局部的指针变量。
1.语法
<类型标识符>*<标识符1>,*<标识符2>,……,*<标识符n>;
;类型标识符:
用来指明指针的类型,可以是基本类型,例如int,char,double等等,也可以是结构类型,例如结构体,文件等等。
υ
;*:
表示指针。
υ
;标识符:
指针数据对象的名字,可以是基本变量名、数组名、结构体变量名、函数名等。
υ
2.实例
(1)int*x,*y,z;
上面的语句定义了三个变量,其中z是整型变量,用来存放整数,x,y是整型指针变量,用来存放整型数据对象的内存地址,或者说用来表示整型数据对象。
z=5;
x=&z;
z中存放整数5,也即z的值是5;x中存放的是z的地址,我们可以用*x来表示z。
x和z的关系如下图所示。
X Z
图6.2 整型变量与整型指针变量的关系
(2)chars1,s2[100],*s3,*s4[100];
上面的语句定义了四个变量:
s1是字符型变量,用来存放字符的ASCII码;
s2是字符数组,最多可以存放100个字符(ASCII码);
s3是字符型指针变量,用来存放字符类型数据对象内存地址,如果其中存放的是字符串的首地址,则它指向一个字符串,或者说它代表一个字符串,例如:
strcpy(s2,"abcd");
s3=s2;
则s2等价于s3,也即strcmp(s2,s3)返回值为0。
两者的关系如下图所示:
s2
字符串
s3
图6.3 字符数组与字符型指针变量的关系
s4是字符型指针数组,最多可以存放100个字符串的指针(地址),也即每一个元素都可能指向一个字符串,如图6.4所示。
S3
图6.4字符指针数组与字符串的关系
可以看出,一个一维字符指针数组可以表示一个n行m列的的文本。
3.指针变量的初始化
我们可以在定义指针变量的同时给其赋初值,其初值是某个数据对象的内存地址,也即该指针就指向对应的数据对象,这一过程也称为:
建立指针。
例如:
inti=10;
int*iptr=&i;
整型指针变量iptr存放的是i的地址,指向整型变量i,对于iptr的引用(可以表示成*iptr)也就是对i的引用。
4.几点说明
(1)标识符前面的“*”并不是名称的一部分,而表示该数据对象的类型为指针类型,也即声明该数据对象是指针类型数据对象。
例如:
int*i,*j;对应的指针变量名是i,j而不是*i,*j。
(2)指针变量可以和其它变量在同一语句中定义。
例如:
doubled1,*d2;
(3)指针变量只能存放同一基类型数据对象的内存地址,换句话说,一个指针变量在任何时候都只能指向同一基类型的数据对象。
这就是所谓“指针类型与实际存储的匹配问题。
例如:
char*c;
inti;
……
c=&i; //错误的赋值,因为c只能指向字符串。
三、指针运算
指针运算实际上是地址操作,包括算术运算(加减运算)、关系运算、赋值以及取地址和间接访问等。
1.指针的赋值
操作指针之前必须赋予确定的值(只能是地址或空值NULL),可以在定义指针的时候赋予初值,也可以用赋值表达式对指针变量赋值。
2.指针的加减运算
可以使用的运算符有:
+、-、++、--,参加运算的指针变量必须是已赋值的。
(1)一个指针量加上(或减去)一个整型量n,表示地址偏移了n个单位,具体向上或向下偏移多少字节,取决于其基类型。
例如一个整型指针变量加上4等于原存放的地址值加上8(字节);而一个双精度型指针变量加上4等于原存放的地址值加上32(字节)。
(2)对数组名施加+,-运算
数组名实际上是一个指针变量,其初始值是数组的首地址,也即指向数组的第一个元素,数组名+i表示指向数组的第i+1个元素。
例如:
inta[100];
a[i]与a+I这两种表示法是等价的。
(3)指针变量的++、--运算
++:
原地址加上一个地址单位(基类型的实际字节数);
--:
原地址减去一个地址单位(基类型的实际字节数);
例如:
int*iptr;
……
iptr++;// iptr=iptr+1,向下移动两个字节,见图6-5;
iptr--;// iptr=iptr-1,向上移动两个字节;
iptr iptr--
0000:
02F8
iptr++
图6.5指针的移动
3.取地址运算
运算符:
&;
作用:
获取数据对象的内存地址,如果是结构数据对象则获取其内存首地址。
实例:
[例1]:
charc1,s1[100],*c2;
……
c2=&c1;//取字符变量c1的内存地址赋予指针变量c2,c2指向
c1。
c2=&s1[0];//取字符数组s1的第1个元素的地址赋予指针变量
c2,c2指向s1[0],该运算与c2=s1是等价的,为什
么?
c2=s1[0]和c2=&s1都是错误的运算,为什么?
[例2]:
inta;
……
scanf("%d",&a);
scanf函数的第二个参数其类型是指针类型,调用该函数所提供的对应实参必须是数据对象的实际地址或存放数据对象地址的另一指针变量。
这种传递方式称为“传地址”,函数对该参数的修改可以返回给主调函数。
4.间接访问
确切地说,是通过指针变量访问该变量所指向的数据对象,而不是数据对象的直接访问,故称为间接访问。
(1)运算符
*,该运算符作用在指针变量上,也即其作用对象是地址。
(2)作用
可以实现对指针所指向的数据对象的间接访问,包括引用和赋值等基本运算。
例如:
[例3]:
inta,b=2,c,*p;
……
p=&b;
scanf("%d",&a);
c=a+*p;//等价于c=a+b,因为*p表示的是p指向的变量,即变量b。
注意:
这里的间接访问是对数据对象的间接引用。
[例4]:
voidmax(intx,inty,int*max)
{
if(x>y)
*max=x;//间接赋值
else
*max=y;//间接赋值
};
(3)关于“*”的说明
;“*”作为算术运算符,表示乘法,例如:
a*b。
υ
;“*”作为类型标识符,用来定义指针类型(出现在数据定义部分),例如:
intυ*p;。
;“*”作为指针运算符,表示间接访问,例如:
a+*p(p是指针变量)。
υ
5.指针应用实例
[例5]:
用指针实现字符串比较。
#include
intstrcmp(char*,char*);
intstrcmp(char*s,char*t)
{
for(;*s==*t;s++,t++)
if(*s=='\0')
return0;
return*s-*t;
}
voidmain()
{
chars1[100],s2[100];
intret;
cin>>s1>>s2;
ret=strcmp(s1,s2);
cout<}
说明:
(1)s和t都是指针,分别指向字符数组s1和s2;
(2)*s和*t表示间接引用s1和s2的当前数组元素;
(3)s++和t++用来改变指针值,使其指向下一个数组元素。
(4)*s-*t得到两个字符串中首次出现的不相等的字符的差值,用来决定两个字符串的大小。
四、指针与数组
我们前面已多次提到数组与指针的关系,数组名用来存放数组的内存首地址,也即第一个数组元素的内存地址,因此数组名是一种特殊的指针变量。
1.数组名是指向数组元素的指针变量
对于数组a,数组名a和数组元素地址关系如下:
a等于&a[0],a+i等于&a[i](见图6.6),这意味着我们可以用数组名指针的地址偏移来代替数组元素的下标描述。
a
a+i
图6.6数组名是数组元素的指针
如果将数组名赋予另一指针变量aptr,aptr=a;则aptr等于&a[0],aptr+i等于&a[i]。
1.通过指针间接访问数组元素
因为数组元素的下标描述可以用数组名指针的偏移来代替,所以我们可以用指针间接访问数组元素,例如,对于数组a,有:
a等于a[0],*(a+i)等于a[i]。
当执行了aptr=a后,aptr等于a[o],*(aptr+i)等于a[i]。
[例6]:
试比较以下三个程序。
程序1
voidmain()
{
inta[10];
inti;
for(i=0;i<10;i++)
scanf("%d",&a[i]);
for(i=0;i<10;i++)
printf("%d",&a[i]);
}
程序2
voidmain()
{
inta[10];
inti;
for(i=0;i<10;i++)
scanf("%d",(a+i));
for(i=0;i<10;i++)
printf("%d",*(a+i));
}
程序3
voidmain()
{
inta[10];
inti,*p;
for(i=0;i<10;i++)
scanf("%d",(a+i));
for(p=a;p<(a+10);p++)
printf("%d",*p);
}
上面的三个程序执行结果是相同的。
字符指针表示字符数组,用在字符串处理上将会显得特别灵活,请看下面的例子。
[例7]:
设有字符串s1,将该串从第5个字符开始直至最后一个字符的右子串部分复制到s2中。
此操作称为“复制右子串”。
程序:
#include
#include
voidRightString(char*,char*,int);
voidmain()
{
chars1[100],s2[100];
intn1;
cin>>s1>>n1;
RightString(s1,s2,n1);
cout<<"s1="<};
voidRightString(char*s1,char*s2,intn)
{
char*p;
p=s1+n-1;//p指向s1[n-1],p表示的数组是s1的一部分。
strcpy(s2,p);
}
试问,如果没有引入p指针,本问题应如何解决?
3.指针数组
(1)什么叫指针数组
元素是指针的数组称为指针数组。
例如name是表格中的一个列,该列有3个单元格,分别存放3个同学的姓名,则name可以表示成字符指针数组:
char*name[]={"Lin","Ding","Zhan"};
赋初值后,name数组的每一个元素都存放一个字符指针,这些字符指针的值就是对应字符串的首地址(如图6.7所示),例如name[0]本身是一个字符指针,它存放的是"Lin"的首地址,实际上可以认为name[0]指向一个一维字符数组,name[1]同样也指向一个一维字符数组……,所以字符指针数组和二维字符数组有相似之处。
(2)指向指针的指针
指针数组是数组,那么指针数组名的类型就是指向指针的指针,称为二级指针,除了描述成指针数组之外,还可以描述成**类型,例如:
char*name[]={"Lin","Ding","Zhan"};
char**pname;
pname=name;
pname等于name[0],*(pname+1)等于name[1]……,依此类推。
[例8]:
分析下面程序的执行结果:
#include
voidmain()
{
char*name[]={"Lin","Ding","Zhan"};
char**pname;
pname=name;
cout<<*pname<<*(pname+1);//等价于cout<
<<>}
输出:
LingDing。
name data`sconstarea
Tu1
图6.7字符指针数组和字符串的内存表示
[例9]:
下面的程序将指符指针数组传递给函数,也即传递二级指针给函数。
程序:
//****************************************
//* cpp6-4 *
//****************************************
#include
voidPrintString(char*[],int);
voidmain()
{
char*pn[]={"Fred","Barney","Wilma","Betty"};
intnum=sizeof(pn)/sizeof(char*);
PrintString(pn,num);
}
voidPrintString(char**arr,intlen)
{
for(inti=0;i cout<<(int)arr[i]<<""<}
注意:
函数参数描述成二级指针**arr,而对参数的引用则描述成arr[i]。
输出arr[i]实际上是输出字符指针,也就是输出该指针所指向的字符串。
五、指针与函数
函数和指针的关系首先体现在函数的参数是指针类型数据,例如数组参数、指针变量参数等等。
这一问题我们在前面已多次提过。
其次,函数和指针的关系还体现在函数的类型本身就是指针类型,这样的函数如何定义,有何用途,下面我们将加以说明。
1.函数的指针类型参数
1)形式:
可以定义成基本指针变量和数组。
2)作用:
返回函数对指针的修改,实质上是返回函数对指针所指向的数据对象的修改,这样可以返回不止一个值,同时还可以节省大量的内存空间,因此具有很大的灵活性和实用性。
3)带有指针参数的函数的实现过程:
(1)在函数声明中定义指针类型参数,例如voidswap(int*x,int*y)。
(2)在函数调用时提供相应的变量或数组地址(传地址),例如swap(&a,&b),这里a和b必须是作用域包含调用swap的函数的整型变量,可以是全局的也可以是局部的。
(3)函数的执行部分对指针形参进行间接访问,例如,对*x和*y的操作,间接地导致对上层函数的a和b两个数据对象进行操作,参见图6.8。
4)使用指针类型参数的副作用
指针类型参数的灵活性体现在它使函数可以访问本函数的局部空间(栈空间)以外的内存区域,但这明显破坏了函数的黑盒特性,带来以下副作用:
(1)可读性问题:
因为对数据对象的间接访问比直接访问相对难以理解。
(2)重用性问题:
函数调用依赖于上层函数或整个外部内存空间环境,丧失其封装特性(黑盒特性),所以无法作为公共模块来使用。
(3)调试的复杂性问题:
跟踪错误的区域从函数的局部数据区扩大到整个内存空间,不但要跟踪变量,还要跟踪地址,错误现象从简单的不能得到相应返回结果,衍生到系统环境遭破坏甚至死机。
2.指针类型函数
函数的类型是指针类型,这样的函数称为指针函数,例如:
char*strcpy(char*s1,const*s2),该函数的类型是字符指针,也即该函数调用结果返回一个字符串s1的地址。
[例10]:
分析下面程序的执行结果?
程序:
#include
#include
main()
{
chars1[100],s2[]={"aaa"};
cout<}
该程序执行结果输出字符串aaa。
注意:
通常将字符串处理函数和内存分配函数定义成指针函数。
目的在于能够直接返回处理以后形成的新字符串地址或所分配到的内存空间首地址(见6.6)
六、堆内存管理
程序运行过程中允许直接进行内存管理是C++的一大特色,通过直接内存管理可以实现动态存储分配,提高内存使用率。
以下几种情况尤其需要这一技术的支持:
;程序(函数)中定义的数组,其大小事先难以确定,如果定义过大,会造成存储空间的浪费,采用即时申请内存空间的办法,不但可以动态建立数组,而且可以保证其大小总是符合实际情况的。
λ
;一个函数中包含太多的数组,一旦该函数被调用,就必须占据大量的栈空间,通常这些数组并不是同时使用的,这同样造成太大的浪费。
采用堆内存管理技术,就可以控制程序在实际需要使用某一数据对象时才去申请数据空间,一旦用完,马上释放。
λ
;程序中定义了结构体、类或其它数据对象,这样的数据对象有时需要超乎寻常的内存空间,同样也需要堆内存的支持。
λ
1.堆内存
堆(Heap)是区别于栈区、全局数据和代码区的另一内存区域,允许用户程序运行过程中动态申请与释放。
管理堆内存的函数有:
malloc、calloc、free、memcpy、memmove、memset以及new和delete等等,后两个函数是C++特有的。
下面分别讨论。
2.申请堆内存
1)calloc函数
(1)格式
void*calloc(size_tn,size_tsize);
;参数υ
n:
数组的长度(数组元素个数),size_t等同与unsignedlong;
size:
数组元素的字节数,可以用sizeof来计算,例如:
sizeof(int)计算整型数据的长度;
sizeof(char)计算整型数据的长度,等等。
;函数类型:
为voidυ*,也即无符指针类型,在实际调用时,必须依据分配对象的类型进行强制转换,例如:
char*s;
int*a;
s =(char*)calloc(10,sizeof(char));//将返回值转换成字符指针
a =(int*)calloc(100,sizeof(int));//将返回值转换成整型指针
null:
申请失败
;函数的返回值 υ
被分配的堆内存空间首地址:
申请成功。
(2)功能
为一个具有n个元素的数组分配内存空间,每个元素的长度为size字节。
注意:
凡是用calloc申请的内存空间(由对应数据指针指向),必须调用free函数按对应数据指针进行释放。
[例11]:
将例7的s1和s2两个字符数组改成通过字符指针动态申请空间。
程序:
#include
#include
#include
voidRightString(char*,char*,int);
main()
{
char*s1,*s2;
intlen,n1;
if((s1=(char*)calloc(100,sizeof(char)))==NULL)
{
cout<<"申请不到内存空间";
//return;
}
cin>>s1>>n1;
len=strlen(s1);//获取s1的实际长度
cout<<"len="<if((s2=(char*)calloc(len-n1+2,sizeof(char)))==NULL)
//len-n1+2是s2的实际长度
{
cout<<"申请不到内存空间";
free(s1);//释放s1所指向的内存;
//return;
}
RightString(s1,s2,n1);
cout<<"s1="< free(s1);