C++基础整理

原创
2022/03/16 17:32
阅读数 3.3K

先写一个HelloWorld

#include <iostream>
//std指标准库
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

运行结果

Hello, World!

C++容器

数组

#include <iostream>
//std指标准库
using namespace std;

int main() {
    int arr[10] = {0,1,2,3,4,5,6,7,8,9};
//    sizeof可以获取数据类型的大小
    int len = sizeof(arr) / sizeof(arr[0]);
    for (int index = 0; index < len; ++index) {
        cout << arr[index] << endl;
    }
    arr[2] = 5;
//    获取数组地址指针
    int *p = arr;
//    获取数组地址偏移量2的值,即为arr[2]
    cout << *(p + 2) << endl;
//    二维数组
    int a[2][4] = {{1,2,3,4},{5,6,7,8}};
    for (int row = 0; row < 2; ++row) {
        for (int col = 0; col < 4; ++col) {
            cout << a[row][col] << " ";
        }
        cout << endl;
    }
    return 0;
}

运行结果

0
1
2
3
4
5
6
7
8
9
5
1 2 3 4 
5 6 7 8 

动态数组Vector

使用简单数组无法实现动态扩容插入元素,因为容量有限

#include <iostream>
#include <vector>
//std指标准库
using namespace std;

//结构体
struct Display {
//    重载运算符()
    void operator()(int i) {
        cout << i << " ";
    }
};

int main() {
//    可扩容的数组
    vector<int> vec = {1,2,3,4};
//    从尾部插入
    vec.push_back(5);
//    从第二个位置后插入
    vec.insert(vec.begin() + 2, 9);
//    从最后一个位置删除
    vec.pop_back();
//    从头部位置删除
    vec.erase(vec.begin());
    for (int index = 0; index < vec.size(); ++index) {
        cout << vec[index] << endl;
    }
//    获取动态数组的容量
    cout << vec.capacity() << endl;
//    动态数组当前存储的容量
    cout << vec.size() << endl;

    int arr[] = {1,2,3,4,5};
//    将数组转化为动态数组
    vector<int> aVec(arr,arr + 5);
    for_each(aVec.begin(),aVec.end(),Display());
    return 0;
}

运行结果

2
9
3
4
8
4
1 2 3 4 5 

字符串

字符串变量:字符串是以空字符'\0'结束的字符数组,空字符'\0'自动添加到字符串的内部表示中。在声明字符串变量的时候,应该为这个空结束符预留一个额外元素空间。

#include <iostream>
//std指标准库
using namespace std;

int main() {
//    如果要写数字的话,这里要写11而不是10
//    char str[11] = {"helloworld"};
    char str[] = {"helloworld"};
    cout << str << endl;
    return 0;
}

运行结果

helloworld

字符串常量:字符串常量是一对双引号括起来的字符序列,字符串中每个字符作为一个数组元素存储。

#include <iostream>
//std指标准库
using namespace std;

int main() {
    char str[] = "helloworld";
    cout << str << endl;
    return 0;
}

字符串指针:char[]和char*的区别,一个是地址,一个是地址存储的信息;一个可变,一个不可变。

#include <iostream>
//std指标准库
using namespace std;

int main() {
    char str[] = "helloworld";
    char* pStr1 = "helloworld";
    char* pStr2 = str;
    char str1[] = "helloworld";
//    此时strstr1相同,返回0
    cout << strcmp(str,str1) << endl;
//    数组下的内容可以修改
    for (int index = 0; index < 10; ++index) {
        str[index] += 1;
    }
    cout << str << endl;
//    常量指针下的内容不可修改
//    for (int index = 0; index < 10; ++index) {
//        pStr[index] += 1;
//    }
//    指向数组地址的指针下的内容可以修改
    for (int index = 0; index < 10; ++index) {
        pStr2[index] += 1;
    }
    cout << pStr1 << endl;
    cout << pStr2 << endl;
//    获取字符串长度
    cout << strlen(str) << endl;
//    获取字符串的容量,包含了结束符
    cout << sizeof(str) << endl;
//    此时是jqnnqyqtnfhelloworld比较,jh2,返回2
    cout << strcmp(str,str1) << endl;
//    str拼接到str1的末尾
    strcat(str1,str);
    cout << str1 << endl;
//    str复制到str1    strcpy(str1,str);
    cout << str1 << endl;
//    查找q字符在str中第一次出现的位置,并返回后面的字符串
    cout << strchr(str,'q') << endl;
//    查找qy字符串在str中第一次出现的位置,并返回后面的字符串
    cout << strstr(str,"qy") << endl;
    return 0;
}

运行结果

0
ifmmpxpsme
helloworld
jgnnqyqtnf
10
11
2
helloworldjgnnqyqtnf
gnnqyqtnf
qyqtnf
qyqtnf

新型字符串

#include <iostream>
#include <string>
//std指标准库
using namespace std;

int main() {
    string s1 = "hello";
    string s2 = "world";
    cout << s1.length() << endl;
    cout << s1.size() << endl;
    cout << s1.capacity() << endl;
    cout << s1[0] << endl;
    cout << (s1 == s2) << endl;
    cout << (s1 != s2) << endl;
//    转换为C风格的字符串
    const char* c_str = s1.c_str();
    cout << c_str << endl;
//    拷贝字符串
    string s = s1;
    cout << s << endl;
//    字符串的连接
    cout << s + s1 << endl;
    return 0;
}

运行结果

5
5
22
h
0
1
hello
hello
hellohello

将string转换成char数组

#include <iostream>
#include <sstream>
#include <string.h>
using namespace std;

int main()
{
    char data[128];
    string str;
    stringstream ss;
    ss << "HelloWorld " << 2023 << " " << "see you again";
    str = ss.str();
    strcpy(data, str.c_str());
    cout << data << endl;
    return 0;
}

运行结果

HelloWorld 2023 see you again

链表

list就是数据结构中的双向链表,因此它的内存空间是不连续的,通过指针来进行数据的访问。

#include <iostream>
#include <list>
//std指标准库
using namespace std;

//结构体
struct Display {
//    重载运算符()
    void operator()(int i) {
        cout << i << " ";
    }
};

int main() {
    list<int> aList = {1,2,3,4,5};
    aList.push_back(9);
    aList.push_back(8);
    aList.push_front(7);
    aList.pop_back();
//    逆序
    aList.reverse();
    for_each(aList.begin(),aList.end(),Display());
    return 0;
}

运行结果

9 5 4 3 2 1 7 

队列

deque是一个双向队列,优化了对序列两端元素进行添加和删除操作的基本序列容器。通常由一些独立的区块组成,第一个区块朝某方向发展,最后一个区块朝另一个方向发展。它允许较为快速地随机访问但它不像vector一样把所有对象保存在一个连续的内存块,而是多个连续的内存块。并且在一个映射结构中保存对这些块以及顺序的跟踪。

#include <iostream>
#include <deque>
#include <queue>
//std指标准库
using namespace std;

//结构体
struct Display {
//    重载运算符()
    void operator()(int i) {
        cout << i << " ";
    }
};

int main() {
    deque<int> deque = {1,2,3,4,5};
    //    从尾部插入
    deque.push_back(5);
//    从第二个位置后插入
    deque.insert(deque.begin() + 2, 9);
//    从最后一个位置删除
    deque.pop_back();
//    从头部位置删除
    deque.erase(deque.begin());
    for_each(deque.begin(),deque.end(),Display());
    cout << endl;
//    优先队列
    priority_queue<int> priorityQueue;
    priorityQueue.push(1);
    priorityQueue.push(2);
    priorityQueue.push(5);
    while (!priorityQueue.empty()) {
        cout << priorityQueue.top() << endl;
        priorityQueue.pop();
    }
    return 0;
}

运行结果

2 9 3 4 5 
5
2
1

stack(堆栈) 是一个容器类的改编,为程序员提供了堆栈的全部功能,也就是说实现了一个先进后出(FILO)的数据结构

#include <iostream>
#include <stack>
//std指标准库
using namespace std;

int main() {
    stack<int> sk;
    sk.push(1);
    sk.push(3);
    sk.push(5);
    while (!sk.empty()) {
        cout << sk.top() << " ";
        sk.pop();
    }
    return 0;
}

运行结果

5 3 1

以上都是序列式容器,还有一种是关联式容器。

集合

set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。

#include <iostream>
#include <set>
//std指标准库
using namespace std;

int main() {
    set<int> s;
//    迭代器
    set<int>::iterator iter;
    s.insert(5);
    s.insert(2);
    s.insert(3);
    s.insert(2);
    cout << s.size() << endl;
    for (iter = s.begin(); iter != s.end() ; ++iter) {
        cout << *iter << endl;
    }
    return 0;
}

运行结果

3
2
3
5

映射

map是STL的一个关联容器,它提供一对一的hash。

  • 第一个可以称为关键字(key),每个关键字只能在map中出现一次;
  • 第二个可能称为该关键字的值(value);
#include <iostream>
#include <map>
//std指标准库
using namespace std;

struct Display {
    void operator()(pair<string,double> info) {
        cout << info.first << ":" << info.second << endl;
    }
};

int main() {
    map<string,double> studentScores;
    studentScores["李明"] = 95.0;
    studentScores.insert(pair<string,double>("刘刚",100));
    studentScores.insert(map<string,double>::value_type("张丽",98.5));
    for_each(studentScores.begin(),studentScores.end(),Display());

    map<string,double>::iterator iter;
    iter = studentScores.find("李明");
    if (iter != studentScores.end()) {
        cout << iter->second << endl;
    }
    return 0;
}

运行结果

刘刚:100
张丽:98.5
李明:95
95

C++指针

内存由很多内存单元组成。这些内存单元用于存放各种类型的数据。计算机对每个内存单元都进行了编号,这个编号就称为内存地址,地址决定了内存单元在内存中的位置。记住这些内存单元地址不方便,于是C++语言的编译器让我们通过名字来访问这些内存位置。

#include <iostream>
//std指标准库
using namespace std;

int main() {
    int a = 112, b = -1;
    float c = 3.14;
    int* d = &a;
    float* e = &c;
//    打印内存地址编号
    cout << d << endl;
    cout << e << endl;
//    打印内存地址的内容
    cout << *d << endl;
    cout << *e << endl;

    int f[4] = {1,2,3,4};
//    数组的指针
    int(*g)[4] = &f;
    for (int i = 0; i < 4; ++i) {
        cout << (*g)[i] << " ";
    }
    return 0;
}

运行结果

0x7ffee5200218
0x7ffee5200210
112
3.14
1 2 3 4 

引用

引用是一种特殊的指针,不允许修改的指针。使用引用不存在空引用,必须初始化,一个引用永远指向它初始化的那个对象。可以认为是指定变量的别名,使用时可以认为是变量本身。

#include <iostream>
#include <assert.h>
//std指标准库
using namespace std;

//使用引用传递可以不产生栈变量
void swap(int& a,int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 1,y = 3;
//    定义一个引用
    int& rx = x;
    rx = 2;
    cout << x << endl;
    cout << rx << endl;
    rx = y;
    cout << x << endl;
    cout << rx << endl;

    int a = 3,b = 4;
    swap(a,b);
    assert(a == 4 && b == 3);
    return 0;
}

运行结果

2
2
3
3

指针的指针

#include <iostream>
//std指标准库
using namespace std;

int main() {
    int a = 123;
    int* b = &a;
    int** c = &b;
    cout << a << endl;
    cout << b << endl;
    cout << c << endl;
    cout << *b << endl;
    cout << *c << endl;
    cout << **c << endl;
}

运行结果

123
0x7ffeef434034
0x7ffeef434028
123
0x7ffeef434034
123

从上面的结果,我们可以看出**c=*b=a,都表示a的内容;而*c=b都表示a的地址。

函数指针

函数指针是C里面的内容,不过这里也做一个说明。它也是一个指针,指针指向某个内存区域,函数指针就是指向函数入口地址的这么一个指针变量,在.c文件中编写一个函数,将.c编译为可执行程序后,在.c文件中编写的函数会存放在可执行程序的代码段中,入口地址就在这。

#include <stdio.h>
             
int val = 1;
 
void Test(int a)
{
    printf("In Test a = %d\n", a);   
}
 
void Test111(int a)
{
    printf("In Test111 a = %d\n", a);   
}
//这里的参数为一个函数指针 
void Formal(void(*p)(int))
{
    printf("In Formal Call:\n"); 
    (*p)(10);
}
//这里的参数为一个函数指针的指针
void Formal111(void(**pp)(int))
{
    printf("In Formal111 Change p\n");
    (*pp) = Test111;  //将Test111的地址传给*pp
    (**pp)(10);  //等同于调用Test111(10)
}
 
int main()
{
    int step;
    // 函数指针声明
    void(*p)(int);
 
    // 函数指针赋值
    p = Test;
    
    printf("Input 1 to Dsp Addr\n");
    printf("Input 2 to Dsp Call\n");
    printf("Input 3 to Dsp Formal parameter1\n");
    printf("Input 4 to Dsp Formal parameter2\n");
    
    while(1)
    {
        scanf("%d", &step); 
        printf("****************************\n");
        if(step == 1)       // 地址展示
        {
            printf("Test = %p\n", Test); //打印Test函数地址
            printf("p    = %p\n", p);  //打印函数指针,代表Test函数地址
            printf("&val = %p\n", &val);  //打印常量地址
        }
        else if(step == 2)  // 调用展示
        {
            (*p)(10);  //打印test(10)的结果
        }
        else if(step == 3)  // 形参展示1
        {
            Formal(p);  //这里面p作为函数指针,等于调用了test(10)
        }
        else if(step == 4)  // 形参展示2
        {
            Formal111(&p);  //这里传递的是函数指针p的地址,等同于指针的指针
        }
        else
        {
            printf("Input Num Must From 1 To 4\n");
        }
        printf("****************************\n");
    }
 
    return 0;
}

运行结果

Input 1 to Dsp Addr
Input 2 to Dsp Call
Input 3 to Dsp Formal parameter1
Input 4 to Dsp Formal parameter2
1
****************************
Test = 0x103200bba
p    = 0x103200bba
&val = 0x103205020
****************************
2
****************************
In Test a = 10
****************************
3
****************************
In Formal Call:
In Test a = 10
****************************
4
****************************
In Formal111 Change p
In Test111 a = 10
****************************

指针函数

指针函数是指一个函数的返回值为地址的函数。

#include <stdio.h>
#include <string.h>

char * del_space(char * s); //定义一个指针函数,返回的是地址

int main()
{   
    char * r;
    char str[] = "     how    are   you   ";
    
    printf("%s\n", str);
    r = del_space(str);  //这里返回的是一个地址
    printf("---%p---\n", r); //打印地址
    printf("---%s---\n", r); //打印内容
    
    return 0;
}

char * del_space(char * s)//char * s = str;
{
    char * r = s;
    char * p = s;

  //当指针s没有走到'\0'时
    while (*s){
        if (*s == ' ')//如果指针指向的是空格,指针++
                s++;
        else{
                *p = *s;//如果指针s指向的不是空格,进行赋值
                s++;
                p++;
        }
    }//循环结束后执行*p = '\0'
    *p = '\0';//当指针p指向\0时结束

    return r;//由于s和p都变化了,所以由r进行返回,将初始地址赋给r
}

运行结果

     how    are   you   
---0x7ffeec41a040---
---howareyou---

C++面向对象

C++使用struct,class来定义一个类,struct的默认成员权限是public,class的默认成员权限是private,除此之外,二者基本无差别。

#ifndef UNTITLED1_COMPLEX_H
#define UNTITLED1_COMPLEX_H


class Complex {
public:
//    构造函数
    Complex();
    Complex(double r,double i);
//    析构函数
    virtual ~Complex();
    double getReal() const; //在成员方法后面加const表示不能修改成员变量的值,为只读函数
    void setReal(double r);
    double getImage() const;
    void setImage(double i);
//    运算符重载
    Complex operator+(const Complex& x);
    Complex& operator=(const Complex& x);

private:
    double _real;
    double _image;
};


#endif //UNTITLED1_COMPLEX_H
#include "Complex.h"
#include <iostream>
using namespace std;

Complex::Complex() {}

Complex::Complex(double r,double i) {
    _real = r;
    _image = i;
    cout << "Complex" << endl;
}

Complex::~Complex() {
    cout << "~Complex" << endl;
}

double Complex::getReal() const {
    return _real;
}

void Complex::setReal(double r) {
    _real = r;
}

double Complex::getImage() const {
    return _image;
}

void Complex::setImage(double i) {
    _image = i;
}
//此处使用引用可以不必再分配内存产生栈对象
Complex Complex::operator+(const Complex &x) {
    Complex temp;
    temp._real = _real + x._real;
    temp._image = _image + x._image;
    return temp;
}

Complex& Complex::operator=(const Complex &x) {
//    这里的&x是一个地址,不是引用
    if (this != &x) {
        _real = x._real;
        _image = x._image;
    }
    return *this;
}

int main() {
    Complex c(1.0,2.0);
    cout << c.getReal() << endl;
    cout << c.getImage() << endl;
    c.setReal(3.0);
    c.setImage(4.0);
    cout << c.getReal() << endl;
    cout << c.getImage() << endl;
    Complex d(8.0,9.0);
    Complex e;
    e = c + d;
    cout << e.getReal() << endl;
    cout << e.getImage() << endl;
    return 0;
}

运行结果

Complex
1
2
3
4
Complex
~Complex
11
13
~Complex
~Complex
~Complex

多态

shape.h

class Shape  //抽象类
{ 
public:
    //virtual要求子类必须重写该方法,即为多态,且为运行时多态,父类中有默认实现
    //但如果virtual fun()=0;则为纯虚方法,父类无需处理,必须由子类处理
    virtual double Area() const = 0;
    void Display();
};

class Square : public Shape
{
public:
    Square(double len):_len(len) {};
    //override表示强制要求父类相同函数需要是虚函数
    double Area() const override;
private:
    double _len;
};

class Circle : public Shape
{
public:
    Circle(double radius):_radius(radius) {};
    double Area() const override;
private:
    double _radius;
};

shape.cpp

#include "shape.h"
#include <iostream>

using namespace std;

void Shape::Display() {
    cout << Area() << endl;
}

double Square::Area() const {
    return _len * _len;
}

double Circle::Area() const {
    return 3.1415 * _radius * _radius;
}

int main() {
    Square s1(2.0);
    Circle c1(2.0);

    Shape* shapes[2];
    shapes[0] = &s1;
    shapes[1] = &c1;

    for(int i = 0; i < 2; i++) {
        shapes[i]->Display();
    }
    return 0;
}

运行结果

4
12.566

Linux下的C++环境

liunx中使用的编译器为gcc或者g++,其中gcc主要是编译C代码的,g++主要是编译C++代码的。运行如下命令可以看见它的版本号

g++ --version

现在来编译我们第一段liunx的C++代码

#include <iostream>
using namespace std;

int main(int argc,char** argv) {
	cout << "Hello" << endl;
	return 0;
}
g++ hello.cpp -o hello

这样就可以编译出一个可执行程序hello。运行

./hello

编译过程

首先源代码会被预处理,然后再通过编译器(Compliler)编译成汇编语言;再经过一个汇编器(Assembler)编译成机器语言;机器语言最终通过链接器(linker)生成打包可执行程序,期间可能会有一些外部程序链接进来供调用。

Makefile的使用和编写

  • 可执行程序的产生过程,相当于使用g++来进行编译的过程,但是在实际的工程项目中,它的过程要复杂的多。一般包含
  1. 配置环境(系统环境)
  2. 确定标准库和头文件的位置
  3. 确定依赖关系(源代码之间编译的依赖关系)
  4. 头文件预编译
  5. 预处理
  6. 编译
  7. 链接
  8. 安装
  9. 和操作系统建立联系
  10. 生成安装包
  • 当依赖关系复杂的时候,make命令工具诞生了,而Makefile文件正是为了make工具所使用的。
  • Makefile描述了整个工程所有文件的编译顺序、编译规则。

多文件编译的简单示例

  • demo1

reply.h

#include <iostream>

class Reply {
        public:
                Reply();
                ~Reply();
                void PrintHello();
};  

reply.cpp

#include "reply.h"

using namespace std;

Reply::Reply()
{}

Reply::~Reply()
{}

void Reply::PrintHello()
{
        cout << "Hello" << endl;
}

main.cpp

#include "reply.h"

int main()
{
        Reply reply;
        reply.PrintHello();

        return 0;
}

此时如果我们使用g++命令是可以生成可执行文件的

g++ reply.cpp main.cpp -o main

由于这个例子比较简单,如果在真实的工程文件中一般不会这么使用,而是使用make命令来生成。

Makefile

main: reply.o main.o
        g++ reply.o main.o -o main
reply.o: reply.cpp
        g++ -c reply.cpp -o reply.o
main.o: main.cpp
        g++ -c main.cpp -o main.o

使用命令

make

此时会生成一个main的可执行程序,同时生成一个main.o和reply.o的中间级组件。

make是一个批处理工具,把linux Shell或者一些其他命令一次性的执行。而它所执行的就是Makefile里面的内容,根据Makefile中的命令进行编译和链接。另外还有一个CMake命令,它可以帮助我们将C++工程文件中的CMakeLists.txt(通常包含了一些项目的库链接)转换成Makefile文件,再调用make进行编译。

Makefile的格式

  • Makefile的基本规则
目标(target)...:依赖(prerequisites)...
        命令(command)

这里需要注意的是在第二行的命令行前面必须是一个Tab字符(制表符),即命令行第一个字符是Tab。

  • Makefile的简化规则
变量定义: 变量 = 字符串
变量使用: $(变量名)

现在我们以之前的那个简单例子来改写Makefile

TARGET = main
OBJS = reply.o main.o
$(TARGET):$(OBJS)
        g++ $(OBJS) -o $(TARGET)
reply.o: reply.cpp
main.o: main.cpp

执行make得到跟之前一样的效果。但是在执行make之前需要将之前的main、main.o、reply.o删除,否则系统不会帮我们重新编译。当然我们也有办法解决这个问题。

TARGET = main
OBJS = reply.o main.o
$(TARGET):$(OBJS)
        g++ $(OBJS) -o $(TARGET)
reply.o: reply.cpp
main.o: main.cpp

clean:
        rm $(TARGET) $(OBJS)

执行

make clean

此时我们会看到之前的main、main.o、reply.o都没有了,重新执行make,又可以重新编译了。这里需要注意的是,执行make clean是默认认为在当前文件夹下是没有以clean命名的文件的,否则同样会产生失败。当然也有办法解决这个问题。

TARGET = main
OBJS = reply.o main.o

.PHONY: clean

$(TARGET):$(OBJS)
        g++ $(OBJS) -o $(TARGET)
reply.o: reply.cpp
main.o: main.cpp

clean:
        rm $(TARGET) $(OBJS)

这个.PHONY表示clean是不依赖于实体文件的存在的目标,无论有没有clean文件的存在都可以执行相应的命令。此时执行make clean就可以对main、main.o、reply.o进行清除了。所以在不生成目标文件的命令最好都设置成假想目标,也就是使用.PHONY关键词。

make工程的安装和卸载

在Linux环境中安装,就是把我们编译出来的程序放到系统目录下便于进行公共的调用。

TARGET = main
OBJS = reply.o main.o

.PHONY: clean

$(TARGET):$(OBJS)
        g++ $(OBJS) -o $(TARGET)
reply.o: reply.cpp
main.o: main.cpp

clean:
        rm $(TARGET) $(OBJS)

install:
        cp ./main /usr/local/bin/

uninstall:
        rm /usr/local/bin/main

依次执行以下命令进行安装,make就是需要先在本地进行编译。

make
sudo make install

这样我们就可以在系统的任何位置都可以执行

main

命令得到我们程序的返回结果。

当然我们要卸载可以执行

sudo make uninstall

这里我们可以通过以下命令查看我们系统的环境变量,有关linux shell的一些常用方法可以参考Linux Shell一些常用记录(一)

echo $PATH

环境变量,动态链接库

在Winodws中,动态链接库通常是以.dll为扩展名的,但是在Linux中,动态链接库是以.so为扩展名的。这里我们继续改写之前的Makefile文件。

TARGET = main
OBJS = reply.o
LIB = libreply.so
CXXFLAGS = -c -fPIC

.PHONY: clean

$(TARGET):$(LIB) main.o
        $(CXX) main.o -o $(TARGET) -L. -lreply -Wl,-rpath ./
$(LIB):$(OBJS)
        $(CXX) -shared $(OBJS) -o $(LIB)
reply.o:reply.cpp
        $(CXX) $(CXXFLAGS) reply.cpp -o $(OBJS)
main.o:main.cpp
        $(CXX) $(CXXFLAGS) main.cpp -o main.o

clean:
        rm $(TARGET) $(OBJS)

install:
        cp ./main /usr/local/bin/

uninstall:
        rm /usr/local/bin/main

make编译后,我们会发现多了一个libreply.so的动态链接库文件。上面的内容改动较大,我们做一个说明:

  1. .o文件实际上是.cpp文件编译后的二进制代码,我们一般称为目标文件。它是一段没有真实机器内存地址但是有地址偏移量的代码,所以它不能被真正执行。.so文件更接近于可执行程序,它跟可执行程序之间的区别只是文件头部(header)中的几位,其他对于执行指令和地址访问跟可执行程序是一样的。
  2. CXXFLAGS = -c -fPIC表示C++的一些编译选项,-c表示生成一个目标文件,-fPIC表示生成一个可以共享的动态链接库,它的好处是可以不用生成多个库,多个应用程序可以使用动态加载的方式使用同一份代码。
  3. $(CXX)是一个隐含环境变量,它的值就是g++;$(CXX) main.o -o $(TARGET) -L. -lreply -Wl,-rpath ./表示生成可执行程序main,-L.表示指定当前路径为生成路径,-lreply表示使用reply这个动态链接库,-Wl,-rpath ./表示动态链接库的链接地址为当前路径。
  4. $(CXX) -shared $(OBJS) -o $(LIB)表示生成一个动态链接库libreply.so,-shared表示为共享模式。

这里除了$(CXX)这个环境变量,其实还有很多的环境变量,如$(LANG)为系统语言,$(SHELL)为系统的命令外壳,$(HOME)为当前用户目录,$(LD_LIBRARY_PATH)为C++的库文件路径。

一般变量说明

这里我们不写任何的C++代码,建一个空的文件夹,编写Makefile文件

ugh = Huh
bar = $(ugh)
foo = $(bar)

test1:
        echo $(foo), foo
test2:
        echo $(bar), bar

执行

make test1

得到打印

Huh, foo

这里我们也可以将真实值定义在最后

foo = $(bar)
bar = $(ugh)
ugh = Huh

test1:
        echo $(foo), foo
test2:
        echo $(bar), bar

它跟上面的写法是一样的。继续追加代码

foo = $(bar)
bar = $(ugh)
ugh = Huh

test1:
        echo $(foo), foo
test2:
        echo $(bar), bar

y := $(x)bar
z = $(x)bar
x := foo

test3:
        echo $(x), x
        echo $(y), y
        echo $(z), z

执行

make test3

得到打印

foo, x
bar, y
foobar, z

这里我们可以看到y跟z的等号方式不同,y是:=,z是=。直接写=的方式,它可以依赖于后面的x,所以z的值为foobar,而:=的方式是一种避免向后依赖的方式,它只能把前面定义的值拿过来用,后面定义的值不行,所以y的值为bar。继续追加代码

foo = $(bar)
bar = $(ugh)
ugh = Huh

test1:
        echo $(foo), foo
test2:
        echo $(bar), bar

y := $(x)bar
z = $(x)bar
x := foo
x += foo1

test3:
        echo $(x), x
        echo $(y), y
        echo $(z), z

执行

make test3

 得到打印

foo foo1, x
bar, y
foo foo1bar, z

这里我们可以看到+=的意思为追加。

  • 多行变量

继续追加代码

foo = $(bar)
bar = $(ugh)
ugh = Huh

test1:
        echo $(foo), foo
test2:
        echo $(bar), bar

y := $(x)bar
z = $(x)bar
x := foo
x += foo1

test3:
        echo $(x), x
        echo $(y), y
        echo $(z), z

define two-lines
foo
echo $(bar)
endef

test4:
        echo $(two-lines)

执行

make test4

得到打印

foo
Huh

自动变量、模式变量、自动匹配

  • 自动变量

之前的变量都属于全局变量,但是有一种变量并不是整个文件生存周期都是存在的,而只是当运行到某一个步骤的时候,这个变量所定义的内容才是存在的,这种变量称之为自动变量。这里我们回到之前的main、reply项目中,对Makefile做一点小小的改动

TARGET = main
OBJS = reply.o
LIB = libreply.so
CXXFLAGS = -c -fPIC

.PHONY: clean

$(TARGET):$(LIB) main.o
	$(CXX) main.o -o $(TARGET) -L. -lreply -Wl,-rpath ./
$(LIB):$(OBJS)
	$(CXX) -shared $^ -o $@
reply.o:reply.cpp
	$(CXX) $(CXXFLAGS) reply.cpp -o $(OBJS)
main.o:main.cpp
	$(CXX) $(CXXFLAGS) main.cpp -o main.o

clean:
	rm $(TARGET) $(OBJS) $(LIB)

install:
	cp ./main /usr/local/bin/

uninstall:
	rm /usr/local/bin/main

执行make后同样能生成之前的程序和动态链接库。

上面实际上是将$(OBJS)使用$^来表示,将$(LIB)用$@来表示。$^和$@实际上就是自动变量,也叫目标变量,它是依赖某些规则而生成的。规则如下

变量类型 变量形式 解释
自动变量 $< 表示第一个匹配的依赖
自动变量 $@ 表示目标
自动变量 $^ 所有依赖
自动变量 $? 所有依赖中更新的文件
自动变量 $+ 所有依赖文件不去重
自动变量 $(@D) 目标文件路径
自动变量 $(@F) 目标文件名称
模式变量 % 表示任意字符

继续修改Makefile,以便与更加简化。

TARGET = main
OBJS = reply.o
LIB = libreply.so
CXXFLAGS = -c -fPIC
LDFLAGS = -L. -lreply -Wl,-rpath $(@D)

.PHONY: clean

$(TARGET):main.o $(LIB)
	$(CXX) $< -o $@ $(LDFLAGS) 
$(LIB):$(OBJS)
	$(CXX) -shared $^ -o $@
reply.o:reply.cpp
	$(CXX) $(CXXFLAGS) $< -o $@
main.o:main.cpp
	$(CXX) $(CXXFLAGS) $< -o $@

clean:
	rm $(TARGET) $(OBJS) $(LIB) main.o

install:
	cp ./main /usr/local/bin/

uninstall:
	rm /usr/local/bin/main

这里只需要注意类似于$(TARGET):main.o $(LIB)这种目标与依赖项不要弄错就可以。

  • 模式变量

模式变量很想搜索中使用的通配符。它的好处在于设定好一个模式之后,可以把变量定义在所有符合这种模式的目标上。不用在依赖项上把每一个文件名(比如main.cpp,main.o)都写上去,只需要通过一种扩展名的方式来找到任意字符为文件名的文件,比如说.o是依赖于.cpp这样的文件,可以大大简化Makefile的编写。继续修改Makefile,以便与更加简化。

TARGET = main
OBJS = reply.o
TESTOBJ = main.o
LIB = libreply.so
CXXFLAGS = -c -fPIC
LDFLAGS = -L. -lreply -Wl,-rpath $(@D)

.PHONY: clean

$(TARGET):$(TESTOBJ) $(LIB)
	$(CXX) $< -o $@ $(LDFLAGS) 
$(LIB):$(OBJS)
	$(CXX) -shared $^ -o $@
%.o:%.cpp
	$(CXX) $(CXXFLAGS) $< -o $@

clean:
	$(RM) $(TARGET) $(OBJS) $(LIB) $(TESTOBJ)

install:
	cp ./main /usr/local/bin/

uninstall:
	rm /usr/local/bin/main

以上我们可以看到,main.o、reply.o的生成被合并成了一行,%.o:%.cpp表示.o的文件是依赖于.cpp的文件来生成的。

自动生成和部署

  • 项目生成和部署

一般来说,至少有下面的目录:

  1. src目录下是头文件的实现文件
  2. include目录下是用到的头文件
  3. bin目录下是可运行文件
  4. build目录下是临时构建的文件
  • CMake的使用

查看版本号

cmake --version

这里我们新建一个文件夹,将之前的main.cpp、reply.cpp、reply.h拷贝过来。新创建两个文件夹src和include,并将.cpp文件放入src,将.h文件放入include中。

新建一个CMakeLists.txt文件,内容如下

#CMakeLists.txt
# 设置cmake最低版本
cmake_minimum_required(VERSION 2.8.0)
# 设置C++标准
set(CMAKE_CXX_STANDARD 11)
# 项目名称
project(cmake_test)
# 包含的头文件目录
include_directories(./include)
set(SRC_DIR ./src)
# 指定生成链接库
add_library(reply ${SRC_DIR}/reply.cpp)
add_library(main ${SRC_DIR}/main.cpp)
# 设置变量
set(LIBRARIES reply main)
set(OBJECT main_test)
# 生成可执行文件
add_executable(${OBJECT} ${SRC_DIR}/main.cpp)
# 为可执行文件链接目标库
target_link_libraries(${OBJECT} ${LIBRARIES})

创建一个build文件夹,进入该文件夹,执行

cmake ..
make

此时就会产生一个main_test的可执行程序。

C++多线程

先来写一个最简单的多线程的例子

#include <iostream>
#include <thread>

using namespace std;
using namespace this_thread;

void ThreadMain() {
    cout << "begin sub thread main, ID " << get_id() << endl;
    for (int i = 0; i < 10; ++i) {
        sleep_for(chrono::seconds(1));
    }
    cout << "end sub thread main, ID " << get_id() << endl;
}

int main(int argc, char* argv[]) {
    cout << "Main thread ID " << get_id() << endl;
    //线程创建启动
    thread th(ThreadMain);
    cout << "begin wait sub thread" << endl;
    //阻塞等待线程退出
    th.join();
    cout << "end wait sub thread" << endl;
    return 0;
}

使用编译命令(Linux环境)

g++ thread.cpp -o thread -lpthread

运行结果

Main thread ID 0x10da00dc0
begin wait sub thread
begin sub thread main, ID 0x70000be85000
end sub thread main, ID 0x70000be85000
end wait sub thread

这里th是一个线程对象,th.join()会将主线程阻塞,如果我们不想阻塞主线程,大家各玩各的,可以写成如下形式

#include <iostream>
#include <thread>

using namespace std;
using namespace this_thread;

void ThreadMain() {
    cout << "begin sub thread main, ID " << get_id() << endl;
    for (int i = 0; i < 10; ++i) {
        sleep_for(chrono::seconds(1));
    }
    cout << "end sub thread main, ID " << get_id() << endl;
}

int main(int argc, char* argv[]) {
    cout << "Main thread ID " << get_id() << endl;
    //线程创建启动
    thread th(ThreadMain);
    cout << "begin wait sub thread" << endl;
    //阻塞等待线程退出
    //th.join();
    //主线程和子线程分离
    th.detach();
    cout << "end wait sub thread" << endl;
    getchar();
    return 0;
}

这里我们加了一个getchar(),主要是为了等待子线程完成操作,否则主线程运行过快,会导致整个程序运行结束,而子线程可能还没开始执行,运行结果如下

Main thread ID 0x11ea98dc0
begin wait sub thread
end wait sub thread
begin sub thread main, ID 0x700008658000
end sub thread main, ID 0x700008658000

等到以上都打印完成,我们再输入任意一个字母,程序结束。这里需要注意的是th(ThreadMain)中的ThreadMain是一个函数指针

子线程传值

#include <iostream>
#include <thread>

using namespace std;
using namespace this_thread;

class Para {
public:
    Para() {
        cout << "Create Para" << endl;
    }
    ~Para() {
        cout << "Drop Para" << endl;
    }
    string name;
};

void ThreadMain(int p1, float p2, string p3, Para p4) {
    sleep_for(chrono::milliseconds(100));
    cout << "ThreadMain " << p1 << " " << p2 << " " << p3 << " " << p4.name << endl;
}

int main(int argc, char* argv[]) {
    thread th;
    float f1 = 10.1;
    Para p;
    p.name = "test Para";
    th = thread(ThreadMain, 101, f1, "test string", p);
    th.join();
    return 0;
}

运行结果

Create Para
Drop Para
ThreadMain 101 10.1 test string test Para
Drop Para
Drop Para
Drop Para

在上面的程序中,我们传递了整数,浮点数,字符串,类的对象。对于整数,浮点数,字符串都没有问题,但是在传递对象的时候,析构函数调用了4次,第1次是Para p的时候,其他3次是在调用回调函数ThreadMain的时候,进行了3次拷贝构造函数的创建。这里我们可以覆写拷贝构造函数就可以知道了。

#include <iostream>
#include <thread>

using namespace std;
using namespace this_thread;

class Para {
public:
    Para() {
        cout << "create Para" << endl;
    }
    Para(const Para& p) {
        cout << "Copy Para" << endl;
    }
    ~Para() {
        cout << "drop Para" << endl;
    }
    string name;
};

void ThreadMain(int p1, float p2, string p3, Para p4) {
    sleep_for(chrono::milliseconds(100));
    cout << "ThreadMain " << p1 << " " << p2 << " " << p3 << " " << p4.name << endl;
}

int main(int argc, char* argv[]) {
    thread th;
    float f1 = 10.1;
    Para p;
    p.name = "test Para";
    th = thread(ThreadMain, 101, f1, "test string", p);
    th.join();
    return 0;
}

运行结果

create Para
Copy Para
Copy Para
drop Para
Copy Para
ThreadMain 101 10.1 test string 
drop Para
drop Para
drop Para

由于类的对象对子线程进行直接传递会有多次创建的过程,会对性能造成影响,所以我们一般会直接传递指针和引用而不是对象本身。

子线程传递指针、引用

  • 指针
#include <iostream>
#include <thread>

using namespace std;
using namespace this_thread;

class Para {
public:
    Para() {
        cout << "create Para" << endl;
    }
    Para(const Para& p) {
        cout << "Copy Para" << endl;
    }
    ~Para() {
        cout << "drop Para" << endl;
    }
    string name;
};

void ThreadMain(Para* p) {
    sleep_for(chrono::milliseconds(100));
    cout << "ThreadMain " << p->name << endl;
}

int main(int argc, char* argv[]) {
    thread th;
    Para p;
    p.name = "test Para";
    th = thread(ThreadMain, &p);
    th.join();
    return 0;
}

运行结果

create Para
ThreadMain test Para
drop Para

这里我们可以看到,它并没有像之前一样,会另外调用三次拷贝构造函数,三次析构函数。

  • 引用
#include <iostream>
#include <thread>

using namespace std;
using namespace this_thread;

class Para {
public:
    Para() {
        cout << "create Para" << endl;
    }
    Para(const Para& p) {
        cout << "Copy Para" << endl;
    }
    ~Para() {
        cout << "drop Para" << endl;
    }
    string name;
};

void ThreadMain(Para& p) {
    sleep_for(chrono::milliseconds(100));
    cout << "ThreadMain " << p.name << endl;
}

int main(int argc, char* argv[]) {
    thread th;
    Para p;
    p.name = "test Para";
    th = thread(ThreadMain, ref(p));
    th.join();
    return 0;
}

运行结果

create Para
ThreadMain test Para
drop Para

子线程传递成员函数

#include <iostream>
#include <thread>

using namespace std;
using namespace this_thread;

class Para {
public:
    void Main() {
        cout << "Para Main " << name << ":" << age << endl;
    }
    string name;
    int age = 37;
};

int main(int argc, char* argv[]) {
    thread th;
    Para p;
    p.name = "test Para";
    //第一参数表示哪个类的哪个方法,第二个参数表示该类的具体对象
    th = thread(&Para::Main, &p);
    th.join();
    return 0;
}

运行结果

Para Main test Para:37

线程基类的封装

#include <iostream>
#include <thread>

/**
 * 线程基类
 */
class XThread {
public:
    /* 线程启动 */
    virtual void Start() {
        is_exit_ = false;
        th_ = std::thread(&XThread::Main, this);
    }
    /* 线程等待 */
    virtual void Wait() {
        if (th_.joinable()) {
            th_.join();
        }
    }
    /* 线程终止 */
    virtual void Stop() {
        is_exit_ = true;
    }

    bool is_exit() {
        return is_exit_;
    }
private:
    virtual void Main() = 0;
    std::thread th_;
    bool is_exit_ = false;
};

/**
 * 线程子类
 */
class TestXThread: public XThread {
public:
    void Main() override {
        std::cout << "TestXThread Main" << std::endl;
        while (!is_exit()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            std::cout << "." << std::flush;
        }
    }
    std::string name;
};

int main(int argc, char* argv[]) {
    TestXThread testXThread;
    testXThread.name = "TestXThread name";
    testXThread.Start();
    std::this_thread::sleep_for(std::chrono::seconds(3));
    testXThread.Stop();
    testXThread.Wait();
    return 0;
}

运行结果

TestXThread Main
..............................

只允许调用一次的函数

#include <iostream>
#include <thread>

using namespace std;

void SystemInit() {
    cout << "Call SystemInit" << endl;
}

void SystemInitOnce() {
    static once_flag flag;
    call_once(flag, SystemInit);
}

int main(int argc, char* argv[]) {
    for (int i = 0; i < 3; ++i) {
        thread th(SystemInitOnce);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

Call SystemInit

从结果可以看到,虽然我们启动了三个线程来同时处理SystemInitOnce函数,但是SystemInit只被调用了一次。

多线程通信和同步

  • 多线程状态(5个状态)
  1. 初始化(Init):该线程正在被创建
  2. 就绪(Ready):该线程在就绪列表中,等待CPU调度。
  3. 运行(Running):该线程正在运行。
  4. 阻塞(Blocked):该线程被阻塞挂起。Blocked状态包括:pend(锁、事件、信号量等阻塞)、suspend(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量等超时等待)。
  5. 退出(Exit):该线程运行结束,等待父线程回收其控制块资源。

  • 竞争状态和临界区
#include <iostream>
#include <thread>

using namespace std;

void TestThread() {
    cout << "===================" << endl;
    cout << "test 001" << endl;
    cout << "test 002" << endl;
    cout << "test 003" << endl;
    cout << "===================" << endl;
}

int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(TestThread);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

======================================
test 001=========================================================
test 001
test 002
===================test 003

test 001

===================

test 001
===================
test 002test 002


test 001test 002
test 003test 003test 001
test 002
test 003


===================
test 002===================



test 001
test 002
test 001test 003
======================================
test 003
test 003test 001
===================
===================


test 002
test 003======================================
test 002
======================================

test 003

===================


test 001
test 002
test 003
===================

从上面的结果可以看出,这个打印是杂乱无章的。因为是10个线程一起运行,完全看不出哪个线程先打印,哪个线程后打印。现在我们使用互斥锁来控制打印的顺序。

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
//互斥锁
static mutex mux;
void TestThread() {
    //如果有多个线程同时调用,则只能有一个线程进入
    mux.lock();
    cout << "===================" << endl;
    cout << "test 001" << endl;
    cout << "test 002" << endl;
    cout << "test 003" << endl;
    cout << "===================" << endl;
    //释放锁
    mux.unlock();
}

int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(TestThread);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================
===================
test 001
test 002
test 003
===================

从结果可以看出,它的打印是非常有次序的。再继续改进这个代码

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
//互斥锁
static mutex mux;
void TestThread() {
    //如果有多个线程同时调用,则只能有一个线程进入
    //mux.lock();
    for (;;) {
        if (!mux.try_lock()) {
            cout << "." << flush;
            this_thread::sleep_for(chrono::milliseconds(100));
            continue;
        }
        cout << "===================" << endl;
        cout << "test 001" << endl;
        cout << "test 002" << endl;
        cout << "test 003" << endl;
        cout << "===================" << endl;
        //释放锁
        mux.unlock();
        this_thread::sleep_for(chrono::milliseconds(1000));
    }
}

int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(TestThread);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

===================..
.test 001...
..test 002
test 003
===================
.===================
......test 001..
test 002
test 003
===================
...===================
test 001
test 002
test 003
.===================
..===================
test 001
test 002
test 003
===================
===================
.test 001
test 002
..test 003
..===================
===================
test 001
test 002
test 003
===================
===================

从结果可以看出,如果拿不到锁的线程就会打点,然后重新争夺锁资源。

  • 独占锁的可能
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
//互斥锁
static mutex mux;
void ThreadMainMux(int i) {
    for (;;) {
        mux.lock();
        cout << i << "[in]" << endl;
        this_thread::sleep_for(chrono::seconds(1));
        mux.unlock();
    }
}

int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(ThreadMainMux, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

0[in]
0[in]
0[in]
0[in]
0[in]
0[in]
0[in]
0[in]
0[in]
0[in]
0[in]

这里就很明显了,一直都是第0个线程强占了锁,其他的线程无法进入。现在我们让每一个线程释放锁之后停顿1毫秒

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
//互斥锁
static mutex mux;
void ThreadMainMux(int i) {
    for (;;) {
        mux.lock();
        cout << i << "[in]" << endl;
        this_thread::sleep_for(chrono::seconds(1));
        mux.unlock();
        this_thread::sleep_for(chrono::milliseconds(1));
    }
}

int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(ThreadMainMux, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

0[in]
3[in]
1[in]
2[in]
5[in]
4[in]
6[in]
7[in]
8[in]
9[in]
0[in]

这样就基本实现了Java中公平锁的功能。有关公平锁的概念可以参考线程,JVM锁整理 中的公平锁。

利用栈特性自动释放锁RAII

RAII(Resurce Acquisition Is Initalization) C++之父Bjarne Stroustrup提出;使用局部对象(栈中的对象)来管理资源的技术称为资源获取即初始化;它的生命周期是由操作系统来管理,无需人工介入;资源的销毁容易忘记,造成死锁或内存泄漏。

  • 自实现
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

/**
 * RAII 无需手动锁定和释放
 */
class XMutex {
public:
    XMutex(mutex &mux):mux_(mux) {
        cout << "lock" << endl;
        mux.lock();
    }

    ~XMutex() {
        cout << "unlock" << endl;
        mux_.unlock();
    }
private:
    mutex &mux_;
};

static mutex mux;

void testMutex(int status) {
    //当整个函数执行完,该锁会自动释放
    XMutex lock(mux);
    if (status == 1) {
        cout << "=1" << endl;
        return;
    } else {
        cout << "!=1" << endl;
        return;
    }
}
int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(testMutex, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

lock
!=1
unlock
locklocklocklocklocklock

lock

!=1

locklock


unlock
!=1
unlock

!=1
unlock
!=1
unlock
=1
unlock
!=1
unlock
!=1
unlock
!=1
unlock
!=1
unlock
  • C++11支持的RAII管理互斥资源lock_guard
  1. C++11实现严格基于作用域的互斥体所有权包装器
  2. adopt_lock C++11类型为adopt_lock_t,假设调用方已拥有互斥的所有权
  3. 通过{}控制锁的临界区,这里有可能只是锁部分代码,而不是整个函数。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

static mutex gmutex;

void testMutex(int status) {
    for (;;) {
        {   //只锁定括号中的代码执行,该括号中执行完释放锁
            lock_guard<mutex> lock(gmutex);
            cout << "In " << status << endl;
        }
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}
int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(testMutex, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

In 0
In 1
In 2
In 3
In 6
In 4
In 5
In 7
In 9
In 8
In 0
In 8
In 2
In 3
In 6
In 4
In 5
In 7
In 9

通过以上的运行结果,我们可以看出,锁并没有被某一个线程强占,而是各个线程都执行了。

unique_lock C++11

  1. unique_lock c++11实现可移动的互斥体所有权包装器
  2. 支持临时释放锁unlock
  3. 支持adopt_lock(已经拥有锁,不加锁,出栈区会释放)
  4. 支持defer_lock(延后拥有,不加锁,出栈区不释放)
  5. 支持try_to_lock尝试获得互斥的所有权而不阻塞,获取失败退出栈区不会释放,通过owns_lock()函数判断
  • 基本用法
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

static mutex gmutex;

void testMutex(int status) {
    for (;;) {
        {   //只锁定括号中的代码执行,该括号中执行完释放锁
            unique_lock<mutex> lock(gmutex);
            cout << "In " << status << endl;
            //临时释放锁
            lock.unlock();
            cout << "Temp unlock" << status << endl;
            //这里出栈时同样会释放
            lock.lock();
        }
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}
int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(testMutex, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

In 0
In Temp unlock0
7
Temp unlock7
In 3
Temp unlock3
In 5
In 6
Temp unlockTemp unlock6
In 8
5
Temp unlock8
In 9
Temp unlock9
In 4
Temp unlock4

这里的用法几乎跟lock_guard是一样的,只是它可以临时释放锁。

  • 参数设定
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

static mutex gmutex;

void testMutex(int status) {
    for (;;) {
        {   
            gmutex.lock();
            //表示已经拥有锁,不锁定,退出解锁
            unique_lock<mutex> lock(gmutex, adopt_lock);
            cout << "In " << status << endl;
        }
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}
int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(testMutex, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

In 0
In 1
In 5
In 4
In 6
In 3
In 7
In 8
In 2
In 9
In 0
In 5
In 1
In 4
In 6
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

static mutex gmutex;

void testMutex(int status) {
    for (;;) {
        {   //延后加锁,不拥有,退出栈区不解锁
            unique_lock<mutex> lock(gmutex, defer_lock);
            //加锁,退出栈区解锁
            lock.lock();
            cout << "In " << status << endl;
        }
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}
int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(testMutex, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

In 0
In 1
In 2
In 4
In 5
In 3
In 6
In 8
In 9
In 7
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

static mutex gmutex;

void testMutex(int status) {
    for (;;) {
        {   //尝试加锁,不阻塞,失败不拥有锁
            unique_lock<mutex> lock(gmutex, try_to_lock);
            //判断是否已经拿到了锁
            if (lock.owns_lock()) {
                cout << "In " << status << endl;
            } else {
                cout << "Not In " << status << endl;
            }
        }
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}
int main(int argc, char* argv[]) {
    for (int i = 0; i < 10; ++i) {
        thread th(testMutex, i);
        th.detach();
    }
    getchar();
    return 0;
}

运行结果

In Not In 01Not In 
2
Not In Not In 7
Not In Not In 8

9
Not In 3
Not In 6
Not In 5
4
In Not In Not In 02Not In 
7Not In 
Not In 5Not In 

这里跟之前最大的不同就是无论线程有没有拿到锁,它都可以往下执行,而不会阻塞在那里。

使用互斥锁+list进行线程通信

该程序的功能是由另个线程(非主线程)向一个list中发送数据,由于list并不是线程安全的,所以需要对list写入时加上线程锁来保障不会出现线程安全问题。该线程一旦创建就会执行Main方法,将list中的数据取出和打印。

先创建一个线程基类

XThread.h

#include <thread>

class XThread {
public:
    virtual void Start();
    virtual void Stop();
    virtual void Wait();
    bool is_exit();

private:
    //线程入口函数
    virtual void Main() = 0;
    bool is_exit_ = false;
    std::thread th_;
};

XThread.cpp

#include "XThread.h"
using namespace std;

void XThread::Start() {
    is_exit_ = false;
    th_ = thread(&XThread::Main, this);
}

void XThread::Stop() {
    is_exit_ = true;
    Wait();
}

void XThread::Wait() {
    if (th_.joinable()) {
        th_.join();
    }
}

bool XThread::is_exit() {
    return is_exit_;
}

线程子类

XMsgServer.h

#include "XThread.h"
#include <string>
#include <list>
#include <mutex>

class XMsgServer: public XThread {
public:
    //给当前线程发消息
    void SendMsg(std::string msg);
private:
    //处理消息的线程入口函数
    void Main() override;
    //消息队列
    std::list<std::string> msgs_;
    //互斥锁
    std::mutex mux_;
};

XMsgServer.cpp

#include <iostream>
#include "XMsgServer.h"
using namespace std;
using namespace this_thread;

void XMsgServer::SendMsg(std::string msg) {
    unique_lock<mutex> lock(mux_);
    msgs_.push_back(msg);
}

void XMsgServer::Main() {
    while (!is_exit()) {
        sleep_for(chrono::milliseconds(10));
        unique_lock<mutex> lock(mux_);
        if (msgs_.empty()) {
            continue;
        }
        while (!msgs_.empty()) {
            //消息处理业务逻辑
            cout << "recv: " << msgs_.front() << endl;
            msgs_.pop_front();
        }
    }
}

测试代码

#include "XMsgServer.h"
#include <sstream>
using namespace std;
using namespace this_thread;

int main(int argc, char* argv[]) {
    XMsgServer server;
    server.Start();
    for (int i = 0; i < 10; ++i) {
        stringstream ss;
        ss << " msg: " << i;
        server.SendMsg(ss.str());
        sleep_for(chrono::milliseconds(500));
    }
    server.Stop();
    return 0;
}

因为是多文件的项目,CMakeLists.txt内容如下

cmake_minimum_required(VERSION 3.21)
project(untitled2)
include_directories(./)
set(CMAKE_CXX_STANDARD 11)
add_library(XThread XThread.cpp)
add_library(XMsgServer XMsgServer.cpp)
set(LIBRARIES XThread XMsgServer)
add_executable(untitled2 main.cpp)
target_link_libraries(untitled2 ${LIBRARIES})

运行结果

recv:  msg: 0
recv:  msg: 1
recv:  msg: 2
recv:  msg: 3
recv:  msg: 4
recv:  msg: 5
recv:  msg: 6
recv:  msg: 7
recv:  msg: 8
recv:  msg: 9

条件变量-生产者消费者信号处理

  • 生产者-消费者模型
  1. 生产者和消费者共享资源变量(list队列)
  2. 生产者生产一个产品,通知消费者消费
  3. 消费者阻塞等待信号-获取信号后消费产品(取出list队列中数据)
#include <thread>
#include <iostream>
#include <mutex>
#include <list>
#include <string>
#include <sstream>
#include <condition_variable>

using namespace std;
using namespace this_thread;

list<string> msgs;
mutex mux;
//条件变量
condition_variable cv;
void ThreadWrite() {
    for (int i = 0;;i++) {
        stringstream ss;
        ss << "Write msg " << i;
        unique_lock<mutex> lock(mux);
        msgs.push_back(ss.str());
        lock.unlock();
        //通知某一个读取线程,发送信号
        cv.notify_one();
        //通知所有读取线程,发送信号
        //cv.notify_all();
        sleep_for(chrono::seconds(1));
    }
}

void ThreadRead(int i) {
    for (;;) {
        cout << "read msg" << endl;
        unique_lock<mutex> lock(mux);
        //解锁,阻塞,等待信号
        cv.wait(lock);
        //获取信号后锁定
        while (!msgs.empty()) {
            cout << i << "read " << msgs.front() << endl;
            msgs.pop_front();
        }
    }
}

int main(int argc, char* argv[]) {
    thread thw(ThreadWrite);
    thw.detach();
    for (int i = 0; i < 3; ++i) {
        thread thr(ThreadRead, i);
        thr.detach();
    }
    getchar();
    return 0;
}

运行结果

read msg
read msg
read msg
0read Write msg 0
0read Write msg 1
read msg
1read Write msg 2
read msg
2read Write msg 3
read msg
0read Write msg 4
read msg
1read Write msg 5
read msg
2read Write msg 6
read msg
0read Write msg 7

这里主要是针对多个线程来竞争资源的时候,我们允许其中的一个线程来激活处理业务。其实跟Java中重入锁的await()方法、singal() 方法类似。具体可以参考线程,JVM锁整理 中的与重入锁结伴的等待与通知。

现在我们使用之前的线程基类、子类的方式来重写上面的代码。

XThread.h

#include <thread>

class XThread {
public:
    virtual void Start();
    virtual void Stop();
    virtual void Wait();
    bool is_exit();

protected:
    bool is_exit_ = false;
    
private:
    //线程入口函数
    virtual void Main() = 0;
    std::thread th_;
};

is_exit_开放给子类;XThread.cpp不变

XMsgServer.h

#include "XThread.h"
#include <string>
#include <list>
#include <mutex>
#include <condition_variable>

class XMsgServer: public XThread {
public:
    //给当前线程发消息
    void SendMsg(std::string msg);
    //重载线程退出方法,防止被cv_.wait一直阻塞
    void Stop() override;
private:
    //处理消息的线程入口函数
    void Main() override;
    //消息队列
    std::list<std::string> msgs_;
    //互斥锁
    std::mutex mux_;
    //条件变量
    std::condition_variable cv_;
};

XMsgServer.cpp

#include <iostream>
#include "XMsgServer.h"
using namespace std;
using namespace this_thread;

void XMsgServer::Stop() {
    is_exit_ = true;
    cv_.notify_all();
    Wait();
}

void XMsgServer::SendMsg(std::string msg) {
    unique_lock<mutex> lock(mux_);
    msgs_.push_back(msg);
    lock.unlock();
    cv_.notify_one();
}

void XMsgServer::Main() {
    while (!is_exit()) {
        //sleep_for(chrono::milliseconds(10));
        unique_lock<mutex> lock(mux_);
        //此处会进行阻塞,不会一直处于循环状态
        //[this] {...}是一个lambda表达式
        cv_.wait(lock, [this] {
            //如果退出不阻塞
            if (is_exit()) {
                return true;
            }
            return !msgs_.empty();
        });
//        if (msgs_.empty()) {
//            continue;
//        }
        while (!msgs_.empty()) {
            //消息处理业务逻辑
            cout << "recv: " << msgs_.front() << endl;
            msgs_.pop_front();
        }
    }
}

测试代码也与之前相同,运行结果

recv:  msg: 0
recv:  msg: 1
recv:  msg: 2
recv:  msg: 3
recv:  msg: 4
recv:  msg: 5
recv:  msg: 6
recv:  msg: 7
recv:  msg: 8
recv:  msg: 9

线程异步和通信

线程异步的主要作用是当一个线程需要从另一个线程获取返回值,但是另一个线程可能执行的时间会比较长,那么获取值的线程就会阻塞等待。

#include <thread>
#include <iostream>
#include <string>
#include <future>

using namespace std;
using namespace this_thread;

void TestFuture(promise<string> p) {
    cout << "begin TestFuture" << endl;
    sleep_for(chrono::seconds(1));
    cout << "begin set vlaue" << endl;
    p.set_value("TestFuture value");
    sleep_for(chrono::seconds(1));
    cout << "end TestFuture" << endl;
}

int main(int argc, char* argv[]) {
    //异步传输变量存储
    promise<string> p;
    //用来获取线程异步值
    auto future = p.get_future();
    thread th(TestFuture, move(p));
    //future.get()会阻塞等待p.set_value("TestFuture value")的值
    cout << "future get =" << future.get() << endl;
    th.join();
    return 0;
}

运行结果

future get =begin TestFuture
begin set vlaue
TestFuture value
end TestFuture

这个性质其实跟Java中的Future接口是一样的,具体可以参考Springboot2吞吐量优化的一些解决方案 以及分布式秒杀 中的内容,只不过future.get()可以设置一个超时时间。

packaged_task异步调用函数打包

packaged_taskpromise相比可以设置超时时间。

#include <thread>
#include <iostream>
#include <string>
#include <future>

using namespace std;
using namespace this_thread;

string TestPack(int index) {
    cout << "begin Test Pack " << index << endl;
    sleep_for(chrono::milliseconds(600));
    return "Test Pack Return";
}

int main(int argc, char* argv[]) {
    //packaged_task包装函数为一个对象,用于异步调用
    packaged_task<string(int)> task(TestPack);
    auto result = task.get_future();
    thread th(move(task), 100);
    cout << "begin result get" << endl;
    //测试是否超时,总超时时间设为1秒,100ms*10=1s
    for (int i = 0; i < 10; ++i) {
        if (result.wait_for(chrono::milliseconds(100)) != future_status::ready) {
            continue;
        } else {
            cout << "status ok " << i << endl;
            break;
        }
    }
    if (result.wait_for(chrono::milliseconds(100)) == future_status::timeout) {
        cout << "wait result timeout" << endl;
    } else {
        //result.get()会阻塞,等待线程函数返回结果
        cout << "result get " << result.get() << endl;
    }
    th.join();
    return 0;
}

运行结果

begin result get
begin Test Pack 100
status ok 5
result get Test Pack Return

如果我们将TestPack中的sleep_for(chrono::milliseconds(600))改成2秒,也就是2000毫秒,则结果为

begin result get
begin Test Pack 100
wait result timeout

async c++11

async可以不需要手动创建线程对象,自身可以创建线程,并同时具有future功能。

#include <thread>
#include <iostream>
#include <string>
#include <future>

using namespace std;
using namespace this_thread;

string TestAsync(int index) {
    cout << "begin Test Async " << index << " " << get_id() << endl;
    sleep_for(chrono::milliseconds(2000));
    return "Test Async Return";
}

int main(int argc, char* argv[]) {
    //打印主线程id
    cout << "main thread id " << get_id() << endl;
    //不创建线程启动异步任务
    auto future = async(launch::deferred, TestAsync, 100);
    cout << "begin future get" << endl;
    //当设置launch::deferred时,只有运行到future.get()或者future.wait()时才会去调用TestAsync函数
    cout << "future.get() = " << future.get() << endl;
    cout << "end future get" << endl;
    return 0;
}

运行结果

main thread id 0x11469fdc0
begin future get
future.get() = begin Test Async 100 0x11469fdc0
Test Async Return
end future get

通过以上结果,我们可以看到主线程的id跟TestAsync打印出来的线程id是一样的,也就是说它并不是由多线程来调用的。

#include <thread>
#include <iostream>
#include <string>
#include <future>

using namespace std;
using namespace this_thread;

string TestAsync(int index) {
    cout << "begin Test Async " << index << " " << get_id() << endl;
    sleep_for(chrono::milliseconds(2000));
    return "Test Async Return";
}

int main(int argc, char* argv[]) {
    //打印主线程id
    cout << "main thread id " << get_id() << endl;
    //默认创建线程启动异步任务
    auto future = async(TestAsync, 100);
    cout << "begin future get" << endl;
    //当设置launch::deferred时,只有运行到future.get()或者future.wait()时才会去调用TestAsync函数
    //默认时会阻塞等待线程结果
    cout << "future.get() = " << future.get() << endl;
    cout << "end future get" << endl;
    return 0;
}

运行结果

main thread id 0x111d2bdc0
begin future get
future.get() = begin Test Async 100 0x7000037be000
Test Async Return
end future get

通过以上结果,我们可以看到主线程的id跟TestAsync打印出来的线程id是不同的,也就是说它是由多线程来调用的。

C++技巧篇

共享内存

内存写入

#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string.h>


int main()
{
    //创建一个共享内存
    int shmid = shmget(100,4096,IPC_CREAT|0664);
    printf("shmid:%d\n",shmid);

    //和当前进程进行关联
    void* ptr = shmat(shmid,NULL,0);

    //需要写入共享内存的数据
    char* str="hello world";
    //将该输入拷贝进共享内存中
    memcpy(ptr,str,strlen(str)+1);

    printf("按任意键继续\n");
    getchar();

    //解除关联
    shmdt(ptr);

    //删除共享内存
    shmctl(shmid,IPC_RMID,NULL);


    return 0;
}

内存读取

#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string.h>


int main()
{
    //获取一个共享内存
    int shmid = shmget(100,0,IPC_CREAT);
    printf("shmid:%d\n",shmid);

    //和当前进程进行关联
    void* ptr = shmat(shmid,NULL,0);

    //读取并打印共享内存中的数据
    printf("%s\n",(char*)ptr);

    printf("按任意键继续\n");
    getchar();

    //解除关联
    shmdt(ptr);

    //删除共享内存
    shmctl(shmid,IPC_RMID,NULL);


    return 0;
}

判断网络是否联通

#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <array>
#include <stdio.h>

using namespace std;

bool checkNet()
{
    vector<string> v;
    array<char, 128> buffer;
    unique_ptr<FILE, decltype(&pclose)> pipe(popen("ping 192.168.3.244 -c 2 -w 2 ", "r"), pclose);
    if (!pipe)
    {
        throw runtime_error("popen() failed!");
    }
    while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
    {
        cout << "qqq: " << buffer.data() << endl;
        v.push_back(buffer.data());
    }

    // 读取倒数第二行 2 packets transmitted, 2 received, 0% packet loss, time 1001ms
    if (v.size() > 1)
    {
        string data = v[v.size() - 2];
        int iPos = data.find("received,");
        if (iPos != -1)
        {
            data = data.substr(iPos + 10, 3); // 截取字符串返回packet loss
            int n = atoi(data.c_str());
            if (n == 0)
                return 1;
            else
                return 0;
        }
    }
    else
    {
        return 0;
    }
}
int main() {
    if (checkNet()) {
        printf("网络连接成功\n");
    } else {
        printf("网络连接失败\n");
    }
    return 0;
}

重启Ubuntu系统

#include <unistd.h>
#include <sys/reboot.h>

int main() {
    sync();
    reboot(RB_AUTOBOOT);
    return 0;
}

编译后,运行需要加root权限

sudo ./reboot

生成Python调用库

安装依赖

sudo apt-get install --no-install-recommends libboost-all-dev

编辑C++调用函数

hw.cpp

#include <iostream>

using namespace std;

void test() {
    cout << "hello, world\n" << endl;
}

编辑Python可调用的C++库

hwpy.cpp

#include <boost/python.hpp>
#include "hw.cpp"

using namespace boost::python;

BOOST_PYTHON_MODULE(hw) {
    def("test", test);
}

编译

g++ hwpy.cpp -fPIC -shared -I/home/dell/anaconda3/envs/pytorch/include/python3.9 -lboost_python310 -o hw.so

使用Python执行

conda activate pytorch
python
>>> import hw
>>> hw.test()
hello, world

Windows C++

创建动态链接库dll

我们这里使用的是Visual Studio 2022 社区版,这里我们只安装C++桌面开发。

我们创建的项目如下

单启动项

Dll1为项目依赖

ClassLib.h

#pragma once
class __declspec(dllexport) ClassLib
{
public:
	ClassLib();
	~ClassLib();
};

这里的__declspec(dllexport)表示生成Windows的.lib文件。

ClassLib.cpp

#include "pch.h"
#include "ClassLib.h"
#include <iostream>

using namespace std;

ClassLib::ClassLib() {
	cout << "Party time" << endl;
}

ClassLib::~ClassLib() {

}

将动态链接库Dll1生成,此时会看到在ConsoleApplication1\x64\Debug生成了Dll1.dll以及Dll1.lib

在启动项目的链接器的输入的附加依赖库中添加Dll1.lib

添加头文件,在C/C++的常规的附加包含目录中添加ClassLib.h的目录

添加Dll1.lib的目录,在链接器的附加库目录中填入

ConsoleApplication1.cpp

#include "ClassLib.h"

int main()
{
    ClassLib cl;
    return 0;
}

运行结果

Party time

使用Clang编译C++

Clang是苹果公司开发的C/C++编译器,结合LLVM(底层虚拟机)。包括预处理 (Preprocess),语法 (lex),解析 (parse),语义分析 (Semantic Analysis),抽象语法树生成 (Abstract Syntax Tree) 的时间,Clang 比 GCC 快2倍多。

sudo apt install clang

代码依然使用最简单的方式

a.cpp

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

编译

clang++ a.cpp -o a

运行结果

Hello, World!

 

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
2 收藏
0
分享
返回顶部
顶部