C++ 学习笔记(三)
参考资料:面向对象程序设计–赵宏
本笔记石墨文档链接
第七章 指针和引用
7.1 指针的概念
声明任何一个变量,系统都会为其分配一定大小的内存,访问变量实际上是访问其所占据的内存空间。比如:
int a=10;
系统在内存中为 a 分配了 sizeof(int)=4的空间,并且该片内存中所存储的数据为10;a 所占据的首地址(即起始地址)可通过”&a"获取,其中“&"称为取址运算符。
cout<<a;
系统将变量a 所占据的内存空间中存储的数据取出来并显示在屏幕上。
指针是用于存放内存地址的一种数据类型。
指针变量 p=&a;
7.2 指针变量的声明,初始化和访问
7.2.1 指针变量的声明
指针变量也是一种变量,使用之前需要先声明,声明形式为:
*数据类型 变量名;
其中,数据类型表示指针变量所指向的数据的类型,”*“表示所要声明的变量为一个指针变量,而不是普通变量。
注意:要同时定义两个指针变量p1和p2,需要写成如下形式:
int *p1,*p2;
如果写成 int *p1,p2; 则表示声明了一个指针变量和一个普通变量。
7.2.2 指针变量的初始化
与普通变量一样,在声明指针变量时,可以对其进行初始化。形式为:
*数据类型 变量名=地址表达式;
地址表达式一般有三种形式:
- 初始化为 NULL 或0:NULL 为系统定义的一个常量,其值为0,表示指针变量指向的是一片无效的不可访问的内存。
int *p=NULL;
- 初始化为已声明变量的地址:将一个已声明变量的地址作为指针变量的初值,此时通过该指针变量可以直接操作已声明变量占据的内存。
int a;
int *p=&a;
- 初始化为某一动态分配内存空间的地址。
注意:
- 在访问指针变量所指向的内存时,要求该片内存必须是有效的可以访问的,当一个指针变量指向无效内存时,应将其赋值为 NULL (注意大写)或0。在访问指针变量所指向内存之前,应判断所指向内存是否有效:
if(p1!=NULL)
{
//访问p1指向的内存
}
- 指针变量的声明类型与其所指向的变量的类型必须一致,否则就要给出显式的强制类型转换。
int a;
int *p1=&a; //正确
char *p2=&a; //错误,指针类型与指向变量类型不一致
char p3=(char)&a; //正确,通过强制转换,将 int型地址转换为char型,不建议使用
7.2.3 指针变量的访问
声明指针变量并保证其指向一片有效的内存地址后,可以通过指针变量访问内存中的数据。
其访问形式为:
*指针变量名
#include<iostream>
using namespace std;
int main()
{
int a=9,b=5;
int *p1=&a,*p2=&b;
cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
*p1=8;
*p2=2;
cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
a=3;
b=6;
cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
*p1=*p2;
cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
p1=p2;
*p1=7;
cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
return 0;
}
提示:
- 至今为止,”*“运算符有三种情况:
(1)乘法运算符;
(2)声明指针变量;
(3)访问指针变量所指向的内存:当出现在非变量声明语句中,且前面没有操作数,后面有一个指针变量时,表示访问紧随其后的指针变量所指向的内存。此时”*“被称为间接访问运算符,或取内容运算符,其与取地址运算符”&“功能相反。
- 程序中既可以修改指针变量指向内存的数据,也可以修改指针变量所指向的内存地址。
*p1=*p1; //修改内容
p1=p2; //修改地址。
7.3 指针与数组
将数组所占据内存空间的首地址赋给指针后,可以使用指针访问数组中的元素。
7.3.1 用指针操作一维数组
步骤:
- 定义相同类型的数组和指针变量;
- 将指针变量指向数组首地址;
- 使用指针变量访问数组元素。
#include<iostream>
using namespace std;
int main()
{
int a[]={1,2},b[]={5,6,7};
int *p=NULL;
p=a;
cout<<p[0]<<" "<<p[1]<<endl;
p=&b[0];
cout<<p[0]<<" "<<p[0]<<endl;
return 0;
}
提示:
- 将数组首地址赋给指针变量后,可以使用指针代替数组名去进行访问数组元素;
- 取一维数组的首地址:数组名或者&数组名[0].
代码中 &数组名 也可以
7.3.2 用指针操作二维数组
可使用两种不同类型的指针变量操作二维数组。
(1)使用指向行的指针变量操作二维数组。
对于二维数组来说,数组名表示数组第0行的首地址,即对于数组a[3][2]来说,a 与 &a[0]等价。指针变量的类型必须与其所存储的地址类型一致。使用数组名或&数组名[0]为指针赋值时,要求该指针必须声明为一个指向行的指针变量。指向行的指针变量声明形式为:
*数据类型 (指针变量名)[行长度];
#include<iostream>
using namespace std;
int main()
{
int a[][2]={1,2},b[][2]={3,4};
int (*p)[2]=NULL;
p=a;
cout<<p[0][0]<<" "<<p[0][1]<<endl;
p=&b[0];
cout<<p[0][1]<<" "<<p[0][1]<<endl;
return 0;
}
除此之外,还可以:
#include<iostream>
using namespace std;
int main()
{
int a[][2]={{1,2},{5,6}},b[][2]={{3,4}}; //注意只有一行的二维数组的声明
int (*p)[2]=NULL;
p=a;
cout<<p[0][0]<<" "<<p[1][1]<<endl; //指向行的指针可以访问数组中任意一个元素
p=&b[0];
cout<<p[0][0]<<" "<<p[0][1]<<endl;
return 0;
}
(2)使用指针变量操作二维数组
二维数组的元素在内存中按照先行后列的顺序连续排放,其存放方式与含有同样元素数目的一维数组完全一样,可以将MN大小的二维数组看成 包含MN 个元素的一维数组。
#include<iostream>
using namespace std;
int main()
{
int a[][2]={1,2,3,4},b[][2]={4,3,2,1};
int *p=NULL;
p=a[0];
cout<<p[0]<<" "<<p[1]<<" "<<p[2]<<" "<<p[3]<<endl;
p=&b[0][0];
cout<<p[0]<<" "<<p[1]<<" "<<p[2]<<" "<<p[3]<<endl;
return 0;
}
提示:
- 使用一级指针变量p 操作二维数组a[M][N]时,要访问元素 a[m][n],则应该写为 p[m*N+n].
- a[0]与 &a[0][0]等价,都表示二维数组第一个元素的首地址。
7.3.3 数组名与指针变量的区别
数组名表示数组的首地址,指针变量可以指向数组的首地址,数组名和指针变量有什么区别吗?
数组名也可以看做是一个指针,只不过是一个指针常量。因此数组名虽然可以用于表示数组的首地址,但是其值在程序中不能发生变化。
指针变量指向的地址可在程序中根据需要修改。
因此,指针变量可以作为赋值语句的左值,数组名不可以。
int array[]={1,2,3};
int a[3];
int *p=NULL;
a=array; //错误。数组名不能作为赋值语句的左值。
p=array; //正确。
二维数组中数组名可以作为数组的首地址,数组名[m]可以表示行地址。
int array={1,2,3};
int a[2][3]={1,2,3,4,5,6};
int b[2][3];
b=a; //错误。
b[0]=array; //错误。
对于数组来说,只有其中的元素可以进行修改,数组首地址、行地址等在声明数组时就已经确定,不能修改。
7.3.4 指针数组
指针是一种数据类型,所以可以创建一个指针类型的数组。指针数组同样可以有不同的维数,一维指针数组的声明形式为:
*数据类型 数组名[长度];
指针数组中每个元素都是指向同一数据类型的指针变量,指针数组元素的访问方法与一般数组元素的访问方法完全一样。
#include<iostream>
using namespace std;
int main()
{
int a[2]={1,2};
int *p[2];
p[0]=a;
p[1]=&a[1];
cout<<a<<" "<<&a[1]<<endl;
cout<<p[0]<<" "<<p[1]<<endl;
return 0;
}
7.4 指针的运算
指针作为一种数据类型,也可以进行运算,包含:指针加减整数、两个指针相减、两个指针做关系运算。(指针存储的是内存地址,对其进行加减乘除没有任何意义)
7.4.1 指针加减整数
设 p 为指针变量,n 为一个整数,指针加减运算后得到的结果仍然是一个地址,指针加减运算共包含以下六种形式:
- p+n
从 p 指向的地址开始后面第 n 项数据的地址.
char 型:1 个字节,p和 p+n之间差 n个字节的内存;
short 型:2 个字节,p和 p+n之间差 n2个字节的内存;
int long float 型:4 个字节,p和 p+n之间差 n4个字节的内存;
double 型:8 个字节,p和 p+n之间差 n*8个字节的内存;
#include<iostream>
using namespace std;
int main()
{
int a[2]={1,2};
int *p[2];
p[0]=a;
p[1]=p[0]+1;
cout<<a<<" "<<&a[1]<<endl;
cout<<p[0]<<" "<<p[1]<<endl;
return 0;
}
提示:
(1)令 p 指向数组 a 第 m 项的地址,那么 p+n 指向第(m+n)项数据的地址。
(2)令 p 指向数组元素的首地址,那么(p+n)指向数组 a 第 n项数据的地址,通过*(p+n)可以访问 a[n]。有如下两种等价关系成立:
a[n], p[n], *(a+n), *(p+n)等价;
&a[n], &p[n], (a+n), (p+n)等价。
(3)不同内存的使用情况下输出数组中各元素的内存地址会有所不同,但相邻两个元素之间的地址间隔固定。
#include<iostream>
using namespace std;
int main()
{
int a[]={1,2,3,4};
int *p=NULL;
p=a;
for(int i=0;i<sizeof(a)/sizeof(int);i++)
{
cout<<"a["<<i<<"]的地址为"<<p+i<<",其中的内容是:"<<*(p+i)<<endl;
}
return 0;
}
- p-n
从 p 指向的地址开始前面第 n 项的地址。
- p++
先获取当前 p 的值进行表达式运算,然后通过 p=p+1将 p+1的值赋给 p,使得 p 指向下一项数据的地址。
- p–
先获取当前 p 的值进行表达式运算,然后通过 p=p-1将 p-1的值赋给 p,使得 p 指向前一项数据的地址。
- ++p
通过 p=p+1将 p+1的值赋给 p,使得 p 指向下一项数据的地址,然后进行表达式运算。
- –p
通过 p=p-1将 p-1的值赋给 p,使得 p 指向前一项数据的地址,然后进行表达式运算。
#include<iostream>
using namespace std;
int main()
{
int a[]={1,2};
int *p=NULL;
p=a;
*(p++)=3;
cout<<a[0]<<" "<<a[1]<<endl;
p=a;
*(++P)=4;
cout<<a[0]<<" "<<a[1]<<endl;
return 0;
}
“*(p++)=3;” 相当于
p=3;
p++;
"(++p)=4;" 相当于
p++;
*p=4;
7.4.2指针相减运算
假设p1、p2 为同一类型的变量指针,通过"p1-p2"能够得到p1 和 p2 之间的数据项的数目,当 p1 指向的地址在 p2 的前面时,结果为负值,否则为正值。
#include<iostream>
using namespace std;
int main()
{
int a[]={1,2};
int *p1=NULL, *p2=NULL;
p1=&a[0];
p2=&a[1];
cout<<p1-p2<<" "<<p2-p1<<endl;
return 0;
}
7.5 指向指针的指针
前面学习的指针指向的内存存储的是非指针类型的数据,这样的指针称为一级指针。指针变量也是一种变量,因此声明一个指针变量后,系统也要为其分配内存空间来存储指针变量的值。那么可以再声明一个指针,用它指向存储一级指针变量的内存,这种指向指针的指针就被称为二级指针,同样的还有三级指针等等。
二级指针变量的声明方式为:
**数据类型 变量名;
使用二级指针操作二维数组
#include<iostream>
using namespace std;
int main()
{
int i,j,a[][2]={1,2,3,4,5,6};
int *p[3]; //声明一个指针数组,大小为3
int **pp;
for(i=0;i<3;i++)
{
p[i]=a[i];
}
pp=p;
for(i=0;i<3;i++)
{
for(j=0;j<2;j++)
cout<<*(*(pp+i)+j)<<" ";
cout<<endl;
}
return 0;
}
((pp+i)+j)可以写成(pp[i]+j)或者 pp[i][j];*
pp 是指向 指针数组 p 的首地址 的指针,pp 是指针数组p 的首地址中的内容,即是 a[0]的地址,因此(pp)是 a[0]的内容,a[1]的地址是 a[0]的地址+1,因此 a[1]的内容是(pp+1).a[2]的地址是指针数组 p[1],pp+1即是 p[1]的地址,p1 的内容是(pp+1),因此 a[2]的内容是*(*(pp+1))。
7.6 const 指针
由于符号常量的值不能被修改,通过加上 const 修饰词限制对指针的赋值操作。根据对不同复制操作的限制,const 指针分为指针常量和常量指针。
7.6.1 常量指针
常量指针的声明形式为:
*const 数据类型 变量名;
常量指针所指向的内存地址可以改变,但通过常量指针只能从内存读取数据,但不能修改内存中的数据。
int a=10,b=20;
const int c=30;
const int *p=&a;
cout<<p<<endl;
*p=15; //错误
a=15; //正确
*p=&b; //正确,可以更改变量指针指向的内存。
*p=&c; //正确,因为常量指针不能修改内存,因此可以将符号常量的地址赋给指针常量。
7.6.2 指针常量
指针常量的声明形式:
*数据类型 const 常量名;
指针常量是一个常量而不是变量,因此,指针常量指向的内存是固定的不能改变。但通过指针常量可以修改指向的内存的数据。
int a=10,b=20;
int *const p=&a;
*p=30; //正确。
p=&b; //错误,不能修改指针常量指向的内存地址
cout<<a<<endl;
可以将一个指针同时声明为指针常量和常量指针,此时既不能改变指针指向的数据地址,也不能通过指针修改内存中的数据。
int a=10,b=20;
const int *const p=&a;
*p=20; //错误
p=&b; //错误
7.7 堆内存分配
在第六章学习数组时,声明数组前必须先确定数组的长度,但是我们可能无法预先知道要处理的数据量有多大。只能使数组定义的足够大,这样会造成内存的浪费。因此我们引入堆内存分配的概念。
堆是内存中的一片空间,它允许程序运行时从中申请某一长度的内存空间。堆内存分配就是动态内存分配,是指程序运行时从堆中为指针变量申请实际需要的内存空间,并且当这片内存空间不再使用时,可以将这片堆内存及时释放。
使用 new 和 delete 两个关键字完成堆内存分配和释放的操作,堆内存分配 new 的用法为:
new 数据类型[表达式];
注意:
- 使用 new 分配内存时,必须使用 delete 释放,不然会造成内存泄漏(即内存空间一直处于被占用状态,导致其他程序无法使用)。系统出现大量内存泄漏时会导致系统可用资源减少,计算速度变慢。
- 在使用 new 分配堆内存时,要注意区分[],().[]中的表达式指定了元素数目,而()中的表达式指定了内存的初值。
int a=3;
int *p1=new int[a]; //分配了3个int型元素大小的内存空间
int *p2=new int(a); //分配了 1 个 int型元素大小的内存空间,且初值为3.
- 在分配堆内存时,为多个元素分配了空间,那么在使用 delete 释放堆内存时必须加[],否则会造成内存泄漏。
int a=3;
int *p=new int[a];
...
delete p; //只释放了第一个 int 型元素所占据的堆内存,后两个没有释放。
- 必须使用指针保存分配的堆内存首地址,这是由于 delete 根据首地址进行堆内存释放,如果不知道首地址则无法释放内存,从而造成内存泄漏。
int *p=new int[2];
*(p++)=10; //将首地址的内存数据赋值为 10 后,p=p+1,p 指向下一个数据的地址。
*(p++)=20;
delete []p; //错误,指针 p 没有指向分配的堆内存的首地址。
其中,表达式可以是常量也可以是变量,其作用与声明数组时[]里的表达式的作用一样,用于指定数目,如果只申请一个元素的空间,那么[表达式]部分可以不写:
new 数据类型;
同时还可以进行内存的初始化工作:
new 数据类型(表达式);
堆内存释放 delete 的用法为:
delete []p; ** //p 为指向待释堆内存首地址的指针。
如果 p 指向的堆内存只有一个元素**,可以将[]省掉。
delete p;
例题:
使用堆内存分配方式实现学生成绩录入功能,要求程序运行时由用户输入学生人数。
#include<iostream>
using namespace std;
int main()
{
int *pScore=NULL;
int n,i;
cout<<"请输入学生人数:";
cin>>n;
pScore=new int[n];
if(pScore==NULL)
{
cout<<"堆内存分配失败!"<<endl;
return 0;
}
for(i=0;i<n;i++)
{
cout<<"请输入第"<<i+1<<"名学生的成绩:";
cin>>pScore[i];
}
for(i=0;i<n;i++)
{
cout<<"第"<<i+1<<"名学生的成绩为:"<<*pScore[i]<<endl; //也可以写为 pScore[i];
}
delete []pScore;
pScore=NULL;
return 0;
}
提示:
- 使用 new运算符申请内存,会返回申请到的内存空间的首地址。在程序中应将这个首地址保存在指针变量中,然后就可以使用该指针变量操作分配的堆内存空间。
- 可以将堆内存分配方式看做是一种动态数组,堆内存分配方式与数组的主要区别在于:数组在程序运行前必须确定长度;堆内存方式分配方式则可以在程序运行时根据实际情况确定长度。堆内存访问方式与数组完全一样。
- 当申请分配的内存太大、系统资源不够时,堆内存分配会失败,此时会返回 NULL,表示堆内存分配失败。因此分配内存后,应判断返回值是否是 NULL,如果是则报错并退出程序。
7.8 引用
7.8.1 引用的概念
引用就是别名,变量的引用就是变量的别名,对引用的操作就是对所引用变量的操作。
7.8.2 引用的声明与特点
引用的声明形式为:
数据类型 &引用名 = 变量名;
其中 & 是引用运算符。
提示:
- 建立引用时,必须用一直变量为其初始化,表示该引用就是该变量的别名。其所引用的对象一旦确定就不能修改。
- “&” 作用于引用名,表示紧随其后的为一个引用。如果要同时定义两个 int 型引用 r1 和 r2,必须写成如下形式:
int a,b;
int &r1=a,&r2=b;
- 至今为止,“&”运算符有以下三种用法:
位于运算符;
取地址运算符;
引用运算符;出现在声明语句中。
#include<iostream>
using namespace std;
int main()
{
int a=3,b=4,*p=&a;
int &r=a;
int *&rp=p;
cout<<"a="<<a<<","<<"r="<<r<<endl;
cout<<"p="<<p<<","<<"rp="<<rp<<endl;
a=5;
rp=&b;
cout<<"a="<<a<<","<<"r="<<r<<endl;
cout<<"p="<<p<<","<<"rp="<<rp<<endl; //指针指向的内存的地址
cout<<"&a="<<&a<<","<<"&r="<<&r<<endl;
cout<<"&p="<<&p<<","<<"&rp="<<&rp<<endl; //指针的地址
return 0;
}
提示:
- 引用就是一个别名,声明引用不会再为其分配内存空间,而是与所引用的对象占据同一片内存空间,因此对用用的操作与对所引用对象的操作完全一样。
- 可以为指针变量声明引用,声明形式为:
*数据类型 &引用名=指针变量名;
7.8.3 const引用
通过加上const 关键字限制对引用的复制操作,与指针不用的是,引用本身一旦初始化后就不能更改其指向,因此,const 没有引用常量和常量引用之分。声明形式有两种:
const 数据类型 &引用名=变量名或常量;
数据类型 const &引用名=变量名或常量;
const 引用只能访问引用对象的值,不能通过引用名改变引用对象的值。
int a=3;
const &r=a;
r=10; //错误。
由于 const 引用不需要修改所引用对象的值,所以 const 引用可以使用常量对其进行初始化,非 const 引用不可以。
const int &r1=3; //正确
int &r2=3; //错误
7.8.4 指针与引用的区别
指针与引用都可以直接操作它们所指向的内存地址,虽然指针和引用是两个概念,但是在编译程序时,编译器一般会将引用自动转换为指针。在具体使用上存在一些区别。
- 指针是一个变量,本身要占据内存空间,引用仅仅是一个别名。
- 引用在声明时确定其指向,初始化后就不能修改;而指针在声明时不必初始化,可在声明后的任何地方赋值且可以进行多次赋值。
- 访问指针所指向的内存中的内容时,需要在指针变量前加“*”,引用则可以直接写引用名。