一、指针的声明和使用
1. 基础
基本语法如下:
int value = 1;
int * pValue = &value;
pValue存储的是value所在的地址,用解引用符*可以来访问指针指向的地址中所存储的值。所以*pValue和value是完全等价的。
2. 指针与指针所指的内容辨析
double * tax_ptr;
char * str;
由于已将tax_ptr声明为一个指向double的指针,因此编译器知道*tax_ptr是一个double类型的值。也就是说,它知道*tax_ptr是一个以浮点格式存储的值,这个值(在大多数系统上)占据8个字节。指针变量不仅仅是指针,而且是指向特定类型的指针。
但虽然tax_ptr和str指向两种长度不同的数据类型,但这两个变量本身的长度通常是相同的。一般来说,地址需要2个还是4个字节,取决于计算机系统(有些系统可能需要更大的地址,系统可以针对不同的类型使用不同长度的地址)。
必须声明指针所指向的类型的原因之一:地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。通过指定指针的类型,程序知道*tax_ptr是8个字节的double值,str是1个字节的char值。程序用cout打印它们的值时,知道要读取多少字节以及如何解释它们。
3. 指针的危险
在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。
为数据提供空间是一个独立的步骤。
long * fellow;
*fellow = 223323;
一定要在对指针应用解引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。
4. 指针和数字
有时候我们可能需要一些特殊的操作,访问一个确定的地址中的值,比如调试过程中,要看看某个地址中的值:
int *pt;
pt = 0xB8000000; // type mismatch
但是,指针不是整型,虽然计算机通常把地址当做整数来处理。在上面这两句代码中,pt是指向int的指针,因此可以把它赋给地址,但右边是一个整数。在C99标准发布之前,C语言允许这样的赋值。但C++在类型一致方面的要求更严格,编译器将显示一条错误消息,通告类型不匹配。要将数字值作为地址来使用,应通过强制类型转换将数字转为适当的地址类型:
int * pt;
pt = (int *) 0xB8000000; // types now match
5. 使用new来分配内存
指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在C预言中,可以用库函数malloc()来分配内存;在C++中仍然可以这样做,但C++还有更好的方法——new运算符。
6. 使用delete来释放内存
使用new和delete时,应遵守以下规则。
- 不要使用
delete来释放不是new分配的内存。 - 不要使用
delete释放同一个内存块两次。 - 如果使用
new[]为数组分配内存,则应使用delete[]来释放。 - 如果使用
new为一个实体分配内存,则应使用delete(没有方括号)来释放。 - 对空指针应用
delete是安全的。
另外,一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性。但稍后会看到,对于返回指针的函数,使用另一个指针确实有道理。
7. 使用new来创建动态数组
如果通过声明来创建数组,则在程序被编译时将它分配内存空间。在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。
但使用new时,如果在运行阶段需要数组,则创建它;如果不需要则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在程序运行时创建的。这种数组叫做动态数组(dynamic array)
二、指针与数组和指针算术
1. 指针和数组
指针和数组基本等价的原因在于指针算术(pointer arithmetic)和C++内部处理数组的方式。
首先,我们来看一看算术。将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。
- 将指向
double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;- 将指向
short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2.
另外,C++将数组名解释为地址。
使用数组表示法时,C++都执行下面的转换:
arrayname[i] becomes *(arrayname + i)
如果使用指针表示法,C++也执行同样的变换:
pointername[i] becomes *(pointername + i)
因此,在很多情况下,可以相同的方式使用指针名和数组名。区别之一是,可以修改指针的值,而数组名是常量:
pointername = pointername + 1; // valid
arrayname = arrayname + 1; // not allowed
另一个区别是,对数组应用sizeof运算符得到的是数组的长度,而对指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组。
double wages[3] = {10000.0, 20000.0, 30000.0};
double *pw = wages;
上述代码中,pw和wages指的是同一个数组,但对它们应用sizeof运算符得到的结果如下:
24 = size of wages array << displaying sizeof wages
4 = size of pw pointer << displaying sizeof pw
这种情况下,C++不会将数组名解释为地址。
对数组取地址时,数组名也不会被解释为其地址。数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。
short tell[10]; // tell an array of 20 bytes
cout << tell << endl; // display &tell[0]
cout << &tell << endl; // display address of whole array
从数字上说,这两个地址相同;但从概念上说,&tell[0](即tell)是一个2字节内存块的地址,而&tell是一个20字节内存块的地址。因此,表达式tell+1将地址值加2,而表达式&tell+1将地址加20.换句话说,tell是一个short指针(* short),而&tell是一个这样的指针,即指向包含10个元素的short数组(short(*)[10])
程序输入结果如下:
tell = 0x7ffc5ff53a80
&tell = 0x7ffc5ff53a80
tell + 1 = 0x7ffc5ff53a82
&tell + 1 = 0x7ffc5ff53a94 // 16进制表示 比&tell增加了14 即十进制的20
对于前面有关&tell的类型描述是如何来的呢?首先,我们可以这样声明和初始化这种指针:
short (*pas)[10] = &tell; // pas points to array of 20 shorts
首先,如果省略括号,优先级规则将使得pas先与[10]结合,导致pas是一个short指针数组,它包含10个元素,因此括号是必不可少的。
其次,如果要描述变量的类型,可将声明中的变量名删除。因此,pas的类型为short (*)[10]。
另外,由于pas被设置为&tell,因此*pas与tell等价,所以(*pas)[0]为tell数组的第一个元素。
总之,使用new来创建数组以及使用指针来访问不同的元素很简单。只要把指针当做数组名对待即可。然而,要理解为何可以这样做,将是一种挑战。要想真正了解数组和指针,应认真复习它们的相互关系。
注意区分 指针数组与数组指针 的区别
2. 指针和字符串
指针和数组的特殊关系可以扩展到C-风格字符串。请看下面代码:
char flower[10] = "rose";
cout << flower << "s are red\n";
数组名是第一个元素的地址,因此cout语句中的flower是包含字符r的char元素的地址。cout对象认为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。 总之,如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。
这里的关键不在于flower是数组名,而在于flower是一个char的地址。
为了与cout对字符串输出的处理保持一致,这个用括号引起来的字符串"s are red\n”也应当是一个地址。
这意味着,对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理方式是一样的,都将传递它们的地址。与逐个传递字符串中的所有字符相比,这样做的工作量确实要少。
在
cout和多数C++表达式中,char数组名、char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
三、 C++管理数据内存的方式
根据用于分配内存的方法,C++有3中管理数据内存的方式:自动存储、静态存储和动态存储(有时也叫做自由存储空间或堆)。(C++11新增了第四种类型——线程存储,将在内存模型章节进行讨论)。
-
自动存储
在函数内部定义的常规变量使用自动存储空间,被称为自动变量。自动变量通常存储在栈中,程序执行过程中,栈将不断地增大和缩小。
其实就是我们在函数中使用的局部变量使用自动存储。 -
静态存储
静态存储是整个程序执行期间都存在的存储方式。
使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字static:
static double fee = 56.50;
自动存储和静态存储的关键在于:这些方法严格地限制了变量的寿命。变量可能存在于程序的整个声明周期(静态变量),也可能只是在特定函数被执行时存在(自动变量)。
- 动态存储
new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在C++中被称为自由存储空间(free store)或堆(heap)。该内存池同用于静态变量和自动变量的内存是分开的。在一个函数中使用new分配内存,而在另一个函数中使用delete释放它。因此,数据的生命周期不完全受程序或函数的生存时间控制。与使用常规变量相比,使用new和delete让程序员对程序如何使用内存有更大的控制权。然而,内存管理也更复杂了。在栈中,自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难。
栈、堆和内存泄漏
如果使用new在自由存储空间(或堆)上创建变量后,没有调用delete。那么随着包含指向该内存的指针的内存由于作用域规则和对象生命周期的原因而被释放,自由存储空间上动态分配的内存依然会持续存在,且无法被访问,这将导致内存泄漏。使用C++智能指针能够有效避免内存泄漏。

本文深入讲解C++指针的基础知识,包括指针的声明、使用、与数组的关系及内存管理等内容。介绍了指针算术、指针与字符串的交互,并探讨了C++中自动存储、静态存储与动态存储的不同。

被折叠的 条评论
为什么被折叠?



