C++的动态内存:C++的指针

在C++里,指针(pointer)变量被用来存储内存地址。C++要求使用特定的类型来定义指针。这个类型被用来指示需要如何去解释内存地址里的数据。我们已经知道,在计算机的内部,内存存储的是1和0,而C++的变量类型将会告诉编译器它所生成的代码应该如何去解释这些比特数据。同时,由于指针变量被用来存储内存地址,因此所有的指针变量都会需要相同的内存空间(32位系统上的4字节)。这就有点像是Python里的引用了。C++的指针是一个类似于Python的引用的概念。它们的区别在于:使用C++的指针,你可以访问指针指向的地址以及数据(也就是这个内存地址处的数据);而Python的引用则只能让你去访问引用所指向的数据。

C++的指针使用星号(*)作为变量名的前缀来进行声明。加上星号将代表着这个变量将会保存存储指定类型的数据值的内存地址。当你想在一个定义语句里声明多个指针的时候,一个常见的错误是忘记在每个变量名之前都加上星号。在下面的这个例子里,b和c将会被声明为指向int类型的指针,而d则会被声明为int类型。这个代码片段里的第二行也是合法的,但是我们并不建议你使用这种风格。在单词int之后跟着放置一个星号,会使得这条语句看起来像是它里面的所有变量都是指向int类型的指针,但是,实际上只有e是指针,而f是一个int类型。因为int类型和指针类型都需要4字节,因此这个例子一共会分配20字节。

int *b, *c, d; // b and c are pointers to an int, d is an int
int* e, f; // only e is a pointer to an int, f is an int

你应该思考的下一个问题是我们应该怎么在指针变量里存储地址。我们并不知道我们的程序被允许使用哪些内存地址,因此我们必须要去请求一个有效的地址。一种方法是使用现有变量的地址。下面的例子里展示了这一方法,同时这个例子还向我们展示了如何去访问指针变量所指向的数据:

// p1.cpp
#include <iostream>using namespace std;int main(){  int *b, *c, x, y;  x = 3;
  y = 5;
  b = &x;  c = &y;  *b = 4;
  *c = *b + *c;  cout << x << " " << y << " " << *b << " " << *c << " ";
  c = b;  *c = 2;
  cout << x << " " << y << " " << *b << " " << *c << endl;
  return 0;
}

一元符号&运算符会计算它所对应的操作数的地址。因此,语句b = &x将会让程序把x的内存地址存储到变量b的内存里。表10.7所示为计算机使用了从1 000到1 015的内存地址来存储我们的变量,而且,还显示了在执行了c = &y语句之后的每个变量的值。与之前类似,计算机并不一定会使用从1 000开始的地址,但在这本书里的示例里,我们会使用这个地址。

表10.7 C++在执行语句c = &y之后的内存情况

C++的动态内存:C++的指针

一元符号*运算符被用来当作指针的解引用(dereference)。解引用一个指针是指访问这个指针所储存的内存地址处的数据。语句*b = 4会让程序把数据值4存储在内存地址1 008里(因为b当前的值是1 008)。在阅读下一段之前,看看你能不能根据这些知识来确定那个示例程序的输出。

语句*c = *b + *c将会得到内存地址1 008(b指向的地址)和内存地址1 012(c指向的地址)的整数值,并且将4和5加在一起。之后,会把结果9存储在内存地址1 012(c指向的地址)处。语句c = b则会把b的数据值(地址1 008)复制到c的内存里(也就是说,1 008会被存储在内存地址1 004里)。你需要注意的是,对指针变量进行赋值和在Python里对两个名称进行赋值基本上是一样的;b和c现在都会指向相同的数据。通过这部分段落里的知识,你应该能够知道,这个程序的输出会是4 9 4 9 2 9 2 2。在执行*c = 2语句之后,内存的变化情况如表10.8所示。

表10.8 C++在执行语句*c = 2之后的内存情况

C++的动态内存:C++的指针

扫描二维码关注公众号,回复: 11767338 查看本文章

需要理解的另一个你可能已经发现了的重要概念是:指向int的指针和int类型并不是同一个类型。继续使用之前那个例子的变量声明的话,语句b = x和x = b都是不合法的。因为变量b是指针,所以必须给它赋地址的值;而x是一个int类型,因此只能给它赋整数值。如果我们使用另一种类型(比如double)来声明指针变量的话,这个现象会更明显,因为在32位系统里,它们用了不同数量的内存。不论对于哪一种类型,指向这个类型的指针和实际的类型都不是兼容的数据类型。

我们现在将编写一个更具有实际意义的例子来展示取地址运算符和解引用运算符。C语言不像C++语言那样支持按引用传递,因此在使用C语言的时候,修改实参的唯一方法就是使用指针。相应的技术方法是传递实参的地址,然后让函数或方法解引用这个指针,从而能够修改与形参对应的内存地址处的值。你也可以在C++里执行这样的操作,但程序员们通常会使用按引用传递来完成这项操作。下面这个例子展示了交换两个整数变量的交换(swap)函数:

// swap.cpp
#include <iostream>
using namespace std;void swap(int *b, int *c)
{  int temp = *b;
  *b = *c;
  *c = temp;
}int main(){  int x = 3, y = 5;  swap(&x, &y);  cout << x << " " << y << endl;
  return 0;
}

形参b和c分别传递了变量x和y的内存地址。因此,赋值语句*b = *c相当于在main函数里直接写x = y。你可以看到这两者之间的相似性以及通过引用进行的传递。如果我们将代码行b = &temp添加到swap函数的末尾会发生什么呢?它会改变x吗?事实上,这个语句将不会对x有任何影响。变量b将会被修改为保存temp变量的地址,但是这并不会修改x变量或者是与x变量所对应的内存地址的值。

在我们的这些例子里,我们使用了一元符号&运算符来给指针变量赋一个有效地址的值。另一种可以把指针设置为指向一个有效地址的方法是new语句。C++的new语句被用来从堆里分配一块动态内存,并且返回这个分配的内存的起始地址。使用new语句的时候,必须要指明想要分配的对象的数据类型。这是因为指定的数据类型将会被用来确定需要分配的内存大小。在C++里显式地分配了内存之后,在不再需要这部分内存的时候,就必须要去释放内存。delete语句将被用来释放这块动态分配的内存。下面这个例子展示了在10.1节里编写的Python和C++程序的显式堆动态版本:

// p2.cpp
#include <iostream>using namespace std;int main(){  int *x, *y, *z;  x = new int;  *x = 3;
  y = new int;  *y = 4;
  z = x;  x = y;  cout << *x << " " << *y << " " << *z << endl;
  delete z;  delete y;  return 0;
}

指针变量x、y和z都是堆栈动态变量,它们所需要的12字节在函数一开始的时候就会自动分配,之后在函数结束的时候会自动释放掉。我们使用了1 000到1 011的内存地址来表示这12字节,如表10.9所示。在new语句被执行的时候,会在内存地址从2 000开始的动态内存堆里分配内存。要注意的是,在这段代码里,有两个new语句,因此我们必须要有两个与此对应的delete语句。虽然我们没有在使用new和delete语句的时候使用一一对应相同的变量名,但是由x = new int语句分配的内存将会被delete z语句释放掉,这是因为z保存了这个new语句分配的内存地址。类似地,delete y语句会释放y = new int语句分配的内存。这个时候,我们其实还可以使用delete x语句代替delete y语句来释放内存,这是因为x = y语句会让x和y储存相同的内存地址。在这里需要记住的关键点是,每个被执行了的new语句,后面都必须要有一个与此对应的delete语句,这个语句会被用来释放new语句所分配的内存。如果你忘记了这个delete语句的话,程序将会出现内存泄漏(memory leak)。虽然有内存泄漏的程序可能不会崩溃,但是这样的代码通常也不会被认为是正确的。

表10.9 new语句在被执行之后的内存情况

C++的动态内存:C++的指针

通常来说,你会像我们在10.1节里那样来编写这个程序,因为那样做的话效率更高。而且,这个使用了指针的代码版本需要更多的内存,并且在解引用指针的时候会需要计算机去访问两个内存地址(cout << *x需要先访问内存地址1 000,然后再去访问内存地址2 004)。这段C++版本的代码在内存的分配方式上和Python版本是类似的。因此,你可以把这个版本代码的内存指示表格和图10.1里的内存图进行比较。同时,这个例子也展示了Python的引用和C++的指针在本质上是相同概念的不同语法。

由于Python只会使用引用,因此它并不需要像C++里的指针那样,有解引用这样一个额外的语法。在C++里把一个指针变量赋给另一个指针变量之后,它们就都会指向同一个值或者是对象。通过使用指针,我们就可以在C++里用像在Python里一样的分配内存的方式来实现我们在这一章开头的那个Rational类的例子了。

但是,使用指针的话,访问类的实例成员时会有一个问题:点运算符(句点)的优先级会高于用于解引用的星号(一元的*符号)运算符。这也就是说,如果我们有一个Rational类的实例r的话,我们就不能像*r1.set(2,3);这样来写代码了,我们应该写成(*r1).set(2,3)这样的形式。好在,C++还提供了一个额外的运算符,让我们可以使用指针并且不用添加括号去访问成员。这个符号是->(减号后跟一个大于符号),所以(*r1).set(2,3)就可以写成r1-> set(2,3)。通常来说,我们会使用->这样的形式来写代码,而不会用有括号的版本。

与这一章前面的Python版本的Rational类相对应的使用C++的指针的代码就像下面这样:

Rational *r1, *r2; // constructors not called
r1 = new Rational; // constructor is called
r1->set(2, 3);
r2 = r1;r1->set(1, 3);
cout << *r1 << endl;cout << *r2 << endl;delete r1;

因为r1和r2是指向了相同内存地址的指针,这个示例程序的结果是会把r1和r2都输出为1/3。这个代码片段的内存情况如表10.10所示。

表10.10 执行使用C++的指针的C++代码之后的内存情况

C++的动态内存:C++的指针

在声明r1和r2的时候,会为每个变量分配4字节,这是因为指针需要4字节。而且,在声明指针的时候,是不会去调用Rational类的构造函数的,这是因为我们只是在创建指针,而并没有去创建一个Rational对象。因此,r1 = new Rational语句将会导致8字节被分配,因为这个实例有两个整数型的实例变量num_和den_,而它们总共需要8字节。r1 = new Rational语句还会让变量r1存储在内存地址2000这个地方。在r1 = new Rational语句被执行的同时,因为它创建了一个Rational对象,所以构造函数也会被调用。接下来,r1->set(2,3)语句将会让2存储在内存地址2000处,相应地3会存储在内存地址2004处。

之后的r2 = r1这个语句,由于r1的值是2000,将会让2000存储在内存地址1004上。因此,现在就和Python例子具有相同的内存结构了,也就是r1和r2都指向了相同的Rational对象。而后,当我们执行r1-> set(1,2)语句时,并不会更改r1里的值,而是会去修改被存储在r1所指向的那个内存地址里的对象。由于r2指向的是与r1相同的对象,因此我们可以得到和Python一样的结果。当包含这样的C++代码片段的函数执行结束的时候,声明的变量的内存地址(1000~1007)会像之前提到过的那样被自动释放,但是我们还需要delete r1语句来释放使用new Rational语句显式分配的内存地址(2000~2007)。因为r1和r2两个指针都指向了相同的地址,我们也可以使用delete r2来释放内存,但我们不能写成delete r1; delete r2这样,这是因为对于每个new语句来说,都必须有且只有一个相应的delete语句。尝试在相同的内存地址再次去释放内存可能会破坏掉这个动态内存堆,从而导致程序崩溃。

在C++里,使用带有动态内存的指针可以为你提供像Python的引用那样的灵活性,但是,由于你需要负责显式地处理内存的分配和释放,因此要比完成相同功能的Python版本的代码更难。如果在使用动态内存的时候粗心大意的话,那么程序的每次运行都会产生不同的结果,甚至可能会整个都崩溃掉。我们将在这一章里讨论显式堆动态内存的这些问题。

本文摘自:《数据结构和算法(Python和C++语言描述)》

C++的动态内存:C++的指针

本书使用Python和C++两种编程语言来介绍数据结构。全书内容共15章。书中首先介绍了抽象与分析、数据的抽象等数据结构的基本原理和知识,然后结合Python的特点介绍了容器类、链式结构和迭代器、堆栈和队列、递归、树;随后,简单介绍了C++语言的知识,并进一步讲解了C++类、C++的动态内存、C++的链式结构、C++模板、堆、平衡树和散列表、图等内容;最后对算法技术进行了总结。每章最后给出了一些练习题和编程练习,帮助读者复习巩固所学的知识。

本书适合作为高等院校计算机相关专业数据结构课程的教材和参考书,也适合对数据结构感兴趣的读者学习参考。

猜你喜欢

转载自blog.csdn.net/epubit17/article/details/108592646