面试题8

206 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1.简述C++右值引用与转移语义

(1)右值引用

一般来说,不能取地址的表达式就是右值引用;能取地址的就是左值引用。

class A{ };
A & r = A();//error,A()是无名变量,是右值
A && r = A();//成功,r是右值引用

(2)转移语义

move本意为“移动”,但是该函数不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。基于move()函数特殊的功能,常用于实现移动语义。

2.简述C++中智能指针的特点

(1)C++中的智能指针有四种,分别为:shared_ptr、unique_ptr、weak_ptr、auto_ptr,其中auto_ptr被C++11弃用。

(2)为什么要使用智能指针: 智能指针的作用就是管理一个指针,因为存放申请的空间在函数结束时容易忘记释放,造成内存泄漏的情况。使用智能指针可以很大程度上避免这个问题,因为智能指针是一个类,当焯熟了类的作用域时,类会自动调用析构函数,自动释放资源。

(3)四种指针以及其各自的特性 1.3.1 auto_ptr auto_ptr指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,所以报错。

1.3.2 unique_ptr unique_ptr规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。

实现原理:将拷贝构造函数和赋值构造函数声明为private或delete。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,通过std::move把一个对象指针变成右值之后,就能移动给另外一个unique_ptr。

1.3.3 shared_ptr 共享指针可以实现多个智能指针指向相同对象,该对象和其相关资源会在引用为0时被销毁释放。

实现原理:有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值拷贝构造函数时,引用计数加1,当引用计数为0时,释放资源。

注意:weak_ptr、shared_ptr存在一个问题,当两个shared_ptr指针相互引用时,那么这两个指针的引用计数不会下降为0,资源得不到释放。因此引入weak_ptr,weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。

3.weak_ptr能不能知道对象计数为0,为什么?

不能。

weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象管理的是那个引用的shared_ptr。

weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的只是为了配合shared_ptr而引入的一种智能指针,配合shared_ptr工作,它只可以从一个shared_ptr或者另一个weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。

4.weak_ptr如何解决shared_ptr的循环引用问题?

为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。

5.shared_ptr怎么知道跟它共享对象的指针释放了?

多个shared_ptr对象可以同时托管一个指针,系统会维护一个托管计数。大概没有shared_ptr托管该指针时,delete该指针。

6.简述一下C++11中的四种类型转换

C++中的四种类型转换分别是:const_cast、static_cast、dynamic_cast、reinterpret_cast,四种转换功能分别如下:

(1)const_cast

将const变量转换成非const变量

(2)static_cast

最常用的一种类型转换,可以用于各种隐式转换,比如非const转const,static_cast可以用于类向上转换,也可以向下转换但是不安全。

(3)dynamic_cast

只能用于含有虚函数的类转换,用于类向上和向下转换

向上转换:指子类向基类转换 向下转换:指基类向子类转换

这两种转换,子类包含父类,当父类转换成子类时可能会出现非法内存访问的问题。

dynamic_cast通过判断变量运行时类型要和转换的类型是否相同来判断是否能进行向下转换。dynamic_cast可以做类之间的上下转换,转换的时候会进行类型检查,类型相等成功转换,类型不等则转换失败。运用RTTI计数,RTTI是“Running Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。在C++层面主要体现在dynamic_cast和typeid,VS中的虚函数表的 -1 的位置存放了指向type_info的指针,对于存在虚函数的类型,dynamic_cast和typeid都会区查询type_info。

(4)reinterpret_cast

可以做任何类型的转换,不过不对转换结果保证,容易出问题。

注意:为什么不用C的强制转换?C的强制转换表面上看起来功能强大,什么都能转,但是转换不够明确,也不能进行错误检查,容易出错。

7.简述一下C++11中auto的具体用法

auto用于定义变量,编译器可以自动判断变量的类型,auto主要有以下几种用法:

1.auto的基本使用方法

(1)基本使用语法如下:

auto name = value;
//name是变量的名字,value是变量的初始值。

注意:auto仅仅是一个占位符,在编译期间它会被真正的类型所替代,或者说,C++中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。

(2)程序实例如下:

auto n = 10;//整型
auto f = 12.8;//double
autp p = &n;//int*指针
auto url = "www.cctv.com";//const char*==常量指针

2.auto和const的结合使用

(1)auto与const结合的用法

a.当类型不为引用时,auto的推导结果将不保留表达式const的属性

b.当类型被引用时,auto的推导结果将保留表达式的const属性

3.使用auto定义迭代器

在使用STL容器的时候,需要使用迭代器来遍历容器里面的元素;不同容器的迭代器有不同的类型,在定义迭代器时必须指明,而有些迭代器的类型有时候会比较复杂,就可以用auto来定义迭代器,更加简洁方便。

4.用于泛型编程

auto的另一个应用就是当我们不知道变量是什么类型,或者不希望知名具体类型的时候,用auto就会很简洁。

8.简述C++11中的Lambda新特性

1.定义

Lambda匿名函数很简单,可以套用如下的语法格式: [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 { 函数体; }; 其中各部分的含义分别为:

a. [外部变量方位方式说明符] [ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。

所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。

b. (参数) 和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;

c. mutable 此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。

注意:对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;

d. noexcept/throw() 可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。

e. -> 返回值类型 指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略"-> 返回值类型"。

f. 函数体 和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。

2.程序实例

#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
    int num[4] = {4, 2, 3, 1};
    //对 a 数组中的元素进行排序
    sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
    for(int n : num){
        cout << n << " ";
    }
    return 0;
}

/*    
    程序运行结果:
          1 2 3 4
*/ 

9.说说数组和指针的区别

(1)概念:

(a)数组适用于存储多个相同类型数据的集合,数组名是首元素的地址;

(b)指针相当于一个变量,但是它和普通变量不一样,它存放的是其他变量在内存中的地址。指针名指向了内存的首地址。

(2)区别:

(a)赋值:同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝

(b)存储方式: 数组在内存中是连续存放的,开辟一块连续的存储空间。数组是更具数组的下标进行访问的,数组的存储空间不是在静态区就是在栈上

指针很灵活,它可以指向任何类型的数据,指针的类型说明了它所指向地址空间的内存。由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。

求sizeof()

数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型)

在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下则是8

10.说说什么是野指针,怎么产生的,如何避免?

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

产生原因:释放内存后指针没有及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误,要避免产生野指针。

避免方法: (1)初始化指针置空 (2)申请内存后判空 (3)指针释放后置空 (4)使用智能指针