在这里插入图片描述

Ⅰ. for_each

​ 这个之前我们讲过了,其实本质上就是迭代器的封装!这里就不细讲了,只要注意一点:使用基于范围的 for 循环, 其 for 循环迭代的范围必须是可确定的

void func(int arr[]) // 形参中的数组不是数组,而是指针变量,存放数组的地址,无法确定元素个数
{
    // ❌使用基于范围的for循环,其for循环迭代的范围必须是可确定的
    for (int& tmp : arr)
        std::cout << tmp << " ";
}

int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    func(arr);//传过去的是arr数组的地址
    return 0;
}

Ⅱ. 静态断言 static_assert

C++中的静态断言 static_assert 是一个编译时断言,用于在编译期间检查某个条件是否为真。如果条件为假,则会导致编译错误,否则不会产生任何代码。

​ 静态断言的语法如下:

static_assert(condition, optional message);

​ 其中,condition 是一个表达式,如果表达式的结果为 false,编译器会报错;optional message 是可选的,用于提供编译器报错时的错误信息。

注意: 只能是常量表达式,不能是变量

​ 下面是一个使用静态断言的示例:

#include <type_traits>

template <class T>
void do_something(T value)
{
    // 如果T不是整数类型,编译器会在这里报错
    static_assert(std::is_integral<T>::value, "T must be an integral type");
    
    // 此处省略具体的代码实现
    ...
}

int main()
{
    do_something(42); // 正常调用
    do_something("hello"); // 编译错误:T must be an integral type
    return 0;
}

​ 在上面的示例中,do_something 函数使用了静态断言来确保传入的类型 T 是整数类型。如果 T 不是整数类型,编译器会在函数体内部报错,从而避免运行时错误。

静态断言的好处

  • 更早的报告错误, 我们知道构建是早于运行的, 更早的错误报告意味着开发成本的降低。
  • 减少运行时调用堆栈开销, 静态断言是编译期检测的, 减少了运行时开销 。

Ⅲ. 异常处理 noexcept

一、异常处理回顾

C++ 中的异常是一种用于处理程序运行过程中出现错误的机制。当程序执行过程中出现错误时,可以通过抛出异常来告诉程序的调用者发生了错误,然后通过捕获异常来处理错误。

​ 下面是一个使用异常处理的示例:

#include <iostream>
#include <stdexcept>
void divide(int x, int y) 
{
	if (y == 0)
		throw std::invalid_argument("divide by zero");  // 抛出异常

	std::cout << "result = " << x / y << std::endl;
}

int main() 
{
    try {
    	divide(10, 0);  // 调用可能抛出异常的函数
    } catch (const std::exception& e) {
    	std::cerr << "Exception caught: " << e.what() << std::endl;  // 捕获异常并处理
    }
    return 0;
}

// 运行结果
Exception caught: divide by zero

​ 在上面的示例中,divide() 函数用于计算两个数的商,如果除数为 0,则抛出一个 std::invalid_argument 类型的异常,然后被 catch 语句捕获并处理,输出错误信息。如果除数不为 0divide() 函数会正常执行,输出结果。

​ 需要注意的是,抛出异常会使程序跳出当前函数,并沿着函数调用链向上查找 try-catch 语句,直到找到能够处理该异常的 catch 语句

​ 如果没有找到能够处理该异常的 catch 语句,则程序会异常终止。因此,在使用异常处理时,应该确保抛出异常的地方能够被 catch 语句捕获,并且 不要滥用异常处理,因为在一些性能要求较高的场景下,异常处理可能会影响程序的性能,应该谨慎使用。

首先强调一点:

  1. noexcept 作为 标识符 时,它的作用是在函数后面 声明一个函数是否会抛出异常
  2. noexcept 作为 函数 时,它的作用是 检查一个函数是否会抛出异常

二、引入 noexcept

C++11 中引入了 noexcept 标识符,用于指示函数不会抛出任何异常,如果抛出异常, 那么程序就会异常终止。也就是说 noexcept 函数执行时出了异常,程序会马上异常终止

​ 在 C++ 中,异常处理是一项昂贵的操作,因为需要构建完整的堆栈跟踪信息。所以 noexcept 主要是解决的问题是减少运行时开销,同时也可以提高代码的可靠性。运行时开销指的是编译器需要为代码生成一些额外的代码用来包裹原始代码,当出现异常时可以抛出一些相关的堆栈 stack unwinding 错误信息,这里面包含错误位置、错误原因、调用顺序和层级路径等信息。使用 noexcept 声明一个函数不会抛出异常,编译器就不会去生成这些额外的代码,直接的减小的生成文件的大小,间接的优化了程序运行效率。

​ 注意:如果函数中存在任何可能抛出异常的操作,则不应该使用 noexcept

作为标识符有几种写法:

  • noexcept:默认表示为下面的 noexcept(true)
  • noexcept(true):表示函数不可能抛出异常,如果抛异常了程序直接异常终止。
  • noexcept(false):表示函数可能会抛出异常。
    • throw():表示函数可能会抛出异常,不建议使用该写法,应该使用 noexcept(false),因为 C++20 放弃这种写法。
  • noexcept(常量表达式) :用于判断表达式是否不会抛出异常。
void example() noexcept  // noexcept相当于noexcept(true)
{
    cout << "hello called" << endl;
}

​ 在这个例子中,我们声明了一个函数 example() 并使用了 noexcept。这意味着,函数 example() 不会抛出任何异常。如果在函数运行时发生了异常,程序会被终止。

​ 另外这是 noexcept(常量表达式) 的使用方式,用于判断表达式是否不会抛出异常:

#include <iostream>
using std::cout;
using std::endl;
using std::boolalpha;

void foo() noexcept(true) 
{
	throw 4;
}

void bar() noexcept(false) 
{
	throw 4;
}

int main() 
{
	// 使用noexcept(常量表达式)判断foo()和bar()是否不会抛异常
	cout << boolalpha << noexcept(foo()) << endl;  
	cout << boolalpha << noexcept(bar()) << endl; 
	return 0;
}		

// 执行结果:
true
false

三、noexcept 函数

noexcept 函数在常规函数中配合 noexcept(expression) 标识符共同完成对其他函数是否声明了 noexcept 的检查,如下所示:

void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y)))
{
    x.swap(y);
}

​ 它表示,如果操作 x.swap(y) 不发生异常,那么函数 swap(Type& x, Type& y) 一定不发生异常。

​ 一个更好的示例是 std::pair 中的移动赋值函数,它表明,如果类型 T1T2 的移动赋值过程中不发生异常,那么该移动构造函数就不会发生异常,如下所示:

pair& operator=(pair&& __p) noexcept(__and_<is_nothrow_move_assignable<_T1>,
                						is_nothrow_move_assignable<_T2>>::value)
{
    first = std::forward<first_type>(__p.first);
    second = std::forward<second_type>(__p.second);
    return *this;
}

四、什么时候该使用 noexcept❓

​ 使用 noexcept 表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上 noexcept 就能提高效率,步子迈大了也容易扯着蛋!以下情形鼓励使用 noexcept

  • 移动构造函数
  • 移动赋值函数
  • 析构函数。这里提一句,在新版本的编译器中,析构函数是默认加上关键字 noexcept 的。
  • 叶子函数。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。

​ 比如下面代码可以检测编译器是否给析构函数加上关键字 noexcept

struct X
{
    ~X() { };
};

int main()
{
    X x;
    static_assert(noexcept(x.~X()), "Ouch!");
}

五、一些大佬的建议

  1. 当对是否需要用 noexcept 有疑问时,选择不用。
  2. noexcept 只是一个优化相关的东西,不用的话并不影响代码的正确性。
  3. 通常情况下,在广泛使用 STL 容器、智能指针的现代 C++ 风格下,编译器能够推导自动生成的析构函数,移动构造和赋值运算符的 noexcept 属性。
  4. noexcept 判断比较复杂,业务代码程序员更关注业务逻辑本身,而且需求变化大,代码可能很复杂,人工判断很容易出错。
  5. 影响接口的灵活性,比如基类某个虚函数设置为 noexcept,派生类覆盖虚函数时也必须遵守,这个有些情况下难以保证。
  6. 用错了危害很大,会强行终止程序,本来能处理的都没有处理机会了。
  7. 就像异常规格的存在版本问题一样,如果一个函数从 noexcept 变为 noexcept(false),调用处可能也需要跟着改动。
  8. C++17 后,noexcept 还影响了函数的签名,进一步影响了代码的复杂性和兼容性。

Ⅳ. 强类型枚举(枚举类)

C++11 中引入了强类型枚举(strongly-typed enum),也称为枚举类enum class),用于替代传统的 C++ 枚举类型

​ 传统枚举类型的值是整数类型,并且在枚举作用域中具有全局可见性,所以 传统枚举类型容易造成命名冲突和类型不安全 等问题。而强类型枚举使用类的语法定义枚举类型,可以避免这些问题,并提供更好的类型安全性和可读性。

​ 🎏强类型枚举的定义方式很简单,只需要在 enum 后加上使用 classstruct

enum class EnumName : underlying_type { 
    enumerator1, 
    enumerator2, 
    ... 
};

​ 其中,EnumName 为枚举类型的名称;underlying_type 为枚举类型的底层数据类型(默认为int);enumerator1, enumerator2 等为枚举常量,用逗号分隔。枚举常量的类型为枚举类型本身,也就是说,枚举常量在枚举类型作用域内具有唯一性和可见性

​ 通常情况下,可以使用 intunsigned intshortunsigned shortlongunsigned long 等整数类型作为底层数据类型,也可以使用 charsigned charunsigned char 等字符类型作为底层数据类型。

​ 但是 不能使用浮点数类型、字符串类型等【非整型类型】作为枚举常量的类型

​ 下面是传统的枚举类型与 C++11 提供的枚举类的区别:

enum Old {Yes, No}; 	    // old style
enum class New {Yes, No};   // new style
enum struct New2 {Yes, No}; // new style

使用不同的底层数据类型会影响强类型枚举常量的取值范围和存储大小

​ 例如,使用 char 类型作为底层数据类型,枚举常量的取值范围为 -128127,存储大小为 1 字节;而使用 int 类型作为底层数据类型,枚举常量的取值范围为整型的取值范围,存储大小为 4 字节。

​ 下面是 C++11 枚举类的定义与使用:

#include <iostream>
using namespace std;

enum class Color : char { Red, Green, Blue };
enum struct Animal : int { Dog, Pig, Cat };
//enum struct Food : double { Rice, HotDog, Hamburger }; // ❌double不是合法的枚举类型

int main()
{
	//Color c = Red; // ❌错误的定义方法,必须要指明Red的作用域
	Color c = Color::Red;
	cout << "The color is red." << endl;
	cout << "this color type is " << typeid(c).name() << endl;
	cout << "this color size is " << sizeof(c) << endl << endl;

	Animal a = Animal::Pig;
	cout << "The animal is pig." << endl;
	cout << "this animal type is " << typeid(a).name() << endl;
	cout << "this animal size is " << sizeof(a) << endl;
	return 0;
}

// 运行结果
The color is red.
this color type is enum Color
this color size is 1

The animal is pig.
this animal type is enum Animal
this animal size is 4

Ⅴ. 常量表达式 constexpr

一、常量表达式

​ 在讲 constexper 之前我们需要了解一下常量表达式(constant expression),常量表达式就是由至少一个常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。

​ 实际开发中,我们经常会用到常量表达式。以定义数组为例,数组的长度就必须是一个常量表达式:

int url[10];    // 正确

int url[6 + 4]; // 正确

int length = 6;
int url[length]; // 错误,length是变量

二、引入 constexpr

​ 我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这三个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而 常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。

​ 对于用 C++ 编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢❓❓❓

​ 除了人为判定外,C++11 标准还提供有 constexpr 关键字。

constexpr 关键字的功能是 使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。在 C++11 标准中,constexpr 可用于修饰【普通变量】、【函数】、【模板函数】以及【类的构造函数】。

​ 注意:获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算的!

​ 比如下面是一个使用常量表达式的示例,计算一个长度为 10 的斐波那契数列:

constexpr int fib(int n) // constexpr修饰fib函数使得其在编译期间就进行计算结果!
{
    return (n <= 1) ? n : fib(n - 1) + fib(n - 2);
}

int main() 
{
    int a[10] = { fib(0), fib(1), fib(2), fib(3), fib(4), fib(5), fib(6), fib(7), fib(8), fib(9) };
    for (int i = 0; i < 10; ++i) 
        std::cout << a[i] << " ";
    return 0;
}

// 运行结果:
0 1 1 2 3 5 8 13 21 34

​ 在这个示例中,使用 constexpr 关键字定义了一个递归函数 fib(),用于计算斐波那契数列的第 n 项。在 main 函数中,使用 fib() 函数初始化了一个长度为 10 的数组 a,并将其输出到标准输出。由于 fib() 函数是一个常量表达式,因此在编译时就可以计算出数组 a 的值,不需要在运行时重新计算。

使用 constexpr 的好处:

  • 为一些不能修改数据的场景提供保障,写出变量则就有被意外修改的风险!
  • 有些场景,编译器可以在编译时将 constexpr 函数或变量直接计算出结果,并在程序运行前就将这些值放到程序中,提高效率
  • 相比宏来说,没有额外的开销,还更加的安全可靠

三、修饰普通变量

​ 在 C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。值得一提的是,使用 constexpr 修改普通变量时,变量必须经过初始化,并且 初始值必须是一个常量表达式。举个例子:

#include <iostream>
using namespace std;

int main()
{
	constexpr int a = 10 + 20;  // 正确

	int tmp = 30;
	constexpr int b = tmp + 40; // ❌因为tmp不是常量
	constexpr int c = a + 50;	// 正确,因为a是常量
	return 0;
}

​ 注意:constconstexpr 并不相同,具体的区别下面会讲!

​ 另外需要重点提出的是,当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++11 标准规定,浮点常量表达式在编译阶段计算的精度要大于等于运行阶段计算出的精度

四、修饰函数

constexpr 修饰函数的返回值时,该函数称为【常量表达式函数】

​ 注意:这不代表 constexpr 可以修改任意函数的返回值。

要想成为常量表达式函数,必须满足如下四个条件:

  1. 整个函数的函数体中,除了 可以包含 usingtypedef 以及 static_assert 外,只能包含一条 return 返回语句

    constexpr int display(int x) {
        int ret = 1 + 2 + x;
        return ret;
    }
    

    ​ 注意:这个函数是无法通过编译的,因为该函数的返回值用 constexpr 修饰,但函数内部包含多条语句。

    ​ 如下是正确的定义 display() 常量表达式函数的写法:

    constexpr int display(int x) {
        // 可以添加using执行、typedef语句以及static_assert断言,且该函数的函数体中只包含一个return语句。
        return 1 + 2 + x;
    }
    
  2. 该函数必须有返回值,即函数的返回值类型不能是 void

  3. 常量表达式函数在使用前,必须要有该函数的定义。我们知道,函数的使用分为【声明】和【定义】两部分的,普通的函数调用只需要提前写好该函数的声明部分即可,但常量表达式函数在使用前,必须要有该函数的定义。

    #include <iostream>
    using namespace std;
    
    // 普通函数的声明
    int noconst_dis(int x);
    
    // 常量表达式函数的定义
    constexpr int display(int x) 
    {
    	return 1 + 2 + x;
    }
    
    int main()
    {
    	// 调用常量表达式函数
    	int a[display(3)] = { 1,2,3,4 };
    	cout << a[2] << endl;
    
    	// 调用普通函数
    	cout << noconst_dis(3) << endl;
    	return 0;
    }
    
    // 普通函数的定义
    int noconst_dis(int x)
    {
    	return 1 + 2 + x;
    }
    

    ​ 如果 display() 函数的定义放在 main 函数下面的话,那么就会报错,可以自行测试!

  4. return 返回的表达式必须是常量表达式

    #include <iostream>
    using namespace std;
    
    int num = 3;
    constexpr int display(int x)
    {
        return num + x;
    }
    
    int main()
    {
        // 调用常量表达式函数
        int a[display(3)] = { 1,2,3,4 };
        return 0;
    }
    
    // 执行结果:
    error C2131: 表达式的计算结果不是常数
    

    ​ 常量表达式函数的返回值必须是常量表达式的原因很简单,如果想在程序编译阶段获得某个函数返回的常量,则该函数的 return 语句中就不能包含程序运行阶段才能确定值的变量。

五、修饰构造函数

​ 构造函数不能使用 const 进行修饰,但是 字面值常量类 的构造函数可以用 constexpr

常量表达式的构造函数有以下限制:

  • 构造函数体必须为空,即所有的成员变量都要通过初始化列表初始化。
  • 必须使用常量给构造函数传参
  • 除了析构函数以外,其它成员函数也可以使用 constexpr 修饰(有些编译器不强制必须使用)。
  • C++11 标准中,不支持用 constexpr 修饰带有 virtual 的成员方法。
class Date 
{
public:
    // constexpr修饰构造函数,构造函数体必须为空
    constexpr Date(int year, int month, int day)
        : year_(year)
        , month_(month)
        , day_(day) 
    {
        // 构造函数体必须为空,那么不能使用赋值初始化,而是使用参数列表初始化
    }

    ~Date() {}
	
    // 也可以不设为constexpr
    int getYear() {
        return year_;
    }
    constexpr int getMonth() {
        return month_;
    }
    constexpr int getDay() {
        return day_;
    }
private:
    int year_;
    int month_;
    int day_;
};

int main() 
{
    Date date(2022, 9, 18);  // 必须使用常量给构造函数传参
    std::cout << date.getYear() << std::endl;
    std::cout << date.getMonth() << std::endl;
    std::cout << date.getDay() << std::endl;
    return 0;
}

// 运行结果
2022
9
18

六、修饰模板函数

C++11 语法中,constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。

​ 针对这种情况 C++11 标准规定:如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数

#include <iostream>
using namespace std;

struct myType 
{
	const char* name;
	int age;
};

// 模板函数
template<class T>
constexpr T dispaly(T t) 
{
	return t;
}

int main()
{
	struct myType stu { "zhangsan", 10 };

	// 普通函数
	struct myType ret = dispaly(stu);
	cout << ret.name << " " << ret.age << endl;

	// 常量表达式函数
	constexpr int ret1 = dispaly(10);
	cout << ret1 << endl;
	return 0;
}

// 执行结果:
zhangsan 10
10

可以看到,示例程序中定义了一个模板函数 display(),但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求:

  • 22 行代码处,当模板函数中以自定义结构体 myType 类型进行实例化时,由于该结构体中没有定义常量表达式构造函数,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的;
  • 26 行代码处,模板函数的类型 Tint 类型,实例化后的函数符合常量表达式函数的要求,所以该函数的返回值就是一个常量表达式。

七、constexpr 与 const 的区别

​ 我们知道,constexprC++ 11 标准新添加的关键字,在此之前只有 const 关键字,其在实际使用中经常会表现出两种不同的语义。举个例子:

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

void dis_1(const int x)
{
    // ❌x是只读的变量,不表示常量
    array <int,x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

void dis_2()
{
    // 正确,x既是一个只读变量,又是值5的常量
    const int x = 5;
    array <int,x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

int main()
{
   dis_1(5);
   dis_2();
}

​ 可以看到,dis_1()dis_2() 函数中都包含一个 const int x,但 dis_1() 函数中的 x 无法完成初始化 array 容器的任务,而 dis_2() 函数中的 x 却可以。

​ 这是因为,dis_1() 函数中的 const int x 只是想强调 x 是一个只读的变量,其本质仍为变量,无法用来初始化 array 容器;而 dis_2() 函数中的 const int x,表明 x 是一个只读变量的同时,x 还是一个值为 5 的常量,所以可以用来初始化 array 容器。

​ 为了解决 const 关键字的双重语义问题,保留了 const 表示 “只读” 的语义,而将 “常量” 的语义划分给了新添加的 constexpr 关键字。

​ 因此在 C++11 标准中,建议将 constconstexpr 的功能区分开,即 凡是表达 只读 语义使用 const,表达 常量 语义使用 constexpr。也就是说在上面的实例程序中,dis_2() 函数中使用 const int x 是不规范的,应使用 constexpr 关键字!

​ 有人可能会问,“只读” 不就意味着其不能被修改吗?答案是否定的,“只读” 和 “不允许被修改” 之间并没有必然的联系,举个例子:

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& con_b = a;
	cout << con_b << endl;

	a = 20;
	cout << con_b << endl;
}

// 执行结果:
10 
20

​ 可以看到,程序中用 const 修饰了 con_b 变量,表示该变量“只读”,即无法通过变量自身去修改自己的值。但这并不意味着 con_b 的值不能借助其它变量间接改变,通过改变 a 的值就可以使 con_b 的值发生变化。

此外,它们之间的主要区别在于

  • const 变量的初始化可以推迟到【运行时】进行,而 constexpr 变量必须在【编译时】进行初始化。
  • const 可以定义【编译期常量】和【运行期常量】,而 constexpr 只能定义【编译期常量】。
  • 通过上面的规则可以推出:
    • 所有的 constexpr 变量都是 const 属性的,但是 const 变量并不是 constexpr 属性的。
  • const 可以用于任何类型的变量或表达式,而 constexpr 只能用于内置类型、引用和指针类型以及符合特定要求的自定义类型。

Ⅵ. 用户定义字面量(自定义后缀)

​ 用户定义字面量,也被称为【自定义后缀】,其主要作用就是 为了简化代码的读写

​ 它的格式如下:

返回值类型 operator"" 自定义后缀名称(参数列表)
{
    函数体
}

​ 下面我们来看个例子:

#include <iostream>
using namespace std;

// 用户自定义字面值, 或者叫“自定义后缀”更直观些, 主要作用是简化代码的读写。
// 自定义变量,名字要求operator"" xxx

long double operator"" _mm(long double x) {
    return x / 1000;
}

long double operator"" _m(long double x) {
    return x;
}

long double operator"" _km(long double x) {
    return x * 1000;
}

int main()
{
    cout << 1.0_mm << endl; // 0.001
    cout << 1.0_m << endl;  // 1
    cout << 1.0_km << endl; // 1000
    return 0;
}

// 运行结果
0.001
1
1000

​ 这种新语法其实很容易理解:#include 之后的三行代码定义了一个用户自定义的新的类型的操作符 operator””,称为 字面量操作符 literal operator。在这个例子中,这个运算符能够转换相应的长度单位,例如,1 mm = 10^-3 m,1 km = 10^3 m,而 1 m = 1 m。因此,我们的操作符就可以自动计算每个长度单位是多少米。

注意 operater "" 与【自定义后缀名称】之间必须有空格,并且 后缀建议以下划线开始

下面是参数的规则:

  1. 如果字面量为 整形数,操作符函数 只接受 unsigned long long 或者 const char* 作为参数,当 unsigned long long 无法容纳该字面量的时候,编译器会自动将该字面量转化为以 \0 结尾的字符串,并调用以 const char* 为参数的版本进行处理。

  2. 如果字面量为 浮点数,操作符函数 只接受 long double 或者 const char* 为参数const char* 版本调用方式与整形一样,在当 long double 无法容纳时调用。

  3. 如果字面量为 字符串,操作符函数 只接受 (const char*, size_ t)为参数

  4. 如果字面量为 字符,操作符函数 只接受一个 char 为参数

下面是字面量操作符的参数的数量和类型的限制,根据 C++ 11 标准,只有下面这些签名是合法的

// 根据 C++ 11 标准, 只有下面参数列表才是合法的:
/*
    char const *  // 也是可以写出const char*,但是注意它们不是表示字符串类型,而是原始字面量操作符
    unsigned long long
    long double
    char const *, size_t  // 注意下面四个都是配套使用的!
    wchar_t const *, size_t
    char16_t const *, size_t
    char32_t const *, size_t
*/

要注意上面列出的第一个签名 char const* 不要同字符串相混淆,它应该被称为 原始字面量 raw literal 操作符,而不是字符串类型!例如:

char const* operator"" _r(char const* s) { return s; }

int main()
{
    cout << 250_r << endl; // ✔
    cout << "liren"_r << endl; // ❌字符串不是原始字面量
    return 0;
}

// 运行结果
250

​ 此外,最后四个签名对于字符串相当有用,因为 第二个参数会自动推导成字符串的长度。例如:

size_t operator"" _len(char const*, size_t len) 
{ 
    return len; 
}

int main()
{
    cout << "lirendada"_len << endl;
    return 0;
}

// 运行结果
9

字面量的返回值并没有被严格限定。我们完全可以提供相容类型的返回值。例如:

string operator"" _rs(char const* s)
{
    return 'x' + string(s) + 'y';
}

int main()
{
    cout << 520_rs << endl;
    return 0;
}

// 运行结果
x520y

Ⅶ. 原始字符串/原生字面值

​ 原始字符串很容易理解,即 不进行转义的完整字符串

​ 平时使用 '\n' 去做转译,而在 **C++11**之后, 出现了原始字符串,定义方式为:

R"xxx(原始字符串)xxx"

​ 表示括号里的字符串是原始的字符串,不需要做转译,其中()两边的字符串可以省略,但是 当存在一边有字符串时,两边字符串都必须相同

​ 下面举个例子:

#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;

    return 0;
}

// 运行结果
D:helloworld    est.text
D:\hello\world\test.text
D:\hello\world\test.text

​ 可以很明显的发现,上面的 str 如果不使用转义字符的话,那么 \ 会和相对应的字符变成转义字符或者是独自当作换行分割标志。

​ 如果像 str1 一样使用双反斜杠 \\ 则可以避免这个问题,但是如果我们每次都要去加两个反斜杠,这样子可读性和方便性就下降了,所以就有了原始字符串字面值,也就是 str2

str2 中使用原始字面量,也就是括号内的字符串,我们写什么,就显示什么,所见即所得

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

#include<iostream>
#include<string>
using namespace std;
int main() 
{
    cout << R"(c++11 \n  
    fighting!    \n)" << endl;

    string str = R"(love is beautiful,are you love me?
    yes,i can not!\n)";
    cout << str << endl;

    return 0;
}

// 运行结果
c++11 \n
    fighting!    \n
love is beautiful,are you love me?
    yes,i can not!\n

在这里插入图片描述

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐