专业编程基础技术教程

网站首页 > 基础教程 正文

面试常见问题之C++篇答案 c++面试官常问的问题

ccvgpt 2024-10-12 13:56:56 基础教程 9 ℃

编译和调试

C/C++程序编译过程

  • C/C++程序编译过程就是把C/C++代码百年成可执行文件的过程, 该过程分为4步
  • 预处理阶段
  1. 进行宏展开和宏替换
  2. 处理条件编译指令, 如#ifdef, #endif等
  3. 去掉注释
  4. 添加行号和文件名标识
  5. 保留#pargma编译器指令(#Pragma命令将设定编译器的状态或者是指示编译器完成一些特定的动作)
  • 编译阶段
  1. 编译程序所要作的工作就是通过词法分析, 语法和语义分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
  2. 代码优化
  3. 重点关注函数压栈方式的编译处理__cdecl是C DECLaration的缩写, 表示C语言默认的函数调用方法: 所有参数从右到左依次入栈. 这些参数由调用者清除,称为手动清栈. 被调用函数不需要求调用者传递多少参数, 调用者传递过多或者过少的参数. 甚至完全不同的参数都不会产生编译阶段的错误。_stdcall 是StandardCall的缩写, 是C++的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话, 最后一个入栈的是this指针。这些堆栈中的参数由被调用的函数在返回后清除, 使用的指令是 retnX,X表示参数占用的字节数. CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈. 函数在编译的时候就必须确定参数个数. 并且调用者必须严格地控制参数的生成,不能多, 不能少, 否则返回后会出错。
  • 汇编阶段
  1. 汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程
  2. 对于被翻译系统处理的每一个C语言源程序, 都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
  3. 目标文件由段组成. 通常一个目标文件中至少有两个段: 代码段和数据段
  • 链接阶段
  1. 将所有的目标文件代码拼接且重定位符号地址, 生成可执行文件
  2. 两种链接方式: 动态链接和静态链接

动态链接和静态链接区别

  • 静态链接
  1. 在编译时静态库和程序链接
  2. 一般命名为:libxxx.a
  3. 运行时,可执行目标文件已经装载完毕,速度快
  4. 但是多个程序需要静态库时每个都会复制一份,造成内存浪费
  5. 更新后需要重新编译
  • 动态链接
  1. 在运行时链接
  2. 一般命名: libxxx.so
  3. 运行时加载链接, 速度相对慢
  4. 运行时多个程序共享同一份动态库, 不会造成内存浪费
  5. 易更新, 无需重新编译

内存管理

new/delete和malloc/free区别

  • new是C++关键字,需要编译器支持. 而malloc是C语言库函数
  • new失败时, 会抛出bad_alloc异常. malloc会返回NULL
  • new执行时, 先分配内存, 再调用类的构造函数初始化, malloc只会分配内存
  • new无需指定分配内存大小, 编译器会根据类型信息自行计算, malloc需要在参数里指出分配内存的大小
  • new成功会直接返回所分配的对象的指针, 而malloc只会返回void指针, 需要转化才能得到想要的对象的指针
  • C++允许重载new/delete操作符, 而malloc不允许重载, new从自由存储区上为对象动态分配内存空间, malloc从堆上分配内存

C++中有几种类型的new

  • plain new: 普通new, 特点在new和malloc区别里说了
  • nothrow new: 在空间分配失败时不会抛出异常, 而是返回NULL. 使用:
  char *p=new(nothrow) char[10e11]
  • new operator: 只做两件事:(1)调用operator new (2)调用类的构造函数
  • operator new: 可以重载, 实际以标准C malloc()完成
  • placement new: 在一块已经分配成功的内存上重新构造对象或对象数组. 不会出现内存分配失败, 因为他只调用构造函数而不会分配内存. 用placement new构造和对象数组, 要显式调用它们的析构函数来销毁, 而不要用delete[], 因为构造起来的对象或数组大小不一定等于原来内存的大小, 用delete会造成内存泄漏或运行时出现错误. 使用:
  #include <iostream>
  #include <string>
  using namespace std;
  class ADT{
      int i;
      int j;
  public:
      ADT(){
          i = 10;
          j = 100;
          cout << "ADT construct i=" << i << "j="<<j <<endl;
      }
      ~ADT(){
          cout << "ADT destruct" << endl;
      }
  };
  int main()
  {
      char *p = new(nothrow) char[sizeof ADT + 1];
      if (p == NULL) {
          cout << "alloc failed" << endl;
      }
      ADT *q = new(p) ADT;  //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
      //delete q;//错误!不能在此处调用delete q;
      q->ADT::~ADT();//显示调用析构函数
      delete[] p;
      return 0;
  }

malloc原理

  • Malloc一般是从heap分配. heap会事先分配n个页, 放在一起, 并在里面做些标记表明哪些块是使用中的哪些是空闲的. malloc只是从这里面找到一个空闲的块。
  • 当开辟的空间小于 128K 时, 调用 brk()函数
  • 当开辟的空间大于 128K 时,调用mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为"文件映射区域"的地方)找一块空间来开辟

C++内存分区

  • 自高地址到低地址依次是栈, 对, 全局数据区, 常量区和代码区
  • 栈是程序的局部变量存储区域, 分配效率高, 但是内存容量有限, 同时离开变量作用域后会存储单元会自动释放
  • 堆是程序由malloc分配的内存块, 需要手动释放内存空间
  • 自由存储区: new操作符从自由存储区(free store)上为对象动态分配内存空间,自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。自由存储区通常是堆
  • 全局/静态存储区: 全局变量和静态变量存储的区域, 未初始化的静态变量会自动初始化为0
  • 代码区: 存放函数体的二进制代码

面向对象

面向对象编程的三大特征

  • 封装 继承 多态

纯虚函数和虚函数

  • 纯虚函数在定义类不能被实现, 只有继承类才能被实现. 可以认为是一种接口函数含有纯虚函数的类被称为抽象类
  • 虚函数是为了重载和多态需要, 在基类中可以实现, 子类重写后会覆盖基类的虚函数

构造函数中能调用其他成员函数吗

  • 可以, 但是不能调用类的静态成员函数, 虚函数以及以来类构造完成的函数

构造函数和析构函数中能用虚函数吗

  • 都不能
  • 在构造函数中: 父类对象会在子类之前进行构造, 此时子类部分数据成员还没初始化, 调用子类的虚函数时不安全的
  • 析构函数: 构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。

构造函数和析构函数能成为虚函数吗

  • (1)构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的. 而在构造一个对象时, 由于对象还未构造成功. 编译器无法知道对象的实际类型, 是该类本身, 还是该类的一个派生类. (2)虚函数的执行依赖于虚函数表. 而虚函数表在构造函数中进行初始化工作,即初始化vptr, 让他指向正确的虚函数表. 而在构造对象期间, 虚函数表还没有被初 始化,将无法进行。
  • 有虚函数的类的析构函数必须为虚函数. 因为只有序析构函数才能自动调用基类的析构函数.

重载, 覆盖和隐藏区别

  • 函数重载: 同一作用域内内, 一组具有不同参数列表的同名函数(静态绑定)
  1. C++编译器对函数名的处理: 作用域+返回类型+函数名+参数列表
  2. 参数列表必须不同(个数, 类型或参数排列顺序)
  3. 仅仅返回值不同不足以称为函数重载
  4. C语言不支持函数重载, 因为C语言在编译过程会保留原始的函数名
  • 覆盖: 重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体要求基类函数必须是虚函数且函数参数列表必须相同(动态绑定)
  • 隐藏: 指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数.
  1. 两个函数参数相同, 但是基类函数不是虚函数
  2. 两个函数参数不同, 无论基类函数是不是虚函数, 都会被隐藏

C++多态如何实现

  • 多态性: 在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数
  • 虚函数表: 在有虚函数的类中,类的最开始部分是一个虚函数表的指针vptr,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
  • 虚函数表结构(通过命令g++ -fdump-class-hierarchy xxx.cpp 查看):
  1. 非空虚基类
  class A
  {
  public:
     int a;
     virtual void v();
  };
  sizeof(A ) = 16  ,align=8
  1. 单继承
  class A {
  public:
    int a;
    virtual void v();
  };
  class B : public A {
  public:
    int b;
  };
  sizeof(B) = 16, align = 8
  1. 简单多继承1
  class A {
  public:
    int a;
    virtual void v();
  };
  class B {
  public:
    int b;
    virtual void w();
  };
  class C : public A, public B {
  public:
    int c;
  };

  sizeof(C) = 32 ,align = 8
  1. 简单多继承2
  class A {
  public:
    int a;
    virtual void v();
  };
  class B {
  public:
    int b;
    virtual void w();
  };
  class C : public A, public B {
  public:
    int c;
    void w();
  };
  sizeof(C) = 32 ,align = 8
  1. 多重继承
  class A {
  public:
    int a;
    virtual void v();
  };
  class B : public A {
  public:
    int b;
    virtual void w();
  };
  class C : public A {
  public:
    int c;
    virtual void x();
  };
  class D : public B, public C {
  public:
    int d;
    virtual void y();
  };
  sizeof(D)  = 40 align = 8
  1. 虚继承
  class A {
  public:
    int a;
    virtual void v();
  };

  class B : public virtual A {
  public:
    int b;
    virtual void w();
  };

  class C : public virtual A {
  public:
    int c;
    virtual void x();
  };

  class D : public B, public C {
  public:
    int d;
    virtual void y();
  };


  sizeof(D) = 48,align = 8

C++有几种构造函数

  • 拷贝构造函数
  • 默认构造函数
  • 移动构造函数
  • 委托构造函数
  • 继承构造函数

标准模板库

list和vector区别

  • vector: 连续存储的容器,动态数组,在堆上分配空间
  1. 底层实现:数组
  2. 两倍容量增长
  3. 查询时间复杂度为O(1), 插入删除平均时间复杂度为O(n)
  • list: 在堆上分配空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间
  1. 插入删除时间复杂度O(1), 查询时间复杂度O(n)
  2. 底层实现: 双向链表

迭代器失效的情况

  • set和map的操作不会使迭代器失效,因为删除只会导致被删除的元素的迭代器失效,因为被删除的节点不会影响其他节点
  • vector的删除会使后面的迭代器都失效
  • list的操作不会导致任何迭代器失效

vector扩容原理

  • vector是一个能够存放任意类型的动态数组
  • vector每次容量满了后会扩容, 一般为当前容量的两倍. 旧内存空间的内容按照原来顺序放到新的内存中
  • 最后将旧内存的内容释放掉, 但是存储空间没有释放

set和map区别

  • map和set都是C++的关联容器, 其底层实现都是红黑树(RB-Tree). 几乎所有的 map 和set的操作行为, 都只是转调 RB-tree 的操作行为。
  • map中的元素是key-value对, 而set只有value
  • set迭代器是const, 不允许修改元素的值. map允许修改value的值, 但是不允许修改key的值. 否则会使迭代器失效
  • map支持下标操作

set和unordered_set区别

  • set是有序的, 底层数据结构是红黑树
  • unordered_set是无序的,底层数据结构是哈希表

迭代器和指针区别

  • 迭代器不是指针, 各类模板, 表现得像指针. 他只是模拟了指针的一些功能, 通过重载了指针的一些操作符,->、*、++、--等.
  • 迭代器本质是封装了原生指针, 是指针概念的一种提升, 提供了比指针更高级的行为
  • 迭代器类的访问方式就是把不同集合类的访问逻辑抽象出来, 使得不用暴露集合内部的结构而达到循环遍历集合的效果

基础

static作用

  • static变量默认初始化为0
  • static全局变量: 表示该变量作用域只在该变量所在的文件中. 在程序运行期间一直存在
  • static局部变量: 作用域仍为局部作用域, 但是离开局部作用后变量并没有销毁, 而是依然驻留在内存中, 下次该局部变量在进入作用域时, 值不变, static局部变量只会被初始化一次.
  • static函数: 函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。不要在头文件声明static函数
  • static成员变量: 静态成员可以实现多个对象之间的数据共享. 即无论对象有几个, 一个类中的static成员变量只有一个
  • static成员函数: 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员. 没有this指针, 所以只能用类的静态成员变量和静态成员函数

指针和引用区别

  • 指针有自己的一块空间. 引用只是个别名
  • sizeof(指针)=4/8字节, 取决于机器位数. sizeof(引用)=引用对象大小
  • 指针可以被初始化为nullptr, 引用必须被初始化为一个已有对象的引用
  • 可以有const指针, 没有const引用
  • 指针可以有多级(**p), 引用只有一级
  • 指针和引用使用++运算符意义不同, 前者
  • 返回东爱内存分配的对象, 必须使用指针, 否则可能引起内存泄漏
  • 指针可以任意改变指向对象, 引用一旦被初始化就不能改变指向指针
  • 指针使用时需要解引用(->), 引用可以直接使用

结构体对齐

  • 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 为编译器默认的一个对齐数与该成员大小中的较小值。vs中默认值是8 Linux默认值为4(当然可以通过#pragma pack()修改),但修改只能设置成1,2,4,8,16.
  • 结构体总大小为最大对齐数的整数倍。(每个成员变量都有自己的对齐数)
  • 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。

区别一下指针类型

int *p[10]   //指针数组, 强调数组概念, 是一个数组, 每个元素都是指向int*
int (*p)[10] //数组指针, 强调指针概念, 只有一个变量, 指向int数组
int *p(int)  //函数,返回值为int*
int (*p)(int)//函数指针,返回值为int

什么情况必须使用初始化成员列表

  • 初始化const成员
  • 初始化reference成员
  • 调用基类的构造函数
  • 调用数据成员对象的构造函数

strlen和sizeof区别

  • sizeof是运算符, 而不是函数, 结果在编译时得到. strlen是C语言库函数
  • sizeof参数可以是任何数据类型, strlen参数只能是字符指针且结尾是'\0'的字符串
  • sizeof不能得到动态分配的变量的大小

常量指针和指针常量区别

  • 常量指针是指向常量的指针: const char *str="zhanyi";
  • 指针常量是指不能改变指向的指针: char *const str=p;

数组名和指针区别

  • sizeof得到结果不同
  • 字符数组内容可以改版, 字符指针内容不能改变

野指针和悬空指针

  • 野指针: 指针指向了被delete/free的内存
  • 空指针: 没有指向的指针

C和C++区别

  • C++是面向对象语言, C是面向过程语言
  • C++具有封装, 继承和多态三种特性
  • C++相比C, 增加许多类型安全的功能
  • C++支持方式编程, 不如模板函数和模板类

extern"C"用法

  • *

内联函数和宏定义区别

  • 根本区别宏定义只是字符替换, 内联函数是个函数(类型安全检查, 自动类型转换)
  • 代码展开发生在程序执行的不同阶段. 前者在预处理阶段, 后者在编译阶段
  • 内陆函数可以作为类的成员函数, 宏定义不能

const和指针

  • 顶层const: const修饰的变量本身是个常量, 即const在*右边
  • 底层const: const修饰的变量指向的对象是个常量, const在*左边
  • const_cast只对底层const起作用

define和const区别

  • define是在预处理阶段起作用, const在编译, 运行阶段起作用
  • define只做替换, 不做类型安全检查和计算, const相反
  • define在内存中有多份相同的备份, const只有一份

#和##在define中的作用

assert

其他

C和C++的类型安全

  • 类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域
  • C的类型安全: C只在局部上下文中表现出类型安全, 比如试图从一种结构体的指针转换成另一种结构体的指针时, 编译器将会报告错误, 除非使用显式类型转换. 然而, C中相当多的操作是不安全的. 比如:
  #include<stdio.h>
  int main(){
      printf("%d\n",10);//正常
      printf("%f\n",10);//输出错误
      printf("%s\n",10);//运行时segmentation
  }
  • C++的类型安全: C++比C更有类型安全性.
  1. 操作符new返回的指针类型严格与对象匹配,而不是void*
  2. C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
  3. 引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换
  4. 一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全
  5. C++提供了dynamiccast关键字,使得转换过程更加安全,因为dynamiccast比static_cast涉及更多具体的类型检查。

volatile关键字

  • 提示编译器不要对访问该变量的代码进行优化
  • 两个作用:保证变量的内存可见性;禁止指令重新排序
  • 变量的内存可见性: 一个线程修改了某个变量, 对其他线程是可见的

explicit关键字

  • 只能用于修饰只有一个参数的类构造函数
  • 它的作用是表明该构造函数是显示的, 而非隐式的. 跟它相对应的另一个关键字是implicit, 意思是隐藏的, 类构造函数默认情况下即声明为implicit(隐式).

C++异常处理

  • 用throw抛出异常, 在try作用域中运行可能抛出异常的代码, 通过catch捕捉异常

内存泄漏

最近发表
标签列表