书接上回,我们继续来盘点那些面向对象里我们可能知道但并不熟悉的知识点。
需要明白的知识点
关于堆和栈
栈是存在于某作用域的一块内存空间。比如函数本身会形成一个占来接受各种数据。
堆是由操作系统提供的一块全局的内存空间。
简单来说,栈是由编译器自动管理的,而堆则是需要手动释放空间的。
{
Complex c1(1,2);
Complex* p=new Complex(3);
}
比如这时c1的空间就是来自栈的,在作用域结束之时结束。
而Complex(3)这个临时对象的空间是new出来的,所以来自堆,需要手动delete,不然等到作用域结束后p就无法找到了,出现内存泄漏。
关于new
new的操作在C++中分为两步:先分配内存,再调用构造函数。
比如对于无指针的Complex类
Complex* pc=new Complex(1,2);
编译器会转化为:
void * mem=operator new(sizeof(Complex));
pc=static_cast<Complex*>(mem);
pc->Complex::Complex(1,2);
首先分配内存, 再将类型转换为需要的Complex,最后调用构造函数。
对于有指针的String类,也是一样的。
String* ps=new String("hello");
void * mem=operator new(sizeof(String));
pc=static_cast<String*>(mem);
pc->String::String("hello");
因为构造指针指向hello内存空间的事情已经交给了构造函数。
关于delete
delete的操作和new刚好相反,也分为两步:先调用析构函数在释放内存。
这里就拿String类举例
String*ps=new String("hello");
...
delete ps;
String::~String(ps);//1
operator delete(ps);//2 free
这里需要明白整个流程,因为delete ps其实和ps指针无关。
我们按顺序说明这个流程。
- 首先调用析构,析构是我们写好的delete[]m_data;
- 对于这个delete,又会分成两步。这里要析构掉m_data new出来的部分,清空hello。调用的是系统的析构函数。然后free(m_data),也就是回收指针m_data所指向的空间。
- 然后回到最外层的第二步,operator delete(ps),本质上是free(ps)。现在ps指向m_data。因为m_data是new出来的,所以需要回收其空间,也就是回收ps所指向的空间。
- ps本身不需要回收,它是在栈上的,在作用域结束后就会消失,但需要在消失前处理那些new出来在堆区的部分。
关于array new和array delete
当我们new一个数组类型时,需要使用delete[]配合,而不能使用delete。
String *p = new String[3];
...
delete[] p;
这样会调用三次析构函数,如果使用delete则只会调用一次析构函数,出现错误。
而数组长度是在new时多申请了头部的4个字节存放的,在调用析构的时候就会知晓长度。
那有没有可以不配套使用的情况呢,也是有的。
当类型为int,float等内置类型时,不需要知道长度为多少。可以直接操作内存,就不需要析构函数了。我们在书写时为了保持规范性还是需要配套使用的。
关于static
static成员变量
这里说的static关键字的使用场景是在class中。一般我们定义的class的对象中包含着成员变量。不同的对象的成员变量相互独立,不受影响。
但有时候也有需要数据共享的场景:比如银行类可以定义多个客户,每个客户拥有自己的成员变量。对于银行来说,汇率这个变量就不能定义为对象的成员变量,而应该是属于银行类的变量。
这时候就可以使用static了。static成员变量有几个关键点:
- static成员变量属于类,不属于具体的对象
- 必须在类外进行初始化,不初始化无法使用
- 访问时可以使用对象访问,也可以使用类::变量名直接访问(因为static成员变量不占用对象的内存)
static成员函数
static也可以声明静态成员函数。
-
普通成员函数可以访问所有成员,而静态成员函数只能访问静态成员。
因为在编译时,普通成员函数会隐式增加形参this,把当前对象的地址赋给this。而静态成员函数是全局的,不需要this,也不知道指向哪个对象,所以只可以访问静态成员。
-
调用时可以通过对象来调用,也可以通过类来调用。比如Student::getTotal()
关于template模板
类模板
类模板是比较常见的一种用法,在STL中也被大量使用。
template<typename T>
class complex{
public:
complex(T r=0, T i=0):re(r),im(i){}
...
private:
T re,im;
}
{
complex<double>c1(2.0,1.6);
complex<int>c2(2,1);
}
类模板在调用的时候需要指明类型
函数模板
函数也可以用模板来进行泛化。比如我们有一个石头类,这个类也想使用min函数来比大小,那么min就可以用函数模板来写。
template <class T>
inline
const T& min(const T& a,const T& b)
{
return b<a?b:a;
}
但这个时候石头类是不知道<怎么比大小,所以需要重载符号
class stone
{
public:
stone(int w,int h,int we)
:_w(w),_h(h),_weight(we){}
bool operator< (const stone& rhs) const
{
return _weight<ths._weight;
}
private:
int _w,_h,_weight;
};
实际调用时也不需要声明类型,编译器可以对函数模板进行参数推导
stone r1(2,3),r2(3,3),r3;
r3=min(r1,r2);//r3=min<stone>(r1,r2);