C++ 学习笔记之---类和动态内存分配
参考自《C++ Primer Plus 6th Edition》
程序对内存的使用:
链接:http://zhidao.baidu.com/link?url=An7QXTHSZF7zN9rAuY05mvaHHar0xIpgK6Yqp9oAkm2GmZYoTAz9UpN4JuhWJvSLsbu0-lOcO47PzXcNWda6gK
1. 栈区 (stack) - 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。
2. 堆区 (heap) - 在内存开辟另一块存储区域。一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3. 全局区 (静态区) (static) - 编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的。初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。- 程序结束后由系统释放。
4. 文字常量区 - 常量字符串就是放在这里的。程序结束后由系统释放
5. 程序代码区-存放函数体的二进制代码。
定义静态成员变量:
可以在类声明中定义静态成员变量,使用 static 修饰。不过,虽说是成员变量,但是不属于这个类的任何一个对象。它们是是分开存储的。
因为对所有对象,这个变量的值都是一样的,存储上也只用存一份就好。访问的时候,使用 "className::varName" 即可。绝大多数语言中可以定义静态变量,只是法上稍有不同。Java中的静态变量,既可以通过对象来访问,也可以通过类来访问。C++中就只能通过类名来访问。不过,Java通过对象来访问静态变量,实质上是通过类名来访问的。好吧,这个问题无关痛痒。
其次,C++不允许在类声明中初始化静态成员变量。而且初始化的时候要使用作用域运算符,"className::varName"。一种"内部"的感觉。
在类中定义常量:
1. 编译时确定的常量
存储: 对所有对象而言,这个常量都是一样的。因此和对象分开存储,仅保留一份副本。
实现: 1. 枚举: 如 enum {SIZE = 100 }; 这就定义了一个枚举常量 SIZE = 100。
当然,你可以定义多个,并给定类型名。
2. 静态成员变量: 如 const static int a = 5;
3. 用const限定并初始化, 如声明成员 const int id = 5 (C++ 11 拓展)
2. 运行时确定的常量
存储: 不同对象,可以有不同常量,属于对象的普通成员
实现: 声明用const修饰的成员,然后用构造函数的成员初始化列表.
#include <iostream> class Student { public: const int id; Student(int ID) : id(ID) { } }; int main() { Student a(10); // a的id常量为10 Student b(20); // b的id常量为20 std::cout << a.id << " " << b.id << std::endl; }
成员初始化列表的初始化工作,是在对象创建后,构造函数函数体的代码执行前做的。对于内置类型成员的初始化,不管是放在初始化列表中初始化,还是放在函数体中初始化,效率是一样的。不过,对于对象成员来说,使用初始化列表来初始化,效率更高。暂且不提。要注意的一点是: 成员初始化列表只能用于构造函数。
复制构造函数 与 赋值运算符:
函数原型:
copy constructor: className (const className &)
assignment operator: className& operator=(const className &)
当定义的类,有指针成员,且使用new初始化的时候,需要定义"深拷贝"的复制构造函数和赋值运算
符。(暂不考虑定位new,因为常规new申请的内存位于堆中,需要程序员手动delete。而定位new申请的内存地址是自行指定的,如果定在堆,则情况相同。如果定义在静态内存中,那就没我们的事儿了。交给OS 吧)
基本概念:
深拷贝:
将一个对象拷贝给另一个对象的时候,被赋值的对象存储赋值对象的一个额外副本。若类成员中含有指针成员,且用new初始化的时候,被赋值的成员,会申请一块内存,将赋值对象的指针成员所指的内存的内容复制到这块内存中。两个指针各自指向自己申请的内存。
浅拷贝:
和深拷贝相似,浅拷贝对于非指针成员都是直接赋值。但是当类成员中含指针成员,且用new初始化的时候,被赋值的成员指针并不会额外申请一块内存,而仅仅是将自己指向赋值对象的指针成员所指的那块内存。两个指针指向同一块内存。
当我们没有定义类的复制构造函数和赋值运算符时,编译器会生成默认的版本,它们使用浅拷贝。
回到上面所说的,为什么我们需要定义"深拷贝"的复制构造函数和赋值运算法捏 ? 难道,是因为默认的浅拷贝会导致错误 ?
没错! 我们知道,如果定义的类中含指针成员,如果它将会使用new申请新内存。在析构函数中,我们会用delete释放相应的内存占用。
考虑两种情况:
1. 一个对象使用另外一个已有对象初始化,这样将调用默认复制构造函数(有可能还会调用赋值操作符,视编译器而定)。由于使用浅拷贝,就会存在这两个对象的指针成员指向同一块内存的情况,当这两个对象弃用时,会调用它们的析构函数。这样会出现同一块内存被释放两次的情况,出现未知的错误。
类似地,如果你定义了一个返回对象的函数,也会造成同一块内存释放两次的情况,为啥 ? 因为这还将调用复制构造函数,按值传递意味着创建原始变量的一个副本。caller和这个函数(callee)中的对象的指针指向同一块内存。当函数返回的时候,函数中的这个对象要被kill掉,调用析构函数了,释放掉占用的内存... 放心,这些都不会告诉你的。嗯,当caller中的那个对象析构时,那块内存又被释放了一次... 仍然是不可预知的错误。类似地,创建临时对象的时候,也会调用复制构造函数,这将发生同样的趣事--同样的奇怪的错误。
2. 两个已有对象之间的赋值,这将调用默认赋值运算符函数。后面的情况和1相同,都是浅拷贝闹的--两个对象的指针指向同一块内存,然后被释放两次。
啰嗦一句,“当定义的类中含有指针成员,且使用常规new(或定位new,定位在申请的堆内存中)初始化的时候,需定义深拷贝的复制构造函数和赋值操作符”,不然会被外星人抓走。
其他的的内存分配、回收问题
将涉及定位new的使用。(不考虑内存不够用的情况)
. 如果使用定位new运算符,定位在静态内存中,就不必释放了 (交给OS吧)
. 如果先用常规new运算符,申请了一块堆内存。然后,再使用定位new运算符在这块堆内存中为我们的对象申请内存捏 ?
这种情况下,你却不能delete这些对象。因为,对对象指针执行delete操作,不仅会调用析构函数,而后还会回收成员所占用的内存。你如果delete了这个对象,然后又delete那块堆内存,就会造成某些内存被释放两次的情况 (正是原来存放对象成员的内存)。
但是! 也因为你没有delete这些对象,这些对象是不会调用析构函数的。万一调用析构函数是必须的 (比如: 对象中有一个指针成员,该指针成员指向了一块用常规new申请的另外一块堆内存,不调用析构函数,这一块内存不就无法回收了吗 ? 飘渺孤鸿影~ 寂寞开无主~ 又恨又爱的孤岛内存~ )
但是! 解决方法还是有的,我们可以显式调用析构函数啊 ! 像这样: p->~className(); 这样,对象就会调用它的析构函数,且不会回收成员所占的内存了。
一个简单的例子:
#include <iostream> #include <string> #include <new> using namespace std; class Student { private: string name; public: Student(const string& s): name(s) { } ~Student() { cout << name << " destroyed\n"; } }; int main() { double * buffer = new double[512]; Student *s1 = new (buffer) Student("Peter"); Student *s2 = new (buffer + sizeof(Student)) Student("Tom"); /* 下面两条语句将引发错误,后面delete[] buffer, * 导致同一块内存被释放两次*/ //delete s1; //delete s2; /*显式调用析构函数, 这里按栈的顺序了,其实都行,不走寻常路 o_O */ s2->~Student(); s1->~Student(); delete[] buffer; return 0; }