类的二三事

类 

类的基本思想就是数据抽象和封装。数据抽象依赖于接口和实现。接口指类外成员对象可使用的函数接口。实现指类的成员函数和成员变量。封装实现了接口和实现的分离。
类本身就是一个作用域。类外访问,可以通过作用域的限定符和成员访问符来进行访问。能否访问类的成员依赖于,该成员被什么访问限定符修饰。

访问限定符:

(1) public:它指公用,被它修饰的类成员在类外可以访问。
(2)protected:它指拥有。被它修饰的类成员只能在类内访问。
(3)private:它指私有。被它修饰的类成员只能在类内访问。(它与第二个的不同体现在继承方面)

类的实例化

  指在通过类的构造函数,创建出来的实际对象。当使用类名定义对象时,编译器先识别类型,再识别类成员变量的声明,最后在调用构造函数进行初始化。
空类的大小

空类的大小为1。单继承的空类大小也为1。(空白基类最优化,当基类的地址与派生类地址一样使发生优化)

当多继承空类时,除过第一个基类大小不算,派生类大小为除第一个外的空类数量(因为每个空类大小为1)加上派生类成员变量大小内存对齐之后的大小为该派生类大小。

this 指针

当一个类对象被初始化后,this指针就被构造出来了(这里指当对象构造完成后,this指针才存在)。this指针中保存的是该变量的地址。在类函数中访问类成员变量实际是通过this指针来访问的。一般编译器给this指针传参数有2种方式,当该类函数为固定参数时,遵守_thiscall调用约定,通过ecx寄存器把对象地址传给this指针。当该类函数为可变参数时,遵守_cdecl调用约定,编译器直接把对象地址压栈到this指针中。this指针是每个类函数的隐藏参数,它也是类函数的第一个参数,也是最后一个被传参的参数。

类的默认函数

默认函数

一个类实例化对象时,是通过调用它的构造函数来实例化的。当我们自定义一个类时,如果你没有写构造函数,析构函数,拷贝构造函数,赋值运算符重载函数,取地址运算符重载和const修饰的取地址运算符重载函数时编译器会为你默认合成它们,当它们在类外被调用时。

类的构造函数

初始化
通常当我们实例化一个类的对象时。编译器会调构造函数为我们初始化对象。

类的初始化,何为初始化,就是在内存这该变量分配内存的同时,将该变量内容初始为某个值。(该值可以是随机的,也可以是你自己给出的)。 那么我们在调类函数时,各个变量已经都被初始化过了。类的构造函数也不列外,所以在构造函数内对成员变量  = 某个值,它不叫做初始化,它叫做赋值。故我们只能在构造函数的初始化列表内对成员变量初始化且初始化顺序为成员变量的声明顺序。
必须初始化的类成员变量

(1)引用类型 和 (2)const变量类型 和 (3)没有默认构造函数或者构造函数没有缺省参数的类对象。 

那么为什么呢?

我们可以回忆下为什么,引用必须初始化这是常识(因为通过赋值并不能修改它原先内容),在C++中const修饰变量时必须初始化,const把变量变为常量。又因为 在类中初始化只能发送在 初始化列表中故,这俩种情况必须在成员列表初始化。至于第三个如不初始化当前对象的成员变量不能构造成功,那么当前对象也一定不能构造成功故必须得初始化。

构造函数被private修饰:

当构造函数被private修饰时在类外是无法构造对象的。除非我们定义个类的静态函数。在静态函数内我们可以构造一个对象并把这个对象返回出去。并且当我们在这个静态函数里增加引用计数,只在第一次进来时构造成功,否则抛出异常。(单列模式)

类的析构函数

1.当我们在函数体内定义一个变量时,它通常在栈上,如main函数体内定义一个类的对象。它的生命周期就在main函数内,当main函数调用结束后,它的生命周期结束,编译器就会掉析构函数,释放该对象内的空间。那么当我们定义多个对象时,编译器会根据生命周期的长短来释放,它会先释放后定义出来的对象,先定义出来的后释放。

2.另外有个需要注意的地方:在实现多态时,我们需要把基类的析构函数声明为虚函数,否则通过new的方式构造出来的派生类对象以一个基类的指针引用时。当我们在使用完在堆上的该对象时,为了防止内存泄漏要delete这个base类指针。这时如果基类的析构函数不是虚函数,那么派生类就没有对它重写,这里直接调用基类自己的虚函数。这时delete做俩件事,首先调用基类析构函数,在调完其析构函数后再通过调用operator delete来释放指针所指向的空间。但它调用的是基类的析构函数,这样就发生了部分派生类对象被释放掉,在派生类地址到该基类地址之间的空间没有被释放掉。(当delete对象是一个指向类对象的指针时,都会先调用其析构函数,因为以防该类对象中有资源管理的问题,列如该类对象中有指针在堆上申请了空间,然后再通过operator delete再释放类对象指针所指向的空间)。

3.但是有个如果在抽象类中,如果我们把抽象类的析构函数声明成纯函数会发生什么?

因为该类是个抽象类,所以该类虚函数应该以接口的形式给出,都声明为纯虚函数,但是析构函数也是纯虚函数的话会出问题,因为编译器在调用一个派生类的构造函数或虚函数时,会对该派生构造函数或析构函数进行扩张。其扩张规则为在构造函数中,按类继承的顺序,在派生类中以该顺序依次静态调用它们的构造函数最后再执行派生类构造函数主体。在析构函数中也会扩张,先执行派生类的析构函数,然后按照类继承的逆序,在派生类析构函数中以该顺序依次静态调用它们的析构函数。如果我们把该析构函数写为纯虚函数,在编译期没问题,但是在链接期就会出错,找不到该析构函数主体,链接失败。所以抽象类的析构函数还是以虚函数给出并给出个定义,可以什么都不做。故我们在抽象类中要么不写它的析构函数,如果写了一定不可以声明为纯虚函数。

4如果不是作为一个多态用处的基类,就不能把其基类析构函数声明为虚函数,因为这样做会多一个虚表指针,多一个指针对象在32位下多4字节,64位下多8字节,这样的代码不具有移植性。

类的拷贝构造函数和 赋值运算符重载

拷贝构造函数

参数:const型的自身类型的引用。当我们写拷贝构造函数时,它的参数为 const 型的自身变量的一个引用。因为当我们把一个对象传参时,编译器会生成一个临时变量,以临时变量来给函数使用,但是这个临时对象是通过调用该对象的拷贝构造函数生成的,那么有因为拷贝构造函数参数又是一个临时对象时,又会再次调用拷贝构造函数,这样会形成无限的递归,故我们使用引用,又因为在调拷贝构造时,我们不希望它改变类外的对象的内容故声明为const,所以拷贝构造函数参数为const型的一个自身类型的引用。
注意的点:当在继承的环境下,我们写拷贝构造函数时,在初始化列表中也应给继承下来基类成员初始化,一般我们直接在初始化列表中调用基类的拷贝构造即可,Base(d),d为传来的形参。Base为基类类型。如果没有调用的话,编译器默认调基类的默认构造函数可能发生出乎意料的结果。

赋值运算符重载

参数:const型自身类型引用。
返还值类型:自身类型的引用。
注意的点:在继承时,保住基类那部分也被赋值了,否则可能为其他值。对于拷贝构造和赋值运算符重载,如果你希望类外不能copy,那么我们可以 定义一个基类,在基类中 把 它的 这俩个函数只声明并用private修饰,我们用我们使用个这个类来继承我们写的这个Base空类且我们不写俩个函数。那么当我们在类外 copy 我们使用的这个类时,编译器就在编译期间报错,当编译器为我们合成这俩个默认函数时会尝试调用基类的copy函数,又因为它们是被隐藏的,故编译时就报错。
                 在对运算符重载时,还有写const 修饰的相应的运算符重载函数,如果不写const对象将没有对应的运算符重载函数。造成这个原因是底层const不能被忽略,用const修饰成员函数时,相当于给this指针加上了底层const。

类中的成员函数

        
         有时候当我们写一个类函数时,在类函数中使用类中的成员变量。但这些成员变量我们把它们的声明写到了类的最后面,为什么类中还是可以识别它们,编译器不会报错。这是因为编译器在对类函数进行语法分析时,它总是把类成员函数内部的变量绑定发生在类定义完毕后,也就是当一个类写完时编译器才会对该成员函数分析,那么这样就解释了为什么我们可以把类成员变量声明写到类最后,成员函数中使用它,编译器不报错。
       那么对于整个函数来说这样的事情只发生在函数内部,对于函数的形参列表来说,变量的绑定是发生在该名称第一次被写入形参列表时,也就是说如果我们有俩种不同数据类型,在全局中有一个,在类中有一个,那么形参会绑定全局的类型,这也就是为什么很多人,在使用某个typedef的数据类型时,总是把这个数据类型放在类的顶部。
       还有一个大家经常疑问的地方,那么类中也保存了函数,为什么函数却不占用实际对象的内存大小呢?
       对于它,我们可以联想到static变量在类中也是不占用每个对象的内存大小的,但是大家都可以使用它,static存放在静态全局区,那么相应的每个对象也共享了一套函数供他们使用,那这些函数存放在相应的代码段。
       那有些人说这样来这不违背了类的封装性吗?我只要在代码段找到这些函数的地址,把它赋值给相应的函数指针,在外部我不就可以不通过类对象调用它们了吗?但是这是不可能的不通过类对象调用类成员函数,因为每个类成员函数都有一个隐含的形参,那就是该类对象的一个指针,通常我们把它称为this。试想不相应的参数,我们是不可能成功调用那个函数的。所以我们得传入一个对象的地址进去,那么我们还是通过类对象去调用的它,所以这很符合类的封装性。
       还有部分人有疑问类中函数是默认为inline函数,inline函数为什么能发生在编译期?
       因为我们知道函数调用(特殊函数除外,列如宏函数和初始化main资源的这些函数)都是发生在链接期,那么我们如何在编译期调用这些inline函数呢。通常编译期时,编译器会通过copy 该函数代码段的代码,把该函数移致在调用处,供其调用。这是不是很聪明的一种做法。当然这样只能发生在函数主体没有大量的递归和迭代,只有函数体比较简单的一些类函数,编译器才把它当成inline函数处理。
     

类成员函数的特点:
用指针使用成员访问符调用成员函数时(pa->fun()),底层并没有对其先指针解引用再调其成员函数而是通过A::Fun(),直接用类名加作用域的方式调用成员函数。

类的一些其他需要注意的地方:

1当在俩个不同源文件中定义俩个类,在每个类中成员变量又是另一个类的成员变量时,我们用定义该类指针的方式作为成员变量。

static 与 const 修饰类成员变量与成员函数

const 修饰成员变量与成员函数
const 修饰成员变量必须初始化,这个跟修饰普通变量一样
const 修饰成员函数,其实相当于修饰的是this 指针。
void Fun(T * const this)  变为 void Fun(const T * const this)
也就是说,经过const 修饰的成员函数,在其中不能修改成员变量。因为this指针指向的对象不能被修改。
class A 
{
  void Fun()const 
  {
  }
};


static 修饰成员变量与成员函数
static 修饰成员变量与成员函数后,访问成员变量与成员函数可以通过, 类::成员函数 或 类::成员变量来访问静态的成员函数或者成员变量
static 修饰成员变量
static 修饰成员变量必须在类外初始化。模板类的静态成员变量初始化如下
template<class T>
class A
{
  static T * p;
}
template<class T> //方法1
T * A<T>::p = NULL;
template<> //方法2 用模板特化来初始化
char * A<char>::p = NULL;
static 修饰成员函数
经过static 修饰的成员函数,成员函数内部不能使用 非静态的成员函数与变量。
因为由static 修饰后,相当于成员函数内部没有了this指针。并不允许使用this指针。
但是 非静态的成员函数 却可以 访问静态成员变量与成员函数。
class A
{
  public:
   void Fun2()
   {
     Fun();
   }
   static Fun()
   {
   }
};



猜你喜欢

转载自blog.csdn.net/sdoyuxuan/article/details/69662256