本文旨在以最小的篇幅,最少的信息,介绍最高频使用的内容,从而掌握C++编程开发的能力。
这种能力,只是语法层面,不涉及具体的函数库,基础库等内容。
能力准备:需要C语言基础。基本的if else, while,基础数据类型等等,不在本文涉及的范围之内。
环境准备
感谢微软的努力,让我们在 Windows环境下可以毫无障碍的进行Linux开发。
推荐使用Windows + wsl2 的环境开发。
打开Microsoft Store,搜索ubuntu,安装最新版本即可。
安装如有问题,请自行百度
使用 vscode + wsl插件的形式,编辑、编译代码。ctrl + ` 可以在命令行和文件编辑之间切换,非常的方便。
在ubuntu下,安装cmake, gcc, g++。安装方法自行百度。
代码的组织结构
build : 编译目录
docs : 文档目录,负责存放该代码相关的信息
libs : 该项目依赖的外部的库及头文件
libs/include 依赖库的头文件
libs/[编译器名称] 平台相关的库文件。比如cc放x86-64位的库,arm-linux-gnu-gcc放该编译器编译出的相关的库文件
source 项目源码目录
test 测试目录,内含测试代码
本文中所有涉及到的示例代码,可在此下载:
链接:https://pan.baidu.com/s/1f73k5uxYTRgtMEORbvgqvA?pwd=tnje
提取码:tnje
编译方法:
cd build
cmake ..
make
该代码展示了一个功能库的目录结构,编译方法。
如果想要做成可执行程序,参考test目录中的内容即可。
类的基本规则
通常定义一个类,我们会分为源文件.cpp,和头文件.h分开来用。
如下为头文件。其中的注释请仔细阅读。
// rtspc.h
/*** @author * @brief rtsp客户端* @version 0.1* @date 2023-11-30* * @copyright Copyright (c) 2023* */
// 使用pragma once 让头文件只引用一次。与下面的 _RTSPC_H_ 作用一致
#pragma once// 头文件避免重复引用。与#pragma once 二选一
#ifndef _RTSPC_H_
#define _RTSPC_H_#include <string>
#include <functional>// 注意,原则上禁止在头文件中使用using namespace xxx。避免命名空间失效
// 实际上,不管源文件和头文件,都不建议using namespace的方式。而是直接写全。
// using namespace std;// 这是命名空间。可以有效隔离类,函数,变量的重名问题。在定义库时都建议添加使用。
namespace rtsp
{class Rtspc
{
// 公共方法,类外部可访问
public:// 回调函数新写法。对应lambda表达式使用using OnData = std::function<void(const char *data, int len)>;Rtspc(bool btcp, OnData onData);// 注意,如果该类会被继承,则务必将它写成虚函数。否则影响析构virtual ~Rtspc();// 建议安装doxygen插件,在函数上方输入 /// 或者 /** 自动生成注释模板。/// @brief 公共方法,大写开头。私有方法,小写开头(代码规范,自行约定)。/// @param url /// @param bTCP /// @return int Run(const std::string &url, bool bTCP);/// @brief 停止/// @return void Stop(){_running = false;}// 保护方法。类内及类的子类可访问。
protected:// 对于不改变类的内容的方法,后面加const// 对于不希望被改变的返回的引用,前面加constconst std::string &getValue() const {return _url;}// 私有方法。通常它和私有成员的private分开写,更清晰一些。
private:void workthread();// 私有成员。成员变量通常为私有成员
private:std::string _url; // 成员变量以 '_' 开头,以便代码中与局部变量,参数做区分。bool _btcp;bool _running; OnData _onData;
};#endif //_RTSPC_H_} // namespace rtsp
源文件长这样。
// rtspc.cpp
#include <iostream>
#include <chrono>
#include <thread>
#include "rtspc.h"namespace rtsp
{// 构造函数
Rtspc::Rtspc(bool btcp, OnData onData)
// 这下面是类成员初始化的写法。据说比写在大括号里效率要高
:_btcp(btcp)
,_running(true)
,_onData(onData)
{}Rtspc::~Rtspc()
{
}int Rtspc::Run(const std::string &url, bool bTCP)
{std::cout << "Running " << url << std::endl;const std::string data = "haha, i am data";while (_running){std::this_thread::sleep_for(std::chrono::milliseconds(400));_onData(data.c_str(), data.size());}return 0;
}} // namespace rtsp
继承与虚函数
干货都在代码中
//rtp-pack.h 这是父类
#pragma once#include <memory>
#include <functional>namespace rtsp
{class RtpPack
{
public:// 回调打包好的数据using OnRtpData = std::function<void(const std::string &rtp)>;using Ptr = std::shared_ptr<RtpPack>;RtpPack(OnRtpData onRtp):_onRtp(onRtp){}// 注意,如果该类会被继承,则务必将它写成虚函数。否则影响析构virtual ~RtpPack(){}virtual int Pack(const uint8_t *data, int len) = 0;/// @brief 创建打包器/// @param encode 编码方式/// @return static Ptr CreatePacker(const char *encode, OnRtpData onData);
protected:OnRtpData _onRtp;
};} // namespace rtsp
// rtp-pack-h264.h 这是子类
#pragma once#include <rtp/rtp-pack.h>namespace rtsp
{
class RtpPackH264 : public RtpPack
{
private:/* data */
public:RtpPackH264(RtpPack::OnRtpData onRtp):RtpPack(onRtp){}virtual ~RtpPackH264(){}virtual int Pack(const uint8_t *data, int len) override{std::string rtp;rtp.append("begin flag");rtp.append((const char *) data, len);_onRtp(rtp);return 0;}// 非虚函数int Demo(){return 0;}
};} // namespace rtsp
如代码所示,RtpPackH264为子类,它继承于RtpPack
虚函数
其中,Pack 在RtpPack中,被定义为纯虚函数。这意味着你无法将RtpPack实例化。
也就是说,RtpPack pack; 是非法的。
只有在子类中实现了 Pack方法,就像RtpPackH264 一样,它才能够被实例化。
std::function 与lambda表达式
它是C++11开始支持的好东西,它有两个作用:
1,替代回调函数
2,替代回调函数的 param回传参数。
最重要的是第二点,结合lambda函数使用,让我们的代码看起来是如此的与众不同。
请参看如下示例。
testrtp.cpp
#include <rtp/rtp-pack-h264.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int main()
{int sock = socket(AF_INET, SOCK_STREAM, 0);connect(sock, xxx);// std::function 的定义与 lambda的应用rtsp::RtpPackH264 rtp([sock](const std::string &rtp){send(sock, rtp.c_str(), rtp.size());});while (1){rtp.Pack("123456", 6);}return 0;
}
可以看到,RtpPackH264 rtp 在实例化的时候,传的参数是一个奇怪的东西:[sock](const std::string &rtp){xxx
这个奇怪的东西,叫作lambda表达式。也叫匿名函数。
[]内部,就相当于我们注册回调函数时,注册进去的param,它通过回调函数再传回给我们。
而这里则不需要这么麻烦,你可以在[ ] 中加入任意多的变量,然后就如代码中的sock一样,在lambda体中使用。
需要注意的是,[sock]这是值传递的写法。它会记录sock的值。还可以这样写:[&sock],引用传递。此时需要注意,它相当于记录了sock的指针。
这里还有另一种写法,可以将lambda表达式写成一个变量:
auto onRtp = [sock](const std::string &rtp){send(sock, rtp.c_str(), rtp.size());};rtsp::RtpPackH264 rtp(onRtp);
小提示:
本节中的代码,没有源文件。类的定义与实现,可以都写在头文件中,只不过这要看实际情况而写。
它的缺点是编译、链接较慢,封装性差。
但有些时候,比如模板,必须写在头文件中。
类的本质是什么?
C++中的类,命名空间,虚实函数,本质上都可以用C来表达。或者换个说法,C++编译器最终会把它变成C语言那样的东西。
就拿RtpPackH264来讲,它在编译器处理后,变成了如下的东西。至于C++的各种特性,都是语法糖。
rtp-pack-h264.c#include <stdlib.h>
#include <stdint.h>typedef void (*rtpCallback_t)(void *userparam, const uint8_t *data, int len);struct rtpH264_class{// 类成员变量rtpCallback_t _onRtp;void *_userparam;// 虚函数表struct rtpH264VirtualFunctionTable{int (*Pack)(struct rtpH264_class *thiz, const uint8_t *data, int len);}functionTable;
};// 虚函数的实现,对应rtsp::RtpPackH264::Pack
// 注意这奇怪的名字,param之后,列出了参数类型。这就是为什么C++允许重名但参数不同的函数。
int rtsp_RtpPackH264_Pack_param_u8_i32(struct rtpH264_class *thiz, const uint8_t *data, int len)
{return 0;
}// 构造函数,生成对象时自动调用。无论是new,还是局部变量
struct rtpH264_class *rtsp_RtpPackH264_RtpPackH264_param_rtpCallback_t_void(rtpCallback_t onRtp, void *userparam){// 分配内存struct rtpH264_class *rtp = malloc(sizeof(struct rtpH264_class));// 构造虚函数表rtp->functionTable.Pack = rtsp_RtpPackH264_Pack_param_u8_i32;rtp->_onRtp = onRtp;rtp->_userparam = userparam;return rtp;
}// 析构函数。在对象生命周期结束时,自动调用
void rtsp_RtpPackH264_del_RtpPackH264(struct rtpH264_class *rtp)
{free(rtp);
}// 非虚函数的实现
int rtsp_RtpPackH264_Demo(struct rtpH264_class *thiz)
{return 0;
}
以上代码中可以看到,类的函数的名字,实际上是由“命名空间+类名+方法名+参数类型”以一定规则,形成的。
而构造函数,实际上是编译器在生成对象是,帮我们调用的。
虚函数表,是在构造函数中指向了各个实际的函数。(不准确,但按此理解无不利影响)
敲黑板
所以,非虚函数,是在编译时就确定了调用关系的。比如调用RtpPackH264::Demo,是在编译时就确定了要调这个函数。
虚函数,是在执行时,查表,确定虚函数表中,指向的是哪个函数,从而完成调用。
请仔细研读,对照上述c实现的类代码,与类本身的关系。
请思考如下代码,最终输出的是什么?
class A{
public:void running(){printf("A running\n");}virtual void VirtualFunc(){printf("A virtual func\n");}
};
class B: public A{
public:void running(){printf("B running\n");}virtual void VirtualFunc(){printf("B virtual func\n");}
};void main()
{B b;A *a = &b;b.running();a->running();b.VirtualFunc();a->VirtualFunc();
}
搜索一下隐藏和覆盖,看看网上五花八门的解释,对照我们把C++的类改成C的写法,你能明白隐藏和覆盖是咋回事了吗?
还有lambda,std::function。
我们将lambda以基础的C++类的方式来实现,它是这样的:
rtp-pack-lambda.hpp (由 rtp-pack 的无lambda写法)
/*** @author LiuFengxiang (20451250@qq.com)* @brief 以C++的类模拟 lambda,捕获等行为* @version 0.1* @date 2023-12-01* * @copyright Copyright (c) 2023* */
#pragma once#include <string>namespace lambdaTest
{// 代替 std::function<void(const std::string &rtp)>
// std::function<xxx> 实际上是模板实例化成了一个类,这个类会记录函数指针和lambda捕获的变量
class RtpPackFunc
{
public:typedef void (*OnRtpData)(RtpPackFunc *thiz, const std::string &rtp);RtpPackFunc(OnRtpData data):_onRtp(data){}virtual ~RtpPackFunc(){}void Call(const std::string &rtp){_onRtp(this, rtp);}
public:OnRtpData _onRtp;
};class RtpPack
{
public:RtpPack(RtpPackFunc *callback):_callback(callback){}~RtpPack(){}void Pack(){_callback->Call("haha");}
private:RtpPackFunc *_callback;
};} // namespace lambdaTest
它的测试代码:testrtp-lambda.cpp, (由testrtp.cpp转化而来)
/*** @author LiuFengxiang (20451250@qq.com)* @brief 对应testrtp.cpp,我们不使用lambda,而是改用基础的类来实现* @version 0.1* @date 2023-12-01* * @copyright Copyright (c) 2023* */#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>#include <rtp/rtp-pack-lambda.hpp>using namespace lambdaTest;// 实际的实现并不相同,但是这样写起来优雅一点儿,也并不妨碍理解。
class MyRtpPackFunc :public RtpPackFunc
{
public:MyRtpPackFunc(RtpPackFunc::OnRtpData data, int sock):RtpPackFunc(data),_sock(sock){}virtual ~MyRtpPackFunc(){}
public:int _sock;
};// 编译器将lambda表达式生成了回调函数
static void MyOnRtpData(RtpPackFunc *func, const std::string &rtp)
{printf("%s running, data: %s\n", __func__, rtp.c_str());MyRtpPackFunc *mine = dynamic_cast<MyRtpPackFunc *> (func);if (mine != nullptr){// 不能真发,没准备好呢if (0)send(mine->_sock, rtp.c_str(), rtp.size(), 0);}
}int main()
{int sock = socket(AF_INET, SOCK_DGRAM, 0);// connect(sock, xxx);/*在使用lambda时,编译器干了很多事情:1,将匿名函数以自有的规则命名(objdump可以看一下,巨长),这里是 MyOnRtpData2,将 std::function模板实例化,相当于 MyRtpPackFunc3,将实例化的类生成对象,也就是这里的 func, 并传入初始化的两个参数: MyOnRtpData, sock*/ MyRtpPackFunc *func = new MyRtpPackFunc(MyOnRtpData, sock);// RtpPack记录的,实际上就是 std::function 的对象: funcRtpPack rtp(func);while (1){rtp.Pack();usleep(1*1000*1000);}}
为了简化写法,我们帮编译器翻译的并不精确,但这并不妨碍理解。
你就记住:lambda表达式,就是编译器帮你起名的匿名函数。而std::function 则是编译器帮你生成的类。
所谓捕获,同样没什么神奇之处,值捕获,在类中直接记录了该变量的值,引用捕获,则是在类中记录了该类的指针。
思考:值捕获和引用捕获的变量,它们的生命周期是怎样的?
本节是想告诉你,C++的很多规则,并不是人为制订出来的,而是语言本身的实现上,必须这么做。它的因果关系是:因为这门语言是这样设计的,所以,产生了这样的规则。
本节只是个引子,借此提示。
多思考!
多思考!!
多思考!!!
多思考背后的机理,那才能举一反三,抓住本质。
记住这句话:C++的所有规则,都是因为设计时,只能这样做。
最常用容器
std::string 其实也是容器。但是我们把它当成一个普通类用就好了
vector 是数组容器。用来管理数量不定的同类型的内容
map 相当于一个映射表,key,value的形式。通过key可以快速的查找到对应的值。
如下代码展示了一些常用的函数,更详细内容查文档:DevDocs
/*** @author LiuFengxiang (20451250@qq.com)* @brief 介绍常用容器的用法* @version 0.1* @date 2023-12-01* * @copyright Copyright (c) 2023* */#include <vector>
#include <string>
#include <map>
#include <iostream>#include <memory> // 智能指针相关class Node
{
public:// 智能指针的技巧。简化写法// 使用的时候 Node::Ptr p 相当于 std::shared_ptr<Node> pusing Ptr = std::shared_ptr<Node>;Node(int val, const std::string &str):_val(val),_str(str){}virtual ~Node(){}const std::string &Str()const {return _str;}int Val()const {return _val;}void SetVal(int val){_val = val;}private:int _val;std::string _str;
};static void vectorDemo()
{// 使用智能指针代替Node * 或者 直接Node。// 如果用指针,在释放时必须逐个 delete。一但遗漏就会有内存泄漏// 如果直接用Node,则每次加入时都会产生拷贝动作。std::vector<Node::Ptr> vec;for (int i = 0; i < 10; i++){auto node = std::make_shared<Node>(i, "haha");vec.push_back(node);}// 遍历,并按条件删除for (auto it=vec.begin();it!= vec.end();){if ((*it)->Val() == 5){// 删除成员时,不能直接it++it = vec.erase(it);}else{it++;}}// 另一种遍历方式for (auto &&it : vec){printf("node val: %d, str: %s\n", it->Val(), it->Str().c_str());it->SetVal(it->Val()+1);}// 注意,如果是 std::vector<Node *> vec, vec.clear() 执行时,并不能自动对每个node做delete动作。// 所以此时,你需要先逐个 delete ,再行 clearvec.clear();
}// map 主要是为了快速通过key 来找到对应的内容。key可以是基础类型,string
// 如果要把自定义的类作为key,则需要自定义比较函数
void mapDemo()
{std::map<int, Node::Ptr> _map;for (int i = 0; i < 10; i++){auto node = std::make_shared<Node>(i, "haha");_map.emplace(i, node);}// 查找int key = 5;auto it = _map.find(key);if (it != _map.end()){std::cout << "we Found it, key: " << it->first << ", str: " << it->second->Str() << std::endl;}else{std::cout << "We Failed found with key: " << key << std::endl;}// 遍历,并按条件删除for (auto it=_map.begin();it!= _map.end();){if (it->first == 5){// 删除成员时,不能直接it++it = _map.erase(it);}else{it++;}}// 另一种遍历方式for (auto &&it : _map){std::cout << "node key: " << it.first << ", val: " << it.second->Str() << std::endl;it.second->SetVal(it.second->Val()+1);}
}int main()
{vectorDemo();mapDemo();return 0;
}
智能指针的使用
智能指针,是现代C++编程非常重要的一个特性。
实际上,有了智能指针之后,我们不应该再使用裸指针了。
下面罗列几个主要的使用场景:
1,配合容器使用
比如有一个类Car,它有很多成员。
如果定义std::vector<Car> _carvec,它的问题是:
Car需要可拷贝,有可能需要实现拷贝构造函数
Car是拷贝了多份的,是独立的。它们之间互相完全无关。
而如果使用指针 std::vector<Car *> _pcarvec。
那你需要注意的是:插入前要new Car, 擦除前要先 delete 成员。
最容易忘的是_pcarvec.clear(). 这个方法执行前,你需要先遍历,逐个delete car
此时,更方便的用法是:std::vector<std::shared_ptr<Car>> _shCarVec;
2,回调函数中使用weak_ptr
(weak_ptr的概念可以先百度一下。)
回调函数有个比较大的问题是,当回调上来之后,数据的消费者可能已经被销毁了。这时我们的指针,是否还生效?如何判断?
如下代码中,rtspc的回调,数据上来之后,窗口是否还存在?
这里,我们通过保存它的weak_ptr句柄,使用时,通过lock的形式来处理。
只要lock成功了,weak_ptr将会升级成为强引用,说明对象还在,我们就可以正常输入数据。
//rtspclient.cpp
#include <memory>
#include <map>
#include <vector>
#include <mutex>
#include <thread>
#include <rtspc/rtspc.h>// 解码窗口
class MyWindow
{
public:using Ptr = std::shared_ptr<MyWindow>;MyWindow(int winid):_winid(winid){}~MyWindow(){printf("window %d destroyed\n", _winid);}int InputMediaData(const char *data, int len){// printf("input data in window: %d\n", _winid);return 0;}
private:int _winid;
};class WindowMgr
{
private:WindowMgr(/* args */){}~WindowMgr(){}
public:// 单例static WindowMgr& Instance(){static WindowMgr _inst;return _inst;}void SetWindowCnt(int cnt){std::lock_guard<std::mutex> guard(_mutex);// 注意哦,这里窗口重建了if ((size_t)cnt != _windows.size()){_windows.clear();// 延时,扩大rtspc上回调时窗口销毁的概率std::this_thread::sleep_for(std::chrono::milliseconds(1000));for (int i = 0; i < cnt; i++){_windows.push_back(std::make_shared<MyWindow>(i));}}}MyWindow::Ptr GetWindow(int winid){std::lock_guard<std::mutex> guard(_mutex);if ((size_t)winid >= _windows.size()){return nullptr;}return _windows[winid];}private:std::vector<std::shared_ptr<MyWindow> > _windows;std::mutex _mutex;
};static void PlayInWindow(int winid, const char *url){std::weak_ptr<MyWindow> weak = WindowMgr::Instance().GetWindow(winid);rtsp::Rtspc rtspc(true, [&rtspc, weak](const char *data, int len){std::shared_ptr<MyWindow> strongPtr = weak.lock();if (strongPtr == nullptr){printf("window destroyed, exit rtspc\n");rtspc.Stop();}else{strongPtr->InputMediaData(data, len);}});rtspc.Run(url, true);
}static void SetWindowCnt(int winCnt)
{printf("Now win cnt: %d\n", winCnt);WindowMgr::Instance().SetWindowCnt(winCnt);for (int i = 0; i < winCnt; i++){std::thread([i](){char url[256];snprintf(url, sizeof(url), "rtsp://192.168.1.2:554/live/chn%d", i);PlayInWindow(i, url);}).detach();}
}int main(int argc, const char *argv[])
{int count = 4;while (true){SetWindowCnt(count);getchar();count++;}return 0;
}
可能还有同学有疑问,如果strongPtr拿到之后,在InputMediaData执行之前,发生了窗口切换怎么办呢?
这完全无须担心,由于我们已经持有了window的强引用,此时它并不会被销毁。只有等我们InputMediaData执行完之后,rtspc的回调函数执行完,strongPtr的生命周期完结,此时智能指针的计数清零,MyWindow才会得到释放。
3,类成员指针
类成员指针,它的重建,需要先delete老的。析构时,也需要析构。
而使用了智能指针,这些工作都不需要做了
如下代码中,智能指针_packer,构造函数中的创建,ChangePacker函数中把它重新赋值,都不需要考虑销毁。因为智能指针会自动析构老的内容。
同时,~Rtsps()析构函数执行时,也不需要手工析构_packer。
/*** @author LiuFengxiang (20451250@qq.com)* @brief rtsp 服务端* @version 0.1* @date 2023-12-9* * @copyright Copyright (c) 2023* */
#pragma once
#include <string>
#include <rtp/rtp-pack.h>namespace rtsp
{class Rtsps
{
public:Rtsps(/* args */){_packer = RtpPack::CreatePacker("H264", [this](const std::string &rtp){onRtpData(rtp);});}// 注意,如果该类会被继承,则务必将它写成虚函数。否则影响析构virtual ~Rtsps(){}/// @brief 更换打包器/// @param encode 打包器名称void ChangePacker(const char *encode){_packer = RtpPack::CreatePacker(encode, [this](const std::string &rtp){onRtpData(rtp);});}int Run(){return 0;}
private:void onRtpData(const std::string &rtp){printf("onRtpData\n");}private:// 打包器的句柄。RtpPack::Ptr _packer;
};} // namespace rtsp