C++11 的一些新特性
用过什么 C++ 11 的语法特性
auto & decltype
C++ 11引入 auto
和 decltype
关键字,使得他们可以在编译器就推导出变量或者表达式的类型,方便开发者编码也简化了代码
auto
: 让编译器在编译期就推导出变量的类型,可以通过 =
右边的类型推导出变量的类型
std::map<int,int> hash;
auto a = 10;
auto iter = hash.begin();
auto的限制:
-
auto的使用必须马上初始化,否则无法推导出类型
-
auto在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败
-
auto不能用作函数参数
-
在类中auto不能用作非静态成员变量
-
auto不能定义数组,可以定义指针
-
auto无法推导出模板参数
auto推导规则
-
在不声明为引用或指针时,auto会忽略等号右边的引用类型和cv限定 (const,volatile)
-
在声明为引用或者指针时,auto会保留等号右边的引用和cv属性
decltyoe
: 相对于 auto
用于推到变量类型,decltype
则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会运算
const int& i = 1;
int a = 2;
decltype(i) b = 2; // b 是 const int&
int func() { return 0; }
decltype(func()) i; // i为int类型
对于decltype(exp)有
-
exp是表达式,decltype(exp)和exp类型相同
-
exp是函数调用,decltype(exp)和函数返回值类型相同
-
其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
更详细的(包括auto和decltype的限制)请看:一文吃透C++11中auto和decltype知识点
移动语义和完美转发
-
左值 : 可以放到等号左边的东西叫左值 / 可以取地址并且有名字的东西就是左值
-
右值 :不可以放到等号左边的东西就叫右值 / 不能取地址的没有名字的东西就是右值
-
左值引用 : 对左值进行引用的类型 int&
-
右值引用 :对右值进行引用的类型 int&&
-
std::move
由深拷贝与浅拷贝引出,移动语义,可以理解为转移所有权,之前深拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,前提是别人不再使用,通过C++11新增的移动语义可以省区很多拷贝负担,怎么利用移动语义呢,就是通过移动构造函数:
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
A(const A& a) { // 拷贝构造函数
size_ = a.size_;
data_ = new int[size_];
cout << "copy " << endl;
}
A(A&& a) { // 移动构造函数
this->data_ = a.data_;
a.data_ = nullptr;
cout << "move " << endl;
}
~A() {
if (data_ != nullptr) {
delete[] data_;
}
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
A c = std::move(a); // 调用移动构造函数
return 0;
}
如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的STL都实现了移动语义,方便我们使用。例如:
// 来自cppreference
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
// 使用 push_back(const T&) 重载,
// 表示我们将带来复制 str 的成本
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
// 使用右值引用 push_back(T&&) 重载,
// 表示不复制字符串;而是
// str 的内容被移动进 vector
// 这个开销比较低,但也意味着 str 现在可能为空。
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"
- 完美转发
std::forward
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward()。
void PrintV(int &t) {
cout << "lvalue" << endl;
}
void PrintV(int &&t) {
cout << "rvalue" << endl;
}
template<typename T>
void Test(T &&t) {
PrintV(t); // 不管我们传入的参数类型是什么,在void Test(T&& t)函数的内部,t都是一个左值引用!
PrintV(std::forward<T>(t));
PrintV(std::move(t));
}
int main() {
Test(1); // lvalue rvalue rvalue
int a = 1;
Test(a); // lvalue lvalue rvalue
Test(std::forward<int>(a)); // lvalue rvalue rvalue
Test(std::forward<int&>(a)); // lvalue lvalue rvalue
Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
return 0;
}
模板中的 T 保存着传递进来的实参的信息,我们可以利用 T 的信息来强制类型转换我们的 param 使它和实参的类型一致。那么,std::forward是怎么利用到 T 的信息的呢。std::forward的源码形式大致是这样:
/*
* 精简了标准库的代码,在细节上可能不完全正确,但是足以让我们了解转发函数 forward 的了
*/
template<typename T>
T&& forward(T ¶m)
{
return static_cast<T&&>(param);
}
我们来仔细分析一下这段代码:
我们可以看到,不管T是值类型,还是左值引用,还是右值引用,T&经过引用折叠,都将是左值引用类型。也就是forward 以左值引用的形式接收参数 param, 然后 通过将param进行强制类型转换 static_cast<T&&> (),最终再以一个 T&&返回
列表初始化
在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。列表初始化也可以用在函数的返回值上;
// 列表初始化也可以用在函数的返回值上
std::vector<int> func() {
return {};
}
// 直接在变量名后面加上初始化列表来进行对象的初始化
struct A {
public:
A(int) {}
private:
A(const A&) {} // 拷贝构造函数
};
int main() {
A a(123);
A b = 123; // error
A c = { 123 };
A d{123}; // c++11
int e = {123};
int f{123}; // c++11
return 0;
}
我们平时开发使用STL过程中可能发现它的初始化列表可以是任意长度,大家有没有想过它是怎么实现的呢,答案是std::initializer_list
,看下面这段示例代码:
struct CustomVec {
std::vector<int> data;
CustomVec(std::initializer_list<int> list) {
for (auto iter = list.begin(); iter != list.end(); ++iter) {
data.push_back(*iter);
}
}
};
我想通过上面这段代码大家可能已经知道STL是如何实现的任意长度初始化了吧,这个std::initializer_list其实也可以作为函数参数。
注意:
std::initializer_list<T>
,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可以转换为T。
std::function & std::bind & lambda表达式
std::function
实验室基类线程池中用到:typedef std::tr1::function<int(ThreadPoolWorkItem*)> POSTREQFUNC;
tr1::bind(&ThreadPool::postRequest,g_pThreadPool,tr1::placeholders::_1)
讲std::function前首先需要了解下什么是可调用对象
满足以下条件之一就可称为可调用对象:
-
是一个函数指针
-
是一个具有operator()成员函数的类对象(传说中的仿函数),lambda表达式
-
是一个可被转换为函数指针的类对象
-
是一个类成员(函数)指针
-
bind表达式或其它函数对象
而std::function就是上面这种可调用对象的封装器,可以把std::function看做一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。
使用参考如下实例代码:
std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#include <functional>
#include <iostream>
struct Foo {
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_ + i << '\n'; }
int num_;
};
void print_num(int i) { std::cout << i << '\n'; }
struct PrintNum {
void operator()(int i) const { std::cout << i << '\n'; }
};
int main() {
// 存储自由函数
std::function<void(int)> f_display = print_num;
f_display(-9);
// 存储 lambda
std::function<void()> f_display_42 = []() { print_num(42); };
f_display_42();
// 存储到 std::bind 调用的结果
std::function<void()> f_display_31337 = std::bind(print_num, 31337);
f_display_31337();
// 存储到成员函数的调用,非静态成员函数有一个隐形参数this
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
const Foo foo(314159);
f_add_display(foo, 1);
f_add_display(314159, 1);
// 存储到数据成员访问器的调用
std::function<int(Foo const&)> f_num = &Foo::num_;
std::cout << "num_: " << f_num(foo) << '\n';
// 存储到成员函数及对象的调用
using std::placeholders::_1;
std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
f_add_display2(2);
// 存储到成员函数和对象指针的调用
std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
f_add_display3(3);
// 存储到函数对象的调用
std::function<void(int)> f_display_obj = PrintNum();
f_display_obj(18);
}
std::bind
使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。
std::bind通常有两大作用:
-
将可调用对象与参数一起绑定为另一个std::function供调用
-
将n元可调用对象转成m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders
具体示例:
#include <functional>
#include <iostream>
#include <memory>
void f(int n1, int n2, int n3, const int& n4, int n5) {
std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << std::endl;
}
int g(int n1) { return n1; }
struct Foo {
void print_sum(int n1, int n2) { std::cout << n1 + n2 << std::endl; }
int data = 10;
};
int main() {
using namespace std::placeholders; // 针对 _1, _2, _3...
// 演示参数重排序和按引用传递
int n = 7;
// ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
n = 10;
f1(1, 2, 1001); // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
// 进行到 f(2, 42, 1, n, 7) 的调用
// 嵌套 bind 子表达式共享占位符
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12); // 进行到 f(12, g(12), 12, 4, 5); 的调用
// 绑定指向成员函数指针
Foo foo;
auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
f3(5);
// 绑定指向数据成员指针
auto f4 = std::bind(&Foo::data, _1);
std::cout << f4(foo) << std::endl;
// 智能指针亦能用于调用被引用对象的成员
std::cout << f4(std::make_shared<Foo>(foo)) << std::endl;
}
lambda
lambda表达式可以说是c++11引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:
auto func = [capture] (params) opt -> ret { func_body; };
其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。
一个完整的lambda表达式:
auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;
如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。
lambda表达式允许捕获一定范围内的变量:
-
[]不捕获任何变量
-
[&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用
-
[=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用
-
[=, &a]值捕获外部作用域所有变量,按引用捕获a变量
-
[a]只值捕获a变量(只读),不捕获其它变量
-
[this]捕获当前类中的this指针
lambda表达式示例代码:
int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;
auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };
代码中的f2是编译不过的,因为我们修改了按值捕获的外部变量,其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。
还可以使用lambda表达式自定义stl的规则,例如自定义sort排序规则:
struct A {
int a;
int b;
};
int main() {
vector<A> vec;
std::sort(vec.begin(), vec.end(), [](const A &left, const A &right) { return left.a < right.a; });
}
总结
std::function和std::bind使得我们平时编程过程中封装函数更加的方便,而lambda表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义STL规则时候也非常方便,让代码更简洁,更灵活,提高开发效率。
并发
std::thread
std::mutex
std::lock -- std::lock_guard & std::unique_lock
-
std::lock通过RAII技术方便了加锁和解锁调用,有std::lock_guard和std::unique_lock。
-
std::lock_gurad相比于std::unique_lock更加轻量级,少了一些成员函数,std::unique_lock类有unlock函数,可以手动释放锁,所以条件变量都配合std::unique_lock使用,而不是std::lock_guard,因为条件变量在wait时需要有手动释放锁的能力,具体关于条件变量后面会讲到。
std::atomic
std::call_once
std::async
#include <functional>
#include <future>
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
int func(int in) { sleep(2); return in + 1; }
int main() {
// std::launch::async 它保证了异步行为,即传递的函数将在单独的线程中执行。
auto res = std::async(std::launch::async,func, 5); // 第一个是启动策略,第二个是回调函数,第三个是函数的参数
cout << "now" << endl;
// res.wait();
cout << res.get() << endl; // 阻塞直到函数返回
return 0;
}
// now
// 6
智能指针
std::unique_ptr
std::unique_ptr
对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是1,std::unique_ptr
对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个std::unique_ptr
对象:
// 1.
std::unique_ptr<int> sp1(new int(123));
// 2.
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
// 3.
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。
std::unique_ptr
禁止复制语义,但存在特例,即可以通过一个函数返回一个std::unique_ptr
:
#include <memory>
std::unique_ptr<int> func(int val)
{
std::unique_ptr<int> up(new int(val));
return up;
}
int main()
{
std::unique_ptr<int> sp1 = func(123);
return 0;
}
既然 std::unique_ptr
不能复制,那么如何将一个 std::unique_ptr
对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:
// 代码利用 std::move 将 sp1 持有的堆内存(值为 123)转移给 sp2,再把 sp2 转移给 sp3。
// 最后,sp1 和 sp2 不再持有堆内存的引用,变成一个空的智能指针对象。
#include <memory>
int main()
{
std::unique_ptr<int> sp1(std::make_unique<int>(123));
std::unique_ptr<int> sp2(std::move(sp1));
std::unique_ptr<int> sp3;
sp3 = std::move(sp2);
return 0;
}
并不是所有的对象的 std::move
操作都有意义,只有实现了移动构造函数(Move Constructor)或移动赋值运算符(operator =)的类才行,而 std::unique_ptr
正好实现了这二者,以下是实现伪码:
template<typename T, typename Deletor>
class unique_ptr
{
//其他函数省略...
public:
unique_ptr(unique_ptr&& rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
}
unique_ptr& operator=(unique_ptr&& rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
return *this;
}
private:
T* m_pT;
};
std::unique_ptr
不仅可以持有一个堆对象,也可以持有一组堆对象,示例如下:
#include <iostream>
#include <memory>
int main()
{
//创建10个int类型的堆对象
//形式1
std::unique_ptr<int[]> sp1(new int[10]);
//形式2
std::unique_ptr<int[]> sp2;
sp2.reset(new int[10]);
//形式3
std::unique_ptr<int[]> sp3(std::make_unique<int[]>(10));
for (int i = 0; i < 10; ++i)
{
sp1[i] = i;
sp2[i] = i;
sp3[i] = i;
}
for (int i = 0; i < 10; ++i)
{
std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;
}
return 0;
}
自定义智能指针对象持有的资源的释放函数
默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr 为例:
#include <iostream>
#include <memory>
class Socket
{
public:
Socket()
{
}
~Socket()
{
}
//关闭资源句柄
void close()
{
}
};
int main()
{
auto deletor = [](Socket* pSocket) {
//关闭句柄
pSocket->close();
//TODO: 你甚至可以在这里打印一行日志...
delete pSocket;
};
std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);
return 0;
}
自定义 std::unique_ptr
的资源释放函数其规则是:std::unique_ptr<T, DeletorFuncPtr>
- 其中 T 是你要释放的对象类型
- DeletorFuncPtr 是一个自定义函数指针
上述代码表示 DeletorFuncPtr 有点复杂,我们可以使用 decltype(deletor) 让编译器自己推导 deletor 的类型,因此可以代码修改为:
std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);
std::shared_ptr
std::unique_ptr
对其持有的资源具有独占性,而std::shared_ptr
持有的资源可以在多个std::shared_ptr
之间共享- 每多一个
std::shared_ptr
对资源的引用,资源引用计数将增加 1,每一个指向该资源的std::shared_ptr
对象析构时,资源引用计数减 1,最后一个std::shared_ptr
对象析构时,发现资源计数为 0,将释放其持有的资源。 - 多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作
std::shared_ptr
引用的对象是安全的)。 std::shared_ptr
提供了一个use_count()
方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr
用法和std::unique_ptr
基本相同。- 和
std::unique_ptr
一样,你应该优先使用std::make_shared
去初始化一个std::shared_ptr
对象。
因为使用new的方式创建
shared_ptr
会导致出现两次内存申请,而std::make_shared
在内部实现时只会申请一个内存。因此建议后续均使用std::make_shared
。
关于shared_ptr有几点需要注意:
• 不要用一个裸指针初始化多个shared_ptr,会出现double_free导致程序崩溃
• 通过shared_from_this()返回this指针,不要把this指针作为shared_ptr返回出来,因为this指针本质就是裸指针,通过this返回可能 会导致重复析构,不能把this指针交给智能指针管理。
class A : public std::enable_shared_from_this<A>
{
shared_ptr<A> GetSelf() {
return shared_from_this();
// return shared_ptr<A>(this); 错误,会导致double free
}
};
std::enable_shared_from_this 用起来比较方便,但是也存在很多不易察觉的陷阱。
陷阱一:不应该共享栈对象的 this 给智能指针对象
陷阱二:避免 std::enable_shared_from_this 的循环引用问题
https://zhuanlan.zhihu.com/p/405432878
-
尽量使用make_shared,少用new。
-
不要delete get()返回来的裸指针。
-
不是new出来的空间要自定义删除器。
-
要避免循环引用,循环引用导致内存永远不会被释放,造成内存泄漏。
#include <iostream>
#include <memory>
using namespace std;
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() {
cout << "A delete" << endl;
}
};
struct B {
std::shared_ptr<A> aptr;
~B() {
cout << "B delete" << endl;
}
};
int main() {
auto aaptr = std::make_shared<A>();
auto bbptr = std::make_shared<B>();
cout << aaptr.use_count() << endl; // 1
aaptr->bptr = bbptr;
bbptr->aptr = aaptr;
cout << aaptr.use_count() << endl; // 2
return 0;
}
root@MY:~/cppLearn# ./smart_ptr
1
2
// 没有执行析构函数
上面代码,产生了循环引用,导致aptr和bptr的引用计数为2,离开作用域后aptr和bptr的引用计数-1,但是永远不会为0,导致指针永远不会析构,产生了内存泄漏,如何解决这种问题呢,答案是使用weak_ptr
。
std::weak_ptr
weak_ptr
是用来监视shared_ptr
的生命周期,它不管理shared_ptr
内部的指针,它的拷贝的析构都不会影响引用计数,纯粹是作为一个旁观者监视shared_ptr
中管理的资源是否存在,可以用来返回this指针和解决循环引用问题。
std::weak_ptr 可以从一个 std::shared_ptr 或另一个 std::weak_ptr 对象构造,std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptr 的 lock() 函数来获得 std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题(即两个std::shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。
作用1:返回this指针,上面介绍的shared_from_this()其实就是通过weak_ptr返回的this指针
作用2:解决循环引用问题。
#include <iostream>
#include <memory>
using namespace std;
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() {
cout << "A delete" << endl;
}
void Print() {
cout << "A" << endl;
}
};
struct B {
std::weak_ptr<A> aptr; // 这里改成weak_ptr
~B() {
cout << "B delete" << endl;
}
void PrintA() {
if (!aptr.expired()) { // 监视shared_ptr的生命周期
auto ptr = aptr.lock();
ptr->Print();
}
}
};
int main() {
auto bbptr = std::make_shared<B>();
{
auto aaptr = std::make_shared<A>();
cout << aaptr.use_count() << endl;
cout << bbptr.use_count() << endl;
aaptr->bptr = bbptr;
bbptr->aptr = aaptr;
cout << aaptr.use_count() << endl;
cout << bbptr.use_count() << endl;
bbptr->PrintA();
}
cout << bbptr.use_count() << endl; // 由于A的析构,A中的std::shared_ptr<B> bptr生命周期结束,故B的shared_ptr引用计数-1
return 0;
}
root@MY:~/cppLearn# ./smart_ptr
1
1
1
2
A
A delete
1
B delete
基于范围的for循环
vector<int> vec;
for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11
cout << *iter << endl;
}
for (int i : vec) { // c++11基于范围的for循环
cout << "i" << endl;
}
委托构造函数 与 继承构造函数
- 委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作
- 继承构造函数可以让派生类直接使用基类的构造函数,如果有一个派生类,我们希望派生类采用和基类一样的构造方式,可以直接使用基类的构造函数,而不是再重新写一遍构造函数;只需要使用using Base::Base继承构造函数,就免去了很多重写代码的麻烦。
// 不使用委托构造函数
struct A {
A(){}
A(int a) { a_ = a; }
A(int a, int b) { // 好麻烦
a_ = a;
b_ = b;
}
A(int a, int b, int c) { // 好麻烦
a_ = a;
b_ = b;
c_ = c;
}
int a_;
int b_;
int c_;
};
// 使用委托构造函数
struct A {
A(){}
A(int a) { a_ = a; }
A(int a, int b) : A(a) { b_ = b; }
A(int a, int b, int c) : A(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
// 不适用继承构造函数
struct Base {
Base() {}
Base(int a) { a_ = a; }
Base(int a, int b) : Base(a) { b_ = b; }
Base(int a, int b, int c) : Base(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
struct Derived : Base {
Derived() {}
Derived(int a) : Base(a) {} // 好麻烦
Derived(int a, int b) : Base(a, b) {} // 好麻烦
Derived(int a, int b, int c) : Base(a, b, c) {} // 好麻烦
};
int main() {
Derived a(1, 2, 3);
return 0;
}
// 使用继承构造函数
struct Base {
Base() {}
Base(int a) { a_ = a; }
Base(int a, int b) : Base(a) { b_ = b; }
Base(int a, int b, int c) : Base(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
struct Derived : Base {
using Base::Base;
};
int main() {
Derived a(1, 2, 3);
return 0;
}
nullptr
nullptr是c++11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针。举例:
void func(void *ptr) {
cout << "func ptr" << endl;
}
void func(int i) {
cout << "func i" << endl;
}
int main() {
func(NULL); // 编译失败,会产生二义性
func(nullptr); // 输出func ptr
return 0;
}
delete
c++中,如果开发人员没有定义特殊成员函数,那么编译器在需要特殊成员函数时候会隐式自动生成一个默认的特殊成员函数,例如拷贝构造函数或者拷贝赋值操作符,如下代码:
struct A {
A() = default;
int a;
A(int i) { a = i; }
};
int main() {
A a1;
A a2 = a1; // 正确,调用编译器隐式生成的默认拷贝构造函数
A a3;
a3 = a1; // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}
而我们有时候想禁止对象的拷贝与赋值,可以使用delete
修饰,如下:
struct A {
A() = default;
A(const A&) = delete;
A& operator=(const A&) = delete;
int a;
A(int i) { a = i; }
};
int main() {
A a1;
A a2 = a1; // 错误,拷贝构造函数被禁用
A a3;
a3 = a1; // 错误,拷贝赋值操作符被禁用
}
delele函数在c++11中很常用,std::unique_ptr
就是通过delete修饰来禁止对象的拷贝的。
新增的STL
- std::forward_list:单向链表,只可以前进,在特定场景下使用,相比于std::list节省了内存,提高了性能
std::forward_list<int> fl = {1, 2, 3, 4, 5};
for (const auto &elem : fl) {
cout << elem;
}
- std::unordered_set:基于hash表实现的set,内部不会排序,使用方法和set类似
- std::unordered_map:基于hash表实现的map,内部不会排序,使用方法和set类似
- std::array:数组,在越界访问时抛出异常,建议使用std::array替代普通的数组
- std::tuple:元组类型,类似pair,但比pair扩展性好
typedef std::tuple<int, double, int, double> Mytuple;
Mytuple t(0, 1, 2, 3);
std::cout << "0 " << std::get<0>(t);
std::cout << "1 " << std::get<1>(t);
std::cout << "2 " << std::get<2>(t);
std::cout << "3 " << std::get<3>(t);
参考:
https://cloud.tencent.com/developer/beta/article/1745592
https://zhuanlan.zhihu.com/p/405432878
https://zhuanlan.zhihu.com/p/436290273
参考公众号:程序喵大人