c++八股文

c++内存管理

1、静态全局变量、全局变量、静态局部变量、局部变量的区别

  • 静态全局变量、全局变量的区别
    (1)静态全局变量和全局变量都存储在全局/静态存储区
    (2)静态全局区只在本文件有效,不可extern到其他文件,而全局变量可以extern到别的文件中调用。
    (3)如果别的文件中定义了一个和该全局变量相同的变量名,会报错。
  • 静态局部变量(比如函数内部的变量用 static 修饰)、局部变量的区别
    (1)静态局部变量是属于常量区的,而函数内部的局部变量属于栈区
    (2)静态局部变量在该函数调用结束时,不会销毁,而是随整个程序结束而结束,但是别的函数调用不了该变量,局部变量随该函数的结束而结束;
    (3)如果定义这两个变量的时候没有初始值时,静态局部变量会自动定义为0,而局部变量就是一个随机值;
    (4)静态局部变量在编译期间只赋值一次,以后每次函数调用时,不在赋值,调用上次的函数调用结束时的值。局部变量在调用期间,每调用一次,赋一次值。

1、在c++中,内存分为 5 个区,

  • 堆区heap:分配堆区内存也叫动态内存分配。程序在运行的时候使用malloc申请任意多少的内存,程序员自己负责在何时用free释放内存。动态内存的生命周期由程序员决定,使用非常灵活,但如果在堆上分配了空间,既有责任回收它,否则运行的程序会出现内存泄漏,频繁的分配和释放不同大小的堆空间将会产生内存碎片,如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 栈区:在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数执行结束时,这些内存单元会自动被释放。栈内存分配运算内置于处理器的指令集,效率高,但是分配的内存容量有限。里面的变量通常是局部变量、函数参数、返回数据、返回地址等。由编译器自动分配释放
  • 自由存储区:就是那些由new/delete等分配的内存块,他和堆是十分相似的,也是用户来申请释放内存
  • 全局/静态存储区:内存在程序编译的时候已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static静态全局变量。程序结束后由系统释放
  • 常量存储区:存放常量、字符串、静态局部变量,程序结束后有系统释放
    在所有函数体定义的是全局量,加了static修饰符后不管在哪里都存放在全局区(静态区),而且表示在该文件中有效,不能extern到别的文件用。在函数内定义的static(静态局部变量)表示只在该函数体内有效。另外,函数中像 “adgfdf” 这样的字符串存放在常量存储区
int a = 0; //全局初始化区,全局存储区
char *p1; //全局未初始化区,全局存储区
void main(){
    int b; //栈,局部变量
    char s[] = "abc"; //栈,局部变量
    char *p2; //栈,局部变量
    char *p3 = "123456"; //  “123456” 在常量区,p3在栈上
    static int c = 0; //全局(静态)初始化区
    p1 = (char *)malloc(10); //分配得来得10字节的区域在堆区
    p2 = (char *)malloc(20); //分配得来得20字节的区域在堆区
    strcpy(p1, "123456"); // “123456” 放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块
}

2、堆与栈的关系和区别
(1)分配效率上。现代计算机有专门的寄存器指向栈所在的地址,有专门的机器指令(push,pop)完成数据入栈出栈的操作,这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因为栈的这种特点,对栈的使用在程序中是非常频繁的。而堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free 函数维护了一套内部的堆数据结构。从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/ 释放内存匹配是良好程序的基本要素。
(2)碎片问题:对于堆来讲,频繁的new/delete或者malloc/free势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
(3)生长方向:对于来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
(4)分配方式都是动态分配的,没有静态分配的堆。有 2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

int* p=new int[5];  // new 分配了一块堆内存(也可以叫自由存储区),指针p分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。

delete []p; // 删除不是用 delete p,应该是delete [] p,这是为了告诉编译器删除的是一个数组

new 和 malloc 的区别

参考:new和malloc的区别
前言

malloc只是简单的申请内存,new还会检查是否申请成功,并且负责调用构造函数(如果有的话),newmalloc要好的多,建议使用。其实,在new中,也是调用malloc申请内存的,只不过封装的更好,更安全。

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

  2. 那么自由存储区和堆区的区别?或者说那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存)?
    这取决于new操作符的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这得看new操作符在哪里为对象分配内存。特别的,new甚至可以不为对象分配内存,比如定位new—人为找到一块分配内存的地址,而不让系统随机分配地址。

  3. new operatoroperator new的区别?调用new的时候编译器做了什么?
    new就是new operator,调用new的时候编译器做了三件事:
    (1)用operator new( )分配内存。new的底层是调用operator new( )分配内存的。该函数调用malloc申请内存
    (2)是调用构造函数(就是new的类类型或者string等类型的构造函数)。这一步是使用 placement new(定位new) 来实现的,即在取得了一块可以容纳指定类型对象的内存之后,在这块内存上构造一个对象
    (3)返回相应数据类型的指针。

new(new operator) 和 malloc 的主要区别

  1. 返回类型的安全性
    new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void *,需要通过强制类型转换将void*指针转换成我们需要的类型。类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图分配自己没被授权的内存区域。
  2. 内存分配失败时的返回值
    new内存分配失败时,会抛出bac_alloc异常,它不会返回NULLmalloc分配内存失败时返回NULL
  3. 是否需要指定内存大小
    使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
class A{...}
A * ptr = new A;
A * ptr = (A *) malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A);而且 malloc(sizeof(A)) 只是返回一个空类型的指针,需要经过 (A*)强制转换成 A 类型
  1. 是否调用构造函数/析构函数
    使用new操作符来分配对象内存时会经历三个步骤
    (1)调用 operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
    (2)编译器运行相应的构造函数以构造对象,并为其传入初值。
    (3)对象构造完成后,返回一个指向该对象的指针。
    使用delete操作符来释放对象内存时会经历两个步骤
    (1)调用对象的析构函数。
    (2)编译器调用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));
	// A * ptr = new A;
	return 0;
}

return处设置断点,观看ptr所指内存的内容:

可以看出 A 的默认构造函数并没有被调用,因为数据成员 a,b 的值并没有得到初始化。而用 new 来分配对象是,可以看到 A 的默认构造函数被调用了。

  1. 对数组的处理
    C++提供了new[]delete[]来专门处理数组类型:
A * ptr = new A[10];//分配10个A对象  
delete [] ptr;  // 使用new[]分配的内存必须使用delete[]进行释放
delete ptr; // 这种做法并不会出现内存泄漏,但是不建议用

new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会出现数组对象部分释放的现象,造成内存泄漏。
至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

int * ptr = (int *) malloc( sizeof(int)* 10 );//分配一个10个int元素的数组
  1. new与malloc是否可以相互调用
    new 的实现(operator new /operator delete)可以基于 malloc,只是封装的更好,更安全。而malloc的实现不可以去调用new。
void * operator new (sieze_t size){   // 可以看出 new 的实现依靠 malloc
    if(void * mem = malloc(size)
        return mem;
    else
        throw bad_alloc();
}
void operator delete(void *mem) noexcept{
    free(mem);
}
  1. 是否可以被重载
    opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本,我们有足够的自由去重载operator new /operator delete ,以决定我们的new/delete如何为对象分配内存,如何回收对象。而malloc/free并不允许重载。

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

  3. 客户处理内存分配不足
    operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler,这个函数程序员可以重新编写决定内存不足以分配时要干什么事。而malloc,客户并不能够去自定义编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL
    总结:malloc给你的就好像一块原始的土地,你要种什么需要自己在土地上来播种。而new帮你划好了田地的分块(数组),帮你播了种(构造函数),还提供其他的设施供用户使用。在C++这种偏重OOP的语言,使用new/delete是更合适的。

new 的用法:

参考:定位new运算符
new 的根本用法就是为某个变量动态地分配内存,主要有下面两种用法:
1、负责在自由存储区中找到一个足以能够满足要求的内存块。

//语法如下:
new (place_address) type  // place_address为一个指针,代表一块内存的地址。当使用上面这种仅以一个地址调用new操作符时,new操作符调用特殊的operator new
void * operator new (size_t,void *)  // 可以看到,它只是简单地返回指针实参,然后用 new 表达式负责在place_address指定的地址进行对象的初始化工作。
//例子:
p1 = new int[10];  // 常规的 new 方法,只要是空闲地址都可以分配

2、定位new运算符(placement new),它能够让程序员指定要使用的位置。既将new运算符用于提供了的地址。定位new运算符不需要相应的delete运算符来释放内存。因为它本身就不开辟新的内存,只是返回将要使用的地址,并将其强制转换为void *,以便我们能够通过强制类型转换将 void *指针赋给任何指针类型。

p2 = new (buffer) int[10];   // 定位 new 运算符,指定new分配的地址,不管该地址是否已经被使用,新值直接覆盖在旧值上面
    for (int i = 0; i < 10; ++i)
        p1[i] = p2[i] = 20 - i;
#include "new"   //  定位 new 运算符需要这个头文件
#include "iostream"
using namespace std;
int main() {
    char buffer[512];   //chunk of memory内存池
    int *p1, *p2, *p3, *p4;
    //常规new:
    p1 = new int[10];
    //定位new:
    p2 = new (buffer) int[10];   // 
    for (int i = 0; i < 10; i++)
        p1[i] = p2[i] = 20 - i;
    cout << "p1 = " << p1 << endl;             //常规new指向的地址
    cout << "buffer = " << (void *)buffer << endl; //内存池地址
    cout << "p2 = " << p2 << endl;             //定位new指向的地址
    cout << "p2[0] = " << p2[0] << endl;
    delete []p1;
    p3 = new (buffer) int;
    *p3 = 1;
    cout << "p3 = " << p3 << endl;
    cout << "p3[0] = " << *p3 << endl;
    p4 = new (buffer + 10 * sizeof(int)) int;
    cout << "p4 = " << p4 << endl;
    return 0;
}
>>>
p1 = 0x161530
buffer = 0x71fbe0
p2 = 0x71fbe0
p2[0] = 20
p3 = 0x71fbe0
p3[0] = 1
p4 = 0x71fc08

重载、构造函数、析构函数

c++重载、重写、隐藏的区别

1、重载:是指同一作用域内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型(统一为void,否则报错)。。。重载就是一种多态,编译时多态

#include <iostream>
using namespace std;
 
class printData{
   public:
      void print(int i) {
        cout << "整数为: " << i << endl;
      }
      void print(double  f) {
        cout << "浮点数为: " << f << endl;
      }
      void print(char c[]) {
        cout << "字符串为: " << c << endl;
      }
};
 
int main(){
   printData pd;
   // 输出整数
   pd.print(5);
   // 输出浮点数
   pd.print(500.263);
   // 输出字符串
   char c[] = "Hello C++";
   pd.print(c);
   return 0;
}

2、重写(覆盖):是指派生类(子类)中存在重新定义的函数。其函数名,参数列表,返回值类型等所有声明都必须同基类中被重写的函数一致只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用基类中的被重写函数。重写的基类中被重写的函数必须有virtual修饰。所以有继承时基类的析构函数一定要设置成虚函数,防止内存泄漏。

3、隐藏:是指派生类(子类)的函数屏蔽了与其同名的基类(父类)函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏
重载的参数类型不同,函数体不同;隐藏只要函数名相同,参数和函数体等可以不同;重写仅仅函数体不同.

重载、构造函数、析构函数

1、重载为什么改变参数类型就可以实现调用不同的函数?
因为C++在编译的时候会对函数进行重命名,保证函数名的唯一性,而重载函数的参数不同,就会被命名为不同的函数名。
2、构造函数可以被重载吗?析构函数可以被重载吗?
构造函数可以被重载,因为构造函数可以有多个且可以带参数。析构函数不可以被重载,因为析构函数只能有一个,且不能带参数。
3、构造函数的作用?

  1. 给创建的对象简历一个标识符

  2. 为对象数据成员开辟内存空间

  3. 完成对象数据成员的初始化

  4. 构造函数有返回值吗?
    没有,构造函数和析构函数都没有返回值。无法返回。

c++多态

  1. 什么是多态机制?
    多态就是说同一个名字的函数可以有多种不同的功能。分为编译时的多态和运行时的多态。编译时的多态就是函数重载,包括运算符重载,编译时根据实参确定调用哪个函数。运行时的多态则和虚函数、继承有关
  2. 多态底层是如何实现的?
    (1)编译时多态(重载):因为C++在编译的时候会对函数进行重命名,保证函数名的唯一性,而重载函数的参数不同,就会被命名为不同的函数名。
    (2)运行时多态:利用虚函数表,先构建一个基类,然后在基类的构造函数中会建立虚函数表,也就是一个存储虚函数地址的数组,内存地址的前四个字节保存指向虚函数表的指针,然后当多个子类继承父类之后,主函数中可以通过父类指针调用子类的继承函数。虚函数表属于类,也属于它的子类等各种派生类。虚函数表由编译器在编译时生成,保存在.rdata只读数据段。
  3. 子类的多态函数是怎么被调用的?
    因为每个子类都继承并设置了自己的虚函数表,每次用用父类指针创建新子类时就会出现,从而最终调用自己的表。
  4. 怎么知道多态时,指向那个虚函数?
    定义的父类指针 new 出哪个子类就是指向哪个子类的虚函数。

虚函数/虚函数表

参考:C++ 虚函数表 C++多态虚函数表详解(多重继承、多继承情况)

  1. 虚函数基础知识
    C++中,一个类存在虚函数,那么编译器就会为这个类生成一个虚函数表,在虚函数表里存放的是这个类所有虚函数的地址。当生成类的对象的时候,编译器会自动的将类对象的前四个字节设置为虚函数表的地址,这四个字节就可以看作是一个指向虚函数表的指针。虚函数表可以看做一个指针数组,该数组中每个元素都是虚函数的地址

2、虚函数表总结
不考虑继承时
(1)一个类里面只要定义了虚函数编译时就会产生一个虚函数表(一个数组),表中存的都是虚函数的地址,而且类经过实例化之后,虚函数表占的是该对象的前 4 个字节(32bit系统)地址,表示的是一个虚函数的头指针。
(2)如果一个类A 有两个实例化对象a1, a2,那么a1, a2的地址是不相同的,但是a1, a2的虚函数表地址是相同的,也就是说同一个类的不同实例共用同一份虚函数表,即
在这里插入图片描述
考虑继承时
(1)单继承且本身不存在虚函数的继承类的内存布局

class Base1{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1{
public:
    int derive1_1;
    int derive1_2;
};

int main(){
	Derive1 d1;
}

在这里插入图片描述
可以看出 d1 这个对象直接把基类的虚函数表给继承了。
(2)存在基类虚函数覆盖的单继承类的内存布局

class Base1{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1{
public:
    int derive1_1;
    int derive1_2;
    virtual void base1_fun1() {}   // 覆盖基类函数
};

在这里插入图片描述
上图可以看出这种情况下,多态出现了也即是说无论是通过 子类 Derive1 的指针还是 基类Base1 的指针来调用此方法, 调用的都将是被继承类重写后的那个方法(函数)。也即是说子类对象与指向子类的基类指针(Base p = derive1)指向的对象,使用同一个虚函数表。当然,这并不会改变基类的虚函数表,如果单独实例化基类对象,并调用基类的成员函数,最后执行的还是基类函数。*

(3)定义了基类没有的虚函数的单继承的类对象布局

class Base1{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1{
public:
    int derive1_1;
    int derive1_2;

    virtual void derive1_fun1() {}   // 子类的虚函数
};

这种情况下,继承类Derive1的虚函数表被加在基类的后面,对于基类来说,他的虚函数表还是只有他自己的虚函数,并不会将子类的虚函数也算到他的虚函数表中
在这里插入图片描述
(4)多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局

class Base1{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Base2{
public:
    int base2_1;
    int base2_2;

    virtual void base2_fun1() {}
    virtual void base2_fun2() {}
};

// 多继承
class Derive1 : public Base1, public Base2{
public:
    int derive1_1;
    int derive1_2;

    // 基类虚函数覆盖
    virtual void base1_fun1() {}
    virtual void base2_fun2() {}

    // 自身定义的虚函数
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

在这里插入图片描述Derive1 d1的虚函数表依然是保存到第 1 个拥有虚函数表的那个基类的后面的.

(4)多继承且第一个基类没有虚函数时又存在自身定义的虚函数的类对象布局

class Base1{
public:
    int base1_1;
    int base1_2;
};

class Base2{
public:
    int base2_1;
    int base2_2;

    virtual void base2_fun1() {}
    virtual void base2_fun2() {}
};

// 多继承
class Derive1 : public Base1, public Base2{
public:
    int derive1_1;
    int derive1_2;

    // 自身定义的虚函数
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

在这里插入图片描述
此时,第二个有虚函数的基类将会占据内存的前面,也就是说谁有虚函数表, 谁就放在前面

(4)多继承且基类都没有虚函数

class Base1{
public:
    int base1_1;
    int base1_2;
};

class Base2{
public:
    int base2_1;
    int base2_2;
};

// 多继承
class Derive1 : public Base1, public Base2{
public:
    int derive1_1;
    int derive1_2;

    // 自身定义的虚函数
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

在这里插入图片描述

总结:
(1)对象怎么找到对应的虚函数表?
对象内存的前 4 个字节(32bit)存的是虚函数表的首地址,可以根据虚函数表的地址偏移找到虚函数的地址
(2)虚函数表的结构是怎样的?
虚函数表是一个函数指针数组,数组里存放的都是函数指针,指向虚函数所在的位置。 对象调用虚函数时,会根据虚指针找到虚表的位置,再根据虚函数声明的顺序找到虚函数在数组的哪个位置,找到虚函数的地址,从而调用虚函数。
(3)A,B两个类,类中有虚函数。C继承AB,有几张虚函数表?
2张,多继承就会有多个虚函数表。因为每个父类的虚函数是不同的,指针也是不同的。如果共用一张虚函数表,就分不清到底子类的实例化是针对哪一个基函数的。
(4)父类构造函数中是否可以调用虚函数?注意是说可不可以调用,并不是说构造函数能不能设置成虚函数
**可以。不过调用会屏蔽多态机制,最终会把基类中的该虚函数作为普通函数调用,而不会调用派生类中的被重写的函数。**构造函数调用层次会导致一个有趣的两难选择。试想:如果我们在构造函数中并且调用了虚函数,那么会发生什么现象呢?在普通的成员函数中,我们可以想象所发生的情况——虚函数的调用是在运行时决定的。这是因为编译时这个对象并不能知道它是属于这个成员函数所在的那个类,还是属于由它派生出来的某个类。于是,我们也许会认为在构造函数中也会发生同样的事情。然而,对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。也就是说,虚机制在构造函数中不工作。
因为构造函数的工作是生成一个对象,并且为该对象初始化,如果构造函数也和虚函数一样的性质,那么很有可能我们所调用的函数会操作某些还没有被初始化的成员,这将导致问题。
(5)继承时构造函数是如何执行的?析构时析构函数是如何执行的?
继承时构造函数调用是按照从基类到最晚派生类的顺序的执行的,而不是直接执行最晚派生类。析构函数是从最晚派生类开始逐渐析构到基类的,和构造顺序相反。

(6)构造函数可以是虚函数吗?
**不可以,因为虚函数存在的唯一目的就是为了多态。而子类并不继承父类的构造函数,构造函数是创建对象时自己主动调用的,不可能被继承,所以没有使父类构造函数变成虚函数的必要。**另外,虚函数表需要父类执行构造函数后才能生成,如果父类的构造函数设置成了虚函数,那么由于多态特性子类在继承时父类的时候父类的构造函数不执行也就没办法出现后序的虚函数。

(7)静态函数可以是虚函数么?为什么?
1、static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
2、静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。所以无法访问vptr. 进而不能访问虚函数表。所以静态函数不能是虚函数

(8)虚函数的安全性有什么问题?
可以通过虚函数表,让父类指针(就是指指向子类地址的父类指针)访问子类的特有虚函数。这带来一定的安全问题。另外,即使父类的虚函数是私有函数或者保护函数,仍然可以通过虚函数表访问,带来一定的安全问题。

(9)析构函数可以是虚函数吗?
在有继承的情况下,析构函数必须是虚函数。

当析构函数是非虚函数时,主函数通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数(也就是静态绑定);而指针是父类指针(就是指指向子类地址的父类指针),所以调用父类的析构函数。析构函数必须是虚函数。因为如果不是虚函数,当在主函数中用父类的指针new出一个子类对象,最后析构的时候,只会调用父类析构函数而不会调用子类析构函数。而且如果不为虚函数,父类指针就不会调用子类成员函数。父类析构函数成为虚函数时,子类的析构函数会自动也变为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;也就是说,指针指向哪个类的对象就调用哪个类的函数。pb、pd 都指向了派生类的对象,所以会调用派生类的析构函数,继而再调用基类的析构函数。

(10)析构函数可以是纯虚函数么?
析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

(11)如果析构函数不是虚函数,一定会出现内存泄露吗?
析构函数是虚函数的主要原因是,如果不是虚函数,每次结束时因为是父类指针所以只会调用父类的析构函数,而不会调用子类的析构函数,如果子类中有指针开辟空间,子类没有调用析构函数释放这个空间,就会导致内存泄露。但是如果子类中没有用指针开辟空间,都是普通的变量,应该就不会出现内存泄漏。

(12)定义一个A* pa= new A[5]; delete pa; 类A的构造函数和析构函数分别执行了几次?
构造函数执行了5次,每new一个对象都会调用一个构造函数,析构函数只调用一次,如果调用delete[] pa 析构函数才会调用5次。

(13)虚函数可以是内联的吗?
内联函数不能为虚函数,原因在于虚表机制需要一个真正的函数地址,而内联函数展开以后,就不是一个函数,而是一段简单的代码(多数C++对象模型使用虚表实现多态,对此标准提供支持),可能有些内联函数会无法内联展开,而编译成为函数。

(14)类里面不能同时存在函数名和参数都一样的虚函数和静态函数
父类的析构函数是非虚的,但是子类的析构函数是虚的,delete子类对象指针会调用父类的析构函数

智能指针

  1. 指针和引用的区别
    (1)引用必须定义时初始化,不能像指针一样仅 int *a;这样定义,必须 int & b=a;
    (2)引用不能改变指向,引用就是个指针常量
    (3)指针可以有多级但引用只能有一级,有 int, 但是没有 int &&
    (4)指针传递的本质是值传递复制实参的地址到函数的栈中,然后在形参中对地址取值操作。而引用的形参是给实参起了一个别名,可以直接操控形参从而实现对实参的控制。

2、

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值