先写一个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"; // 此时str和str1相同,返回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; // 此时是jqnnqyqtnf与helloworld比较,j比h大2,返回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++来进行编译的过程,但是在实际的工程项目中,它的过程要复杂的多。一般包含
- 配置环境(系统环境)
- 确定标准库和头文件的位置
- 确定依赖关系(源代码之间编译的依赖关系)
- 头文件预编译
- 预处理
- 编译
- 链接
- 安装
- 和操作系统建立联系
- 生成安装包
- 当依赖关系复杂的时候,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的动态链接库文件。上面的内容改动较大,我们做一个说明:
- .o文件实际上是.cpp文件编译后的二进制代码,我们一般称为目标文件。它是一段没有真实机器内存地址但是有地址偏移量的代码,所以它不能被真正执行。.so文件更接近于可执行程序,它跟可执行程序之间的区别只是文件头部(header)中的几位,其他对于执行指令和地址访问跟可执行程序是一样的。
- CXXFLAGS = -c -fPIC表示C++的一些编译选项,-c表示生成一个目标文件,-fPIC表示生成一个可以共享的动态链接库,它的好处是可以不用生成多个库,多个应用程序可以使用动态加载的方式使用同一份代码。
- $(CXX)是一个隐含环境变量,它的值就是g++;$(CXX) main.o -o $(TARGET) -L. -lreply -Wl,-rpath ./表示生成可执行程序main,-L.表示指定当前路径为生成路径,-lreply表示使用reply这个动态链接库,-Wl,-rpath ./表示动态链接库的链接地址为当前路径。
- $(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的文件来生成的。
自动生成和部署
- 项目生成和部署
一般来说,至少有下面的目录:
- src目录下是头文件的实现文件
- include目录下是用到的头文件
- bin目录下是可运行文件
- 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个状态)
- 初始化(Init):该线程正在被创建
- 就绪(Ready):该线程在就绪列表中,等待CPU调度。
- 运行(Running):该线程正在运行。
- 阻塞(Blocked):该线程被阻塞挂起。Blocked状态包括:pend(锁、事件、信号量等阻塞)、suspend(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量等超时等待)。
- 退出(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
- C++11实现严格基于作用域的互斥体所有权包装器
- adopt_lock C++11类型为adopt_lock_t,假设调用方已拥有互斥的所有权
- 通过{}控制锁的临界区,这里有可能只是锁部分代码,而不是整个函数。
#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
- unique_lock c++11实现可移动的互斥体所有权包装器
- 支持临时释放锁unlock
- 支持adopt_lock(已经拥有锁,不加锁,出栈区会释放)
- 支持defer_lock(延后拥有,不加锁,出栈区不释放)
- 支持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
条件变量-生产者消费者信号处理
- 生产者-消费者模型
- 生产者和消费者共享资源变量(list队列)
- 生产者生产一个产品,通知消费者消费
- 消费者阻塞等待信号-获取信号后消费产品(取出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_task跟promise相比可以设置超时时间。
#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!