面经——C++语言1

面经——C++语言1、2

主要参考《阿秀笔记》,次要参考博客和侯捷老师视频及C++ Primer

基本语法1
  1. 在main函数调用之前和之后执行的代码是什么?(侯捷——C++程序的生前死后)

    • C++最开始执行一个_stdcall 格式的Entry-Point Symbol函数(也称为startup code),这个函数通常由编译器的设置,主要进行运行库和程序运行环境进行初始化
    • 调用main之前执行的主要函数
      • _heap_init(…):分配内存,构建堆栈
      • ioinit():分配具有64个File结构体指针元素的静态数组,并初始化File结构体和静态数组
      • 4个字符串处理函数:为main函数接受的命令行参数进内存分配并处理
      • 调用可执文件和各动态库的构建s函数
    • main函数执行完毕之后,返回到入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程
  2. 结构体内存对齐问题

    • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。

    • 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

      struct alignas(4) Info2 {
        uint8_t a;
        uint16_t b;
        uint8_t c;
      };
      
  3. 指针和引用的区别

    • 本质上:指针是一个存储地址的变量,引用是原来变量的别名(类似const指针)
    • 层级上:指针可以有多级,引用只能有一级
    • 初始化:指针可以为空,引用不能为空,在定义时必须初始化
    • sizeof:指针式指针本身的大小,通常和寻址位数有关。引用得到的是所指向的变量大小
    • 赋值上:引用初始化后不可改变指向,指针的声明和定义可以分开
  4. 在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?

    • 类对象作为参数传递的时候要使用引用,这是C++类对象传递的标准方式
    • 引用传递不需要创建临时变量,开销更小,适合栈空间大小敏感的使用
    • 指针传参本质也是值传递(形参和实参相互独立),引用传参后对形参的任何操作都会间接寻址到实参
    • 对于局部变量的引用没有意义
  5. 堆和栈的区别

    • 申请方式不同
      • 栈是系统自动分配的,也可以动态分配
      • 堆是程序员申请和释放的,只能动态分配
    • 大小不同
      • 栈的大小是预设好的,通常向低地址生长。可通过ulimit -a查看和ulimit -s进行修
      • 堆是向高地址生长的,是不连续的内存区域,大小可以灵活调整
    • 申请效率不同
      • 栈由系统分配,速度快,无碎片
      • 堆由程序员分配,速度慢,有碎片
    • 内存管理方式
      • 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序
      • 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出
    • 效率
      • 堆是函数库提供的,分配内存需要通过算法运算,通常效率低
      • 栈是系统级的数据结构,栈操作都有专门的指令,效率较高
  6. new和delete是如何实现的?

    • new的本质:调用operator new分配内存,将该内存进行转型后赋值给指针,然后调用构造函数进行赋值

      Complex *pc = new Complex(1, 2);
      // 用c语言进行其编译功能的解释
      Complex *pc;
      try{
          //分配内存,实质调用malloc
          void *mem = operator new(sizeof(Complex));
          // 内存转型,赋值给指针
          pc = static_cast<Complex*>(mem);
          // 调用构造函数实例化内存(赋值)
          pc->Complex::Complex(1, 2);
          
      }catch(std::bad_alloc){
          // 若allocation失败就不执行constructor
      }
      
      
    • operator new的本质:使用malloc进行分配内存,同时进行异常处理。可以重载

      // 第二参数保证函数不抛出异常
      void *operator new(size_t size, const std::nothrow_t &){
          void *p;
          // 如果内存耗尽导致分配失败 (实质调用malloc)
          while((p=malloc(size)) == 0){
              _TRY_BEGIN
                  if(_callnewh(size) == 0)// 调用自定义函数进行处理
                      break;
              _CATCH(std::bad_alloc)
                  return 0;
              _CATCH_END
          }
          return p;
      }
      
      
      
    • delete的本质:先调用析构函数处理类对象,后调用operator delete函数进行释放。operator delete函数本质是调用free函数

      delete pc;
      // 使用c语言进行翻译
      pc->~Complex();// 先析构对象
      operator delete(pc);// 后释放
      
      //operator delete源码
      void __cdecl operator delete(void *p)_THROW0(){
          free(p);
      }
      
      
  7. 区别以下指针类型

    int *p[10];		// 指针数组,数组内的每个元素int类型的指针
    int (*p)[10];	// 数组指针,是一个指向具有10个元素的数组的指针变量
    int *p(int);	// 函数声明,函数名是p,参数是int类型的,返回值是int*类型的
    int (*p)(int);	// 函数指针,指向参数为int类型并返回值也为int类型的函数
    
  8. new / delete 与 malloc / free的异同

    相同点

    • 都用于内存的动态申请和释放

    不同点

    • 根本不同:new/delete底层使用的是malloc/free,还封装了对象创建时候的构造函数,和销毁的时候要执行的析构函数。被free回收的内存是立即返还给操作系统吗?

      不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片

    • malloc和free是标准库函数,支持覆盖。new和delete是运算符,支持重载

    • 空间分配上,new可以自动计算空间分配大小,malloc需要手工计算

    • 安全性上,new是类型安全的,malloc不检查分配内存是否可以被内存直接使用

    • malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

    • new和delete实现不同,见上

  9. 被free回收的内存是立即返还给操作系统吗?

不会,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。避免了频繁的系统调用,提高系统资源利用率。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片

  1. 宏定义和函数有何区别?

    • 宏定义在在预编译阶段完成替换,执行效率高,但是增加了代码量
    • 函数调用会进行类型检查和堆栈操作,效率慢
    • 宏定义通常用于系统底层接口参数和功能的抽象
  2. 宏定义和typedef的区别

    • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
    • 宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
    • 宏不检查类型;typedef会检查数据类型。
    • 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
  3. 变量声明和定义的区别

    • 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。
    • 相同变量可以在多处声明(外部变量extern),但只能在一处定义。
  4. strlen和sizeof区别?

    • sizeof是运算符,结果在编译时得到而非运行中获得。strlen是字符处理的库函数
    • sizeof参数可以是任何数据的类型或者数据。strlen的参数只能是字符指针且结尾是’\0’的字符串。
  5. 一个指针占的字节数?

    • 一个指针占内存的大小跟编译环境有关,而与机器的位数无关

    64位处理器上64位操作系统的32位编译器,指针大小8字节。
    64位处理器上32位操作系统的16位编译器,指针大小4字节。

    32位处理器上32位操作系统的32位编译器,指针大小4字节。
    32位处理器上32位操作系统的16位编译器,指针大小2字节。

    32位处理器上16位操作系统的16位编译器,指针大小2字节。
    16位处理器上16位操作系统的16位编译器,指针大小2字节。

  6. 常量指针和指针常量区别?

    • 指针常量是指向常量的指针,int const *p / const int *p

    • 常量指针:指针是个常量,不能改变指向,必须初始化且之后不能改变

      int *const p

  7. int a[10];int (*p)[10];a和&a有什么区别?

    • a是数组名,也是数组首元素地址。+1表示数组下一个元素的首地址
    • &a数组的指针,类型为int(*)[10],+1表示数组首地址加整个数组的偏移量,即数组末尾下一个元素的地址
  8. C++和Python的区别

    • 执行方式上:python是一个脚本语言,是解释执行的,更容易跨平台,但是效率低一些。C++是编译语言,效率高。
    • 代码风格上:python语法简洁,使用缩进来区分不同的代码块,C++用花括号进行区分
    • python库函数比C++多,调用方便
  9. 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等等
基本语法2
  1. C++和Java的区别

    • 编译上:Java源码可以一次编译,然后在不同平台的JVM上翻译对应的机器码运行,实现了良好的跨平台特性。C++一次性编译链接直接生成机器码,性能高但跨平台能力差。
    • 指针上:C++可以通过指针直接进行内存的操作,效率高但是不安全。Java使用自动的内存管理机制,减少了指针操作失误,但JVM内部仍然使用的是指针,不能杜绝内存泄漏的问题。
    • 多重继承:C++支持多重继承,允许多个父类派生一个类,功能强但是使用复杂。Java不支持多重继承,但是允许一个类继承多个接口,减少继承多个实现的父类带来的复杂性
    • 内存管理机制:Java使用自动的内存管理机制,JVM可以自动进行无用内存的释放。C++没有垃圾回收机制,需要手动释放申请的堆内存
    • 操作符重载:Java不支持操作符重载,是C++的突出特征
    • 字符串:Java具有类对象实现的字符串,统一和简化了操作
    • 类型转换:C++具有隐式的自动转换,但是Java只能由程序进行强制类型转换
  2. C++中struct和class的区别

    • 属性上:如果对成员不指定struct 默认是公有,class默认是私有
    • 继承上:struct默认是公有继承,class默认是私有继承
    • C++保留struct关键字的原因是兼容C(成员是public),但是C不能进行成员函数的定义
    • class还可以定义模板参数,但是关键字struct不能用于定义模板参数
  3. define宏定义和const的区别

    • 作用域:define发生在预编译阶段进行文本替换,const作用于编译过程
    • 安全性:define不进行类型检查,最好使用大括号包含替换的内容。const具有数据类型,编译器会进行类型的安全性检查
    • 内存占用:宏定义数据没有分配内存空间。const定义的变量值不能变,但要分配内存空间
  4. C++中const和static的作用

    static关键字:

    • 修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。

    • 修饰局部变量时,在全局数据区分配内存,不会因函数终止而丢失

    • 修饰函数时,表明该函数不能被其他文件所用,而其他文件可以定义同名函数

    • 修饰类的数据成员,表明该实例归所有该类对象共有。

    • 修饰类内的函数,该函数可以被非静态函数访问,但是它只能访问静态的函数和数据

    const关键字:

    • const变量在定义时必须进行初始化,之后无法更改且在其他文件无法使用
    • const形参可以接受const和非const类型的形参
    • const成员变量,只能通过构造函数初始化列表进行初始化
    • const成员函数,不能调用非const成员函数,但可以被非const成员函数调用
  5. C++的顶层const和底层const

    • 顶层const:const修饰的变量本身无法修改,指的是指针,就是 * 号的右边
    • 底层const:const修饰的变量所指向的对象是一个无法修改,就是 * 号的左边
    • 总结:const总是修饰的是其后面的整体
    // 顶层const,表示b1是常量
    const int b1 = 20; int const b1 = 20;
    // 顶层const,表示b1是常量
    int a = 10;
    int *const b2 = &a;
     
    // 底层const,b3可变,但是指向的对象不可变
    const int *b3 = &a;
    // 底层const,引用变量不可变
    const int &b5 = a;
    
  6. 数组名和指向数组首元素的指针区别?

    • 数组名不是真正意义上的指针,没有自增、自减等操作。
    • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小
  7. final和override关键字

    • final:修饰类名表示该类不会被继承,修饰虚函数表明该函数不能被重写。如果被继承或重写,编译器会报错

      class Base
      {
          virtual void foo();
      };
       
      class A : public Base
      {
          void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
      };
      
      class B final : A // 指明B是不可以被继承的
      {
          void foo() override; // Error: 在A中已经被final了
      };
       
      class C : B // Error: B is final
      {
      };
      
    • override:声明该函数时重写的父类的虚函数,如果不是父类函数会报错,防止写错

      class A
      {
          virtual void foo();
      };
      class B : public A
      {
          virtual void f00(); //OK,这个函数是B新增的,不是继承的
          virtual void f0o() override; 
          //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
      };
      
  8. 用于类对象的直接初始化和拷贝初始化
    详细解释的博客

    • 直接初始化:直接调用与实参匹配的构造函数,通常是"( )"赋值的形式

    • 拷贝初始化:总是调用拷贝构造函数,会先调用构造函数创建临时对象,后调用复制构造函数用改临时对象初始化,通常是" = "赋值的形式

    string str1("I am a string");//语句1 直接初始化
    string str2(str1);//语句2 直接初始化
    string str3 = "I am a string";//语句3,拷贝初始化
    string str4 = str1;//语句4,拷贝初始化
    
    • 编译器会进行拷贝初始化的优化,但是以下几种情况只能使用直接初始化
      • 拷贝构造函数是private
      • 使用explicit修饰构造函数
  9. extern "C"的用法

    • 作用:告诉C++编译器该部分代码使用C语言进行编译,只能放在cpp和h文件中,将C与C++桥梁代码包裹起来
    // 1. C++调用C函数
    //xx.h
    extern int add(...)
    //xx.c
    int add(){
    }
    //xx.cpp
    extern "C" {
        #include "xx.h"
    }
    
    // 2. C调用C++函数
    //xx.h
    extern "C"{
        int add();
    }
    //xx.cpp
    int add(){    
    }
    //xx.c
    extern int add();
    
    
  10. 野指针和悬空指针

    • 野指针:未被初始化的指针,访问行为不可控。未使用的指针初始化应赋值为
      nullptr,这样在使用时编译器会报错,避免非法访问
    • 悬空指针:指针最初指向的指针被释放。指针释放后用其赋值的指针应该置空。C++的智能指针可以避免悬空指针的出现
  11. 类型安全

    • 定义:类型安全的代码只能访问被授权的内存区域,若无强制类型转换则会报错
    • C++的类型安全机制
      • 操作符new返回的指针类型严格与对象匹配
      • 模板函数支持类型检查
      • const关键字进行作用域定义
      • 使用inline和函数重载,可在类型安全的情况下支持多种类型
      • 提供dynamic_cast关键字进行更强的类型检查
    • 解决方法:减少强制类型转换和空指针类型void*的使用
  12. C++中重载、重写(覆盖)和隐藏的区别

    • 重载:
      • 函数参数的个数、类型或顺序存在不同,但函数名相同。
      • 函数类型也可变,但不能只重载函数类型。
    • 重写:
      • 在派生类中覆盖基类中的同名函数
      • 重写的基类函数必须是虚函数,且与基类虚函数具有相同的参数列表和返回值类型
    • 隐藏:
      • 派生类函数声明与基类同名的函数,但是基类函数不是虚函数
  13. C++有哪几种的构造函数

    • 默认构造函数(无参数):
      • 定义类的对象时,没有提供初始化式就会调用类的默认构造函数
      • 由编译器创建的构造函数是合成的默认构造函数,这种合成可能失败
      • 为所有形参提供实参初始化的构造函数是自定义的默认构造函数
    • 初试化构造函数:
      • 使用实参在创建对象时为对象的成员属性赋值,由编译器自动调用
    • 拷贝构造函数
      • 用于将对象作为函数参数、返回值或初始化其他对象时调用,进行深拷贝后传递
      • 如果没有定义拷贝构造函数,编译器会自行定义。
    • 移动构造函数
      • 临时对象转移内存所属权时调用,使用右值引用作为参数
    • 委托构造函数
      • 类中往往有多个构造函数,只是参数表和初始化列表不同,其初始化算法都是相同的。为了避免代码重复,可以使用委托构造函数
    • 转换构造函数
      • 只有一个参数的构造函数,而且该参数又不是本类的const引用
    #include <iostream>
    using namespace std;
    
    class Student{
    public:
        //1. 默认构造函数,没有参数
        Student(){
            this->age = 20;
            this->num = 1000;
        };  
        // 2. 初始化构造函数,有参数和参数列表
        Student(int a, int n):age(a), num(n){};
        // 3. 拷贝构造函数,参数是对象
        Student(const Student& s){
            this->age = s.age;
            this->num = s.num;
        };
        // 4. 移动构造函数,参数是右值引用
        Student(const Student&& s){
            this->age = s.age;
            this->num = s.num;
        }; 
        
        // 3. 转换构造函数,形参是其他类型变量,且只有一个形参
        Student(int r){   //
            this->age = r;
    		this->num = 1002;
        };
        ~Student(){}
    public:
        int age;
        int num;
    };
    
  14. 浅拷贝和深拷贝的区别

    • 浅拷贝:拷贝的是内存对象的地址,而不是内容。即多个指针指向同一个内存对象,如果其中一个释放了该内存对象,其他浅拷贝指针会出现错误
    • 深拷贝:拷贝内存对象到一个新的内存区域,此时存在多个相同的内存对象。即使原来对象析构了,也不影响拷贝的新对象。
  15. 内联函数和宏定义的区别

    • 执行时间:宏只在编译前进行简单的字符串替换,内联函数在编译时进行类型检查,将代码嵌入目标代码中,省去函数调用开销来提高效率,有返回值也可以重载
    • 书写形式:宏尽量括号括起来,否则容易出现歧义。内联函数和普通函数一样,使用宏的地方都可以使用内联函数
  16. public,protected和private访问和继承权限的区别

    修饰变量和函数:

    • public:在类的内部外部都可以访问。
    • protected:只能在类的内部和其派生类中访问。
    • private:只能在类内访问

    权限继承:

    • public继承:基类中各成员属性保持不变,基类中private成员被隐藏,派生类不能访问
    • protected继承:基类中各成员属性均变为protected,基类中private成员被隐藏,派生类不能访问
    • private继承:基类中的各成员属性全变成private,基类中private成员被隐藏,派生类不能访问

    派生类对基类的访问

    • 内部访问:由派生类中新增的成员函数对从基类继承来的成员的访问
    • 外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问
    • 除了public继承的public其他外部访问均不可
  17. 如果用代码判断大小端存储

    • 大端存储:字数据的高字节存储在低字节

    • 小端存储:字数据的低字节存储在低地址中

    • 注意:在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输

  18. volatile、mutable和explicit关键字的用法

    volatile关键字

    • 修饰类型变量,编译器将不在对该变量代码进行优化,从而提供稳定的访问
    • 声明变量的值,系统总是重新从它所在的内存中读取数据,而不是使用寄存器的备份
    • 赋值上,不能把非volatile对象赋给一个volatile对象,其他可以
    • 修饰类,C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。
    • 多线程下,防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行

    mutable

    • 在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置。
    • const对象不能调用非const对象,但是加上mutable便可以访问

    explicit

    • explicit关键字用来修饰类的单参数的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换
  19. 什么情况下调用拷贝构造函数

- 通常是在类使用其他对象初始化、作为函数参数传递或者函数返回值是类对象时,但是g++中函数返回局部对象不会引发拷贝构造
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

逆羽飘扬

如果有用,请支持一下。

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

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

打赏作者

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

抵扣说明:

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

余额充值