C++ 语法基础篇

C++程序基础

C和C++的区别是什么

  C是一门结构化的语言,它的重点在于算法和数据结构。对于语言本身而言,C++是兼容C的。C语言程序设计首要考虑的是如何通过一个过程,对输入的数据运算处理,得到输出。对于C++,首要考虑的是如何构造一个对象模型,这个模型能够很好的配合应对各种问题,这样可以通过获取对象的状态信息得到输出或者实现对过程的控制。C和C++的最大区别在于解决问题的思想方法不一样。
  C++在C的基础上引入了重载、内联函数、异常处理等,同时拓展了面向对象设计的内容,如类、继承、虚函数、模板和容器类等。
  在C++中不仅需要考虑数据封装,还需要考虑对象粒度的选、对象接口的设计和继承、组合与继承的使用等问题。

C++程序的内存划分情况


  图片来源:https://blog.csdn.net/AngelDg/article/details/104871782
  内核空间:存放操作系统相关的代码和数据。
  栈:非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  内存映射段:高效的I/O映射方式,用与装载一个共享的动态内存库。用户可以使用系统创建共享内存,用做进程间通信。
  堆:用于程序运行时动态内存分配的区域,堆是可以向上增长的。
  数据段:存储全局数据和静态数据。
  代码段:存放可执行的二进制代码。

i++和++i哪个效率更高

  如果是内建数据类型时,两者的效率相差不大,因为编译器生成的汇编语言对两个没有区别(如果使用时其数值需要严格区分先后顺序,那么需要考虑区别),所以从效率上看两者完全一样。如果是自定义类型(通常指类),那么前缀式(++i)和后缀式(i++)是有很大区别的。其中,前缀式可以返回对象的引用,但后缀式因为需要保存自增之前的值,所以必须返回值。后缀式返回值的视情况下必然导致了较大的复制开销,所以单纯只是为了增减的情况下应该尽可能的使用前缀式。

选择编程风格良好的比较语句

  对于比较,常见的有bool型、int型、double型、指针类型的比较,这几类比较最好采用特征明显的比较语句,示例如下:
  bool型比较:

//使用这种形式能够方便看出这是bool型的判断
bool flag=flase;
if(flag)
    cout<<"True"<<endl;
if(!flag)
    cout<<"False"<<endl;

  int型比较:

int value=0;
if(value==0)
    cout<<"value is 0"<<endl;
if(value!=0)
    cout<<"value is not 0"<<endl;

  double型比较:

double value=0;
const double EPSILON=0.0000000001;
if(value>=-EPSILON&&value<=EPSILON)
    cout<<"value is 0"<<endl;
if(value<-EPSILON||value>EPSILON)
    cout<<"value is not 0"<<endl;
//其中EPSILON为浮点数比较使用的精度
//C++中有定义好的宏DBL_EPSILON可以使用

  指针的比较:

int* p=nullptr;
if(p==nullptr)
    cout<<"p point to nothing"<<endl;
if(p!=nullptr)
    cout<<"p point to something"<<endl;
//建议使用nullptr,而不是使用NULL,虽然NULL并不会出错

不使用任何中间变量交换两个int型的数值

  交换int型数值有三种方法,使用中间变量、使用加减法、使用异或运算,分别如下所示:

void swap1(int & a,int & b){
    int temp=a;
    a=b;
    b=temp;
}
void swap2(int & a,int & b){
    a+=b;
    b=a-b;
    a-=b;
}
void swap3(int & a,int & b){
    a^=b;
    b^=a;
    a^=b;
}
//使用加减法可能有溢出的风险,建议使用异或运算的方法

include和#include"head.h"有什么区别

  尖括号<>表明这个文件是一个工程或者标准头文件。查找过程中会检查预定义的目录,如果文件名使用引号括起来,查找该文件时将从当前文件目录中寻找,然后在标准位置寻找文件。

C++中main函数执行完还会执行其他语句吗

  程序退出的时候一般还会有释放资源的操作,程序退出的方式有很多种,exit()退出,用户使用结束控制符终止程序等等,因此需要一种与程序退出方式无关的方法来处理程序结束时的必要处理。方法就是使用atexit()函数来注册程序正常终止时需要被调用的函数。
  该函数的原型如下所示:

//该函数的参数为一个函数指针
int atexit(void (*)(void));

  使用方法如下所示:

#include<bits/stdc++.h>
using namespace std;
void fun1(){
    std::cout << "calling fun1()" << endl;
}
void fun2(){
    std::cout << "calling fun2()" << endl;
}
int main(void){
    atexit(fun1);
    atexit(fun2);
    printf("main -- Hello world!\n");
    return 0;
}
/* 运行结果如下:
main -- Hello world!
calling fun2()
calling fun1()
*/

预处理、const、static与sizeof

说明#define和const的区别

  #define定义的宏只是用来做文本替换的。当程序编译时,编译器会先把#define定义的宏进行替换,然后在进行编译。因此,#define常量是一个Compile-Time的概念,他的生命周期止于编译期,它存在于程序的代码段,在实际程序中它就是一个常数、一个命令中的参数,并没有实际的存在。
  const常量存在于程序的数据段,并在堆栈分配了空间。const常量是一个Run-Time的概念,他在程序中确确实实存在着并可以被调用、传递。const常量有数据类型,而宏常量没有数据类型,仅是一个文本的替换的标记符号。编译器可以对const常量进行类型安全检查。

C++中的const有什么作用

  (1)const用于定义常量:const定义的常量,编译器可以对其进行数据静态类型的安全检查。
  (2)const修饰函数形参:当输入的参数为用户自定义类型或者抽象类型时,应该将“值传递”改为“const&传递”,可以提高效率,并且避免函数对形参进行不符合预期的修改。
  (3)const修饰函数的返回值:如给“指针传递”的函数返回值加上const,则返回值不能被直接修改,且该返回值只能被赋值给加上const修饰的同类型指针。
  (4)const修饰类的成员函数(定义处修饰):任何不会修改数据成员的函数都应用cosnt修饰,这样,如果不小心修改了数据成员或者调用了非const成员函数时,编译器都会报错。使用方法如下所示:

int getCount(void)const;

C++中static有什么作用

  (1)修饰局部变量
  ⼀般情况下,对于局部变量在程序中是存放在栈区的,并且局部的⽣命周期在包含语句块执⾏结束时便结束了。但是如果⽤static关键字修饰的话,该变量便会存放在静态数据区,其⽣命周期会⼀直延续到整个程序执⾏结束。但是要注意的是,虽然⽤static对局部变量进⾏修饰之后,其⽣命周期以及存储空间发⽣了变化,但其作⽤域并没有改变,作⽤域还是限制在其语句块。该变量的内存只被分配⼀次,因此其值在下次调⽤时仍维持上次的值;
  (2)修饰全局变量
  对于⼀个全局变量,它既可以在本⽂件中被访问到,也可以在同⼀个⼯程中其它源⽂件被访问(添加extern进⾏声明即可)。⽤static对全局变量进⾏修饰改变了其作⽤域范围,由原来的整个⼯程可⻅变成了本⽂件可⻅。
  (3)修饰函数
  ⽤static修饰函数,情况和修饰全局变量类似,也是改变了函数的作⽤域。
  (4)修饰类
  如果C++中对类中的某个函数⽤static修饰,则表示该函数属于⼀个类⽽不是属于此类的任何特定对象;如果对类中的某个变量进⾏static修饰,则表示该变量属于所有的对象所有,存储空间中只存在⼀个副本,可以通过类和对象去调⽤。
  (5)类成员/类函数声明 static
  在类中的static成员变量属于整个类所拥有,对类的所有对象只有⼀份拷⻉;
  在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因⽽只能访问类的static成员变量;
  static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;
  由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针,正因为没有this指针,所以static类成员函数不能访问⾮static的类成员,只能访问static修饰的类成员;
  static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每⼀个对象分配⼀个vptr指针,⽽vptr是通过this指针调⽤的,所以不能为virtual。

补充:静态⾮常量数据成员,其只能在类外定义和初始化,在类内仅是声明⽽已。

sizeof有哪些用途

  sizeof可以与存储分配和I/O系统那样的例程进行通信;可以查看某个类型的对象在内存中所占的字节数;
  在动态分配一个对象时,可以让系统知道要分配多少内存;
  便于一些类型的扩充,windows中有很多结构类型都具备一个专门表示所占字节大小的字段;
  操作数的字节数可能在是现实时有可能出现变化,建议在设计操作数字节大小的使用使用sizeof来代替常量计算;
  如果操作数是函数中的数组形参或者函数类型的形参,则可以用sizeof给出其指针的大小。

sizeof作用在不同类型上有什么区别

  (1)作用于普通变量,可以计算出普通变量所占空间的字节数。
  (2)作用于数组和指针,如果数组变量被传入函数中,那么使用sizeof运算和对指针的运算没有区别,因为数组传入函数时退化成了指针,所以结果是指针所占的字节数,比如64位的平台下都是8字节。否则,对数组使用sizeof运算符会得到数组所占的空间字节数,指针会得到指针所占的字节数(大小固定)。
  (3)作用于一般的结构体和类,C++中结构体可以像类一样使用,对结构体而言,所有的成员变量都相当于是public类型,考虑到编译器会做字节对齐处理,所以sizeof得到的结果并不是简单的把所以的成员变量所占字节数相加。详细的对齐规则可以参考结构体内存对齐
  (4)作用于含有虚函数的类对象,对于类对象而言,普通函数不占用内存,但是只要有虚函数,就会占用一个指针大小的内存,原因是系统多用了一个指针维护这个类的虚函数表。

宏和内联函数的区别与联系

  区别:
  (1)内联函数在编译是展开,宏在预编译的时候展开;
  (2)编译时,内联函数可以直接被嵌入到目标代码中,宏只是简单的文本替换;
  (3)内联函数能够完成诸如类型检测、语句是否正确等编译功能,宏就不具有这样的功能;
  (4)宏不是函数,而内联函数是函数;
  (5)宏定义时需要小心处理宏的参数,一般需要使用括号括起来,否则可能出现二义性,内联函数定义时不会出现二义性;
  联系:
  内联函数的出现就是为了取代宏定义函数的表达形式,因为内联函数消除了宏的缺点,同时又很好的继承了宏的优点。

内联函数为什么可以取代表达式形式的宏

  inline定义的内联函数,其代码被放在符号表中,当使用时直接进行替换(像宏一样展开),没有了调用的开销,效率也很高;类的内联函数也是一个真正的函数。编译器在调用内联函数的时候会检查参数,保证调用的正确;也会对其进行一系列和函数相同的检查,以消除它的隐患和局限性,使用时就像一个真正的函数一样。

内联函数的使用场合

  适合使用的场景:
  首先,使用内联函数可以完全取代表达式形式的宏。内联函数在C++类中应用最广的应用是用来定义存取函数。我们定义的类中一般把数据成员定义成私有或者保护的,这样对于这些数据成员的读取必须使用接口函数来进行。如果我们把这些函数定义成内联函数的话,可以获得比较好的效率。
  不适合使用的场景:
  如果函数中有循环等执行时间较长的代码,函数执行的的开销远远大于调用的开销,那么定义为inline的效果并不会有很大提高。如果内联函数太多,那么会导致总代码量增大,消耗更多的内存。类的析构函数和构造函数可能会有一些隐藏的行为,比如执行基类或者成员对象的构造函数,所以不要随便将析构和构造函数作为内联函数或直接定义在类的声名中。

引用和指针

指针和引用的区别

  (1)初始化要求不同。引用创建时必须初始化,即引用到一个有效的对象。而指针在定义的时候不必初始化,可以在定义后面的任何地方进行重新赋值。
  (2)可修改性不同。引用一旦被初始化为一个对象的引用,那么他就不能被改编成另一个对象的引用;而指针在任何时候都可以改变为另一个对象。给引用赋值并不改变它和原始对象的绑定关系。
  (3)不存在空引用。引用不能是空的,初始化的时候必须指向某个对象而指针则可以为nullptr,不需要总是指向某些对象。所以指针更加灵活,但是也更容易出错。
  (4)测试需要的区别。由于引用不会指向空值,这意味着使用引用之前不需要测试引用的合法性;而指针则需要经常进行判空测试,所以使用引用要比使用指针更加高效。
  (5)应用的区别。如果一旦指向某个对象后便不会更改,那么应该使用引用;如果可能在不同时刻指向不同的对象,那么就该使用指针。

各种指针的定义

  void (*f)(int,int);  f是指向void max(int x,int y)类型的指针。
  int *fn();  fn是一个返回int指针类型的函数。
  const int *p;  p是一个指针,指向一个int常量,即不可以使用p改变int常量的值。
  int *const q;  q是一个const指针,不能改变q指向的地址,但是可以改变其指向地址所存放的数据。
  const int * const ptr;  ptr是一个指向常量的const指针,即不可以改变ptr指向的地址,也不可以改变地址中的值。

什么是野指针

  野指针是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为都会进行判断。但是野指针很危险,因为对野指针判空不起作用。出现野指针的主要原因有:指针变量没有被初始化;指针被free之后没有被置为空。

malloc/free与new/delete的比较

  相同之处:
  他们都可以用于申请动态内存和释放动态内存。
  不同之处:
  malloc/free是标准库函数,new/delete是C++的运算。
  malloc需要手动计算分配空间的大小,而new不用;
  对象在创建之前需要自动执行构造函数,消亡之前需要自动执行析构函数。malloc/free无法满足动态对象的要求,因为他们属于库函数,不是运算符,所以不在编译器控制权限之内,无法把构造函数和析构函数的任务强加于malloc/free。
  malloc/free是标准库函数,支持覆盖;new/delete是运算符,不能重载。
  补充:定位new
  可以在程序中定义一块空间,然后使用new从这个空间上进行分配,但是需要程序员自己控制每次分配的起始位置。

什么是句柄

  句柄是windows中一个重要的概念,在很多地方都扮演者重要的角色。在windows中,句柄用来标识项目,这些项目包括:模块、任务、实例、文件、内存块、菜单、控制、字体、资源、GDI对象等等。
  windows是一个以虚拟系统为基础的操作系统。在这种系统环境下,windows内存管理器经常在内存中来回移动对象,以此来满足各种应用程序的内存需要。这就意味这对象的地址经常变动,所以windows为各个应用程序腾出一些内存地址,并专门用来登记各个对象的地址的变化,而这些用于管理的地址本身是不变的。windows内存管理器在移动对象在内存中的位置以后,会把对象新的地址告知句柄地址来保存,通过间接寻址就可以访问到资源。这个地址在对象装载时由系统分配,并在卸载时释放给系统。

指针和句柄的区别

  句柄和指针都是地址,不同之处在于:句柄所指的可以是一个很复杂的结构,并且可能与系统有关,通常由系统负责维护其相关资源。而指针也可以指向复杂的结构,但是通常是由用户定义的,所有必须的工作都需要用户完成,特别是在删除的时候。

字符串

  此章节主要为使用代码实现相关的字符串操作,没有概念性的问题,暂时省略。

位运算与嵌入式编程

设置或清除特定的位

  在某种场景下,需要设置某个数值的固定bit位,比如int型的bit3为1,或者清除其bit3位为0,同时保持其他位不变。可是使用位操作来实现:

void set_bit_n(int &val,int n){
    const int flag = 0x1 << n;
    val |= flag;
}
void clear_bit_n(int &val,int n){
    const int flag = 0x1 << n;
    val &= ~flag;
}

计算一个字节中由多少bit是1

  使用位的&运算来统计,每次处理一位即可:

int calculate(unsigned char ch){
    int count = 0;
    unsigned char comp = 0x1;
    for(int i=0; i<sizeof(ch)*8; i++){
        if((ch&comp)!=0)
            count++;
        comp<<=1;
    }
    return count;
}

列举并解释C++中的4种运算符转化及其不同点

  (1)const_cast操作符:用来帮助调用那些应该使用却没有使用const关键字的函数,即解除const限定的运算符,使其能够间接更改特定属性。
  (2)dynamic_cast操作符:如果启动了支持运行时类型信息(RTTI),dynamic_cast可以有助于判断运行时所指向对象的确切类型。可以将一个基类指针指向许多不同的子类型,然后将被转型为基础类的对象还原为原来的类。不过,限于对象指针的类型转换,而非对象变量。
  (3)reinterpret_cast操作符:将一个指针转换为其他类型的指针,新类型的指针与旧指针可以毫不相关。通常用于某些非标准的指针数据类型转换,例如将void*转为char*。
  (4)static_cast操作符:它能在相关的对象和指针类型之间进行类型转换。有关的类之间必须通过继承、构造函数或者转换函数发生联系。通常情况下,static_cast操作符大多用于数域较宽的类型转换为较小的类型。

C++面向对象

struct和class的区别

  C语言的struct和C++的class的区别:struct只作为一种复杂的数据结构类型定义,不能用于面向对象编程。
  C++中的struct和class的区别:对于成员访问权限以及继承方式,class中默认的private的,而struct中的则是public的。class还可以用于表示模板类型,struct不行。C++中的struct也可以使用成员函数。

与全局对象相比,使用静态数据成员有什么优势

  静态数据成员是属于类作用域的,所以不存在程序中其他全局命名冲突的可能性。
  使用静态数据成员可以隐藏信息,因为静态成员可以是private的,但是全局对象不能。

哪些情况只能使用initialization list,而不能使用assignment

  一般情况下,使用初始化列表和赋值操作最终的结果都是相同的。不同之处在于初始化的位置不同,对于const和reference类型的成员变量,只能够被初始化而不能做赋值操作,因此只能使用初始化列表;还有一类情况是类的构造函数需要调用基类构造函数的时候。

C++中的空类默认会产生哪些类成员函数

  C++中默认产生的构造函数有默认构造函数、复制构造函数、析构函数、赋值构造函数以及取地址运算符。

构造函数explicit与普通构造函数的区别

  explicit构造函数用来防止隐式转换,普通构造函数会被隐式调用,而explicit函数构造函数只能被显示调用。

class Test1{
public:
    Test1(int n){num=n;}
private:
    int num;
};
class Test2{
public:
    explicit Test2(int n){num=n;}
private:
    int num;
};

Test1 t1=12;    //隐式调用构造函数
Test2 t2=12;    //编译不通过,不能隐式调用构造函数
Test2 t3(12);   //正确,调用构造函数

复制构造函数是什么?什么是深复制和浅复制

  赋值构造函数是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构造及初始化。以下情况会调用复制构造函数:
  (1)一个对象以值传递的方式进入函数体
  (2)一个对象以值传递的方式从函数返回
  (3)一个对象需要通过另外一个对象进行初始化
  深复制和浅复制的区别通常再成员变量为指针的时候才会表现出来。浅复制的指针是直接进行成员变量的复制,两个指针还是指向了同一个内存区域;深复制则是创建一个与旧对象无关的独立复制,即新开辟一份独立的内存空间复制构造的对象。

复制构造函数和赋值函数有什么区别

  两者的区别有三:
  (1)复制构造函数是使用一个对象初始化一个内存区域以创建一个全新的对象,而赋值函数是对一个已经被初始化的对象使用operator=操作。区别如下所示:

class A;
A a;
A b=a;  //调用复制构造函数
A c(a); //调用复制构造函数

A d;
d=a;    //调用赋值函数

  (2)一般来说在数据成员包含指针对象的时候,需要应对两种不同的处理需求:复制指针对象,引用指针对象。复制构造函数大多数时候时复制,赋值函数则是引用对象。
  (3)实现不一样。复制构造函数本质是一个构造函数,通过调用时传入的参数来初始化对象;而赋值函数则是把一个对象赋值给一个原本就存在的对象,那么首先需要判断两个对象是不是同一个对象,如果是同一个则不需要处理;其次如果涉及到指针,则需要先释放原有的内存,然后再赋值。

什么是临时对象?临时对象在什么情况下产生?

  临时对象是不可见的,不会出现在程序中。大多数情况下,产生临时变量会影响程序的执行效率,所以尽量避免临时对象的产生。通常以下两种情况会产生临时对象:
  (1)参数按值传递
  (2)返回值按值传递

C++中的重载是如何实现的?如何正确声明重载的函数?

  C++中的函数名在经过编译器处理后包含了原函数名、函数参数数量及返回类型信息,因此同名函数可以通过不同的数据类型或者不同的参数个数来区分,以此实现函数的重载。
  就函数的返回值而言,C++的重载中无法只通过返回值来区分两个函数,所以只有返回值不同的函数不是合法有效的重载;就函数的参数而言,没有参数和全部是默认参数可以视为一样,不足以区分参数列表,const以及引用符号&的添加与否也不能区分一个参数。另外,一组重载函数中,只能有一个函数被指定为extern "C"。以下为说明示例:

//不正确
//const不足以区分两个函数
int calc(int ,int);
int calc(const int ,const int);

//不正确
//只有返回值不同也不足以区分两个函数
int get();
double get();

//不正确
//无参数和全部是默认参数也不足以区分两个函数
int set();
double set(int val=0);

//正确
int reset(int *);
double reset(double *);

重载(overload)、重写(overwrite)、覆盖(override)的区别

  重载(overload)是指在同一范围定义的同名函数,也就是同一个函数的不同版本,重载的函数有以下特征:
  (1)函数名必须相同;
  (2)参数列表必须不同,与参数列表的顺序无关;
  (3)返回值类型可以不相同。
  重写(overwrite)是指派生类的函数屏蔽了与其同名的基类函数。主要有两种情况:
  (1)如果派生类的函数与基类的函数同名,但是参数不同。那么无论是否有virtual关键字,基类的函数都会被隐藏。(对于基类和派生类来说,其实就是一个新的函数。)
  (2)如果派生类的函数与基类的函数同名,并且参数相同,如果基类没有virtual关键字,基类的函数也会被隐藏。(不适用于virtual的多态,所以会隐藏。)
  覆盖(override)也是用于实现C++多态性的,即派生类重新编写了父类声明为virtual的函数,覆盖的特征如下:
  (1)不同的范围,分别位于派生类和基类;
  (2)函数名相同;
  (3)参数列表完全相同;
  (4)基类函数必须有virtual关键字。

C++继承与多态

C++类中的三种权限

  (1)public
  public为公有成员,外部可以访问公有成员,可以调用公有成员函数或者访问修改公有成员变量。
  (2)private
  private为私有成员,私有成员对于类外部而言是不可以访问的,但是可以通过公有成员函数实现数据交互。对于类内部而言,公有成员可以访问同类对象实例的私有成员,也就是说私有权限是对整个类而言的。
  (3)protected
  protected是保护成员,protected成员的权限特性只有在继承的时候才能显示出来。对于外部世界而言,protected和private相似,对于派生类而言,基类的protected与public相似。

C++类继承中的三种关系

  继承概念中的实现方式有两种:实现继承和接口继承
  (1)实现继承:实现继承是指可以直接使用基类的属性和方法而无需额外的编码。
  (2)接口继承:接口继承是指仅使用属性和方法的名称,但是子类必须提供实现的能力。
  其中继承主要有三种关系:public、private、protected
  (1)public
  public继承是一种接口继承,派生类可以代替基类完成接口所声明的行为。从语法的角度来说,public继承会保留基类中成员的可见性不变。
  (2)private
  private继承是一种实现继承,派生类不能代替基类完成基类接口声明的行为。从语法角度来说,private继承会将基类中public和protected成员变为自己的private成员。
  (3)protected
  protected继承是一种实现继承,派生类不能代替基类完成基类接口声明的行为。从语法角度来说,protected继承会将基类中public成员变为自己的protected成员。
  总而言之,不同继承方式对于成员属性影响结果如下所示:

继承方式 public成员 protected成员 private成员
public继承 public protected 不可见
protected继承 protected protected 不可见
private继承 private private 不可见

虚函数的实现原理

  从表象上看在基类的函数前加上virtual关键字,在派⽣类中覆盖(override)该函数,运⾏时将会根据对象的实际类型来调⽤相应的函数。如果对象类型是派⽣类,就调⽤派⽣类的函数,如果是基类,就调⽤基类的函数。
  实际上,当一个类中包含虚函数的时候,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址。同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们的定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。
  运行时,如果一个基类指针或者引用指向派生类,那么当调用函数时,就会根据实际所指真正对象的虚函数表指针去寻找虚函数的地址,从而调用正确的函数。

继承关系中编译器如何处理虚函数表

  对于派生过程而言,编译器建立虚函数表的过程一共有三个步骤:
  (1)拷贝基类的虚函数表,如果是多继承就拷贝所以每个有虚函数基类的虚函数表;
  (2)其中有一个基类的虚函数表和派生类自身的虚函数表共用,也称某个基类为派生类的主基类;
  (3)查看派生类中是否有覆盖(override)基类中的虚函数,如果有,则替换成已经覆盖(override)的虚函数地址;查看派生类是否有属于自身的虚函数,如果有,就追加到自身的虚函数表中。
  示例如下:

构造函数与虚函数

  (1)构造函数中调用虚函数并不会起到虚函数的作用。
  基类构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还未初始化。如果基类构造函数期间向下匹配到派生类,派生类的函数必定涉及到派生类的数据成员,但是这些成员都没有初始化,因此访问未初始化的部分是十分危险的,因此虚函数机制在构造函数中调用时并不会起作用。
  (2)构造函数不能是虚函数。
  从实现层面来看,由于虚函数表指针在构造函数调用后才建立,因此构造函数不可能成为虚函数。从实际使用上来看,由于构造函数只起到了初始化的作用,在对象的声明周期中只会运行一次,不涉及对象的多态性,因此没必要成为虚函数。

简述多重继承及其优缺点

  实际生活中,一些事物可能同时拥有两个或者两个以上的事务属性。为了解决这个问题,C++引入了多重继承。比如,人(Person)可以派生出作者(Author)和程序员(Programmer),而程序员作者(Programmer_Author)可以有作者(Author)和程序员(Programmer)两种属性,即既是程序员也是作者。
  多重继承的优点是:对象可以调用多个基类的接口。
  多重继承的缺点是:容易出现继承方向上的二义性,比如多个基类中的同名函数、多重继承导致基类的基类具有多份拷贝。

多重继承中的构造函数顺序

  假设有以下关系:基类(Parent)、派生类Child1和Child2(由Parent单一派生出Child1,Child2)、派生类Derived(由Child1和Child2多重派生而来)。

class Parent{
    //...
};
class Child1: public Parent{
    //...
};
class Child2: public Parent{
    //...
};
class Derived: public Child1,public Child2{
    //...
};

  如果不存在virtual继承的情况,则Derived类的构造函数执行顺序如下所示:
  (1)构造Child1:由于Child1继承自Parent,所以会调用Parent的构造函数,再调用Child1构造函数;
  (2)构造Child2:由于Child2继承自Parent,所以也会调用一遍Parent的构造函数,再调用Child2构造函数;
  (3)构造函数Derived。
  可见这种情况下,Dervied有两个Parent的拷贝,并不符合我们的预期。所以C++中采用虚继承来解决这个问题。
  如果Child1和Child2改用virtual继承,则Derived的构造函数顺序为:
  先为Derived加入一个虚基类的拷贝(调用虚基类的默认构造函数),然后调用Child1和Child2的构造函数,最后是自身Dervied的构造函数。
  综上所述,多重继承中的构造函数遵循以下的顺序:
  (1)任何虚拟基类的构造函数按照他们被继承的顺序构造;
  (2)任何非虚拟基类的构造函数按照他们被构造的顺序构造;
  (3)任何成员对象的构造按照它们声明的顺序调用;
  (4)类自身的构造函数。

虚函数和纯虚函数的区别

  虚函数和纯虚函数的区别有:
  (1)类中如果声明了虚函数,那么这个函数就需要实现,哪怕是空实现。它的作用就是为了能让这个函数在子类中被覆盖,这样编译器和实现动态绑定以实现多态。纯虚函数只是一个接口,是一个函数的声明而已,它会保留到子类中去实现。
  (2)虚函数在子类中可以不重载,但是纯虚函数必须在子类中实现。将函数加上virtual是一个好习惯,虽然牺牲了性能,但是保证了多态。
  (3)虚函数即继承了接口也继承了实现,但是子类可以覆盖。纯虚函数只是继承了接口。
  (4)带纯虚函数的类叫做虚基类,这种基类无法直接生成对象,只能用于继承,因此这种类也叫抽象类。

泛型编程

什么是泛型编程

  泛型编程是指编写完全一般化并可重复使用的算法,其效率与针对某种特定数据类型而设计的算法相同。所谓泛型,是指在多种数据类型上皆可操作的含意,在C++中实际上使用模板实现。

函数模板和类模板的概念与区别

  1.什么是函数模板和模板类?
  函数模板是一种抽象的函数定义,代表了一类同构函数。通过用户提供的具体参数,C++编译器在编译时刻能够将函数模板实例化,根据同一个模板创建出不同的具体函数。
  类模板是一种抽象的类定义,用于使用相同的代码创建不同的类。
  2.函数模板和类模板有什么区别?
  使用函数模板的时候不一定必须指明具体类型,函数会根据调用时候的类型自动完成实例化。而类模板的实例化必须显示地指定具体类型。

使用模板有什么缺点?我们如何避免

  模板的缺点:不当地使用模板会导致代码膨胀,即二进制代码臃肿,会严重影响程序的运行效率。
  解决办法:把C++模板中与参数无关的代码分离出来。

STL

  正在施工中......


当珍惜每一片时光~