C++简单入门
VisualStudio 编写c++ 代码,参考: https://www.zhihu.com/question/30315894/answers/updated
1. 简单入门
1. helloworld
// study1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> int main() { std::cout << "Hello World!\n"; return 0; } // 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单 // 调试程序: F5 或调试 >“开始调试”菜单 // 入门使用技巧: // 1. 使用解决方案资源管理器窗口添加/管理文件 // 2. 使用团队资源管理器窗口连接到源代码管理 // 3. 使用输出窗口查看生成输出和其他消息 // 4. 使用错误列表窗口查看错误 // 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目 // 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件
结果会打印hello world!
2. 调用方法实现求和
// & - 指针运算符,返回变量的地址。例如 &a; 将给出变量的实际地址。 // * - 指针运算符.指向一个变量。例如,*var; 将指向变量 var。 #include <iostream> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; // 简单的相加 int addNum(int i) { return i+1; } // 传引用后,对引用的值自增 void addNum2(int* i) { int temp = *i; *i = temp+1; } int main() { int num = 0; int num2 = addNum(num); cout << num2 << endl; // 1 cout << num << endl; // 0 addNum2(&num); addNum2(&num); cout << num << endl; // 2 }
在C++中*代表指针,而&代表引用,而*&代表指针引用
指针是一个变量(它的值是一个地址),而指针引用指的是这个变量的引用;在C++中如果参数不是引用的话会调用参数对象的拷贝构造函数(习惯java语法的函数调用直接引用传递就好了),所以如果有需求想改变指针所指的对象(换句话说,就是要改变指针里面存的地址),就要使用指针引用。
2. 多线程
c++11 提供了新的创建线程的方式。
#include <thread> // 创建一个thread 对象,名称为t。 fun是需要调用的函数的名称, param 是调用的参数 thread t(fun, param) t.detach(); // 异步操作 t.join(); // 等待上面fun 函数结束后执行, 相当于java 的join
也可以使用lambda 表达式,两者结合一起实现一个线程。
#include <thread> // 创建一个thread 对象,名称为t。 fun是需要调用的函数的名称, param 是调用的参数 thread t([闭包变量](paramType param) { // code }, param); t.detach(); // 异步操作 t.join(); // 等待上面fun 函数结束后执行, 相当于java 的join
例如:
(1) 不使用lambda
// pro1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> #include <thread> using namespace std; int addNum(int i) { cout << std::this_thread::get_id() << endl; return i + 1; } int main() { cout << "main" << std::this_thread::get_id() << endl; // 创建一个thread 对象,名称为t。 fun是需要调用的函数的名称, param 是调用的参数 thread t(addNum, 1); // t.detach(); // 异步操作 t.join(); // 等待上面fun 函数结束后执行, 相当于java 的join }
(2) 使用lambda
// pro1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> #include <thread> using namespace std; int main() { cout << "main" << std::this_thread::get_id() << endl; // 创建一个thread 对象,名称为t。 fun是需要调用的函数的名称, param 是调用的参数 int num = 1; thread t([num](int num2) { cout << std::this_thread::get_id() << endl; cout << num2 + num << endl; // 4 }, 3); // t.detach(); // 异步操作 t.join(); // 等待上面fun 函数结束后执行, 相当于java 的join }
1. 开100个线程实现求和
#include <iostream> #include <thread> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; // 传引用后,对引用的值自增 void addNum2(int* i) { *i = ++ * i; } int main() { int num = 0; for (int i = 0; i < 100; i++) { thread t(addNum2, &num); // 允许后台执行 t.detach(); } // 主线程休眠3s, 等待上面线程执行完毕 this_thread::sleep_for(std::chrono::seconds(3)); cout << "num\t" << num << endl; return 0; }
结果:
num 100
2. 存在线程安全问题:
#include <iostream> #include <thread> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; // 传引用后,对引用的值自增 void addNum2(int* i) { int temp = *i; temp++; cout << "addNum2" << std::this_thread::get_id() << endl; *i = temp; } int main() { int num = 0; for (int i = 0; i < 100; i++) { thread t(addNum2, &num); // 允许后台执行 t.detach(); } // 主线程休眠3s, 等待上面线程执行完毕 this_thread::sleep_for(std::chrono::seconds(3)); cout << "num\t" << num << endl; return 0; }
这里num 一直为2. 猜测是因为cout, 耗时比较长。 所以30个线程同时拿到为0的数据加一后在cout执行长时间操作后都改为1.
3. 线程安全问题
上面多线程求和有线程安全的问题,在java 里面一般会使用原子类或者使用synchronized、lock 进行同步控制。下面研究c++ 的线程安全机制。
1. std::mutex 加锁
#include <iostream> #include <mutex> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; int main() { std::mutex _mutex; _mutex.lock(); cout << "getLock: " << std::this_thread::get_id << endl; _mutex.unlock(); cout << "unlock: " << std::this_thread::get_id << endl; }
结果:
这个锁好像不支持重入,也就是一个线程不能多次lock。
加锁解决上面的问题:
#include <iostream> #include <thread> #include <mutex> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; std::mutex _mutex; // 传引用后,对引用的值自增 void addNum2(int* i) { _mutex.lock(); int temp = *i; temp++; cout << "addNum2" << std::this_thread::get_id() << endl; *i = temp; _mutex.unlock(); } int main() { int num = 0; for (int i = 0; i < 100; i++) { thread t(addNum2, &num); // 允许后台执行 t.detach(); } // 主线程休眠3s, 等待上面线程执行完毕 this_thread::sleep_for(std::chrono::seconds(3)); cout << "num\t" << num << endl; return 0; }
2. lock_guard 加锁
lock_guard 用来管理一个 std::mutex对象,通过定义一个 lock_guard 一个对象来管理 std::mutex 的上锁和解锁。在 lock_guard 初始化的时候进行上锁,然后在 lock_guard 析构的时候进行解锁。这样避免对 std::mutex 的上锁和解锁的管理。
它的特点如下:
(1) 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
(2) 不能中途解锁,必须等作用域结束才解锁
(3) 不能复制
#include <iostream> #include <thread> #include <mutex> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; std::mutex _mutex; // 传引用后,对引用的值自增 void addNum2(int* i) { const std::lock_guard<std::mutex> lock(_mutex); int temp = *i; temp++; cout << "addNum2" << std::this_thread::get_id() << endl; *i = temp; } int main() { int num = 0; for (int i = 0; i < 100; i++) { thread t(addNum2, &num); // 允许后台执行 t.detach(); } // 主线程休眠3s, 等待上面线程执行完毕 this_thread::sleep_for(std::chrono::seconds(3)); cout << "num\t" << num << endl; return 0; }
效果同上面加锁一样。
查看其源码:
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock _MyMutex.lock(); } lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock ~lock_guard() noexcept { _MyMutex.unlock(); }
3. unique_lock
unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。unique_lock比lock_guard使用更加灵活,功能更加强大。使用unique_lock需要付出更多的时间、性能成本。
1. 自动加锁解锁:
#include <iostream> #include <thread> #include <mutex> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; std::mutex _mutex; // 传引用后,对引用的值自增 void addNum2(int* i) { std::unique_lock<std::mutex> lock(_mutex); // 等价于 std::lock_guard<std::mutex> lock(_mutex); 自动加锁解锁 int temp = *i; temp++; cout << "addNum2" << std::this_thread::get_id() << endl; *i = temp; } int main() { int num = 0; for (int i = 0; i < 100; i++) { thread t(addNum2, &num); // 允许后台执行 t.detach(); } // 主线程休眠3s, 等待上面线程执行完毕 this_thread::sleep_for(std::chrono::seconds(3)); cout << "num\t" << num << endl; return 0; }
2. 手动加锁解锁
#include <iostream> #include <thread> #include <mutex> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; std::mutex _mutex; // 传引用后,对引用的值自增 void addNum2(int* i) { std::unique_lock<std::mutex> lock(_mutex, std::defer_lock); lock.lock(); int temp = *i; temp++; cout << "addNum2" << std::this_thread::get_id() << endl; *i = temp; lock.unlock(); // 这句可以不写,让析构函数自动释放锁 } int main() { int num = 0; for (int i = 0; i < 100; i++) { thread t(addNum2, &num); // 允许后台执行 t.detach(); } // 主线程休眠3s, 等待上面线程执行完毕 this_thread::sleep_for(std::chrono::seconds(3)); cout << "num\t" << num << endl; return 0; }
补充: 递归锁的使用
#include <iostream> #include <mutex> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; std::recursive_mutex _mutex; int main() { std::cout << "Hello World!\n"; cout << "currentThreadId: " << std::this_thread::get_id() << endl; _mutex.lock(); _mutex.lock(); _mutex.unlock(); _mutex.unlock(); std::cout << "Hello World!\n"; return 0; }
4. 线程间通信
实现一个有界阻塞队列, 也可以理解为生产者消费者模式的实现。使用锁加条件变量实现线程安全加线程间通信。
#include <iostream> #include <mutex> #include <vector> #include <condition_variable> #include <list> using namespace std; class MyBlockingList { private: int capacity = 3; std::list<int> datas; std::mutex _mutex; std::condition_variable not_full_cond; std::condition_variable not_emp_cond; public: int getCapacity() { return capacity; } void setCapacity(int capacityParam) { capacity = capacityParam; } MyBlockingList(int capacityParam) : capacity(capacityParam) {} void add(int num) { std::unique_lock<std::mutex> lock(_mutex); while (capacity == datas.size()) { not_full_cond.wait(lock); } cout << std::this_thread::get_id() << " produce: " << num << "\n"; datas.push_back(num); not_emp_cond.notify_all(); } int consume() { std::unique_lock<std::mutex> lock(_mutex); while (datas.size() == 0) { not_emp_cond.wait(lock); } int num = datas.front(); datas.pop_front(); not_full_cond.notify_all(); cout << std::this_thread::get_id() << " consume: " << num << "\n"; return num; } }; // 通过函数传递需要传递引用 void test(MyBlockingList& listData) { cout << "main currentThreadId: " << listData.getCapacity() << "\n"; listData.setCapacity(8); } int main() { MyBlockingList myblocking(30); vector<thread> threads; int num = 0; for (int i = 0; i < 5; i++) { if (i % 2 == 0) { threads.push_back(thread([&myblocking, &num]() { while (true) { this_thread::sleep_for(std::chrono::seconds(2)); num++; // cout << std::this_thread::get_id() << " prepare produce: " << num << "\n"; myblocking.add(num); } })); } else { threads.push_back(thread([&myblocking]() { while (true) { this_thread::sleep_for(std::chrono::seconds(2)); myblocking.consume(); } })); } } for (int i = 0; i < 3; i++) { threads.at(i).join(); } return 0; }
上面代码实际类似于java 中的下面代码。 生产和消费的时候获取锁,获取到锁之后进行生产消费。到达队列最大大小或者队列为空后分别进行等待。
package com.xm.ggn.test; import org.apache.commons.lang3.RandomStringUtils; import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class PlainTest { public static void main(String[] args) throws InterruptedException { int maxPoolSize = 5; ContainContext containContex = new ContainContext(maxPoolSize); // 基于wait notify 实现生产者消费者 int producerNum = 3; int consumerNum = 1; // producer for (int index = 0; index < producerNum; index++) { Thread producer = new Thread(() -> { while (true) { try { Thread.sleep(1 * 1000); } catch (InterruptedException e) { } containContex.addElement(RandomStringUtils.randomNumeric(3)); } }); producer.setName("producer" + index); producer.start(); } // producer for (int index = 0; index < consumerNum; index++) { Thread producer = new Thread(() -> { while (true) { try { Thread.sleep(1 * 1000); } catch (InterruptedException e) { } containContex.removeFirst(); } }); producer.setName("consumer" + index); producer.start(); } } } class ContainContext { private ReentrantLock lock = new ReentrantLock(); private Condition producerCondition = lock.newCondition(); private Condition consumerCondition = lock.newCondition(); private LinkedList<String> container = new LinkedList<>(); private int maxSize; public ContainContext(int maxSize) { this.maxSize = maxSize; } public void addElement(String t) { lock.lock(); try { // 达到最大值,阻塞生产者 while (container.size() == maxSize) { producerCondition.await(); consumerCondition.signalAll(); } container.add(t); System.out.println("tName: " + Thread.currentThread().getName() + " 生产消息: " + t); consumerCondition.signalAll(); } catch (Exception e) { // ignore } finally { lock.unlock(); } } public String removeFirst() { lock.lock(); try { while (container.size() == 0) { consumerCondition.await(); producerCondition.signalAll(); } String removed = container.remove(); System.out.println("tName: " + Thread.currentThread().getName() + " 消费消息: " + removed); consumerCondition.signalAll(); return removed; } catch (Exception e) { // ignore } finally { lock.unlock(); } return ""; } }
补充:关于cond1.notify_all 如果不手动释放锁,是在等锁作用域结束自动释放后才会notify
#include <iostream> #include <mutex> #include <condition_variable> #include <thread> using namespace std; std::mutex _mutex; std::condition_variable cond1; void addNum() { std::unique_lock<std::mutex> lock(_mutex); cout << std::this_thread::get_id() << " wait" << endl; cond1.wait(lock); cout << std::this_thread::get_id() << " end wait" << endl; } int main() { cout << "main " << std::this_thread::get_id() << endl; thread t(addNum); t.detach(); cout << "main " << std::this_thread::get_id() << " sleep" << endl; this_thread::sleep_for(std::chrono::seconds(3)); std::unique_lock<std::mutex> lock(_mutex); cond1.notify_all(); lock.unlock(); // 这里必须手动释放锁, 否则会等到锁作用域结束锁自动释放才会进行notify。 this_thread::sleep_for(std::chrono::seconds(3)); cout << "main " << std::this_thread::get_id() << " end" << endl; return 0; }
简单了解下c++ 关于线程、同步、以及基于条件的线程通信的方式。
补充: C++ #define 宏的用法
#include <iostream> // 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std,这样才能使用类似于cout这样的C++标识符 using namespace std; // # define 相当于定义宏。 // 先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。 // 一般会将宏加括号,防止产生的其他歧义 #define N 2+2 #define M (2+2) int main() { cout << N << endl; cout << M << endl; // 等价于 int a = 2+2*2+2 int a = N * N; cout << a << endl; // 等价于 int a = (2+2)*(2+2) int b = M * M; cout << b << endl; return 0; }
结果:
4 4 8 16
补充: .h 文件以及#include 引入
.h 文件是cpp的头文件,我自己理解可以将它看作java 的工具类,提取一些变量、方法、类的结构定义等。
1. 项目结构
2. head1.h
#pragma once #define N 2+2 #define M (2+2) // 简单的相加 int addNum(int i) { return i + 1; }
3. client1.cpp
#include <iostream> using namespace std; #include "head1.h" /* * #include 叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。 * #include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。 * #include 的用法有两种,如下所示: #include <head1.h> #include "head1.h" 使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同: 使用尖括号< >,编译器会到系统路径下查找头文件; 使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。也就是双引号功能更为强大。 */ int main() { cout << N << endl; cout << M << endl; // 等价于 int a = 2+2*2+2 int a = N * N; cout << a << endl; // 等价于 int a = (2+2)*(2+2) int b = M * M; cout << b << endl; cout << addNum(a) << endl; return 0; }
结果:
4 4 8 16 9