🥭01-C++面试之C++11新特性总结
0 前述
针对于自己在秋招的面试中,对于Cpp
部分遇到的问题,其中大部分是以此为起点,尼克以基于这些点,将自己对于Cpp
的学习,串联起来,无论面试官,问这一类问题中的那个点,你都应该可以将这一个珠子串联到自己一大串知识点上来讲。这是一种拓展知识的能力。
在此专栏下面个人校招记录:回馈牛客,对CPP
做一个小小的总结。
本部分关于C++11
新特性总结,挑选几个比较重要的点来展开即可,如**lambda
表达式、右值引用和三种智能**指针来展开。
- 下面对应的是之前发布的个人校招其他公司面试总结,希望可以更好的帮到你
这里是Cpp
一些面试问题整理
1 关键字/语法层面
-
nullptr
替代 NULL -
引入了 auto 和 decltype 这两个关键字实现了类型推导:使得编译器在编译阶段就推导出变量的类型,可以通过
=
右边的类型推导出变量的类型。(比较方便,自己经常使用到的) -
继承关键字:final 和override,默认构造函数**=default**、=delete,内存对齐alignof和alignas,delete,using的使用。(这些完全都可以是个引子,展开再说)
final
一个规范化的关键字:
如果后面不允许继承该类的话,后面应使用
final
关键字来结束:// final 用法 class A final { // }; //声明为final,B再继承的话,编译报错 class B : A { // };
-
override
(重写/覆盖)关键字:要求基类函数是寻函数,其派生类去override
这个虚函数,需要保证与基类的虚函数名字、类型、参数等完全一致。好的写法应该是:在重写函数的后面加上关键字override
。1)避免写错函数名,若写错的话,编译器会认为这是一个独立的函数,编译器不会检查这个错误。
1 using
和typedef
的区别使用:
using可以用于模版别名,typedef
不可以用于模版别名。
(自己认为,C++里面还是using用着比较方便)
template<typename T>
class A {
public:
A() {
// ...
}
};
template <typename T>
using B =A<T>;
template <typename T>
typedef A<T> C;
int main() {
A<int> a;
B<int> b; // Ok, B is an alias of class A
C<int> c; // Syntax Error, C cannot be recognized as a type
return 0;
}
-
基于范围的for循环,结合auto关键字,也使用的比较多
for (auto& val : array) { // ... }
2 标准库层面
- 增加无序容器:哈希表
hashtable
; - 正则表达式
1 Lambda表达式(展开讲讲)
-
基本形式:
[捕获列表](参数列表){函数体};其中捕获列表和函数体不能省略但是捕获列表可以为空,也就是最简单的lambda表达式:
[]{};
。 -
lambda 表达式又叫匿名函数,C++中,一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数,特别注意,他们的返回值类型由函数体的return语句决定,因此一般使用auto关键字来自动推导。
-
使用实例:
int i =[] {return 1;}
-
捕获列表
[]
使用:[=],表示所有捕获的参数都是以值传入的,不可修改
[&],表示所有捕获的参数都是以应用传入的,可以修改,修改之后,对外部的变量产生影响。
int p =100, q =200; auto func =[&] {q++, p+=2; return p +q;} //
-
带调用参数的lambda表达式
// 返回和 int a, int b是调用时传递的参数 auto myADD =[](int v1, int v2) { return a +b; } // 用法 auto ans =myADD(2, 3);
-
带明确的返回值的lambda表达式,使用
->
// 返回最大值 auto myMAX =[](int a, int b) ->int { if (a >b) return a; else return b; } // 用法 auto ans =myMAX(2, 3);
-
利用lambda表达式可以编写内嵌的匿名函数,用以独立函数或者函数对象,在向算法传递函数的时候,比如sort排序的时候,我们按照什么顺序你,可以河面穿进去一元谓词(接受一个参数)或者二元谓词(接受两个参数)
-
Lambda 的类型被定义为“闭包”的类,其通常用于STL 库中,在某些场景下可用于简化仿函数的使用,同时Lambda 作为局部函数,也会提高复杂代码的开发加速,轻松在函数内重用代码,无须费心设计接口。
// 语法形式: // [capture list](parameter list) -> return type {function body} // [capture list]: 声明使用那些局部变量,但只能使用那些明确指明的变量,只有捕获了才能在{function body}中使用。
-
参考网址
3 右值引用
1 右值引用概念
右值引用也是别名,但是只能对右值进行引用。引用的形式是int&& ra =10
,只是为了区分C++98里面的引用,C++11将该种方式称之为右值引用。
2 左值与右值使用形式
- 普通类型的变量,
int a
,因为有名字,可以取地址,都认为是左值。 - const修饰的常量,代表是不可修改,只读类型,但是我们可以取地址,C++11认为其是左值。也可以指向右值,比如
vector
的push_back (const int & val)
函数的设计,直接插入push_back (5)
就是这个原理。 - 表达式的运行结果是一个临时变量或者对象,如(a+b),认为是右值;
- 表达式的运行结果是单个变量或是一个引用,如(c=a+b)认为是左值;
- 右值区分:
- C语言中的纯右值,比如说:
a+b, 100
- 将亡值,也就是生命周期即将结束的变量,比如临时变量:表达式的中间结果,函数按照值的方式进行返回,匿名变量。(后面展开说)
- C语言中的纯右值,比如说:
3 实际用法介绍
int main()
{
// 1.普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
//const引用既可以引用左值也可以引用右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
- 普通引用只能引用左值,不能引用右值;const引用既可以引用左值,也可以引用右值;
- C++11中右值引用;只能引用右值,一般情况不能直接引用左值(需要使用move处理一下);
int main()
{
// 10纯右值,本来只是一个符号,没有具体的内存地址,
// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int&& r1 = 10; // 可以是常数,出来之后就可以表示一个左值来用
r1 = 100;
int a = 10;
// int&& r2 = a; // 编译失败:右值引用不能引用左值
int&& r3=std::move(a);// 作为函数&& 返回值是右值 ,直接声明出来的 && 是左值
return 0;
}
-
右值引用只能引用右值,不能引用左值;
-
右值引用可以进行引用std::move()处理以后的左值,move表示将该变量(左值)识别为右值,并没有移动什么,只是将左值强制转换为右值;
-
右值引用本质是将引用的右值内容存储到空间中,使得该右值引用变量具有名称和地址,所以右值引用变量一般是一个左值。作为函数&& 返回值是右值 ,直接声明出来的 && 是左值。
4 右值引用和std::move
的使用场景
1)一点结论
- 从性能上讲,左右值引用并没有区别,传参使用左右值引用都可以避免一次参数拷贝。
- 右值引用可以直接指向右值,还可以通过
std::move()
指向左值;而左值引用只能指向左值(除去const左值引用,也能指向右值) - 作为函数的形参时,右值引用更加灵活,虽然const左值引用也可以做到左右值都接受,但它无法修改传进来的参数,有一定的局限性。
2)右值实现移动语义
在实际的场景中,右值引用和std::move
别广泛用于在STL和自定义的类中实现移动语义,避免拷贝,从而提升程序性能
在没有右值引用之前,一个数组类需要有构造函数、拷贝构造、赋值运算符重载、析构函数等。
class Array {
public:
Array(int size) : size_(size) {
data_ = new int[size_];
}
// 深拷贝构造
Array(const Array& temp_array) {
size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
}
// 深拷贝赋值
Array& operator=(const Array& temp_array) {
delete[] data_;
size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
}
// 析构函数
~Array() {
delete[] data_;
}
public:
int *data_;
int size_;
};
该类的拷贝构造、赋值运算符重载函数已经通过使用左值引用来避免一次多余的拷贝了,但是内部实现还是要深拷贝,无法避免。
这时候,就感觉可不可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝。
创造类似下面这样的情况:
// 移动构造函数,可以浅拷贝,不优雅的实现方式
Array(const Array& temp_array, bool move) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
}
有2个问题
- 不优雅,要与构造函数区分,需要引入额外的参数
- 无法实现,const类型的左值引用,你不能修改参数类型。若改成非const类型的,直接插入的形式(直接常量,直接一个数组)就不能使用了。
这样,左值引用就用着很不爽了,右值引用的出现解决了这个问题,STL的容器中,都实现了以右值引用为参数的移动构造函数和移动赋值重载函数;常见的比如vector
的push_back()
和emplace_back()
,如下P5进行展示。参数为左值引用意味着拷贝,为右值引用意味着移动
// 优雅
Array(Array&& temp_array) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
}
5 右值引用中std::move
语义在STL容器中的使用
// 例2:std::vector和std::string的实际例子
int main() {
std::string str1 = "aacasxs";
std::vector<std::string> vec;
vec.push_back(str1); // 传统方法,copy
vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
vec.emplace_back("axcsddcas"); // 当然可以直接接右值
}
// std::vector方法定义
void push_back (const value_type& val);
void push_back (value_type&& val);
void emplace_back (Args&&... args);
在vector和string这个场景,加个std::move
会调用到移动语义函数,避免深拷贝。可用在对象在需要拷贝并且拷贝之后不再被需要的场合,建议使用std::move
触发移动 语义,提升性能。
另外,在STL中有些类是move-only
的,比如unique_ptr
只有移动构造函数。
6 完美转发std::forward
和std::move
一样,它的兄弟std::forward
也充满了迷惑性,forward意思是转发,但是它并不会做转发,同样也是类型转换,
不过他更高级比兄弟move,``move
只能转出右值,forward是左右值都可以。
std::forward(u)有两个参数:T与 u。
a. 当T为左值引用类型时,u将被转换为T类型的左值;
b. 否则u将被转换为T类型右值
看下面两个例子,进一步加深理解:
例1:
void B(int&& ref_r) {
ref_r = 1;
}
// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
B(ref_r); // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
B(std::forward<int>(ref_r)); // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}
int main() {
int a = 5;
A(std::move(a));
}
例2:
void change2(int&& ref_r) {
ref_r = 1;
}
void change3(int& ref_l) {
ref_l = 1;
}
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
change2(ref_r); // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
change1(ref_r); // ok 表示的左值就没问题
change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
change2(std::forward<int &&>(ref_r)); // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
// 可见,forward可以把值转换为左值或者右值
}
int main() {
int a = 5;
change(std::move(a));
}
7 参考链接
4 三个智能指针
C++11引入智能指针的概念,方便管理堆内存。使用普通指针容易造成内存泄漏,忘记释放、或者是二次释放、程序发生异常是内存泄漏问题。使用智能指针能够更好的管理堆内存;包含在头文件#include <memory>
中。智能指针是RAII(resource acquire instance initalize🙆♂️)最具代表性的实现。三种指针的大小测试说明:
1 shared_ptr(正常指针大小的二倍):
使用引用计数,每一个shared_ptr的拷贝都指向相同的内存地方。每次使用拷贝,内部的应用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。另外,shared_ptr的内部引用计数是线程安全的,但是对象的读取是需要加锁的。
初始化的时候。智能指针是个模板类,1)可以指定类型,传入指针通过构造函数初始化。2)也可以使用make_shared
函数初始化。不能将指针直接赋值给一个智能指针,因为一个是类,一个是指针。例如std::shared_ptr p4 =new int(1)
,需要使用构造函数的形式。
2 unique_ptr(正常指针大小):
表示唯一拥有其所指对象,同一时刻只能有一个unique_ptr
指向给定对象(通过禁止拷贝语义,只用移动语义来实现)。
相比于原始指针,unique_ptr
指针用于其RAII(Resource Acquisition is Initialization
)直译过来就是,资源获取即初始化,也就是说在构造函数中申请分配资源,在析构函数中释放资源。由于C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
在unique_ptr
的声明周期内,可以改变其所指对象,如创建智能指针是通过构造函数指定,中间通过reset方法重新指定,通过release方法释放所有权,通过移动语义转移所有权。
3 weak_ptr(正常指针大小的二倍):
是一种不控制对象生命周期的智能指针,它指向shared_ptr
管理的对象,weak_ptr只是提供了对管理对象的一个访问手段。引入的目的是为了配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只能从一个shared_ptr
或者另一个weak_ptr
进行对象构造,它的构造和析构不会引起引用计数的增加或者减少。
解决shared_ptr
循环引用形成的锁,问题如下:
template <typename T>
class Node {
public:
Node(const T& value) :_pre(NULL), _next(NULL), _value(value) {
cout << "Node()" << endl;
}
~Node() {
cout << "~Node()" << endl;
cout << "this:" << this << endl;
}
shared_ptr<Node<T>> _pre;
shared_ptr<Node<T>> _next;
T _value;
};
void fun_test() {
shared_ptr<Node<int>> sp1(new Node<int>(1));
shared_ptr<Node<int>> sp2(new Node<int>(2));
cout << "sp1.use_count: " << sp1.use_count() << endl;
cout << "sp2.use_count: " << sp2.use_count() << endl;
sp1->_next =sp2; // sp2 引用+1
cout << "sp1.use_count: " << sp1.use_count() << endl;
cout << "sp2.use_count: " << sp2.use_count() << endl;
sp2->_pre =sp1; // sp1 引用+1
cout << "sp1.use_count: " << sp1.use_count() << endl;
cout << "sp2.use_count: " << sp2.use_count() << endl;
}
int main() {
fun_test();
system("pause"); // 导致离开作用域的时候,两个都不会释放,造成内存泄漏
return 0;
}
// 运行结果
Node()
Node()
sp1.use_count: 1
sp2.use_count: 1
sp1.use_count: 1
sp2.use_count: 2
sp1.use_count: 2
sp2.use_count: 2
请按任意键继续. . .
在实际的编程过程,应该尽量避免出现智能指针之间相互指向的情况,如果不可避免,可以使用weak_ptr
它不增加引用计数,只要出了作用域就会自动析构。
在使用的过程中,需要得知该std::weak_ptr
指向的资源是否有效呢?STL提供一个expired
的方法来检测,返回true的话,表示该资源有效,这时,可以使用std::weak_ptr
的lock
函数得到一个std::shared_ptr
对象后继续操作资源。
// weak_sp是一个weaked_ptr
if (weak_sp.expired()) {
return;
}
std::shared_ptr<int> shared_sp =weak.sp.lock();
if (shared_sp) {
//
}
// 这是不允许进行判断的
if (weak_sp) {
}
注意这里不能像一般指针那样去判断weak_ptr
是否有效,不能直接判断资源是否有效的方式进行解决。因为,std::weak_ptr
没有重写operator->
和operator*
,因此,该指针不能直接操作对象,也没有重写bool()
操作,不能通过判断本身是否为nullptr
来确定该资源是否有效。
4 shared_ptr
的底层实现一个简单版本
template <typename T>
class shared_ptr {
public:
// constructor
shared_ptr(T* ptr =NULL) : _ptr(ptr), _pcount(new int(1)) {}
// copy constructor
shared_ptr(const shared_ptr& s) : _ptr(s._ptr), _pcount(s._pcount) {
*(_pcount)++;
}
// copy assignment
shared_ptr<T>& operator=(const shared_ptr& s) {
if (this !=&s) {
if (--(*(this->_pcount)) ==0) {
delete this->_ptr;
delete this->_pcount;
}
_ptr =s._ptr;
_pcount =s._pcount;
*(_pcount)++;
}
return *this;
}
// overloading operator
T& operator*() {
return *(this->_ptr);
}
T* operator->() {
return this->ptr;
}
// destruct constructor
~shared_ptr() {
--(*(this->_pcount));
if (this->_pcount ==0) {
delete _ptr;
_ptr =NULL;
delete _pcount;
_pcount =NULL;
}
}
private:
T* _ptr; // 指向内存处的地址
int* _pcount; // 指向引用计数的指针
};
5 延伸一个问题:智能指针的应用计数在内存部分:堆上:hamster:
在shared_ptr
中会有引用计数的出现,unique_ptr
和weak_ptr
是没有使用引用计数的
一句话:为了共享这个reference count值。
首先一点需要明确:如果有多个智能指针指向同一对象(内存地址),他们的reference count必须相同。rf值的更改需要“广播”给所有对应的智能指针。
考虑如下代码:
shared_ptr<int> p1 =make_shared(3); // rf(p1) =1;
shared_ptr<int> p2(p1); // copy constructor rf(p1) =rf(p2) =2
p1 =nullptr;
p2 =nullptr;
-
如果引用计数在栈区,那么当p1重新改指向nullptr时,如何通知p2的rf该减1,如果各自有各自的rf,那么就不会统一指向同一资源的rf值了。就会造成一定的内存泄漏,就乱套了
-
如果rf在堆区,每个智能指针保存的是它在堆上的地址。那么当p1更改指向时,就调用(*rf)--,其作用就直接扩散到p2 的rf上来了,其堆内存才能正确释放。
所以引用计数分配在堆上,就是使得一个智能指针更新rf时,其它指向这块内存的智能指针的rf值都能一起更新。
另一种说法:
因为STL的shared_ptr想设计成“可以指向任何东西”,这就意味着你必须额外为引用计数来分配空间,不然引用计数放哪?放栈上的话函数return后引用计数就废了!
换个思路,我看到的一些自研游戏引擎是这样做的:做一个基类ISharedObject,这里面放引用计数。所有能被智能指针(引擎自己实现的智能指针)指向的东西都继承自这个类(也就意味着不继承自这个类的对象都不能被智能指针指向)。这样这个引用计数就被放到了object上。就不用额外new一个引用计数了。其实这是牺牲了一点通用性但是换来了性能提升(不用额外new引用计数,cache也是连续的)
作者:知乎用户7Az15m 链接:https://www.zhihu.com/question/290143401/answer/471490511 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。s
6 网址参考
#C++##晒一晒我的offer##我发现了面试通关密码##秋招开了,你想投哪些公司呢#这是一个求职总结专栏,求职过程中,牛客里面各位同志,提供了很多面试的信息,对我个人有很大的帮助。这里简单将自己面试记录总结于此。 本人23届校招生,双非硕士,投递岗位嵌入式,控制算法,后台开发均有涉猎,优先级递减。简历累计投递数量:提前批(34)+正式批(128),累计Offer(5+)。