C++ 学习笔记:C++11 新特性学习

C++11是曾经被叫做C++0x,是对目前 C++ 语言的扩展和修正,C++11 不仅包含核心语言的新机能,而且扩展了 C++ 的标准程序库(STL),并入了大部分的 C++ Technical Report 1(TR1)程序库(数学的特殊函数除外)。

C++11 包括大量的新特性:包括lambda表达式,类型推导关键字auto decltype,和模板的大量改进。
本文将对 C++11 的以上新特性进行简单的讲解,以便大家能够快速了解到 C++11 对 C++ 的易用性方面起到的巨大作用。

auto

在C++11之前,auto 关键字用来指定存储期。在新标准中,它的功能变为类型推断auto 现在成了一个类型的占位符,通知编译器去根据初始化代码推断所声明变量的真实类型。各种作用域内声明变量都可以用到它。例如,名空间中,程序块中,或是for 循环的初始化语句中。

auto a; // 错误,auto是通过初始化表达式进行类型推导,如果没有初始化表达式,就无法确定a的类型
auto i = 42;        //  i is an int
auto l = 42LL;      //  l is an long long
auto p = new foo(); // p is a foo*
auto d = 1.0;
auto str = "Hello World";
auto ch = 'A';
auto func = less<int>();
vector<int> iv;
auto it = iv.begin();

使用auto 通常意味着更短的代码(除非你所用类型是 int,它会比auto 少一个字母)。试想一下当你遍历 STL 容器时需要声明的那些迭代器(iterator)。现在不需要去声明那些 typedef 就可以得到简洁的代码了。

为了在遍历容器时支持“foreach”用法,C++11 扩展了for 语句的语法。用这个新的写法,可以遍历 C 类型的数组、初始化列表以及任何重载了非成员的 begin() 和 end() 函数的类型。
如果你只是想对集合或数组的每个元素做一些操作,而不关心下标、迭代器位置或者元素个数,那么这种 foreach 的 for 循环将会非常有用。

template<typename T>
void printVec1D(const vector<T>& vec) {// 打印一维数组
//    for(typename vector<T>::const_iterator it=vec.begin(); it != vec.end(); it++)
//        cout << *it << " ";
    for(const auto& val : vec)
        cout << val << " ";
}

map<string, vector<int> > Map;
for(auto it = Map.begin(); it != Map.end(); ++it) {
    //do something with it
}
需要注意的是,auto 不能用来声明函数的返回值。但如果函数有一个尾随的返回类型时,auto 是可以出现在函数声明中返回值位置。这种情况下,auto 并不是告诉编译器去推断返回类型,而是指引编译器去函数的末端寻找返回值类型。在下面这个例子中,函数的返回值类型就是operator操作符作用在T1、T2类型变量上的返回值类型。

template <typename T1, typename T2>
auto compose(T1 t1, T2 t2) -> decltype(t1 + t2) {
    return t1+t2;
}
auto v = compose(2, 3.14); // v's type is double


decltype

decltype 实际上有点像auto 的反函数,auto 可以让你声明一个变量,而decltype 则可以从一个变量或表达式中得到类型,有实例如下:
int x = 3;  
decltype(x) y = x; 
有什么实际应用呢?举个例子,上面代码中auto compose(T1 t1, T2 t2)->decltype(t1+ t2用 decltype 解析返回值类型。

nullptr

nullptr 是为了解决原来 C++ 中 NULL 的二义性问题而引进的一种新的类型,因为 NULL 实际上代表的是 0,但由于 0 可以被隐式类型转换为整型,这就会存在一些问题。关键字nullptr 是 std::nullptr_t 类型的值,用来指代空指针。nullptr 和任何指针类型以及类成员指针类型的空值之间可以发生隐式类型转换,同样也可以隐式转换为bool 型(取值为 false)。但是不存在到整型的隐式类型转换

void F(int a) {
    cout<<a<<endl;
}

void F(int *p) {
    assert(p != NULL);
    cout<< p <<endl;
}

int main() {
    int *p = nullptr;
    int *q = NULL;
    bool Equal = ( p == q ); // Equal的值为 true,说明 p 和 q 都是空指针
    int a = nullptr; // 编译失败,nullptr 不能转型为 int
    F(0); // 在 C++98 中编译失败,有二义性;在C++11中调用 F(int)
    F(nullptr);// 调用 f(int*)

    return 0;
}
为了向前兼容,0 仍然是个合法的空指针值。


override 和 final

我总觉得 C++ 中虚函数的设计很差劲,因为时至今日仍然没有一个强制的机制来标识虚函数会在派生类里被改写。vitual 关键字是可选的,这使得阅读代码变得很费劲。因为可能需要追溯到继承体系的源头才能确定某个方法是否是虚函数。为了增加可读性,我总是在派生类里也写上 virtual 关键字,并且也鼓励大家都这么做。即使这样,仍然会产生一些微妙的错误。看下面这个例子:

class Base {
public:
    virtual void f(short) {
        std::cout << "Base::f" << std::endl;
    }
};

class Derived : public Base {
public:
    virtual void f(int) {
        std::cout << "Derived::f" << std::endl;
    }
};

因为这里形参不同,所以这里并不是重写,而是派生类 Derived 中新定义一个虚函数。

因为这里形参不同,所以这里并不是重写,而是派生类 Derived 中新定义一个虚函数。
但我的意图是重写幸运的是现在有一种方式能描述你的意图。新标准加入了两个新的标识符(不是关键字):overridefinal

  1. override,表示函数应当重写基类中的虚函数。(如果指定override,就表示这个函数是重写的,如果我们函数写的不符合,就会编译出错提示)
  2. final,表示派生类不应当重写这个虚函数。

class Derived : public Base {
public:
    // 这里加上 override 表示这个函数是重写的,因为形参表不同,无法重写,编译提示错误:error: 'virtual void Derived::f(int)' marked override, but does not override|
    virtual void f(int) override {
        std::cout << "Derived::f" << std::endl;
    }
};
//=======================================================
class B {
public:
    virtual void f(int) {
        std::cout << "B::f" << std::endl;
    }
};

class D : public B {
public:
    // 这里加上 final 表示函数 f(int) 不可以被其派生类重写
    virtual void f(int) override final {
        std::cout << "D::f" << std::endl;
    }
};

class F : public D {
public:
    virtual void f(int) override {// 编译错误,不能重写
        std::cout << "F::f" << std::endl;
    }
};
在派生类中,可以同时使用overridefinal 标识。

Strongly-typed enums 强类型枚举

传统的 C++ 枚举类型存在一些缺陷:它们会将枚举常量暴露在外层作用域中(这可能导致名字冲突,如果同一个作用域中存在两个不同的枚举类型,但是具有相同的枚举常量就会冲突),而且它们会被隐式转换为整形,无法拥有特定的用户定义类型。

在 C++11 中通过引入了一个称为强类型枚举的新类型,修正了这种情况。强类型枚举由关键字 enumclass 标识。它不会将枚举常量暴露到外层作用域中,也不会隐式转换为整形,并且拥有用户指定的特定类型(传统枚举也增加了这个性质)。

enum class Options {
    None, One, All
};
Options o = Options::All;// 需要使用作用域。

Lambdas

匿名函数(也叫 lambda)已经加入到 C++中,并很快异军突起。这个从函数式编程中借来的强大特性,使很多其他特性以及类库得以实现。你可以在任何使用函数对象或者函子(functor)或 std::function 的地方使用 lambda。

Lambda的语法如下:
函数对象参数 ]( 操作符重载函数参数 )->返回值类型{ 函数体 }

vector<int> iv{5, 4, 3, 2, 1};  
int a = 2, b = 1;  
std::for_each(iv.begin(), iv.end(), [b](int &x){cout<<(x + b)<<endl;});// (1)  
std::for_each(iv.begin(), iv.end(), [=](int &x){x *= (a + b);});// (2)  
std::for_each(iv.begin(), iv.end(), [=](int &x)->int{return x * (a + b);});// (3)

1.  [ ] 内的参数指的是 Lambda 表达式可以取得的全局变量。(1)函数中的 b 就是指函数可以得到在 Lambda 表达式外的全局变量,如果在[ ] 中传入= 的话,即是可以取得所有的外部变量,如(2)和(3)Lambda 表达式

2.   ( ) 内的参数是每次调用函数时传入的参数。
3.   -> 后加上的是 Lambda 表达式返回值的类型,如(3)中返回了一个int 类型的变量


变长参数的模板

我们在C++中都用过pair,pair可以使用 make_pair 构造,构造一个包含两种不同类型的数据的容器。比如

auto p = make_pair(1, "C++ 11");

由于在 C++11 中引入了变长参数模板,所以发明了新的数据类型:tuple,tuple 是一个 N 元组,可以传入 1 个, 2 个甚至多个不同类型的数据器。比如

auto t1 = make_tuple(1, 2.0, "C++ 11");
auto t2 = make_tuple(1, 2.0, "C++ 11", {1, 0, 2});


更优雅的初始化

在被引入 C++ 之前,只有数组能使用初始化列表,其他容器想要使用初始化列表,只能用一下方法:

int arr[3] = {1, 2, 3};
vector<int> v(arr, arr + 3); 
在 C++11 中,我们可以使用如下方式初始化:
int arr[3]{1, 2, 3};  
vector<int> iv{1, 2, 3};  

map<int, string>{{1, "a"}, {2, "b"}};
 
string str{"Hello World"}; 

参考这个地方这个地方






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章