• 面经——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;
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
    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(mem);
            // 调用构造函数实例化内存(赋值)
            pc->Complex::Complex(1, 2);
            
        }catch(std::bad_alloc){
            // 若allocation失败就不执行constructor
        }
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
      • 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;
        }
        
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
      • 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);
        }
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
    7. 区别以下指针类型

      int *p[10];		// 指针数组,数组内的每个元素int类型的指针
      int (*p)[10];	// 数组指针,是一个指向具有10个元素的数组的指针变量
      int *p(int);	// 函数声明,函数名是p,参数是int类型的,返回值是int*类型的
      int (*p)(int);	// 函数指针,指向参数为int类型并返回值也为int类型的函数
      
      • 1
      • 2
      • 3
      • 4
    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;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    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
        {
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
      • override:声明该函数时重写的父类的虚函数,如果不是父类函数会报错,防止写错

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

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

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

      string str1("I am a string");//语句1 直接初始化
      string str2(str1);//语句2 直接初始化
      string str3 = "I am a string";//语句3,拷贝初始化
      string str4 = str1;//语句4,拷贝初始化
      
      • 1
      • 2
      • 3
      • 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();
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    10. 野指针和悬空指针

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

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

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

      • 默认构造函数(无参数):
        • 定义类的对象时,没有提供初始化式就会调用类的默认构造函数
        • 由编译器创建的构造函数是合成的默认构造函数,这种合成可能失败
        • 为所有形参提供实参初始化的构造函数是自定义的默认构造函数
      • 初试化构造函数:
        • 使用实参在创建对象时为对象的成员属性赋值,由编译器自动调用
      • 拷贝构造函数
        • 用于将对象作为函数参数、返回值或初始化其他对象时调用,进行深拷贝后传递
        • 如果没有定义拷贝构造函数,编译器会自行定义。
      • 移动构造函数
        • 临时对象转移内存所属权时调用,使用右值引用作为参数
      • 委托构造函数
        • 类中往往有多个构造函数,只是参数表和初始化列表不同,其初始化算法都是相同的。为了避免代码重复,可以使用委托构造函数
      • 转换构造函数
        • 只有一个参数的构造函数,而且该参数又不是本类的const引用
      #include 
      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;
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
    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++中函数返回局部对象不会引发拷贝构造
    
    • 1
  • 相关阅读:
    Vue路由及Node.js环境搭建
    树莓派开箱
    多激光雷达内外参标定
    一文搞懂APT攻击
    重庆助学自考学费多少?
    算法深度解析:视频实时美颜SDK背后的技术奥秘
    PLC有几种编程语言以及它们的特点是什么
    全新自适应导航网模板 导航网系统源码 网址导航系统源码 网址目录网系统源码
    mapperXML标签总结
    Unity插件Obi.Rope详解
  • 原文地址:https://blog.csdn.net/qq_43840665/article/details/126805527