目录
🌼前言
目前,新的架构,会取代旧的技术架构,应用到新产品上
但是这个新的架构,是 C 为主的,目前业务,需要将其中的多个模块,全面重构为 C++
需要对 C++,CMake 有所了解
同时,C 中复杂的互相调用,重构时会有较大风险,需要先弄清楚代码逻辑
作为一个每天打杂的实习生,需要利用上班摸鱼时间,以及每天 8 点后下班时间
输出技术博客,完成对当天周会,或任务的复盘
以下对部分基础知识,进行回顾
👆以上,18篇 CMake 实战,可以认真学习下
本博客主要目的,不是全部背熟记住,而是需要的时候,你知道这个东西怎么用,为什么会这样,以便快速利用文档,AI 进行查阅,提高后续开发效率
并且公司目前全面普及 cursor 了,使用公司内部的免费 cursor,对项目代码进行全面解读
🎂周会技术回顾
socket 通信,CMake,.cc .cfa .sh .bin .so 文件类型,override,enum,static_cast
1.1 .cc文件
与 .cpp 完全等价
代码示例
// camera_controller.cc - 摄像头控制器实现
#include <iostream>
#include <string>
#include "camera_controller.h"
namespace camera { // 避免命名冲突,camera::CameraController
// 摄像头控制器类
class CameraController {
private:
int camera_id_; // 摄像头ID
bool is_initialized_; // 是否已初始化
std::string device_path_; // 设备路径
public:
// 构造函数:初始化摄像头ID
CameraController(int id) : camera_id_(id), is_initialized_(false) {
device_path_ = "/dev/video" + std::to_string(id);
}
// 初始化摄像头设备
bool Initialize() {
if (camera_id_ >= 0 && camera_id_ < 4) {
is_initialized_ = true;
return true;
}
return false;
}
// 开始视频流
void StartStreaming() {
if (!is_initialized_) {
return;
}
std::cout << "Starting video stream from camera " << camera_id_ << std::endl;
}
// 析构函数:清理资源
~CameraController() {
if (is_initialized_) {
}
}
};
} // namespace camera
编译与链接
# -c: 只编译不链接,生成目标文件
g++ -c camera_controller.cc -o camera_controller.o
# -o: 指定输出文件名
g++ camera_controller.cc main.cc -o camera_app
# -std=c++17: 使用C++17标准,-O2: 开启优化
g++ -std=c++17 -O2 camera_controller.cc -o camera_app
2.1 .cfa 文件
项目的配置文件
# camera.cfa - 摄像头配置文件
[camera_0]
resolution=1920x1080 # 分辨率
fps=30 # 帧率
format=YUV420 # 图像格式
[camera_1]
resolution=3840x2160
fps=60
format=RGB24
3.1 CMake 简单示例
makefile 示例
# 设置CMake的最低版本要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名称
project(SimpleCameraApp)
# 设置C++编译器选项,开启所有警告并设置优化等级为2
set(CMAKE_CXX_FLAGS "-Wall -O2")
# 设置链接选项,链接pthread库
set(CMAKE_EXE_LINKER_FLAGS "-lpthread")
# 添加可执行文件,指定源文件
add_executable(camera_app camera.cc main.cc)
# 自定义目标:特定平台编译
add_custom_target(platform_5555
COMMAND ${CMAKE_COMMAND} -E echo "Build completed for platform 5555"
COMMAND ${CMAKE_COMMAND} --build . --target clean
COMMAND ${CMAKE_COMMAND} --build .
COMMENT "Building for platform 5555"
)
# 添加清理目标
add_custom_target(clean
COMMAND ${CMAKE_COMMAND} --build . --target clean
COMMENT "Cleaning build files"
)
# 声明伪目标
add_custom_target(all
COMMAND ${CMAKE_COMMAND} --build .
COMMENT "Building all targets"
)
编译
# 创建一个构建目录,用于存放构建文件
mkdir build
# 进入构建目录
cd build
# 运行CMake,指定源代码目录(通常是上一级目录)
cmake ..
# 使用make命令构建项目
make
# 使用make命令调用清理目标,删除构建文件
make clean
# 使用make命令调用特定平台的构建目标
make platform_5555
# 并行编译,使用4个线程
make -j4
4.1 .sh 文件
脚本示例
#!/bin/bash
# build.sh - 自动化编译脚本
# 设置变量
TARGET=5555
BUILD_DIR=build
# 创建构建目录(如果不存在)
if [ ! -d "$BUILD_DIR" ]; then
mkdir $BUILD_DIR
fi
# 进入构建目录
cd $BUILD_DIR
# 执行编译
echo "开始编译目标:$TARGET"
make $TARGET
# 检查编译是否成功
if [ $? -eq 0 ]; then
echo "编译成功!"
else
echo "编译失败,请检查错误信息。"
exit 1
fi
脚本执行
chmod +x build.sh # 赋予执行权限
./build.sh # 执行脚本
5.1 .bin 文件
二进制可执行文件
生成 bin 文件
# 假设你已经用 g++ 或 arm-linux-gnueabihf-g++ 等工具编译 .cc 文件
# 生成了 firmware.elf 文件
# .elf 是 Linux 和嵌入式系统常用的可执行文件格式(Executable and Linkable Format)
# 包含调试信息和符号表
# Windows 下常见的是 .exe 文件,但嵌入式和 Linux 下一般是 .elf
# 下面这条命令将 .elf 文件转换为 .bin 文件,.bin 是纯二进制格式,适合烧录到硬件设备
arm-linux-gnueabihf-objcopy -O binary firmware.elf firmware.bin
# 参数解释:
# arm-linux-gnueabihf-objcopy :交叉工具链中的 objcopy 工具,用于文件格式转换
# -O binary :指定输出格式为 binary(纯二进制)
# firmware.elf :输入文件(可执行文件,含调试信息)
# firmware.bin :输出文件(纯二进制文件,适合烧录)
操作 bin 文件
// bin_example.cc - 读取并显示二进制文件前8个字节
// 说明:.cc 是 C++ 源代码文件,编译后会生成可执行文件(在 Linux 下通常是 .elf 格式,Windows 下是 .exe)。
// 在嵌入式开发中,.elf 文件是包含调试信息的可执行文件,常用于后续生成 .bin 固件文件。
#include <iostream>
#include <fstream>
int main() {
// 打开二进制文件 firmware.bin
std::ifstream file("firmware.bin", std::ios::binary);
if (!file) {
std::cout << "无法打开文件" << std::endl;
return 1;
}
char buffer[8];
// 读取前8个字节
file.read(buffer, 8);
std::cout << "前8个字节:";
for (int i = 0; i < file.gcount(); ++i) {
printf("%02X ", (unsigned char)buffer[i]);
}
std::cout << std::endl;
return 0;
}
6.1 .so 文件
动态链接库文件(Unix 下是 .so,Windows 下是 .dll)
动态加载一个共享库
libcamera.so
,获取其中的函数指针,并调用这些函数来操作摄像头设备。通过动态加载共享库,程序可以在运行时决定加载哪个库
共享库示例
// camera_lib.cc - 一个简单的摄像头共享库实现
// 说明:本文件将被编译为 .so 动态库,供其他程序调用
#include <iostream>
extern "C" { // 按 C 语言规则链接
// 打开摄像头,返回摄像头ID
int camera_open(int id) {
std::cout << "打开摄像头,ID: " << id << std::endl;
return id;
}
// 开始摄像头预览
void camera_start_preview(int id) {
std::cout << "摄像头 " << id << " 开始预览" << std::endl;
}
// 关闭摄像头
void camera_close(int id) {
std::cout << "关闭摄像头,ID: " << id << std::endl;
}
}
编译共享库
# 编译 camera_lib.cc 为共享库 libcamera.so
# -fPIC: 生成位置无关代码,-shared: 生成动态库
g++ -fPIC -shared -o libcamera.so camera_lib.cc
使用共享库
// main.cc - 动态加载并使用 libcamera.so
// 说明:演示如何在程序中动态加载 .so 文件并调用其中的函数
#include <iostream>
#include <dlfcn.h>
int main() {
// 加载共享库
void* handle = dlopen("./libcamera.so", RTLD_LAZY);
if (!handle) {
std::cerr << "无法加载库: " << dlerror() << std::endl;
return 1;
}
// 获取函数指针
// 定义函数指针类型,用于匹配 libcamera.so 中的函数签名
typedef int (*open_func)(int);
typedef void (*preview_func)(int);
typedef void (*close_func)(int);
// 使用 dlsym 函数获取 libcamera.so 中的函数指针
// dlsym 的第一个参数是 dlopen 返回的句柄,第二个参数是函数名
open_func camera_open = (open_func)dlsym(handle, "camera_open");
preview_func camera_start_preview = (preview_func)dlsym(handle, "camera_start_preview");
close_func camera_close = (close_func)dlsym(handle, "camera_close");
// 调用库函数
int cam_id = camera_open(1);
camera_start_preview(cam_id);
camera_close(cam_id);
// 卸载库
dlclose(handle);
return 0;
}
编译 + 运行主程序
# 编译主程序,-ldl 用于链接动态库加载相关函数
g++ -o main main.cc -ldl
# 运行前需保证 libcamera.so 在当前目录或 LD_LIBRARY_PATH 路径下
./main
7.1 override 关键字
虚函数重写,安全保障
(基类指针或引用,通过动态绑定,调用子类的虚函数的实现)
示例
// override_demo.cc - override关键字演示
#include <iostream>
// 基类:设备
class Device {
public:
virtual ~Device() = default;
// 虚函数:启动设备
virtual void Start() {
std::cout << "设备启动" << std::endl;
}
// 虚函数:获取设备信息
virtual std::string GetInfo() const {
return "通用设备";
}
};
// 派生类:摄像头
class Camera : public Device {
public:
// 使用override确保正确重写虚函数
void Start() override {
std::cout << "摄像头启动" << std::endl;
}
// 重写获取信息函数
std::string GetInfo() const override {
return "摄像头设备";
}
};
int main() {
Camera cam;
Device* device = &cam; // 多态
device->Start(); // 调用Camera::Start()
std::cout << device->GetInfo() << std::endl; // 调用Camera::GetInfo()
return 0;
}
保障
// 1. 防止拼写错误
// void Stat() override; // 编译错误:应该是Start,拼写错了
// 2. 防止参数类型错误
// void SetMode(float mode) override; // 编译错误:参数类型不匹配
// 3. 防止参数数量错误
// void Configure(int id) override; // 编译错误:参数个数不对
// 4. 防止const修饰符错误
// std::string GetInfo() override; // 编译错误:缺少const
// 基类的 GetInfo() 函数后面有 const,表示这个函数承诺不会修改对象
// 派生类重写时,必须保持相同的 const 修饰符
// 如果基类是 const 函数,派生类忘记加 const,override 会检测到这个错误并报编译错误
// 这确保了函数签名的完全一致性
// 5. 防止返回类型错误
// void GetStatus() const override; // 编译错误:返回类型不匹配
// 6. 防止基类函数被删除后的错误
// void DeletedFunction() override; // 编译错误:基类中不存在此虚函数
8.1 enum class
强类型枚举
示例
// enum_demo.cc - enum class演示
#include <iostream>
#include <cstdint> // 用于 uint8_t
// 传统枚举的问题
enum OldStatus {
IDLE, // 全局作用域,容易命名冲突;隐式转换,存在安全问题
RUNNING
};
// 强类型枚举解决问题
enum class CameraStatus : uint8_t { // 指定底层类型为 uint8_t
Idle = 0, // 空闲状态
Running = 1, // 运行状态
Error = 255 // 错误状态
};
enum class DeviceType : int {
Unknown = 0, // 未知设备
Camera = 1, // 摄像头
Microphone = 2 // 麦克风
};
// 枚举转换函数
std::string StatusToString(CameraStatus status) {
switch (status) {
case CameraStatus::Idle: return "Idle"; // 空闲状态
case CameraStatus::Running: return "Running"; // 运行状态
case CameraStatus::Error: return "Error"; // 错误状态
default: return "Unknown"; // 未知状态
}
}
// 使用示例
int main() {
// 类型安全,必须指定作用域
CameraStatus status = CameraStatus::Idle; // 摄像头状态设置为 Idle
DeviceType type = DeviceType::Camera; // 设备类型设置为 Camera
// 不能隐式转换为 int
// int value = status; // 编译错误
int value = static_cast<int>(status); // 必须显式转换
// 输出状态字符串
std::cout << "Status: " << StatusToString(status) << std::endl;
std::cout << "Status value: " << value << std::endl;
// 类型安全的比较
if (status == CameraStatus::Idle) {
std::cout << "Camera is idle" << std::endl;
}
return 0;
}
9.1 static_cast<int>
类型安全转换
// static_cast_demo.cc - static_cast演示
#include <iostream>
enum class Priority : int {
Low = 1, // 低优先级
Medium = 5, // 中优先级
High = 10 // 高优先级
};
// 类型转换示例
class TypeCastDemo {
public:
// 基本类型转换
static void BasicCast() {
int int_val = 42;
float float_val = static_cast<float>(int_val); // int转float
double double_val = static_cast<double>(int_val); // int转double
}
// 枚举类转换
static void EnumCast() {
Priority pri = Priority::High;
int pri_value = static_cast<int>(pri); // 枚举转int
std::cout << "Priority value: " << pri_value << std::endl;
// int转枚举(需谨慎)
Priority new_pri = static_cast<Priority>(5);
std::cout << "New priority: " << static_cast<int>(new_pri) << std::endl;
}
// 指针转换
static void PointerCast() {
int value = 100;
void* void_ptr = &value; // int*转void*
int* int_ptr = static_cast<int*>(void_ptr); // void*转int*
}
};
int main() {
TypeCastDemo::BasicCast();
TypeCastDemo::EnumCast();
TypeCastDemo::PointerCast();
return 0;
}
10.1 socket 通信
TCP 服务器
// tcp_server.cc - 简单的TCP服务器
// 说明:服务器就像一个餐厅,等待客户(客户端)来点餐(发送请求)
#include <iostream>
#include <sys/socket.h> // socket相关函数
#include <netinet/in.h> // 网络地址结构
#include <unistd.h> // close函数
#include <cstring> // strlen函数
// 简单的TCP服务器类
class SimpleTCPServer {
private:
int server_fd_; // 服务器的socket文件描述符(像餐厅的门牌号)
int port_; // 监听的端口号(像餐厅的地址)
bool is_running_; // 服务器是否在运行
public:
// 构造函数:设置服务器监听的端口
SimpleTCPServer(int port) : port_(port), is_running_(false) {}
// 启动服务器
bool Start() {
// 1. 创建socket(相当于开一家餐厅)
// socket(协议族, socket类型, 协议)
// AF_INET: IPv4协议族, SOCK_STREAM: TCP协议, 0: 自动选择协议
server_fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd_ == -1) {
std::cerr << "创建socket失败" << std::endl;
return false;
}
// 2. 设置地址复用(允许重复使用同一个地址)
int opt = 1;
// setsockopt(socket描述符, 协议级别, 选项名, 选项值指针, 选项值长度)
// SOL_SOCKET: socket级别, SO_REUSEADDR: 地址复用选项
setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 绑定地址和端口(给餐厅确定地址)
struct sockaddr_in address;
address.sin_family = AF_INET; // 使用IPv4协议
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口(0.0.0.0)
address.sin_port = htons(port_); // htons: 主机字节序转网络字节序
// bind(socket描述符, 地址结构指针, 地址结构长度)
if (bind(server_fd_, (struct sockaddr*)&address, sizeof(address)) < 0) {
std::cerr << "绑定地址失败" << std::endl;
return false;
}
// 4. 开始监听(餐厅开始营业,等待客人)
// listen(socket描述符, 最大排队连接数)
// 3表示最多排队3个客户等待连接
if (listen(server_fd_, 3) < 0) {
std::cerr << "监听失败" << std::endl;
return false;
}
is_running_ = true;
std::cout << "服务器在端口 " << port_ << " 开始监听..." << std::endl;
return true;
}
// 接受客户端连接(等待客人进餐厅)
void AcceptConnections() {
while (is_running_) {
struct sockaddr_in client_addr; // 客户端地址信息
socklen_t client_len = sizeof(client_addr);
// 5. 接受一个客户端连接(客人进来了)
// accept(服务器socket, 客户端地址结构指针, 客户端地址长度指针)
// 返回新的socket描述符,专门用于与这个客户端通信
int client_fd = accept(server_fd_, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
std::cerr << "接受连接失败" << std::endl;
continue; // 继续等待下一个客户
}
std::cout << "客户端已连接" << std::endl;
// 6. 处理这个客户端的请求(服务客人)
HandleClient(client_fd);
// 7. 服务完毕,关闭连接(客人离开)
// close(文件描述符) - 关闭socket连接
close(client_fd);
std::cout << "客户端已断开连接" << std::endl;
}
}
// 处理客户端请求(具体服务客人)
void HandleClient(int client_fd) {
char buffer[1024] = {0}; // 接收数据的缓冲区(像记录客人点的菜)
// 接收客户端发送的数据
// recv(socket描述符, 接收缓冲区, 缓冲区大小, 标志位)
// client_fd: 客户端socket, buffer: 存放数据的缓冲区
// sizeof(buffer): 缓冲区大小, 0: 无特殊标志
int bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
if (bytes_read > 0) {
std::cout << "收到消息: " << buffer << std::endl;
// 发送回复给客户端
const char* response = "你好,我是服务器!";
// send(socket描述符, 发送数据指针, 数据长度, 标志位)
// client_fd: 客户端socket, response: 要发送的数据
// strlen(response): 数据长度, 0: 无特殊标志
send(client_fd, response, strlen(response), 0);
}
}
// 停止服务器(餐厅关门)
void Stop() {
is_running_ = false;
if (server_fd_ != -1) {
// close(文件描述符) - 关闭服务器socket
close(server_fd_);
}
std::cout << "服务器已停止" << std::endl;
}
// 析构函数
~SimpleTCPServer() {
Stop();
}
};
TCP 客户端
// tcp_client.cc - 简单的TCP客户端
// 说明:客户端就像顾客,主动去餐厅(服务器)点餐(发送请求)
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton函数
#include <unistd.h>
#include <cstring>
// 简单的TCP客户端类
class SimpleTCPClient {
private:
int client_fd_; // 客户端的socket文件描述符
std::string server_ip_; // 服务器IP地址(餐厅地址)
int server_port_; // 服务器端口号
public:
// 构造函数:设置要连接的服务器地址和端口
SimpleTCPClient(const std::string& ip, int port)
: server_ip_(ip), server_port_(port), client_fd_(-1) {}
// 连接到服务器(顾客走进餐厅)
bool Connect() {
// 1. 创建socket(准备去餐厅)
// socket(协议族, socket类型, 协议)
// AF_INET: IPv4协议族, SOCK_STREAM: TCP协议, 0: 自动选择协议
client_fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd_ == -1) {
std::cerr << "创建socket失败" << std::endl;
return false;
}
// 2. 设置服务器地址信息
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4协议
server_addr.sin_port = htons(server_port_); // htons: 主机字节序转网络字节序
// inet_pton(地址族, IP字符串, 二进制地址结构)
// AF_INET: IPv4, server_ip_.c_str(): IP字符串, &server_addr.sin_addr: 输出地址
inet_pton(AF_INET, server_ip_.c_str(), &server_addr.sin_addr);
// 3. 连接服务器(走进餐厅)
// connect(socket描述符, 服务器地址结构指针, 地址结构长度)
if (connect(client_fd_, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "连接服务器失败" << std::endl;
return false;
}
std::cout << "已连接到服务器 " << server_ip_ << ":" << server_port_ << std::endl;
return true;
}
// 发送消息给服务器(向服务员点餐)
bool SendMessage(const std::string& message) {
if (client_fd_ == -1) {
std::cerr << "未连接到服务器" << std::endl;
return false;
}
// 发送数据
// send(socket描述符, 发送数据指针, 数据长度, 标志位)
// client_fd_: 客户端socket, message.c_str(): 消息字符串
// message.length(): 消息长度, 0: 无特殊标志
ssize_t bytes_sent = send(client_fd_, message.c_str(), message.length(), 0);
if (bytes_sent < 0) {
std::cerr << "发送消息失败" << std::endl;
return false;
}
std::cout << "已发送: " << message << std::endl;
return true;
}
// 接收服务器的响应(收到餐厅的回复)
std::string ReceiveResponse() {
if (client_fd_ == -1) {
return "";
}
char buffer[1024] = {0}; // 接收缓冲区
// recv(socket描述符, 接收缓冲区, 缓冲区大小, 标志位)
// client_fd_: 客户端socket, buffer: 存放接收数据的缓冲区
// sizeof(buffer): 缓冲区大小(1024字节), 0: 无特殊标志
ssize_t bytes_received = recv(client_fd_, buffer, sizeof(buffer), 0);
if (bytes_received > 0) {
// string(字符数组, 长度) - 根据接收到的字节数创建字符串
std::string response(buffer, bytes_received);
std::cout << "收到回复: " << response << std::endl;
return response;
}
return "";
}
// 断开连接(离开餐厅)
void Disconnect() {
if (client_fd_ != -1) {
// close(文件描述符) - 关闭客户端socket
close(client_fd_);
client_fd_ = -1;
std::cout << "已断开与服务器的连接" << std::endl;
}
}
// 析构函数
~SimpleTCPClient() {
Disconnect();
}
};
客户端与服务器通信
// socket_demo.cc - 演示客户端和服务器通信
#include <iostream>
#include <thread> // 多线程支持
#include <chrono> // 时间相关
int main() {
// 在单独的线程中启动服务器(就像在后台开一家餐厅)
// std::thread(lambda函数) - 创建新线程执行lambda函数
std::thread server_thread([]() {
SimpleTCPServer server(8080); // 在8080端口开餐厅
if (server.Start()) {
server.AcceptConnections(); // 开始接待客人
}
});
// 等待服务器启动完成(等餐厅准备好)
// sleep_for(时间间隔) - 当前线程休眠指定时间
// milliseconds(100) - 100毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 创建客户端并连接服务器(顾客去餐厅)
// SimpleTCPClient(IP地址, 端口号)
// "127.0.0.1": 本机IP地址, 8080: 服务器端口号
SimpleTCPClient client("127.0.0.1", 8080);
if (client.Connect()) {
// 发送消息并接收回复
client.SendMessage("你好,服务器!");
client.ReceiveResponse();
client.Disconnect();
}
// 注意:实际应用中需要优雅地停止服务器线程
// 这里为了演示简单,没有处理线程的正确关闭
return 0;
}
编译
# 编译服务器程序
# -std=c++11: 使用C++11标准(支持线程)
# -pthread: 链接线程库(pthread)
g++ -std=c++11 -pthread tcp_server.cc -o server
# 编译客户端程序
g++ -std=c++11 -pthread tcp_client.cc -o client
# 编译演示程序(包含服务器和客户端)
g++ -std=c++11 -pthread socket_demo.cc -o socket_demo
# 运行方式1:分别运行服务器和客户端
./server & # &符号表示后台运行服务器
./client # 前台运行客户端
# 运行方式2:运行演示程序
./socket_demo
可以把所有代码放一个 .cc,一次编译即可
# 创建一个完整的文件 complete_socket.cc,包含:
# - SimpleTCPServer类
# - SimpleTCPClient类
# - main函数
# 然后一次编译
g++ -std=c++11 -pthread complete_socket.cc -o socket_demo
./socket_demo