专题——C++中的指针

来源:互联网 发布:手帕 知乎 编辑:程序博客网 时间:2024/04/29 23:37

       指针作为程序设计语言中的一个重要概念,在教学和实践项目中都显得非常重要。在此作一个有关C++指针的专题介绍,旨在总结指针的概念、特点以及应用情况,以帮助广大C++初学者更好地使用C++语言解决他们在学习以及工作中遇到的困难。

1、程序、进程与内存

       当我们说编写一个具有什么功能的程序,或者运行某一个程序时,这里面所设计的到的程序是一个静态的概念,并不会产生任何结果;而当我们说一个正在运行的程序,或者某个程序占用了多少内存的时候,这样的程序则是一个动态的概念,即进程。进程将会占用系统资源以完成其预定义的功能并产生相应的结果。不过人们在实际工作中有时候并不是太过于区分程序和进程的概念,而往往以程序论之。

       当一个程序要运行的时候,操作系统首先需要把该程序调入内存,这样就创建了一个进程,这个过程称为加载。加载所要做的工作很多,但是最基本的任务是把程序代码以机器可以理解的方式调入内存,并且程序运行所需要的数据也会调入进程的地址空间。为了提高对内存中数据的访问速度,操作系统都是对内存地址进行编号了的,就好比如果你去小区找一个人,如果你知道这个人住小区的确切地址,比如43单元210,你就可以根据这个地址直接找到这个人。相应的,如果你知道了存储所需要数据的内存地址编号,就可以直接根据该编号访问内存中的数据了。这里所提的数据既可以是代表数学意义上的数值,也可以是程序设计意义上的程序代码(二进制代码),因为程序在运行时,其代码也是被加载进了内存的。

2、内存模型

       计算机系统中,内存的基本单位是字节。因此,内存编号也是以一个字节为一个基本单位的。内存编号的起止范围决定于操作系统的可寻址范围。例如,如果操作系统是16位的,即是说该操作系统支持16位的寻址空间,因此其地址范围从20~216-1。这就决定了对应于该16位操作系统的内存地址编号从0x0000-0xffff,见图1

         

同样,如果操作系统是32位的,即该操作系统支持32位的寻址空间,其地址范围从20~232-1,决定了对应于该32位操作系统的内存地址编号从0x00000000-0xffffffff

2.1、内存数据格式

       当得到某一个有效内存地址编号的时候,我们就可以操作以该编号为开始的内存数据了。为什么这里面要强调“有效”二字呢?操作系统为了保证自身的安全和稳定,以及一些其它因素,由用户所创建进程的运行地址空间范围是有限制的,访问内存数据所依赖的地址编号也是严格限制的,这样可以避免用户有意或无意地访问了操作系统核心数据,保证了操作系统的安全和稳定。

       这样,当得到一个有效内存地址编号时,我们怎么确定内存中是何种格式的数据,以及如何读取或更改所需要格式的数据呢?一般来说,内存中的数据主要分为两类:数值型和字符串。

       数值型的数据就是具有数学意义上的量,如121.234567-2-3e-4,等等;而字符串则表达了具有一定文法意义上的文本,如“你好!”,“hello, world!”等等。因此,在访问内存数据之前,首先需要确定访问的数据类型是什么。除此之外,还要确定要访问内存的大小,即从某一内存地址编号开始要访问多少个字节的内存。对于数值型的数据来讲,内存的大小决定了量的范围,而对于字符串型的数据来讲,内存的大小则决定了所表达的文本信息量。

       例如,假设编号为0x80000000的内存地址是一个有效的可访问地址,从该地址开始的连续4个字节内存的二进制内容依次为01000001010000100100001100000000,如图2

       

    可以用下表概括从地址编号为0x80000000开始,访问的数据类型与内存大小所决定的访问结果。

1. 数据类型与内存大小决定的访问结果

大小       类型

数值型

字符串型

1字节

65

“A”

2字节

16706

“AB”

3字节

4276803

“ABC”

4字节

1094861636

“ABCD”

       由此可以看到,即使获得了一个有效的内存地址编号,我们也需要用数据类型和内存大小来约束通过该编号进行的内存数据访问,以得到确切的结果。

3C++语言中的指针

       C语言中,指针的概念就已经存在,并且凭借指针的强大,赋予了C语言编程灵活、可直接访问内存、执行效率高等一系列的优点,可以毫不夸张地说,指针就是C语言的灵魂。作为与C语言完全兼容的C++语言则完美继承了指针的概念,并依托面向对象程序设计语言的特点,扩展了原有C语言指针中的概念,使得指针的使用达到了一个新的高度。

       但是,C++语言的初学者往往被指针弄得莫名其妙,即使当时能够明白指针的含义,然后看到某些源代码中的有关指针用法后,原本以为已正确理解指针的感觉荡然无存。指针这种被C++初学者认为神秘的东西是学习C++语言的一个障碍,而这个障碍可能使诸多C++学习者转向其它面向对象编程的语言了。

本节将通过三个小节的内容来阐述指针,希望对初学者有所帮助。

3.1C++语言中指针的本质

       C++语言中的指针本质是什么?恐怕这个问题是理解和使用指针的一个基本前提。其实,指针并不神秘,因为指针实际上就是一个变量,即指针的本质是变量

       为什么这么说呢?我们在使用C++语言编程的时候,经常所说的指针实际上是通过变量来表达的,即指针变量。由于程序员口头之间在交流时,为了方便或者某些原因,常常把“变量”两个字省略,但是这并不阻碍交流的结果,因为程序员们对此心领神会。在变量前面加上“指针”二字构成“指针变量”,又在一定程度上表明了指针变量和其它类型的普通变量有一定区别,即指针变量具有自身的特殊性。

       与普通变量相比较,指针变量具有以下几点特殊性,这也是C++初学者理解指针的难点所在,特别是后两条:

Ø        定义方法与普通变量不同

Ø        指针变量的值与普通变量所表达的含义不同

Ø        指针变量的运算方式与普通变量不同

3.1.1、指针变量的定义方法与普通变量不同

       int i;       // 定义一个int类型的普通变量,变量名为i

       int *i;     // 定义一个int类型的指针变量,变量名为i

       可见,一个星号(*)决定了一个变量在定义时是普通变量,还是指针变量。我们应该养成良好的代码编写风格,在定义指针变量时,为了强调该变量与普通变量之间的区别,经常在命名该指针变量时,将第一字符定为p,因此更好的定义指针变量的方式应该为:

       int *pi;   // 养成良好的定义指针变量的习惯

3.1.2、指针变量的值与普通变量所表达的含义不同

       数值型普通变量的值表达的意义非常明确,就是一个数学意义上的量。这个量的大小,即是变量的值。但是指针变量的值所表达的意义却是计算机系统内存的地址编号,虽然该编号也是一个数学意义上的量,但它具有更深一层的意义。

       int i = 3;              // 变量i的值为3,数学意义上的量

       int *pi = 3;          // 变量pi的值为3,实际意义是编号为3的内存地址单元

       这样,就可以根据第2节的知识来访问内存地址编号为3处的确切数据了,因为要访问的数据类型和内存大小已经由声明指针的数据类型决定了。即可以通过pi来访问一个int类型大小的内存空间,最后得到的结果是一个int类型的数据,至于这个数据为多少,则由这段内存空间中的位信息决定。

       指针变量的值除了可以是保存数据的内存地址编号之外,还可以是程序执行代码的内存地址编号。我们已经知道,程序的代码在执行之前也是被加载进了内存的,因此一些函数的执行代码也处于相应的进程地址空间,我们可以将指针变量的值赋为函数执行代码所在处的地址编号,这样可以根据需要以后通过使用该指针变量来调用相应的函数。如果指针变量的值代表了函数执行代码所在处的地址编号,则这样的指针变量又可以称为函数指针。

       // 定义一个全局函数

       int maxOf(int a, int b)

       {

              return (a > b ? a : b);

       }

       // 定义一个函数指针,值保存了maxOf函数代码在内存中的地址

       int (*pf)(int, int) = &maxOf;

       // 使用函数指针来调用maxOf函数求两个int数值的较大值

       int m = (*pf)(3, 5);     // 该条语句执行完毕后,m的值为5

3.1.3 指针变量的运算方式与普通变量不同

       在指针变量上的运算有以下几种:

ü        取值(*

ü        加或自增(+++

ü        减或自减(---

ü        访问指向对象的成员(->

取值运算是指针变量的一个基本运算,因为指针变量的值代表了内存地址编号,而我们更关心的是位于地址编号开始的一段内存空间中的数据。

int i = 3;              // 定义一个int变量i,并赋初始值3

int *pi = &i;        // 定义一个指针变量pi,并将变量i在内存中的地址赋予pi

std::cout << “the value pi points to is: ” << *pi << std::endl;

正如以前所说,指针变量本质上也是一个变量,因此在定义一个指针变量时,编译器也会为该指针变量分配内存空间,并设置该空间内的值为变量i在内存中的地址。当对pi施加取值运算时,实际上是通过指针变量pi所保存的内存地址编号来访问对应内存空间中的数据。所以代码的运行结果将会在标准输出设备上打印出3

取值运算符(*)与定义指针变量中的*作用不同。普通变量不能像指针变量那样在变量名前面加上*进行取值运算。

指针变量可以进行加、减以及自增和自减运算,和普通整型变量相比,其运算方式具有很大不同。

int i = 3;              // 定义一个int类型变量i,并赋初始值3

int j = i + 1;         // 定义另一个int类型变量j,赋初始值为i+1

i++;                     // 变量i自增运算

对普通变量进行加、减运算时,就是在表达数学意义的量的基础上进行加法和减法运算,并没有任何其它的特别之处。因此以上代码片断执行完毕后,变量j的值为4,变量i的值也为4

int *pi = &i;        // 定义指针变量pi,并将变量i的地址赋予pi

       int *p = pi + 1;    // 定义另外一个指针变量p,并赋初始值为pi+1

       pi++;                   // 指针变量pi自增运算

       对于指针变量,当进行加、减运算时,所遵循的规则不再是数学意义上的加减法原则了,实际变化量是指针所指向数据类型乘以变化值。因此,对于上述代码片断,指针变量p的值不是变量i在内存中的地址值加上1,而是加上1int类型数据在内存中占用的大小,即指针变量p的值为pi的值加上sizeof(int)。指针变量pi在进行完自增运算后,其值为原值加上sizeof(int)。利用这个性质,可以很容易地将指针变量与数组联系起来,通过指针变量的变化来访问数组元素。

       int a[4] = { 1, 2, 3, 4 };      // 定义并初始化一个具有4int类型元素的数组

       int *p = a;                         // 定义一个指针变量并将数组的首地址赋初值

       // 通过指针变量输出数组每一个元素的值

        std::cout << *p << ‘ ‘ << *(p+1) << ‘ ‘ << *(p+2) << ‘ ‘ << *(p+3) << std::endl;

       最后,当一个指针变量的值是某一个对象在内存中的地址值时,可以通过指针运算符->来访问该对象的成员,前提是该对象具有可供访问的成员。

       class sample {

       public:

              sample(int d) : _data(d) {}

              void display() const {

std::cout << “data of sample is: “ << _data << std::endl;

              }

       private:

            int _data;

      };

// pssample类型的指针变量,且指向有效对象

ps->display();      // 显示ps所指向对象的信息

3.2、易混淆的概念

       本小节介绍两组与指针有关的易混淆的概念。

3.2.1、指针常量与常量指针

       const关键字修饰的变量只能进行读操作,而不能更改变量的值。

       指针常量是指指针所指向内存地址中的数据是用const修饰的,即内存数据只能读取,而不能更改,但是指针变量的值却可以更改,即可以重新指向新的内存地址。

        int i = 3, j = 4;

        const int *p = &i;       // 定义一个指针常量,并赋初始值为变量i的地址

        *p = 1;          // 这条语句将发生编译错误,不能修改指针常量指向内存中的值

        p = &j;          // 可以更改指针常量所指向的内存地址

       常量指针则是指指针变量的值是一个常量,即不能重新指向其它的内存地址,但是却可以更改所指向内存地址中的数据。

       int* const p = &i;       // 定义一个常量指针,不赋初始值为变量i的地址

       *p = 1;          // 可以更改所指向内存地址中的数据

       p = &j;          // 这条语句将发生错误,不能再指向其它的地址

       如果指针变量名前的关键字即为const,则该指针变量就是一个常量指针,否则它是一个指针常量。另外,也可以这样定义一个指针常量:

int const *p = &i;       // 定义一个指针常量

这样,只要const关键字在*之前,指针变量就是一个指针常量,const关键字在*之后,它即为一个常量指针。

当然,如果综合前面两种情况,可以定义一个指向常量的常量指针:

       const int* const p = &i;    // 指针变量p是一个指向常量的常量指针

       这样,指针变量p既不能更改所指向内存地址中的数据,也不能再指向其它的内存地址。

数组名实际上是一个常量指针,即数组名不能再被赋予新的地址了,但是可以以指针的方式是用数组名来更改数组中的元素。数组名是第一个元素的地址。

        int a[3] = { 1, 2, 3 };

*a = 2;          // 更改第一个元素的值为2

*(a + 1) = 3; // 更改第二个元素的值为3

*(a + 2) = 4; // 更改第三个元素的值为4

3.2.2、指针数组与数组指针

       指针数组是一个各元素保存的均是内存地址值的数组,因此各元素都可以当做一个指针来使用。

       int i = 3, j = 4, k = 5;                              // 定义3int类型变量并初始化

                         int *pa[3] = { &i, &j, &k };                   // 定义一个指针数组,并初始化各个

// 元素为相应变量的地址

       *pa[0] = 6, *pa[1] = 8, *pa[2] = 10;      // 以指针方式更改变量的值

       执行以上代码片断,变量ijk的值分别为6810

数组指针是一个指向数组的指针,因此在对数组指针进行取值运算时(在指针变量名前面加*),所得到的结果是一个数组,即含有若干元素的数组的首地址。

       假设有如下代码片断:

       int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; // 定义一个具有2行,3列的二维数组

       int (*p)[3] = a;    // 定义一个数组指针,并使其指向数组a

       对于数组指针p,指向了一个2行,3列的二维数组a,因此在对p进行取值运算时,*p所得到的结果指向了一个一维数组,即数组a中的第1行中的3列元素所构成的一个具有3个元素的数组,值为数组a中第1行第1列元素的地址;同理,*(p+1)所得到的结果指向了数组a中第2行中的3列元素所构成的一个具有3个元素的一维数组,值为数组a中第2行第1列元素的地址。但是,*(p+2)则会访问越界,以后对p的操作可能会发生意想不到的情况。

       在此基础上,如果我们再进行深一层次的取值操作,则可以取得数组a中相应元素的值了,表2是对数组指针p操作的部分结果。

2. 对数组指针操作的部分结果

p操作方式

结果

等效的a操作的结果

*p

数组a1行元素首地址

a[0]

*(p+1)

数组a2行元素首地址

a[1]

**p

数组a1行第1列元素的值

a[0][0]

**(p+1)

数组a2行第1列元素的值

a[1][0]

*(*(p+1)+1)

数组a2行第2列元素的值

a[1][1]

p[0][0]

数组a1行第1列的值

a[0][0]

p[1][2]

数组a2行第3列的值

a[1][2]

       可见,指针数组与数组指针在声明或者定义语法上只差一个小括号(),但是其意义以及得到的结果却相差甚远。

3.2.3、多级指针

       多级指针是指指向指针的指针,最简单的多级指针是二级指针,而我们之前所涉及到的指针都是一级指针。如果在定义指针变量时,在变量名前冠以一个星号代表一级的话,二级指针变量在定义时就应该在变量名前加上连续的两个星号,三级指针变量则在变量名前加上三个连续的星号,以此类推。在此,我们只介绍二级指针的原理,三级或更高级别的指针原理相同。

       有如下代码片断:

       int i = 3;              // 定义一个int型变量i,并赋初始值

       int *p = &i;         // 定义一个一级指针变量,并赋值为i在内存中的地址

       int **pp = &p;    // 定义一个二级指针变量,并赋值为指针p在内存中的地址

       上述代码片断执行完毕后,各变量在内存中的布局如下:

      

       当对一级指针变量p进行取值运算时,*p所得到的值即为p所指向内存地址中的值,即3。而当对二级指针pp进行一次取值运算时,*pp所得到的值为pp所指向内存地址中的值,即是指针变量p的值;当再次对pp进行取值运算时,**pp的值为指针变量p所指向内存地址中的值,为3

3.3、指针应用举例

3.3.1、一维数组的动态分配与释放

       内存的动态分配与释放离不开指针的使用,可以借助指针实现一维数组的动态分配与释放。

       int *pa = 0;                // 定义一个指针,并置空

       // 动态分配可以保存3int数据的内存空间,并将地址赋予pa

       pa = new int[3];

       pa[0] = 1, pa[1] = 2, pa[2] = 3;       // 通过指针为元素赋值

       // 对元素做一些其它操作

       delete[] pa;   // 释放内存

3.3.2、二维数组的动态分配与释放

       二维及二维以上数组的动态分配比一维数组要显得复杂,主要在于要从低到高依次为每一维分配内存空间保存维的相应信息。

       // 以下代码片断分配23列的二维数组

       int **ppa = 0;     // 定义一个二级指针保存成功分配二维数组的地址

       // 分配第一维的大小:行

       ppa = new int*[2];

       // 为每一行分配列元素

       for (int i = 0; i < 2; i++)

               ppa[i] = new int[3];

       // 可以使用p[][]方式访问二维数组中的元素

       // 回收内存

        for (int i = 0; i < 2; i++)

              delete[] ppa[i];     // 释放每一行列元素所占内存

       delete[] ppa;               // 释放行所占内存

3.3.3、类对象的动态分配与释放

       当一个指针变量所指向的内存地址中保存的是一个类对象,则可以通过指针运算符->访问该对象的成员。

       class data {   // 定义一个简单类

        public:

       data(int d) : _d (d) {}         // 构造函数,初始化私有变量

       void display() const {        // 公有成员访法,输出对象信息

       std::cout << “value of data is: ” << _d << std::endl;

       }

        private:

              int _d;    // 类的私有成员

       };

       data *pd = new data(3);   // 动态创建一个simple类对象

pd->display();      // 用指针运算符访问对象的方法

delete pd;             // 回收动态创建的对象所占用的内存

3.3.4this指针

       this指针是一种只能在类的非静态成员函数中使用的特殊指针。this指针指向了调用成员函数的对象本身。

       3.3.3小节中data类的display成员函数可以重新定义如下:

       void display() const {

               std::cout << “value of data is: “ << this->_d << std::endl;

      }

      data d(3);     // 定义一个data类对象

      d.display();

上述代码片断中,d.display()将会调用display成员函数显示类对象的信息。此时display成员函数体中的this指针实际上指向了对象d在内存中的地址,因为display成员函数是通过对象d调用的。

this指针的另外一个重要用途就是使类的非静态成员函数返回类对象本身的引用,使得通过一个类对象连续调用成员函数成为可能。

3.3.3小节中data类添加一个新的公有成员函数add,代码片断如下:

       data& add() {

        _d += 1;

return *this;       // 返回调用该成员函数的类对象本身的引用

       }

       data d(3);

d.add().add().add();   // 对对象d连续多次调用add()成员函数

在上述代码片断中,d.add()的返回结果是d对象本身,因而可以接下来直接再次调用add成员函数,同时使该对象的_d成员的值增加1;在接下来的两次对add成员函数的调用都会使_d成员值增加1

3.3.5、利用指针实现多态

       多态(polymorphism)是面向对象程序设计语言的一个重要特性,C++中的虚拟函数(virtual function)是实现这一特性的重要支撑手段。但是利用虚拟函数实现多态只能通过指向对象的指针或者对象的引用两种方法。

       // 定义一个描述形状的抽象基类

class shape {

public:

       virtual void draw() = 0;

};

       // 定义一个派生的三角形类

class triangle {

public:

       triangle(const int (&a)[2], const int (&b)[2], const int (&c)[2]) {

              p1[0] = a[0], p1[1] = a[1];

              p2[0] = b[0], p2[1] = b[1];

              p3[0] = c[0], p3[1] = c[1];

}

       void draw() {

              std::cout << “to draw triangle” << std::endl;

                     // 根据三个顶点信息画三角形

       }

private:

       int p1[2], p2[2], p3[2];      // 三角形的三个顶点

};

       // 定义另外一个派生的圆类

class circle {

public:

       circle(const int (&a)[2], int r) {

              c[0] = a[0], c[1] = a[1], radius = r;

}

       void draw() {

              std::cout << “to draw circle” << std::endl;

                     // 根据圆心和半径画圆

}

private:

       int c[2];        // 圆心坐标

              int radius;    // 圆半径

};

shape *ps[2];

int a[2] = { 10, 10 }, b[2] = { 20, 20 }, c[2] = { 30, 30 };

int r = 50;

ps[0] = new triangle(a, b, c);   // 动态创建一个三角形对象

ps[1] = new circle(a, r);           // 动态创建一个圆对象

for (int i = 0; i < 2; i++)

       ps[i]->draw();                    // 利用指针实现多态,调用正确的绘制方法

delete ps[0];                              // 释放动态创建的三角形对象

delete ps[1];                              // 释放动态创建的圆对象

3.3.6、函数指针作为函数参数实现回调

       指针可以指向函数地址,可以将函数指针作为函数参数,则可以实现回调功能。以下代码片断演示了如何将一个函数指针作为比较的规则。

int compare(int a, int b)

{

       if (a == b) return 0;

       return (a > b ? 1 : -1);

}

int result(int a, int b, int (*pfun)(int, int))

{

       return (*pfun)(a, b);

}

int a = 3, b = 4;

int (*pf)(int, int) = &compare;

std::cout << result(a, b, pf) << std::endl;

       上述代码中,函数指针pf指向了函数compare的地址,并且作为函数result的参数来充当比较参数ab的规则,如果将pf指向另外的函数作为判断规则,就可以得到不同的结果。

3.3.7、指向类成员函数的指针实现回调

       指针除了可以指向全局函数地址之外,也可以指向类的成员函数。应用这一特性可以方便的实现另外一种风格的回调功能。

       假设已经定义好了3.3.3小节的data类,则以下代码片断:

       // 定义一个指向data类成员函数display

void (data::*pf)() const = &data::display;

data *pd = new data(9);

       (pd->*pf)();         // 通过指针调用成员函数

delete pd;

4、善用指针

       指针在C++语言中是非常重要,非常灵活的东西,同时在使用指针的问题上,它又是一把双刃剑。在对指针充分理解的基础上,指针使用得当,不但使程序设计优雅,灵活而且能够保证程序的执行效率;另一方面,如果对指针的概念一知半解,则在使用指针问题上应当非常谨慎,否则将会导致程序的执行带来意想不到的后果。

       本文是作者在对指针认识的基础上写的一篇介绍指针的文章,其中的措辞可能不甚严谨,属虚也难免有误,欢迎各位指正,谢谢!

 

email: blldw@163.com

原创粉丝点击