分类: C/C++
2010-05-15 15:35:34
指针作为程序设计语言中的一个重要概念,在教学和实践项目中都显得非常重要。在此作一个有关C++指针的专题介绍,旨在总结指针的概念、特点以及应用情况,以帮助广大C++初学者更好地使用C++语言解决他们在学习以及工作中遇到的困难。
当我们说编写一个具有什么功能的程序,或者运行某一个程序时,这里面所设计的到的程序是一个静态的概念,并不会产生任何结果;而当我们说一个正在运行的程序,或者某个程序占用了多少内存的时候,这样的程序则是一个动态的概念,即进程。进程将会占用系统资源以完成其预定义的功能并产生相应的结果。不过人们在实际工作中有时候并不是太过于区分程序和进程的概念,而往往以程序论之。
当一个程序要运行的时候,操作系统首先需要把该程序调入内存,这样就创建了一个进程,这个过程称为加载。加载所要做的工作很多,但是最基本的任务是把程序代码以机器可以理解的方式调入内存,并且程序运行所需要的数据也会调入进程的地址空间。为了提高对内存中数据的访问速度,操作系统都是对内存地址进行编号了的,就好比如果你去小区找一个人,如果你知道这个人住小区的确切地址,比如4栋3单元210,你就可以根据这个地址直接找到这个人。相应的,如果你知道了存储所需要数据的内存地址编号,就可以直接根据该编号访问内存中的数据了。这里所提的数据既可以是代表数学意义上的数值,也可以是程序设计意义上的程序代码(二进制代码),因为程序在运行时,其代码也是被加载进了内存的。
计算机系统中,内存的基本单位是字节。因此,内存编号也是以一个字节为一个基本单位的。内存编号的起止范围决定于操作系统的可寻址范围。例如,如果操作系统是16位的,即是说该操作系统支持16位的寻址空间,因此其地址范围从20~216-1。这就决定了对应于该16位操作系统的内存地址编号从0x0000-0xffff,见图1。
同样,如果操作系统是32位的,即该操作系统支持32位的寻址空间,其地址范围从20~232-1,决定了对应于该32位操作系统的内存地址编号从0x00000000-0xffffffff。
当得到某一个有效内存地址编号的时候,我们就可以操作以该编号为开始的内存数据了。为什么这里面要强调“有效”二字呢?操作系统为了保证自身的安全和稳定,以及一些其它因素,由用户所创建进程的运行地址空间范围是有限制的,访问内存数据所依赖的地址编号也是严格限制的,这样可以避免用户有意或无意地访问了操作系统核心数据,保证了操作系统的安全和稳定。
这样,当得到一个有效内存地址编号时,我们怎么确定内存中是何种格式的数据,以及如何读取或更改所需要格式的数据呢?一般来说,内存中的数据主要分为两类:数值型和字符串。
数值型的数据就是具有数学意义上的量,如1,2,1.234567,-2,-3e-4,等等;而字符串则表达了具有一定文法意义上的文本,如“你好!”,“hello, world!”等等。因此,在访问内存数据之前,首先需要确定访问的数据类型是什么。除此之外,还要确定要访问内存的大小,即从某一内存地址编号开始要访问多少个字节的内存。对于数值型的数据来讲,内存的大小决定了量的范围,而对于字符串型的数据来讲,内存的大小则决定了所表达的文本信息量。
例如,假设编号为0x80000000的内存地址是一个有效的可访问地址,从该地址开始的连续4个字节内存的二进制内容依次为01000001,01000010,01000011,00000000,如图2。
可以用下表概括从地址编号为0x80000000开始,访问的数据类型与内存大小所决定的访问结果。
表1. 数据类型与内存大小决定的访问结果
大小 类型 | 数值型 | 字符串型 |
1字节 | 65 | “A” |
2字节 | 16706 | “AB” |
3字节 | 4276803 | “ABC” |
4字节 | 1094861636 | “ABCD” |
由此可以看到,即使获得了一个有效的内存地址编号,我们也需要用数据类型和内存大小来约束通过该编号进行的内存数据访问,以得到确切的结果。
在C语言中,指针的概念就已经存在,并且凭借指针的强大,赋予了C语言编程灵活、可直接访问内存、执行效率高等一系列的优点,可以毫不夸张地说,指针就是C语言的灵魂。作为与C语言完全兼容的C++语言则完美继承了指针的概念,并依托面向对象程序设计语言的特点,扩展了原有C语言指针中的概念,使得指针的使用达到了一个新的高度。
但是,C++语言的初学者往往被指针弄得莫名其妙,即使当时能够明白指针的含义,然后看到某些源代码中的有关指针用法后,原本以为已正确理解指针的感觉荡然无存。指针这种被C++初学者认为神秘的东西是学习C++语言的一个障碍,而这个障碍可能使诸多C++学习者转向其它面向对象编程的语言了。
本节将通过三个小节的内容来阐述指针,希望对初学者有所帮助。
C++语言中的指针本质是什么?恐怕这个问题是理解和使用指针的一个基本前提。其实,指针并不神秘,因为指针实际上就是一个变量,即指针的本质是变量。
为什么这么说呢?我们在使用C++语言编程的时候,经常所说的指针实际上是通过变量来表达的,即指针变量。由于程序员口头之间在交流时,为了方便或者某些原因,常常把“变量”两个字省略,但是这并不阻碍交流的结果,因为程序员们对此心领神会。在变量前面加上“指针”二字构成“指针变量”,又在一定程度上表明了指针变量和其它类型的普通变量有一定区别,即指针变量具有自身的特殊性。
与普通变量相比较,指针变量具有以下几点特殊性,这也是C++初学者理解指针的难点所在,特别是后两条:
Ø 定义方法与普通变量不同
Ø 指针变量的值与普通变量所表达的含义不同
Ø 指针变量的运算方式与普通变量不同
int i; // 定义一个int类型的普通变量,变量名为i
int *i; // 定义一个int类型的指针变量,变量名为i
可见,一个星号(*)决定了一个变量在定义时是普通变量,还是指针变量。我们应该养成良好的代码编写风格,在定义指针变量时,为了强调该变量与普通变量之间的区别,经常在命名该指针变量时,将第一字符定为p,因此更好的定义指针变量的方式应该为:
int *pi; // 养成良好的定义指针变量的习惯
数值型普通变量的值表达的意义非常明确,就是一个数学意义上的量。这个量的大小,即是变量的值。但是指针变量的值所表达的意义却是计算机系统内存的地址编号,虽然该编号也是一个数学意义上的量,但它具有更深一层的意义。
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
在指针变量上的运算有以下几种:
ü 取值(*)
ü 加或自增(+,++)
ü 减或自减(-,--)
ü 访问指向对象的成员(->)
取值运算是指针变量的一个基本运算,因为指针变量的值代表了内存地址编号,而我们更关心的是位于地址编号开始的一段内存空间中的数据。
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,而是加上1个int类型数据在内存中占用的大小,即指针变量p的值为pi的值加上sizeof(int)。指针变量pi在进行完自增运算后,其值为原值加上sizeof(int)。利用这个性质,可以很容易地将指针变量与数组联系起来,通过指针变量的变化来访问数组元素。
int a[4] = { 1, 2, 3, 4 }; // 定义并初始化一个具有4个int类型元素的数组
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;
};
// ps是sample类型的指针变量,且指向有效对象
ps->display(); // 显示ps所指向对象的信息
本小节介绍两组与指针有关的易混淆的概念。
用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