浮萍晓生的开发日志

记录学习的旅程,把握可预见的未来

导航

C++ 11 新增语法

Posted on 2014-01-08 16:44  浮萍晓生  阅读(330)  评论(0)    收藏  举报

C++11的标准已经确定,除了增加了不少库函数外,在语法方便也得到了许多增强。其中如下几个语法就是我比较喜欢的:

自动类型推导auto

现在c++终于在编译器级别支持类似C#的var关键字了,在c++里的关键字是auto,基本用法如下:

    auto i = 0;        //int
    auto c = 'c';    //char
    auto s = "hello world";    //const char*

auto关键字的一个很直观的好处是我们可以简化stl容器遍历里的那个iterator了:

    for(auto it = v.begin(); it != v.end(); it++)
    {
        cout << *it << endl;
    }

Lambda 表达式

Lambda 表达式的主要用来构造匿名函数,它可以替换绝大部分场景的仿函数(感觉把刚刚引入STL库的std::bind也顺手给秒了),并且有更好的可读性。一个简单的示例如下:

    auto k = [](int xint y) { return x + y; };
    cout << k(3, 2) << endl;

可以看到,它的基本语法形式为: [capture] (parameters) {body}后两部分和C#的匿名函数的语法非常,但最前面的 [capture] 的作用是什么呢?它是用来引用表达式外的局部变量的,例如:

    int i1 = 0, i2 = 3;
    auto k = [&i1, &i2] () { i1 = 3; i2 = 5; };
    cout << i1 << " " << i2 << endl;

除了是前面所述的普通局部变量外,还可以是如下特殊符号:

  • =    所有局部变量,以按值的方式传递(包括this指针)
  • &    所有局部变量,以按引用的方式传递(包括this指针)
  • this this指针

Range-based for-loop

这个其实就是类似C#里面的foreach,不过它并没有引入新关键字,而是直接用的for

    int p[8] = {2, 3, 5, 7, 11, 13, 17, 19};
    for (auto& i: p) 
    {
        printf("%d ", i);
    }

除了支持数组外,它也支持stl中的迭代器遍历,for_each函数基本上可以下岗了。至于它的原理,可以参考这篇文章

枚举类

在C语言中,枚举等同于一个数字常量,编译器并没有对它做过多的处理,在C++11中,引入了enum class以替换枚举,它的定义方式如下:

    enum class Color { RedBlue};
    enum 
class Fruit { BananaApple};

除了多加了一个class外,没有别的变化,但它定义的枚举功能和C#中的枚举相似。和C语言的传统枚举相比,主要有如下两个区别:

  1. 强类型的语法检查
  2. 枚举名称只需要在class范围内唯一

强类型的语法检查可以有效防止枚举之间的非法转换和比较,Color::Red == Fruit::Banana之类的判断无法编译通过。

而枚举名称只需要在class范围内唯一则有效缩短了枚举的长度,对于Color枚举,在C语言中,为了防止歧义和命名冲突,往往需要定义为Color_Red、Color_Blue的形式。

静态断言static_assert

静态断言主要提供了在代码中实现编译期可以确定的断言,如果出错则直接以编译错误的形式显示出来,从而加强程序的健壮性。这个其实就是以前的boost.static_assert,不过在编译器的支持下变得更加友好了。示例如下:

    static_assert(sizeof(int) == 4, "int needs to be 4 bytes to use this code");

密闭类和密闭方法

C++11中也引入了类似C# seal关键字的方式实现密闭类和密闭方法,以阻止对象继承、方法重载。不过它的关键字是final,和java类似。

    final类

    class Base final
    {
    }; 
    class Derived : public Base //
继承密闭类,语法错误
    {
    };

    final方法

    class Base
    {
        virtual void A() 
final
    }; 
    class Derived : public Base 
    {
        virtual void A(); //
重写密闭方法,编译出错

    };

显式覆盖

对于虚函数的重写,在c++ 11中可以通过关键字显式覆盖,从而获得更严格的语法检查。

    class Base
    {
        virtual void A(float=0.0);
        virtual void B() const;
        virtual void C();
        void D();
    };
    class Derivedpublic Base
    {
        virtual void A(int=0) 
override//
定义不一致,编译错误

        virtual void B() 
override;        //
返回类型不是const,编译错误
        virtual void C() 
override;        //
正确
        void D() 
override;                //
不是虚函数,编译错误
    };

PS:我倒觉得这个语法检查应该默认就加上,通过关键字显式去掉检查,而不是像这样显式打开检查。万恶的向上兼容呀。

 

原生字符串(raw string literals)

很多时候,当我们只需要一行字符串的时候,字符串转义往往成了一个负担,和写和读都带了很大的不便。例如,对于如下路径"C:\Program Files\Microsoft.NET\ADOMD.NET",我们必须把它写成如下形式:

    string path = "C:\\Program Files\\Microsoft.NET\\ADOMD.NET";

可能你会说这个并没有多大影响,下面这个正则表达式的例子呢?你能看出来原文到底是什么吗?

    string exp = "('(?:[^\\\\']|\\\\.)*'|\"(?:[^\\\\\"]|\\\\.)*\")|";

在C#中,我们可以通过@关键字来取消字符串转义。现在,在C++ 11中,也增加了这样的语法。对于前面的例子,它的非转义形式为:

    string path = R"(C:\Program Files\Microsoft.NET\ADOMD.NET)";

从上面的例子中可以看出,它的语法格式如下:

  1. 字符串前加'R'前缀
  2. 字符串首尾加上括号()

它的语法格式比C#的@前缀要稍微复杂点,不过这个复杂也有复杂的好处,那就是字符串里面可以带引号,例如:

    string path = R"(this "word" is escaped)";

而C#就无法保持原始字符串格式,对引号仍需要转义:

    string path = @"this ""word"" is escaped";

 

委托构造函数(Delegating constructors

C++的构造是不能复用的,为了复用其初始化操作,我们往往会增加一个Initial函数:

    class Foo
    {
    private:
        int A;

    public:
        Foo() : A(0)
        {
            Init();
        }
        Foo(int a) : A(a)
        {
            Init();
        }

    private:
        void Init()
        {
            // do something
        }
    };

这样一来就增加了一个只调用一次的Init函数,并且一旦这个Init函数被其它成员函数调用的话,可能导致重复初始化,也是一个隐患。PS:本例比较简单,通过构造函数默认参数也可以解决构造函数复用问题,但默认参数也有一些局限和带来一些问题,限于篇幅就不做更多的讨论了。

在C++ 11中,引入了委托构造函数的语法,其功能和C#中的this构造函数非常类似,就是语法上稍有差异:

    class Foo
    {
    private:
        int A;

    public:
        Foo() : Foo(0)
        {
        }
        Foo(int a) : A(a)
        {
            // do something
        }
    };

 

初始化列表(initializer list)

在C++ 03中,可以用列表的形式来初始化数组,这种方式非常直观,但只能适用于数组,不能适用于我们自定义的容器:

    int anArray[5] = { 3, 2, 7, 5, 8 }; // ok

    std::vector<int> vArray = { 3, 2, 7, 5, 8 }; // not ok

在C++ 11中,我们则可以使得我们自定义的容器对象支持这种列表的形式的初始化方式:

    template <typename T>
    class MyArray
    {
    private:
        vector<T> m_Array;

    public:
        MyArray() { }

        MyArray(const initializer_list<T>& il)
        {
            for (auto x : il
                m_Array.push_back(x);
        }
    };

    void main()
    {
        MyArray<int> foo = { 3, 4, 6, 9 };
    }

 

统一初始化(Uniform initialization)

C++的对象初始化方式是非常多样的:

    int a = 2;        //"赋值风格"的初始化
    int aa [] = { 2, 3 }; //
用初始化列表进行的赋值风格的初始化
    complex z(1, 2);    //"
函数风格"的初始化

C++ 11中,允许通过以花括号的形式来调用构造函数。这样多种对象构造方式便可以统一起来了:

    int a = { 2 };   
    int aa [] = { 2, 3 };  

    complex z = { 1, 2 }; 

值得一提的是,这种花括号的构造方式还可以用于函数的参数和返回值的类型推导,非常简洁。

    void useMyStruct(MyStruct x)
    {
    }
    useMyStruct({ 2, 3.5f }); 

    MyStruct makeMyStruct(void)
    {
        return { 2, 3.5f };
    }

 

非静态成员直接初始化

在C++ 03的时候,非静态成员变量只能在对象的构造函数里初始化,例如:

    struct A
    {
        int m;
        A() : m (7) { }
    };

当对象成员比较多的时候,这个对象成员的初始化是非常难看的。尤其是在构造函数较多的情况下,由于C++ 03不支持委托构造函数,这一大堆的成员需要在每一个构造函数中初始化一遍,是十分冗繁而容易出错的。

在C++ 11中,非静态成员也能以静态成员那种方式直接初始化的,这样就直观的多了:

    struct A
    {
        int m = 7;
    };

 

启用和禁止默认函数

在C++中,编译器会生成一些默认函数,例如对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如

  1. A(void);                    // 缺省的无参数构造函数
  2. A(const A &a);                // 缺省的拷贝构造函数
  3. ~A(void);                    // 缺省的析构函数
  4. A & operate =(const A &a);    // 缺省的赋值函数

在C++ 11中,支持通过 default 和 delete 两个关键字来管理这默认函数。delete意为禁止默认函数,default则使用默认函数。

例如,当我们自定义你空构造函数时,编译器就不会给我们生成缺省无参构造函数,此时则可以通过= default来让编译器产生默认构造函数。

    struct A
    {
        A() = default;
        A(int n) {}
    };

至于= delete,一个比较典型的用途是可以定义一个不可拷贝的对象,如:

    struct NoCopy
    {
        NoCopy & operator =(const NoCopy &) = delete;
        NoCopy(const NoCopy &) = delete;
    };

另外,= delete也可以用于禁止那些隐式转换而执行的函数,例如:

    struct A
    {
        void foo(int k){}
        void foo(double k) = delete;
    };

这样,类似a.foo(3.2)之类的调用就会有编译错误(话说,应该像C#那样天生报编译错误才更为合理)。

 

类型别名

虽然自C语言时代就支持通过typedef来为类型起别名,但typedef对函数指针起别名显得比较难看,并且不支持模板。因此,在C++ 11种新增了using为对象起别名的语法:

    // typedef std::ios_base::fmtflags flags;
    using flags = std::ios_base::fmtflags;

    // typedef void (*func)(int, int);
    using func = void(*) (intint);


    template<class Tusing ptr = T*;
    //'ptr<T>' 
等价于
 T 的指针
    ptr<int> x;