本文旨在以最小的篇幅,最少的信息,介绍最高频使用的内容,从而掌握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
#include
// 注意,原则上禁止在头文件中使用using namespace xxx。避免命名空间失效
// 实际上,不管源文件和头文件,都不建议using namespace的方式。而是直接写全。
// using namespace std;
// 这是命名空间。可以有效隔离类,函数,变量的重名问题。在定义库时都建议添加使用。
namespace rtsp
{
class Rtspc
{
// 公共方法,类外部可访问
public:
// 回调函数新写法。对应lambda表达式使用
using OnData = std::function;
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
// 对于不希望被改变的返回的引用,前面加const
const 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
#include
#include
#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
#include
namespace rtsp
{
class RtpPack
{
public:
// 回调打包好的数据
using OnRtpData = std::function;
using Ptr = std::shared_ptr;
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
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 一样,它才能够被实例化。
它是C++11开始支持的好东西,它有两个作用:
1,替代回调函数
2,替代回调函数的 param回传参数。
最重要的是第二点,结合lambda函数使用,让我们的代码看起来是如此的与众不同。
请参看如下示例。
testrtp.cpp
#include
#include /* See NOTES */
#include
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
#include
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 ([email protected])
* @brief 以C++的类模拟 lambda,捕获等行为
* @version 0.1
* @date 2023-12-01
*
* @copyright Copyright (c) 2023
*
*/
#pragma once
#include
namespace lambdaTest
{
// 代替 std::function
// std::function 实际上是模板实例化成了一个类,这个类会记录函数指针和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 ([email protected])
* @brief 对应testrtp.cpp,我们不使用lambda,而是改用基础的类来实现
* @version 0.1
* @date 2023-12-01
*
* @copyright Copyright (c) 2023
*
*/
#include /* See NOTES */
#include
#include
#include
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 (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可以看一下,巨长),这里是 MyOnRtpData
2,将 std::function模板实例化,相当于 MyRtpPackFunc
3,将实例化的类生成对象,也就是这里的 func, 并传入初始化的两个参数: MyOnRtpData, sock
*/
MyRtpPackFunc *func = new MyRtpPackFunc(MyOnRtpData, sock);
// RtpPack记录的,实际上就是 std::function 的对象: func
RtpPack 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 ([email protected])
* @brief 介绍常用容器的用法
* @version 0.1
* @date 2023-12-01
*
* @copyright Copyright (c) 2023
*
*/
#include
#include
#include
智能指针,是现代C++编程非常重要的一个特性。
实际上,有了智能指针之后,我们不应该再使用裸指针了。
下面罗列几个主要的使用场景:
1,配合容器使用
比如有一个类Car,它有很多成员。
如果定义std::vector
Car需要可拷贝,有可能需要实现拷贝构造函数
Car是拷贝了多份的,是独立的。它们之间互相完全无关。
而如果使用指针 std::vector
那你需要注意的是:插入前要new Car, 擦除前要先 delete 成员。
最容易忘的是_pcarvec.clear(). 这个方法执行前,你需要先遍历,逐个delete car
此时,更方便的用法是:std::vector
2,回调函数中使用weak_ptr
(weak_ptr的概念可以先百度一下。)
回调函数有个比较大的问题是,当回调上来之后,数据的消费者可能已经被销毁了。这时我们的指针,是否还生效?如何判断?
如下代码中,rtspc的回调,数据上来之后,窗口是否还存在?
这里,我们通过保存它的weak_ptr句柄,使用时,通过lock的形式来处理。
只要lock成功了,weak_ptr将会升级成为强引用,说明对象还在,我们就可以正常输入数据。
//rtspclient.cpp
#include
#include
可能还有同学有疑问,如果strongPtr拿到之后,在InputMediaData执行之前,发生了窗口切换怎么办呢?
这完全无须担心,由于我们已经持有了window的强引用,此时它并不会被销毁。只有等我们InputMediaData执行完之后,rtspc的回调函数执行完,strongPtr的生命周期完结,此时智能指针的计数清零,MyWindow才会得到释放。
3,类成员指针
类成员指针,它的重建,需要先delete老的。析构时,也需要析构。
而使用了智能指针,这些工作都不需要做了
如下代码中,智能指针_packer,构造函数中的创建,ChangePacker函数中把它重新赋值,都不需要考虑销毁。因为智能指针会自动析构老的内容。
同时,~Rtsps()析构函数执行时,也不需要手工析构_packer。
/**
* @author LiuFengxiang ([email protected])
* @brief rtsp 服务端
* @version 0.1
* @date 2023-12-9
*
* @copyright Copyright (c) 2023
*
*/
#pragma once
#include
#include
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