C++ Primer Plus第6版第4章(2)

指针和自由存储空间

在第 3 章的开头, 提到了计算机程序在存储数据时必须跟踪的 3 种基本属性。 为了方便, 这里再次列出了这些属性:

  • 存储的信息是什么类型:信息的类型决定了可以对信息的操作
  • 信息存储在何处: 实现信息的实际存在
  • 存储的值为多少: 这是个变化的值, 可能没初始化, 可能需要求值,等等

两种定义数据变量的方式:

  • 一种策略是声明变量. 声明语句指出了值的类型和符号名, 还让程序为值分配内存, 并在内部跟踪该内存单元。使用常规变量时, 值是指定的量, 而地址为派生量。
  • 另一种策略以指针为基础, 指针是一个变量, 其存储的是值的地址, 而不是值本身。指针策略刚好相反, 将地址视为指定的量, 而将值视为派生量。 一种特殊类型的变量— 指针用于存储值的地址。 因此, 指针名表示的是地址。 因此, 指针名表示的是地址。* 运算符被称为间接值( indirect velue) 或解除引用( dereferencing) 运算符, 将其应用于指针, 可以得到该地址处存储的值( 这和乘法使用的符号相同; C + + 根据上下文来确定所指的是乘法还是解除引用)。

两种方式通过地址运算实现联通:

  • 对于常规变量 , 只需对变量应用地址运算符(&), 就可以获得它的位置; 例如, 如果 home 是一个变量, 则& home 是它的地址。
  • 对于指针变量, * 解除引用运算, 就可以获得它的值.
  • 指针变量和常规变量在源码中的字面表示上不同, 背后的实质是没有差别的, 要么是地址要么是值.
#include <iostream>
#include <string>
using  namespace std;

int main()
{
    double d;  // 声明了一个double常规变量, d 指代值, &d指代值对应的指针
    double *pd; // 声明了一个double指针变量, *pd 指代值,pd 指代值对应的指针

    // 初始化
    d = 10.0;
    pd = &d;

    cout << "d 的地址:" << &d << " d的值:" << d << endl;


    cout << "pd 的地址:" << pd << " pd的值:" << *pd << endl;

    // 通过指针修改值
    *pd = 20;
    cout << "d 的地址:" << &d << " d的值:" << d << endl;
    cout << "pd 的地址:" << pd << " pd的值:" << *pd << endl;

    d 的地址: 0x61fe10 d的值: 10
    // pd 的地址:0x61fe10 pd的值:10
    // d 的地址:0x61fe10 d的值:20
    // pd 的地址:0x61fe10 pd的值:20
}

指针与 C + + 基本原理

面向对象编程与传统的过程性编程的区别在于, OOP 强调的是在运行阶段( 而不是编译阶段) 进行决策。

运行阶段指的是程序正在运行时, 编译阶段指的是编译器将程序组合起来时。 运行阶段决策就好比度假时, 选择参观哪些景点取决于天气和当时的心情; 而编译阶段决策更像不管在什么条件下, 都坚持预先设定的日程安排。

运行阶段决策提供了灵活性, 可以根据当时的情况进行调整。 例如, 考虑为数组分配内存的情况。 传统的方法是声明一个数组。 要在 C + + 中声明数组, 必须指定数组的长度。 因此, 数组长度在程序编译时就设定好了; 这就是编译阶段决策。 您可能认为, 在 80% 的情况下, 一个包含 20 个元素的数组足够了, 但程序有时需要处理 200 个元素。 为了安全起见, 使用了一个包含 200 个元素的数组。 这样, 程序在大多数情况下都浪费了内存。OOP 通过将这样的决策推迟到运行阶段进行, 使程序更灵活。 在程序运行后, 可以这次告诉它只需要 20 个元素, 而还可以下次告诉它需要 205 个元素。

总之, 使用 OOP 时, 您可能在运行阶段确定数组的长度。 为使用这种方法, 语言必须允许在程序运行时创建数组。 C + + 采用的方法是, 使用关键字 new 请求正确数量的内存以及使用指针来跟踪新分配的内存的位置。 在运行阶段做决策并非 OOP 独有的, 但使用 C + + 编写这样的代码比使用 C 语言简单。

计算机计算是基于存储的计算,指针策略是 C + + 内存管理编程理念的核心.

声明和初始化指针

计算机需要跟踪指针指向的值的类型。 例如, char 的地址与 double 的地址看上去没什么两样, 但 char 和 double 使用的字节数是不同的, 它们存储值时使用的内部格式也不同。 因此, 指针声明必须指定指针指向的数据的类型。

int *p1;    // C   style, 强调 *p 是一个 int 类型的值。
int* p2;    // C++ style, 强调 int* 是一种类型— 指向 int 的指针。
int*p3;
int * p4;

变量声明中的 * 只是标明p 是一个指针,并不是对p求值.

四种声明方式都是合法的, 只有第三种是相对最明确说明声明语句的实质含义的: int* 代表指向int的指针类型, 指针变量名是p2.

但是从下面的p1,p2 的定义看,C style, 强调 *p 是一个 int 类型的值更合理.

#include <iostream>
#include <string>
using  namespace std;

int main()
{
    double d;  // 声明了一个double常规变量, d 指代值, &d指代值对应的指针
    double *pd; // 声明了一个double指针变量, *pd 指代值,pd 指代值对应的指针

    // 初始化
    d = 10.0;
    pd = &d;

    cout << "d 的地址:" << &d << " d的值:" << d << endl;


    cout << "pd 的地址:" << pd << " pd的值:" << *pd << endl;

    // 通过指针修改值
    *pd = 20;
    cout << "d 的地址:" << &d << " d的值:" << d << endl;
    cout << "pd 的地址:" << pd << " pd的值:" << *pd << endl;

    // d 的地址: 0x61fe10 d的值: 10
    // pd 的地址:0x61fe10 pd的值:10
    // d 的地址:0x61fe10 d的值:20
    // pd 的地址:0x61fe10 pd的值:20

    double *p1, p2;
    p1 = &d;
    p2 = 10;
    cout << "p1 的地址:" << p1 << " p1的值:" << *p1 << endl;
    cout << "p2 的地址:" << &p2 << " p2的值:" << p2 << endl;
    // p1 的地址:0x61fe08 p1的值:20
    // p2 的地址:0x61fe00 p2的值:10
}

指针变量不仅仅是指针, 而且是指向特定类型的指针。

定义常规变量实际上也是在定义指针, 只是这个指针的实际值是用 &VarName 表示的.

可以在声明语句中初始化指针。 在这种情况下, 被初始化的是指针, 而不是它指向的值。

指针的危险

危险更易发生在那些使用指针不仔细的人身上。 极其重要的一点是: 在 C + + 中创建指针时, 计算机将分配用来存储地址的内存, 但不会分配用来存储指针所指向的数据的内存。 为数据提供空间是一个独立的步骤

  • 定义指针变量时, 计算机为该变量分配了内存
  • 指针变量也是变量的一种, 也需要初始化, 没有初始化之前,它指向的可能是任何一个可能的地址.
  • 没有初始化的指针导致不可控的操作,引发的错误可能会导致一些最隐匿、 最难以跟踪的 bug。
  • 一定要在给指针指向的值赋值之前,即一定要在对指针应用解除引用运算符(*) 之前, 将指针初始化为一个确定的、 适当的地址。 这是关于使用指针的金科玉律。
#include <iostream>
#include <string>
using  namespace std;

int main()
{
    double *d; // 没有初始化
    *d=200;
    cout<<"d 的地址和值:" << d << " "<<*d; //幸运的时候,提供一个无效地址, 然后打印空
}

指针和数字

指针不是整型, 虽然计算机通常把地址当作整数来处理。 从概念上看, 指针与整数是截然不同的类型。

  • 整数是可以执行加、 减、 除等运算的数字, 而指针描述的是位置, 将两个地址相乘没有任何意义。
  • 从可以对整数和指针执行的操作上看, 它们也是彼此不同的。 因此, 不能简单地将整数赋给指针
  • 要将数字值作为地址来使用, 应通过强制类型转换将数字转换为适当的地址类型:
#include <iostream>
#include <string>
using  namespace std;

int main()
{
    double *d; 
    // double* 是一个double类型的指针
    //d = 0x61fe10; // invalid conversion from 'int' to 'double*' [-fpermissive]
    d = (double*)0x61fe10;
    cout << "d 的指针值:" << d << " d指向的值是:" << *d << endl;
    // d 的指针值:0x61fe10 d指向的值是:8.19468e-317
}

使用 new 来分配内存

对指针的工作方式有一定了解后, 来看看它如何实现在程序运行时分配内存。

前面我们都将指针初始化为变量的地址; 变量是在编译时分配的有名称的内存, 而指针只是为可以通过名称直接访问的内存提供了一个别名。

指针真正的用武之地在于, 在运行阶段分配未命名的内存以存储值。 在这种情况下, 只能通过指针来访问内存。

在 C 语言中, 可以用库函数 malloc( )来分配内存; 在 C + + 中仍然可以这样做, 但 C + + 还有更好的方法— new 运算符。

下面来试试这种新技术, 在运行阶段为一个 int 值分配未命名的内存, 并使用指针来访问这个值。 这里的关键所在是 C + + 的 new 运算符。 程序员要告诉 new, 需要为哪种数据类型分配内存; new 将找到一个长度正确的内存块, 并返回该内存块的地址。 程序员的责任是将该地址赋给一个指针。

下面是一个这样的示例: new int 告诉程序, 需要适合存储 int 的内存。 new 运算符根据类型来确定需要多少字节的内存。 然后, 它找到这样的内存, 并返回其地址。 接下来, 将地址赋给 pn, pn 是被声明为指向 int 的指针。 现在, pn 是地址, 而* pn 是存储在那里的值。

int *pn=new int;  // 动态为pn分配int类型的地址

之前的方式:

int i;
int *pt=&i;
  • pn 指向的内存没有名称, 我们说 pn 指向一个数据对象, 这里的“ 对象” 不是“ 面向对象编程” 中的对象, 而是一样“ 东西”。
  • 术语“ 数据对象” 比“ 变量” 更通用, 它指的是为数据项分配的内存块。 因此, 变量也是数据对象, 但 pn 指向的内存不是变量。
  • 乍一看, 处理数据对象的指针方法可能不太好用, 但它使程序在管理内存方面有更大的控制权。

为一个数据对象( 可以是结构, 也可以是基本类型) 获得并指定分配内存的通用格式如下:

typeName *pointer_name = new typeName;

需要在两个地方指定数据类型: 用来指定需要什么样的内存和用来声明合适的指针。 当然, 如果已经声明了相应类型的指针, 则可以使用该指针, 而不用再声明一个新的指针。

如果按数据流在计算机的硬件侧的实际流动来理解, 指针更直接; 如果按人的脱离计算机的抽象思维来讲, 变量名方式更直接.

#include <iostream>
#include <string>
using  namespace std;

int main()
{
    int i = 1000; // 人类好理解的方式,变量名
    int *pt = new int; // 更接近硬件的方式:指针. 给一块存int的内存
    *pt = 1000; //这块内存存入整数值1000

    cout << "常规变量名 i 对应的内存地址:" << &i << ", 变量i的内存地址存入的值:" << *(&i) << endl;
    cout << "指针变量pt存入的内存地址:" << pt << ", 变量pt 指向的内存地址存入的值:" << *pt;
    // 常规变量名 i 对应的内存地址:0x61fe14, 变量i的内存地址存入的值:1000
    // 指针变量pt存入的内存地址:0xea1660, 变量pt 指向的内存地址存入的值:1000
}

new 分配的内存块通常与常规变量声明分配的内存块不同。

  • 常规变量的值都存储在被称为栈( stack) 的内存区域中
  • new 从被称为堆( heap) 或自由存储区( free store) 的内存区域分配内存。 第 9 章将更详细地讨论这一点。

内存被耗尽?

计算机可能会由于没有足够的内存而无法满足 new 的请求。 在这种情况下, new 通常会引发异常— 一种将在第 15 章讨论的错误处理技术; 而在较老的实现中, new 将返回 0。

在 C + + 中, 值为 0 的指针被称为空指针( null pointer)。 C + + 确保空指针不会指向有效的数据, 因此它常被用来表示运算符或函数失败( 如果成功, 它们将返回一个有用的指针)。 将在第 6 章讨论的 if 语句可帮助您处理这种问题; 就目前而言, 您只需如下要点: C + + 提供了检测并处理内存分配失败的工具。

使用 delete 释放内存

delete 运算符使得在使用完内存后, 能够将其归还给内存池, 这是通向最有效地使用内存的关键一步。 归还或释放( free) 的内存可供程序的其他部分使用。

使用 delete 时, 后面要加上指向内存块的指针( 这些内存块最初是用 new 分配的): 这将释放 ps 指向的内存, 但不会删除指针 ps 本身。 例如, 可以将 ps 重新指向另一个新分配的内存块。

一定要配对地使用 new 和 delete; 否则将发生内存泄漏( memory leak), 也就是说, 被分配的内存再也无法使用了。 如果内存泄漏严重, 则程序将由于不断寻找更多内存而终止。

  • 不要尝试释放已经释放的内存块, C + + 标准指出, 这样做的结果将是不确定的, 这意味着什么情况都可能发生。
  • 另外, 不能使用 delete 来释放声明变量所获得的内存.只能用 delete 来释放使用 new 分配的内存。 然而, 对空指针使用 delete 是安全的。

new 和 delete 成对使用; delete 只删除 new分配的内存.

创建两个指向同一个内存块的指针将增加错误地删除同一个内存块两次的可能性。

#include <iostream>
#include <string>
using  namespace std;

int main()
{
    int *pi= new int;
    delete pi;
    delete pi; // 不会报错, 但可能导致不可预知的后果

    int j;
    int *k=&j;
    delete k; // 不会报错, 但可能导致不可预知的后果
}

使用 new 来创建动态数组

在编译时给数组分配内存被称为静态联编( static binding), 意味着数组是在编译时加入到程序中的。 但使用 new 时, 如果在运行阶段需要数组, 则创建它; 如果不需要, 则不创建。 还可以在程序运行时选择数组的长度。 这被称为动态联编( dynamic binding), 意味着数组是在程序运行时创建的。 这种数组叫作动态数组( dynamic array)。 使用静态联编时, 必须在编写程序时指定数组的长度; 使用动态联编时, 程序将在运行时确定数组的长度。

在 C + + 中, 创建动态数组很容易; 只要将数组的元素类型和元素数目告诉 new 即可。 必须在类型名后加上方括号, 其中包含元素数目。new 运算符返回第一个元素的地址。

当程序使用完 new 分配的内存块时, 应使用 delete 释放它们。 然而, 对于使用 new 创建的数组, 应使用另一种格式的 delete 来释放:

delete [] pointer_name

方括号告诉程序, 应释放整个数组, 而不仅仅是指针指向的元素。 请注意 delete 和指针之间的方括号。 如果使用 new 时, 不带方括号, 则使用 delete 时, 也不应带方括号。 如果使用 new 时带方括号, 则使用 delete 时也应带方括号。new 与 delete 的格式不匹配导致的后果是不确定的, 这意味着程序员不能依赖于某种特定的行为。

#include <iostream>
#include <string>
using  namespace std;

int main()
{
    int num;
    cout<<"请输入需要计算平均数的数值数量:\n";
    cin>>num;
    int *nums = new int [num];
    double sum =0,average;
    int i;
    for (i=0;i<num;i++){
        nums[i]=i;
        sum+= nums[i];
        cout<<nums[i]<<endl;
    }
    average = sum/i;
    cout<<"sum and average:"<<sum<<" "<<average;
    delete [] nums;  // 没有该语句不会报错,但会造成内存泄露
}
#include <iostream>
#include <string>
using  namespace std;

int main()
{
    int num;
    cout<<"请输入需要计算平均数的数值数量:\n";
    cin>>num;
    int *nums = new int [num];
    double sum =0,average;
    int i;
    for (i=0;i<num;i++){
        nums[i]=i;
        sum+= nums[i];
        cout<<nums[i]<<endl;
    }
    average = sum/i;
    cout<<"sum and average:"<<sum<<" "<<average;
    delete [] nums;  // 没有该语句不会报错,但会造成内存泄露
}
/* 请输入需要计算平均数的数值数量:
8
0
1
2
3
4
5
6
7
sum and average:28 3.5 */

总之, 使用 new 和 delete 时, 应遵守以下规则:

  • 不要使用 delete 来释放不是 new 分配的内存。
  • 不要使用 delete 释放同一个内存块两次。
  • 如果使用 new [] 为数组分配内存, 则应使用 delete []来释放。
  • 如果使用 new [] 为一个实体分配内存, 则应使用 delete( 没有方括号) 来释放。
  • 对空指针应用 delete 是安全的。

程序确实跟踪了分配的内存量, 以便以后使用 delete []运算符时能够正确地释放这些内存。 但这种信息不是公用的, 例如, 不能使用 sizeof 运算符来确定动态分配的数组包含的字节数。程序员的责任是跟踪内存块中的元素个数。 也就是说, 由于编译器不能对动态数组元素个数进行跟踪, 因此编写程序时, 必须让程序跟踪元素的数目。

C 和 C + + 内部都使用指针来处理数组。 数组和指针基本等价是 C 和 C + + 的优点之一

指针、 数组和指针算术

指针和数组基本等价的原因在于指针算术( pointer arithmetic) 和 C + + 内部处理数组的方式。 首先, 我们来看一看算术。 将整数变量加 1 后, 其值将增加 1; 但将指针变量加 1 后, 增加的量等于它指向的类型的字节数。 将指向 double 的指针加 1 后, 如果系统对 double 使用 8 个字节存储, 则数值将增加 8; 将指向 short 的指针加 1 后, 如果系统对 short 使用 2 个字节存储, 则指针值将增加 2。

#include <iostream>
#include <string>
using  namespace std;

int main()
{
    double d1[] = {100, 200, 300.0};
    short s1[] = {400, 500, 600};

    double *pd1 = d1; // 数组名为指针
    short *ps1 = &s1[0]; // 数组第一个元素的地址

    int i;
    cout << "d1:";
    for (i = 0; i < 3; i++) {
        cout << d1[i] << " ";
    }
    cout << endl;

    cout << "pd1:";
    for (i = 0; i < 3; i++) {
        cout << *(pd1 + i) << " ";
    }
    cout << endl;

    // d1:100 200 300
    // pd1:100 200 300

    int ai[3];
    // 数组名对应的指针是常量
    // ai +=1; // incompatible types in assignment of 'int' to 'int [3]'
}

将指针变量加 1 后, 其增加的值等于指向的类型占用的字节数。

数组名和指针的区别:

  • 在很多情况下, 可以相同的方式使用指针名和数组名。符(*)。 在多数表达式中, 它们都表示地址。 区别之一是, 可以修改指针的值, 而数组名是常量
  • 对数组应用 sizeof 运算符得到的是数组的长度,这种情况下, C + + 不会将数组名解释为地址;而对指针应用 sizeof 得到的是指针的长度, 即使指针指向的是一个数组。
    • 对数组取地址时, 数组名也不会被解释为其地址。
      • 数组名被解释为其第一个元素的地址,
      • 而对数组名应用地址运算符时, 得到的是整个数组的地址
#include <iostream>
#include <string>
using  namespace std;

int main()
{
    double d1[] = {100, 200, 300.0};
    short s1[] = {400, 500, 600};

    double *pd1 = d1; // 数组名为指针
    short *ps1 = &s1[0]; // 数组第一个元素的地址

    int i;
    cout << "d1:";
    for (i = 0; i < 3; i++) {
        cout << d1[i] << " ";
    }
    cout << endl;

    cout << "pd1:";
    for (i = 0; i < 3; i++) {
        cout << *(pd1 + i) << " ";
    }
    cout << endl;

    // d1:100 200 300
    // pd1:100 200 300

    int ai[3];
    // 数组名对应的指针是常量
    // ai +=1; // incompatible types in assignment of 'int' to 'int [3]'

    cout << "size of d1:" << sizeof(d1) << endl; //24
    cout << "size of pd1:" << sizeof(pd1) << endl; //8

    int tell[10];
    cout << "tell:" << tell << endl;
    cout << "&tell[0]:" << &tell[0] << endl;
    cout << "&tell:" << &tell << endl;
    // tell:0x61fdb0
    // &tell[0]:0x61fdb0
    // &tell:0x61fdb0

    cout << "tell+1:" << tell + 1 << endl;
    cout << "&tell[0]+1:" << &tell[0] + 1 << endl;
    cout << "&tell+1:" << &tell + 1 << endl;
    // tell+1:0x61fdb4
    // &tell[0]+1:0x61fdb4
    // &tell+1:0x61fdd8
}

您对使用 C-风格字符串和 cstring 库的一些方面有了了解后, 便可以理解为何使用 C + + string 类型更为简单了: 您不用担心字符串会导致数组越界, 并可以使用赋值运算符而不是函数 strcpy( )和 strncpy( )。

使用 new 创建动态结构

在运行时创建数组优于在编译时创建数组, 对于结构也是如此。 需要在程序运行时为结构分配所需的空间, 这也可以使用 new 运算符来完成。 通过使用 new, 可以创建动态结构。 同样,“ 动态” 意味着内存是在运行时, 而不是编译时分配的。 由于类与结构非常相似, 因此本节介绍的有关结构的技术也适用于类。

创建动态结构时, 不能将成员运算符句点用于结构名, 因为这种结构没有名称, 只是知道它的地址。 C + + 专门为这种情况提供了一个运算符: 箭头成员运算符( − >)。 该运算符由连字符和大于号组成, 可用于指向结构的指针, 就像点运算符可用于结构名一样。

有时, C + + 新手在指定结构成员时, 搞不清楚何时应使用句点运算符, 何时应使用箭头运算符。 规则非常简单。 如果结构标识符是结构名, 则使用句点运算符; 如果标识符是指向结构的指针, 则使用箭头运算符。

另一种访问结构成员的方法是, 如果 ps 是指向结构的指针, 则* ps 就是被指向的值— 结构本身。 由于*ps 是一个结构, 因此(*ps). price 是该结构的 price 成员。 C + + 的运算符优先规则要求使用括号。

#include <iostream>
#include <string>
using  namespace std;

int main()
{
    struct things{
        int id;
        int num;
    };

    things *pt = new things;

    // 两种访问结构体成员变量的方式
    pt->id=1;
    pt->num=10;

    cout<<(*pt).id<<endl;    //1
    cout<<(*pt).num<<endl;   //10

    delete pt;
}

例子:定义 一个函数 getString( ), 该函数返回一个指向输入字符串的指针。 该函数将输入读入到一个大型的临时数组中, 然后使用 new [] 创建一个刚好能够存储该输入字符串的内存块, 并返回一个指向该内存块的指针。 对于读取大量字符串的程序, 这种方法可以节省大量内存( 实际编写程序时, 使用 string 类将更容易, 因为这样可以使用内置的 new 和 delete)。

在这个例子中, getString( )分配内存, 而 main( )释放内存。 将 new 和 delete 放在不同的函数中通常并不是个好办法, 因为这样很容易忘记使用 delete。 不过这个例子确实把 new 和 delete 分开放置了, 只是为了说明这样做也是可以的。

#include <iostream>
#include <string>
#include <cstring>
using  namespace std;

char *getString(void);  // 函数原型

int main()
{
    char *myStr;
    myStr = getString(); // 这里调用了 new 操作
    cout << myStr;
    // 请输入一个长字符串:
    // fdhjakhfjadshfhadsfaslkifoewuriewqlkfvdsnmcvmxnajfaejflkdjalkjfadsTTT
    // fdhjakhfjadshfhadsfaslkifoewuriewqlkfvdsnmcvmxnajfaejflkdjalkjfadsTTT
    delete [] myStr; // 容易忘记的操作
}

char *getString()
{
    char temp[1000]; // 一个比较大的临时变量, 跳出函数后temp会被释放
    cout << "请输入一个长字符串:\n";
    cin.getline(temp, 1000);

    // 按需分配一个数组
    char *pc = new char[sizeof(temp) + 1];
    strcpy(pc, temp);
    return pc;
}

自动存储、 静态存储和动态存储

根据用于分配内存的方法, C + + 有 3 种管理数据内存的方式: 自动存储、 静态存储和动态存储( 有时也叫作自由存储空间或堆)。 在存在时间的长短方面, 以这 3 种方式分配的数据对象各不相同。 下面简要地介绍每种类型( C + + 11 新增了第四种类型— 线程存储, 这将在第 9 章简要地讨论)。

1. 自动存储

在函数内部定义的常规变量使用自动存储空间, 被称为自动变量( automatic variable), 这意味着它们在所属的函数被调用时自动产生, 在该函数结束时消亡。

实际上, 自动变量是一个局部变量, 其作用域为包含它的代码块。 代码块是被包含在花括号中的一段代码。 到目前为止, 我们使用的所有代码块都是整个函数。 然而, 在下一章将会看到, 函数内也可以有代码块。 如果在其中的某个代码块定义了一个变量, 则该变量仅在程序执行该代码块中的代码时存在。

自动变量通常存储在栈中。 这意味着执行代码块时, 其中的变量将依次加入到栈中, 而在离开代码块时, 将按相反的顺序释放这些变量, 这被称为后进先出( LIFO)。 因此, 在程序执行过程中, 栈将不断地增大和缩小。

自动存储并不需要程序员干预进行 new 和 delete操作.

2.静态存储

静态存储是整个程序执行期间都存在的存储方式。 使变量成为静态的方式有两种:

  • 一种是在函数外面定义它, 也就是全局变量
  • 另一种是在声明变量时使用关键字 static: static double i;

第 9 章将详细介绍静态存储。 自动存储和静态存储的关键在于: 这些方法严格地限制了变量的寿命。 变量可能存在于程序的整个生命周期( 静态变量), 也可能只是在特定函数被执行时存在( 自动变量)。

3. 动态存储

new 和 delete 运算符提供了一种比自动变量和静态变量更灵活的方法。 它们管理了一个内存池, 这在 C + + 中被称为自由存储空间( free store) 或堆( heap)。 该内存池同用于静态变量和自动变量的内存是分开的。

new 和 delete 让您能够在一个函数中分配内存, 而在另一个函数中释放它。 因此, 数据的生命周期不完全受程序或函数的生存时间控制。 与使用常规变量相比, 使用 new 和 delete 让程序员对程序如何使用内存有更大的控制权。

然而, 内存管理也更复杂了。 在栈中, 自动添加和删除机制使得占用的内存总是连续的, 但 new 和 delete 的相互影响可能导致占用的自由存储区不连续, 这使得跟踪新分配内存的位置更困难。

栈、 堆和内存泄漏

如果使用 new 运算符在自由存储空间( 或堆) 上创建变量后, 如果没有调用 delete, 则即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放, 在自由存储空间上动态分配的变量或结构也将继续存在。

实际上, 将会无法访问自由存储空间中的结构, 因为指向这些内存的指针无效。 这将导致内存泄漏。 被泄漏的内存将在程序的整个生命周期内都不可使用; 这些内存被分配出去, 但无法收回。 极端情况( 不过不常见) 是, 内存泄漏可能会非常严重, 以致于应用程序可用的内存被耗尽, 出现内存耗尽错误, 导致程序崩溃。 另外, 这种泄漏还会给一些操作系统或在相同的内存空间中运行的应用程序带来负面影响, 导致它们崩溃。

即使是最好的程序员和软件公司, 也可能导致内存泄漏。 要避免内存泄漏, 最好是养成这样一种习惯, 即同时使用 new 和 delete 运算符, 在自由存储空间上动态分配内存, 随后便释放它。 C + + 智能指针有助于自动完成这种任务, 这将在第 16 章介绍。

指针是功能最强大的 C + + 工具之一, 但也最危险, 因为它们允许执行对计算机不友好的操作, 如使用未经初始化的指针来访问内存或者试图释放同一个内存块两次。 另外, 在通过实践习惯指针表示法和指针概念之前, 指针是容易引起迷惑的。 由于指针是 C + + 编程的重要组成部分, 本书后面将更详细地讨论它。 本书多次对指针进行了讨论, 就是希望您能够越来越熟悉它。

数组的替代品

本章前面说过, 模板类 vector 和 array 是数组的替代品。

1.模板类 vector

模板类 vector 类似于 string 类, 也是一种动态数组。 您可以在运行阶段设置 vector 对象的长度, 可在末尾附加新数据, 还可在中间插入新数据。 基本上, 它是使用 new 创建动态数组的替代品。 实际上, vector 类确实使用 new 和 delete 来管理内存, 但这种工作是自动完成的。

这里不深入探讨模板类意味着什么, 而只介绍一些基本的实用知识。

  • 首先, 要使用 vector 对象, 必须包含头文件 vector。
  • 其次, vector 包含在名称空间 std 中, 因此您可使用 using 编译指令、 using 声明或 std:: vector。
  • 第三, 模板使用不同的语法来指出它存储的数据类型。
  • 第四, vector 类使用不同的语法来指定元素数。

把 vector看作一个定义好的类,有不同的变量和方法就行了.

一般而言, 下面的声明创建一个名为 vtName 的 vector 对象, 它可存储 n_elem 个类型为 typeName 的元素, 其中参数 n_elem 可以是整型常量, 也可以是整型变量。

vector <typeName> vtName(n_elem);

#include <iostream>
#include <vector>
using  namespace std;

int main()
{
    vector<int> vi; // 定义了长度为0的vector<int> 类型 i, 省略了()
    vector<double> vd(10);
}

有了OOP思维, 以及 new,delete 操作后, 可以定义很多类来封装常用的操作对象.

2.模板类 array( C + + 11)

  • 变长数组:vector. vector 类的功能比数组强大, 但付出的代价是效率稍低。
  • 固定长度数组:如果您需要的是长度固定的数组, 使用数组是更佳的选择, 但代价是不那么方便和安全。
  • 方便安全的固定长度数组:array.有鉴于此, C + + 11 新增了模板类 array, 它也位于名称空间 std 中。 与数组一样, array 对象的长度也是固定的, 也使用栈( 静态内存分配), 而不是自由存储区, 因此其效率与数组相同, 但更方便, 更安全。

要创建 array 对象, 需要包含头文件 array。 array 对象的创建语法与 vector 稍有不同,之,下面的声明创建一个名为 arr 的 array 对象, 它包含 n_elem 个类型为 typename 的元素: 与创建 vector 对象不同的是, n_elem 不能是变量。

array<typeName,n_elem> arr;

#include <iostream>
#include <array>
using  namespace std;

int main()
{
    array<int,5> ai;
    array<double,2> ad={1.2,3.4};
}

比较数组、 vector 对象和 array 对象

  • 首先, 注意到无论是数组、 vector 对象还是 array 对象, 都可使用标准数组表示法来访问各个元素。
  • 与 C 语言一样, C + + 也不检查数组、 vector 对象和 array 对象的超界错误。
  • 从地址可知, array 对象和数组存储在相同的内存区域( 即栈) 中, 而 vector 对象存储在另一个区域( 自由存储区或堆) 中。
  • 第三, 注意到可以将一个 array 对象赋给另一个 array 对象; 而对于数组, 必须逐元素复制数据。
  • 可以使用 vector 和 array 对象的成员函数 at():中括号表示法和成员函数 at() 的差别在于, 使用 at() 时, 将在运行期间捕获非法索引, 而程序默认将中断。 这种额外检查的代价是运行时间更长, 这就是 C + + 让允许您使用任何一种表示法的原因所在。 另外, 这些类还让您能够降低意外超界错误的概率。 例如, 它们包含成员函数 begin() 和 end(), 让您能够确定边界, 以免无意间超界.
#include <iostream>
#include <array>
#include <vector>
using  namespace std;

int main()
{
    int i[5] = {1, 2, 3, 4, 5};
    array<int, 5> ai = {1, 2, 3, 4, 5};
    vector<int> vi {1, 2, 3, 4, 5}; // 初始化不用 = 和 ()

    cout << "i[4]" << i[4] << endl;
    cout << "ai[4]" << ai[4] << endl;
    cout << "vi[4]" << vi[4] << endl;
    // i[4]5
    // ai[4]5
    // vi[4]5

    cout << "i地址" << &i[0] << endl;
    cout << "ai地址" << &ai[0] << endl;
    cout << "vi地址" << &vi[0] << endl;
    // i地址0x61fd00
    // ai地址0x61fce0
    // vi地址0xed1660


    // []访问都没有越界检查
    cout << "i[-4]" << i[-4] << endl;
    cout << "ai[-4]" << ai[-4] << endl;
    cout << "vi[-4]" << vi[-4] << endl;
    // i[-4]5
    // ai[-4]1840756
    // vi[-4]842231040

    // cout<<"ai.at(-4)"<<ai.at(-4)<<endl;  //ai.at(-4)terminate called after throwing an instance of 'std::out_of_range'    //  what():  array::at: __n (which is 18446744073709551612) >= _Nm (which is 5)
    // cout<<"vi.at(-4)"<<vi.at(-4)<<endl; // vi.at(-4)terminate called after throwing an instance of 'std::out_of_range' //  what():  vector::_M_range_check: __n (which is 18446744073709551612) >= this->size() (which is 5)


    cout<<"ai.begin(),ai.end():"<<ai.begin()<<" "<<ai.end()<<endl;
    // ai.begin(),ai.end():0x61fce0 0x61fcf4
}
© Licensed under CC BY-NC-SA 4.0

我注意过,即便是那些声称一切都是命中注定的而且我们无力改变的人,在过马路之前都会左右看。——史提芬·霍金

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!