文章主要引用:
牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网 (nowcoder.com)
一、新特性
说说 C++11 的新特性有哪些(百度)
参考回答
C++新特性主要包括包含语法改进和标准库扩充两个方面,主要包括以下11点:
-
语法的改进
(1)统一的初始化方法
(2)成员变量默认初始化
(3)auto关键字 必须马上初始化、不能用作函数参数、不能定义数组、可以定义指针等,auto自动类型推断发生在编译期,所以不会影响程序执行期间的性能;
(4)decltype 求表达式的类型
(5)智能指针 shared_ptr
(6)空指针 nullptr(原来NULL)
(7)基于范围的for循环 for(int i : nums)
(8)右值引用和move语义 让程序员有意识减少进行深拷贝操作
-
标准库扩充(往STL里新加进一些模板类,比较好用)
(9)无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高
(10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串
(11)Lambda表达式(bind机制 )
统一的初始化方法
-
统一的初始化方法
C++98/03 可以使用初始化列表(initializer list)进行初始化:
int i_arr[3] = { 1, 2, 3 }; long l_arr[] = { 1, 3, 2, 4 }; struct A { int x; int y; } a = { 1, 2 };
但是这种初始化方式的适用性非常狭窄,只有上面提到的这两种数据类型可以使用初始化列表。在 C++11 中,初始化列表的适用性被大大增加了。它现在可以用于任何类型对象的初始化,实例如下:
class Foo { public: Foo(int) {} private: Foo(const Foo &); }; int main(void) { Foo a1(123); Foo a2 = 123; //error: 'Foo::Foo(const Foo &)' is private Foo a3 = { 123 }; Foo a4 { 123 }; int a5 = { 3 }; int a6 { 3 }; return 0; }
在上例中,a3、a4 使用了新的初始化方式来初始化对象,效果如同 a1 的直接初始化。a5、a6 则是基本数据类型的列表初始化方式。可以看到,它们的形式都是统一的。这里需要注意的是,a3 虽然使用了等于号,但它仍然是列表初始化,因此,私有的拷贝构造并不会影响到它。a4 和 a6 的写法,是 C++98/03 所不具备的。在 C++11 中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。
-
成员变量默认初始化
好处:构建一个类的对象不需要用构造函数初始化成员变量。
//程序实例 #include<iostream> using namespace std; class B { public: int m = 1234; //成员变量有一个初始值 int n; }; int main() { B b; cout << b.m << endl; return 0; }
-
auto关键字
用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化)。
//程序实例 #include <vector> using namespace std; int main(){ vector< vector<int> > v; vector< vector<int> >::iterator i = v.begin(); return 0; }
可以看出来,定义迭代器 i 的时候,类型书写比较冗长,容易出错。然而有了 auto 类型推导,我们大可不必这样,只写一个 auto 即可。
-
decltype 求表达式的类型
decltype 是 C++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。
(1)为什么要有decltype
因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。
auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:
auto varname = value; decltype(exp) varname = value;
其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。
auto 根据"="右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟"="右边的 value 没有关系。
另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:
decltype(exp) varname;
(2)代码示例
// decltype 用法举例 int a = 0; decltype(a) b = 1; //b 被推导成了 int decltype(10.8) x = 5.5; //x 被推导成了 double decltype(x + 100) y; //y 被推导成了 double
-
智能指针 shared_ptr
和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。
#include <iostream> #include <memory> using namespace std; int main() { //构建 2 个智能指针 std::shared_ptr<int> p1(new int(10)); std::shared_ptr<int> p2(p1); //输出 p2 指向的数据 cout << *p2 << endl; p1.reset();//引用计数减 1,p1为空指针 if (p1) { cout << "p1 不为空" << endl; } else { cout << "p1 为空" << endl; } //以上操作,并不会影响 p2 cout << *p2 << endl; //判断当前和 p2 同指向的智能指针有多少个 cout << p2.use_count() << endl; return 0; } /* 程序运行结果: 10 p1 为空 10 1 */
-
空指针 nullptr(原来NULL)
一般C++会把NULL、0视为同⼀种东⻄。NULL是一个在很多头文件中都有定义的宏,被定义为0;
C++11引⼊nullptr关键字来区分空指针和0。
nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。也就是说,nullpter 仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。值得一提的是,nullptr 可以被隐式转换成任意的指针类型。例如:int * a1 = nullptr; char * a2 = nullptr; double * a3 = nullptr; 显然,不同类型的指针变量都可以使用 nullptr 来初始化,编译器分别将 nullptr 隐式转换成 int、char 以及 double 指针类型。另外,通过将指针初始化为 nullptr,可以很好地解决 NULL 遗留的问题,比如:
#include <iostream> using namespace std; void isnull(void *c){ cout << "void*c" << endl; } void isnull(int n){ cout << "int n" << endl; } int main() { isnull(NULL); isnull(nullptr); return 0; } /* 程序运行结果: int n void*c */
-
基于范围的for循环
如果要用 for 循环语句遍历一个数组或者容器,只能套用如下结构:
for(表达式 1; 表达式 2; 表达式 3){ //循环体 } //程序实例 #include <iostream> #include <vector> #include <string.h> using namespace std; int main() { char arc[] = "www.123.com"; int i; //for循环遍历普通数组 for (i = 0; i < strlen(arc); i++) { cout << arc[i]; } cout << endl; vector<char>myvector(arc,arc+3); vector<char>::iterator iter; //for循环遍历 vector 容器 for (iter = myvector.begin(); iter != myvector.end(); ++iter) { cout << *iter; } return 0; } /* 程序运行结果: www.123.com www */
-
右值引用和move语义
-
右值引用
C++98/03 标准中就有引用,使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:
int num = 10; int &b = num; //正确 int &c = 10; //错误
如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。
注意,虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值,例如:
int num = 10; const int &b = num; const int &c = 10;
我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。
为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。
需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:
int num = 10; //int && a = num; //右值引用不能初始化为左值 int && a = 10;
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
int && a = 10; a = 100; cout << a << endl; /* 程序运行结果: 100 */
另外值得一提的是,C++ 语法上是支持定义常量右值引用的,例如:
const int&& a = 10;//编译器不会报错
但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。
-
move语义
move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。基于 move() 函数特殊的功能,其常用于实现移动语义。move() 函数的用法也很简单,其语法格式如下:
move( arg ) //其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。
//程序实例 #include <iostream> using namespace std; class first { public: first() :num(new int(0)) { cout << "construct!" << endl; } //移动构造函数 first(first &&d) :num(d.num) { d.num = NULL; cout << "first move construct!" << endl; } public: //这里应该是 private,使用 public 是为了更方便说明问题 int *num; }; class second { public: second() :fir() {} //用 first 类的移动构造函数初始化 fir second(second && sec) :fir(move(sec.fir)) { cout << "second move construct" << endl; } public: //这里也应该是 private,使用 public 是为了更方便说明问题 first fir; }; int main() { second oth; second oth2 = move(oth); //cout << *oth.fir.num << endl; //程序报运行时错误 return 0; } /* 程序运行结果: construct! first move construct! second move construct */
-
-
无序容器(哈希表)
用法和功能同map一模一样,区别在于哈希表的效率更高。
(1) 无序容器具有以下 2 个特点:
a. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
b. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。
(2) 和关联式容器一样,无序容器只是一类容器的统称,其包含有 4 个具体容器,分别为 unordered_map、unordered_multimap、unordered_set 以及 unordered_multiset。功能如下表:
无序容器 功能 unordered_map 存储键值对 <key, value> 类型的元素,其中各个键值对键的值不允许重复,且该容器中存储的键值对是无序的。 unordered_multimap 和 unordered_map 唯一的区别在于,该容器允许存储多个键相同的键值对。 unordered_set 不再以键值对的形式存储数据,而是直接存储数据元素本身(当然也可以理解为,该容器存储的全部都是键 key 和值 value 相等的键值对,正因为它们相等,因此只存储 value 即可)。另外,该容器存储的元素不能重复,且容器内部存储的元素也是无序的。 unordered_multiset 和 unordered_set 唯一的区别在于,该容器允许存储值相同的元素。 (3) 程序实例(以 unordered_map 容器为例)
#include <iostream> #include <string> #include <unordered_map> using namespace std; int main() { //创建并初始化一个 unordered_map 容器,其存储的 <string,string> 类型的键值对 std::unordered_map<std::string, std::string> my_uMap{ {"教程1","www.123.com"}, {"教程2","www.234.com"}, {"教程3","www.345.com"} }; //查找指定键对应的值,效率比关联式容器高 string str = my_uMap.at("C语言教程"); cout << "str = " << str << endl; //使用迭代器遍历哈希容器,效率不如关联式容器 for (auto iter = my_uMap.begin(); iter != my_uMap.end(); ++iter) { //pair 类型键值对分为 2 部分 cout << iter->first << " " << iter->second << endl; } return 0; } /* 程序运行结果: 教程1 www.123.com 教程2 www.234.com 教程3 www.345.com */
-
正则表达式
可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串。常用符号的意义如下:
符号 意义 ^ 匹配行的开头 $ 匹配行的结尾 . 匹配任意单个字符 […] 匹配[]中的任意一个字符 (…) 设定分组 \ 转义字符 \d 匹配数字[0-9] \D \d 取反 \w 匹配字母[a-z],数字,下划线 \W \w 取反 \s 匹配空格 \S \s 取反 + 前面的元素重复1次或多次 * 前面的元素重复任意次 ? 前面的元素重复0次或1次 {n} 前面的元素重复n次 {n,} 前面的元素重复至少n次 {n,m} 前面的元素重复至少n次,至多m次 | 逻辑或 -
Lambda匿名函数
所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。
Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。
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 */
二、智能指针
介绍一下RALL机制。
RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”(使用类来封装资源,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放”)。是C++的一种管理资源、避免泄漏的惯用法。,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。
so,什么是RALL机制?
利用C++对象生命周期的概念来控制程序的资源,确保资源能得到及时释放。
1.使用 C++ 时,最让人头疼的便是内存管理,但却又正是对内存高度的可操作性给了 C++ 程序猿极大的自由与装逼资本。
2.当我们 new 出一块内存空间,在使用完之后,如果不使用 delete 来释放这块资源则将导致内存泄露,这在中大型项目中是极具破坏性的。java是自动释放的。在c++中,我们需要做的便是将资源托管给某个对象,或者说这个对象是资源的代理,在这个对象析构的时候完成资源的释放。
使用RALL优点:(1)不需要显式地释放资源。(2)采用这种方式,对象所需的资源在其生命期内始终保持有效。
说说 C++ 中智能指针?(百度)
基于RALL机制智能指针auto_ptr,以帮助自动完成释放内存。
智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。
分别为:shared_ptr、unique_ptr、weak_ptr、auto_ptr,其中auto_ptr被C++11弃用。
因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以,智能指针的作用原理就是在函数结束时自动释放内存空间,避免了手动释放内存空间。
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露,将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。 它使用计数机制来表明资源被几个指针共享。use_count()来查看资源的所有者个数。交叉引用(循环引用):比如有两个类A和B,在A里面使用std::shared_ptr引用了B,在B里面使用std::shared_ptr引用了A,这样就成了循环引用,析构B的时候,会先将自己的成员析构掉,这个时候去析构A,但是在析构A的时候,又要将B析构掉,这样就成了一个循环了
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象,weak_ptr只是提供了对管理对象的一个访问手段,不会修改引用计数的值 。为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互交叉引用时的死锁,并造成内存泄漏问题,两个指针的引用计数永远不可能下降为0,资源永远不会释放。把其中一个改为weak_ptr就可以。
智能指针的实现类型:
实现主要分为两种,一种是侵入式,一种是非侵入式。比较常用的是非侵入式实现。
1、非侵入式智能指针(Non-Intrusive Smart Pointers): 非侵入式智能指针是不需要修改被管理类的接口,而是通过包装类来管理资源。被管理的类不需要了解智能指针的存在。非侵入式智能指针通常通过构造函数或者工厂函数接受一个原始指针,并在析构时释放资源。它的实现完全放在智能指针模版中,模版类有一个用于保存资源类对象的指针变量,和一个用于记录资源对象使用计数的指针变量,这两个东西都是所有智能指针对象共享的,所以通过指针保存。
class MyObject {
public:
// ...
};
// 非侵入式智能指针
template <typename T>
class SmartPtr {
public:
SmartPtr(T* ptr) : object(ptr) {}
~SmartPtr() { delete object; }
private:
T* object;
};
2、侵入式智能指针(Intrusive Smart Pointers): 侵入式智能指针是将智能指针的功能嵌入到被管理的类中。这意味着被管理的类需要修改其接口,以支持智能指针的功能。通常,在侵入式智能指针中,被管理的类会提供成员函数来增加引用计数、减少引用计数以及在引用计数达到零时进行资源释放。它的实现分散在智能指针模版和使用智能指针模版的类中,模版类只有一个用于保存对象的指针变量,对象的计数放在了资源类中。
class MyObject {
public:
void AddRef() { /* 增加引用计数 */ }
void Release() { /* 减少引用计数,释放资源 */ }
};
// 侵入式智能指针
class IntrusivePtr {
public:
IntrusivePtr(MyObject* ptr) : object(ptr) {
if (object) object->AddRef();
}
~IntrusivePtr() {
if (object) object->Release();
}
private:
MyObject* object;
};
相比非侵入式智能指针,侵入式的好处是:1.一个资源对象无论被多少个侵入式智能指针包含,从始至终只有一个引用计数变量,不需要在每一个使用智能指针对象的地方都new一个计数对象,这样子效率比较高,使用内存也比较少,也比较安全。2.因为引用计数存储在对象本身,所以在函数调用的时候可以直接传递资源对象地址,而不用担心引用计数值丢失(非侵入式智能指针对象的拷贝,必须带着智能指针模板,否则就会出现对象引用计数丢失)。
缺点:1.资源类必须有引用计数变量,并且该变量的增减可以被侵入式智能指针模板基类操作,这显得麻烦; 2.如果该类并不想使用智能指针,它还是会带着引用计数变量。
现在由于考虑到适配的缘故,一般使用的还是非侵入式的智能指针。
简述 C++ 中智能指针的特点
参考回答
-
C++中的智能指针有4种,分别为:shared_ptr、unique_ptr、weak_ptr、auto_ptr,其中auto_ptr被C++11弃用
-
为什么要使用智能指针:智能指针的作用是管理一个指针,因为存在申请的空间在函数结束时忘记释放,造成内存泄漏的情况。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,自动释放资源。
-
四种指针各自特性
(1)auto_ptr
auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,自然报错。(不适合共享所有权,析构函数不是虚函数,复制和赋值操作不直观)。
(2)unique_ptr
unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。
实现原理:将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,通过std:move把一个对象指针变成右值之后可以移动给另一个unique_ptr
(3)shared_ptr
共享指针可以实现多个智能指针指向相同对象,该对象和其相关资源会在引用为0时被销毁释放。
实现原理:有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值拷贝构造函数时,引用计数加1,当引用计数为0时,释放资源。
(4)weak_ptr
shared_ptr存在一个问题,当两个shared_ptr指针相互引用时,那么这两个指针的引用计数不会下降为0,资源得不到释放。因此引入weak_ptr,weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。
weak_ptr 能不能知道对象计数为 0,为什么?
参考回答
不能。
weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象管理的是那个引用的shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的只是为了配合shared_ptr而引入的一种智能指针,配合shared_ptr工作,它只可以从一个shared_ptr或者另一个weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。
weak_ptr 如何解决 shared_ptr 的循环引用问题?
参考回答
为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
share_ptr 怎么知道跟它共享对象的指针释放了
参考回答
多个shared_ptr对象可以同时托管一个指针,系统会维护一个托管计数。当无shared_ptr托管该指针时,delete该指针。
说说智能指针及其实现,shared_ptr 线程安全性,原理
参考回答
1. C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
-
为什么要使用智能指针
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
-
线程安全性
多线程环境下,调用不同shared_ptr实例的成员函数是不需要额外的同步手段的,即使这些shared_ptr拥有的是同样的对象。但是如果多线程访问(有写操作)同一个shared_ptr,则需要同步,否则就会有race condition 发生。也可以使用 shared_ptr overloads of atomic functions来防止race condition的发生。
多个线程同时读同一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。例子如下:
//程序实例 shared_ptr<long> global_instance = make_shared<long>(0); std::mutex g_i_mutex; void thread_fcn() { //std::lock_guard<std::mutex> lock(g_i_mutex); //shared_ptr<long> local = global_instance; for(int i = 0; i < 100000000; i++) { *global_instance = *global_instance + 1; //*local = *local + 1; } } int main(int argc, char** argv) { thread thread1(thread_fcn); thread thread2(thread_fcn); thread1.join(); thread2.join(); cout << "*global_instance is " << *global_instance << endl; return 0; }
在线程函数thread_fcn的for循环中,2个线程同时对global_instance进行加1的操作。这就是典型的非线程安全的场景,最后的结果是未定的,运行结果如下:
*global_instance is 197240539
如果使用的是每个线程的局部shared_ptr对象local,因为这些local指向相同的对象,因此结果也是未定的,运行结果如下: *global_instance is 160285803
因此,这种情况下必须加锁,将thread_fcn中的第一行代码的注释去掉之后,不管是使用global_instance,还是使用local,得到的结果都是:
*global_instance is 200000000
请你回答一下智能指针有没有内存泄露的情况
参考回答
智能指针有内存泄露的情况发生。
-
智能指针发生内存泄露的情况
当两个对象同时使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄露。
-
智能指针的内存泄漏如何解决? 为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
答案解析
//程序实例 #include <memory> #include <iostream> using namespace std; class Child; class Parent{ private: std::shared_ptr<Child> ChildPtr; public: void setChild(std::shared_ptr<Child> child) { this->ChildPtr = child; } void doSomething() { if (this->ChildPtr.use_count()) { } } ~Parent() { } }; class Child{ private: std::shared_ptr<Parent> ParentPtr; public: void setPartent(std::shared_ptr<Parent> parent) { this->ParentPtr = parent; } void doSomething() { if (this->ParentPtr.use_count()) { } } ~Child() { } }; int main() { std::weak_ptr<Parent> wpp; std::weak_ptr<Child> wpc; { std::shared_ptr<Parent> p(new Parent); std::shared_ptr<Child> c(new Child); p->setChild(c); c->setPartent(p); wpp = p; wpc = c; std::cout << p.use_count() << std::endl; std::cout << c.use_count() << std::endl; } std::cout << wpp.use_count() << std::endl; std::cout << wpc.use_count() << std::endl; return 0; } /* 程序运行结果: 2 2 1 1 */
上述代码中,parent有一个shared_ptr类型的成员指向孩子,而child也有一个shared_ptr类型的成员指向父亲。然后在创建孩子和父亲对象时也使用了智能指针c和p,随后将c和p分别又赋值给child的智能指针成员parent和parent的智能指针成员child。从而形成了一个循环引用。
三、右值
左值引用和右值引用的区别?左值引用可以直接引用右值吗?
左值引用不可以直接引用右值,要么加const,常引用。 要么&&T 右值引用,使右值可以绑定到右值引用的参数上。
右值引用的主要目的是为了实现转移语义和完美转发,消除两个对象交互时不必要的对象拷贝,也能够更加简洁明确地定义泛型函数。
在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。
左值:可以放在等号左边,也可放右边,可以取地址并有名字。有持久性;
右值:不可以放在等号左边,只能放右边,不能取地址,没有名字。表达式求值过程中创建的无名临时对象,短暂性的。
左值和右值主要的区别之一是左值可以被修改,而右值不能。
右值又分为纯右值和将亡值,其中纯右值:指的是临时变量和不跟对象关联的字面量值,可见立即数,函数返回的值等都是纯右值;将亡值:C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
左值引用:就是对一个左值进行引用的类型;左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。
右值引用:就是对一个右值进行引用的类型,C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用。
右值引用的主要目的是为了实现转移语义和完美转发,消除两个对象交互时不必要的对象拷贝,也能够更加简洁明确地定义泛型函数。
-
转移语义 :消除两个对象交互时不必要的对象拷贝
-
完美转发 :更加简洁明确地定义泛型函数。
等号两边必须是左值对应左引用,右值对应右引用。
int x = 100; // x是左值,100是右值 int &y = x; // 左值引用,y引用x int &l1 = x * 100; // 错误,x*100是一个右值 const int &l2 = x * 10; // 正确,可以将一个const引用绑定到一个右值,常引用,但无法修改其内容. int &&l3 = x * 10; // 正确,右值引用 int &&l4 = x; // 错误,x是一个左值 int &&l5 = std::move(x); //正确,std::move()将左值强制转换为右值
带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。
移动语义:转移所有权,对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为⾃⼰所拥有,别⼈不再拥有也不会再使⽤。
浅复制:a和b的指针指向了同⼀块内存,就是浅拷⻉,只是数据的简单赋值;
深复制:深拷⻉就是在拷⻉对象时,如果被拷⻉对象内部还有指针引⽤指向其它资源,⾃⼰需要新开辟⼀块新内存存储资源。
补充:万能引用和完美转发
1.万能引用:只有两种形式的引用:左值引用和右值引用,万能引用不是一种引用类型,而是代表要么是左值引用要么是右值引用。
(1)万能引用用在需要推断类型的场合,即以下两种场合:
(a)用在模板
(b)auto推断类型
形式(T&&)万能引用虽然跟右值引用的形式一样,但右值引用需要是确定的类型,如: int && ref = x;
(2)万能引用能够接收左值或右值,返回左值引用或右值引用
2.完美转发
使用场景:通过函数模板调用另外一个函数,如:
template<typename F,typename T, typename U> void tempFun(F f, T && t1, U && t2){ f(t1, t2); }
我们已经知道模板中使用万能引用是有益的,这样既能接收左值也能接收右值。但对于函数内部来说不管接收的是左值还是右值,模板函数内部对于形参都是左值(T && t1=var, t1本身是左值)。
此时如果f函数的第一个参数需要右值,我们必须这样调用:f(std::move(t1), t2);但模板是通用的,我们不能直接用std::move()写死,这样就不能调用接收左值的函数了。
c++标准提供std::forward<>模板类来保持参数的原有类型。这样传过来的参数t1、t2的类型被直接转发到函数f()中去,称为完美转发。
匿名函数Lanbda
lambda表达式,可以引入外部变量吗? (bind和lambda区别)
lambda表达式是一个匿名函数-即没有函数名的函数。表示⼀个可调⽤的代码单元,没有命名的内联函数,不需要函数名因为我们直接(⼀次性的)⽤它,不需要其他地⽅调⽤它。
lambda最⼤的⼀个优势是在使⽤STL中的算法(algorithms)库 。
例如:数组排序
sort(myvec.begin(), myvec.end(), cmp); // 用谓词函数做函数对象 也可用仿函数做函数对象 sort(arr.begin(), arr.end(), [](const int& a, const int& b) {return a > b}); //降序排列 sort(arr.begin(), arr.end(), [](const auto& a, const auto& b) {return a > b}); //降序排列,不依赖a和b的具体类型
Lambda表达式可以使用其可见范围内的外部变量,但必须明确声明(明确声明哪些外部变量可以被该Lambda表达式使用)。那么,在哪里指定这些外部变量呢?Lambda表达式通过在最前面的方括号[]来明确指明其内部可以访问的外部变量,捕捉列表只能捕捉当前作用域的局部变量,作用域以外的局部变量或者非局部变量都会报错。这一过程也称过Lambda表达式“捕获”了外部变量。外部变量的捕获方式有以下几种:
(1)值捕获 (2)引用捕获 (3)隐式捕获 (4)混合捕获
仿函数(函数对象)的使用和lambda表达式差不多。仿函数(Functor)又称为函数对象(Function Object)是一个能行使函数功能的类。
lambda表达式原理:实际编译器在全局作用域自动生成了一个类,在类中重载了仿函数, 仿函数的内容就是lambda表达式的内容。可以理解成lambda表达式底层还是仿函数。本来是要程序员编写,现在变成了编译器自动生成,我们看起来更方便了。从广义上说,lamdba表达式产生的是函数对象。
补充:std::bind机制:
bind机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体。
C++98中,有两个函数bind1st和bind2nd,它们分别可以用来绑定functor的第 一个和第二个参数,它们都是只可以绑定一个参数。各种限制,使得bind1st和bind2nd的可用性大大降低。
vector<int> coll = {1,2,3,4,8,9,7,10,11,54,23,12}; // 查找元素值大于10的元素的个数 // 也就是使得10 < elem成立的元素个数 //当使用bind1st的时候,表示绑定了left参数,即left参数不变了,而right参数就是对应容器中的element; int res = count_if(coll.begin(), coll.end(), bind1st(less<int>(), 10)); cout << res << endl; // 查找元素值小于10的元素的个数 // 也就是使得elem < 10成立的元素个数 //当使用bind2nd的时候,表示绑定了right参数,即right参数不变了,而left参数就是对应容器中的element。 res = count_if(coll.begin(), coll.end(), bind2nd(less<int>(), 10)); //less<int>()是一个仿函数 less<int> functor = less<int>(); bool bRet = functor(10, 20); // 返回true
简单的认为就是std::bind
就是std::bind1st
和std::bind2nd
的加强版。绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制。
int TestFunc(int a, char c, float f){ cout << a << endl; cout << c << endl; cout << f << endl; } int main(){ auto bindFunc1 = bind(TestFunc, std::placeholders::_1, 'A', 100.1); bindFunc1(10); cout << "=================================\n"; auto bindFunc2 = bind(TestFunc, std::placeholders::_2, std::placeholders::_1, 100.1); bindFunc2('B', 10); cout << "=================================\n"; auto bindFunc3 = bind(TestFunc, std::placeholders::_3, std::placeholders::_1, std::placeholders::_2); bindFunc3(100.1, 30, 'C'); using namespace std::placeholders; //可将std::placeholders::_2直接用_2;
std::placeholders是一个占位符。当使用bind生成一个新的可调用对象时,std::placeholders表示新的可调用对象的第 几个参数和原函数的第几个参数进行匹配。
1.bind预先绑定的参数需要传具体的变量或值进去,对于预先绑定的参数,是pass-by-value的;
2.对于不事先绑定的参数,需要传placeholders进去,从_1开始依次递增。placeholder是pass-by-reference的;
3.bind的返回值是可调用实体,可以直接赋给std::function对象;
使用std::bind
生成一个可调用对象,这个对象可以直接赋值给std::function
对象;在类中有一个std::function
的变量,这个std::function
由std::bind
来赋值,而std::bind
绑定的可调用对象可以是Lambda表达式或者类成员函数等可调用对象
lambda表达式和bind机制的区别:
bind() 和 lambda 表达式都可以实现类似的功能。
1.lambda 运行速度会比bind() 函数快很多
2.对于用bind来生成function和用lambda表达式来生成function, 通常情况下两种都是ok的,但是在参数多的时候,bind要传入很多的std::placeholders,而且看着没有lambda表达式直观,所以通常建议优先考虑使用lambda表达式。
-
lambda 不支持“多态性”(其实就是泛型),需要在定义的时候指定参数类型, lambda 可以通过 std::function 实现“多态”特性
其他
C++20 新特性 — 模块、概念、范围、协程。
概念:使用模板进行通用编程的关键思想是定义能通过各种类型(type)使用的函数和类。但是,在实例化模板时经常会出现用错类型的问题。概念让你能为模板编写要求,而编译器则可以检查这个要求。概念的要求是接口的一部分。
协程:协程是广义的函数,能在保持状态的同时暂停或继续。协程通常用来编写事件驱动型应用。事件驱动型应用可以是模拟、游戏、服务器、用户接口或算法。协程也通常被用于协作式多任务
模块:模块承诺能够实现:更快的编译时间,表达代码的逻辑结构,不必再使用头文件等.
std::function可以封装那些实体?可以封装函数对象吗?(可)
std::function是一个函数包装模板 (类模板),是一种通用、多态的函数封装,可以包装下列这几种可调用元素类型:函数、指针、函数指针、类成员函数,lambda表达式、函数对象(仿函数)。我们可以使用std::function将不同类型的可调用对象共享同一种调用形式。 即统一了可调用对象的各种操作,可调用对象虽然类型不同,但是共享了一种调用形式:
#include <functional> function<bool(int, int)> fun; //声明一个function类型,接受两个int,返回bool bool compare_com(int a, int b){ //普通函数 return a > b; } auto compare_lambda = [](int a, int b) { return a > b; }; //lambda表达式 class compare_class{ public: bool operator()(int a, int b) //仿函数,函数对象 { return a > b; } }; class Test{ public: int i = 0; void func(int x, int y) { //类的成员函数 cout << x << " " << y << endl; } }; int main(){ fun = compare_com; cout << fun(1, 2) << endl; //结果 0 fun = compare_lambda; cout << fun(2, 1) << endl; //结果 1 fun = compare_class(); cout << fun(3, 1) << endl; //结果 1 Test obj; //创建对象 //绑定成员函数多了一个成员变量即对象 故需要用bind进行绑定 function<void(int, int)> f1 = bind(&Test::func, &obj, _1, _2); f1(1,2); //输出1,2 }
std::function
可以绑定全局函数,静态函数,但是绑定类的成员函数时,要借助std::bind
的帮忙。
简述一下 C++11 中四种类型转换
隐式类型转换是安全的,显示类型转换是有风险的。
-
const_cast<类型说明符>(表达式) P246
const_cast可以用来将数据类型中的const属性去除。它可以将常指针转换成普通指针,将常引用转换成普通引用。 但是不能将常对象转换为普通对象,因为这是没有意义的。(相对不安全,很容易被滥用)。
例如: const int* p; //指向常量的指针,不能通过指针来改变所指对象的值
int* p = const_cast<int*> (cp); //转换
(*p)++; //可以改变指向的值。
-
dynamic_cast<类型说明符>(表达式) P337 重要
本质:C++中,从特殊指针转换到一般指针是安全的,因此允许隐含转化;从一般指针转换到特殊指针是不安全的,只能显示转化。
派生类指针可以隐含的转换为基类指针,之所以允许这种转换隐含发生,是因为它是安全的。而基类指针想要转换为派生类指针,转换一定要显示进行。
用dynamic_cast 执行基类向派生类的转换 可以用其将基类的指针(引用),显示转换为派生类的指针(引用)。 它在转换前会检查指针(或引用)所指向对象的实际类型是否与转换的目的类型兼容,如果兼容转换才会发生,才能得到派生类的指针(或引用),否则:
(1)如果执行的是指针转换,会得到空指针。
(2)如果执行的是引用类型转换,会抛出异常。
补充: 为什么要将基类指针显示转换为派生类指针?
基类指针可以指向派生类的对象,通过这样的指针,可以利用多态性来执行派生类提供的功能,但这仅限于调用基类中声明的虚函数。如果想对派生类对象调用派生类中引入的新函数,则无法通过基类指针解决。——解决办法-用dynamic_cast动态(运行时类型识别机制)的将基类指针显式转换为派生类指针。
-
reinterpret_cast<类型说明符>(表达式) P244
reinterpret 是“重新解释”的意思,顾名思义,reinterpret_cast 可以允许将任意指针转换到其他指针类型,也允许做任意整数类型和任意指针类型之间的转换。转换时,执行的是逐个比特复制的操作。这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,所以风险很高。 它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。
例如:两个具体不同类型指针之间的转换。
int i = 2;
float* p = reinterpret_cast<float*> (&i);
本质:把&i得到的地址直接作为转换结果,并为这一结果赋予float*类型。
-
static_cast<类型说明符>(表达式)
static_cast用于良性转换(用于基本数据类型之间的转换、子类向父类的安全转换、
void*
和其他类型指针之间的转换),一般不会导致意外发生,风险很低,基于内容的数据类型转换。例如 short 转 int、int 转 double、const 转非 const。不能用于无关类型之间的转换,因为这些转换都是有风险的double z = 95.5;
int n = static_cast<int> (z);
简述一下 C++ 11 中 auto 的具体用法
参考回答
auto用于定义变量,编译器可以自动判断变量的类型。auto主要有以下几种用法:
-
auto的基本使用方法
(1)基本使用语法如下
auto name = value; //name 是变量的名字,value 是变量的初始值
注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
(2)程序实例如下
auto n = 10; auto f = 12.8; auto p = &n; auto url = "www.123.com";
a. 第 1 行中,10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
b. 第 2 行中,12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
c. 第 3 行中,&n 的结果是一个 int* 类型的指针,所以推导出变量 f 的类型是 int*。
d. 第 4 行中,由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const char*,也即一个常量指针。
-
auto和 const 的结合使用
(1) auto 与 const 结合的用法
a. 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
b. 当类型为引用时,auto 的推导结果将保留表达式的 const 属性。
(2)程序实例如下
int x = 0; const auto n = x; //n 为 const int ,auto 被推导为 int auto f = n; //f 为 const int,auto 被推导为 int(const 属性被抛弃) const auto &r1 = x; //r1 为 const int& 类型,auto 被推导为 int auto &r2 = r1; //r1 为 const int& 类型,auto 被推导为 const int 类型
a. 第 2 行代码中,n 为 const int,auto 被推导为 int。
b. 第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时,auto 不会 使用 const 属性,而是直接推导出 non-const 类型。
c. 第 4 行代码中,auto 被推导为 int 类型,这个很容易理解,不再赘述。
d. 第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结合时,auto 的推导将保留 表达式的 const 类型。
-
使用auto定义迭代器
在使用 stl 容器的时候,需要使用迭代器来遍历容器里面的元素;不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,请看下面的例子:
#include <vector> using namespace std; int main(){ vector< vector<int> > v; //vector< vector<int> >::iterator i = v.begin(); auto i = v.begin(); //使用 auto 代替具体的类型,该句比上一句简洁,根据表达式 v.begin() 的类型(begin() 函数的返回值类型)来推导出变量i的类型 return 0; }
-
用于泛型编程
auto 的另一个应用就是当我们不知道变量是什么类型,或者不希望指明具体类型的时候,比如泛型编程中。请看下面例子:
#include <iostream> using namespace std; class A{ public: static int get(void){ return 100; } }; class B{ public: static const char* get(void){ return "www.123.com"; } }; template <typename T> void func(void){ auto val = T::get(); cout << val << endl; } int main(void){ func<A>(); func<B>(); return 0; } /* 运行结果: 100 www.123.com */
本例中的模板函数 func() 会调用所有类的静态函数 get(),并对它的返回值做统一处理,但是 get() 的返回值类型并不一样,而且不能自动转换。这种要求在以前的 C++ 版本中实现起来非常的麻烦,需要额外增加一个模板参数,并在调用时手动给该模板参数赋值,用以指明变量 val 的类型。但是有了 auto 类型自动推导,编译器就根据 get() 的返回值自己推导出 val 变量的类型,就不用再增加一个模板参数了。
简述一下 C++11 中的可变参数模板新特性
参考回答
可变参数模板(variadic template)使得编程者能够创建这样的模板函数和模板类,即可接受可变数量的参数。例如要编写一个函数,它可接受任意数量的参数,参数的类型只需是cout能显示的即可,并将参数显示为用逗号分隔的列表。
int n = 14; double x = 2.71828; std::string mr = "Mr.String objects!"; show_list(n, x); show_list(x*x, '!', 7, mr); //这里的目标是定义show_list() /* 运行结果: 14, 2.71828 7.38905, !, 7, Mr.String objects! */
要创建可变参数模板,需要理解几个要点:
(1)模板参数包(parameter pack);
(2)函数参数包;
(3)展开(unpack)参数包;
(4)递归。