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 指针变量的初始化

与普通变量一样,在声明指针变量时,可以对其进行初始化。形式为:
*数据类型 变量名=地址表达式;
地址表达式一般有三种形式:

  1. 初始化为 NULL 或0:NULL 为系统定义的一个常量,其值为0,表示指针变量指向的是一片无效的不可访问的内存。

int *p=NULL;

  1. 初始化为已声明变量的地址:将一个已声明变量的地址作为指针变量的初值,此时通过该指针变量可以直接操作已声明变量占据的内存。

int a;
int *p=&a;

  1. 初始化为某一动态分配内存空间的地址。

注意:

  1. 在访问指针变量所指向的内存时,要求该片内存必须是有效的可以访问的,当一个指针变量指向无效内存时,应将其赋值为 NULL (注意大写)或0。在访问指针变量所指向内存之前,应判断所指向内存是否有效:

if(p1!=NULL)
{
//访问p1指向的内存
}

  1. 指针变量的声明类型与其所指向的变量的类型必须一致,否则就要给出显式的强制类型转换。

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. 至今为止,”*“运算符有三种情况:

(1)乘法运算符;
(2)声明指针变量;
(3)访问指针变量所指向的内存:当出现在非变量声明语句中,且前面没有操作数,后面有一个指针变量时,表示访问紧随其后的指针变量所指向的内存。此时”*“被称为间接访问运算符,或取内容运算符,其与取地址运算符”&“功能相反。

  1. 程序中既可以修改指针变量指向内存的数据,也可以修改指针变量所指向的内存地址。

*p1=*p1; //修改内容
p1=p2; //修改地址。

7.3 指针与数组

将数组所占据内存空间的首地址赋给指针后,可以使用指针访问数组中的元素。

7.3.1 用指针操作一维数组

步骤:

  1. 定义相同类型的数组和指针变量;
  2. 将指针变量指向数组首地址;
  3. 使用指针变量访问数组元素。
#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;
}

提示:

  1. 将数组首地址赋给指针变量后,可以使用指针代替数组名去进行访问数组元素;
  2. 取一维数组的首地址:数组名或者&数组名[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;
}

提示:

  1. 使用一级指针变量p 操作二维数组a[M][N]时,要访问元素 a[m][n],则应该写为 p[m*N+n].
  2. 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 为一个整数,指针加减运算后得到的结果仍然是一个地址,指针加减运算共包含以下六种形式:

  1. p+n

从 p 指向的地址开始后面第 n 项数据的地址.
char 型:1 个字节,p和 p+n之间差 n个字节的内存;
short 型:2 个字节,p和 p+n之间差 n2个字节的内存;
int long float 型:4 个字节,p和 p+n之间差 n
4个字节的内存;
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;
}
  1. p-n

从 p 指向的地址开始前面第 n 项的地址。

  1. p++

先获取当前 p 的值进行表达式运算,然后通过 p=p+1将 p+1的值赋给 p,使得 p 指向下一项数据的地址。

  1. p–

先获取当前 p 的值进行表达式运算,然后通过 p=p-1将 p-1的值赋给 p,使得 p 指向前一项数据的地址。

  1. ++p

通过 p=p+1将 p+1的值赋给 p,使得 p 指向下一项数据的地址,然后进行表达式运算。

  1. –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 数据类型[表达式];
注意:

  1. 使用 new 分配内存时,必须使用 delete 释放,不然会造成内存泄漏(即内存空间一直处于被占用状态,导致其他程序无法使用)。系统出现大量内存泄漏时会导致系统可用资源减少,计算速度变慢。
  2. 在使用 new 分配堆内存时,要注意区分[],().[]中的表达式指定了元素数目,而()中的表达式指定了内存的初值。
int a=3;
int *p1=new int[a];    //分配了3个int型元素大小的内存空间 
int *p2=new int(a);    //分配了 1 个 int型元素大小的内存空间,且初值为3.
  1. 在分配堆内存时,为多个元素分配了空间,那么在使用 delete 释放堆内存时必须加[],否则会造成内存泄漏。
int a=3;
int *p=new int[a];
...
delete p;       //只释放了第一个 int 型元素所占据的堆内存,后两个没有释放。
  1. 必须使用指针保存分配的堆内存首地址,这是由于 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;
}

提示:

  1. 使用 new运算符申请内存,会返回申请到的内存空间的首地址。在程序中应将这个首地址保存在指针变量中,然后就可以使用该指针变量操作分配的堆内存空间。
  2. 可以将堆内存分配方式看做是一种动态数组,堆内存分配方式与数组的主要区别在于:数组在程序运行前必须确定长度;堆内存方式分配方式则可以在程序运行时根据实际情况确定长度。堆内存访问方式与数组完全一样。
  3. 当申请分配的内存太大、系统资源不够时,堆内存分配会失败,此时会返回 NULL,表示堆内存分配失败。因此分配内存后,应判断返回值是否是 NULL,如果是则报错并退出程序。

7.8 引用

7.8.1 引用的概念

引用就是别名,变量的引用就是变量的别名,对引用的操作就是对所引用变量的操作。

7.8.2 引用的声明与特点

引用的声明形式为:
数据类型 &引用名 = 变量名;
其中 & 是引用运算符。
提示:

  1. 建立引用时,必须用一直变量为其初始化,表示该引用就是该变量的别名。其所引用的对象一旦确定就不能修改。
  2. “&” 作用于引用名,表示紧随其后的为一个引用。如果要同时定义两个 int 型引用 r1 和 r2,必须写成如下形式:
int a,b;
int &r1=a,&r2=b;
  1. 至今为止,“&”运算符有以下三种用法:

位于运算符;
取地址运算符;
引用运算符;出现在声明语句中。

#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;
}

提示:

  1. 引用就是一个别名,声明引用不会再为其分配内存空间,而是与所引用的对象占据同一片内存空间,因此对用用的操作与对所引用对象的操作完全一样。
  2. 可以为指针变量声明引用,声明形式为:

*数据类型 &引用名=指针变量名;

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 指针与引用的区别

指针与引用都可以直接操作它们所指向的内存地址,虽然指针和引用是两个概念,但是在编译程序时,编译器一般会将引用自动转换为指针。在具体使用上存在一些区别。

  1. 指针是一个变量,本身要占据内存空间,引用仅仅是一个别名。
  2. 引用在声明时确定其指向,初始化后就不能修改;而指针在声明时不必初始化,可在声明后的任何地方赋值且可以进行多次赋值。
  3. 访问指针所指向的内存中的内容时,需要在指针变量前加“*”,引用则可以直接写引用名。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值