【面经总结】C++版

开始总结面试中经历的问题,也包括一些高频考点和重难点。

一、 C++11特性

(1) C++的四种强制类型转换

四种强制类型转换操作符为别为:static_cast, dynamic_cast, const_cast, reinterpret_cast

dynamic_cast:用于多态类型的转换。
用于将一个父类的指针/引用转化为子类的指针/引用(安全的下行转换)
需要注意以下几点:

  • 基类必须有虚函数,运行时检查类型信息(存储在类的虚函数表中)
  • 对指针进行转换,失败返回NULL,成功给返回正常cast后的对象指针
  • 对引用进行转换,失败抛出一个异常,成功返回正常cast后的对象引用

static_cast:用于非多态类型的转换。(代替C中通常的转换操作)
在基本数据类型之间转换,如把int转换为char,这种带来的安全性问题由程序员自己来保证;
在有类型指针与void*之间转换;
用于类层次结构中基类和派生类之间指针或引用的转换(一般不使用)
派生类---->基类:上行转换,安全
基类---->派生类:下行转换,没有动态类型检查,不安全

const_cast:用于删除const、volatile和_unaligned特性
常量指针与非常量指针之间的转换(比较危险,避免使用)

reinterpret_cast:用于对位进行简单的重新解释(高危操作)
用在任意指针(或引用)类型之间的转换;能够将整型转换为指针,也可以把指针转换为整型或数组。

(2)C++的四种智能指针

智能指针是对指针进行封装,可以像普通指针一样进行使用,同时可以自行进行释放,避免忘记释放指针指向的内存地址造成内存泄漏。C++的智能指针包括auto_ptr, shared_ptr, weak_ptr和unique_ptr。

  • auto_ptr是较早版本的智能指针,在进行指针拷贝和赋值的时候,新指针直接接管旧指针的资源并且将旧指针指向空,但是这种方式在需要访问旧指针的时候,就会出现问题
  • unique_ptr是auto_ptr弃用之后的替代品,不能赋值也不能拷贝,保证一个对象同一时间只有一个智能指针。赋值可以使用移动语义move函数进行
  • shared_ptr可以使得一个对象可以有多个智能指针,当这个对象所有的智能指针被销毁时就会自动进行回收。(内部使用计数机制进行维护,但可能会出现两个智能指针循环引用造成无法正常释放资源)
  • weak_ptr是为了协助shared_ptr而出现的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。它的构造和析构不会引起计数的增加或减少

(3)lambda表达式

利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。

// 指明返回类型
auto add = [](int a, int b) -> int { return a + b };
// 自动推断返回类型
auto multiply = [](int a, int b){ return a * b };

闭包[]中的内容:

  • []:默认不捕获任何变量;
  • [=]:默认以值捕获所有变量
  • [&]:默认以引用捕获所有变量
  • [x]:仅以值捕获x,其他变量不捕获
  • [&x]:仅以引用捕获x,其他变量不捕获
  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获
  • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获
  • [this]:通过引用捕获当前对象(其实是复制指针)
  • [*this]:通过传值方式捕获当前对象

(4)拷贝构造函数和移动构造函数的区别

  • 拷贝构造函数用于从一个已存在的对象创建创建一个新的对象,即复制构造函数。它通常有一个类对象作为参数,返回一个新的对象,该对象与原始对象具有相同的值。如果一个类没有定义拷贝构造函数,则编译器会生成一个默认的拷贝构造函数,它将逐个赋值所有非静态成员。如果一个类具有指针或引用成员,则需要自己编写的拷贝构造函数,以确保正确地复制指针或引用所指向的对象
  • 移动构造函数用于从一个临时对象创建一个新对象,以提高效率和减少内存使用。它们采用右值引用的语法,并将临时对象的资源移动到新对象中,而不是复制它们。如果一个类具有指针和其他资源成员,则需要实现移动构造函数和移动赋值运算符,以确保正确地移动这些资源。
  • 区别
    • 功能不同:拷贝构造函数会通过复制原始对象的所有变量的值,从而创建一个新的、与原始对象相同的对象;移动构造函数用于从一个右值引用的临时对象创建一个新的对象。它会“窃取”原始对象的资源(例如指针或文件句柄)
    • 形参类型不同:拷贝构造函数的参数通常是const引用类型的对象;移动构造函数的参数通常是右值引用类型的对象
      //拷贝构造函数
      MyClass(const MyClass& other);
      
      //移动构造函数
      MyClass(MyClass&& other);
    • 调用时机不同:拷贝构造函数通常在用一个对象初始化另一个对象、传递一个对象作为参数到函数中、从函数中返回一个对象被调用;移动构造函数通常在从一个临时对象创建一个新对象、将一个临时对象作为参数传递到函数中、从函数中返回一个临时对象被调用。
    • 适用范围不同:拷贝构造函数适用于所有类型的对象;移动构造函数适用于具有可移动资源的对象,例如指针、文件句柄、unique_ptr等,但不适于具有固定位置内存的对象,例如数组、vector等。

二、C++基础

(1)C++构造函数和析构函数的调用顺序

构造函数:父类构造函数->成员变量构造函数->自身构造函数

析构函数:自身析构函数->成员变量析构函数->父类析构函数

(2)C++虚函数相关

虚函数:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

C++的虚函数是实现多态的机制。它是通过虚函数表实现的,虚函数表是每个类中存放虚函数地址的指针数组,类的实例在调用函数时会在虚函数表中寻找函数地址进行调用,如果子类覆盖了父类的函数,则子类的虚函数会指向子类实现的函数地址,否则指向父类的函数地址。一个类的所有实例都共享同一张虚函数表。

(3)基类的析构函数一般写成虚函数的原因

当析构一个指向子类的父类指针时,编译器可以根据虚函数表寻找到子类的析构函数进行调用,从而正确释放子类对象的资源。

如果析构函数不被声明为虚函数,则编译器实施静态绑定,在删除指向子类的父类指针时,只会调用父类的析构函数而不调用子类析构函数,这样就会造成子类对象析构不完全造成内存泄露。

(4)构造函数为什么一般不定义为虚函数

虽然虚函数表在程序编译的时候创建,但是对于虚函数指针来说,它基于对象,因此是在运行时创建。
创建一个对象需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构建一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等;
虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了。

(5)纯虚函数

纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承。它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象

(6)C++中const的用法

const修饰类的成员变量时,表示常量不能被修改
const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数
对于指针,如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量

(7)宏定义#define和常量const的区别

  • 类型和安全检查不同:宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查
  • 编译器处理不同:宏定义是一个“编译时”概念,在预处理阶段展开,不能对宏定义进行调试;const常量是一个“运行时”概念,在程序运行使用,类似于一个只读行数据
  • 存储方式不同:宏定义是直接替换,不会分配内存,存储于程序的代码段中;const常量需要进行内存分配,存储于程序的数据段。
  • 定义域不同:宏定义适用于全局定义域;const常量只存在局部函数中
  • 是否可以做函数参数:宏定义不能作为参数传递给函数;const常量可以在函数的参数列表中出现

(8)自动局部变量与静态局部变量

局部变量在声明时,在类型前可以增加修饰(auto、static)
auto int a;表示a是自动局部变量,auto关键字可以省略,它与int a;等价
static int b;表示b是静态局部变量

C++11中作出更新,auto更新为自动类型推断,register显示地指出变量是自动的

(9)友元函数(friend)

  • 友元函数是单向的,A在B类中被声明为友元函数/友元类(定义无需friend修饰),表示A是B的友元函数/友元类,但B不是A的友元函数/友元类
  • 友元函数/友元类具有和类成员一样的权限,可以访问protected和private权限的成员,但不是类的成员,因此破坏了封装性
  • 友元函数不能被继承,同时也不具有传递性。
  • 友元函数本质上属于普通函数,不在类范畴中,没有this、成员的概念。

(10)运算符的重载

双目运算符:使用友元函数重载,使其符合交换律
单目运算符:使用成员函数重载
特例:<< >>左侧为流的对象引用,所以必须使用友元函数重载

(11)C++中的new/delete和C中的malloc/free的区别

  • malloc/free是C++/C语言的标准库函数,而new/delete是C++的运算符
  • new建立的是一个对象,malloc分配的是一块内存
  • new建立的对象,用成员函数进行访问,最好不要直接访问它的地址空间;malloc分配的是一片内存区域,可以直接用指针访问,而且还可以在其中移动指针
  • new可以认为是malloc加上构造函数组成,delete可以认为是free加上析构函数组成
  • new构建的指针是带类型信息的(对象指针),而malloc返回的都是void*指针

(12)static的应用场景

  • 静态全局变量:限制了该变量的作用域为当前文件,不能被其他文件访问。
  • 静态局部变量:在程序执行到该对象的声明处时被首次初始化;虽然初始化位置在全局数据区,但作用域为局部作用域,当定义函数或者语句块结束时,作用域随之结束
  • 静态函数:不能为其他文件所用;其他文件可以定义相同名字的函数,不会发生冲突
  • 静态数据成员:类中的所有对象共有,只分配一次内存
  • 静态成员函数:类中的所有对象都可以调用的成员函数,属于类定义的一部分,没有this指针,也不能访问类对象的非静态数据成员

(13)迭代器失效问题

  • 序列式容器(vector、deque):erase和insert会使当前位置到容器末尾元素的迭代器全部失效,解决办法是利用erase方法可以返回下一个有效的iterator;扩容时,所有迭代器都会失效
  • 关联式容器(map、set、multimap、multiset):删除当前的iterator,仅仅会使当前的失效;插入不会使得任何迭代器失效
  • 链表式容器(list):同上
  • 哈希容器(unordered_map,unordered_set):同上

(14)继承和组合的区别

组合是指在类中嵌入对象,从而使组合类可以使用嵌入对象的全部或部分成员和方法。与继承不同,组合使静态的,即组合类与嵌入对象之间的关系在编译时就已经确定,不能再运行时动态改变。
组合可以分为成员变量组合和指针组合两种

  • 成员变量组合:指嵌入对象作为组合类的成员
  • 指针组合:指嵌入对象作为指针成员,需要在组合类的构造函数中动态分配内存
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值