C++ Primer 5th 第12章 动态内存

时间:2023-12-20 19:01:08

到本章为止,我们还没有接触过动态内存,接下来我们会学习动态内存的知识。

关于动态内存,第一个疑问是:为什么我们要使用动态内存?它有什么优点?

要了解动态内存,得先了解一个进程在内存中的基本空间结构。进程是被执行起来的程序,程序是未执行的二进制文件。

大概来说,程序包含了三大部分内容:一是程序代码,称为text段,二是程序中定义的初始化了的全局和静态变量(非0初始值),称为data段,三是未初始化的全局和静态变量(或者初始值为0),称为bss段。

C++ Primer 5th 第12章 动态内存

进程是执行起来的程序,自然包含上面的三部分内容,另外还会额外增加进两部分内容。一个是堆,另一个是栈。栈用来存储我们在程序中用到的局部变量,而堆则是用于本章所学习的动态内存存储。

栈的效率高,速度快,局部访问,自动管理,空间大小有限。

堆是一个内存池,它也被称为*存储,堆的空间可以很大,也能全局访问共享,手动管理。

有了这些基本知识,再回到开头的第一个问题:为什么使用动态内存及其优点。因为动态内存是存在堆中的,所以其优点是空间大小可以很大。除了优点之外,更有几个不使用动态内存则无法解决的问题,这也是为什么要使用动态内存的原因:

1.我们不知道程序到底会需要多少空间或者说多少个对象。比如说,程序经常需要处理一批数据,有多少个数据不确定,为了存储这些数据,我们只能根据当时的情况来申请内存空间来进行存储和处理。

2.为了实现面向对象的动态绑定功能。这点将在后面的章节进行学习。

3.程序的多个对象之间进行数据的共享,或者说全局访问共享。典型的例子是接下来即将学习的智能指针。

简而言之,动态内存的关键点在于“动”字上。

加上动态内存,在C++总共就有了3种类型的内存:静态内存、动态内存、栈内存。静态内存存储全局变量、静态局部变量、类静态数据成员;栈内存用于存储局部变量、动态内存用于存储手动分配的对象。

对于手动通过堆分配的内存来说,我们必须手动管理内存的释放,这是相当有难度的,尤其在C++中那些可能出现异常的地方。由于正确的管理内存很棘手,因此标准库提供了两个“智能指针”来帮助我们动态使用内存。一种是“共享指针”,它允许多个指针指向一个对象,而在没有指针指向对象时自动释放内存;另一种是“独占指针”,某一时刻,它只允许一个指针指向对象。标准库还有一个tiny工具weak_ptr,是一个辅助工具,用于指向“共享指针”指向的对象,但不会引起“共享指针”计数增加。

头文件

#include <memory>

提供工具

shared_ptr;
unique_ptr;
weak_ptr;
make_shared;

shared_ptr和vector一样,是一个模板,在提供了足够信息后,用于生成特定类型的智能指针。用法如下:

shared_ptr<int>         *pi;    //int类型指针
shared_ptr<string> *ps; //string类型指针
shared_ptr<vector<int>> *pv; //vector<int>容器指针

以上3个指针都是默认初始化的,在智能指针中,默认初始化的指针将会被初始化为nullptr。

智能指针的使用方式与普通指针相同,解引用智能指针返回所指对象,在条件判断中使用智能指针就是检测其是否为空。例如:

if (pv && pv->empty())
{
pv.push_back();
}

最安全的分配和使用动态内存的方法是使用make_shared标准库函数,该函数能够代替我们去动态分配内存,并且将对象进行初始化,之后返回一个智能指针给我们使用。同样的,make_shared也是一个模板,虽然他是一个函数模板,但是却不能根据我们传入的参数进行类型推断。原因是该函数的模板参数存在于返回类型之中,导致无法直接推断,因此我们必须手动指定参数类型。其使用方式如下:

shared_ptr<string> ps = make_shared<string>();
auto ps2 = make_shared<string>(, 'c');

对于智能指针对象ps和ps2的初始化,我们传给make_shared的参数将会用于初始化ps和ps2所指内存中对象的初始化。如果我们没有传递参数给make_shared,那么ps和ps2依然会被初始化,但是是值初始化,也即ps和ps2指向了各自的内存(内存中的对象没有初始化),而不是默认初始化的nullptr;总而言之,make_shared一定会返回一个指向一块内存的智能指针,而不会返回nullptr;

另外,由于make_shared不会返回nullptr,所以我们总是可以用auto关键词来让编译器进行推断,进而得出我们想要的特定类型的智能指针。

我们可以认为每个shared_ptr对象都有一个关联的计数器,通常称为引用计数。当进行一些操作时,计数会根据实际的情况进行增加或减少。例如,拷贝一个智能指针,此时会增加指向对象的指针个数;一个智能指针超出作用域,此时,会减少指向对象的指针个数。当一个计数变为0时,该智能指针会自动释放所指的动态内存。减少计数和销毁动态内存是通过智能指针的析构函数来完成的。

动态对象的生存周期同引用类型对象相同,当引用对象超出作用域时,引用对象本身会销毁,但引用对象所引用(或者说所指向)的对象不会销毁。同样的,当指向动态对象的对象(或者说指针)超出作用域之后,指向动态对象的对象销毁和动态对象之间并无关系。实例如下:

int i = ;
{ //新作用域
int &ri = i;
} //引用对象ri超出作用域

{
   int *p = new int{};
}

上面代码中引用对象ri超出作用域之后,引用对象ri所引用的对象i并不会销毁。同样的,无名动态对象被指针p所指,当p超出作用域被销毁后,无名对象并不会被销毁。

直接管理内存

在C++中也支持直接手动分配和释放内存,使用new来分配内存,使用delete来释放内存,其中new和delete都是类似于"+"、"/"一样的运算符。

使用new运算符得到是相应类型对象的指针,并且该对象是匿名的,只能使用该对象的指针进行间接访问。当然,我们也可以使用一个引用来绑定到这个动态对象上,从而通过引用来访问对象。

使用new运算符分配的对象,默认情况下是默认初始化的。对于内置类型,这意味着对象的值是不确定的;对于自定义类型,意味着使用默认构造函数。如果我们想要对动态获得的对象进行初始化,可以使用直接初始化或者列表初始化的方式,当然,也可以使用值初始化的方式。

对于自定义类类型,使用默认初始化,还是值初始化结果都是调用默认构造函数,通常情况下是没有区别的,除非是非常简单的没有定义任何构造函数的类类型会有差异。

对于内置类型,使用默认初始化和值初始化则不相同,默认初始化的值是未定义的,值初始化的值则为0。

我们还可以使用auto配合new来进行类型推断并初始化,语法如下:

auto p1 = new auto(obj);
auto p2 = new auto{obj}; //错误,不允许使用花括号

该语句的作用是使用obj来推断动态对象的类型,并使用obj来初始化该动态对象,new返回的指针存储于p1中。

动态对象也可以是const类型的,和其他const对象一样,const对象必须初始化。例如:

const int *p1 = new const int();
const int *p2 = new int();

对于p1来说,p1是一个指向const对象的指针。

对于p2来说,p2也是一个指向const对象的指针,然后实际对象并不是const对象。

new分配内存也存在失败的情况。需要知道的是,new即使分配内存失败,也不会导致内存泄露。new内部自身会进行一些处理以防止内存泄露。默认情况下,new在妥善处理后,会抛出一个异常来告知内存分配出现问题。当然,如果我们的程序不接受异常或者没有异常处理,则可以在new后使用"(nothrow)"来告知new不要抛出异常,此时,new返回nullptr指针。如果要使用这些特性,需要包含头文件“new”。

动态分配内存使得我们能够手动的申请内存,相应的,我们需要手动的释放申请的内存。方法是使用delete运算符,例如

delete p;

该运算符和一个运算对象组成一个表达式,此表达式的结果是:销毁p指向的对象,并且释放该对象所占用的内存。

该运算符只能对动态申请的内存进行释放,并且只能释放一次。对非动态内存进行释放或者对某一内存多次释放,都是未定义的行为。

另外,由于动态对象的生存周期是我们手动管理的,因此可以在动态对象的生存期间在整个程序中进行共享。

如前所述,使用智能指针shared_ptr进行内存管理时,我们如果没有初始化shared_ptr对象,那么它指向一个空指针,除非我们使用make_shared来生成智能指针。

除了使用make_shared来生成有效的智能指针,我们也可以使用new生成的指针来初始化shared_ptr对象从而得到一个有效的智能指针。方法是在定义智能指针对象时,使用new返回的指针来直接初始化。如下:

shared_ptr<int> pi1(new int);
shared_ptr<int> pi2 = new int; //错误,shared_ptr的构造函数是explicit的

由于shared_ptr的构造函数是explicit的,因此我们不能使用拷贝构造函数来进行初始化,必须使用直接初始化,即直接匹配构造函数的方式来进行初始化。

还有一个问题是:当我们默认初始化了一个智能指针,该智能指针是指向nullptr的,随后我们自己使用new手动分配了动态内存,如何让默认初始化的智能指针指向我们分配的动态内存?我们可以使用智能指针的reset方法。如下:

p.reset();
p.reset(q);
p.reset(q, d);

如果p是唯一一个指向动态对象的智能指针,则reset会释放该动态对象,如果向reset传递了普通的非智能指针q,那么p会新指向q所指的内存,如果还向reset传递了d,那么会使用d来释放q,而不是默认的delete。

unique_ptr

一个unique_ptr拥有它所指向的对象,和shared_ptr不同,不同的unique_ptr之间不能“共享”指针指向的内存,当一个unique_ptr被销毁时,unique_ptr所指向的对象也会一并销毁。unique_ptr对象只能通过实例化unique_ptr类得到,没有类似make_shared的方法。

unique_ptr可以默认初始化指向一个空指针,或者使用new返回的指针直接初始化,除此之外,没有其他的初始化方式了。unique_ptr没有拷贝构造和赋值运算的功能。

虽然不能拷贝或者赋值unique_ptr,但是可以通过unique_ptr的release或者reset方法来转移unique_ptr中指针的所有权。例如:

unique_ptr<string> p2(p1.release());    //p1所指对象所有权转移到p2,p1置空
p4.reset(p3.release());      //p3所指对象所有权转移到p4,p4原对象释放

weak_ptr

weak_ptr是一种不控制所指对象生存周期的智能指针,也即当weak_ptr析构时,它不会销毁所指对象及释放所指对象的内存。

动态数组

在C++中,new和delete支持一次性分配或释放多个对象的动态内存。对比C语言,可以发现C语言是没有动态分配数组这一说的,C语言是典型的面向过程的编程语言。在动态分配时,C语言解决问题的思路过程是:我们需要申请多少个字节的内存;而C++则是我们需要申请构造一个动态对象还是一组动态对象,并没有C语言中直接意义上的多少字节内存。

C++中动态数组的分配方式是:在欲申请的对象类型后"[ ]"中来指明需要对象的个数;在欲释放的动态数组的指针前面使用"[ ]"来告知delete释放整个数组。例如:

int *p = new int[];    //申请动态数组
delete [] p;    //释放动态数组

在动态分配数组时,和静态数组不同,动态数组的元素数量可以是个变量,但是该变量的类型必须是整形。

除了直接使用"[ ]"来分配数组,我们也可以类型别名来用于new表达式分配动态数组,例如:

using int_Arr1 = int [];
typedef int int_Arr2[]; auto p1 = new int_Arr1; //p1指向含有10个元素的动态数组
auto p2 = new int_Arr2; //p2指向含有15个元素的动态数组

对于动态分配得到的数组,new返回的是一个单纯的指针类型,而不是数组类型,回忆第三章关于数组的内容,我们知道数组也是一种类型,是一种复合的复杂类型,其类型包括其中元素类型和元素个数。由于对于数组类型来说其类型由元素的个数和类型决定,因此我们可以使用sizeof对数组进行大小计算,也可以使用新的range-for形式for循环来遍历数组,当然也可以使用标准库的begin和end函数来取得其迭代器。但是new [ ]分配得到的数组却不是一个数组类型,而是一个指针类型,因此该动态数组不能像静态数组那样进行迭代器获取行为。

动态数组中的元素默认也是默认初始化的,我们可以进行值初始化,如下:

int *p1 = new int[]();
int *p2 = new int[] {}; //C++11

我们也可以使用C++11的列表初始化,如下:

int *p = new int[] {, , , , };    //前5个元素使用给定值初始化,其余元素值初始化

如果我们初始化动态数组时,提供的元素个数多于数组最大容纳个数,则new分配失败,并抛出一个异常。

关于动态数组分配的最后一个问题是:如果动态分配的数组元素个数是0,那么new的行为是什么?答案是new正常返回一个非空指针,但不能对该指针解引用,该指针是一个尾后指针。

释放动态数组的语法前面已经做出说明,现在考虑一下动态数组元素销毁的次序。对于动态数组释放,元素销毁的次序是逆序的,即先销毁最后一个元素,然后是倒数第二个,以此类推。

我们在释放动态数组时,必须在指针名前使用"[ ]"以告知编译器销毁的是一个动态数组,而不是单一对象,如果忘记使用这个方括号,那么行为是未定义的,可能造成内存泄露,或者程序崩溃。

令我们惊讶的是,标准库还提供了一个用于管理动态数组的智能指针,该智能指针是unique_ptr。为了使用unique_ptr,我们需要提供一个类型参数供模板实例化一个具体类。我们提供的用于管理动态数组的类型参数是int [ ],如下:

unique_ptr<int []> up(new int[]);
up.release()    //自动使用delete []来释放数组

和普通的unique_ptr不同,指向动态数组版本的unique_ptr不支持点和箭头运算符,因为这些解引用操作无意义。另一方面,我们的unique_ptr是指向动态数组的,所以我们可以使用下标索引("[ ]")来访问数组中的元素。

因为unique_ptr同一时刻只能有一个对象拥有动态数组,如果我们需要在多个对象间共享需要怎么做?我们可以使用sharded_ptr,但是我们必须提供一个删除工具,来替代默认delete操作。如下:

shared_ptr<int> sp(new int[], [](int *p)
{
delete [] p;
});

上面我们提供了一个lambda工具给shared_ptr,使得shared_ptr能够在没有对象使用动态数组时,使用该lambda正确释放动态数组。

由于shared_ptr不支持动态数组管理,因此也就没有提供下标运算符,所以如果我们需要访问动态数组,只能使用shared_ptr中提供的get方法,通过get得到指针之后再继续我们的访问。

allocator类

对程序效率要求极高时,new的行为可能会造成一些局限。new运算符在分配动态内存时,无论如何都会对动态对象进行初始化,也即内存的申请和对象的初始化是组合在一起的。如果某些时候需要申请很大空间的动态数组,那么数组中元素的初始化可能就不那么有意义或者说有些开销浪费,因为我们需要在使用内存的时候自己去构造对象。而且对于没有默认构造函数的类来说,我们还必须列表初始化所有的元素。

标准库allocator类提供了一些方法,能使得动态内存的分配和对象初始化分离开来,它存在于头文件中,如下:

#include <memory>

allocator类分配的内存是原始未构造的。

类似vector,allocator也是一个模板,需要我们提供类型参数以便allocator类来分配相应类型的内存。allocator类会根据类型参数自动的确定内存的对齐方式。

allocator类的使用示例如下:

allocator<int> ai;        //实例化allocator类对象
int *p = ai.allocate(); //使用类对象的方法来申请动态内存
int *q = p; ai.construct(p, ); //对申请内存中第一个元素进行构造
cout << *p << endl; //输出被构造的对象的值
++q; //移动到第二个未构造的元素
cout << *q++ << endl; //未定义的行为,该内存处无对象

练习12.1:在此代码的结尾,b1 和 b2 各包含多少个元素?

StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
}

b2被销毁,b1包含4个元素。

练习12.2:编写你自己的StrBlob 类,包含const 版本的 front 和 back。

const string& StrBlob::front() const
{
check(, "front on empty StrBlob");
return data->front();
} const string& StrBlob::back() const { check(, "back on empty StrBlob");
return data->front();
}

练习12.3:StrBlob 需要const 版本的push_back 和 pop_back吗?如果需要,添加进去。否则,解释为什么不需要。

不需要,因为push_back和poo_back是写操作,不能使用const。

练习12.4:在我们的 check 函数中,没有检查 i 是否大于0。为什么可以忽略这个检查?

因为size_type是无符号类型,一定大于等于0。

练习12.5:我们未编写接受一个 initializer_list explicit 参数的构造函数。讨论这个设计策略的优点和缺点。

练习12.6:编写函数,返回一个动态分配的 int 的vector。将此vector 传递给另一个函数,这个函数读取标准输入,将读入的值保存在 vector 元素中。再将vector传递给另一个函数,打印读入的值。记得在恰当的时刻delete vector。

#include <iostream>
#include <vector> std::vector<int>* get_vector()
{
auto p = new std::vector<int>;
return p;
} void use_vector(std::istream &in, std::vector<int> *p)
{
int i;
while (in >> i)
{
p->push_back(i);
}
} void print_vector(std::vector<int> *p)
{
for (auto b = p->begin(), e = p->end(); b != e; ++b)
{
std::cout << *b << '\t';
}
} int main()
{
auto p = get_vector();
use_vector(std::cin, p);
print_vector(p);
delete p;
std::cout << std::endl;
return ;
}

练习12.7:重做上一题,这次使用 shared_ptr 而不是内置指针。

#include <iostream>
#include <memory>
#include <vector> std::shared_ptr<std::vector<int>> get_vector()
{
return std::make_shared<std::vector<int>>();
} void use_vector(std::istream &in, std::shared_ptr<std::vector<int>> p)
{
int i;
while (in >> i)
{
p->push_back(i);
}
} void print_vector(std::shared_ptr<std::vector<int>> p)
{
for (auto b = p->begin(), e = p->end(); b != e; ++b)
{
std::cout << *b << '\t';
}
} int main()
{
auto p = get_vector();
use_vector(std::cin, p);
print_vector(p);
std::cout << std::endl;
return ;
}

练习12.8:下面的函数是否有错误?如果有,解释错误原因。

bool b()
{
int* p = new int;
// ...
return p;
}

有错误,函数返回值是bool,指针无法转换到bool

练习12.9:解释下面代码执行的结果。

int *q = new int(), *r = new int();
r = q;
auto q2 = make_shared<int>(), r2 = make_shared<int>();
r2 = q2;

q赋值给r,r所指的内存没有释放导致内存泄露。q2赋值给r2,r2的引用计数减1后为0,自动销毁。

练习12.10:下面的代码调用了第413页中定义的process 函数,解释此调用是否正确。如果不正确,应如何修改?

shared_ptr<int> p(new int());
process(shared_ptr<int>(p));

正确。用p初始化了一个临时的智能指针,该操作递增了引用计数,当临时对象被销毁时,智能指针的计数恢复但不为0。

练习12.11:如果我们像下面这样调用 process,会发生什么?

process(shared_ptr<int>(p.get()));
将会导致智能指针所指的内存被释放2次。

练习12.12:p 和 q 的定义如下,对于接下来的对 process 的每个调用,如果合法,解释它做了什么,如果不合法,解释错误原因:

auto p = new int();
auto sp = make_shared<int>();
(a) process(sp);
(b) process(new int());
(c) process(p);
(d) process(shared_ptr<int>(p));

(a)正确
(b)错误,不允许隐式转换
(c)错误,类型不匹配
(d)错误,可能会导致p所指内存被释放2次

练习12.13:如果执行下面的代码,会发生什么?

auto sp = make_shared<int>();
auto p = sp.get();
delete p;

sp所指内存被释放2次

练习12.14:编写你自己版本的用 shared_ptr 管理 connection 的函数。

void end_connection(connection *p)
{
disconnect(*p);
} void f(destination &d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
}

练习12.15:重写第一题的程序,用 lambda (参见10.3.2节,第346页)代替end_connection 函数。

void f(destination &d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, [](connection * p)
{
disconnect (*p);
});
}

练习12.16:如果你试图拷贝或赋值 unique_ptr,编译器并不总是能给出易于理解的错误信息。编写包含这种错误的程序,观察编译器如何诊断这种错误。

note: 'unique_ptr' has been explicitly marked deleted here
unique_ptr(const unique_ptr&) = delete;

练习12.17:下面的 unique_ptr 声明中,哪些是合法的,哪些可能导致后续的程序错误?解释每个错误的问题在哪里。

int ix = 1024, *pi = &ix, *pi2 = new int(2048);
typedef unique_ptr<int> IntP;
(a) IntP p0(ix);
(b) IntP p1(pi);
(c) IntP p2(pi2);
(d) IntP p3(&ix);
(e) IntP p4(new int(2048));
(f) IntP p5(p2.get());

(a)错误,不能用int来初始化unique_ptr
(b)错误,unique_ptr必须使用动态分配的内存来初始化
(c)正确。
(d)错误,unique_ptr必须使用动态分配的内存来初始化
(e)正确
(f)错误,多个unique_ptr指向同一块内存。

练习12.18:shared_ptr 为什么没有 release 成员?
release 成员的作用是放弃对指针控制权,一个对象只能被一个unique_ptr所拥有,而shared_ptr则是多个指向同一个对象,因此不需要 release 成员。

练习12.19:定义你自己版本的 StrBlobPtr,更新 StrBlob 类,加入恰当的 friend 声明以及 begin 和 end 成员。

#include <iostream>
#include <vector>
#include <memory> using namespace std; class StrBlob; class StrBlobPtr
{
public:
StrBlobPtr();
StrBlobPtr(StrBlob &a, size_t sz);
std::string& deref() const;
StrBlobPtr& incr(); private:
shared_ptr<vector<string>> check(std::size_t, const std::string&) const;
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr;
}; StrBlobPtr::StrBlobPtr(): curr()
{ } shared_ptr<vector<string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock();
if (!ret)
{
throw std::runtime_error("unbound StrBlobPtr");
}
if (i >= ret->size())
{
throw std::out_of_range(msg);
}
return ret;
} std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
} StrBlobPtr& StrBlobPtr::incr()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
} class StrBlob
{
friend class StrBlobPtr;
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
StrBlobPtr begin();
StrBlobPtr end();
size_type size() const
{
return data->size();
}
bool empty() const
{
return data->empty();
}
void push_back(const std::string &t)
{
data->push_back(t);
}
void pop_back();
std::string& front();
std::string& back(); private:
std::shared_ptr<std::vector<std::string>> data;
void check(size_type i, const std::string &msg) const;
}; StrBlobPtr::StrBlobPtr(StrBlob &a, size_t sz = ): wptr(a.data), curr(sz)
{ } StrBlobPtr StrBlob::begin()
{
return StrBlobPtr(*this);
} StrBlobPtr StrBlob::end()
{
auto ret = StrBlobPtr(*this, data->size());
return ret;
} int main()
{ std::cout << std::endl;
return ;
}

练习12.20:编写程序,逐行读入一个输入文件,将内容存入一个 StrBlob 中,用一个 StrBlobPtr 打印出 StrBlob 中的每个元素。

练习12.21:也可以这样编写 StrBlobPtr 的 deref 成员:

std::string& deref() const {
return (*check(curr, "dereference past end"))[curr];
}
你认为哪个版本更好?为什么?

原版更易读。

练习12.22:为了能让 StrBlobPtr 使用 const StrBlob,你觉得应该如何修改?定义一个名为ConstStrBlobPtr 的类,使其能够指向 const StrBlob。

重载构造函数接受const Strblob &形参, 然后给 Strblob 类添加两个 const 成员函数 cbegin 和 cend,返回 ConstStrBlobPtr。

练习12.23:编写一个程序,连接两个字符串字面常量,将结果保存在一个动态分配的char数组中。重写这个程序,连接两个标准库string对象。

#include <iostream>
#include <cstring> using namespace std; int main()
{
char *p = new char[strlen("hello world") + ];
strcat(p, "hello ");
strcat(p, "world");
cout << p << endl;
delete [] p; string s1("hello "), s2("world");
cout << s1 + s2 << endl;
return ;
}

练习12.24:编写一个程序,从标准输入读取一个字符串,存入一个动态分配的字符数组中。描述你的程序如何处理变长输入。测试你的程序,输入一个超出你分配的数组长度的字符串。

#include <iostream>

int main()
{
char *p = new char[];
std::cout << "input: ";
std::string s;
std::cin >> s;
if (s.size() < )
{
for (int i = ; i < ; ++i)
{
p[i] = s[i];
}
} delete [] p; return ;
}

练习12.25:给定下面的new表达式,你应该如何释放pa?

int *pa = new int[];
delete [] pa;