——new和malloc 

~malloc

malloc是从C语言那里继承过来的一个函数,其用于分配一片内存,它的返回结果是一个指向你所需求的内存的指针,其函数原型和使用例子如下:
/*
  函数原型
  其中__size是你要分配的大小,其单位是byte
*/
void* malloc(size_t __size);

// 用例
int* pInt = (int*) malloc(sizeof(int));               // 分配了一个int
double* pDoubleArray = (double*) malloc(sizeof(double) * 5);   // 分配了一个double数组,其大小为5

一般来说,malloc总是能为你分配出内存。但是也存在山穷水尽,内存不够用的情况。这时候malloc会返回一个空指针(NULL, nullptr)。当你使用malloc的时候,你最好每次都要判断一下返回的指针是否为空。

现在内存已经分配出来了。当程序运行到某一些时刻,我又不想要这些内存了。这时候我们就要手动释放内存,否则的话就会造成内存泄露。通过free() 函数来释放内存,其函数原型和使用例子如下:

// 原型
void free(void* __ptr);

// 用例
free(pInt);
free(pDoubleArray);

有趣的是你传给free函数的只是一个指针,然而不管你是分配了一个元素还是分配了一个数组,free总能帮你释放掉这些内存(free 怎么知道你分配的数组的大小?)

    详细的说说malloc在分配内存的时候干了什么。malloc在分配内存的时候,不仅仅会分配你需要的内存大小,它还会在你的内存的头部和尾部加上一些额外的信息(俗称cookie)。比如说用来DEBUG的信息和你的内存的大小。这就解释了为什么能free掉你的内存,因为它知道你这块内存是多大的。值得一提的是这些Cookie会占用一些内存。。。


~new

new是c++提供的一个操纵符(或者说关键字)。其也是用于分配内存的。其用例如下:

int* pInt = new int(3);            // 分配并构造一个int
double* pDoubleArray = new double[5];    // 分配了一个double数组,其大小是5

delete pInt;                   // 删除单元素
delete[] pDoubleArray;             // 删除数组
般来说程序是能为你分配出内存的,但是实在山穷水尽了怎么办?这时候程序就会抛出一个std::bad_alloc异常。注意,这是new 和 malloc 的区别之一。但是难能可贵的是C++提供了一个机制来处理bad_alloc异常,其做法如下:
void BadAllocateHandler()
{
  std::cout << "啊,内存分配失败了" << std::endl;
  exit(0);
}

std::set_new_handler(BadAllocateHandler);
BadAllocateHandler是程序员自己写的当分配失败时的处理函数。而set_new_handler是c++提供的机制。一般来说当山穷水尽的时候你所应该做的事只有两件。要么让程序退出,要么想办法从别的地方挖一些内存来继续分配。

    已经知道了new是一个关键字。对于一个程序来说,一切的动作都将回到函数调用。所以new的时候到底发生了什么呢?当你new的时候,首先程序会去调用::operator new()这个函数。然后::operator new()中程序会去调用malloc()。 喔!一切都明了了,原来new的本质也是去调用malloc函数!!同理,delete的本质是去调用free()函数。

虽然new的本质是去调用malloc,但是new 和 malloc还有一点很大的不同。那就是new 出来内存后,new会帮你把对象给构造出来,而malloc只是分配内存。具体例子如下:

class MyObj {
public:
  public MyObj(int value) : val(value) {}
  int val;
};

MyObj* obj = new MyObj(4);
MyObj* obj1 = (MyObj*) malloc(sizeof(MyObj));
new 的做法是在malloc分配出内存后,编译器会直接调用类的构造函数在这片内存中构造出对象。注意!只有编译器能直接调用类的构造函数。而你使用malloc的话,是无法直接在上面构造出对象的。



——new和malloc的区别

0.       属性

new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。

1.       参数

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

2.       返回类型

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

3.       分配失败

new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

4.      自定义类型

         new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

         malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

5.      重载

C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。

6.       内存区域

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。


1)申请的内存所在位置

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从上动态分配内存。

自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

特别的,new甚至可以不为对象分配内存!定位new的功能可以办到这一点:
new (place_address) type
place_address为一个指针,代表一块内存的地址。当使用上面这种仅以一个地址调用new操作符时,new操作符调用特殊的operator new,也就是下面这个版本:
void * operator new (size_t,void *) //不允许重定义这个版本的operator new
这个operator new不分配任何的内存,它只是简单地返回指针实参,然后右new表达式负责在place_address指定的地址进行对象的初始化工作。


2)返回类型安全性

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图分配自己没被授权的内存区域。关于C++的类型安全性可说的又有很多了 


3)内存分配失败时的返回值

new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

在使用C语言时,我们习惯在malloc分配内存后判断分配是否成功

int *a  = (int *)malloc ( sizeof (int ));
if(NULL == a)
{
    ...
}
else
{
    ...
}
C++语言中使用异常机制判断分配是否成功:
try
{
    int *a = new int();
}
catch (bad_alloc)
{
    ...
}


4)是否需要指定内存大小

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
1 class A{...}
2 A * ptr = new A;
3 A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A);

5)是否调用构造函数/析构函数

使用new操作符来分配对象内存时会经历三个步骤:

第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。

第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。

第三部:对象构造完成后,返回一个指向该对象的指针。

使用delete操作符来释放对象内存时会经历两个步骤:

第一步:调用对象的析构函数

第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。

总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。

class A
{
public:
    A() :a(1), b(1.11){}
private:
    int a;
    double b;
};
int main()
{
    A * ptr = (A*)malloc(sizeof(A));
    return 0;
}
在return处设置断点,观看ptr所指内存的内容:

可以看出A的默认构造函数并没有被调用,因为数据成员a,b的值并没有得到初始化,这也是上面我为什么说使用malloc/free来处理C++的自定义类型不合适,其实不止自定义类型,标准库中凡是需要构造/析构的类型通通不合适。

而使用new来分配对象时:
1 int main()
2 {
3     A * ptr = new A;
4 }
查看程序生成的汇编代码可以发现,A的默认构造函数被调用了:



6)对数组的处理

C++提供了new[]与delete[]来专门处理数组类型:
A * ptr = new A[10];//分配10个A对象
使用new[]分配的内存必须使用delete[]进行释放:
 delete [] ptr;
new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。

至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:
int * ptr = (int *) malloc( sizeof(int)* 10 );//分配一个10个int元素的数组


7)new与malloc是否可以相互调用

operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。下面是编写operator new /operator delete 的一种简单方式,其他版本也与之类似:
void * operator new (sieze_t size)
{
    if(void * mem = malloc(size)
        return mem;
    else
        throw bad_alloc();
}
void operator delete(void *mem) noexcept
{
    free(mem);
}


8)是否可以被重载

opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本:
//这些版本可能抛出异常
void * operator new(size_t);
void * operator new[](size_t);
void * operator delete (void * )noexcept;
void * operator delete[](void *0)noexcept;
//这些版本承诺不抛出异常
void * operator new(size_t ,nothrow_t&) noexcept;
void * operator new[](size_t, nothrow_t& );
void * operator delete (void *,nothrow_t& )noexcept;
void * operator delete[](void *0,nothrow_t& )noexcept;

我们可以自定义上面函数版本中的任意一个,前提是自定义版本必须位于全局作用域或者类作用域中。太细节的东西不在这里讲述,总之,我们知道我们有足够的自由去重载operator new /operator delete ,以决定我们的new与delete如何为对象分配内存,如何回收对象。

而malloc/free并不允许重载。


9)能够直观地重新分配内存

使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

new没有这样直观的配套设施来扩充内存。


10)客户处理内存分配不足

在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型:
1 namespace std
2 {
3     typedef void (*new_handler)();
4 }
指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:
1 namespace std
2 {
3     new_handler set_new_handler(new_handler p ) throw();
4 }

set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。

对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。


~总结





——const


~在类成员函数后加const

编译器会自动给每一个函数加一个this指针。在一个类的函数后面加上const后,就表明这个函数是不能改变类的成员变量的(加了mutable修饰的除外)。实际上,也就是对这个this指针加上了const修饰”。
举个例子:
class test
{
public:
  test(int a=10):aa(a){}
  ~test(){}
 
  int getA() const {aa++,bb++;return aa}
 
private:
  int aa;
  mutable int bb;
};
int main()
{
  test t(100);
  int cc = t.getA();
};
这里编译器会报错, C2166: 左值指定 const 对象,getA()函数不能执行aa++操作,但执行bb++是可以的。


~const限定类型判断

const 限定一个对象为只读属性。
先从一级指针说起:
(1)const char p    限定变量p为只读。这样如p=2这样的赋值操作就是错误的。
(2)const char *p   p为一个指向char类型的指针,const只限定p指向的对象为只读。这样,p=&a或  p++等操作都是合法的,但如*p=4这样的操作就错了,因为企图改写这个已经被限定为只读属性的对象。
(3)char *const p  限定此指针为只读,这样p=&a或  p++等操作都是不合法的。而*p=3这样的操作合法,因为并没有限定其最终对象为只读。
(4)const char *const p 两者皆限定为只读,不能改写。
有了以上的对比,再来看二级指针问题:
(1)const char **p  p为一个指向指针的指针,const限定其最终对象为只读,显然这最终对象也是为char类型的变量。故像**p=3这样的赋值是错误的,而像*p=? p++这样的操作合法。
(2)const char * const *p 限定最终对象和 p指向的指针为只读。这样 *p=?的操作也是错的。
(3)const char * const * const p 全部限定为只读,都不可以改写。

const char * const * pp等价于  (const char)(*const)*pp 说明*pp是const型常量 **pp是const char型常量,而pp是变量


——sizeof()计算类的大小


sizeof一个空类

class A { };
 
cout<<sizeof(A)<<endl;//1

注:class A是一个空类型,它的实例不包含任何信息,本来求sizeof应该是0。

但当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。

至于占用多少内存,由编译器决定。Visual Studio 2008中每个空类型的实例占用一个byte的空间。


sizeof一个带有析构和构造函数的类

class B
 
{
 
public:
 
  B() {}
 
   ~B() {}
 
};
 
cout<<szieof(B)<<endl;//1

注:class B在class A的基础上添加了构造函数和析构函数。

       由于构造函数和析构函数的调用与类型的实例无关(调用它们只需要知道函数地址即可),在它的实例中不需要增加任何信息。

       所以sizeof(B)和sizeof(A)一样,在Visual Studio 2008中都是1。


sizeof一个带有虚函数的类

class C
 
{
 
public:
 
  C() {}
 
  virtual ~C() {}
 
};
 
cout<<sizeof(C)<<endl;//4

注:class C在class B的基础上把析构函数标注为虚拟函数。C++的编译器一旦发现一个类型中有虚拟函数,

       就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。

       在32位的机器上,一个指针占4个字节的空间,因此sizeof(C)是4。

       C++标准规定类的大小不为0,空类的大小为1,当类不包含虚函数和非静态数据成员时,其对象大小也为1。

       如果在类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表VTable,

       在32位机器上,一个对象会增加4个字节来存储此指针,它是实现面向对象中多态的关键。而虚函数本身和其他成员函数一样,是不占用对象的空间的。


sizeof一个带有成员变量的类

class D {
 
    char ch;
 
    void func() { }
 
};
 
class E {
 
    char ch1; //占用1字节
 
    char ch2; //占用1字节
 
    virtual void func() { }
 
};
class F {
 
    int in;
 
    virtual void func() { }
 
};
 
cout << "D的大小"<< sizeof(D) << endl;//1
cout << "E的大小" << sizeof(E) << endl;//8
cout << "F的大小" << sizeof(E) << endl;//8
注:类和结构体一样,需要考虑数据对齐和补齐规则



——register 关键字


register 关键字请求“编译器”将局部变量存储于寄存器中——最快的关键字

register 这个关键字请求编译器尽可能的将变量存在CPU内部寄存器,而不是通过内存寻址访问(不能用&),以提高效率注意是尽可能,不是绝对


因为,如果定义了很多register变量,可能会超过CPU的寄存器个数,超过容量。所以只是可能。

数据从内存里拿出来先放到寄存器,然后CPU 再从寄存器里读取数据来处理,处理完后同样把数据通过寄存器存放到内存里,CPU 不直接和内存打交道。

速度!就是因为速度。寄存器其实就是一块一块小的存储空间,只不过其存取速度要比内存快得多。

进水楼台先得月嘛,它离CPU 很近,CPU 一伸手就拿到数据了,比在那么大的一块内存里去寻找某个地址上的数据是不是快多了?


举例:

register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度。例如下面的内存块拷贝代码,

#ifdef NOSTRUCTASSIGN
  memcpy (d, s, l)
  {
        register char *d;
      register char *s;
      register int i;
      while (i--)
          *d++ = *s++;
  }
  #endif

但是使用register修饰符有几点限制。

1.register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。

2.因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。

3.早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译环境能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令

C++编译器有自己的优化方式,即使不使用register关键字编译器也会自动优化。

比如我们定义一个循环变量,


for(int i; i<100;i++)
{
i *= i;
}


C++编译器会自动把变量 i 化成register类型,加快运算速度。



——this和super的区别


~this关键字

(1.)每个类的每个非静态方法(没有被static修饰)都会隐含一个this关键字,它指向调用这个方法的对象;当在方法中使用本类属性时,都会隐含地使用this关键字,当然也可以明确使用。

          this可以看成是一个变量,它的值就是当前对象的引用

          this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用如果是在同一类中调用另外一个方法,则可以不用写this,直接调用

(2.)为了区分属性和局部变量,可以通过this关键字来调用

(3.)this关键字的用法
  • 当类中非静态方法的参数名与类的某个成员变量名相同时,为了避免参数作用范围覆盖了成员变量的作用范围,必须明确使用this关键字来指定
  • 如果某个构造方法的第一条语句具有形式this(...),那么这个构造方法将调用本类中的其他构造方法
  • 如果某个方法需要传入当前对象,则可以将当前的对象作为参数传递给它
 public class pra {
                     private String  name;
                     private String  sex;
                     private Integer age;

              public pra(String name, String sex, Integer age) {

                         super();
                         this.name = name;
                         this.sex = sex;
                         this.age = age;
              }

              public pra() {

               this("by", "女", 15);  //调到了有参的构造方法
            }
} 


~super关键字

 super代表了父类空间的引用

super的作用:

子父类存在着同名的成员时,在子类中默认时访问子类的成员,可以通过super关键字指定访问父类的成员
创建子类对象时,默认会先调用父类无参的构造方法,可以通过super关键字指定调用父类的构造方法 
  1. super可以用来引用直接父类的实例变量。
  2. super可以用来调用直接父类方法。
  3. super()可以用于调用直接父类构造函数

class C {
	String name = "A";

	public void work() {
		System.out.println("A工作!");
	}
}


class B extends C {
	String name = "B";

	public B() {
		super(); // 调用父类构造方法
	}

	public void work() {
		System.out.println("B工作!");
	}

	public void pint() {
		System.out.println(name);
		System.out.println(super.name); // 调用父类的实例变量
		super.work(); // 调用父类的方法
	}
}


public class A {
	public static void main(String[] args) {
		B b = new B();

		b.pint();
	}
}

注意事项:

            *如果在子类的构造方法上没有指定调用父类的构造方法,java编译器会在子类的构造器里面加上super()语句

            * super关键字调用父类的构造函数时,该语句必须要是子类构造函数的第一个语句

            *super和this不能同时出现在同一个构造函数中调用其他的构造函数,因为两个语句都要是第一个语句

this和super的区别

          1)代表的事物不同

              super代表的是父类空间的引用

              this代表的是所属函数的调用者对象

          2)使用前提不同

              super必须要有继承关系才能使用

              this不需要继承关系也能使用

          3)调用的构造函数不同

               super:调用父类的构造函数

               this:调用所属类的构造函数



——virtual

1)virtual关键字主要是什么作用?

c++中的函数调用默认不适用动态绑定。😮要触发动态绑定,必须满足两个条件:第一,指定为虚函数;第二,通过基类类型的引用或指针调用。
由此可见,virtual主要主要是实现动态绑定。


2)哪些情况下可以使用virtual关键字?

virtual可用来定义类函数和应用到虚继承。
 
😮友元函数 构造函数 static静态函数 不能用virtual关键字修饰;
普通成员函数 和析构函数 可以用virtual关键字修饰;


3)virtual函数的效果

 
	
  1. class GrandFather  
  2. {  
  3. public:  
  4.     GrandFather() {}  
  5.     virtual void fun()  
  6.     {  
  7.         cout << "GrandFather call function!" << endl;  
  8.     }  
  9. };  
  10.   
  11. class Father : public GrandFather  
  12. {  
  13. public:  
  14.      Father() {}  
  15.      void fun()  
  16.      {  
  17.          cout << "Father call function!" << endl;  
  18.      }  
  19. };  
  20.   
  21.   
  22. class Son : public Father  
  23. {  
  24. public:  
  25.     Son() {}  
  26.     void fun()  
  27.     {  
  28.         cout << "Son call function!" << endl;  
  29.     }  
  30. };  
  31.   
  32. void print(GrandFather* father)  
  33. {  
  34.     father->fun();  
  35. }  
  36.   
  37. int _tmain(int argc, _TCHAR* argv[])  
  38. {  
  39.     Father * pfather = new Son;  
  40.     pfather->fun();  
  41.     GrandFather * pgfather = new Father;  
  42.     print(pgfather);  
  43.     return 0;  
  44. }  
输出为
Son call function      Father call function


4)virtual的继承性

😮只要基函数定义了virtual,继承类的该函数也就具有virtual属性
即 GrandFather Father Son同时定义virtual void fun()与GrandFather一个定义virtual void fun效果是一样的


5)虚析构函数

class GrandFather  
{  
public:  
    GrandFather() {}  
    virtual void fun()  
    {  
        cout << "GrandFather call function!" << endl;  
    }  
  
    ~GrandFather()   
    {  
        cout << "GrandFather destruction!" << endl;  
    }  
};  
  
class Father : public GrandFather  
{  
public:  
    Father() {}  
    void fun()  
    {  
        cout << "Father call function!" << endl;  
    }  
  
    ~Father()  
    {  
        cout << "Father destruction!" << endl;  
    }  
};  
  
  
class Son : public Father  
{  
public:  
    Son() {}  
    void fun()  
    {  
        cout << "Son call function!" << endl;  
    }  
  
     ~Son()  
    {  
        cout << "Son destruction!" << endl;  
    }  
};  
  
void print(GrandFather* p)  
{  
    p->fun();  
}  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
    Father * pfather = new Son;  
    delete pfather;  
    return 0;  
}  
输出为
Father destruction!        GrandFather destruction!

上面执行了Son的构造函数,没执行Son的析构函数,故把GrandFather的析构函数设置为virtual
则输出:
Son destruction!        Father Destruction!        GrandFather destruction!


6)纯虚函数

纯虚函数定义如下:
class GrandFather  
{  
public:  
    GrandFather() {}  
    virtual void fun() = 0  
    {  
        cout << "GrandFather call function!" << endl;  
    }  
  
    virtual ~GrandFather()   
    {  
        cout << "GrandFather destruction!" << endl;  
    }  
};  
  • 纯虚函数为后代类提供可覆盖的接口,但这个类中的版本决不会调用。
  • 含有(或继续)一个或多个纯虚函数的类是抽象基类,抽象基类不能实例化!
  • 继承类只有重写这个接口才能被实例化


7)虚继承

虚继承主要解决交叉继承带来的问题。https://www.cnblogs.com/malecrab/p/5572730.html  (虚函数实现的基本原理)
class GrandFather  
{  
public:  
    GrandFather() {}  
    void fun()  
    {  
        cout << "GrandFather call function!" << endl;  
    }  
  
    virtual ~GrandFather()   
    {  
        cout << "GrandFather destruction!" << endl;  
    }  
};  
  
class Father1 : public GrandFather  
{  
public:  
    Father1() {}  
    void fun()  
    {  
        cout << "Father call function!" << endl;  
    }  
      
};  
  
class Father2 : public GrandFather  
{  
public:  
    Father2() {}  
    void fun()  
    {  
        cout << "Father call function!" << endl;  
    }  
  
};  
  
  
class Son : public Father1, public Father2  
{  
public:  
    Son() {}  
    //void fun()  
    //{  
    //  cout << "Son call function!" << endl;  
    //}  
};  
  
void print(GrandFather* p)  
{  
    p->fun();  
}  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
    Son* son = new Son;  
    son->fun();  
    return 0;  
}  
😣编译时会提示报错对fun的访问不明确
如果Father1和Father2都用虚继承继承GrandFather类则可以解决这个问题


8) 构造函数和析构函数中的虚函数

😣如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本


9)虚函数实现机制


10)小结

class classA  
{  
 public:  
    classA()  
    {  
        clear();  
    }  
    virtual ~classA()  
    {  
    }  
    void clear()  
    {  
        memset(this , 0 , sizeof(*this));  
    }  
    virtual void func()  
    {  
        printf("func\n");  
    }  
};  
  
class classB : public classA  
{  
};  
  
int main(void)  
{  
    classA oa;  
    classB ob;  
    classA * pa0 = &oa;  
    classA * pa1 = &ob;  
    classB * pb = &ob;  
    oa.func(); // 1  
    ob.func(); // 2  
    pa0->func(); // 3  
    pa1->func(); // 4  
    pb->func(); // 5  
    return 0;  
}  
程序输出依次是
func
func
出错
func
func


当classA oa;  oa.func();
不存在动态调用的过程,所以func虽然是虚函数,但是函数调用不通过虚表访问,所以即使
memset(this , 0 , sizeof(*this));  
找不到虚表地址也没有关系
在执行classB ob;的时候,注意memset的是classA的地址,所有ob的虚表是存在的
即是如下,通过指针或引用(动态绑定)访问oa的func函数(需要从虚表访问),会出错
访问ob的func和函数,无论静态访问还是动态访问,都不会出错

当把classB的代码改成如下时 
classB()
{
        clear();
}
virtual ~classB()
{
}
void clear()
{
      memset(this , 0 , sizeof(*this)); }
输出为
func
func
出错
出错
出错



——explicit关键字


首先, C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).

这里的重点就是 只能 修饰 只有一个参数的 类的构造函数。也就是说:
  • 只可以修饰类的构造函数,一般的函数不能修饰
  • 只有类的构造函数有一个参数时候,才有效,当有多个参数时候就没什么意义了(这里有个例外,就是当除了第一个参数以外的其他参数都有默认值的时候,explicit依然有效,此时调用构造函数时只传入一个参数,等效于只有一个参数的类构造函数)。

class MyClass   //没有使用explicit关键词,默认为隐式声明
{
public:
    int m_size;
    char *m_pStr;
 
    MyClass(int size)                  //没有使用explicit关键词,默认为隐式声明
    {
        m_size = size;                        //大小
        m_pStr = (char *)malloc(size + 1);    //给m_pstr 分配size的大小的内存
        memset(m_pStr, 0, size + 1);
    }
 
    MyClass(const char *pStr)            //没有使用explicit关键词,默认为隐式声明
    {
        m_size = strlen(pStr);
        m_pStr = (char *)malloc(m_size + 1); //给m_pstr分配内存
        strcpy(m_pStr, pStr);          //复制内容
    }
 
    MyClass(int size, const char *pStr)       //参数为两个,所以不管是否使用explicit关键字,都是无效的。
    {
        m_size = size;
        int size1 = strlen(pStr);             //大小
        m_pStr = (char *)malloc(size1 + 1); //给m_pstr分配内存
        strcpy(m_pStr, pStr);
    }
 
};
下面是调用:
    MyClass c1(10);        // ok, 显式调用 MyClass(int size)
    MyClass c2 = 12;        // ok, 隐式调用 MyClass(int size)
    MyClass c3;          // 错误, 没有响应的构造函数
    MyClass c4("hello");     // ok, 显式调用 MyClass(const char *pStr)
    MyClass c5 = "nihao";   // ok, 隐式调用
    MyClass c6(5, "hello");    // ok, 显式调用 MyClass(int size, const char *pStr)
 
    c1 = 5;          // ok, 隐式调用 MyClass(int size)
    c2 = 6;           // ok, 隐式调用 MyClass(int size)
    c4 = "hello world";    // ok, 隐式调用 MyClass(const char *pStr)
 
    c3 = c1;           // 错误, 编译是没问题的,但是必须在类里重载“=”运算符才行
上面代码 c2 = 12; c5 = "nihao"为什么是可以的呢?
      因为在C++中,如果构造函只有一个参数时,那么编译的时候就会有一个缺省的转换操作将该构造函数对应的数据类型的数据转换为该类对象。
 也就是说MyClass c2 = 12;等同于MyClass c2(12);

但是上面的代码中的MyClass c2(12),中12表示分配内存大小,那么c2 = 12 与c5 = "nihao"这样的隐式转换显得不伦不类,容易被误解,怎么避免使用这样的隐式转换呢?
       使用关键字explicit

我们将类中的构造函数添加上explicit关键字试试:
class MyClass
{
public:
    int m_size;
    char *m_pStr;
 
    explicit MyClass(int size)
    {
        m_size = size;                //大小
        m_pStr = (char *)malloc(size + 1);    //给m_pstr 分配size的大小的内存
        memset(m_pStr, 0, size + 1);
    }
 
    explicit MyClass(const char *pStr)
    {
        m_size = strlen(pStr);
        m_pStr = (char *)malloc(m_size + 1);   //给m_pstr分配内存
        strcpy(m_pStr, pStr);                //复制内容
    }
 
    explicit MyClass(int size, const char *pStr)  //两个参数,所以添加explicit是无效的。
    {
        m_size = size;
        int size1 = strlen(pStr);             //大小
        m_pStr = (char *)malloc(size1 + 1);   //给m_pstr分配内存
        strcpy(m_pStr, pStr);
    }
 
};
再次编译一下结果如下:

发现隐式调用的全部错误,这就是使用关键字explicit,显式调用的效果。也就是说,关键字explicit的作用就是防止函数的隐式转换。

上面也说过了,explicit关键字只对有一个参数的类构造函数有效,如果函数的参数大于或者等于两个时候,是不会产生隐式转换的,所以explicit也就是无效的。
一个例外就是,当除了第一个参数意外其他参数都有默认值的时候,explicit关键字依然有效,此时当调用构造函数时,只传入一个参数,等同于只有一个参数的类构造函数。
  explicit MyClass(int size, const char *pStr = "hello")
    {
        m_size = size;
        int size1 = strlen(pStr);            //大小
        m_pStr = (char *)malloc(size1 + 1);  //给m_pstr分配内存
        strcpy(m_pStr, pStr);
    }