C/C++后端开发面经(2)——C/C++相关

C/C++后端开发面经(2)——C/C++相关

C++11并发编程-条件变量(condition_variable)详解
C++:继承访问属性(public/protected/private)
C++之Lambda表达式
C++中 =defaule 和 =delete 使用

2.1 c和c++区别、概念相关面试题

2.1.1 new和malloc的区别⭐⭐⭐⭐⭐

  1. new、delete是C++中的操作符,而malloc和free是标准库函数
  2. malloc free是库函数而不是运算符,不在编译器控制范围之内,不能够自动调用构造函数和析构函数。而new在为对象申请分配内存空间时,可以自动调用构造函数,同时也可以完成对对象的初始化。同理,delete也可以自动调用析构函数。
    而mallloc只是做一件事,只是为变量分配了内存所以在使用的时候必须指定大小,同理,free也只是释放变量的内存。
  3. new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。而malloc需要我们计算申请内存的大小,并且在返回的是void*类型,然后再强行转换为实际类型的指针。
  4. C++ 允许重载 new/delete操作符malloc和free不能重载
  5. new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL
  6. 内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存(自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配)C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

2.1.2 malloc的底层实现⭐⭐⭐⭐

在这里插入图片描述
图2-1进程空间示意图

解析:回顾进程的空间模型,如图2-1所示,与1.1.5节的图相比,多了一个program break指针,Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。
在这里插入图片描述
图2-2堆内部机制

获取了break地址,也就是内存申请的初始地址,下面是malloc的整体实现方案:
malloc函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。 调用malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。 调用free函数时,它将用户释放的内存块连接到空闲链表上。 到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。

2.1.3在1G内存的计算机中能否malloc(1.2G)?为什么?⭐⭐

答:是有可能申请1.2G的内存的。
解析:回答这个问题前需要知道malloc的作用和原理,应用程序通过malloc函数可以向程序的虚拟空间申请一块虚拟地址空间,与物理内存没有直接关系,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
我们要申请空间的大小为1.2G=230 × 1.2 Byte ,转换为十六进制约为 4CCC CCCC ,这个数值已经超过了int类型的表示范围,但还在unsigned的表示范围。幸运的是malloc函数要求的参数为unsigned 。在当前正在使用的Windows环境中,可申请的最大空间超过1.9G。实际上,具体的数值会受到操作系统版本、程序本身的大小、用到的动态/共享库数量、大小、程序栈数量、大小等的影响,甚至每次运行的结果都可能存在差异,因为有些操作系统使用了一种叫做随机地址分布的技术,使得进程的堆空间变小。感兴趣的读者可以去研究操作系统中的相关内容。
综上,是有可能通过malloc( size_t ) 函数调用申请超过该机器物理内存大小的内存块的。

2.1.4指针与引用的相同和区别;如何相互转换?⭐⭐⭐⭐⭐

相同:
都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
区别:

  1. 指针是一个实体,而引用仅是个别名
  2. 指针和引用的自增(++)运算意义不一样,指针是对内存地址的自增,引用是对值的自增
  3. 引用使用时无需解引用(*),指针需要解引用;
  4. 引用只能在定义时被初始化一次,之后不可变;指针可变;
  5. 引用不能为空,指针可以为空;
  6. 引用没有const,指针有const;(好像有啊,const int &a = 1)
  7. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量)
  8. 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域

指针和引用之间怎么转换:
指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。
引用转指针:把引用类型的对象用&取地址就获得指针了。

int a = 0;
 
int *pA = &a;
 
void fun(int &va){}

此时调用: fun(pA);
pA是指针,加个
号后可以转换成该指针指向的对象,此时fun的形参是一个引用值,pA指针指向的对象会转换成引用va。

2.1.5 C语言检索内存情况 内存分配的方式⭐⭐⭐

解析:检索内存:顾名思义,对某段内存进行遍历搜索。
内存分配:
1、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
2、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3、从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

2.1.6 extern”C” 的作用

extern”C”的作用在于C++代码中调用的C函数的声明,或C++中编译的函数要在C中调用。

解析:我们可以在C++中使用C的已编译好的函数模块,这时候就需要用到extern”C”。这是为了避免C++ name mangling,主要用于动态链接库,使得在C++里导出函数名称与C语言规则一致(不改变),方便不同的编译器甚至是不同的开发语言调用。
那么我们来看看C++语音规则和C语音规则有何不同。如果我们定义一个函数:

int fun(int a)

如果是C++编译器,则可能将此函数改名为int_fun_int,(C++重载机制就这么来的)。如果有加上extern”C修饰,则c++编译器会按照C语音编译器一样编译为_fun。

注意:
1)C++调用一个C语言编写的.so库时,包含描述.so库中函数的头文件时,应该将对应的头文件放置在extern “C”{}格式的{}中,。
2)C中引用C++中的全局函数时,C++的头文件需要加extern “C”,而C文件中不能用extern “C”,只能使用extern关键字。
3)也就是extern“C” 都是在c++文件里添加的!

2.1.7 extern容易忽略的知识点

1)头文件声明时加extern,定义时不要加,因为extern可以多次声明,但只有一个定义。
2)extern在链接阶段起作用(四大阶段:预处理–编译–汇编–链接)。

2.1.8 函数参数压栈顺序,即关于__stdcall和__cdecl调用方式的理解

__stdcall和__cdecl都是函数调用约定关键字,我们先来看看__stdcall和__cdecl调用方式的概念:

  1. __stdcall:
    是StandardCall的缩写,是C++的标准调用方式,参数由右向左压入堆栈,由调用者负责把参数压入栈,最后由被调用者负责清除栈的内容。
  2. __cdecl:
    是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:参数也是由右向左压入堆栈;由调用者负责把参数压入栈,最后也由调用者负责清除栈的内容。

总结:
因为以上2点,_cdecl这种调用约定的特点是支持可变数量的参数,比如printf方法,__stdcall不支持可变数量的参数。
假设函数fun()作为调用者调用printf打印东西时,可以输入不同数量的参数,printf作为被调用者,并不知道调用者fun()到底将多少参数压入堆栈,因此printf就没有办法自己清理堆栈,所以只有函数退出之后,由fun清理堆栈,因为fun总是知道自己传入了多少参数。

2.1.9 重写memcpy()函数需要注意哪些问题?

自己动手实现memcpy()时就需要考虑地址重叠的情况。我们来看个简单的例子。有一个5个元素的数组,不妨设为int arr = {1,2,3,4,5};考虑2种情况:

  1. 源地址是arr[2],目标地址是arr[0],自前向后拷贝3个元素后arr为{3,4,5,1,2}
  2. 源地址是arr[0],目标地址是arr[2],自后向前拷贝3个元素后arr为{1,2,1,2,3}
    第一种情况,由低地址向高地址逐个将源地址的元素拷贝到目标地址就行,容易;
    第二种情况需要注意,如果是按第一种情况由低地址拷贝到高地址,需要分3个步骤把arr[0]=1,arr[1]=2,arr[2]=3三个元素逐个拷贝,重点在于第一步是将arr[0]拷贝到arr[2]的位置,这样就会把原来的arr[2]=3改为arr[2]=1,覆盖了原来的值,因此在这种情况,我们需要自后向前拷贝,也就是高地址向低地址拷贝。也就是第一步将arr[2]放到arr[4],第二步将arr[1]放到arr[3],第一步将arr[0]放到arr[2].

看一下代码:

void *Memcpy(void *dst, const void *src, size_t1, size)
{
    char *psrc;  //源地址
    char *pdst;  //目标地址
  
    if(NULL == dst || NULL == src)
    {
        return NULL;
    }
 
    if((src < dst) && (char *)src + size > (char *)dst)  //源地址在前,对应上述情况2,需要自后向前拷贝
    {
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while(size--)
        {
            *pdst-- = *psrc--;
        }
    }
    else   //源地址在后,对应上述第一种情况,直接逐个拷贝*pdst++ = *psrc++即可
    {
        psrc = (char *)src;
        pdst = (char *)dst;
        while(size--)
        {
            *pdst++ = *psrc++;
        }
    }
    return dst;
 
}void *Memcpy(void *dst, const void *src, size_t1, size)
{
    char *psrc;  //源地址
    char *pdst;  //目标地址
  
    if(NULL == dst || NULL == src)
    {
        return NULL;
    }
 
    if((src < dst) && (char *)src + size > (char *)dst)  //源地址在前,对应上述情况2,需要自后向前拷贝
    {
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while(size--)
        {
            *pdst-- = *psrc--;
        }
    }
    else   //源地址在后,对应上述第一种情况,直接逐个拷贝*pdst++ = *psrc++即可
    {
        psrc = (char *)src;
        pdst = (char *)dst;
        while(size--)
        {
            *pdst++ = *psrc++;
        }
    }
    return dst;
}

2.1.10数组到底存放在哪里

  1. 固定数组在函数体内分配(不带static)是在栈中的
  2. 固定数组是全局变量和带static前缀的局部数组是在全局数据的
  3. 固定数组在类中分配是在堆中的
  4. 动态数组(通过malloc或者new出来的空间)不管在函数体中、类中、全局变量都是在堆中

2.1.11 struct和class的区别

首先要知道C++是完全兼容C语言的,因此大家可能会随着学习的深入觉得C++中的struct并没有必要保存,因为struct可以完成的事情,class都可以完成。甚至在C++中struct也可以有构造函数,析构函数,结构体之间也可以继承等等。也就是C++中的struct和class其实意义一样。

总结:C++中存在struct的唯一意义就是为了让C语言程序员有归属感,是为了让C++编译器兼容以前用C语言开发的项目。

答:两者最大区别是struct里面默认的访问控制是public,而class中的默认访问控制是private。

2.1.12 char和int之间的转换;

  1. 首先char与int都分为signed与unsigned类型,默认情况下都是signed类型。
  2. 从长字节数据类型转换为短字节数据类型,会产生截断:如从4字节的int类型转换成1个字节的char类型,则取int数据的最低的一个字节,将这个字节的数据赋给char型数据,且是有符号的,即首位为符号位;而如果是从int转换成unsigned char类型,则整个一个字节都是数据,没有符号位。
  3. 从短字节类型转换为长字节类型,从char转换为int:则在前面的三个字节补符号位,即补上0xffffff(char的首位为1),或0x000000(char的首位为0)。从unsigned char转换为int,则前面补上0x000000.

2.1.13 static的用法(定义和用途)

在C语言中,static作用:“改变生命周期” 或者 “改变作用域”。有以下特性:

  1. static局部变量:局部变量为动态存储,即指令执行到定义处才分配内存,将一个变量声明为函数的局部变量,使其变为静态存储方式(静态数据区),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
  2. static全局变量:全局变量即定义{}外面,其本身就是静态变量,编译时就分配内存,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。
  3. static函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可。
    到了C++的时候,static多了几个其他的作用:
  4. static类成员变量:表示这个成员为全类所共有,对类的所有对象只有一份拷贝,可以借助类名直接访问。必须在类外初始化
  5. static类成员函数:表示这个函数为全类所共有,而且只能访问static静态成员变量,因为这个函数不接收this指针。

2.1.14 const的用法(定义和用途)

const就是常量修饰符,const变量应该在声明的时候就进行初始化,如果在声明常量的适合没有提供值,则该常量的值是不确定的,且无法修改。
const修饰主要用来修饰变量、函数形参和类成员函数:

  1. const常量:定义时就初始化,以后不能更改。
  2. const形参:func(const int a){};该形参在函数里不能改变
  3. const修饰类成员函数:该函数对成员变量只能进行只读操作,就是const类成员函数是不能修改成员变量的数值的。另外不会调用非const修饰的成员函数
    1. 前面使用const 表示返回值为const
    2. 后面加 const表示函数不可以修改class的成员

2.1.15 const常量和#define的区别(编译阶段、安全性、内存占用等)

解析:主要有以下区别

  1. 用#define MAX 255定义的常量是没有类型的(不进行类型安全检查,可能会产生意想不到的错误),所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常量的名字联系起来,define所定义的宏变量在预处理阶段的时候进行替换,在程序中使用到该常量的地方都要进行拷贝替换;
    用const float MAX = 255;定义的常量有类型(编译时会进行类型检查)名字,存放在内存的静态区域中,在编译时确定其值。在程序运行过程中const变量只有一个拷贝,而#define所定义的宏变量却有多个拷贝,所以宏定义在程序运行过程中所消耗的内存要比const变量的大得多
  2. 用define定义的常量是不可以用指针变量去指向的,用const定义的常量是可以用指针去指向该常量的地址的;
  3. 用define可以定义一些简单的函数(宏替换只作替换,不做计算,不做表达式求解),const是不可以定义函数的.
  4. 宏定义的作用范围仅限于当前文件。 而默认状态下,const对象只在文件内有效,当多个文件中出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。 如果想在多个文件之间共享const对象,必须在变量定义之前添加extern关键字(在声明和定义时都要加)。

2.1.16 volatile作用和用法

解析:.volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,编译器对访问该变量的代码就不再进行优化,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。

答:volatile关键词的作用是影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错。

案例:我们来看看以下几个使用volatile的案例:

  1. 中断服务程序中修改的供其它程序检测的变量需要加volatile;
int i=0;
 
int main(void)
{
     ...
     while (1){
        if (i) dosomething();
     }
}
/* Interrupt service routine. */
void ISR_2(void)
{
      i=1;
}

程序的本意是希望ISR_2中断产生时,在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该是volatile int i;

  1. 多任务环境下(如多线程)各任务间共享的标志应该加volatile;
  2. 存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义。
    例如:假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。
int  *output = (unsigned  int *)0xff800000;//定义一个IO端口;
int   init(void)
{
      int i;
      for(i=0;i< 10;i++){
         *output = i;
}

经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,所以编译器最后给你编译编译的代码结果相当于:

int  init(void)
{
      *output = 9;
}

如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
例如:

volatile  int *output=(volatile unsigned int *)0xff800000;//定义一个I/O端口

2.1.17有常量指针 指针常量 常量引用 没有 引用常量

解析:这几个概念面试官偶尔会调皮的问一下,所以还是要区分好几个概念,不要混淆,以上4个概念都可以从最后两个字开始记起,后面两个字代表这是个什么东东,前面两个字代表这个是什么类型:
(1)常量指针:也叫常指针,最后两个字是“指针”,代表这是一个指针,但指向的是一个常量,如下:
int a = 0;
const int *p = &a; //不可以通过p改变a
(2)指针常量:后面两个字是“常量”,代表这是个常量,不过是指针类型的常量,
int a = 0;
int *const p = &a; //从后往前看,这是个指针常量,指向的a的值可以改变,但p本身不可改变
注意:如果从代码来区分常量指针指针常量,那么可以从后往前看const的位置,
const int p = &a //从后往前看,const修饰的是p,所以指针p指向的数值不可变
int *const p = &a; //从后往前看,const修饰的是p,所以指针p本身不可变
(3)常量引用:后两个字是“引用”,那么这个是引用,并且是常量的引用,那么就有两个性质,如下:
double a;
const int &r = a; //正确 性质1:不可通过常量引用r来改变a
const int &r = 10;//正确 性质2:常量引用可以直接引用具体数值
(4)没有引用常量:后面两个字代表这个是常量,前面代表这个是引用类型的常量,然而常量就是常量了,并没有引用类型的常量。

2.1.18没有指向引用的指针,但是有指针的引用

解析:如何理解这句话呢,首先,没有指向引用的指针,因为指针是本质上是指向某一块内存空间的,而引用只是一个变量的别名,本身是没有地址的,如果要创建一个指针指向某个引用,那么其实指向的是这个引用所引用的对象,看下面代码:

int v = 1;
int &ri = v; //整型变量v的引用
int *p = &ri; //指针p其实指向的是变量v

其次,有指针的引用,我们直接看代码:

int v = 1;
int *p = &v;
int *&rp = p;

第一个是要理解int *&rp = p; 这是定义了一个变量rp,还是从后往前看,距离rp左边最近的修饰符决定rp是个什么东东,剩下的就是rp的具体值。因此我们发现距离rp左边最近的是&,代表rp是个引用,所以int *&rp = p; 可以看作int *(&rp )= p; 如果我们把(&rp)当作一个整体,又可以看作int * RP = p;到此为止,我们就可以很明显的知道这句话其实就是定义了一个引用rp指向指针p。

2.1.19 c/c++中变量的作用域

解析:作用域规则告诉我们一个变量的有效范围,它在哪儿创建,在哪儿销毁。变量的有效作用域从它的定义点开始,到和定义变量之前最邻近的开括号配对的第一个闭括号。也就是说,作用域由变量所在的最近一对括号确定。

答:

  1. 全局变量:
    全局变量是在所有函数体的外部定义的,程序的所在部分(甚至其它文件中的代码)都可以使用。全局变量不受作用域的影响(也就是说,全局变量的生命期一直到程序的结束)。如果在一个文件中使用extern关键字来声明另一个文件中存在的全局变量,那么这个文件可以使用这个数据。
  2. 局部变量:
    局部变量出现在一个作用域内,它们是局限于一个函数的。局部变量经常被称为自动变量,因为它们在进入作用域时自动生成,离开作用域时自动消失。关键字auto可以显式地说明这个问题,但是局部变量默认为auto,所以没有必要声明为auto。
  3. 寄存器变量
    寄存器变量是一种局部变量。关键字register告诉编译器“尽可能快地访问这个变量”。加快访问速度取决于现实,但是,正如名字所暗示的那样,这经常是通过在寄存器中放置变量来做到的。这并不能保证将变置在寄存器中,甚至也不能保证提高访问速度。这只是对编译器的一个暗示。
    注意:
    使用register变量是有限制的:(1)不可能得到或计算register变量的地址; (2) register变量只能在一个块中声明(不可能有全局的或静态的register变量(c语言里register关键字可以在全局中定义变))。然而可以在一个函数中(即在参数表中)使用register变量作为一个形式参数。
    一般地,不应当推测编译器的优化器,因为它可能比我们做得更好。因此,最好避免使用关键字register。
  4. 静态变量
    关键字static有一些独特的意义。通常,函数中定义局部变量在函数中作用域结束时消失。当再次调用这个函数时,会重新创建变量的存储空间,其值会被重新初始化。如果想使局部变量的值在程序的整个生命期里仍然存在,我们可以定义函数的局部变量为static(静态的),并给它一个初始化。初始化只在函数第一次调用时执行,函数调用之间变量的值保持不变,这种方式,函数可以“记住”函数调用之间的一些信息片断。这也就是所谓的静态局部变量,具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只在定义自己的函数体内始终可见。
    我们可能奇怪为什么不使用全局变量。static局部变量的优点是在函数范围之外它是不可用的,所以它不可能被轻易改变。这会使错误局部化。
    此外同样存在静态全局变量,具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
  5. 外部变量
    extern告诉编译器存在着一个变量和函数,即使编译器在当前的文件中没有看到它。这个变量或函数可能在一个文件或者在当前文件的后面定义。例如extern int i;编译器会知道i肯定作为全局变量存在于某处。当编译器看到变量i的定义时,并没有看到别的声明,所以知道它在文件的前面已经找到了同样声明的i。
  6. const常量
    const告诉编译器这个名字表示常量,不管是内部的还是用户定义的数据类型都可以定义为const。如果定义了某对象为常量,然后试图改变它,编译器将会产生错误。在C++中一个const必须有初始值。
  7. volatile变量
    限定词const告诉编译器“这是不会改变的”(这就是允许编译器执行额外的优化);而限定词volatile则告诉编译器“不知道何时变化”,防止编译器依据变量的稳定性作任何优化。

2.1.20 c++中类型转换机制?各适用什么环境?dynamic_cast转换失败时,会出现什么情况?(对指针,返回NULL对。引用,抛出bad_cast异常)

解析:C++中,四个与类型转换相关的关键字:static_cast、const_cast、reinterpret_cast dynamic_cast。

  1. static_cast
    特点:静态转换,在编译处理期间。
    应用场合:主要用于C++中内置的基本数据类型之间的转换,但是没有运行时类型的检测来保证转换的安全性。
    用于基类和子类之间的指针或引用之间的转换,这种转换把子类的指针或引用转换为基类表示是安全的;进行下行转换,把基类的指针或引用转换为子类表示时,由于没有进行动态类型检测,所以是不安全的。上行安全下行不安全
    把void类型的指针转换成目标类型的指针(不安全)。
    不能用于两个不相关的类型转换。
    不能把const对象转换成非const对象。
  2. const_cast
    特点:去常转换,编译时执行。不是运行时执行
    应用场合:const_cast操作不能在不同的种类间转换。相反,它仅仅把它作用的表达式转换成常量。它可以使一个本来不是const类型的数据转换成const类型的,或者把const属性去掉。
    去掉const属性:const_case<int*> (&num),常用,因为不能把一个const变量直接赋给一个非const变量,必须要转换。
int main()
{
    const int constant = 26;
    const int* const_p = &constant;
    int* modifier = const_cast<int*>(const_p);
    *modifier = 3;
    cout<< "constant:  "<<constant<<endl;  //26
    cout<<"*modifier:  "<<*modifier<<endl;   //3
    return 0;
}

加上const属性:const int* k = const_case<const int*>(j),一般很少用,因为可以把一个非const变量直接赋给一个const变量,比如:const int* k = j;
const_case只能转换指针或引用 不能转换变量
const int i = 3;
int j = const_cast(i);是不行的

  1. reinterpret_cast
    特点: 重解释类型转换
    应用场合:它有着和c风格强制类型转换同样的功能;它可以转化任何的内置数据类型为其他的类型,同时它也可以把任何类型的指针转化为其他的类型;它的机理是对二进制进行重新的解释,不会改变原来的格式。
  2. dynamic_cast < type-id > ( expression )
    含义:将一个指向基类的指针转换成指向派生类的指针;如果失败,返回空指针。
    该运算符将expression转换成type_id类型的对象。type_id必须是类的指针,类的引用或者空类型的指针。
    a、如果type_id是一个指针类型,那么expression也必须是一个指针类型,如果type_id是一个引用类型,那么expression也必须是一个引用类型。
    b、如果type_id是一个空类型的指针,在运行的时候,就会检测expression的实际类型,结果是一个由expression决定的指针类型。
    c、如果type_id不是空类型的指针,在运行的时候指向expression对象的指针能否可以转换成type_id类型的指针。
    d、在运行的时候决定真正的类型,如果向下转换是安全的,就返回一个转换后的指针,若不安全,则返回一个空指针。
    e、主要用于上下行之间的转换,也可以用于类之间的交叉转换。上行转换时和static_cast效果一样,下行转换时,具有检测功能,比static_cast更安全。
class CBasic{
public:
 
    CBasic(){};
    ~CBasic(){};
    virtual void speak() {     //要有virtual才能实现多态,才能使用dynamic cast,如果父类没有虚函数,是编译不过的
        printf("dsdfsd");
    }
private:
 
};
 
//哺乳动物类
class cDerived:public CBasic{
public:
    cDerived(){};
    ~cDerived(){};
private:
};
 
int main()
{
     CBasic  cBasic;
     CDerived  cDerived;
     
     CBasic * pB1 = new CBasic;
     CBasic * pB2 = new CDerived;
     
     //dynamic cast failed, so pD1 is null.  pB1指向对象和括号里的Derived *不一样,转换失败
     CDerived * pD1 = dynamic_cast<CDerived * > (pB1);    
     
     //dynamic cast succeeded, so pD2 points to  CDerived object       
     //dynamic cast 用于将指向子类的父类指针或引用,转换为子类指针或引用 ,pB2指向对象和括号里的Derived *一样,转换成功    
     CDerived * pD2 = dynamic_cast<CDerived * > (pB2);    
     
     //dynamci cast failed, so throw an exception.             
     CDerived & rD1 = dynamic_cast<CDerived &> (*pB1);   
     
     //dynamic cast succeeded, so rD2 references to CDerived object.
     CDerived & rD2 = dynamic_cast<CDerived &> (*pB2);    
     return 0;
}

2.1.21各种数据类型大小(有问题)

#include<stdio.h>
int main()
{
        printf("int = %d\n",sizeof(int));
        printf("short = %d\n",sizeof(short));
        printf("long int = %d\n",sizeof(long int));
        printf("long long int = %d\n",sizeof(long long int));
        printf("char = %d\n",sizeof(char));
        printf("_Bool = %d\n",sizeof(_Bool));
        printf("float = %d\n",sizeof(float));
        printf("double = %d\n",sizeof(double));
        printf("long double = %d\n",sizeof(long double));
}
输出 
int = 4
short = 2
long int = 4
long long int = 8
char = 1
_Bool = 1
float = 4
double = 8
long double = 12
32位编译器:64位编译器:
char :1个字节char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节)char*(即指针变量): 8个字节
short int : 2个字节short int : 2个字节
int: 4个字节int: 4个字节
unsigned int : 4个字节unsigned int : 4个字节
float: 4个字节float: 4个字节
double: 8个字节double: 8个字节
long: 4个字节long: 8个字节
long long: 8个字节long long: 8个字节
unsigned long: 4个字节unsigned long: 8个字节

2.1.22 各种数据类型与0比较

  1. bool类型和0比较
    不可将布尔变量直接与TRUE、FALSE或者1、0进行比较。
    假设布尔变量名字为flag,它与零值比较的标 准if语句如下:
if (flag)    // 表示flag为真
if (!flag)    // 表示flag为假

其他为不良写法。

  1. 整型变量与零值比较
    应当将整型变量用 “==” 或 “!=”直接与0比较。
    假设整型变量的名字为value,它与零值比较的标准if语句如下:
if (value == 0)  
if(value != 0)
  1. 浮点变量与零值比较
    不可将浮点变量用 “” 或 “!=”与任何数字比较。
    具体的说,实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学记数法。所以浮点数在运算过程中通常伴随着因为无法精确表示而进行的近似或舍入。用"
    "来比较两个double应该相等的类型,返回真值完全是不确定的。计算机对浮点数的进行计算的原理是只保证必要精度内正确即可。
    无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==” 或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
    假设浮点变量的名字为x,
应当将      if (x == 0.0)     // 隐含错误的比较
转化为      if ((x>=-EPSINON) &&(x<=EPSINON)) 或者  if(abs(x) <= EPSINON)
其中EPSINON是允许的误差(即精度)。 
const float EPSINON = 0.000001,至于为什么取0.000001,可以自己按实际情况定义。
  1. 指针变量与零值比较
    应当将指针变量用 “==”或“!=”与NULL比较。
    指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。假设指针变量的名 字为p,它与零值比较的标准if语句如下:
if (p ==NULL)    // p与NULL显式比较,强调p是指针变量
if (p != NULL)

2.1.23 i++和++i 详解

i++ :先引用后增加,先在i所在的表达式中使用i的当前值,后让i加1
++i :先增加后引用,让i先加1,然后在i所在的表达式中使用i的新值

A  operator ++()         //前++
{
	  *this=this+1;
	  return *this;
}
A  operator ++(int)      //后++
{
	  A t=*this;          //先保存一份变量
	  ++(*this);          //调用前++
	  return t;
}

2.1.24 数组名为a,a+1和&a+1的区别

数组名代表整个数组的时候只有两种情况,
1.sizeof(数组名),这里的数组名表示整个数组。
2.&数组名,这里的数组名表示整个数组。

#include<stdio.h>

int main()
{
    int a[]={1,2,3,4};
    printf("%d\n",sizeof(a));   //16 a表示数组的首元素,首地址因此用sizeof计算它们的值,就是整个二维数组所占用的内存空间
	printf(%p\n”,a++);
	printf(%p\n”,a);
    printf("%d\n",sizeof(a+0));      //4 a+0为a[0]的首地址
    printf("%d\n",sizeof(*a));  //4  首元素大小 就相当于 sizeof(*a)=4 =sizeof(a[0]);
    printf("%d\n",sizeof(a+1));  //4 从首元素向后偏移一个整型的地址,即第二个元素的地址
    printf("%d\n",sizeof(a[1]));   //4 a[1]的字节数为4
    printf("%d\n",sizeof(&a));    //4  &a为数组地址和a即a[0]的地址一样
    printf("%d\n",sizeof(&a+1));  //4 跳过整个数组 指向数组后面的一个地址
    printf("%d\n",sizeof(&a[0]));   //4 首元素地址
    printf("%d\n",sizeof(&a[0]+1));
    return 0;
}

2.1.25. 数据声明(Data declarations)用变量a给出下面的定义

a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数 ( An array of ten pointers to functions that take an integer argument and return an integer )

答案是:

a) int a; 			// An integer
b) int *a; 			// A pointer to an integer
c) int **a; 		// A pointer to a pointer to an integer
d) int a[10];		 // An array of 10 integers
e) int *a[10]; 		// An array of 10 pointers to integers
f) int (*a)[10]; 		// A pointer to an array of 10 integers
g) int (*a)(int); 	// A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer

人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试 的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答 案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?

2.1.26 int转字符串字符串转int ? strcat,strcpy,strncpy, memset, memcpy 的内部实现?

strncat:将一个字符数组的前n个拷到目标串,并在后面加上’\0’
strcpy:目标串要有足够的空间放置src,否则出现缓冲区溢出
strncpy:如果n大于src,将src拷贝完后,会一直追加’\0,效率低
			如果n小于src,并不会在dest后追加‘\0’
snprintf:会在拷贝结束后自动添加‘\0’,更加安全。
memset:内存初始化
memcpy:内存拷贝
char* strcat(char* s1,const char* s2)
{
	char* s;
	for(s=s1;*s!='\0';s++);
	for(;(*s=*s2)!='\0';s++,s2++);
	return s1;
}

int strcmp(const char* s1,const char* s2)
{
	for(;*s1==*s2;s1++,s2++)
		if(*s1=='\0')
			return 0;
	return *(unsigned char*)s1<*(unsigned char*)s2?-1:1;
}

char* strcpy(char* s1,const char* s2)
{
	char* s=s1;
	for(s=s1;(*s++=*s2++)!='\0';);
	return s1;
}

char* strncpy(char* s1,const char* s2,size_t n)
{
	char* s=s1;
	for(;n>0&&*s2!='\0';n--)
		*s++=*s2++;
	for(;n>0;n--)
		*s++='\0';
	return s1;
}

void* memset(void *s,int c,size_t n)
{
	const unsigned char uc=c;
	unsigned char* su;
	for(su=s;0<n;++su,--n)
		*su=uc;
	return s;
}

void* memcpy(void* s1,const void* s2,size_t n)
{
	char* su1;
	const char* su2;
	for(su1=s1,su2=s2;n>0;su1++,su2++)
		*su1=*su2;
	return s1;
}

2.2 C++

C和C++的区别和优势

区别

C语言是面向过程的一种编程语言,而C++则是面向对象的一种编程语言。

什么是面向过程?

面向过程就是分析并解决问题,并将解决问题的步骤一步一步的实现,使用时依次调用就行。

什么是面向对象?

面向对象编程就是把问题分解成各个对象,建立对象的目的不是为了完成某一个步骤,而是为了描述某个事物在整个问题的步骤中的行为。

C++优势及缺陷

面向过程的性能比面向对象高,因为类的调用需要实例化,开销比较大,比较耗资源。但是面向过程却没有面向对象那样易于维护,以及易复用,易扩展。由于面向对象有,封装,继承,多态等性质,可以设计出低耦合的系统。

2.2.1 继承和虚继承

解析:因为C++支持多重继承,那么在这种情况下会出现重复的基类这种情况,也就是说可能出现将一个类两次作为基类的可能性。比如像下面的情况

为了节省内存空间,可以将DeriverdA、DeriverdB对Base的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如 下:

class Base
class DeriverdA:public virtual Base; //虚继承
class DeriverdB:public virtual Base; //虚继承
class D:public DeriverdA,DeriverdB;  //普通继承

虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。
注意:不要全部都使用虚继承,因为虚继承会破坏继承体系,不能按照平常的继承体系来进行类型转换(如C++提供的强制转换函数static_cast对继承体系中的类对象转换一般可行的,这里就不行了)。所以不要轻易使用虚继承,更不要在虚继承的基础上进行类型转换,切记切记!

2.2.2 多态的类,内存布局是怎么样的

sizeof计算对象所占内存的大小详解

解析:关于类的内存布局主要是考某个类所占用的内存大小,以下通过几个案例加以分析。

(1)虚继承:如果是虚继承,那么就会为这个类创建一个虚表指针,占用4个字节

#include <stdio.h>  
class A {  
public:  
    int a;  
}; //sizeof(A)=4,因为a是整形,占用4字节
 
class B : virtual public A {  
public:  
    int b;  
};//sizeof(B)=4(A副本)+4(虚表指针占用4字节)+4(变量b占用4字节)=12  
 
class C : virtual public B {       
};//sizeof(c)= 12(B副本)+4(虚表指针) = 16,如果这里改为直接继承,那么sizeof(c)=12,因为此时就没有虚表指针了

(2)多重继承:如果是以虚继承实现多重继承,记得减掉基类的副本

#include <stdio.h>  
class A {  
public:  
    int a;  
};//sizeof(A) = 4  
 
class B : virtual public A {  
};// sizeof(B) =4+4=8  
 
class C : virtual public A {          
};//sizeof(C) =4+4=8  
 
class D : public B, public C{         
};  
//sizeof(D)=8+8-4=12这里需要注意要减去4,因为B和C同时继承A,只需要保存一个A的副本就好了,sizeof(D)=4(A的副本)+4(B的虚表)+4(C的虚表)=12,也可以是8(B的副本)+8(c的副本)-4(A的副本)=

(3)普通继承(含有:空类、虚函数)

class A      //result=1  空类所占空间的大小为1  
{     
};    
 
class B     //result=8  1+4   字节对齐后为 8  
{  
    char ch;     
    virtual void func0()  {  }   
};   
 
class C    //result=8  1+1+4 字节对齐后为 8,没有继承的,此时类里即使出现多个虚函数,也只有一个虚指针
{  
    char ch1;  
    char ch2;  
    virtual void func()  {  }    //也只有一个虚指针
    virtual void func1() {  }    //也只有一个虚指针
};  
   
class D: public A, public C  //result=12  8(C的副本)+4(整形变量d占用4字节)=12  
{     
    int d;     
    virtual void func()  {  }   //继承了C,C里已经有一个虚指针,此时D自己有虚函数,
    virtual void func1()  {  } //也不会创建另一个虚指针,所以D本身就变量d需要4字节
};     
   
class E: public B, public C  //result=20   8( B的副本)+8(C的副本)+4(E本身)=20
{     
    int e;     
    virtual void func0()  {  }   //同理,E不会创建另一个虚指针,所以E本身就变量e需
    virtual void func1()  {  }  //要4字节
}; 

(4)虚继承(多重继承和虚函数)

class CommonBase  
{  
    int co;  
};// size = 4  
   
class Base1: virtual public CommonBase            
{  
public:  
    virtual void print1() {  }  
    virtual void print2() {  }  
private:  
    int b1;  
};//4(父类副本)+4(自己有虚函数,加1个虚指针空间)+4(自身变量b1)+4(虚继承再加1个虚指针空间)=16  
 
class Base2: virtual public CommonBase          
{  
public:  
    virtual void dump1() {  }  
    virtual void dump2() {  }  
private:  
    int b2;  
};//同理16  
   
class Derived: public Base1, public Base2       
{  
public:  
    void print2() {  }  
    void dump2() {  }  
private:  
    int d;  
};//16+16-4+4=32  

前辈总结说:如果不是虚继承的类,即便有虚函数也不会因此增加存储空间,如果是虚继承的类,没有虚函数就添加一个虚指针空间,有虚函数不论多少个,就添加两个虚指针空间。
本人将前辈总结归纳为:如果此时类里有一个或多个虚函数,那么需要加1个虚指针空间,如果还是虚继承,那么需要再加1个虚指针空间,最多就2个虚指针空间。
(5)虚继承与虚函数

class A  
{  
public:  
    virtual void aa() {  }  
    virtual void aa2() {  } //如果此时类里有一个或多个虚函数,那么需要加1个虚指针空间
private:  
    char ch[3];  
}; // 1+4 =补齐= 8   
   
class B: virtual public A //如果还是虚继承,那么需要再加1个虚指针空间,最多就2个虚指//针空间。
{  
public:  
    virtual void bb() {  }  
    virtual void bb2() {  }  
}; // 8(副本)+4(虚继承)+4(虚指针)= 16  

【小结】重要的事情讲三遍!!!
如果此时类里有一个或多个虚函数,那么需要加1个虚指针空间,如果还是虚继承,那么需要再加1个虚指针空间,最多就2个虚指针空间。

2.2.3被隐藏的基类函数如何调用或者子类调用父类的同名函数和父类成员变量

解析:父类的同名函数和父类成员变量被隐藏不代表其不存在,只是藏起来而已,C++有两种方法可以调用被隐藏的函数:
1)用using关键字:使用using后,父类的同名函数就不再隐藏,可以直接调用,如下:

class Child:public Parent
{
public:
    Child(){};
    using Parent::add;
    int add(void){};
};

2)用域操作符,可以调用基类中被隐藏的所有成员函数和变量。
如子类child和父类father都有add()函数,可以通过下面代码实现子类对象调用父类的add()函数:

Child c;
c.Parent::add(10);

2.2.4 C++如何实现多态(讲明多态实现的三个条件、实现的原理)

解析:只要是有涉及到c++的面试,面试官百分百会问到多态相关的问题,尤其是让你解释下多态实现的原理,此时首先要知道多态实现的三个条件:

1)要有继承
2)要有虚函数重写
3)要有父类指针(父类引用)指向子类对象

答:编译器发现一个类中有虚函数,便会立即为此类生成虚函数表vtable。虚函数表的各表项为指向类里面的虚函数的指针。编译器还会在此类中隐含插入一个指针vptr(对 vc 编译器来说,它插在类的内存地址的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr 与 vtable 的关联代码,即将vptr 指向对应的 vtable,将类与此类的vtable 联系了起来。
另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this 指针,这样依靠此 this 指针即可得到正确的 vtable,如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
上面这段话可能有点难以理解,我本人当初也是理解好一会哈,我们直接看个例子:

#include "stdafx.h"
#include <iostream>
#include <stdlib.h>
using namespace std;
 
class Father
{
public:
    void Face()
    {
        cout << "Father's face" << endl;
    }
    virtual void Say()
    {
        cout << "Father say hello" << endl;
    }
};
 
class Son:public Father
{
public:    
    void Say()
    {
        cout << "Son say hello" << endl;
    }
};
void main()
{
    Son son;
    Father *pFather=&son; //隐式类型转换
    pFather->Say();
}

程序输出:Son say hello

我们重点来看这行代码Father *pFather=&son;
此时指向基类的指针pFather已经变成指向具体的类son的this指针,那么我们调用这个pFather父类指针,就相当于调用了等号右边的类即子类son的this指针,这个this所能调用的函数,自然就是子类son本身的函数。即pFather->Say();这行代码调用的是子类的Say()函数。因此我们就成功的实现了用父类指针pFather调用子类函数,也就是实现了多态。

2.2.5对拷贝构造函数 深浅拷贝 的理解 拷贝构造函数作用及用途?

解析:
简单的来说,浅拷贝是增加了一个指针,指向原来已经存在的内存。浅拷贝在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误。
深拷贝是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。深拷贝和浅拷贝的不同之处,仅仅在于修改了下拷贝构造函数,以及赋值运算符的重载。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

提问:什么时候需要自定义拷贝构造函数?

答:默认拷贝构造函数执行的是浅拷贝,对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。

2.2.6析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?

参考:https://www.jianshu.com/p/77f6a0074dc3
解析:
C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源,这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常(析构函数里delete this指针也会造成程序崩溃,因为delete this指针就是要调用析构函数,这样就变成无限循环了),会造成程序崩溃的问题。

答:析构函数不能抛出异常,除了资源泄露还可能造成程序崩溃。

2.2.7什么情况下会调用拷贝构造函数(三种情况)

解析:类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

  1. 一个对象以值传递的方式传入函数体
//全局函数,传入的是对象
void g_Fun(CExample C)
{
    cout<<"test"<<endl;
}
int main()
{
    CExample test(1);
    //传入对象
    g_Fun(test);
}
调用g_Fun()时,会产生以下几个重要步骤:

(1)test对象传入形参时,会先会产生一个临时变量,就叫C吧。
(2)然后调用拷贝构造函数把test的值给C。整个这两个步骤有点像:CExample C(test);
(3)等g_Fun()执行完后,析构掉C对象。

  1. 一个对象以值传递的方式从函数返回
//全局函数
CExample g_Fun()
{
    CExample temp(0);
    return temp;
}
int main()
{
    g_Fun();
}

当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1)先会产生一个临时变量,就叫XXXX吧。
(2)然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3)在函数执行到最后先析构temp局部变量。
(4)等g_Fun()执行完后再析构掉XXXX对象.

  1. 一个对象需要通过另外一个对象进行初始化。
CExample A(100);
CExample B = A; //这句和下句都会调用拷贝构造函数。
CExample B(A);

2.2.8析构函数一般写成虚函数的原因

在实现多态时(基类指针可以指向子类的对象),如果析构函数是虚函数,那么当用基类操作子类的时候(基类指针可以指向子类的对象),如果删除该基类指针时,就会调用该基类指针指向的子类析构函数,而子类的析构函数又自动调用基类的析构函数,这样整个子类的对象完全被释放。这是最理想的结果。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不会像上一段那样调用子类析构函数,这样就会造成子类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。

2.2.9构造函数为什么一般不定义为虚函数

参考:https://www.cnblogs.com/mengfanrong/p/4011342.html
注意:面试时起码回答出第一和第二点,最好还有第三点,第四第五是加分项。
答:

  1. 从存储空间角度:
    大家都知道虚函数相应一个指向vtable虚函数表的指针,而这个指向vtable的指针事实上是存储在对象的内存空间的。如果此时构造函数是虚的,就须要通过vtable来调用,但是对象还没有实例化,也就是内存空间还没有vtable,怎么找vtable呢?所以构造函数不能是虚函数。
  2. 从使用角度:
    虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
  3. 从实现上看:
    vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。
  4. 构造函数不须要是虚函数,也不同意是虚函数,由于创建一个对象时我们总是要明白指定对象的类型,虽然我们可能通过实验室的基类的指针或引用去訪问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
  5. 当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR。因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。并且,仅仅要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE,但假设接着另一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它自己的VTABLE。假设函数调用使用虚机制,它将仅仅产生通过它自己的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。

2.2.10什么是纯虚函数⭐⭐⭐⭐⭐

解析:纯虚函数声明:virtual函数类型 函数名(参数表列)= 0;
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。如果在一个类中声明了纯虚函数,在其派生类中没有对其函数进行定义,则该虚函数在派生类中仍然为纯虚函数。
注意:
(1)纯虚函数没有函数体;
(2)最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”;
(3)这是一个声明语句,最后有分号。

Ps:抽象类:不用定义对象而只作为一种基本类型用作继承的类叫做抽象类(也叫接口类),凡是包含纯虚函数的类都是抽象类,抽象类的作用是作为一个类族的共同基类,为一个类族提供公共接口,抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。

2.2.11静态绑定和动态绑定的介绍⭐⭐⭐⭐

解析:

静态类型:对象在声明时采用的类型,在编译期既已确定;
动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;

静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期
动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期

非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)

2.2.12 C++所有的构造函数 ⭐⭐⭐

在面向对象编程中,创建对象时系统会自动调用构造函数来初始化对象,构造函数是一种特殊的类成员函数,它有如下特点:

1.构造函数的名字必须和类名相同,不能任意命名;
2.构造函数没有返回值;
3.构造函数可以被重载,但是每次对象创建时只会调用其中的一个;

C++中的构造函数可以分为4类:

  1. 默认构造函数。
以Student类为例,默认构造函数的原型为
Student();//没有参数
Student(int num=10,int age=10);
  1. 初始化构造函数
Student(int num,int age);//有参数
  1. 复制(拷贝)构造函数
Student(const Student&);//形参是本类对象的引用
  1. 转换构造函数
Student(int r)//形参是其他类型变量,且只有一个形参

1)默认构造函数和初始化构造函数在定义类的对象的时候,完成对象的初始化工作。

Student s2(1002,1008;

2)复制构造函数用于复制本类的对象。
默认的复制构造函数可能会发生【浅拷贝】的问题

Student(Student &b)
{
    this.x=b.x;
    this.y=b.y;}

Student (s2);//将对象s2复制给s3。注意复制和赋值的概念不同。
Student s4;
s4=s2;//这种情况叫做赋值,自己体会吧

3)转换构造函数的作用是将一个其他类型的数据转换为一个类的对象。转换构造函数也是一种构造函数,它遵循构造函数的一般原则,我们通常把仅有一个参数的构造函数用作类型转换,所把它称为转换构造函数。
转换构造函数中的类型数据可以是普通类型,也可以是类类型。
下面的转换构造函数,将int类型的r转换为Student类型的对象,对象的age为r,num为1004

Student(int r)
{
    int num=1004int age= r;
}

2.2.13 重写(覆盖)、重载、隐藏(重定义)的区别⭐⭐⭐⭐⭐

解析:至于什么是重写、重载(overload)、覆盖,读者可以自行了解,这是必须掌握的重点概念哦。

  1. 重写(override):
    函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致
    1、不在同一个作用域(分别位于派生类与基类)
    2、函数名字相同
    3、参数相同列表(参数个数,两个参数列表对应的类型)
    4、基类函数必须有 virtual 关键字,不能有 static,大概是多态的原因吧…
    5、返回值类型(或是协变),否则报错
    6、重写函数的访问修饰符可以不同。尽管 virtual 是 private 的,派生类中重写改写为 public,protected 也是可以的
  2. 重载(overload):
    几个具有不同参数列表(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型
    1、相同的范围(在同一个作用域中)
    2、函数名字相同
    3、参数不同列表
    4、virtual 关键字可有可无
    5、返回类型可以不同
  3. 重定义(隐藏Hide):
    隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏

(1)重写和重载主要有以下几点不同。

范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
virtual的区别:重写的基类中被重写的函数必须要有virtual 修饰,而重载函数和被重载函数可以被virtual修饰,也可以没有。

(2)隐藏和重写、重载有以下几点不同。

与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。
当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。
说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。

2.2.14成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?⭐⭐⭐⭐

从概念上讲,调用构造函数时,对象在程序进入构造函数函数体之前被创建。也就是说,调用构造函数的时候,先创建对象,再进入函数体。所以如果类成员里面有引用数据成员与const数据成员,因为他们在创建时初始化,若是在构造函数中初始化则会报错。

只有构造函数可以使用初始化列表语法。

class MyClass
{
private:
    int a;
    int b;
    const int max;
};
 
MyClass(int c)
{
    a = 0;
    b = 0;
    mac = c;//这里会出错 const数据成员若是在构造函数中初始化则会报错。
}

正确的是:
MyClass(int x):a(0),b(0),max(x)
{
 
}
 
MyClass(int x):max(x)
{
    a = 0;
    b = 0;
}

对于普通数据类型,复合类型(指针,引用)等,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的。对于用户定义类型(类类型),结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体时已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,这时调用一个构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)。

#include<iostream>
Using namespace std;
Class A
{
Public:
    A()
    {
        cout<<"A()"<<endl;
    }
    A(int a)
    {
        value = a;
        cout<<"A(int"<<value<<")"<<endl;
    }
    A(const A& a)
    {
        value = a.value;
        cout<<"A(const A& a):"<<value<<endl;
    }
    int value;
};
 
class B
{
public:
    B():a(1)
    {
        b = A(2);
    }
    A a;
    A b;
};
 
int main()
{
    B b;
    system("pause");
}

以上代码对于变量a使用初始化列表初始化,对于变量b使用构造函数初始化;

对于用户定义类型,使用列表初始化可以减少一次默认构造函数调用过程.简单的来说,对于用户定义类型:
1)如果使用类初始化列表,直接调用对应的构造函数即完成初始化
2)如果在构造函数中初始化,那么首先调用默认的构造函数,然后调用指定的构造函数,要调用2次,所以不推荐在构造函数内初始化
问题:引用是否能实现动态绑定,为什么引用可以实现

答:只有指定为虚函数的成员函数才能进行动态绑定,且必须通过基类类型的引用或指针进行函数调用,因为每个派生类对象中都拥有基类部分,所以可以使用基类类型的指针或引用来引用派生类对象。而指针或引用是在运行期根据他们绑定的具体对象确定。

2.2.15如何避免编译器进行的隐式类型转换;(explicit)⭐⭐⭐⭐

explicit关键字的作用就是防止类构造函数的隐式自动转换. explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了.但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数

2.2.16 C++11新特性

2.2.16.1 关键字及新语法

  1. auto关键字及用法
    使用auto的时候,编译器根据上下文情况,确定auto变量的真正类型
  2. nullptr关键字及用法
    nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,由于NULL实际上代表的是0
  3. for循环语法
  4. Long long 类型
  5. 列表初始化
  6. Long long
  7. constepr

2.2.16.2 STL容器

  1. Array
  2. Forward_list
  3. Unordered_map
  4. Unordered_set
  5. Cbegin 和 cend

2.2.16.3 多线程

  1. thread
  2. atomic
  3. condition_variable

2.2.16.4 智能指针内存管理

  1. auto_ptr:
    不可以用于容器,不建议使用,不支持复制和赋值,如果进行了赋值和复制操作,并不报错
  2. unique_ptr:
    不支持复制和赋值,直接赋值会报错,同一时刻对象仅能拥有一个unique_ptr指向自身
  3. shared_ptr:
    解决指针间对象共享所有权的问题,auto_ptr是独享,允许多个指针指向同一个对象。基于引用计数,不要一个原始指针初始化多个shared_ptr,避免循环使用避免内存泄露。
    对象初始化时应用数为1,指向对象成为另一个对象副本,引用数加一,析构减一。
  4. weak_ptr:
    不控制对象生命周期的智能指针,只提供了管理对象的访问手段,用于协助shared_ptr的工作,用于观测资源的使用情况。use_count()可以一观察资源的应用数。

2.2.16.5 其他

  1. function
  2. bind
  3. lambda
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何蔚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值