C++ 学习笔记2
C++ Chapter2
异常处理
异常是程序在执行期间产生的问题。 C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。try 在不同情况下会抛出不同的异常,这时候就需要多个 catch 块语句。
1 | try |
抛出异常
您可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
1 | double division(int a, int b) |
捕获异常
catch 块跟在 try 块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。
1 | try |
如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 …
1 | try |
抛出异常并捕获的代码示例:
1 |
|
c++ 标准的异常
一系列标准的异常定义在 exception 头文件中。exception 定义了所有 C++ 异常的父类。
exception
头文件: 这个头文件定义了 C++ 异常处理的基本机制。它包含了异常类的基类 std::exception
,其他异常类都是从这个基类派生而来的。std::exception
包含了一个 what
函数,允许派生类提供一个描述异常的 C 字符串。
stdexcept
头文件: 这个头文件继承了 exception
头文件的功能,同时定义了一些标准的异常类,这些异常类派生自 std::exception
。这些异常类包括 std::logic_error
、std::runtime_error
等,它们用于在程序运行时检测到错误时抛出异常。
以下异常类需要作用域 std::
。
上述父子层次结构异常的说明如下:
异常 | 描述 |
---|---|
std::exception | 该异常是所有标准 C++ 异常的父类。 |
std::bad_alloc | 该异常可以通过 new 抛出。 |
std::bad_cast | 该异常可以通过 dynamic_cast 抛出。 |
std::bad_typeid | 该异常可以通过 typeid 抛出。 |
std::bad_exception | 这在处理 C++ 程序中无法预期的异常时非常有用。 |
std::logic_error | 理论上可以通过读取代码来检测到的异常。 |
std::domain_error | 当使用了一个无效的数学域时,会抛出该异常。 |
std::invalid_argument | 当使用了无效的参数时,会抛出该异常。 |
std::length_error | 当创建了太长的 std::string 时,会抛出该异常。 |
std::out_of_range | 该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator[]() 。 |
std::runtime_error | 理论上不可以通过读取代码来检测到的异常。 |
std::overflow_error | 当发生数学上溢时,会抛出该异常。 |
std::range_error | 当尝试存储超出范围的值时,会抛出该异常。 |
std::underflow_error | 当发生数学下溢时,会抛出该异常。 |
定义自己的异常(定义新异常)
what() 是异常(exception)类提供的一个公共方法(虚成员函数),它已被所有子异常类重载。这将返回异常产生的原因。
1 |
|
const char * what () const throw ()
中的 throw() 是异常规格说明, 表示 what 函数可以抛出异常的类型,类型说明放到 () 里。若为空,则声明这个函数不会抛出异常;通常函数不写后面的 throw() 就表示函数可以抛出任何类型的异常。
void fun() throw(A,B,C,D);
这里表示 fun()
函数可能但不一定抛出 A、B、C、D 四种类型的异常。
C++11 引入了新的异常规范 noexcept
,用于明确指定函数是否抛出异常。
1 | void fun() noexcept; // 表示该函数不抛出异常 |
动态内存
使用 new 分配,delete 释放。
new type[10] 分配数组,delete [] name 释放数组内存。
C++ 程序中的内存分为两个部分:
- 栈:在函数内部声明的所有变量都将占用栈内存。
- 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
若内存的大小需要在运行时才能确定,则可以可以使用 new
运算符为给定类型的变量在运行时分配堆内存,这会返回所分配的空间地址。
new 和 delete 运算符
new 的使用语法: new data-type;
1 | double* pvalue = NULL; // 初始化为 null 的指针 |
若是自由存储区(堆内存)已被用完,则可能无法成功分配内存。
new 返回 NULL 指针代表内存分配失败。可进行如下检查:
1 | double* pvalue = NULL; |
delete 的使用语法:delete pvalue;
当您觉得某个已经动态分配内存的变量不再需要使用时,您可以使用 delete 操作符释放它所占用的内存
小结:
如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:
- delete ptr – 代表用来释放内存,且只用来释放ptr指向的内存。
- delete[] rg – 用来释放rg指向的内存,!!还逐一调用数组中每个对象的 destructor!!
数组的动态内存分配
为一个字符数组(一个有 20 个字符的字符串)分配内存,我们可以使用上面实例中的语法来为数组动态地分配内存,如下:
1 | char* pvalue = NULL; // 初始化为 null 的指针 |
释放动态数组内存:
1 | delete [] pvalue; // 删除 pvalue 所指向的数组 |
各维度数组分配:
一维数组
1
2
3
4
5// 动态分配,数组长度为 m
int *array=new int [m];
//释放内存
delete [] array;二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14int **array;
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
array[i] = new int [n];
}
//释放
for( int i=0; i<m; i++ )
{
delete [] array[i];
}
delete [] array;三维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int ***array;
// 假定数组第一维为 m, 第二维为 n, 第三维为h
// 动态分配空间
array = new int **[m];
for( int i=0; i<m; i++ )
{
array[i] = new int *[n];
for( int j=0; j<n; j++ )
{
array[i][j] = new int [h];
}
}
//释放
for( int i=0; i<m; i++ )
{
for( int j=0; j<n; j++ )
{
delete[] array[i][j];
}
delete[] array[i];
}
delete[] array;
对象的动态内存分配
如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。
1 |
|
malloc 和 new 的区别
malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。
new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。
new
和 malloc
都用于在堆上动态分配内存,但它们之间有一些关键的区别,涉及到语法、功能和适用情景。以下是它们之间的主要区别:
语法和类型安全性:
new
是 C++ 的运算符,而不仅仅是一个函数。它使用类的构造函数来初始化分配的内存,因此是类型安全的。malloc
是 C 的标准库函数,它只分配一定大小的未初始化内存块,不调用构造函数,因此不提供类型安全性。
1
2
3
4
5
6// 使用 new
int* arr = new int[5]; // 分配一个 int 数组
MyClass* obj = new MyClass(); // 分配一个 MyClass 对象
// 使用 malloc
int* arr = (int*)malloc(5 * sizeof(int)); // 分配一块未初始化的内存构造函数和析构函数的调用:
new
在分配内存后会调用类的构造函数,因此适用于类类型。malloc
只是分配一块内存,并不会调用任何构造函数,适用于内置类型和结构体。
返回类型:
new
返回正确类型的指针,不需要进行强制类型转换。malloc
返回void*
,需要手动进行类型转换。
1
2
3
4
5// 使用 new
int* arr = new int[5]; // 不需要显式类型转换
// 使用 malloc
int* arr = (int*)malloc(5 * sizeof(int)); // 需要显式类型转换大小信息:
new
知道要分配的是什么类型,因此不需要显式指定大小。malloc
需要显式指定要分配的字节数。
1
2
3
4
5// 使用 new
int* arr = new int[5]; // 不需要显式指定大小
// 使用 malloc
int* arr = (int*)malloc(5 * sizeof(int)); // 需要显式指定大小free 和 delete:
malloc
和calloc
配合使用free
来释放内存。new
配合使用delete
来释放内存。使用delete[]
释放数组内存。
1
2
3
4
5
6
7// 使用 malloc 和 free
int* arr = (int*)malloc(5 * sizeof(int));
free(arr);
// 使用 new 和 delete
int* arr = new int[5];
delete[] arr;
总体来说,如果在 C++ 中使用动态内存分配,推荐使用 new
和 delete
,因为它们提供了更好的类型安全性和更方便的内存管理。如果在 C 代码中使用,或者需要与 C 代码兼容,可以使用 malloc
和 free
。在现代 C++ 中,使用 new
和 delete
的情况更为普遍。
命名空间
命名空间作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
定义命名空间
命名空间的定义使用关键字 namespace,后跟命名空间的名称
1 | namespace namespace_name { |
调用带有命名空间的函数或变量,需要在前面加上命名空间的名称:
1 | namespace_name::code; // code 可以是变量或函数 |
using 指令
使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
1 |
|
using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分(其他 std 命名空间的代码依然要加 std:: 作用域),您可以使用如下的语句:
1 | using std::cout; |
名称从使用 using 指令开始是可见的,直到该范围结束。此时,在范围以外定义的同名实体是隐藏的。
不连续的命名空间
也就是同一个命名空间分布在不同的文件中。
下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:
1 | namespace namespace_name { |
嵌套的命名空间
1 | namespace namespace_name1 { |
访问嵌套命名空间的方式如下:
1 | // 访问 namespace_name2 中的成员 |
关于命名空间冲突
::a
表示访问全局变量。
当定义了一个全局变量,并且使用了 using 包含了一个命名空间,该命名空间也有一个与全局变量同名的变量,此时会产生冲突(编译时错误)。
但是当有一个与命名空间和全局变量都同名的局部变量,则不会冲突,会优先使用局部变量。
1 |
|
模板
函数模板 和 类模板
函数模板
函数模板定义的一般形式如下:
1 | template <typename type> |
type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
函数模板代码示例:
1 |
|
类模板
显式模板、隐式模板什么的。。。
对于每个类中成员函数在外部定义时,定义前都要加上类模板的声明(并且要加上类的模板类型,如
template <class T> void Stack<T>::pop ()
)。
类模板泛型类声明的一般形式:
1 | template <class type> |
类模板代码示例如下:
1 |
|
预处理器
预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。
预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。
所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。
预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。
#define 预处理
#define 预处理指令用于创建符号常量。该符号常量通常称为宏,指令的一般形式是:
1 |
上述代码代表将 macro-name 用 replacement-text 全部替换。
使用示例如下:
1 |
|
使用 -E 选项进行编译,并把结果重定向到 test.p。现在,如果您查看 test.p 文件,将会看到它已经包含大量的信息,而且在文件底部的值被改为如下:
也就是在预处理阶段将所有宏在源码中完成替换。
1 | $ gcc -E test.cpp > test.p |
参数宏
示例:
1 |
|
条件编译
#ifdef、#ifndef、#if、#endif 等
有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。
1 |
|
#
和 ##
运算符
#
运算符会把 replacement-text 令牌转换为用引号引起来的字符串。
- 宏定义参数的左右两边的空格会被忽略,参数的各个 Token 之间的多个空格会被转换成一个空格。
- 宏定义参数中含有需要特殊含义字符如”或\时,它们前面会自动被加上转义字符 \。
下列代码将参数 HELLO C++
作为 C 风格字符串进行输出。
1 |
|
##
运算符用于连接两个令牌。
将多个 Token 连接成一个 Token。要点:
- 它不能是宏定义中的第一个或最后一个 Token。
- 前后的空格可有可无。
下列代码将参数 x
和 y
连接在了一起,也就是 xy
,而我们宏定义是将 concat(a, b)
替换为 x ## y
,也就是 xy
。
1 |
|
预定义宏
宏 | 描述 |
---|---|
__LINE__ |
这会在程序编译时包含当前行号。 |
__FILE__ |
这会在程序编译时包含当前文件名。 |
__DATE__ |
这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。 |
__TIME__ |
这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。 |
信号处理
信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断。
有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal>
中。
信号 | 描述 |
---|---|
SIGABRT | 程序的异常终止,如调用 abort。 |
SIGFPE | 错误的算术运算,比如除以零或导致溢出的操作。 |
SIGILL | 检测非法指令。 |
SIGINT | 程序终止(interrupt)信号。 |
SIGSEGV | 非法访问内存。 |
SIGTERM | 发送到程序的终止请求。 |
signal() 函数
signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:
1 | void (*signal (int sig, void (*func)(int)))(int); |
也就是:
第一个参数是要设置的信号的标识符。
第二个参数是指向信号处理函数的指针。
函数返回值是一个指向先前信号处理函数的指针。
如果先前没有设置信号处理函数,则返回值为 SIG_DFL。如果先前设置的信号处理函数为 SIG_IGN,则返回值为 SIG_IGN。
1 | signal(registered signal, signal handler) |
signal 使用实例:
1 |
|
使用 ctrl + C 中断程序,则结果如下:
1 | Going to sleep.... |
raise() 函数
函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:
1 | int raise (signal sig); |
sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。
代码示例如下:
1 |
|
进程与线程
基于进程和基于线程。
- 基于进程的多任务处理是程序的并发执行。
- 基于线程的多任务处理是同一程序的片段的并发执行。
进程
fork()
使用 fork()
函数创建子进程,返回值分为两种情况:
- 在父进程中,也就是调用
fork()
的进程,返回值为子进程 pid 号; - 在子进程中,也就是被
fork()
创建的进程,返回值为 0。
wait()
#include <sys/wait.h>
pid_t wait(int* statloc);
wait()
函数用于向创建子进程的父进程传递子进程的 exit 参数值 或 return 语句的返回值,避免僵尸进程。
参数:整型变量指针,用于保存子进程返回值(可如下分离)。
- WIFEXITED(statloc):子进程正常终止返回 true。
- WEXITSTATUS(statloc):返回子进程返回值。
返回值:成功返回终止的子进程 id,失败返回 -1。
waitpid()
wait 可能会引起程序阻塞,waitpid 可避免阻塞。
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* statloc, int options);
参数:options
- 0: 默认行为,等待任意子进程结束。
- WNOHANG: 如果没有子进程已经退出,立即返回,不阻塞。如果指定了这个选项,
waitpid
将立即返回,不管是否有子进程退出,父进程可以继续执行其他任务。 - WUNTRACED: 除了已经退出的子进程之外,还会返回因信号而停止的子进程信息。这对于检测子进程是否被暂停是有用的。
- WCONTINUED: 返回那些曾经停止过并已经继续的子进程信息。
这些选项提供了对等待子进程的不同方式的控制。通过组合这些选项,可以实现更灵活的等待子进程的策略。例如,可以使用
WNOHANG | WUNTRACED
来非阻塞地等待任何已经停止或退出的子进程。在调用
waitpid
时,可以通过检查status
参数的值来获取子进程的退出状态。如果子进程正常退出,可以使用WIFEXITED(status)
来检查,然后通过WEXITSTATUS(status)
获取退出码。如果子进程被信号终止,可以使用WIFSIGNALED(status)
来检查,然后通过WTERMSIG(status)
获取信号编号。其他宏函数还包括WIFSTOPPED
和WSTOPSIG
,用于检查子进程是否停止。返回值:成功返回终止的子进程id,失败返回 -1。当返回值为 0 时,表示使用了
WNOHANG
选项,没有已退出的子进程。
1 |
|
线程
多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。线程是在进程中单独执行流的单位。
Linux 操作系统,我们要使用 POSIX 编写多线程 C++ 程序(pthread.h)。
C++ 11中提供了 thread 库。
线程为了保持多条代码执行流,只分离了栈区域。
上下文切换时不需要切换数据区和堆。
可以利用数据区和堆交换数据。
pthread_create()
pthread_create
函数用于创建一个新的线程,并启动该线程执行指定的函数。它是 POSIX 线程库(Pthreads)中的函数。以下是 pthread_create
的函数原型:
1 | include <pthread.h> |
thread
参数是一个指向pthread_t
类型的指针,用于存储新线程的标识符。通过这个标识符,可以对新线程进行操作,例如等待它的结束。attr
参数是一个指向pthread_attr_t
类型的指针,表示线程的属性。可以通过传递nullptr
使用默认属性。start_routine
参数是一个函数指针,指向新线程将要执行的函数。这个函数应该有如下形式:void* start_routine(void* arg)
。它接受一个void*
类型的参数,并返回一个void*
类型的指针。这个参数通常用于传递给线程的函数,用于向线程传递数据。arg
参数是传递给start_routine
函数的参数。
返回值:
- 如果成功创建线程,返回 0;
- 如果创建线程失败,返回相应的错误码。
pthread_join()
pthread_join
函数用于等待一个指定的线程终止。它会阻塞调用线程,直到指定的线程结束执行为止。该函数的原型如下:
1 | int pthread_join(pthread_t thread, void** retval); |
thread
参数是要等待的线程的标识符,即线程 ID,由pthread_create
函数返回。retval
参数是一个指向指针的指针,用于存储被等待线程的退出状态。如果不关心线程的退出状态,可以将retval
设为nullptr
。
返回值:
如果线程成功结束,返回 0;
如果线程终止时出错,返回相应的错误码。
下面是一个简单的例子,演示了 pthread_create
和 pthread_join
的使用:
1 |
|
在这个例子中,主线程创建了一个新线程,传递了一个整数参数。主线程通过 pthread_join
函数等待新线程的结束,并获取新线程的退出状态。这个例子中,线程的退出状态是一个整数的两倍。最后,主线程释放了动态分配的内存。
注意:使用 pthread_join
可以防止主线程在子线程结束之前退出。如果主线程在子线程结束之前退出,那么子线程可能会成为“孤儿线程”(orphan thread),继续在后台运行,直到它自己结束。
示例代码:
在多线程环境中,线程的执行顺序是不确定的,因此每次执行的结果可能不同。这是因为线程调度由操作系统负责,而不同的调度决策可能导致线程以不同的顺序执行。
1 |
|
线程的可连接状态和分离状态
在 POSIX 线程中,线程可以被设置为两种状态:可连接状态(joinable)和分离状态(detached)。这两种状态影响了主线程与新创建的线程之间的关系,特别是在新线程结束时的处理方式。
可连接状态(Joinable):
- 默认情况下,线程是可连接状态。
- 如果一个线程是可连接状态,那么当这个线程结束时,其状态和一些资源(如线程的退出状态)将被保留,直到其他线程通过
pthread_join
函数等待这个线程结束并获取其状态。 - 主线程可以通过
pthread_join
函数等待可连接状态的线程结束,以获取线程的退出状态。
示例代码:
1
2
3
4
5pthread_t thread;
pthread_create(&thread, nullptr, threadFunction, nullptr);
// 等待线程结束并获取退出状态
pthread_join(thread, nullptr);分离状态(Detached):
- 如果一个线程是分离状态,那么当这个线程结束时,其状态和资源将被自动释放,不需要其他线程调用
pthread_join
。 - 分离状态的线程不能被等待,主线程无法获取它的退出状态。
- 分离状态的线程通常用于执行一些后台任务,不需要与其他线程同步或等待其结束。
示例代码:
1
2
3
4
5
6
7
8
9
10pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
// 设置线程为分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&thread, &attr, threadFunction, nullptr);
// 线程结束后,资源会自动释放,无需调用pthread_join- 如果一个线程是分离状态,那么当这个线程结束时,其状态和资源将被自动释放,不需要其他线程调用
在实际应用中,选择线程的状态取决于程序的需求。如果需要获取线程的退出状态或者确保线程的资源在结束时不会泄漏,那么可连接状态可能更适合。如果线程仅执行一些后台任务,不需要主线程等待其结束,分离状态可能更合适。需要注意的是,一旦线程被设置为分离状态,就不能再被设置为可连接状态。
线程的属性
初始化和销毁:
pthread_attr_init(pthread_attr_t* attr)
:初始化线程属性。pthread_attr_destroy(pthread_attr_t* attr)
:销毁线程属性。
设置和获取线程栈大小:
pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize)
:设置线程栈的大小。pthread_attr_getstacksize(const pthread_attr_t* attr, size_t* stacksize)
:获取线程栈的大小。
设置和获取线程分离状态:
pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate)
:设置线程的分离状态。可以是PTHREAD_CREATE_JOINABLE
或PTHREAD_CREATE_DETACHED
。pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate)
:获取线程的分离状态。
设置和获取调度策略:
pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy)
:设置线程的调度策略。pthread_attr_getschedpolicy(const pthread_attr_t* attr, int* policy)
:获取线程的调度策略。
设置和获取线程优先级:
pthread_attr_setschedparam(pthread_attr_t* attr, const struct sched_param* param)
:设置线程的调度参数,包括优先级。pthread_attr_getschedparam(const pthread_attr_t* attr, struct sched_param* param)
:获取线程的调度参数。
设置属性示例:
1 |
|
pthread_detach和在pthread_create中设置线程属性为分离状态有什么区别
pthread_detach
函数和在 pthread_create
中设置线程属性为分离状态都可以用来将线程设置为分离状态,但它们的使用方式和时机有一些不同。
pthread_create
中设置线程属性为分离状态:使用
pthread_attr_setdetachstate
函数,通过线程属性设置线程的分离状态。在调用pthread_create
时,将这个属性作为参数传递给函数。1
2
3
4
5
6
7
8
9
10
11
12pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
// 设置线程为分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 创建线程时使用线程属性
pthread_create(&thread, &attr, threadFunction, nullptr);
// 线程属性销毁
pthread_attr_destroy(&attr);这样设置线程属性的好处是,可以在创建线程的同时指定其分离状态,使得线程在启动时就具备了分离属性。
pthread_detach
函数:使用
pthread_detach
函数,通过在线程创建后的任何时候将线程设置为分离状态。这个函数需要在线程创建后尽快调用,通常在新线程开始执行之前。1
2
3
4
5pthread_t thread;
pthread_create(&thread, nullptr, threadFunction, nullptr);
// 设置线程为分离状态
pthread_detach(thread);这种方式更加灵活,可以在创建线程后的任何时候设置线程为分离状态。但要注意,如果线程已经处于分离状态,再次调用
pthread_detach
将会产生未定义行为。
总体而言,两者的效果是相同的,都将线程设置为分离状态。选择使用哪种方式取决于具体的需求和代码结构。如果在创建线程时就确定了线程的分离属性,使用 pthread_create
中设置属性的方式更为直观。如果希望在线程开始执行后再将其设置为分离状态,可以使用 pthread_detach
函数。
(待)互斥锁和信号锁
C++11 thread 库
C++11引入的线程库(<thread>
头文件)为 C++ 添加了原生的多线程支持。这个标准线程库提供了一组类和函数,用于创建、管理和同步线程。下面一步一步举例详细讲解 C++11 标准线程库的主要特性。
步骤 1: 包含头文件
首先,我们需要包含 <thread>
头文件:
1 |
步骤 2: 创建线程
使用 std::thread
类来创建一个新的线程。以下是一个简单的例子:
1 | void threadFunction() { |
在这个例子中,threadFunction
是新线程执行的函数,myThread
是一个 std::thread
对象,用于表示新线程。通过调用 join
函数,主线程等待新线程执行完毕。
步骤 3: 传递参数给线程函数
可以通过构造函数为线程传递参数:
1 | void threadFunction(int value) { |
步骤 4: 使用 Lambda 表达式
可以使用 Lambda 表达式作为线程函数:
1 | int main() { |
步骤 5: 同步线程
C++11 标准库提供了一些用于同步线程的工具,例如 std::mutex
,std::lock_guard
等。以下是一个简单的例子:
1 |
|
这个例子中,使用 std::mutex
来保护共享资源,确保线程安全。
步骤 6: std::async
和 std::future
std::async
和 std::future
提供了一种异步执行函数的方式,并且可以获取函数的返回值。以下是一个简单的例子:
1 |
|
在这个例子中,std::async
异步执行 factorial
函数,然后主线程继续执行其他任务。最后,通过 result.get()
获取异步操作的结果。
以上是 C++11 引入的标准线程库的基本用法。这个库提供了丰富的功能,包括互斥量、条件变量、原子操作等,以支持更复杂的多线程应用。