深入了解C++(3)——C++11特性小结

 C++的第一版发布于98年,但是随着时代的变迁,一些更加现代化的思想也渐渐融入到C++的标准之中,特别显著的就是C++11的版本。今天就简要的介绍C++11的新特性。

一 nullptr

 在C++11的新标准中,利用nullptr替换了NULL关键字。这个关键词的目的主要是解决0的二义性。我们可以从下面的示例代码简要了解C和C++中NULL通常的定义方式。

1
2
3
4
5
#ifdef __cplusplus ---简称:cpp c++ 文件
#define NULL 0
#else
#define NULL ((void *)0)
#endif

 因为在C++中void*不能隐式的转换为其他类型的指针,然后为了为了解决空指针的问题所以特别引入0代表空指针的操作,但是这样的操作在下面的情况就会造成严重的二义性。

1
2
void bar(sometype1 a, sometype2 *b);
void bar(sometype1 a, int i);

 为了解决二义性问题,有时候会采取这样的代码来避免二义性,即使用static_cast对0进行显式类型转换。

1
2
3
bar(a, NULL)//×××
bar(a, static_cast<sometype2 *>(0)); //√√√
bar(a, static_cast<sometype2 *>(NULL)); //√√√

 上述的这种操作可以说是异常别扭的。在进行这样特殊的重载时,使用NULL必须使用手动转换,这可以说时让人十分头皮发麻的。所以这里引入nullptr极大程度的解决了这个问题。

1
bar(a, nullptr)//√√√ C++11

 使用nullptr可以解决NULL的二义性问题,这可以说是一波让人心情舒畅的操作。

二 使用using代替typedef

 使用using替代typedef的简要代码如下:

1
2
3
4
5
6
7
8
9
typedef double db;
//c98
using db = double;
//c++11

using query_record = std::tuple<time_t, std::string>;
//c++11
template<class T> using twins = std::pair<T, T>;
//更广泛的还可以用于模板

 就个人感觉而言,这种变化带来的最大优势如下:

1
2
3
#define ll long long
typedef long long ll
using ll = long long

 使用typedef感觉会产生巨大的歧义,这句话的含义可以是ll=>long long 但是其实某种意义上也能被理解为 long ll => long??。不得不说这里利用using代替typedef让人无比的舒畅。

三 神器auto与基于范围的遍历

 不得不说,在C++11中,对于程序员来说最让人欣喜的就是auto关键字。不管从哪方面来看,auto关键字都大大的减少了程序员敲键盘的频率(大雾)。有效的缓解了程序员的肌肉劳损。

 auto关键字的最大功能就是推测类型,然后我们还可以利用decltype获取变量的类型,简要的功能代码如下:

1
2
3
4
5
6
auto a = 1;
auto task = std::function<void ()>([this, a] {
~~~~~
});

decltype(a) b = 2;

 上述的代码可以说还是小case,auto关键词最大的优势可能在下面的新特性中:

1
2
3
4
5
6
7
8
map<int,int>all;
for(map<int,int>::iterator i(all.begin());i!=all.end();i++){
//~~~
}

for(auto &i:all){
//~~~
}

 可以说简直让人爽到原地起飞,如果说map里面的类型名非常的长,这个时候利用auto就可以顺利的避开敲这么一长串。当然部分开发领域为了保证阅读时语义清晰,还是会采用上面的写法,but很显然下面的写法逐渐成了主流,毕竟python里面迭代一个容器只需要下面的代码:

1
2
3
a = [1, 2, 3, 4, 5]
for i in a:
print(i)

 虽然不知道这种迭代方式最初源自于哪个语言,但不得不说这是C++的一大进步。

四 右值引用

 一般来说我们采用一个非常简单的方法来判断一个值是左值还是右值:看能不能对表达式取地址,如果能,则为左值,否则为右值。对于C++11来说右值引用主要是为了拯救函数的返回值。

 通常来说一个函数的返回值会销毁掉,比如下面的这个函数:

1
2
3
4
5
6
int add(int a,int b){
return a+b;
}
int main(){
int c = add(1, 2);
}

 这个函数返回的是一个临时变量,他在运算结束后就会被销毁。如同上面的变量C,它是利用add函数的返回的临时变量给自己赋值,然后临时变量在运行完该行代码后会被销毁。在C++11之前,为了减少这样的开销,通常人们会采用神奇的常用左值引用的办法。

1
2
3
int main(){
const int &c = add(1, 2);
}

 这样的操作虽然可以延长返回值的生命周期,但是缺点也是显而易见的,变量c的值可能被固定不能修改。

1
2
3
int main(){
int && c= add(1, 2);
}

 使用&&后可以使add的返回值重获新生。该返回值将会一直存活下去直到c变量消亡。

五 move

 move其实是右值引用相关的内容,但是由于内容规划的问题不知道如何衔接了,所以特别开出另外一个板块。move的作用之一就是将一个左值转换成右值。主要用于下面的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 // 构造函数
MyString(const char* cstr=0){
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}

// 拷贝构造函数
MyString(const MyString& str) {
CCtor ++;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 移动构造函数
MyString(MyString&& str) noexcept
:m_data(str.m_data) {
MCtor ++;
str.m_data = nullptr; //不再指向之前的资源了
}

 首简要贴出三种构造函数。

1
2
3
4
5
6
7
8
9
10
11
vector<Mystring> vec_str;
for(int i(0);i<100;i++){
Mystring a = "hello world"
vec_str.push_back(a);
}

vector<Mystring> vec_str;
for(int i(0);i<100;i++){
Mystring a = "hello world"
vec_str.push_back(std::move(a));
}

 通过自定义的类我们可以发现,前一段代码调用了拷贝构造函数,而后一段代码调用了移动构造函数。

 最后作为总结贴出一个利用move的特性构造的swap。在类定义了移动拷贝函数时,这种方式将大大降低开销,在没有定义时它跟普通函数相似。

1
2
3
4
5
6
7
template <typename T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}

总结

 这篇东西是在晚上睡不着觉但是又不想刷手机的时候查资料敲出来的,不知道有没有错,但是其实是知道关于右值引用的部分写的是有一定的欠缺的。倘若未来有幸使用C++11进行真正的工程时间,必将把该部分继续完善。頑張りましょう~

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×