C++11 温故知新

1 decltype && auto

decltype (declare tpe) 和 auto 一样都是在编译期进行自动类型推导。

auto varname = value;
decltype(exp) varname = value;
  • auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。
  • auto 要求变量必须初始化,而 decltype 不要求
  • auto 虽然在书写格式上比 decltype 简单,但是它的推导规则复杂,有时候会改变表达式的原始类型;而 decltype 比较纯粹,它一般会坚持保留原始表达式的任何类型,让推导的结果更加原汁原味。
  • [1] C++ auto和decltype的区别

2 返回值类型后置

**返回类型后置(trailing-return-type,又称跟踪返回类型)**语法,将 decltype 和 auto 结合起来完成返回值类型的推导。

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
    return t + u;
}
int& foo(int& i);
float foo(float& f);
template <typename T>
auto func(T& val) -> decltype(foo(val))
{
    return foo(val);
}

==返回值类型后置语法,是为了解决函数返回值类型依赖于参数而导致难以确定返回值类型的问题。==有了这种语法以后,对返回值类型的推导就可以用清晰的方式(直接通过参数做运算)描述出来,而不需要像 C++98/03 那样使用晦涩难懂的写法。

3 using && typedef

如重定义一个函数指针:

  • typedef void (*func_t)(int, int)
  • using func_t = void (*)(int, int)
/* C++98/03 */
template <typename T>
struct func_t
{
    typedef void (*type)(T, T);
};
// 使用 func_t 模板
func_t<int>::type xx_1;
/* C++11 */
template <typename T>
using func_t = void (*)(T, T);
// 使用 func_t 模板
func_t<int> xx_2;

从示例中可以看出,通过 using 定义模板别名的语法,只是在普通类型别名语法的基础上增加 template 的参数列表。使用 using 可以轻松地创建一个新的模板别名,而不需要像 C++98/03 那样使用烦琐的外敷模板。

4 lambda匿名函数

[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
   函数体;
};
外部变量格式功能
[]空方括号表示当前 lambda 匿名函数中不导入任何外部变量。
[=]只有一个 = 等号,表示以值传递的方式导入所有外部变量;
[&]只有一个 & 符号,表示以引用传递的方式导入所有外部变量;
[val1,val2,…]表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序;
[&val1,&val2,…]表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序;
[val,&val2,…]以上 2 种方式还可以混合使用,变量之间没有前后次序。
[=,&val1,…]表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
[this]表示以值传递的方式导入当前的 this 指针。
[外部变量]的定义方式

5 for循环

for (declaration : expression){
    //循环体
}

新语法格式的 for 循环还支持遍历用{ }大括号初始化的列表,比如:

#include <iostream>
using namespace std;
int main() {
    for (int num : {1, 2, 3, 4, 5}) {
        cout << num << " ";
    }
    return 0;
}

在使用新语法格式的 for 循环遍历某个序列时,如果需要遍历的同时修改序列中元素的值,实现方案是在 declaration 参数处定义引用形式的变量。

#include <iostream>
#include <vector>
using namespace std;
int main() {
    char arc[] = "abcde";
    vector<char>myvector(arc, arc + 5);
    //for循环遍历并修改容器中各个字符的值
    for (auto &ch : myvector) {
        ch++;
    }
    //for循环遍历输出容器中各个字符
    for (auto ch : myvector) {
        cout << ch;
    }
    return 0;
}

declaration 参数既可以定义普通形式的变量,也可以定义引用形式的变量,应该如何选择呢?
其实很简单,如果需要在遍历序列的过程中修改器内部元素的值,就必须定义引用形式的变量;反之,建议定义const &(常引用)形式的变量(避免了底层复制变量的过程,效率更高),也可以定义普通变量。

**注意:**在使用基于范围的 for 循环遍历容器时,应避免在循环体中修改容器存储元素的个数。很大可能会导致迭代器失效,得不到想得到的结果。

6 constexpr

constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。

C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。

7 右值引用

右值引用主要用于实现移动(move)语义和完美转发

判断某个表达式是左值还是右值,最常用的有以下 2 种方法。

  • 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。
  • 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。

常见左值引用示例:

int num = 10;
int &b = num; //正确
int &c = 10; //错误

常量左值引用既可以操作左值,也可以操作右值:

int num = 10;
const int &b = num;
const int &c = 10;

右值引用也必须立即进行初始化操作,且只能使用右值进行初始化

int num = 10;
//int && a = num;  //右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。

int && a = 10;
a = 100;
cout << a << endl;

8 移动构造函数

所谓移动语义,指的就是以==移动而非深拷贝==的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”,以提高执行效率。

事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

9 move()函数:将左值强制转换为右值

move( arg )

其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。

#include <iostream>
using namespace std;
class movedemo{
public:
    movedemo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    //拷贝构造函数
    movedemo(const movedemo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //移动构造函数
    movedemo(movedemo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
public:     //这里应该是 private,使用 public 是为了更方便说明问题
    int *num;
};
int main(){
    movedemo demo;
    cout << "demo2:\n";
    movedemo demo2 = demo;
    //cout << *demo2.num << endl;   //可以执行
    cout << "demo3:\n";
    movedemo demo3 = std::move(demo);
    //此时 demo.num = NULL,因此下面代码会报运行时错误
    //cout << *demo.num << endl;
    return 0;
}

输出:

construct!
demo2:
copy construct!
demo3:
move construct!

10 完美转发

完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变

C++11 标准为 C++ 引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。

C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。

C++11 标准引入了一个模板函数forword<T>(),可以很方便地解决将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数

#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {
    cout << "lvalue\n";
}
void otherdef(const int & t) {
    cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t) {
    otherdef(forward<T>(t));
}
int main()
{
    function(5);
    int  x = 1;
    function(x);
    return 0;
}

输出:

rvalue
lvalue

11 智能指针

C++ 从来没有公开得支持过垃圾回收机制。但C++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。

#include <memory>

11.1 shared_ptr

#include <memory>
// 创建 shared_ptr<T>
std::shared_ptr<int> p1;
std::shared_ptr<int> p2(nullptr);

// 在构建 shared_ptr 时,也明确指向
std::shared_ptr<int> p3(new int(10));
std::shared_ptr<int> p4 = std::make_shared<int>(10);

// shared_ptr<T> 模板还提供有相应的拷贝构造函数和移动构造函数
// 调用拷贝构造函数
std::shared_ptr<int> p5(p4);  // 或者 std::shared_ptr<int> p5 = p4;
// 调用移动构造函数
std::shared_ptr<int> p5(std::move(p4));  // 或者 std::shared_ptr<int> p5 = std::move(p4);

//  注意:同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会发生异常
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误

/**
 * 在初始化 shared_ptr 智能指针时,还可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,
 * 会优先调用我们自定义的释放规则。
 * 比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释
 * 放规则,才能正确地释放申请的堆内存。
 */
// 指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
// 自定义释放规则
void deleteInt(int *p) {
    delete []p;
}
std::shared_ptr<int> p7(new int[10], deleteInt);

// 借助 lambda 表达式
std::shared_ptr<int> p7(new int[10], [](int* p){delete []p;});

参考文档:http://c.biancheng.net/view/7898.html

11.2 unique_ptr

#include <memory>

// 创建出空的 unique_ptr 指针
std::unique_ptr<int> p1();
std::unique_ptr<int> p2(nuppltr);

// 创建 unique_ptr 时明确其指向
std::unique_ptr<int> p3(new int);

// 基于 unique_ptr 类型指针不共享各自拥有的堆内存,C11只提供了移动构造函数。
std::unique_ptr<int> p4(new int);
std::unique_ptr<int> p5(p4);//错误,堆内存不共享
std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数

// 默认情况下,unique_ptr 指针采用 std::default_delete<T> 方法释放堆内存。当然,我们也可以自定义符合实际场景的释放规则。值得一提的是,和 shared_ptr 指针不同,为 unique_ptr 自定义释放规则,只能采用函数对象的方式。
// 自定义释放规则
struct myDel
{
    void operator()(int *p) {
		delete p;
    }
};

std::unique_ptr<int, myDel> p6(new int);
// std::unique_ptr<int, myDel> p6(new int, myDel());

参考文档:http://c.biancheng.net/view/7909.html

11.3 weak_ptr

weak_ptr 不能单独使用,要和 shared_ptr 配合使用,视为 shared_ptr 的一种辅助工具,主要用于防止 shared_ptr 互相指向导致内存泄漏问题而引出。

当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。

weak_ptr 模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。

// weak_ptr 的创建
std::weak_ptr<int> wp1;
std::weak_ptr<int> wp2(wp1);

std::shared_ptr<int> sp(new int);
std::weak_ptr<int> wp3(sp);
#include <iosream>
#include <memory>
using namespace std;

int main() {
    std::shared_ptr<int> sp1(new int (10));
    std::shared_ptr<int> sp2(sp1);
    std::weak_ptr<int> wp(sp2);
    cout << wp.use_count() << endl;
    sp2.reset();
    cout << wp.use_count() << endl;
    cout << *(wp.lock()) << endl;
    return 0;
}

参考文档:http://c.biancheng.net/view/7918.html

附录

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值