c++上手记录

快学快用式的学习,主要目的在于快速上手应用,深入学习放在之后进行

Posted by R on 2023-06-15

前言

最近项目上屡屡碰到需要使用c++的地方,并且随着各种方面开发的进行觉得实在应该扎扎实实的学习一门语言吃饭,无奈时间实在不太够用,于是先从上手的角度对c++进行了初步学习,起码达到能看懂程序的阶段。然后根据需求写了关于opencv的几个函数,并且尝试了多线程的处理,效果是很理想的,多线程的方式简直就是解决卡顿最大的利器。刚刚开始入门,很多语句还看不太懂,先把我应用实现的一些小功能做个记录,方便以后回看。

多线程上手记录

快速上手就是啥不懂看啥,先要了解 c++ 是什么东西,官方来说C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。但是对于我目前了解的,就是c++必须经过编译才可以执行(以linux端为例讲的,win端可能有所不同,还没去研究)。像我是从python开始上手编程的,就很不理解为什么不可以直接执行,要编译以后才可以执行。于是我专门去搜了搜,了解到python其实也是需要编译的,只是它把这一过程内置成解释器了,你在执行的过程中每一句其实都要经过翻译再执行,而c++或c则是编译好再执行,这也是python速度慢最大的原因。至于过程化编程和泛型编程我不太懂,等到以后掌握的更加深入后再去体会,面向对象编程则是c++的一个很大的特点,我也只有一个朦胧的概念。

面向对象是和面向过程相区分的,像c就是面向过程,c++则是面向对象。面向对象关注的是对象,这个对象能做什么,有什么样的属性;而面向过程关注的是过程,这个过程内有什么流程,应该有什么顺序。这是我理解的区分,面向过程把一件事分成每个步骤来进行处理,面向对象则把一件事涉及到的各个角色赋予功能,让他们去共同执行。具体体会可以去搜搜网上的很多例子,在此就不做赘述。还有c++编译器等的安装,不同平台的安装办法,这些都是程序性的东西,不需要进行记录。

基本语法

python中模块的导入是import,在c++中,模块变成了头文件,import也变成了 #include<>。在导入头文件后一般需要定义命名空间,这是c++的一个比较独特的概念,我目前还没有了解学习,刚开始就按别人的程序导入了什么文件定义什么空间就好。
单行注释是 //,多行注释则是 / 开头,/ 结尾 。另外c++中每个语句都需要以分号做结尾,c++不以行末作为结束符的标识,因此可以在一行上用分号断开放置多个语句。空格标识符等要求同其它语言相差不大,遇到时再查找即可。

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

// main() 是程序开始执行的地方

int main()
{
cout << "Hello World"; // 输出 Hello World
return 0;
}

以上为一段简单的c++程序,根据刚才的讲述,可能只会对主程序中的第一句产生疑问。这种样子的语句在c++程序中会经常使用到,称为输入输出运算符。输入输出是数据传送的过程,c++中将此过程形象的称为流,在输入操作时,字节流从输入设备流向内存;在输出操作时,字节流从内存流向输出设备。流中的内容可以是ASCII码值、二进制形式数据、数字音频视频、图形图像或者其他形式的信息。

在c++中,输入输出流被定义为类,c++的I/O库中的类为流类,用流类定义的对象称为流对象。

标准输入设备与输入:
cin 是标准输入设备(相当于键盘),连续从键盘读取数据,”>>”为提取运算符。
输入的使用:cin >> 变量 (cin在输入字符串时,空格作为结束符)

标准输出设备与输出:
cout 是标准输出设备(相当于屏幕),”<<”为插入运算符。
输出的使用:cout << 输出项 << endl (endl 相当于换行符”\n”)

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

using namespace std;

int main()
{
int a;
char ch;
cout << "请输入一个常数" << endl;
cin >> a;

cout << "请输入一个字母" << endl;
cin >> ch;

cout << "常数为" << a << endl;
cout << "字母为" << ch << endl;
return 0;
}

1

以上流传输的原理是在内存中为每个数据流开辟一个内存缓冲区,用来存放流中的数据。当用cin和>>输入数据时,从键盘获取的数据先放入键盘的缓冲区中,按回车键,键盘缓冲区数据输入到程序中输入缓冲区,形成cin流,然后用提取符”>>”从输入缓冲区中提取数据给程序中相关变量。

在输出时,先将数据放入程序中的输出缓冲区保存,缓冲区满了或遇到endl时,将缓冲区中的数据传到显示器显示出来。

ios是抽象基类,派生出istream类和ostream类,”i”和”o”分别代表输入和输出,由istream类和ostream类经过多重继承派生出的类为iostream类,支持输入输出操作,iostream类库中包含许多用于输入输出的类。因此,一般iostream类是必要的导入类。

常用结构

掌握了基本的语法结构以后,还需要对常用的一些逻辑结构有所掌握,循环、判断是上手阶段必须学会用的,其它功能可以暂且放一放。另外,有很多内容是没有涉及的,比如数据类型、变量类型、常量等等,这些内容无论哪个都值得深入研究,不适合在快速上手的阶段去了解,现在凭着一些基础的常识足以应付。

循环结构主要是while循环和for循环,和其它程序差不太多。判断语句主要是if&else语句以及switch,由于我上手阶段只用到常用的if和for循环,因此不作对比。

多线程

多线程涉及上属于c++更高级的一部分概念,只是因为现在需要立马用才会进行学习,因此可能一些点讲的不是很透彻,只针对多线程本身作相对深入的讲解。传统的c++并没有引入线程概念,从c++11标准以后才有了头文件thread,提供了语言层面上的多线程,因此在操作前请先确定c++版本。

同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。但需要注意由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁。

在我的需求中,我目前通过opencv循环的方式显示摄像头画面,并且一旦识别到工件会对继电器发出信号控制吹气阀门通断。在之前的程序中这一部分就比较耗时,现在新的需求需要延时控制通断,如果直接在循环中进行处理会导致画面非常卡顿,甚至可能会由于重复发送语句引起阻塞。因此我需要通过分支线程来解决这个问题,控制分支线程进行延时,在主程序中只需要启动分支线程即可。

  1. 线程初步

    在头文件中引入线程文件后就可以开始对线程进行操作:

    1
    #include <thread>

    先定义一个函数void thread_1(),之后线程开启将执行所定义的函数。(需要注意分支线程一般都是用于处理主程序中有延迟的部分)

    1
    2
    3
    4
    5
    6
    void thread_1()
    {
    sleep(10);
    cout << "测试" << endl;
    return(0);
    }

    接着要在主线程中加入创建线程的语句,加入之前先对其三种形式进行了解:

    1
    2
    3
    4
    5
    6
    7
    8
    //形式1
    std::thread myThread ( thread_1);
    myThread.join();
    //形式2
    std::thread myThread ( thread_1(100));//带参数的形式
    myThread.join();
    //形式3
    std::thread (thread_1,1).join();//直接创建线程,没有名字

    如果需要带参数记得在定义函数的部分就设定好它的参数,我前面的例子中括号内是空的,没有定义变量。

    由上述三个形式中可以看出,第一个语句是启动对应线程的语句,那么紧接着的myThread.join()是什么意思?这是要确定你启动后接下来该怎么办,有两种方式:join和detach。detach方式下,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。join方式,等待启动的线程完成,主程序才会继续往下执行。

    因此,我肯定需要使用detach方式使得线程并发运行,于是在循环中加入如下语句:

    1
    2
    std::thread myThread (thread_1);
    myThread.detach();

    这么一看,好像问题已经解决了。如果这样想,说明没有把思维放在循环的大背景下。循环在不断的以毫秒的速度反复进行,而分支线程一次执行需要延时几秒。在这段时间内循环已经反复调用了很多次分支线程,这样的结果就是引起未知的错误,可能会阻塞,也可能会崩溃。

  2. 互斥锁

    这个问题并不是无解的,只是需要引入一个新的概念——互斥锁。在其他语言中运用过线程的应该对这个概念并不算陌生,没接触过也不影响,这是一个很好理解的概念。例如线程1要工作,我就对我需要用到的变量加锁,锁上以后其他线程都不能调用,除非线程1工作结束后进行解锁,这样变量就又回到了大家都可以调用的状态。

    每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。但是应注意:同一时刻,只能有一个线程持有该锁。

    当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。也就是说C线程没有去问锁是不是锁住的状态就直接访问,这样可以访问,但是整个就乱套了。锁是统一的规则,每个线程都要遵守,如果没有遵守这个规则,混乱随之而生。

    引入锁需要导入mutex头文件,mutex头文件主要声明了与互斥量(mutex)相关的类。

    1
    2
    #include <mutex>
    std::mutex mtx; //声明一个互斥量mtx

    在我的需求中,我目前没有需要共同操作的变量,因此我定义了一个互斥变量mtx,打算分支线程运行时就锁上,主线程每次循环需要看互斥锁锁上没有,如果锁上就不调用分支线程,没锁上就去调用。还需要注意一个重要原则,锁住的代码要尽量的少。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //循环中添加的部分
    if (mtx_trylock() == 0)
    {
    std::thread myThread (thread_1);
    myThread.detach();
    mtx_unlock();
    }
    else
    {
    printf("wait!");
    }

    分支线程调用的函数修改如下:

    1
    2
    3
    4
    5
    6
    7
    8
    void thread_1()
    {
    mtx_lock();
    sleep(10);
    cout << "测试" << endl;
    return(0);
    mtx_unlock();
    }

    运行效果十分理想,这里主要使用了三个常用操作函数:

    • lock():资源上锁
    • unlock():解锁资源
    • trylock():尝试上锁,如果已经锁住的状态会返回错误号,没有锁定则返回0表示加锁成功,因此后面需要解锁。

至此,多线程的基本运用实现。

结语

事实上,在初次看到c++程序的时候头很大,感觉一个字都看不懂,但是随着慢慢的了解,一个程序中复用的结构语句、运算符是很多的。只要掌握这些基础概念,读懂程序并不算太难。上手的最好办法就是解决问题,有目的去学习还是要快很多。但这样容易造成基础不牢的场景,此后还需要深入全面的对c++进行学习。