[ 摘要]
指针是 C和 C++语言编程中最重要的概念之一,也是最容易产生困惑并导致程序出错的问题之一。利用指针编程可以表示各种数据结构 , 通过指针可使用主调函数和被调函数之间共享变量或数据结构,便于实现双向数据通讯;并能像汇编语言一样处理内存地址,从而编出精练而高效的程序。指针极大地丰富了C和 C++语言的功能。
在本文中,主要分两部分对指针进行讨论。首先,基础篇讨论关于指针的内容和运算操作等,可以是读者对指针的知识有一定了解和认识;随后在使用篇中重点讨论指针的各种应用,揭破指针在日常编程中的精髓,从而使读者能够真正地了解、认识和使用指针。
[ 关键字 ] C C++ 指针 引用 数组 结构体 类
第一篇 : 基础篇
1.1 指针的概念
谈到指针 ,它的灵活性和难控制性让许多程序员谈虎色变 ;但它的直接操作内存,在数据
操作方面有着速度快,节约内存等优点,又使许多 C++程序员的深爱不以 .那么指针究竟是怎么样一个概念呢 ?
其实 , 指针就是一类变量,是一类包含了其他变量或函数的地址的变量。与其他变量所不同的是,一般的变量包含的是实际的真实的数据 ,而指针是一个指示器,它告诉程序在内存的哪块区域可以找到数据。
好了 ,在这里我们可以这样定义指针 :指针是一类包含了其他变量或函数的地址的变量 ,它里面存储的数值被解释成为内存的地址 .
1.2 指针的内容
简单讲 ,指针有四个方面的内容 :即指针的类型 ,指针所指向的类型 ,指针的值 ,指针本身所
占有的内存区 .下面我们将分别阐述这些内容 .
1.2.1 指针的类型
从语法的角度看,指针的类型是指把指针声明语句中的指针名字去掉所剩下的部分。这是指针本身所具有的类型。例如:
int*ip; // 指针的类型是 int*
char*ip; // 指针的类型是 char*
int**ip; // 指针的类型是 int**
int(*ip)[5]; // 指针的类型是 int(*)[5]
1.2.2 指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么类型来看待。从语法的角度看,指针所指向的类型是指针声明语句中的指针名字和名字左边的指针声明符 *去掉所剩下的部分。例如:
int*ip; // 指针所指向的类型是 int
char*ip; // 指针所指向的类型是 char
int**ip; // 指针所指向的类型是 int*
int(*ip)[5]; // 指针所指向的类型是 int()[5]
1.2.3 指针的值(或称指针所指向的内存区)
指针的值或者叫指针所指向的内存区或地址,是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在 32位程序里,所有类型的指针的值都是一个 32位整数,因为 32位程序里内存地址全都是 32位长。 指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为 sizeof(指针所指向的类型 )的一片内存区。以后,我们说一个指针的值是 XX,就相当于说该指针指向了以 XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在上例中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?
1.2.4 指针本身所占有的内存区
指针本身所占有的内存区是指针本身占内存的大小,这个你只要用函数 sizeof(指针的
类型 )测一下就知道了。在 32位平台里,指针本身占据了 4个字节的长度。
指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。
1.3 指针与内存管理
利用指针你可以将数据写入内存中的任意位置,但是,一旦你的程序中有一个野指针 ("wild” pointer),即指向一个错误位置的指针,你的数据就危险了—存放在堆中的数据可能会被破坏,用来管理堆的数据结构也可能会被破坏,甚至操作系统的数据也可能会被修改,有时,上述三种破坏情况会同时发生。所以合理的正确的分配指针的地址是非常重要的。
1.3.1 内存分配的方式
内存分配方式有三种:
( 1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量, static变量。
( 2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
( 3) 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc或 new申请任意多少的内存,程序员自己负责在何时用 free或 delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多,以下我们重点讲解动态内存分配。
1.3.2 malloc/free 的使用要点
malloc与 free是 C/C++语言的标准库函数,它用于申请动态内存和释放内存。
函数 malloc的原型如下:
void * malloc(size_t size);
用 malloc申请一块长度为 length的整数类型的内存,程序如下:
int *ip = (int *) malloc(sizeof(int) * length);
我们应当把注意力集中在两个要素上:“类型转换”和“ sizeof”。
malloc函数返回值的类型是 void *,所以在调用 malloc时要显式地进行类型转换,将 void * 转换成所需要的指针类型。
malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。例如 int变量在 16位系统下是 2个字节,在 32位下是 4个字节;而 float变量在 16位系统下是 4个字节,在 32位下也是 4个字节。这个你可以用 sizeof(类型 )去测试。
在 malloc的“ ()”中使用 sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 ip = malloc(sizeof(ip))这样的程序来。
函数 free的原型如下:
void free( void * memblock );
为什么 free函数不象 malloc函数那样复杂呢?这是因为指针 p的类型以及它所指的内存的容量事先都是知道的,语句 free(p)能正确地释放内存。如果 p是 NULL指针,那么 free对 p无论操作多少次都不会出问题。如果 p不是 NULL指针,那么 free对 p连续操作两次就会导致程序运行错误。
1.3.3 new/delete 的使用要点
对于非内部数据类型的对象而言,光用 maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。
因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个能完成清理与释放内存工作的运算符 delete。注意 new/delete不是库函数 ,只是 C++的运算符。我们来看如下例子就知道怎么回事了。
class Object
{
public :
Object(void){std::cout << “Initialization”<< std::endl; }
~Object(void){std::cout << “Destroy”<< std::endl; }
void Initialize(void){std:: cout << “Initialization”<< std::endl; }
void Destroy(void){ std::cout << “Destroy”<< std::endl; }
}
void UseMallocFree(void)
{
Object *ip = (Object *)malloc(sizeof(Object)); // 申请动态内存
ip->Initialize(); // 初始化
// …
ip->Destroy(); // 清除工作
free(ip); // 释放内存
}
void UseNewDelete(void)
{
Object *ip = new Object; // 申请动态内存并且初始化
// …
Delete ip; // 清除并且释放内存
}
用 malloc/free和 new/delete如何实现对象的动态内存管理
类 Object的函数 Initialize模拟了构造函数的功能,函数 Destroy模拟了析构函数的功能。函数 UseMallocFree中,由于 malloc/free不能执行构造函数与析构函数,必须调用成员函数 Initialize和 Destroy来完成初始化与清除工作。函数 UseNewDelete则简单得多。
所以我们不要企图用 malloc/free来完成动态对象的内存管理,应该用 new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言 malloc/free和 new/delete是等价的。 new内置了 sizeof、类型转换和类型安全检查功能 , ,对于非内部数据类型的对象而言, new在创建动态对象的同时完成了初始化工作。
new/delete 常使用的方法如下:
typeof *ip = new typeof[length];
类 / 结构 *ip = new 类结构;
一般释放如下: delete ip;
数组的释放如下: delete [] ip;
1.3.4 内存耗尽怎么办?
如果在申请动态内存时找不到足够大的内存块, malloc和 new将返回 NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
( 1)判断指针是否为 NULL,如果是则马上用 return语句终止本函数。例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
return;
}
…
}
( 2)判断指针是否为 NULL,如果是则马上用 exit(1)终止整个程序的运行。例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
std::cout << “ Memory Exhausted ” << std::endl;
exit(1);
}
…
}
( 3)为 new和 malloc设置异常处理函数。例如 Visual C++可以用 _set_new_hander函数为 new设置用户自己定义的异常处理函数,也可以让 malloc享用与 new相同的异常处理函数。详细内容请参考 C++使用手册。
有一个很重要的现象要告诉大家。对于 32位以上的应用程序而言,无论怎样使用 malloc与 new,几乎不可能导致“内存耗尽”。因为 32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。
1.3. 5 杜绝“野指针”
“野指针”不是 NULL指针,是指向“垃圾”内存的指针。人们一般不会错用 NULL指针,因为用 if语句很容易判断。但是“野指针”是很危险的, if语句对它不起作用。 “野指针”的原因主要有如下几种:
( 1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为 NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。例如
char *ip = NULL;
char *ip = new char;
( 2)指针 ip被 free或者 delete之后,没有置为 NULL,让人误以为 ip是个合法的指针。
( 3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:
class A
{
public:
void Func(void){ std::cout << “ Func of class A ” << std::endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生命期
}
p->Func(); // p 是“野指针”
}
函数 Test在执行语句 p->Func()时,对象 a已经消失,而 p是指向 a的,所以 p就成了“野指针”。但奇怪的是有些编译器运行这个程序时居然没有出错,这可能与编译器有关。
1.3.6 指针参数是如何传递内存的?
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。见如下例子:
void GetMemory(char *ip, int num)
{
ip = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
}
试图用指针参数申请动态内存
毛病出在函数 GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数 ip的副本是 _ip,编译器使 _ip = ip。如果函数体内的程序修改了 _ip的内容,就导致参数 ip的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中, _ip申请了新的内存,只是把 _ip所指的内存地址改变了,但是 ip丝毫未变。所以函数 GetMemory并不能输出任何东西。事实上,每执行一次 GetMemory就会泄露一块内存,因为没有用 free释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见如下示例:
void GetMemory(char **p, int num)
{
*ip = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100); // 注意参数是 &str ,而不是 str
strcpy(str, "hello");
std::cout<< str << std::endl;
free(str);
}
用指向指针的指针申请动态内存
当然,我们也可以用函数返回值来传递动态内存。这种方法更加简单,见如下示例:
char *GetMemory(int num)
{
char *ip = (char *)malloc(sizeof(char) * num);
return ip;
}
void Test(void)
{
char *str = NULL;
str = GetMemory(100);
strcpy(str, "hello");
std::cout<< str << std::endl;
free(str);
}
用函数返回值来传递动态内存
用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把 return语句用错了。这里强调不要用 return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见如下示例:
char *GetString(void)
{
char p[] = "hello world";
return p; // 编译器将提出警告
}
void Test(void)
{
char *str = NULL;
str = GetString(); // str 的内容是垃圾
std::cout<< str << std::endl;
}
return语句返回指向“栈内存”的指针
最后,根据以上阐述,我们总结如下使用规则供大家参考:
【规则 1】用 malloc或 new申请内存之后,应该立即检查指针值是否为 NULL。防止使用指针值为 NULL的内存。
【规则 2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则 3】避免数组或指针的下标越界,特别要当心发生“多 1”或者“少 1”操作。
【规则 4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则 5】用 free或 delete释放了内存之后,立即将指针设置为 NULL,防止产生“野指针”。
1.4 指针的运算
1.4.1 赋值运算
指针变量的赋值运算有以下几种形式:
1.4.1 .1 指针变量初始化赋值如下:
int a;
int *ip=&a;
1.4.1 .2 把一个变量的地址赋予指向相同数据类型的指针变量。例如:
int a;
int *ip;
ip=&a; // 把整型变量 a 的地址赋予整型指针变量 ip
1.4.1 .3 把一个指针变量的值赋予指向相同类型变量的另一个指针变量。例如:
int a;
int *pa=&a;
int *pb;
pb=pa; // 把 a 的地址赋予指针变量 pb
由于 pa,pb均为指向整型变量的指针变量,因此可以相互赋值。
1.4.1 .4 把数组的首地址赋予指向数组的指针变量。例如:
int a[5],*pa;
pa=a; // 数组名表示数组的首地址,故可赋予指向数组的指针变量 pa
也可写为:
pa=&a[0]; // 数组第一个元素的地址也是整个数组的首地址也可赋予 pa
当然也可采取初始化赋值的方法:
int a[5],*pa=a;
以上是一些基本的数组赋值方法,后面我们会详细讨论指针在数组中的使用。
1.4.1 .5 把字符串的首地址赋予指向字符类型的指针变量。例如:
char *pc;
pc="c language";
或用初始化赋值的方法写为:
char *pc=" c language ";
这里应说明的是并不是把整个字符串装入指针变量, 而是把存放该字符串的字符数组的首地址装入指针变量。
1.4.1 .6 把函数的入口地址赋予指向函数的指针变量。例如:
int (*pf)();
pf=f; //f 为函数名
1.4.2 加减运算
对于指向数组的指针变量,可以加上或减去一个整数 n。设 ip是指向数组 a的指针变量,则 ip+n,ip-n,ip++,++ip,ip--,--ip 运算都是合法的。指针变量加或减一个整数 n的意义是把指针指向的当前位置 (指向某数组元素 )向前或向后移动 n个位置。应该注意,数组指针变量向前或向后移动一个位置和地址加 1或减 1 在概念上是不同的。因为数组可以有不同的类型, 各种类型的数组元素所占的字节长度是不同的。如指针变量加 1,即向后移动 1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加 1。看如下例子:
char a[20];
int*ip=a;
...
ip++;
在上例中,指针 ip的类型是 int*,它指向的类型是 int,它被初始化为指向整形变量 a。接下来的第 3句中,指针 ip被加了 1,编译器是这样处理的:它把指针 ip的值加上了 sizeof(int),在 32位程序中,是被加上了 4。由于地址是用字节做单位的,故 ip所指向的地址由原来的变量 a的地址向高地址方向增加了 4个字节。
由于 char类型的长度是一个字节,所以,原来 ptr是指向数组 a的第 0号单元开始的四个字节,此时指向了数组 a中从第 4号单元开始的四个字节。再看如下例子:
char a[20];
int*ip=a;
...
ip+=5;
在这个例子中, ip被加上了 5,编译器是这样处理的:将指针 ip的值加上 5乘 sizeof(int),在 32位程序中就是加上了 5乘 4=20。由于地址的单位是字节,故现在的 ip所指向的地址比起加 5后的 ip所指向的地址来说,向高地址方向移动了 20个字节。在这个例子中,没加 5前的 ip指向数组 a的第 0号单元开始的四个字节,加 5后, ptr已经指向了数组 a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。
如果上例中, ip是被减去 5,那么处理过程大同小异,只不过 ip的值是被减去 5乘 sizeof(int),新的 ip指向的地址将比原来的 ip所指向的地址向低地址方向移动了 20个字节。
总结一下,一个指针 ipold加上一个整数 n后,结果是一个新的指针 ipnew, ipnew的类型和 ipold的类型相同, ipnew所指向的类型和 ipold所指向的类型也相同。 ipnew的值将比 ipold的值增加了 n乘 sizeof(ipold所指向的类型 )个字节。就是说, ipnew所指向的内存区将比 ipold所指向的内存区向高地址方向移动了 n乘 sizeof(ipold所指向的类型 )个字节。
一个指针 ipold减去一个整数 n后,结果是一个新的指针 ipnew, ipnew的类型和 ipold的类型相同, ipnew所指向的类型和 ipold所指向的类型也相同。 ipnew的值将比 ipold的值减少了 n乘 sizeof(ipold所指向的类型 )个字节,就是说, ipnew所指向的内存区将比 ipold所指向的内存区向低地址方向移动了 n乘 sizeof(ipold所指向的类型 )个字节。
1.4.3 关系运算
指向同一个数组中的不同元素的两个指针可以进行各种关系运算。例如:
ip1==ip2表示 ip1和 ip2指向同一数组元素
ip1>ip2表示 ip1处于高地址位置
ip1<ip2表示 ip2处于低地址位置
指针变量还可以与 0比较。设 ip为指针变量,则 ip==0表明 ip是空指针,它不指向任何变量; ip!=0表示 ip不是空指针。空指针是由对指针变量赋予 0值而得到的。例如:
#define NULL 0
int *ip=NULL;
对指针变量赋 0值和不赋值是不同的。指针变量未赋值时,可以是任意值,是不能使用的。否则将造成意外错误。而指针变量赋 0值后,则可以使用,只是它不指向具体的变量而已。
1.4.4 取地址运算符‘ & ’和取内容运算符‘ * ’
取地址运算符 &是单目运算符,其结合性为自右至左,其功能是取变量的地址。
取内容运算符 *是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在 *运算符之后跟的变量必须是指针变量。需要注意的是指针运算符 *和指针变量说明中的指针说明符 * 不是一回事。在指针变量说明中,‘ *’是类型说明符,表示其后的变量是指针类型。而表达式中出现的‘ *’则是一个运算符用以表示指针变量所指的变量。如下例子:
int a=12;
int b;
int *p;
int **ptr;
p=&a; //&a 的结果是一个指针,类型是 int* ,指向的类型是 int ,指向的地址是 a 的
// 地址。
*p=24; //*p 的结果,在这里它的类型是 int ,它所占用的地址是 p 所指向的地址。
ptr=&p; //&p 的结果是个指针,该指针的类型是 p 的类型加个 * ,在这里是 int ** 。该
// 指针所指向的类型是 p 的类型,这里是 int* 。该指针所指向的地址就是指针
//p 自己的地址。
*ptr=&b;//*ptr 是个指针, &b 的结果也是个指针,且这两个指针的类型和所指向的类型 // 是一样的,所以用 &b 来给 *ptr 赋值就是毫无问题的了。
**ptr=34;//*ptr 的结果是 ptr 所指向的东西,在这里是一个指针,对这个指针再做一次 *
// 运算,结果就是一个 int 类型的变量。
1.4.5 关于括号组合
在解释组合说明符时, 标识符右边的方括号和圆括号优先于标识符左边的“*”号,而方括号和圆括号以相同的优先级从左到右结合。但可以用圆括号改变约定的结合顺序。
阅读组合说明符的规则是“从里向外”。从标识符开始,先看它右边有无方括号或园括号,如有则先作出解释,再看左边有无*号。 如果在任何时候遇到了闭括号,则在继续之前必须用相同的规则处理括号内的内容。
1.5 指针表达式
一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表式。所以指针表达式也具有指针所具有的四个要素 :指针的类型 ,指针所指向的类型 ,指针指向的内存区 ,指针自身占据的内存。(预知后事如何,且听下回分解!!!)
作者简介:
马伟,现任大方软件开发工程师,在 C家族( C, C++, C# 呵呵,因为都有个 C,所以我习惯称为 C家族)方面有一定的见解,希望和广大技术朋友共同交流。
E-MAIL: madengwei-88163@163.com QQ:328941810 MSN:madengwei@hotmail.com
网站和个人主页: www.itspzo.com|
最后版权声明一下:本文章归本作者所有,任何个人和公司不得随意把本文章的部分或者全部用于商业目的,如有转载或者其他目的请联系作者。呵呵!!!