21C++复习.新类型及方法/新关键字及用法

一、新类型及新语法

1、新的数据类型

C++ 11新增了类型long long和unsigned long long,以支持64位(或者更宽)的整型;新增了类型char16_t和char32_t,以支持16位和32位的字符表示;还新增了“原始”字符串。
示例

#include <iostream>

using namespace std;

int main()
{
    long long number;
    unsigned long long num;
    
    char16_t ch1;
    char32_t ch2;

    cout << sizeof(number) << endl;
    cout << sizeof(num) << endl;
    cout << sizeof(ch1) << endl;
    cout << sizeof(ch2) << endl;
    //输出"原始"字符串
    cout << R"(hello, \n
    			world)" << endl;
    return 0;
}

2、统一的初始化

C++11扩大了用大括号括起来的列表(初始化列表)的使用范围,使其可用于所用内置类型和用户自定义类型(即类对象)。

使用初始化列表时,可添加等号(=),也可不添加:

int x = {5}; doutble y {3.14}; 
short quar[5] {4, 5, 2, 12, 1};

需要注意的是,当用于内置类型的变量时,这种初始化形式有一个重要的特点:如果我们使用初始化且初始值存在丢失信息的风险,则编译器报错;

例如:

long double ld = 3.1414141414;
int a{ld}, b = {ld}; //报错 int c(ld), d = ld; //正确
cout << "a:" << a << "b:" << b 
     << "c:" << c << "d" << d << endl;
<font size=4>&ensp;&ensp;&ensp;&ensp;运行的时候,a,b则会提示报错信息:error: type 'long double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing],这是因为使用long double的值初始化int变量时可能会丢失数据,所以拒绝a和b的初始化请求;虽然c,d虽然没有报错,但是确实丢失了数据。
<font size=4>&ensp;&ensp;&ensp;&ensp;另外,列表初始化语法也适用于new表达式:
    int *arr = new int[4] {2, 4, 5, 7};

创建对象时,也可使用大括号(而不是圆括号)括起的列表来调用构造函数:

class Stump
{
private:
    int roots;
    double weight;
public:
    Stump(int r, double w):roots(r), weight(w){};
};
Stump s1(3, 23.4);	//old style
Stump s2{3, 23.4};      //C++ 11
Stump s3 = {4, 32.1};   //C++ 11

1、缩窄

初始化列表语法可防止缩窄,即禁止将数值赋值给无法存储它的数值变量。常规初始化允许程序员执行可能没有意义的操作:

    char ch1 = 1.57e27;//double to char,undefined behavior
    char ch2 = 45685904;//int to char,undefined behavior

然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换,即将值存储到比它窄的变量中。

    char ch1 {1.57e27};//double to char,compile-time error
    char ch2 = {45685904};//int to char,compile-time error

当允许转换为更宽的类型,另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型,也是允许的:

    char ch1 {66};//int to char , in range, allowed
    double ch2 = {66};//int to double, allowed

2、std::initializer_list

C++ 11提供了模板类initializer_list,可将其用作构造函数的参数,如果类有接受initializer_list作为参数的构造函数,则初始化列表语法就只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。STL容器提供了将initializer_list作为参数的构造函数:

    vector<int> a1(10);//uninitialized vector with 10 elements
    vector<int> a2{10};//initializer-list, a2 has 1 element set to 10
    vetcot<int> a3{4, 6, 1};//3 elements set to 4, 6, 1

头文件initializer-list提供了对模板类initializer-list的支持。这个类包含成员函数begin()和end(), 可用于获取列表的范围。除用于构造函数外,还可以将initializer-list作为常规函数的参数:

#include <initializer_list>

double sum(std::initializer_list<double> il);

int main()
{
    double total = sum({2.5, 3.1, 4});// 4 converted to 4.0
    ...
}
double sum(std::initializer_list<double> il)
{
    double tot = 0;
    for(auto p = il.begin(); p != il.end; p++)
    {
        tot += *p;
    }
    return tot;
}

二、新关键字及用法

C++11提供了很多简化声明的功能,尤其在使用模板时。

(1)auto用法

以前,关键字auto是一个存储类型说明符,C++11将其用于实现自动类型推断。则要求进行显示初始化,让编译器能够将变量的类型设置为初始值的类型:

    auto maton = 11; // maton is type int
    auto pt = &maton; //pt is type int *
    double fm(double, int);//
    auto pf = fm;//pf is type double(*)(double, int)

关键字auto还可以简化模板声明。例如,如果il是一个std::initializer_list对象,则可将下述代码:

for(std::initializer_list<double>::iterator p = il.begin();
					 p != il.end(); p++)

替换为如下代码:

for(auto p = il.begin(); p != il.end; p++)

(2)decltype用法

关键字decltype将变量的类型声明为表达式指定的类型。下面的语句的含义是,让y的类型与x相同,其中x是一个表达式:

    decltype(x) y;

下面是几个示例:

double x;
int n;
decltype(x*n) q;//q same type as x*n, i.e,double
decltype(&x) pd;//pd same type as &x, i.e,double *

这在定义模板时,特别有用,因为只有等到模板被实例化时才能确定类型:

template<typename T, typename U>
void ef(T t, U u)
{ 
    decltype(T*U) tu;
    ... ...
}

其中tu将为表达式TU的类型,这里假定定义了运算TU。例如,如果T为char,U为short,则tu将为int,这是由整型算术自动执行整型提升导致的。
    decltype的工作原理比auto复杂,根据使用的表达式,指定的类型可以为引用和const。下面是几个示例:

    int j = 3;
    int &k = j;
    const int &n = j;
    decltype(n) i1;//i1 type const int &,这里会报错,因为i1是引用必须初始化
    decltype(j) i2;//i2 type const int
    decltype((j)) i3;//i3 type int &,这里会报错,因为i1是引用必须初始化
    delctype(k + 1) i4;//i4 type int

(3)返回类型后置

C++11新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型:

    double f1(double, int);//traditional syntax
    auto f2(double, int) -> double;//new syntax, return type is double

就常规函数的可读性而言,这种新语法好像是倒退了,但让您能够使用decltype来指定模板函数的返回类型:

templete<typename T, typename U>
auto eff(T t, U u) -> decltype(T*U)
{
	... 
}

这里解决的问题是,咋编译器遇到eff的参数列表前,T和U还不在作用域内,因此必须在参数列表后使用decltype。这种新语法使得能够这样做。

(4)类型别名:using

对于冗长或复杂的标识符,如果能够创建其别名将很方便。以前,C++为此提供了typedef:

    typedef std::vector<std::string>::iterator itType;

C++11提供了另一种创建别名的语法:

    using itType = std::vector<std::string>::iterator;

两者差别在于,新语法也可用于模板部分具体化,当typedef不能:

    template<tepename T>
        using arr12 = std::array<T, int>;//template for multiple aliases

上述语句具体化模板array<T, int>(将参数int设置文12)。例如对于下述声明:

    std::array<double, 12> a1;
    std::array<std::string, 12> a2;

可将它们替换为如下声明:

    arr12<double> a1;
    arr12<std::string> a2;

(5)nullptr用法

nullptr表示空指针,引入nullptr的原因,这个要从NULL说起。对于C和C++程序员来说,一定不会对NULL感到陌生。但是C和C++中的NULL却不等价。NULL表示指针不指向任何对象,但是问题在于,NULL不是关键字,而只是一个宏定义(macro)。

NULL在C中的定义
    在C中,习惯将NULL定义为void *指针值为0:

    #define NULL (void*)0

但同事,也允许将NULL定义为整数常数0。
NULL在C++中的定义
    在C++中,NULL却被明确定义为整数常数0:

    // lmcons.h中定义NULL的源码   
    #ifndef NULL   
    #ifdef __cplusplus   
    #define NULL    0   
    #else   
    #define NULL    ((void *)0)   
    #endif   
    #endif  

为什么C++在NULL上选择不完全兼容C
    根本原因和C++的重载函数有关。C++通过搜索匹配参数的机制,试图找到最佳匹配(best-match)的函数,而如果继续支持void *的隐式类型转换,则会带来语义二义性(syntax ambiguous)的问题。

    // 考虑下面两个重载函数   
    void foo(int i);  
    void foo(char* p)  
      
    foo(NULL); // which is called?  

nullptr的应用场景
    如果我们的编译器是支持nullptr的话,那么我们应该直接使用nullptr来替代NULL的宏定义。正常使用过程中他们是完全等价的。对于编译器,Visual Studio 2010已经开始支持C++0x中的大部分特性,自然包括nullptr。而VS2010之前的版本,都不支持此关键字。Codeblocks10.5附带的G++ 4.4.1不支持nullptr,升级为4.6.1后可支持nullptr(需开启-std=c++0x编译选项)

需要注意的是:不能将nullptr赋值给整型。

(6)constexpr变量

我们在定义常量的时候一般使用const来定义,一个常量必须在定义的时候进行初始化,并且之后不可更改。一个常量必须使用一个常量表达式进行初始化,并且在编译期间就可以得到常量的值,但是如何确定一个表达式就是常量表达式呢,这个通常是由程序员自己确定的,例如:

    const int a =20;
    //20是一个字面值,当然也是一个常量表达式,所以用20来为a赋值是没有问题的
    //然而下面的代码也可以通过编译,g++ 5.3.1
    int a = 20 ;
    const int x =  a;
    int b[x]={0};

为常量x赋值的是一个变量a,这样做应该是不合理的,但是编译器没有报告任何错误,当然这种错误是显而易见的,但是在复杂的系统中如何判断一个表达式是否是常量表达式是很困难的,例如这里的a我们一眼就可以判断其并不是一个常量表达式。为此C++11提供了一个新的关键字constexpr,使用该关键字定义的常量,由编译器检查为其赋值的表达式是否是常量表达式,例如上面的代码改成:

    int a = 20 ;
    constexpr int x =  a; 

编译器编译的时候就会报错说a并不是常量。显然constexpr关键字将常量表达式的检查转交给编译器处理,而不是程序员自己,所以使用constexpr定义常量要比const安全。

(7)constexpr函数

普通的函数一般是不能用来为constexpr常量赋值的,但是C++11允许定义一种constexpr的函数,这种函数在编译期间就可以计算出结果,这样的函数是可以用来为constexpr赋值的。定义constexpr函数需要遵守一些约定,函数的返回类型以及所有形参的类型都应该是字面值,一般情况下函数体中必须有且只有一条return语句。

    constexpr int size()
    {
        return 42;
    }
 
    constexpr int si = size();

执行初始化的时候编译器将函数的调用替换成结果值,constexpr函数体中也可以出现除了return之外的其他语句,但是这些语句在运行时不应该执行任何操作,例如空语句,using声明等。constexpr函数允许其返回值并非是一个字面值,例如:

constexpr int size(int s)
{
    return s*4;
}
 
int a = 20;
const int b = 30;
constexpr int c = 40;
constexpr int si = size(a);  //error a是一个变量所以函数返回的是一个可变的值
constexpr int si1 = size(20); //ok 函数返回的实际上是一个常量
constexpr int si2 = size(b);  //ok
constexpr int si3 = size(c);  //ok

由上可知constexpr函数并不一定返回常量,如果应用于函数的参数是一个常量表达式则返回常量,否则返回变量,而该函数调用到底是一个常量表达式还是非常量表达式则由编译器来判断。这就是constexpr的好处。

(8)for循环语法

C++11 引入了一种类似C#或java中foreach的用法。这种for语句可以很方便的遍历容器或其他序列的所有元素:
【示例1】

    int numbers[] = { 1,2,3,4,5 };
    std::cout << "numbers:" << std::endl;
    for (auto number : numbers)
    {
        std::cout << number << std::endl;
    }

【示例2】

    vector<int> vec = {1,2,3,4,5,6};
    for(int x: vec)
    {
        cout<<x<<endl;
    }

posted @ 2022-03-11 16:02  底层逻辑  阅读(229)  评论(0)    收藏  举报