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

 

posted @ 2021-11-20 22:43  QiaoZhi  阅读(583)  评论(0编辑  收藏  举报