4.动态内存
为了最大化运行速度,通过将对象存放在栈中或静态存储区域中,存储和生命期可以在编写程序时确定。
栈是内存中的一个区域,可以直接由微处理器在程序执行期间存放数据。在栈中的变量有时称为自动变量(automatic variable)或局部变量(scoped variable)。
静态存储区域简单说是内存的一个固定块,在程序开始执行以前分配。使用栈或静态存储区,可以快速分配和释放,有时这是有价值的。
然而,我们牺牲了灵活性,因为程序员必须在写程序时知道对象的准确数量、生命期和类型。
如果程序员正在解决一个更一般的问题,例如计算机辅助设计、仓库管理或者空
中交通控制,这就太受限制了。
第二种方法是在称为堆(heap)的区域动态创建对象。
用这种方法,可以直到运行时还不知道需要多少个对象,它们的生命期是什么和它们的准确的数据类型是什么。
这些决定是在程序运行之中作出的。如果需要新的对象,直接使用new关键字让它在堆上生成。
当使用结束时,用关键字delete释放。
因为这种存储是在运行时动态管理的,所以在堆上分配存储所需要的时间比在栈上创建存储的时间长得多(在栈上创建存储常常只是一条向下移动栈指针的微处理器指令,另外一条是移回指令)。
动态方法做出了一般性的逻辑假设,即对象趋向于更加复杂,所以,为找出存储和释放这个存储的额外开销对于对象的创建没有重要的影响。
另外,对于解决一般性的程序设计问题,最大的灵活性是主要的。
另一个问题是对象的生命期。如果在栈上或在静态存储上创建一个对象,编译器决定这个对象持续多长时间并能自动销毁它。
然而,如果在堆上创建它,编译器则不知道它的生命期。
在C++中,程序员必须编程决定何时销毁此对象。然后使用delete关键字执行这个销毁任务。
作为一个替换,运行环境可以提供一个称为垃圾收集器(garbage collector)的功能,当一个对象不再被使用时此功能可以自动发现并销毁这个对象。
当然,使用垃圾收集器编写程序是非常方便的,但是它需要所有应用软件能忍受垃圾收集器的存在及垃圾收集的系统开销。
这并不符合C++语言的设计需要,因此C++没有包括它,尽管存在用于C++的第三方垃圾收集器。
前面我们讲到指针,上面的例子中指针都是初始化为变量的地址。
而变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。 这有什么用呢?
指针的真正用武之地在于在运行阶段分配未命名的内存以存储值,在这种情况下,只能通过指针来访问内存。
在C语言中,可以用库函数malloc()来分配内存。在C++中仍然可以这样做,但C++还有更好的方法-new运算符。
下面在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。
这里的关键所在是C++的new运算符。程序员要告诉new,需要为哪种数据类型分配内存:new将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋值给一个指针:
int *pn = new int;
*pn = 1001;
对于指针,需要指出的另一点是,new分配的内存块通常与常规变量声明的内存块不同。
变量pn的值都存储在被称为栈(stack)的内存中,而new从被称为堆(heap)或自由存储区(free store)的内存区域分配内存。
计算机可能会由于没有足够的内存而无法满足new的请求。在这种情况下,new通常会引发异常。而在较老的实现中,new将返回0。在C++中,值为0的指针被称为空指针(null pointer)。
有些程序员使用0表示空指针,有些程序员使用NULL,这是一个表示空指针的C语言宏,在C++11中提供了更好的解决方案:引入新关键字nullptr,用于表示空指针。
C++确保空指针不会指向有效的数据,因此它常被用于表示运算符或函数失败。
delete释放内存
当需要内存时,可以使用new来请求,在使用完内存后,需要使用delete运算符将其归还给内存池:
int* ps = new int;
...
delete ps;
int *p2 = new int[10];
delete[] p2; // 数组的delete必须带[]
使用new来创建动态数据
如果程序只需要一个值,则可能会声明一个简单的变量,因为对于管理一个小型数据对象来说,这样能比使用new和指针更简单,尽管给人留下的印象不那么深刻。
通常,对于大型数据(如数组、字符串和结构),应使用new,这正是new的用武之地。
例如,编写一个程序,它是否需要数组取决于运行时用户提供的信息。如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存。
在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。
但使用new时,如果在运行阶段需要数组,则创建它。如果不需要则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic array)。
使用静态联编时,必须在编写程序时指定数组的长度。
使用动态联编时,程序将在运行时确定数组的长度。
使用new和delete管理动态内存存在三个常见问题:
- 忘记delete内存。忘记释放动态内存会导致内存泄漏问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄漏错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
- 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
- 同一块内存释放两次。当有两个指针指向相同的内存分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了,如果我们随后又delete第二个指针,自由空间就可能被破坏。
坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会去自动释放它。
为了更容易(同时也更安全)的使用动态内存,C++标准库提供了智能指针(smart pointer)类型来管理动态对象。 智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。 C++标准库提供了四种指针来管理底层指针的方式:
- std::shared_ptr: 共享式指针,多个指针指向同一个对象,最后一个指针被销毁时,这个对象就会被释放。shared_ptr的工作机制是使用引用计数,每一个shared_ptr指向相同的对象(内存),所以很想然,只有最后一个指向该对象的shared_ptr指针不需要再指向该对象时,这个shared_ptr才会去析构所指向的对象。
- std::unique_ptr:独占式指针,同一个时间内只有一个指针能够指向该对象,当然,该对象的拥有权是可以移交出去的。
- std::auto_ptr:C++98中提出的,目前已经完全被std::unique_ptr取代。
- std::weak_ptr:它是一种弱引用,指向shared_ptr所管理的对象。弱引用可以理解成是监视shared_ptr(强引用)的生命周期用的,是一种对shared_ptr的扩充,不是一种独立的智能指针,不能用来操作所指向的资源,所以它看起来像是一个shared_ptr的助手。所以它的智能也就智能在能够监视到它所指向的对象是否存在了。
weak_ptr不会产生强引用,因此可以很好的解决互相引用的问题.
作为智能指针,程序员不用担心内存的释放问题,即便忘记了delete,系统也能够帮助程序员delete,这是智能指针的本职工作。
shared_ptr<string> p1; // shared_ptr可以指向string
shared_ptr<list<int>> p2; // shared_ptr可以指向int的list
// 如果p1不为空,检查它是否指向一个空string
if (p1 && p1 -> empty()) {
*p1 = "hi"; // 如果p指向一个空string,解引用p1,赋值hi
}
默认初始化的智能指针中保存着一个空指针。 智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。
当要用make_shared时,必须指定想要创建的内存的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:
// 指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一个值为“9999”的string
shared_ptr<string> p4 = make_shared<string>(4, "9");
// p5指向一个值初始化的int,即值为0
shared_ptr<int> p5 = make_shared<int>();
我们通常用auto定义一个对象来保存make_shared的结果,这种方式较为简单:
// p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();
auto q(p6); // q和p6指向相同对象,此对象有两个引用者
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。
不论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会增加。
当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。
一旦一个shared_ptr的计数器变成0,它就会自动释放自己所管理的对象。
智能指针的选择
- 如果程序中要使用多个指向同一个对象的指针,应选择shared_ptr。
- 如果程序中不需要多个指向同一个对象的指针,则可使用unique_ptr。
总之,在选择的时候,优先考虑使用unique_ptr,如果unique_ptr不能满足需求,再考虑使用shared_ptr。
函数的值传递
C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋值给一个新的变量。例如:
double side = 2.333;
double volume = cube(side);
double cube(double x); // cube函数的头
被调用cube()时,该函数会创建一个新的名为x的double变量,并将其初始值为2.333.这样,cube()执行的操作将不会影响main()中的数据,因为cube()使用的是side的副本,而不是原来的数据。
用于接受传递值的变量被称为形参。
传递给函数的值被称为实参。
在函数中声明的变量(包括参数)是该函数私有的。在函数被调用时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些变量使用的内存。这样的变量称为局部变量,因为他们被限制在函数中。
malloc/free vs new/delete
有过C语言学习经验的读者应该知道,malloc和free是C语言的标准库函数。前面介绍的new和delete是C++的运算符,这两种形式都可用于申请动态内存和释放内存。
既然库函数malloc()和free()可以实现动态内存的管理,那为什么还要new和delete运算符呢?
这是因为对于非内部数据类型的对象而言,只用库函数malloc()和free()无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc()和free()是库函数而不是运算符,它们不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc和free函数。
因此,C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
- malloc返回一个
void *指针,c++不允许将void *赋值给其他任何指针,必须强转。 - malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
- 重要: malloc/free不会调用构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。
- new/delete不是C++的库函数,而是运算符,而malloc/free才是C和C++的标准库函数。
类型转换
const Person *p1 = new Person();
Person *p2 = const_cast<Person *>(p1);
Person *p3 = (Person *)p1;
c风格类型转换
- (type)expression
- type(expression)
C++风格类型转换
- const_cast: 用于const类型的指针转换为非const类型
- dynamic_cast:用于多态类型的转换,有运行时安全检测
- static_cast:相对于dynamic_cast,缺乏运行时安全检测,用于基本数据类型的转换,把非const转成const。
- reinterpret_cast:属于比较底层的强制转换,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝。
STL
STL(Standard Template Library,标准模板库)是惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室所开发出来的。现在虽说它主要出现在C++中,但在被引入C++之前,该技术就已经存在了很长一段时间。STL的目的是标准化组件,用户不用重新开发它们,而可以使用这些现成的组件。STL现在是C++的一部分,因此不用额外安装什么,其被内建在编译器之内。
在C++标准中,STL被组织为13个头文件:
- algorithm
- deque
- functional
- iterator
- vector
- list
- map
- memory
- numeric
- queue
- set
- stack
- utility
- 邮箱 :charon.chui@gmail.com
- Good Luck!