C++ 学习笔记2

C++ Chapter2

异常处理

异常是程序在执行期间产生的问题。 C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。

try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。try 在不同情况下会抛出不同的异常,这时候就需要多个 catch 块语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

抛出异常

您可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型

1
2
3
4
5
6
7
8
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!"; // 抛出 const char* 类型的异常,则需要 catch(const char* err)
}
return (a/b);
}

捕获异常

catch 块跟在 try 块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的

1
2
3
4
5
6
7
try
{
// 保护代码
}catch( ExceptionName e )
{
// 处理 ExceptionName 类型的异常的代码
}

如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 …

1
2
3
4
5
6
7
try
{
// 保护代码
}catch(...)
{
// 能处理任何异常的代码
}

抛出异常并捕获的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}

int main ()
{
int x = 50;
int y = 0;
double z = 0;

try {
z = division(x, y);
cout << z << endl;
}catch (const char* msg) {
cerr << msg << endl;
}

return 0;
}

c++ 标准的异常

一系列标准的异常定义在 exception 头文件中。exception 定义了所有 C++ 异常的父类。

exception 头文件: 这个头文件定义了 C++ 异常处理的基本机制。它包含了异常类的基类 std::exception,其他异常类都是从这个基类派生而来的。std::exception 包含了一个 what 函数,允许派生类提供一个描述异常的 C 字符串。

stdexcept 头文件: 这个头文件继承了 exception 头文件的功能,同时定义了一些标准的异常类,这些异常类派生自 std::exception。这些异常类包括 std::logic_errorstd::runtime_error 等,它们用于在程序运行时检测到错误时抛出异常。

以下异常类需要作用域 std::

piBxgfS.png

上述父子层次结构异常的说明如下:

异常 描述
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <exception>
using namespace std;

struct MyException : public exception
{
const char * what () const throw ()
{
return "C++ Exception";
}
};

int main()
{
try
{
throw MyException();
}
catch(MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch(std::exception& e)
{
//其他的错误
}
}

const char * what () const throw () 中的 throw() 是异常规格说明, 表示 what 函数可以抛出异常的类型,类型说明放到 () 里。若为空,则声明这个函数不会抛出异常;通常函数不写后面的 throw() 就表示函数可以抛出任何类型的异常。

void fun() throw(A,B,C,D); 这里表示 fun() 函数可能但不一定抛出 A、B、C、D 四种类型的异常。

C++11 引入了新的异常规范 noexcept,用于明确指定函数是否抛出异常。

1
2
void fun() noexcept; // 表示该函数不抛出异常
void fun() throw(); // 这两行代码是相同作用的,都表示函数不抛出任何异常。

动态内存

使用 new 分配,delete 释放。

new type[10] 分配数组,delete [] name 释放数组内存。

C++ 程序中的内存分为两个部分:

  • 栈:在函数内部声明的所有变量都将占用栈内存。
  • 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。

若内存的大小需要在运行时才能确定,则可以可以使用 new 运算符为给定类型的变量在运行时分配堆内存,这会返回所分配的空间地址。

new 和 delete 运算符

new 的使用语法: new data-type;

1
2
double* pvalue  = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存

若是自由存储区(堆内存)已被用完,则可能无法成功分配内存。

new 返回 NULL 指针代表内存分配失败。可进行如下检查:

1
2
3
4
5
6
7
double* pvalue  = NULL;
if( !(pvalue = new double ))
{
cout << "Error: out of memory." <<endl;
exit(1);

}

delete 的使用语法:delete pvalue;

当您觉得某个已经动态分配内存的变量不再需要使用时,您可以使用 delete 操作符释放它所占用的内存

小结:

如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:

  • delete ptr – 代表用来释放内存,且只用来释放ptr指向的内存。
  • delete[] rg – 用来释放rg指向的内存,!!还逐一调用数组中每个对象的 destructor!!

数组的动态内存分配

为一个字符数组(一个有 20 个字符的字符串)分配内存,我们可以使用上面实例中的语法来为数组动态地分配内存,如下:

1
2
char* pvalue  = NULL;   // 初始化为 null 的指针
pvalue = new char[20]; // 为变量请求内存

释放动态数组内存:

1
delete [] pvalue;        // 删除 pvalue 所指向的数组

各维度数组分配:

  1. 一维数组

    1
    2
    3
    4
    5
    // 动态分配,数组长度为 m
    int *array=new int [m];

    //释放内存
    delete [] array;
  2. 二维数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int **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;
  3. 三维数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    int ***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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class Box
{
public:
Box() {
cout << "调用构造函数!" <<endl;
}
~Box() {
cout << "调用析构函数!" <<endl;
}
};

int main( )
{
Box* myBoxArray = new Box[4];

delete [] myBoxArray; // 删除数组
return 0;
}

malloc 和 new 的区别

malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。

new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。

newmalloc 都用于在堆上动态分配内存,但它们之间有一些关键的区别,涉及到语法、功能和适用情景。以下是它们之间的主要区别:

  1. 语法和类型安全性:

    • 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)); // 分配一块未初始化的内存
  2. 构造函数和析构函数的调用:

    • new 在分配内存后会调用类的构造函数,因此适用于类类型。
    • malloc 只是分配一块内存,并不会调用任何构造函数,适用于内置类型和结构体。
  3. 返回类型:

    • new 返回正确类型的指针,不需要进行强制类型转换。
    • malloc 返回 void*,需要手动进行类型转换。
    1
    2
    3
    4
    5
    // 使用 new
    int* arr = new int[5]; // 不需要显式类型转换

    // 使用 malloc
    int* arr = (int*)malloc(5 * sizeof(int)); // 需要显式类型转换
  4. 大小信息:

    • new 知道要分配的是什么类型,因此不需要显式指定大小。
    • malloc 需要显式指定要分配的字节数。
    1
    2
    3
    4
    5
    // 使用 new
    int* arr = new int[5]; // 不需要显式指定大小

    // 使用 malloc
    int* arr = (int*)malloc(5 * sizeof(int)); // 需要显式指定大小
  5. free 和 delete:

    • malloccalloc 配合使用 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++ 中使用动态内存分配,推荐使用 newdelete,因为它们提供了更好的类型安全性和更方便的内存管理。如果在 C 代码中使用,或者需要与 C 代码兼容,可以使用 mallocfree。在现代 C++ 中,使用 newdelete 的情况更为普遍。

命名空间

命名空间作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。

定义命名空间

命名空间的定义使用关键字 namespace,后跟命名空间的名称

1
2
3
4
namespace namespace_name {
// 代码声明
void code(){}
}

调用带有命名空间的函数或变量,需要在前面加上命名空间的名称:

1
namespace_name::code;  // code 可以是变量或函数

using 指令

使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space; // 即在之后的代码,调用first_space这个命名空间的代码
int main ()
{

// 调用第一个命名空间中的函数
func();

return 0;
}

using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分(其他 std 命名空间的代码依然要加 std:: 作用域),您可以使用如下的语句:

1
using std::cout;

名称从使用 using 指令开始是可见的,直到该范围结束。此时,在范围以外定义的同名实体是隐藏的。

不连续的命名空间

也就是同一个命名空间分布在不同的文件中。

下面的命名空间定义可以是定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:

1
2
3
namespace namespace_name {
// 代码声明
}

嵌套的命名空间

1
2
3
4
5
6
namespace namespace_name1 {
// 代码声明
namespace namespace_name2 {
// 代码声明
}
}

访问嵌套命名空间的方式如下:

1
2
3
4
5
// 访问 namespace_name2 中的成员
using namespace namespace_name1::namespace_name2;

// 访问 namespace_name1 中的成员
using namespace namespace_name1;

关于命名空间冲突

::a 表示访问全局变量。

当定义了一个全局变量,并且使用了 using 包含了一个命名空间,该命名空间也有一个与全局变量同名的变量,此时会产生冲突(编译时错误)。

但是当有一个与命名空间和全局变量都同名的局部变量,则不会冲突,会优先使用局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;
namespace A {
int a = 100;
namespace B //嵌套一个命名空间B
{
int a = 20;
}
}

int a = 200;//定义一个全局变量


int main(int argc, char *argv[]) {
cout << "A::a =" << A::a << endl; //A::a =100
cout << "A::B::a =" << A::B::a << endl; //A::B::a =20
cout << "a =" << a << endl; //a =200
cout << "::a =" << ::a << endl; //::a =200

using namespace A; // 与全局变量冲突
cout << "a =" << a << endl; // Reference to 'a' is ambiguous // 命名空间冲突,编译期错误
cout << "::a =" << ::a << endl; //::a =200

int a = 30; // 优先使用局部变量
cout << "a =" << a << endl; //a =30
cout << "::a =" << ::a << endl; //::a =200

//即:全局变量 a 表达为 ::a,用于当有同名的局部变量时来区别两者。

using namespace A;
cout << "a =" << a << endl; // a =30 // 当有本地同名变量后,优先使用本地,冲突解除
cout << "::a =" << ::a << endl; //::a =200


return 0;
}

模板

函数模板类模板

函数模板

函数模板定义的一般形式如下:

1
2
3
4
5
template <typename type>
ret-type func-name(parameter list)
{
// 函数的主体
}

type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。

函数模板代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>

using namespace std;

template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
int main ()
{

int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;

double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl;

string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;

return 0;
}

类模板

显式模板、隐式模板什么的。。。

对于每个类中成员函数在外部定义时,定义前都要加上类模板的声明(并且要加上类的模板类型,如 template <class T> void Stack<T>::pop ())。

类模板泛型类声明的一般形式:

1
2
3
4
5
6
template <class type>
class class-name {
.
.
.
}

类模板代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack {
private:
vector<T> elems; // 元素

public:
// Default constructor
Stack() = default;

// Constructor with an initial vector
Stack(const std::vector<T>& initialVector);

void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};

template <class T>
Stack<T>::Stack(const std::vector<T>& initialVector)
: vec(initialVector)
{
}

template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}

template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}

template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}

int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈

// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;

// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}

预处理器

预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。

预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。

所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。

预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。

#define 预处理

#define 预处理指令用于创建符号常量。该符号常量通常称为,指令的一般形式是:

1
#define macro-name replacement-text 

上述代码代表将 macro-name 用 replacement-text 全部替换。

使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

#define PI 3.14159

int main ()
{

cout << "Value of PI :" << PI << endl;

return 0;
}

使用 -E 选项进行编译,并把结果重定向到 test.p。现在,如果您查看 test.p 文件,将会看到它已经包含大量的信息,而且在文件底部的值被改为如下:

也就是在预处理阶段将所有宏在源码中完成替换。

1
2
3
4
5
6
7
8
9
10
$ gcc -E test.cpp > test.p

...
int main ()
{

cout << "Value of PI :" << 3.14159 << endl;

return 0;
}

参数宏

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

#define MIN(a,b) (a<b ? a : b)

int main ()
{
int i, j;
i = 100;
j = 30;
cout <<"较小的值为:" << MIN(i, j) << endl;

return 0;
}

条件编译

#ifdef、#ifndef、#if、#endif 等

有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
#define DEBUG

#define MIN(a,b) (((a)<(b)) ? a : b)

int main ()
{
int i, j;
i = 100;
j = 30;
#ifdef DEBUG
cerr <<"Trace: Inside main function" << endl;
#endif

#if 0
/* 这是注释部分 */
cout << MKSTR(HELLO C++) << endl;
#endif

cout <<"The minimum is " << MIN(i, j) << endl;

#ifdef DEBUG
cerr <<"Trace: Coming out of main function" << endl;
#endif
return 0;
}

### 运算符

# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串

  • 宏定义参数的左右两边的空格会被忽略,参数的各个 Token 之间的多个空格会被转换成一个空格。
  • 宏定义参数中含有需要特殊含义字符如”或\时,它们前面会自动被加上转义字符 \。

下列代码将参数 HELLO C++ 作为 C 风格字符串进行输出。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

#define MKSTR( x ) #x

int main ()
{
cout << MKSTR(HELLO C++) << endl; // C++ 预处理器转换为 cout << "HELLO C++" << endl;

return 0;
}

## 运算符用于连接两个令牌。

将多个 Token 连接成一个 Token。要点:

  • 它不能是宏定义中的第一个或最后一个 Token。
  • 前后的空格可有可无。

下列代码将参数 xy 连接在了一起,也就是 xy,而我们宏定义是将 concat(a, b) 替换为 x ## y,也就是 xy

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

#define concat(a, b) a ## b
int main()
{
int xy = 100;

cout << concat(x, y); // 预处理器转换为 cout << xy;
return 0;
}

预定义宏

描述
__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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <csignal>
#include <unistd.h>

using namespace std;

void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";

// 清理并关闭
// 终止程序

exit(signum);

}

int main ()
{
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);

while(1){
cout << "Going to sleep...." << endl;
sleep(1);
}

return 0;
}

使用 ctrl + C 中断程序,则结果如下:

1
2
3
4
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.

raise() 函数

函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:

1
int raise (signal sig);

sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <csignal>
#include <unistd.h>

using namespace std;

void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";

// 清理并关闭
// 终止程序

exit(signum);

}

int main ()
{
int i = 0;
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);

while(++i){
cout << "Going to sleep...." << endl;
if( i == 3 ){
raise( SIGINT);
}
sleep(1);
}

return 0;
}

进程与线程

基于进程和基于线程

  • 基于进程的多任务处理是程序的并发执行。
  • 基于线程的多任务处理是同一程序的片段的并发执行。

进程

fork()

使用 fork() 函数创建子进程,返回值分为两种情况:

  1. 在父进程中,也就是调用 fork() 的进程,返回值为子进程 pid 号;
  2. 在子进程中,也就是被 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) 获取信号编号。其他宏函数还包括 WIFSTOPPEDWSTOPSIG,用于检查子进程是否停止。

  • 返回值:成功返回终止的子进程id,失败返回 -1。当返回值为 0 时,表示使用了 WNOHANG 选项,没有已退出的子进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdlib>

using std::cout;
using std::endl;

int main()
{
pid_t pid = fork();
int pidnum;

if (pid == 0)
{
// 子进程
cout << "这是子进程" << endl;
exit(42);
}
else if (pid > 0)
{
// 父进程
int status;
do {
pidnum = waitpid(pid, &status, WNOHANG);
if (pidnum == -1) {
// 处理 waitpid 错误
perror("waitpid");
exit(EXIT_FAILURE);
} else if (pidnum == 0) {
// 子进程还在运行,等待一段时间再检查
sleep(1);
}
} while (pidnum == 0);

cout << "这是父进程" << endl;
if (WIFEXITED(status))
{
cout << "Successfully Exit:" << WEXITSTATUS(status) << endl;
}
}
else
{
// 处理 fork 失败的情况
cout << "fork() error" << endl;
}

return 0;
}

线程

多线程程序包含可以同时运行的两个或多个部分。这样的程序中的每个部分称为一个线程,每个线程定义了一个单独的执行路径。线程是在进程中单独执行流的单位。

Linux 操作系统,我们要使用 POSIX 编写多线程 C++ 程序(pthread.h)。

C++ 11中提供了 thread 库。

线程为了保持多条代码执行流,只分离了栈区域。

  • 上下文切换时不需要切换数据区和堆。

  • 可以利用数据区交换数据。

pthread_create()

pthread_create 函数用于创建一个新的线程,并启动该线程执行指定的函数。它是 POSIX 线程库(Pthreads)中的函数。以下是 pthread_create 的函数原型:

1
2
3
4
include <pthread.h>

int pthread_create(pthread_t* thread, const pthread_attr_t* attr,
void* (*start_routine)(void*), void* arg);
  • 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_createpthread_join 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <pthread.h>

void* threadFunction(void* arg) {
int value = *((int*)arg);
std::cout << "Thread value: " << value << std::endl;
return reinterpret_cast<void*>(new int(value * 2));
}

int main() {
pthread_t thread;
int arg = 42;

int result = pthread_create(&thread, nullptr, threadFunction, static_cast<void*>(&arg));
if (result != 0) {
std::cerr << "Error creating thread: " << result << std::endl;
return 1;
}

void* threadResult;
pthread_join(thread, &threadResult);

std::cout << "Main thread received result: " << *static_cast<int*>(threadResult) << std::endl;

delete static_cast<int*>(threadResult); // Release memory

return 0;
}

在这个例子中,主线程创建了一个新线程,传递了一个整数参数。主线程通过 pthread_join 函数等待新线程的结束,并获取新线程的退出状态。这个例子中,线程的退出状态是一个整数的两倍。最后,主线程释放了动态分配的内存。

注意:使用 pthread_join 可以防止主线程在子线程结束之前退出。如果主线程在子线程结束之前退出,那么子线程可能会成为“孤儿线程”(orphan thread),继续在后台运行,直到它自己结束。

示例代码:

在多线程环境中,线程的执行顺序是不确定的,因此每次执行的结果可能不同。这是因为线程调度由操作系统负责,而不同的调度决策可能导致线程以不同的顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <unistd.h>
#include <pthread.h>

#define NUM 5

using std::cout;
using std::endl;

void* func(void* arg);

int main()
{
pthread_t pid[NUM];
int* args[NUM];
for (int i = 0; i < NUM; ++i)
{
args[i] = new int(i);
pthread_create(&pid[i], nullptr, func, static_cast<void*>(args[i]));
// sleep(1); // 等待一会,等每个线程执行完成,顺序就是预期的
}

for (int i = 0; i < NUM; i++)
{
pthread_join(pid[i], nullptr);
delete args[i];
}

return 0;
}

void* func(void* arg)
{
int* i = static_cast<int*>(arg);
cout << "这是第" << *i << "个子线程" << endl;
return nullptr;
}
线程的可连接状态和分离状态

在 POSIX 线程中,线程可以被设置为两种状态:可连接状态(joinable)和分离状态(detached)。这两种状态影响了主线程与新创建的线程之间的关系,特别是在新线程结束时的处理方式。

  1. 可连接状态(Joinable):

    • 默认情况下,线程是可连接状态。
    • 如果一个线程是可连接状态,那么当这个线程结束时,其状态和一些资源(如线程的退出状态)将被保留,直到其他线程通过 pthread_join 函数等待这个线程结束并获取其状态。
    • 主线程可以通过 pthread_join 函数等待可连接状态的线程结束,以获取线程的退出状态。

    示例代码:

    1
    2
    3
    4
    5
    pthread_t thread;
    pthread_create(&thread, nullptr, threadFunction, nullptr);

    // 等待线程结束并获取退出状态
    pthread_join(thread, nullptr);
  2. 分离状态(Detached):

    • 如果一个线程是分离状态,那么当这个线程结束时,其状态和资源将被自动释放,不需要其他线程调用 pthread_join
    • 分离状态的线程不能被等待,主线程无法获取它的退出状态。
    • 分离状态的线程通常用于执行一些后台任务,不需要与其他线程同步或等待其结束。

    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    pthread_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

在实际应用中,选择线程的状态取决于程序的需求。如果需要获取线程的退出状态或者确保线程的资源在结束时不会泄漏,那么可连接状态可能更适合。如果线程仅执行一些后台任务,不需要主线程等待其结束,分离状态可能更合适。需要注意的是,一旦线程被设置为分离状态,就不能再被设置为可连接状态。

线程的属性
  1. 初始化和销毁:

    • pthread_attr_init(pthread_attr_t* attr):初始化线程属性。

    • pthread_attr_destroy(pthread_attr_t* attr):销毁线程属性。

  2. 设置和获取线程栈大小:

    • pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize):设置线程栈的大小。

    • pthread_attr_getstacksize(const pthread_attr_t* attr, size_t* stacksize):获取线程栈的大小。

  3. 设置和获取线程分离状态:

    • pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate):设置线程的分离状态。可以是 PTHREAD_CREATE_JOINABLEPTHREAD_CREATE_DETACHED

    • pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate):获取线程的分离状态。

  4. 设置和获取调度策略:

    • pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy):设置线程的调度策略。

    • pthread_attr_getschedpolicy(const pthread_attr_t* attr, int* policy):获取线程的调度策略。

  5. 设置和获取线程优先级:

    • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <pthread.h>

void* threadFunction(void* arg) {
// 线程执行的任务
std::cout << "This is a new thread." << std::endl;
return nullptr;
}

int main() {
pthread_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);

// 主线程继续执行其他任务

return 0;
}
pthread_detach和在pthread_create中设置线程属性为分离状态有什么区别

pthread_detach 函数和在 pthread_create 中设置线程属性为分离状态都可以用来将线程设置为分离状态,但它们的使用方式和时机有一些不同。

  1. pthread_create 中设置线程属性为分离状态:

    使用 pthread_attr_setdetachstate 函数,通过线程属性设置线程的分离状态。在调用 pthread_create 时,将这个属性作为参数传递给函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    pthread_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);

    这样设置线程属性的好处是,可以在创建线程的同时指定其分离状态,使得线程在启动时就具备了分离属性。

  2. pthread_detach 函数:

    使用 pthread_detach 函数,通过在线程创建后的任何时候将线程设置为分离状态。这个函数需要在线程创建后尽快调用,通常在新线程开始执行之前。

    1
    2
    3
    4
    5
    pthread_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
#include <iostream>
#include <thread>
步骤 2: 创建线程

使用 std::thread 类来创建一个新的线程。以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
void threadFunction() {
std::cout << "This is a new thread." << std::endl;
}

int main() {
std::thread myThread(threadFunction);

// 主线程继续执行其他任务

myThread.join(); // 等待新线程结束

return 0;
}

在这个例子中,threadFunction 是新线程执行的函数,myThread 是一个 std::thread 对象,用于表示新线程。通过调用 join 函数,主线程等待新线程执行完毕。

步骤 3: 传递参数给线程函数

可以通过构造函数为线程传递参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void threadFunction(int value) {
std::cout << "Received value: " << value << std::endl;
}

int main() {
int param = 42;
std::thread myThread(threadFunction, param);

// 主线程继续执行其他任务

myThread.join();

return 0;
}
步骤 4: 使用 Lambda 表达式

可以使用 Lambda 表达式作为线程函数:

1
2
3
4
5
6
7
8
9
10
11
int main() {
std::thread myThread([](){
std::cout << "This is a thread with a Lambda function." << std::endl;
});

// 主线程继续执行其他任务

myThread.join();

return 0;
}
步骤 5: 同步线程

C++11 标准库提供了一些用于同步线程的工具,例如 std::mutexstd::lock_guard 等。以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <mutex>

std::mutex myMutex;

void threadFunction() {
std::lock_guard<std::mutex> guard(myMutex);
std::cout << "This is a thread with a mutex." << std::endl;
}

int main() {
std::thread myThread(threadFunction);

{
std::lock_guard<std::mutex> guard(myMutex);
std::cout << "This is the main thread." << std::endl;
} // myMutex 在这里释放

myThread.join();

return 0;
}

这个例子中,使用 std::mutex 来保护共享资源,确保线程安全。

步骤 6: std::asyncstd::future

std::asyncstd::future 提供了一种异步执行函数的方式,并且可以获取函数的返回值。以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <future>

int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}

int main() {
std::future<int> result = std::async(factorial, 5);

// 主线程继续执行其他任务

int finalResult = result.get(); // 等待异步操作完成并获取结果
std::cout << "Factorial: " << finalResult << std::endl;

return 0;
}

在这个例子中,std::async 异步执行 factorial 函数,然后主线程继续执行其他任务。最后,通过 result.get() 获取异步操作的结果。

以上是 C++11 引入的标准线程库的基本用法。这个库提供了丰富的功能,包括互斥量、条件变量、原子操作等,以支持更复杂的多线程应用。

C++ web编程

C++ web编程