本文源自于个人github仓库:https://github.com/forthespada/InterviewGuide
github仓库内有PDF版本下载方式,欢迎各位star、fork~
立志收录计算机校招、社招面试最全面试八股文,无内鬼来点八股文~

181、为什么友元函数必须在类内部声明?

因为编译器必须能够读取这个结构的声明以理解这个数据类型的大、行为等方面的所有规则。

有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。

182、用C语言实现C++的继承

#include <iostream>

using namespace std;

//C++中的继承与多态
struct A

{

  virtual void fun()  //C++中的多态:通过虚函数实现

  {
    cout<<"A:fun()"<<endl;

  }

  int a;

};

struct B:public A     //C++中的继承:B类公有继承A类

{
  virtual void fun()  //C++中的多态:通过虚函数实现(子类的关键字virtual可加可不加)

  {

   cout<<"B:fun()"<<endl;

  }
  int b;

};

//C语言模拟C++的继承与多态

typedef void (*FUN)();   //定义一个函数指针来实现对成员函数的继承

struct _A    //父类

{

  FUN _fun;  //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现
  int _a;

};

struct _B     //子类

{

  _A _a_;   //在子类中定义一个基类的对象即可实现对父类的继承

  int _b;

};

void _fA()    //父类的同名函数

{

  printf("_A:_fun()\n");

}

void _fB()    //子类的同名函数

{

  printf("_B:_fun()\n");

}

void Test()

{

  //测试C++中的继承与多态

  A a;  //定义一个父类对象a

  B b;  //定义一个子类对象b



  A* p1 = &a;  //定义一个父类指针指向父类的对象

  p1->fun();  //调用父类的同名函数

  p1 = &b;   //让父类指针指向子类的对象

  p1->fun();  //调用子类的同名函数



  //C语言模拟继承与多态的测试

  _A _a;  //定义一个父类对象_a

  _B _b;  //定义一个子类对象_b

  _a._fun = _fA;    //父类的对象调用父类的同名函数

  _b._a_._fun = _fB;  //子类的对象调用子类的同名函数



  _A* p2 = &_a;  //定义一个父类指针指向父类的对象

  p2->_fun();   //调用父类的同名函数

  p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转

  p2->_fun();   //调用子类的同名函数

}

183、动态编译与静态编译

1) 静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

2) 动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

184、hello.c 程序的编译过程

以下是一个 hello.c 程序:

#include <stdio.h>

int main()
{
    printf("hello, world\n");
    return 0;
}


在 Unix 系统上,由编译器把源文件转换为目标文件。

gcc -o hello hello.c

这个过程大致如下:

图片说明

  • 预处理阶段:处理以 # 开头的预处理命令;
  • 编译阶段:翻译成汇编文件;
  • 汇编阶段:将汇编文件翻译成可重定位目标文件;
  • 链接阶段:将可重定位目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。
静态链接

静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:

  • 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
  • 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

图片说明

目标文件
  • 可执行目标文件:可以直接在内存中执行;
  • 可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;
  • 共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;
动态链接

静态库有以下两个问题:

  • 当静态库更新时那么整个程序都要重新进行链接;
  • 对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。

共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点:

  • 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
  • 在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。

图片说明

源代码-->预处理-->编译-->优化-->汇编-->链接-->可执行文件

1) 预处理

读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i预处理后的c文件,.ii预处理后的C++文件。

2) 编译阶段

编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。.s文件

3) 汇编过程

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件

4) 链接阶段

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

185、介绍一下几种典型的锁

读写锁

  • 多个读者可以同时进行读
  • 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
  • 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁

条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

《互斥锁、读写锁、自旋锁、条件变量的特点总结》:https://blog.csdn.net/RUN32875094/article/details/80169978

186、说一下C++左值引用和右值引用

C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。

1) 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。

2) C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。

3) 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型