【编译基础】面经常见问题

面经常见问题

在这里插入图片描述



😊点此到文末惊喜↩︎

一级标签

1.语言对比

  1. Python语言特点
    • 解释型的脚本语言:容易跨平台但执行效率低
    • 语法简洁
    • 有丰富的第三方库
  2. C++和C语言的区别
    • 内存管理:C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
    • 字符串处理:C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数
    • C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
    • C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
    • C++可以重载,C语言不允许。
    • 在C++中,除了值和指针之外,新增了引用
    • C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等
  3. C++和Java的区别
    • 编译上:Java源码可以一次编译,然后在不同平台的JVM上翻译对应的机器码运行,实现了良好的跨平台特性。C++一次性编译链接直接生成机器码,性能高但跨平台能力差。
    • 指针上:C++可以通过指针直接进行内存的操作,效率高但是不安全。Java使用自动的内存管理机制,减少了指针操作失误,但JVM内部仍然使用的是指针,不能杜绝内存泄漏的问题。
    • 多重继承:C++支持多重继承,允许多个父类派生一个类,功能强但是使用复杂。Java不支持多重继承,但是允许一个类继承多个接口,减少继承多个实现的父类带来的复杂性
    • 内存管理机制:Java使用自动的内存管理机制,JVM可以自动进行无用内存的释放。C++没有垃圾回收机制,需要手动释放申请的堆内存
    • 操作符重载:Java不支持操作符重载,是C++的突出特征
    • 字符串:Java具有类对象实现的字符串,统一和简化了操作
    • 类型转换:C++具有隐式的自动转换,但是Java只能由程序进行强制类型转换

2.零拷贝(mmap的底层)

  1. 定义:不需要cpu参与I/O操作的数据复制,从而减少上下文切换以及CPU拷贝次数
  2. 方法
    • 内存映射mmap
      把内核空间和用户空间的虚拟地址映射到同一个物理地址,即逻辑上将用户缓冲区映射到内核缓冲区,缓冲区的数据传输直接在内核中完成,从而减少数据拷贝次数
      在这里插入图片描述
      • 用户进程通过调用mmap方法向操作系统内核发起IO调用,上下文从用户态切换至内核态
      • CPU利用DMA控制器,将数据从硬盘拷贝到内核缓冲区
      • 上下文从内核态切换回用户态,mmap方法返回
      • 用户进程通过调用write方法向操作系统内核再次发起IO调用,上下文从用户态切换至内核态
      • CPU将内核缓冲区的数据拷贝到socket缓冲区
      • CPU利用DMA控制器,将数据从socket缓冲器拷贝到网卡,上下文从内核态切换至用户态,write方法返回
    • 系统调用函数sendfile
      在操作系统内核的缓冲区间直接拷贝的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作
      在这里插入图片描述
      • 用户进程发起sendfile系统调用,上下文从用户态切换至内核态
      • DMA控制器将数据从硬盘拷贝到内核缓冲区
      • CPU将读缓冲区中的数据拷贝到socket缓冲区
      • DMA控制器异步把数据从socket缓冲器拷贝到网卡
      • 上下文从内核态切换至用户态,sendfile函数返回
    • sendfile +DMA scatter/gather
      对sendfile中的DMA拷贝进行优化,通过scatter/gather操作可以直接将内核缓冲区中的连续的数据以离散的形式拷贝到设备端口,从而减少cpu到设备的拷贝
      在这里插入图片描述
      • 用户进程发起sendfile系统调用,上下文从用户态切换至内核态
      • DMA控制器将数据从磁盘拷贝到内核缓冲器
      • CPU把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)直接发送到socket缓冲区
      • DMA控制器根据文件描述符信息直接把数据从内核缓冲区拷贝到网卡
      • 上下文切换至用户态,sendfile返回

3.异常处理

  1. 程序错误类型
    • 编译错误,通常在编译时就能发现的语法错误
    • 执行错误,在运行时,由于程序逻辑错误才会出现的异常,如数组下标越界和系统内存不足等
      • gdb断点调试
      • 异常捕捉
      • 分析日志
      • 分析coredump文件
      • 防御性编程
      • 模块化分析定位
      • 对数器
  2. C++异常处理机制
    • 执行一个函数的过程中发现异常,不立即终止程序进行处理, 而是抛出该异常,让函数的调用者直接或间接处理这个问题
    • 注意:在try 和catch中如果要return,会先去执行finally中的内容再返回
	try{  
	    // 待异常检测的代码,没有抛出异常则继续执行,抛出则转入相应的catch中执行
	    if (异常条件成立)
			throw(对应异常类型);
	}catch(类型名[形参名]){//捕获特定类型的异常 
		// 处理1; 
	}catch(类型名[形参名] ) { 
		// 处理2; 
	}catch(...){//捕获所有类型的异常 
		// 处理代码;
	}finally{
	   // 只要执行try就一定要执行的代码
	}
  1. coredump问题调试
    • What:在程序运行过程中出现异常情况导致进程终止时,系统会将该进程的内存转储到磁盘上的coredump文件上
    • How:对这个文件使用gdb进行分析可以定位到程序异常时出现的堆栈调用信息。
    • 常见情况
      • 堆栈溢出
      • 多线程程序使用了线程不安全的函数。
      • 多线程读写的数据未加锁保护。
      • 使用空指针

4.强制转换

  1. const_cast<new_type> (obj)
    • 作用:用于去除指针或引用的常量性
    • 特点
      • 去除常量性后,其指向不改变
      • const_cast一般用于修改底指针。如const char *p形式
	int arr[4] = {1, 2, 3, 4};  // 原始数组
    const int *c_ptr = arr;  // 常量化数组指针
    int *ptr = const_cast<int*>(c_ptr); // 通过const_cast<TypeName> 去常量
  1. static_cast<new_type> (obj)
    • 作用:显式的强制类型转换,只进行编译时检查不进行运行时检查。
    • 特点:
      • 用于基本数据类型之间的转换和指针类型之间的转换,转换的安全性需要开发人员进行保证。
      • 用于父子类间的指针或引用的转换时,编译器都不会报错,但是下行转换(父转子)不安全
      • 不能转换掉expression的const、volatile、或者__unaligned属性
// 1.基本数据类型的转换
	char a = 'c';
    int b = static_cast<int>(a);
// 2. 父子类间的指针或引用的转换
 	// 父类指针转为子类,下行,编译通过,不安全
    Person *p = nullptr;
    Son *s = static_cast<Son *>(p);
    // 子类指针转为父类,上行,编译通过,安全
    Son *s0 = nullptr;
    Person *p0 = static_cast<Person *>(s0);
    
    // 父类引用转为子类,下行,编译通过,不安全
    Person p_;
    Person &p3 = p_;
    Son &s3 = static_cast<Son &>(p3);
    // 子类引用转为父类,上行,编译通过,安全
    Son s_;
    Son &s4 = s_;
    Person &p4 = static_cast<Person &>(s4);
    
 	//父类对象无法转为子类对象
    // Person p1;
    // Son s1 = static_cast<Son>(p1);
    // 子类对象可以赋值,初始化父类对象
    Son s2;
    Person p2 = static_cast<Person>(s2);
// 3. 空指针转换
    // 空指针转化为目标类型的指针
    void *pPtr = nullptr;
    int *iPtr = static_cast<int *>(pPtr);
// 4. 空指针类型的转换
    // 任何指针转化为void指针类型
    int *aInt = nullptr;
    void *aVoid = static_cast<void *>(aInt);
  1. dynamic_cast <new_type> (obj)
    • 作用
      • 用于类层次间的上下行转换,还可以用于类之间的交叉转换
      • 只在运行时进行检查,如果发现转换错误则会返回空指针或抛出std::bad_cast异常
    • 特点
      • 上行转换(子类转父类),转换安全,成功返回类对象指针, 此时和static_cast 作用一样。
      • 下行转换(父类转子类), 父类中要有虚函数,否则编译器报错。
        • 父类指针指向子类对象,转换安全, dynamic_cast返回类对象指针。
        • 不兼容,若指针转换失败返回nullptr,若引用转换失败返回bad_cast
        • 若使用static_cast,返回非空指针,更不安全。
    Base *base= nullptr;// 基类指针
    Derive *d = nullptr;// 派生类指针
    Base baseObj;// 基类对象
    Derive *deriveObj;// 派生类对象
    // 向上转换(子类转父类)安全
    base = dynamic_cast<Base*>(d);
    // 向下转换(父类转子类),父类中必须含有虚函数,不然编译器报错
        // 1. 父类指针指向子类对象,安全
    d = dynamic_cast<Derive*>(base);
    	
    
  2. reinterpret_cast<new_type> (obj)
    • 作用:可用于不同类型指针间的转换或者指针和整型间的转换,转换结果不保证可靠
    //将整型指针通过reinterpret_cast强制转换成了双精度浮点型指针。
    int *a = new int;
    double *d = reinterpret_cast<double *>(a);
    

5.静态与动态

  1. 概念
    • 静态类型:对象在声明时采用的类型,在编译期即已确定
    • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期 决定的;
    • 静态绑定:在编译时,将调用的方法和调用主体进行绑定
    • 动态绑定:在运行时,根据具体对象决定绑定主体
  2. 区别
    • 静态绑定发生在编译期,无法更改,性能高
    • 动态绑定发生在运行期,可以修改,性能低
    • 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;
  3. 注意点
    • 任何条件下都要禁止重新定义继承而来的非虚函数,非虚函数都是静态绑定,父子类的对象去调用同名函数会产生不同的行为,违反了非虚函数继承时不变性凌驾于特异性的原则
    • 引用是否可以实现动态绑定。因为引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数
    class B{
    public:
        void mf(){
            cout<<"B"<<endl;
        }
    };
    class D:public B{
    public:
        void mf(){//名字覆盖
            cout<<"D"<<endl;
        }
    };
    int main(){
        D x;
        B* pb=&x;
        pb->mf();
    
        D* pD=&x;
        pD->mf();
        return 0;
    }
    // 结果为:B D
    // 原因:静态绑定导致对象指针只会调用自己类内的同名非虚函数
    

6.C++代码标准库

  1. 组成
    • C库:由C标准库扩展而来,强调结构、函数和过程,不支持面向对象技术。
    • 面向对象类库:类及相关函数的集合
    • 标准模板库(STL):包含了常用的基本数据结构和基本算法。
  2. 避免头文件的重定义
    • #ifndef的使用:如果标识符(XXXX_H全部大写)已经被定义了,则不编译下面的程序段
      #ifndef  XXXX_H
      #define  XXXX_H
      
      程序段;
      
      #endif
      
    • 优点
      • 保证内容完全相同的代码片段不会被同时引入
      • 受到c++语言标准支持, 不会被编译器平台限制
    • 缺点
      • 如果不同头文件宏名相同,可能导致头文件存在但是编译器找不到声明的情况
      • 编译器对重复定义需要打开所有头文件进行判定,在编译大型项目时可能导致编译时间增加
    • 部分编译器支持#pragma once,有一些优点
      • 同一个物理意义上的文件不会被包含多次
      • 不会发生宏名碰撞问题
      • 已经引入的头文件会直接跳过,而不需要打开,可以提高编译速度
        最终形态:在避免引入同一物理意义上的头文件的同时,防止代码段的重定义
    #pragma once
    
    #ifndef  XXXX_H
    #define  XXXX_H
    
    程序段;
    
    #endif
    

7.编程注意点

  1. 判断两个浮点数相等
    • 原因:对于整型可以使用相等判断,对于浮点数在计算机中使用科学计数法进行存储,进行运算时可能处出现无法精确表示而进行近似的误差
    • float的精度误差在1e-6;double精度误差在1e-15
// fabs是求绝对值函数

// float类型的判断
if(fabs(a-b) < 1e-6)
    
// double类型的判断
if(fabs(a-b) < 1e-15)
  1. 不使用额外空间交换两个数
1. 算术

x = x + y;
y = x - y;
x = x - y; 

2. 异或(只适用于intchar等类型)
x = x^y;
y = x^y;
x = x^y;
x ^= y ^= x;
  1. const char* 与string之间的关系
    • string是C++标准库中的字符串处理类,封装了对字符串的操作
//1. string转const char* ,需要调用string类对象调用c_str()函数
string s = “abc”; 
const char* c_s = s.c_str(); 

//2. const char* / char* 转string,直接赋值即可 
const char* c_s = “abc”; 
string s(c_s); 
char* c = “abc”; 
string s(c); 
//3. string 转char* 
 string s = “abc”; 
 char* c; 
 const int len = s.length(); 
 c = new char[len+1]; 
 strcpy(c,s.c_str()); 

//4. const char* 转char* 
 const char* cpc = “abc”; 
 char* pc = new char[strlen(cpc)+1]; 
 strcpy(pc,cpc);

//5. char* 转const char*,直接赋值即可 
 char* pc = “abc”; 
 const char* cpc = pc;
  1. debug和release的区别
    • 调试版本,包含调试信息,所以容量比Release大很多,并且不进行任何优化(优化会使调试复杂化,因为源代码和生成的指令间关系会更复杂),便于程序员调试。Debug模式下生成两个文件,除了.exe或.dll文件外,还有一个.pdb文件,该文件记录了代码中断点等调试信息
    • 发布版本,不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的。(调试信息可在单独的PDB文件中生成)。Release模式下生成一个文件.exe或.dll文件。
  2. printf函数和cout对象
    • printf函数
      • c语言遵循_cdedl调用规则,printf函数的参数从右向左依次入栈。
      • 栈由高地址向低地址生长, 栈顶指针指向的第一个参数是字符串的指针,通过解析判断参数的个数和数据类型,计算出栈顶指针的偏移量
      • printf是一个可变参数表的函数,使用stdarg.h提供的数据类型va-list和三个宏(va-start、va-arg和va-end)
    • cout对象
      • printf是函数,而cout是ostream类的对象,配合重载的<<使用等价于对象调用函数cout.operator<<(“endl”)
      • printf是变参函数,没有类型检查,不安全。cout是通过运算符重载实现的,安全。
      • cout可以通过重载扩展自定义类型
  3. 重载函数的调用匹配原则
    • 确定候选函数:根据作用域和函数名选择同名同域的重载函数
    • 确定可行函数:根据被调函数的参数列表,选择对应的可行函数集
    • 确定最佳匹配函数:若有多个最佳匹配则出现函数二义性错误
      • 精确匹配:参数类型完全匹配,只做常规性转换,如数组名到指针、函数名到指向函数的指针、T到const T
      • 提升匹配:短字节的实参类型向调用函数的长字节形参类型转换
      • 标准类型转换强制匹配:当前两个均无法找到可行函数,如果将实参进行类型强制转换可以找到,则使用该形式获取最佳匹配函数
      • 使用用户自定义匹配
      • 使用省略号匹配:类似printf中省略号参数


少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。
秘籍(点击图中书籍)·有缘·赠予你


🚩点此跳转到首行↩︎

参考博客

  1. 什么是零拷贝
  2. C++的四种强制转换
  3. 参考编译原理·龙书
  4. C++类型转换之dynamic_cast
  5. 待定引用
  6. 待定引用
  7. 待定引用
  8. 待定引用
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

逆羽飘扬

如果有用,请支持一下。

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

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

打赏作者

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

抵扣说明:

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

余额充值