C++11一些常用的特性(稳定性和兼容性)

C++11一些常用的特性(稳定性和兼容性)

一、原始字面量

在C++11中添加了定义原始字符串的字面量,定义方式为:R“xxx(原始字符串)xxx” 其中()两边的字符串可以省略。原始字面量R可以直接表示字符串的实际含义,而不需要额外对字符串做专一或连接等操作。

比如:编程过程中,使用的字符串中常带有一些特殊字符,对于这些字符往往要做专门的处理,使用了原始字面量就可以轻松的解决这个问题了,比如打印路径:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str = "D:\hello\world\test.text";
    cout << str << endl;
    string str1 = "D:\\hello\\world\\test.text";
    cout << str1 << endl;
    string str2 = R"(D:\hello\world\test.text)";
    cout << str2 << endl;
	system("pause");
    return 0;
}


输出的结果为:

D:helloworld    est.text
D:\hello\world\test.text
D:\hello\world\test.text

在 D:\hello\world\test.text 中 \h 和 \w 转义失败,对应的字符会原样输出
在 D:\hello\world\test.text 中路径的间隔符为 \ 但是这个字符又是转义字符,因此需要使用转义字符将其转义,最终才能得到一个没有特殊含义的普通字符
在 R"(D:\hello\world\test.text)" 使用了原始字面量 R()中的内容就是描述路径的原始字符串,无需做任何处理

通过测试可以看到,使用原始字面量 R 可以直接得到其原始意义的字符串,再看一个输出 HTML 标签的例子:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str = "<html>\
        <head>\
        <title>\
        海贼王\
        </title>\
        </head>\
        <body>\
        <p>\
        我是要成为海贼王的男人!!!\
        </p>\
        </body>\
        </html>";
    cout << str << endl;
    return 0;
}

在 C++11 之前如果一个字符串分别写到了不同的行里边,需要加连接符,这种方式不仅繁琐,还破坏了表达式的原始含义,如果使用原始字面量就变得简单很多,很强直观,可读性强。

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str = R"(<html>
        <head>
        <title>
        海贼王
        </title>
        </head>
        <body>
        <p>
        我是要成为海贼王的男人!!!
        </p>
        </body>
        </html>)";
    cout << str << endl;

    return 0;
}

最后强调一个细节:在R “xxx(raw string)xxx” 中,原始字符串必须用括号()括起来,括号的前后可以加其他字符串,所加的字符串会被忽略,并且加的字符串必须在括号两边同时出现。

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str1 = R"(D:\hello\world\test.text)";
    cout << str1 << endl;
    string str2 = R"luffy(D:\hello\world\test.text)luffy";
    cout << str2 << endl;
#if 0
    string str3 = R"luffy(D:\hello\world\test.text)robin";	// 语法错误,编译不通过
    cout << str3 << endl;
#endif

    return 0;
}

测试代码输出的结果为:

D:\hello\world\test.text
D:\hello\world\test.text

通过输出的信息可以得到如下结论:使用原始字面量 R “xxx (raw string) xxx”,()两边的字符串在解析的时候是会被编译器忽略的,因此一般不用指定。如果在()前后指定了字符串,那么前后的字符串必须相同,否则会出现语法错误。

二、final和override

C++ 中增加了 final 关键字来限制某个类不能被继承,或者某个虚函数不能被重写,和 Java 的 final 关键字的功能是类似的。如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。

1、final修饰函数

如果使用 final 修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了:

#include<iostream>
#include<string>
using namespace std;

class Base
{
public:
    virtual void test()
    {
        cout << "Base class...";
    }
};

class Child : public Base
{
public:
    void test() final
    {
        cout << "Child class...";
    }
};

class GrandChild : public Child
{
public:
    // 语法错误, 不允许重写 因为基类test函数被final修饰
    void test()
    {
        cout << "GrandChild class...";
    }
};

在上面的代码中一共有三个类:

基类:Base
子类:Child
孙子类:GrandChild

test() 是基类中的一个虚函数,在子类中重写了这个方法,但是不希望孙子类中继续重写这个方法了,因此在子类中将 test() 方法标记为 final,孙子类中对这个方法就只有使用的份了,不能再去重写该方法了。

1.2 final 修饰类

使用 final 关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类。

class Base
{
public:
    virtual void test()
    {
        cout << "Base class...";
    }
};

class Child final: public Base
{
public:
    void test()
    {
        cout << "Child class...";
    }
};

// error, 语法错误 因为child类继承Base是final继承,不允许再有派生类
class GrandChild : public Child
{
public:
};

1.3 override关键字

override 关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和 final 一样这个关键字要写到方法的后面。使用方法如下:

class Base
{
public:
    virtual void test()
    {
        cout << "Base class...";
    }
};

class Child : public Base
{
public:
    void test() override
    {
        cout << "Child class...";
    }
};

class GrandChild : public Child
{
public:
    void test() override
    {
        cout << "Child class...";
    }
};

上述代码中第 13 行和第 22 行就是显示指定了要重写父类的 test() 方法,使用了 override 关键字之后,假设在重写过程中因为误操作,写错了函数名或者函数参数或者返回值编译器都会提示语法错误,提高了程序的正确性,降低了出错的概率。

三、模板的优化

1. 模板的右尖括号

在泛型编程中,模板实例化有一个非常繁琐的地方,那就是连续的两个右尖括号(>>)会被编译器解析成右移操作符,而不是模板参数表的结束。我们先来看一段关于容器遍历的代码,在创建的类模板 Base 中提供了遍历容器的操作函数 traversal():

// test.cpp
#include <iostream>
#include <vector>
using namespace std;

template <typename T>
class Base
{
public:
    void traversal(T& t)
    {
        auto it = t.begin();
        for (; it != t.end(); ++it)
        {
            cout << *it << " ";
        }
        cout << endl;
    }
};


int main()
{
    vector<int> v{ 1,2,3,4,5,6,7,8,9 };
    Base<vector<int>> b;
    b.traversal(v);

    return 0;
}

如果使用 C++98/03 标准来编译上边的这段代码,就会得到如下的错误提示:

test.cpp:25:20: error: '>>' should be '> >' within a nested template argument list
     Base<vector<int>> b;

根据错误提示中描述模板的两个右尖括之间需要添加空格,这样写起来就非常的麻烦,C++11改进了编译器的解析规则,尽可能地将多个右尖括号(>)解析成模板参数结束符,方便我们编写模板相关的代码。

上面的这段代码,在支持 C++11 的编译器中编译是没有任何问题的,如果使用 g++ 直接编译需要加参数 -std = c++11:

$ g++ test.cpp -std=c++11 -o app
2. 默认模板参数
#include <iostream>
using namespace std;

template <typename T=int, T t=520>
class Test
{
public:
    void print()
    {
        cout << "current value: " << t << endl;
    }
};

int main()
{
    Test<> t;
    t.print();

    Test<int, 1024> t1;
    t1.print();

    return 0;
}

但是不支持函数的默认模板参数,在C++11中添加了对函数模板默认参数的支持:

#include <iostream>
using namespace std;

template <typename T=int>	// C++98/03不支持这种写法, C++11中支持这种写法
void func(T t)
{
    cout << "current value: " << t << endl;
}

int main()
{
    func(100);
    return 0;
}

通过上面的例子可以得到如下结论:当所有模板参数都有默认参数时,函数模板的调用如同一个普通函数。但对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随 <> 来实例化。

另外:函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同,它没有必须写在参数表最后的限制。这样当默认模板参数和模板参数自动推导结合起来时,书写就显得非常灵活了。我们可以指定函数模板中的一部分模板参数使用默认参数,另一部分使用自动推导,比如下面的例子:

#include <iostream>
#include <string>
using namespace std;

template <typename R = int, typename N>
R func(N arg)
{
    return arg;
}

int main()
{
    auto ret1 = func(520);
    cout << "return value-1: " << ret1 << endl;

    auto ret2 = func<double>(52.134);
    cout << "return value-2: " << ret2 << endl;

    auto ret3 = func<int>(52.134);
    cout << "return value-3: " << ret3 << endl;

    auto ret4 = func<char, int>(100);
    cout << "return value-4: " << ret4 << endl;

    return 0;
}

测试代码输出的结果为:

return value-1: 520
return value-2: 52.134
return value-3: 52
return value-4: d

根据得到的日志输出,分析一下示例代码中调用的模板函数:

auto ret = func(520);

函数返回值类型使用了默认的模板参数,函数的参数类型是自动推导出来的为 int 类型。

auto ret1 = func<double>(52.134);

函数的返回值指定为 double 类型,函数参数是通过实参推导出来的,为 double 类型

cauto ret3 = func<int>(52.134);

函数的返回值指定为 int 类型,函数参数是通过实参推导出来的,为 double 类型

auto ret4 = func<char, int>(100);

函数的参数为指定为 int 类型,函数返回值指定为 char 类型,不需要推导
当默认模板参数和模板参数自动推导同时使用时(优先级从高到低):

如果可以推导出参数类型则使用推导出的类型
如果函数模板无法推导出参数类型,那么编译器会使用默认模板参数
如果无法推导出模板参数类型并且没有设置默认模板参数,编译器就会报错。

看一下下面的例子:

#include <iostream>
#include <string>
using namespace std;

// 函数模板定义
template <typename T, typename U = char>
void func(T arg1 = 100, U arg2 = 100)
{
    cout << "arg1: " << arg1 << ", arg2: " << arg2 << endl;
}

int main()
{
    // 模板函数调用
    func('a');
    func(97, 'a');
    // func();    //编译报错
    return 0;
}

程序输出的结果为:

arg1: a, arg2: d
arg1: 97, arg2: a

分析一下调用的模板函数 func():

func(‘a’):参数 T 被自动推导为 char 类型,U 使用的默认模板参数为 char 类型
func(97, ‘a’);:参数 T 被自动推导为 int 类型,U 使用推导出的类型为 char
func();:参数 T 没有指定默认模板类型,并且无法自动推导,编译器会直接报错
模板参数类型的自动推导是根据模板函数调用时指定的实参进行推断的,没有实参则无法推导
模板参数类型的自动推导不会参考函数模板中指定的默认参数。

四、数值类型和字符串之间的转换

在 C++11 中提供了专门的类型转换函数,可以非常方便的使用它们进行数值类型和字符串类型之间的转换。

1. 数值转换为字符串

使用 to_string() 方法可以非常方便地将各种数值类型转换为字符串类型,这是一个重载函,函数声明位于头文件 中,函数原型如下:

// 头文件 <string>
string to_string (int val);
string to_string (long val);
string to_string (long long val);
string to_string (unsigned val);
string to_string (unsigned long val);
string to_string (unsigned long long val);
string to_string (float val);
string to_string (double val);
string to_string (long double val);

关于函数的使用是非常简单的,示例代码如下:

#include <iostream>
#include <string>
using namespace std;

int main()
{
    string pi = "pi is " + to_string(3.1415926);
    string love = "love is " + to_string(5.20 + 13.14);
    cout << pi << endl;
    cout << love << endl;
    return 0;
}

2.字符串转化为数值

由于 C++ 中的数值类型包括整形和浮点型,因此针对于不同的类型提供了不同的函数,通过调用这些函数可以将字符串类型转换为对应的数值类型。

函数的原型如下:

// 定义于头文件 <string>
int       stoi( const std::string& str, std::size_t* pos = 0, int base = 10 );
long      stol( const std::string& str, std::size_t* pos = 0, int base = 10 );
long long stoll( const std::string& str, std::size_t* pos = 0, int base = 10 );

unsigned long      stoul( const std::string& str, std::size_t* pos = 0, int base = 10 );
unsigned long long stoull( const std::string& str, std::size_t* pos = 0, int base = 10 );

float       stof( const std::string& str, std::size_t* pos = 0 );
double      stod( const std::string& str, std::size_t* pos = 0 );
long double stold( const std::string& str, std::size_t* pos = 0 );

str:要转换的字符串
pos:传出参数,记录从哪个字符开始无法继续进行解析,比如: 123abc, 传出的位置为 3
base:若 base 为 0 ,则自动检测数值进制:若前缀为 0 ,则为八进制,若前缀为 0x 或 0X,则为十六进制,否则为十进制。

这些函数虽然都有多个参数,但是除去第一个参数外其他都有默认值,一般情况下使用默认值就能满足需求。关于函数的使用也给大家提供了一个例子,示例代码如下:

#include <iostream>
#include <string>
using namespace std;
int main()
{
    string str1 = "45";
    string str2 = "3.14159";
    string str3 = "9527 with words";
    string str4 = "words and 2";

    int myint1 = std::stoi(str1);
    float myint2 = std::stof(str2);
    int myint3 = std::stoi(str3);
    // 错误: 'std::invalid_argument'
    // int myint4 = std::stoi(str4);

    cout << "std::stoi(\"" << str1 << "\") is " << myint1 << endl;
    cout << "std::stof(\"" << str2 << "\") is " << myint2 << endl;
    cout << "std::stoi(\"" << str3 << "\") is " << myint3 << endl;
    // cout << "std::stoi(\"" << str4 << "\") is " << myint4 << endl;
    
    return 0;
}

示例代码输入的结果如下:

std::stoi("45") is 45
std::stof("3.14159") is 3.14159
std::stoi("9527 with words") is 9527

从上述测试程序可以得出这样的结论,在 C++11 提供的这些转换函数将字符串转换为数值的过程中:

如果字符串中所有字符都是数值类型,整个字符串会被转换为对应的数值,并通过返回值返回
如果字符串的前半部分字符是数值类型,后半部不是,那么前半部分会被转换为对应的数值,并通过返回值返回
如果字符第一个字符不是数值类型转换失败

文章作者: 苏丙榅
文章链接: https://subingwen.com/cpp/convert/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙!

上一篇:工信部信发司在进博会论坛全面解读《“工业互联网+安全生产”行动计划(2021-2023年)》(全文)


下一篇:2021牛客寒假算法基础集训营1-补题