2024华中科技大学计算机学院C++程序设计实验企业开发实践华为
实验1-高质量编程基础
前言
欢迎参加C++企业软件开发实践课程。本课程旨在通过完整开发案例,分享企业
软件开发中的实践、经验和要求,帮助在校学生提升软件开发技能,养成良好的
软件开发习惯。
实践课程共有4次实验,本课程为实验1,您将学习高质量编程的基础知识,包括:
⚫ 开发者测试:掌握确保代码正确性和需求完整性的测试方法
⚫ 命名:了解提高代码可读性的命名原则、规范和最佳实践
⚫ 代码版本管理:学习使用git进行代码版本管理的规范和实践
期待您在课程中的精彩表现!
目标
通过本课程的学习,您将能够:
⚫ 掌握测试的基本理论和方法
⚫ 实践有效的开发者测试技术
⚫ 掌握使用git进行代码版本管理的规范和实践
了解C++编程的可读性要求,建立高质量编程的意识,养成良好的开发习惯
1 课程导读
企业软件开发的特点
-
企业高质量产品对软件开发的要求:企业软件规模大、生命周期长、可靠性要求高,软件代码不仅满足功能和性能要求,还需要满足代码质量和安全要求,软件开发人员需要掌握良好的软件开发技能,遵循企业软件开发规范,应用软件代码实现的最佳实践。
-
《企业软件开发实战》系列课程通过完整的需求开发,帮助学生提升满足企业软件开发要求的软件实践能力:
- 企业软件开发工程化思维,软件开发不仅仅是编写代码,还包括版本管理、开发者测试、代码审查、自动化构建等;
- 语言编程实践动手能力,遵循编码规范,合理运用语言特性,学习编写高质量代码(可读、可维护、安全、高效等)的方法,养成良好的编程习惯。
业务特点 | 对软件技术的诉求 | 高质量代码要求 |
---|---|---|
质量要求高 | 1. 可靠性、稳定性要求高,异常处理完善; 2. 并发性能高,资源利用率高; |
安全、可靠性、高效 |
客户众多,全球化分布 | 1. 适应不同的环境和场景; 2. 快速地响应不同用户的差异化需求; |
可维护—可修改/扩展、可复用、可测试 |
产品生命周期长,迭代演进 | 1. 长期开发和运营,需要不断地进行迭代和优化,以适应市场的变化,保持产品的竞争力; 2. 软件规模大,需要能够方便地进行修改和扩展,能够有效地进行复用和测试,能够高效地进行维护和更新; |
可读、可维护、可复用、可测试 |
课程整体框架
Level1 课程模块 | Level2 课程目录 | 学时(h) | Level3 主要内容 | 内容要点 |
---|---|---|---|---|
实验1 | 高质量编程基础 | 4 | 开发者自测试 | 如何确保代码的正确性和需求的完整性 |
命名 | 代码可读性的原则、规范、实践 | |||
代码版本管理 | 使用git进行代码版本管理的规范和实践 | |||
实验2 | 面向对象编程 | 4 | 封装 | 掌握面向对象编程的基本概念,包括类和对象的封装,以及如何通过封装实现代码的复用性 |
继承与多态 | 掌握面向对象中,继承、多态等特性,实现代码的良好扩展性 | |||
实验3 | 面向对象编程 | 4 | 表驱动和STL容器 | 有效地使用STL容器和智能指针,提升代码的可扩展性和灵活性 |
解耦循环依赖 | 如何通过分层设计和抽象化来有效地解耦代码中的循环依赖 | |||
实验4 | 函数式编程 | 4 | 函数式编程 | 掌握C++新特性带来的代码简洁性收益,写出简洁优雅的代码 |
对象生命周期安全 | 关注C++中使用过程中编译器附带可能导致的问题,对背后原理有深根,学习C++特性整体体系 | |||
面向接口编程 | 通过C++泛型编程技术,提升代码效率和可扩展性 | |||
任务编排 | 将复杂问题分解为原子操作,逐步实现,提升代码扩展性 | |||
目录结构规划 | 理解和应用物理设计的原则,优化代码结构和组织 | |||
课后作业 | 课程内容巩固 | 0.5~4 | all | 课程内容巩固 |
2 项目实践
2.1 需求澄清
- ADAS系统介绍
Advanced Driver Assistance System,ADAS是辅助驾驶员
安全驾驶车辆的系统,可以增加车辆安全和道路安全,同时可以明
显减轻汽车驾驶的操作负担。 - ADAS Executor组件主要功能:
-由Config组件进行初始化及配置- 接收Controller组件的各种移动控制命令(如前进、转向等),
维护车的位置状态
- 接收Controller组件的各种移动控制命令(如前进、转向等),
课程实训需求1 支持基本控制指令
设计一个C++程序,模拟自动驾驶车辆的执行器Executor组件功能。
Executor组件提供初始化接口,负责将车初始化在指定位置,输入数据为:
(x, y, heading),其中:
- x,y对应地点位置(均为int32类型)
- heading对应四个方向(N、S、E、W)(均为char类型)
Executor组件可以执行如下的移动指令: - M: 前进,1次移动1格
Executor组件可以执行如下的转向指令: - L: 左转90度,位置不变
- R: 右转90度,位置不变
Executor组件提供获取车当前的坐标位置和朝向接口,如果Executor未被
初始化位置,则接口默认返回(0,0,N)。 X轴移动的方向为EW方向,Y轴移动的方向为NS方向。
要求
- 设计Executor组件对外的接口,
- 设计测试用例,构建上述功能的测试防护网
约束
- 整个过程中坐标(x,y)的值都在
int32范围内,不考虑整数溢出场景 - 调用者保证了传给Executor接口
的参数都是合法的,不考虑非法参
数场景
2.2 接口设计
2.3 命名实践
2.4 开发者测试
2.5 代码版本管理
2.6 编码实践
3 总结
4 实现
4.1 Executor.hpp
#pragma once
#include <string>
namespace adas
{
struct Pose // 位置和朝向
{
int x;
int y;
char heading;
};
// Executor 类是一个抽象类,定义了执行指令和查询当前位置的接口
class Executor
{
public:
// 工厂方法,创建 Executor 实例,默认初始位置为 (0, 0, 'N')
static Executor *NewExecutor(const Pose &pose = {0, 0, 'N'}) noexcept;
// 默认构造函数和析构函数
Executor(void) = default;
virtual ~Executor(void) = default;
// 不能拷贝
Executor(const Executor &) = delete;
// 不能赋值
Executor &operator=(const Executor &) = delete;
public:
// 执行指令的纯虚函数接口
virtual void Execute(const std::string &command) noexcept = 0;
// 获取当前位置和朝向的纯虚函数接口
virtual Pose Query(void) const noexcept = 0;
};
} // namespace adas
4.2 ExecutorImpl.hpp
#pragma once
#include "Executor.hpp"
#include <string>
namespace adas
{
/*
Executor的具体实现
*/
class ExecutorImpl : public Executor
{
public:
// 构造函数
explicit ExecutorImpl(const Pose &pose) noexcept;
// 默认析构函数
~ExecutorImpl() noexcept = default;
// 不能拷贝
ExecutorImpl(const ExecutorImpl &) = delete;
// 不能赋值
ExecutorImpl &operator=(const ExecutorImpl &) = delete;
public:
// 查询当前的车辆姿态,是父类抽象方法Query的具体实现
Pose Query(void) const noexcept override;
// override是C++11的特性,用于显式地声明一个函数是重载了基类中的虚函数
// 第二阶段新增加的纯虚函数,执行一个用字符串表示的指令,是父类抽象方法Execute的具体实现
void Execute(const std::string &command) noexcept override;
private:
// 私有数据成员,记录当前车辆的姿态
Pose pose;
private:
// 私有方法,处理前进、左转、右转指令
void moveForward();
void turnLeft();
void turnRight();
};
} // namespace adas
4.3 ExecutorImpl.cpp
#include "ExecutorImpl.hpp"
#include <iostream>
#include <new>
namespace adas
{
ExecutorImpl::ExecutorImpl(const Pose &pose) noexcept : pose(pose) {} // 构造函数初始化 pose
// Query()方法用于查询当前位置和朝向
Pose ExecutorImpl::Query(void) const noexcept
{
return pose;
}
/*
std::nothrow 是 C++ 标准库的一个常量,用于指示在分配内存时不抛出任何异常。
它是 std::nothrow_t 类型的实例,通常用在 new 运算符和 std::nothrow 命名空间中,
以请求内存分配器在分配失败时返回一个空指针,而不是抛出 std::bad_alloc 异常。
*/
Executor *Executor::NewExecutor(const Pose &pose) noexcept
{
return new (std::nothrow) ExecutorImpl(pose); // 只在 C++17 下有效
}
// moveForward()方法用于处理前进指令,根据当前朝向更新车辆的位置
void ExecutorImpl::moveForward()
{
if (pose.heading == 'E')
{
pose.x += 1; // 向东前进
}
else if (pose.heading == 'W')
{
pose.x -= 1; // 向西前进
}
else if (pose.heading == 'N')
{
pose.y += 1; // 向北前进
}
else if (pose.heading == 'S')
{
pose.y -= 1; // 向南前进
}
}
// turnLeft()方法用于处理左转指令,根据当前朝向更新车辆的朝向
void ExecutorImpl::turnLeft()
{
if (pose.heading == 'E')
{
pose.heading = 'N'; // 东转北
}
else if (pose.heading == 'W')
{
pose.heading = 'S'; // 西转南
}
else if (pose.heading == 'N')
{
pose.heading = 'W'; // 北转西
}
else if (pose.heading == 'S')
{
pose.heading = 'E'; // 南转东
}
}
// turnRight()方法用于处理右转指令,根据当前朝向更新车辆的朝向
void ExecutorImpl::turnRight()
{
if (pose.heading == 'E')
{
pose.heading = 'S'; // 东转南
}
else if (pose.heading == 'W')
{
pose.heading = 'N'; // 西转北
}
else if (pose.heading == 'N')
{
pose.heading = 'E'; // 北转东
}
else if (pose.heading == 'S')
{
pose.heading = 'W'; // 南转西
}
}
// Execute()方法用于执行指令,根据指令调用相应的处理方法
void ExecutorImpl::Execute(const std::string &commands) noexcept
{
for (const auto cmd : commands) // const auto cmd : commands是C++11的特性,用于遍历字符串
{
if (cmd == 'M')
{
moveForward(); // 前进
}
else if (cmd == 'L')
{
turnLeft(); // 左转
}
else if (cmd == 'R')
{
turnRight(); // 右转
}
else
{
std::cout << "未知指令: " << cmd << std::endl;
}
}
}
} // namespace adas
4.4 ExecutorTest.cpp
剩下的自己写哦
#include <gtest/gtest.h>
#include <memory>
#include <tuple>
#include "../include/Executor.hpp"
namespace adas
{
// 重载Pose的==操作符,用于比较两个Pose对象是否相等
bool operator==(const Pose &lhs, const Pose &rhs)
{
return std::tie(lhs.x, lhs.y, lhs.heading) == std::tie(rhs.x, rhs.y, rhs.heading);
}
// 测试返回初始化的 Pose
TEST(ExecutorTest, should_return_init_pose_when_without_command)
{
// given 给定测试条件
// 测试条件是就是调用Executor的静态方法NewExecutor返回一个指向 Executor 对象的智能指针 executor,这样我们就不需要delete了
std::unique_ptr<Executor> executor(Executor::NewExecutor({0, 0, 'E'})); // 给了初始姿态
// when
// then
const Pose expectedPose = {0, 0, 'E'}; // 构造一个姿态对象,其内容为(0, 0, 'E')
// 既然构造对象时的初始姿态是(0, 0, 'E'),那么调用Query()方法返回的姿态也应该是(0, 0, 'E')
// 所以这里用了断言,executor->Query()返回的姿态应该等于expectedPose,否则测试失败,说明Query()方法有问题
ASSERT_EQ(expectedPose, executor->Query()); // 内部调用了重载的pose的==操作符
}
// 测试返回默认的 Pose
TEST(ExecutorTest, should_return_default_pose_when_without_init_and_command)
{
// given 给定测试条件
std::unique_ptr<Executor> executor(Executor::NewExecutor());
// when
// 不给初始姿态,也不给指令
// then
const Pose expectedPose = {0, 0, 'N'}; // 构造一个姿态对象,其内容为(0, 0, 'N')
// 由于没有给定初始姿态,所以调用Query()方法返回的姿态应该是默认的(0, 0, 'N')
ASSERT_EQ(expectedPose, executor->Query());
}
}