第6章 提高性能和操作硬件的能力

6.1 常量表达式

6.1.1 运行时常量 与 编译时常量

#define GetConst 1     // 编译时常量
const int nNum = 100;  // 编译时常量
const int GetConst() { return 1; }
int main(){	
	int arr[GetConst()] = { 1 };//无法通过编译
	enum {
        e1 = GetConst() //无法通过编译
	};
	int cond = 0;
	switch (cond){
		case GetConst(): //无法通过编译
			break;
		default:
			break;
	}
}

C++ 11 针对编译时期常量新增了 常量表达式 constexpr 关键字。则解决以上GetConst 函数在编译时不通过问题可以如下解决:

constexpr int GetConst(){ return 1;}

6.1.2 常量表达式函数

常量表达式函数约定

    1. 函数体只有一个return 返回语句
    1. 函数必须有返回值,不能是void
    1. 使用之前必须已有定义
    1. return 返回语句表达式中不能有非常量表达式的函数、全局变量,只能是常量表达式

第一条约束的示例:
函数体内含有多条语句则不能通过编译

constexpr int data(){ const int i = 1; return i;}

但是不产生实际代码的多条语句可以通过编译

constexpr int f(int x){
static_assert(0==0,“assert fail.”);
return x;
}

第二条约束示例:

constexpr void f(){} //不可以通过编译

第三条约束示例:

constexpr int f(); // 仅仅是声明 没实现
int a=f();
const int b=f();
constexpr int c=f();//无法通过编译
constexpr int f(){return 1;}
constexpr int d=f(); // 可以通过编译

第四条约束示例:

const int e(){return 1;}
constexpr int g(){return e();}
或者形如
int g=3;
constexpr int h(){return g;}

6.1.3 常量表达式值

const int i = 1;
constexpr int j = 2; // 可以仅仅作为编译时的值,不分配空间
注意:
constexptr 不能用于修饰自定义类型。如

constexptr struct MyType{int i;} // 无法通过编译
constexptr MyType mt = {0}; // 无法通过编译

C++ 11中需要定义自定义常量构造函数

struct MyType{
  constexpr MyType(int x):i(x){}   // 常量构造函数
  int i;
 };
 constexpr  MyType mt = {0}

常量构造函数约束

    1. 函数体必须为空
    1. 初始化列表只能由常量表达式来赋值。

6.1.3 常量表达式的其他应用

常量表达式是可以用于模板函数。但是由于模版中类型的不确定性,如果常量表达式的模板函数在某个实例之后,不满足常量表达式需求则constexpr被自动忽略。

struct NotLiteral{
  NotLiteral(){i=5;}
  int i;
};

NotLiteral nl;
template<typename T>constexpr T ConstExp(T t){

  return t;
}
void g(){
  NotLiteral nl;
  NotLiteral nl1=ConstExp(nl);
  constexpr NotLiteral nl2=ConstExp(nl);//无法通过编译
  constexpr int a=ConstExp(1);

}

如何实现在编译时期计算斐波那契

#include <iostream>
using namespace std;
constexpr int Fibonacci(int n){
  return(n==1)?1:((n==2)?1:Fibonacci(n-1)+Fibonacci(n-2));
}
int main(){
  int fib[]={
    Fibonacci(11),Fibonacci(12),
    Fibonacci(13),Fibonacci(14),
    Fibonacci(15),Fibonacci(16)
  };
  for(int i:fib){
    cout<<i<<endl;
  }
}

6.2 变长模版

6.2.1 变长函数

C99中的变长宏_VA_ARGS_

#include<stdio.h>
#define LOG(...){\
    fprintf(stderr,"%s: Line %d:\t",__FILE__,__LINE__);\
    fprintf(stderr,__VA_ARGS__);\
    fprintf(stderr,"\n");\
}
int main(){
    int x=3;
    LOG("x=%d",x);//D:\study\c++\c++11\__VA_ARGS__.cpp: Line 10:    x=3
}

普通函数的变长,必须第一个函数 告诉有几个参数

#include<stdio.h>
#include<stdarg.h>

double SumOfFloat(int count,...){
    va_list ap;
    double sum=0;
    va_start(ap,count);//使得ap初始化为参数列表的handle
    for(int i=0;i<count;i++)
        sum+=va_arg(ap,double);//每次读取sizeof(double)的字节
    va_end(ap);
    return sum;
}
int main(){
    printf("%f\n",SumOfFloat(3,1.2f,3.4,5.6));//10.200000
}

C++ 11中用 initializer_list作为函数形参

#include<iostream>
#include<initializer_list>
using namespace std;
double SumOfFloat(const initializer_list<double>& l){
    double sum=0;
    for(auto &val:l)
        sum+=val;
    return sum;
}
int main(){
    printf("%f\n",SumOfFloat({1.2f,3.4,5.6}));//10.200000
}

变长模板

#include<iostream>
template<typename... T>double SumOfFloat(T...);//模板函数声明

template<typename ...Args>//递归定义
double SumOfFloat(double a,Args... args){
    return a+SumOfFloat(args...);
}

double SumOfFloat(double a){return a;}//边界条件

int main(){
    printf("%f\n",SumOfFloat(1.2f,3.4,5.6));//10.200000
}	

6.2.2 变长模版:模板参数包和函数参数包

C++11中模板参数有4种:

  • 类型的
  • 非类型的
  • 模板类型的
  • 模板参数包

模板参数包又可以细分为3种:

  • 类型的模板参数包
  • 非类型的模板参数包
  • 模板类型的模板参数包

6.2.2.1 模版参数包

模板参数包是一种pack,下面我们从模板推导角度来解释这种pack:

(类型的)模板参数包

template <typename T1,typename T2>class B{};
template <typename... A>class Template: private B<A...>{};
Template<X,Y> xy;

上面中,<typename…A>中A是 (类型的)模板参数包,它可以接收任意多个类型参数作为模板参数,具体来说,Template<X,Y>会将A推导为X和Y类型的pack。
B<A…>中A…是一种包扩展,它是模板参数包unpack的结果。由于A被推导为X和Y的pack,所以A…就被具体解释为X,Y,然后具体化为B<X,Y>。
如果我们使用Template<X,Y,Z> xyz就会引发推导错误,没有任何一个模板适配,这是因为此时A…被解释为3个类型:X,Y,Z,它无法和B匹配。

(非类型的)模板参数包

template<int i,long j,unsigned int k>class B{};
template<int ...A> struct Pack: private B<A...>{};
Pack<1,0,2> data; 

<int … A>中A是 (非类型的)模板参数包,它可以接收分离多个非类型参数作为模板参数,具体来说,Pack<1,0,2>会将A推导为整值1,0,2的pack,而B<A…>中A…是一种包扩展,由于A推导为整值1,0,2的pack,所以A…被具体解释为1,0,2,然后具体化为B<1,0,2>

(模板类型的)模板参数包

template <typename T> class A;
template <typename T> class B;
template<template<typename> class T1,template<typename> class T2> class C{};
template<template<typename> class ...T> struct Pack:private C<T...>{};
Pack<A,B> data;

< t e m p l a t e < t y p e n a m e > c l a s s . . . T > \color{red}{<template<typename> class ...T>} <template<typename>class...T> T \color{red}{T} T是 (模板类型的)模板参数包,它可以接收多个模板作为模板参数,具体来说,Pack<A,B>会将T推导为A和B的pack,而C<T…>中T…就是一种包扩展,由于T推导为A和B的pack,所以T…就被具体解释为A,B,然后具体化为C<A,B>

6.2.2.2 三个简单的例子

(类型的)模板参数包的使用

template<typename... Elements> class tuple;//模板声明
template <typename Head,typename... Tail>//递归定义
class tuple<Head,Tail...>:private tuple<Tail...>{
    Head head;
};
template<> class tuple<>{};//边界条件

同样也是递归定义,这种递归的设计就是变长模板最晦涩的地方。
当实例化tuple<double,int,char,float>类时,
第一次:Head被推导为double,Tail…被推导为int,char,float
第二次:Head被推导为int,Tail…被推导为char,float
第三次:Head被推导为char,Tail…被推导为float
第三次:Head被推导为float,Tail…被推导为空
最后由class tuple<>进行递归构造出模板

(非类型的)模板参数包的使用

#include<iostream>
using namespace std;

template<long... nums> struct Multiply;//模板声明

template<long first,long... last>//递归定义
struct Multiply<first,last...>
{
    static const long val=first*Multiply<last...>::val;
};
template <>//边界条件
struct Multiply<>
{
    static const long val=1;
};
int main()
{
    cout<<Multiply<2,3,4,5>::val<<endl;
    cout<<Multiply<22,44,66,88,9>::val<<endl;
}

上面这种编程方式,叫做模板元编程,他将乘法的计算放到模板推导过程中,就是把计算过程放在编译阶段,这样运行时就不需要计算了

(模板类型的)模板参数包的使用

template <typename T> class Module1{};
template <typename T> class Module2{};

template<typename I,template<typename>class ... B>struct Container;//模板声明
template<typename I,template<typename> class A,template<typename> class... B>
struct Container<I,A,B...>//递归定义
{
    A<I> a;
    Container<I,B...> b;
};
template<typename I> struct Container<I>{};//边界条件

int main()
{
    Container<int,Module1,Module2> a;
}

6.2.2.3 函数参数包

函数参数包是变长模版函数中的一个概念,也是一种pack类型变量

void g(int,char,double);
template<typename ... T>  // T是(类型的)模板参数包
void f(T... args)  // T... 叫包扩展, args是一种类型为T...的变量叫 函数参数包
{
    g(args...); // args 是包扩展,将args unpack后的产物
}
f(1,'c',1.2);  // 将T 推导为 int char  doubule的pack  args的值是1,'c',1.2的pack

变长模板函数的示例 printf()

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

void Printf(const char*s)//边界条件
{
    while(*s)
    {
        if(*s=='%' && *++s!='%')//确保`%%`不出现
            throw runtime_error("invalid format string: missing arguments");
        cout<<*s++;
    }
}
template<typename T,typename ...Args>//递归定义
void Printf(const char*s,T value,Args... args)
{
    while(*s)
    {
        if(*s=='%' && *++s!='%')//确保`%%`不出现
        {
            cout<<value;
            return Printf(++s,args...);
        }
        cout<<*s++;
    }
    throw runtime_error("extra arguments provided to Printf");
}
int main()
{
    Printf("hello %s\n",(string)"world");
}

6.2.2.4 包扩展的进阶

…符号可以放在意想不到的地方,例如:

template<typename... A> class T:private B<A>...{};//#1
template<typename... A> class T:private B<A...>{};//#2

对于实例化T<X,Y>,#1会被解释为
class T<X,Y> class T:private B,private B{};
#2会被解释为
class T<X,Y> class T:private B<X,Y>{};

示例:

#include<iostream>
using namespace std;

template<typename... T>
void DummyWrapper(T... t){};

template<typename T>
T pr(T t){
    cout<<t;
    return t;
}
template<typename... A>
void VTPrint(A... a)
{
    DummyWrapper(pr(a)...);
}
int main()
{
    VTPrint(1,", ",1.2,", abc\n");
}

结果:

, abc
1.2, 1

6.2.2.5 sizeof…() 的使用

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

template<typename... A>
void Print(A... arg){
    assert(false);
}

void Print(int a1,int a2,int a3,int a4,int a5,int a6){
    cout<<a1<<", "<<a2<<", "<<a3<<", "<<a4<<", "<<a5<<", "<<a6<<endl;
}

template<class... A>int Vaargs(A... args){
    int size=sizeof...(args);//或者sizeof...(A)
    switch (size){
        case 0:Print(99,99,99,99,99,99);
                break;
        case 1:Print(99,99,args...,99,99,99);
                break;
        case 2:Print(99,99,args...,99,99);
                break;
        case 3:Print(args...,99,99,99);
                break;
        case 4:Print(99,args...,99);
            break;
        case 5:Print(99,args...);
            break;
        case 6:Print(args...);
            break;
        default:
            Print(0,0,0,0,0,0);
    }
}
int main()
{
    Vaargs();//99, 99, 99, 99, 99, 99
    Vaargs(1);//99, 99, 1, 99, 99, 99 
    Vaargs(1,2);//99, 99, 1, 2, 99, 99
    Vaargs(1,2,3);//1, 2, 3, 99, 99, 99  
    Vaargs(1,2,3,4);//99, 1, 2, 3, 4, 99   
    Vaargs(1,2,3,4,5);//99, 1, 2, 3, 4, 5 
    Vaargs(1,2,3,4,5,6);//1, 2, 3, 4, 5, 6  
    Vaargs(1,2,3,4,5,6,7);//0, 0, 0, 0, 0, 0 
}

6.2.2.6 变长模板和完美转发的配合

#include<iostream>
using namespace std;

struct A
{
    A(){};
    A(const A&a){cout<<"Copy Constructed "<<__func__<<endl;}
    A(A&& a){cout<<"Move Constructed "<<__func__<<endl;}
};

struct B
{
    B(){};
    B(const B&b){cout<<"Copy Constructed "<<__func__<<endl;}
    B(B&& b){cout<<"Move Constructed "<<__func__<<endl;}
};

template<typename... T> struct  MultiTypes;//模板声明

template<typename T1,typename... T>//递归定义
struct MultiTypes<T1,T...>: public MultiTypes<T...>
{
    T1 t1;
    MultiTypes<T1,T...>(T1 a,T... b):t1(a),MultiTypes<T...>(b...)
    {
        cout<<"MultiTypes<T1,T...>(T1 a,T... b)"<<endl;
    }
};

template<> struct MultiTypes<>//边界条件
{
    MultiTypes<>(){cout<<"MultiTypes<>()"<<endl;}
};

template<template<typename...> class VariadicType,typename... Args>
VariadicType<Args...> Build(Args&& ... args)
{
    return VariadicType<Args...>(std::forward<Args>(args)...);
}

int main()
{
    A a;
    B b;
    Build<MultiTypes>(a,b);
    //等价于Build<MultiTypes,A,B>(a,b);
}

6.3 原子类型与原子操作

6.3.1 原子类型

原子操作:多线程程序中“最小的且不可并行化”的操作。
如果对一个共享资源进行原子操作的情况下,有且仅有一个线程对该资源进行操作。

Linux 环境使用 POSIX 标准的 pthread 库实现多线程下的原子操作:

#include <pthread.h>
#include <iostream>
using namespace std;
int64_t total=0;
pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;

//线程函数,用于累加
void* threadFunc(void* args) {
	int64_t endNum=*(int64_t*)args;
	for(int64_t i=1;i<=endNum;++i) {
		pthread_mutex_lock(&m);
		total+=i;	
		pthread_mutex_unlock(&m);
	}
}
int main() {
        int64_t endNum=100;
        pthread_t thread1ID=0,thread2ID=0;
        // 创建线程1
        pthread_create(&thread1ID,NULL,threadFunc,&endNum);
        // 创建线程2
        pthread_create(&thread2ID,NULL,threadFunc,&endNum);
        // 阻塞等待线程1结束并回收资源
        pthread_join(thread1ID,NULL);
        // 阻塞等待线程2结束并回收资源
        pthread_join(thread2ID,NULL);
        cout<<"total="<<total<<endl;	//10100
}

上面的代码,两个线程同时对 total 进行操作,为了保证total+=i 的原子性,采用互斥锁来保证同一时刻只有同一线程执行total+=i操作,所以得出正确结果total=10100。如果没有做互斥处理,那么 total 同一时刻可能会被两个线程同时操作,即会出现两个线程同时读取了寄存器中的 total 值,分别操作之后又写入寄存器,这样就会有一个线程的增加操作无效,会得出一个小于 10100 随机的错误值。

C++11 进行原子操作:
在 C++11 之前,使用第三方 API 可以实现并行编程,比如 pthread 多线程库,但是在使用时需要创建互斥锁,以及进行加锁、解锁等操作来保证多线程对临界资源的原子操作,这无疑增加了开发的工作量。不过从 C++11 开始,C++ 从语言层面开始支持并行编程,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作等各种类。新标准极大地提高了程序的可移植性,以前的多线程依赖于具体的平台,而现在有了统一的接口。

#include <atomic>
#include <thread>
#include <iostream>
using namespace std;
atomic_int64_t total = 0;       //atomic_int64_t相当于int64_t,但是本身就拥有原子性
//线程函数,用于累加
void threadFunc(int64_t endNum)
{
	for (int64_t i = 1; i <= endNum; ++i)
	{
		total += i;
	}
}
int main()
{
	int64_t endNum = 100;
	thread t1(threadFunc, endNum);
	thread t2(threadFunc, endNum);
	t1.join();
	t2.join();
	cout << "total=" << total << endl;    //10100
}

在C++11的并行程序中,使用原子类型是非常容易的。事实上,由于C++11与C11标准都支持原子类型,因此我们可以简单地通过#include<cstdatomic>头文件中来使用对应于内置类型的原子类型定义

原子类型名称对应内置类型
atomic_bool bool
atomic_charatomic_char
atomic_charsigned char
atomic_ucharunsigned char
atomic_shortshort
atomic_ushortunsigned short
atomic_intint
atomic_uintunsigned int
atomic_long
atomic_ulongunsigned long
atomic_llonglong long
atomic_ullongunsigned long long
atomic_ullongunsigned long long
atomic_char16_tchar16_t
atomic_char32_tchar32_t
atomic_wchar_twchar_t

6.3.2 原子操作

原子类型不允许进行拷贝构造、移动构造以及使用operator=

atomic af{1.2f}
atomic af2{af} // 编译不过,不允许拷贝构造

有一个比较特殊的原子类型是 atomic_flag,因为 atomic_flag 与其他原子类型不同,它是无锁(lock_free)的,即线程对其访问不需要加锁,而其他的原子类型不一定是无锁的。
atomic_flag 只支持 test_and_set() 以及 clear() 两个成员函数

  • test_and_set()函数检查 std::atomic_flag 标志,如果 std::atomic_flag 之前没有被设置过,则设置 std::atomic_flag 的标志;如果之前 std::atomic_flag 已被设置,则返回 true,否则返回 false。
  • clear()函数清除 std::atomic_flag 标志使得下一次调用
#include <unistd.h>
#include <atomic>
#include <thread>
#include <iostream>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void func1(){
	while (lock.test_and_set(std::memory_order_acquire))    // 在主线程中设置为true,需要等待t2线程clear
    {
        std::cout << "func1 wait" << std::endl;
    }
    std::cout << "func1 do something" << std::endl;
}
void func2(){
    std::cout << "func2 start" << std::endl;
    lock.clear();
}
int main(){
    lock.test_and_set();             // 设置状态
    std::thread t1(func1);
    usleep(1);					 	//睡眠1us
    std::thread t2(func2);

    t1.join();
    t2.join();

    return 0;
}

可以通过Lock()和UnLock()的方式来互斥地访问临界区。

void Lock(atomic_flag& lock){ while ( lock.test_and_set()); }
void UnLock(atomic_flag& lock){ lock.clear(); }

6.3.2 内存模型:强顺序与弱顺序

以下汇编语言

1: Load    reg3, 1;           // 将立即数1放入寄存器reg3
2: Move    reg4,reg3;         // 将reg3的数据放入reg4
3: Store   reg4, a;           // 将reg4的数据存入内存地址a
4: Load    reg5, 2;           // 将立即数2放入寄存器reg5
5: Store   reg5, b;           // 将reg5的数据存入内存地址b

汇编语言翻译的是如下C++代码

atomic<int> a{0};
atomic<int> b{0};
//线程函数
int valueSet(int)
{
	int t=1;
	a.store(t);
	b.store(2);
}

如果原子类型变量a和b并没有要求执行的顺序性,那么可以采用一种松散的内存模型来放松对原子操作的执行顺序的要求。改造如下:

void func1()
{
	int t=t;
    a.store(t, std::memory_order_relaxed);
    b.store(2, std::memory_order_relaxed);
}

上面的代码使用了store函数进行赋值,store函数接受两个参数,第一个是要写入的值,第二个是名为memory_order的枚举值。这里使用了std::memory_order_relaxed,表示松散内存顺序,该枚举值代表编译器可以任由编译器重新排序或则由处理器乱序处理。这样a和b的赋值执行顺序性就被解除了。在C++11中一共有7种memory_order枚举值,默认按照memory_order_seq_cst执行:

枚举值定义规则
memory_order_relaxed不对执行顺序做任何保障
memory_order_acquire本线程中,所有后续的读操作均在本条原子操作完成后执行
memory_order_release本线程中,所有之前的写操作完成后才能执行本条原子操作
memory_order_acq_rel同时包含memory_order_acquire和memory_order_release标记
memory_order_consume本线程中,所有后续的有关本原子类型的操作,必须在本条原子操作完成后执行
memory_order_seq_cst全部存取都按顺序执行

需要注意的是,不是所有的memory_order都能被atomic成员使用:

  • (1)store函数可以使用memory_order_seq_cst、memory_order_release、memory_order_relaxed。
  • (2)load函数可以使用memory_order_seq_cst、memory_order_acquire、memory_order_consume、memory_order_relaxed。
  • (3)需要同时读写的操作,例如test_and_flag、exchange等操作。可以使用memory_order_seq_cst、memory_order_release、memory_order_acquire、memory_order_consume、memory_order_relaxed。

原子类型提供的一些操作符,比如operator=、operator+=等函数,都是以memory_order_seq_cst为memory_order的实参的原子操作封装,所以他们都是顺序一致性的。如果要指定内存顺序的话,则应该采用store、load、atomic_fetch_add这样的版本。

最后说明一下,std::atomic和std::memory_order只有在多线程无锁编程时才会用到。在x86_64平台,由于是强顺序内存模型的,为了保险起见,不要使用std::memory_order,使用std::atmoic默认形式即可,因为std::atmoic默认是强顺序内存模型。

6.4 线程局部存储

概念引入:进程是资源分配的最小单位,线程除了自身需要的一些栈、寄存器等资源,其余的资源都是和其他线程共享的,这样就会存在资源竞争和线程安全问题。

线程局部存储:一种特殊的内存机制,将数据存储在当前线程的私有内存中,线程就有自己私有的数据存储空间,不会被其他线程干扰。

C语言早期支持定义线程局部变量

pthread_key_create(&key, NULL);
pthread_setspecific(key, value);
pthread_getspecific(key);

C++11 之后支持并统一了定义线程局部变量的方式 thread_local

thread_local int gVal_0 = 180;
thread_local static int gVal_1 = 100;

变量加上 thread_local之后在每个线程中都会存在一份独立的副本,互不干扰。

哪些变量可以被声明为thread_local

    1. 命名空间下的全局变量
    1. 类的static成员变量
    1. 局部变量

全局变量 示例:

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印, 加锁指示为了方便多线程打印
thread_local int x = 1;
void thread_func(const std::string&thread_name){
    for(int i = 0; i < 50; i++){
        x++;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: x = " << x << std::endl;
    }
}
int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

6.5 快速退出

C++11 中引入了quick_exit 函数,该函数并不执行函数析构函数,只是使程序终止。quick_exit与exit 同属于正常退出。程序正常终止,不会完全清理资源。

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

struct A {
  ~A() {
    cout << "Destructor A. " << endl;
  }
};

void closeDevice() {
  cout << "device is closed" << endl;
}

int main() {
  A a;
  at_quick_exit(closeDevice);
  quick_exit();
}

输出为:

device is closed // a的析构函数不会被调用。

C++中存在正常退出和异常退出两种情况,其中异常退出主要是对于发生异常而未被捕获时产生,此时会调用terminate,而terminate默认行为是调用abort函数终止程序,当然也可以通过set_terminate修改默认行为。

对于abort而言,不会调用任何析构函数,而是抛出一个信号终止程序,这可能会导致与之交互的其他进程进入交互“中间态”,而产生一些问题。

而exit则是正常退出,会正常调用自动变量的析构函数,以及atexit注册的函数(反顺序调用)。

当然exit来自与C,在C++中类可能零散的分布于堆上,而调用exit则需要依次调用这些类的析构函数释放内存,这无疑是耗时的。而实际上,这些堆内存在进程结束时由操作系统统一回收会很快。因此在这种场景下我们往往需要更快速的退出。

因此,在C++11中,标准引入了quick_exit函数,该函数并不执行析构函数而只是使程序终止。与abort不同的是,abort的结果通常是异常退出(可能系统还会进行coredump等以辅助程序员进行问题分析),而quick_exit与exit同属于正常退出。此外,使用at_quick_exit注册的函数也可以在quick_exit的时候被调用。这样一来,我们同样可以像exit一样做一些清理的工作(这与很多平台上使用_exit函数直接正常退出还是有不同的)。在C++11标准中,at_quick_exit和at_exit一样,标准要求编译器至少支持32个注册函数的调用。

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值