现代C++语言核心特性解析part1

第1章 新基础类型(C++11~C++20)

1.1 整数类型long long

C++标准中定义,long long是一个至少为64位的整数类型。C++标准还为其定义LL和ULL作为这两种类型的字面量后缀。要强调的是,字面量后缀并不是没有意义的,在某些场合下我们必须用到它才能让代码的逻辑正确,比如下面的代码:

    long long x1 = 65536 << 16;
    std::cout << "x1 = " << x1 << std::endl;

    long long x2 = 65536LL << 16;
    std::cout << "x2 = " << x2 << std::endl;

    //x1 = 0
    //x2 = 4294967296

这里的65536被当作32位整型操作,在左移16位以后,这个32位整型的值变成了0。在计算x2的过程中,代码给65536添加了字面量后缀LL,这使编译器将其编译为一个64位整型,左移16位后仍然可以获得正确的结果:4294967296(0x100000000)。

和其他整型一样,long long也能运用于枚举类型和位域,例如:

enum longlong_enum : long long {
	x1,
	x2
};
struct longlong_struct {
	long long x1 : 8;
	long long x2 : 24;
	long long x3 : 32;
};
std::cout << sizeof(longlong_enum::x1) << std::endl; // 输出大小为8
std::cout << sizeof(longlong_struct) << std::endl; // 输出大小为8

尽量少使用宏,使用numeric_limits类模板获得整形的大小限制:

#include 
#include 
#include 
int main(int argc, char *argv[])
{
// 使用宏方法
std::cout << "LLONG_MAX = " << LLONG_MAX << std::endl;
std::cout << "LLONG_MIN = " << LLONG_MIN << std::endl;
std::cout << "ULLONG_MAX = " << ULLONG_MAX << std::endl;
// 使用类模板方法
std::cout << "std::numeric_limits::max() = "
<< std::numeric_limits<long long>::max() << std::endl;
std::cout << "std::numeric_limits::min() = "
<< std::numeric_limits<long long>::min() << std::endl;
std::cout << "std::numeric_limits::max() = "
<< std::numeric_limits<unsigned long long>::max() << std::endl;
// 使用printf打印输出
std::printf("LLONG_MAX = %lld\n", LLONG_MAX);
std::printf("LLONG_MIN = %lld\n", LLONG_MIN);
std::printf("ULLONG_MAX = %llu\n", ULLONG_MAX);
}

输出结果:
LLONG_MAX = 9223372036854775807
LLONG_MIN = -9223372036854775808
ULLONG_MAX = 18446744073709551615
std::numeric_limits<long long>::max() = 9223372036854775807
std::numeric_limits<long long>::min() = -9223372036854775808
std::numeric_limits<unsigned long long>::max() = 18446744073709551615
LLONG_MAX = 9223372036854775807
LLONG_MIN = -9223372036854775808
ULLONG_MAX = 18446744073709551615

1.2 新字符类型char16_t和char32_t

在C++11标准中添加两种新的字符类型char16_t和char32_t,它们分别用来对应Unicode字符集的UTF-16和UTF-32两种编码方法。

1.2.1 字符集和编码方法

UTF-32是最简单的编码方法,该方法用一个32位的内存空间(也就是4字节)存储一个字符编码,由于Unicode字符集的最大个数为0x10FFFF(ISO 10646),因此4字节的空间完全能够容纳任何一个字符编码。
UTF-16编码方法所需的内存空间从32位缩小到16位(占用2字节),但是由于存储空间的缩小,因此UTF-16最多只能支持0xFFFF个字符,这显然不太够用,于是UTF-16采用了一种特殊的方法来表达无法表示的字符。简单来说,从0x0000~0xD7FF以及0xE000~0xFFFF直接映射到Unicode字符集,而剩下的0xD800~0xDFFF则用于映射0x10000~0x10FFFF的Unicode字符集,映射方法为:字符编码减去0x10000后剩下的20比特位分为高位和低位,高10位的映射范围为0xD800~0xDBFF,低10位的映射范围为0xDC00~0xDFFF。
最后说一下我们最常用的UTF-8编码方法,它是一种可变长度的编码方法。由于UTF-8编码方法只占用8比特位(1字节),因此要表达完数量高达0x10FFFF的字符集,它采用了一种前缀编码的方法。这个方
法可以用1~4字节表示字符个数为0x10FFFF的Unicode(ISO 10646)字符集。为了尽量节约空间,常用的字符通常用1~2字节就能表达,其他的字符才会用到3~4字节,所以在内存空间可以使用UTF-8,但是计算字符串长度和查找字符在UTF-8中却是一个令人头痛的问题。

1.2.2 使用新字符类型char16_t和char32_t

使用char处理UTF-8, char16_t处理UTF-16, char32_t处理UTF-32。除此之外,C++11标准还为3种编码提供了新前缀用于声明3种编码字符和字符串的字面量,它们分别是UTF-8的前缀u8、UTF-16的前缀u和UTF-32的前缀U:

char utf8c = u8'a'; // C++17标准
//char utf8c = u8'好';
char16_t utf16c = u'好';
char32_t utf32c = U'好';
char utf8[] = u8"你好世界";
char16_t utf16[] = u"你好世界";
char32_t utf32[] = U"你好世界";

char utf8c = u8’a’在C++11标准中实际上是无法编译成功的,因为在C++11标准中u8只能作为字符串字面量的前缀,而无法作为字符的前缀。这个问题直到C++17标准才得以解决,所以上述代码需要C++17的环境来执行编译。
char utf8c = u8’好’是无法通过编译的,因为存储“好”需要3字节,显然utf8c只能存储1字节,所以会编译失败。

1.2.3 wchar_t存在的问题

在C++98的标准中提供了一个wchar_t字符类型,并且还提供了前缀L,用它表示一个宽字符。
起初在定义
wchar_t时并没有规定其占用内存的大小。于是就给了实现者充分的自由,以至于在Windows上wchar_t是一个16位长度的类型(2字节),而在Linux和macOS上wchar_t却是32位的(4字节)。这导致了一个严重的后果,我们写出的代码无法在不同平台上保持相同行为。

1.2.4 新字符串连接

字符串连接的规则:如果两个字符串字面量具有相同的前缀,则生成的连接字符串字面量也具有该前缀。如果其中一个字符串字面量没有前缀,则将其视为与另一个字符串字面量具有相同前缀的字符串字面量,其他的连接行为由具体实现者定义。
另外,这里的连接操作是编译时的行为,而不是一个转换。例如连接"\xA" “B"的结果应该是”\nB"(换行符和字符B),而不是一个字符"\xAB"。

1.2.5 库对新字符类型的支持

C11在中增加了4个字符的转换函数,包括:

size_t mbrtoc16( char16_t* pc16, const char* s, size_t n, mbstate_t* ps );
size_t c16rtomb( char* s, char16_t c16, mbstate_t* ps );
size_t mbrtoc32( char32_t* pc32, const char* s, size_t n, mbstate_t* ps );
size_t c32rtomb( char* s, char32_t c32, mbstate_t* ps );

//它们的功能分别是多字节字符和UTF-16编码字符互转,以及多字节字符和UTF-32编码字符互转。
//在C++11中,我们可以通过包含来使用这4个函数。

1.3 char8_t字符类型

C++20标准新引入的类型char8_t,它可以代替char作为UTF-8的字符类型。char8_t具有和unsigned char相同的符号属性、存储大小、对齐方式以及整数转换等级。引入char8_t类型后,在C++17环境下可以编译的UTF-8字符相关的代码会出现问题,例如:

char str[] = u8"text"; // C++17编译成功;C++20编译失败,需要char8_t
char c = u8'c';

char8_t c8a[] = "text"; // C++20编译失败,需要char
char8_t c8 = 'c';

在 C++17 中,可以使用 u8 字符串文字初始化 char 的数组。 在 C++20 中,此初始化格式不正确,并导致编译器错误 C2440。 这种行为可能是一个破坏源的变化。

第2章 内联和嵌套命名空间(C++11~C++20)

2.1 内联命名空间的定义和使用

开发一个大型工程必然会有很多开发人员的参与,也会引入很多第三方库,这导致程序中偶尔会碰到同名函数和类型,造成编译冲突的问题。为了缓解该问题对开发的影响,我们需要合理使用命名空间:

namespace S1 {
	void foo() {}
}
namespace S2 {
	void foo() {}
}
using namespace S1;
int main()
{
	foo();
	S2::foo();
}

C++11标准增强了命名空间的特性,提出了内联命名空间的概念。内联命名空间能够把空间内函数和类型导出到父命名空间中,这样即使不指定子命名空间也可以使用其空间内的函数和类型了,比如:

#include 
namespace Parent {
	namespace Child1
	{
		void foo() { std::cout << "Child1::foo()" << std::endl; }
	}
	
	inline namespace Child2
	{
		void foo() { std::cout << "Child2::foo()" << std::endl; }
	}
}

int main()
{
	Parent::Child1::foo();
	Parent::foo();
}

结果:
Child1::foo()
Child2::foo()

该特性可以帮助库作者无缝升级库代码,让客户不用修改任何代码也能够自由选择新老库代码。

#include 
//升级前:
namespace Parent {
	void foo() { std::cout << "foo v1.0" << std::endl; }
}

//升级后:
namespace Parent {
	namespace V1 {
		void foo() { std::cout << "foo v1.0" << std::endl; }
	}
	inline namespace V2 {
		void foo() { std::cout << "foo v2.0" << std::endl; }
	}
}

int main()
{
	Parent::foo();
}

虽然foo函数从V1升级到了V2,但是客户的代码并不需要任何修改。如果用户还想使用V1版本的函数,则只需要统一添加函数版本的命名空间,比如Parent::V1::foo()。
请注意,示例代码中只能有一个内联命名空间,否则编译时会造成二义性问题,编译器不知道使用哪个内联命名空间的foo函数。

2.2 嵌套命名空间的简化语法

C++17标准允许使用一种更简洁的形式描述嵌套命名空间,例如:

namespace A::B::C {
	int foo() { return 5; }
}

以上代码等同于:

namespace A {
	namespace B {
		namespace C {
			int foo() { return 5; }
		}	
	}
}

另外有些遗憾的是,在C++17标准中没有办法简洁地定义内联命名空间,这个问题直到C++20标准才得以解决。在C++20中,我们可以这样定义内联命名空间:

namespace A::B::inline C {
	int foo() { return 5; }
}
// 或者
namespace A::inline B::C {
	int foo() { return 5; }
}

它们分别等同于:

namespace A::B {
	inline namespace C {
		int foo() { return 5; }
	}
}
namespace A {
	inline namespace B {
		namespace C {
			int foo() { return 5; }
		}
	}
}

请注意,inline可以出现在除第一个namespace之外的任意namespace之前。

你可能感兴趣的:(c++)