熟悉 C++
吞下药丸
吞下药丸
一些背景
C++ 是我的自学白鲸。这些年来,我曾多次尝试深入学习 Stroustrup 的经典著作,但总是很快便功亏一篑。它实在太庞大,而且有点复杂。我一直对它充满敬畏,渴望能够充分利用它的强大功能,但由于其他语言更容易上手,我始终抽不出时间。我的自律性一直不够。
现在我回到了学校,所以抓住了这个机会,特意报了C++方向的课程。我觉得,结构化的学术指导或许能让我更好地投入时间和精力。我终于要用C++做一个重要的、为期一个月的期末项目了,所以这是我第一次真正意义上的C++自由探索之旅,现在我对这门语言的了解比以往任何时候都要多。
事实证明我甚至喜欢它,但它是来自 Rust、JavaScript、C 和公司的一种觉醒。
这是什么
这篇日志只是我完成这项作业过程中学到的一些技巧的入门级总结。我并不声称自己找到了这些问题的最佳解决方案,而更像是一篇记录最终对我有用的经验的日志。Si vez algo, di algo。
目前我最熟悉的类似语言是 Rust(或者可能是 C,我不确定——它们由于不同的原因而相似),而 Rust 恰好也是我最近使用最多的语言,所以无论好坏,我或多或少都会以 Rust 项目的方式处理这个项目。我很快就能发现它们的用法非常不同,但你总得从某个地方开始。
该项目是一个 Battleship 的 CLI 游戏。代码可以在GitHub上找到。
使用语句
我的第一个困惑来自命名空间的礼仪。我知道我不喜欢using namespace std
,所以我决定使用作用域级别的 using 语句:
std::string someFunc()
{
using std::string;
string myString = "";
}
这使它在全局范围内保持明确,但允许我将特定的东西拉入函数而不牺牲清晰度 - 您可以在那里看到它来自哪里。
然后我对语句感到困惑#include
——有时一个头文件会被包含进多层包含结构中,因为预处理器实际上只是将代码粘贴到其他代码中来解析这些代码。因此很难看出某个特定函数实际上被包含在哪里。
有人推荐我读这篇文章,值得一读。对我来说,最大的收获是,如果你只是使用指向特定对象的指针,则无需实际包含它,你可以(或许应该)直接进行前置声明。
调试
到目前为止,我主要从事println
调试工作。我知道如何使用,gdb
但从未发现它比在某个地方添加debug!()
输出更简单。
我现在非常感激。gdb
我的程序根本就不够大。我所需要的一切,简单概括一下:
-
使用该标志进行编译
-g
。 -
调用
gdb my_executable
→ gdb build/volley
GNU gdb (Gentoo 8.3 vanilla) 8.3
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
// blah blah blah
Reading symbols from build/volley...
(gdb)
您在(gdb)
提示符下输入命令。
- 在您想要检查的函数上中断:
(gdb) break runFiring
Breakpoint 1 at 0x40e913: file src/game.cpp, line 38.
- 使用命令浏览您的程序:
a. run
/ r
:运行已加载的程序进行调试,直到下一个断点
b. next
/ n
:逐行逐步执行,而不是步入函数。
c. step
/ s
:逐行逐步执行,步入所有函数
d. print
/ p
:打印变量的值
e. examine
/ x
:检查变量的内存
f. continue
/ c
:停止逐行逐步执行并恢复执行到下一个断点(或程序完成)
g. kill
/ k
:终止正在调试的程序而不退出,gdb
以便从顶部再次执行(使用quit
/q
返回到 shell)
您可以随时添加断点,并使用 将其移除delete
。使用向上和向下箭头访问命令历史记录,将其留空并点击Enter
将重复上一个命令 - 这对于逐行执行非常有用。
还有很多很多,这里有一个很棒的 PDF 速查表。我发现自己用了info locals
很多,它显示了当前堆栈框架中的所有变量。
这比添加、删除 println 语句并重新编译好太多了。它更具探索性和交互性,效率也高出百万倍。我现在也只是勉强知道如何使用它。
自己清理
有一种非常快捷的方法来检查您是否已完成内存泄漏工作:valgrind。
这是另一个我不知道如何使用但已经从中受益匪浅的工具:
± |master U:13 ✗| → valgrind build/volley
==7744== Memcheck, a memory error detector
==7744== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==7744== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==7744== Command: build/volley
==7744==
Battleship!!!
// ... etc - play a game
Game over!
==7744==
==7744== HEAP SUMMARY:
==7744== in use at exit: 672 bytes in 8 blocks
==7744== total heap usage: 3,970 allocs, 3,962 frees, 177,854 bytes allocated
==7744==
==7744== LEAK SUMMARY:
==7744== definitely lost: 0 bytes in 0 blocks
==7744== indirectly lost: 0 bytes in 0 blocks
==7744== possibly lost: 0 bytes in 0 blocks
==7744== still reachable: 672 bytes in 8 blocks
==7744== suppressed: 0 bytes in 0 blocks
==7744== Rerun with --leak-check=full to see details of leaked memory
==7744==
==7744== For lists of detected and suppressed errors, rerun with: -s
==7744== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
等等,退出时堆还在使用中?啊,当然——我写了析构函数,但实际上从未delete
在任何地方调用过顶层实例!快速修改后:
$ valgrind build/volley
==8122== Memcheck, a memory error detector
==8122== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==8122== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==8122== Command: build/volley
==8122==
Battleship!!!
// ... etc - play a game
Game over!
==8122==
==8122== HEAP SUMMARY:
==8122== in use at exit: 0 bytes in 0 blocks
==8122== total heap usage: 3,993 allocs, 3,993 frees, 178,686 bytes allocated
==8122==
==8122== All heap blocks were freed -- no leaks are possible
==8122==
==8122== For lists of detected and suppressed errors, rerun with: -s
==8122== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
现在总共有 672 个字节,并且都已记录在案。太棒了!我只需要提醒自己再检查一下,而不是直接运行,这个工具还能帮你做很多事情。
结构相等
我一开始遇到的一个问题是std::find()
。这用于定位向量中的元素。显然,这样的函数会比较元素是否相等。在 Rust 中,你需要PartialEq
在结构体上派生或手动实现该特性才能启用该行为。C++ 没有这个功能,但你仍然需要能够为结构体定义相等性。
结构体基本上等同于类,但其成员默认是公开的。这是我在教科书上学到的,但从未用过。
如果不提供定义,您会得到这个有点不透明的错误clang
:
usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/include/g++-v9/bits/predefined_ops.h:241:17: error: invalid operands to binary expression ('Cell' and 'const Cell')
{ return *__it == _M_value; }
发生这种情况是因为std::find()
我们尝试在两个结构体上使用==
,但我们尚未定义如何操作。我认为问题在于它原本期望通过引用传递,结果却通过值传递了。
==
您可以通过重载运算符并专门传递引用来允许相等性检查对您定义的结构进行操作const
:
// A single cell on the board
typedef struct Cell
{
int row;
char col;
bool operator==(const Cell &other) const
{
return row == other.col && col == other.col;
}
} Cell;
这看起来很像手写的impl PartialEq block
(来自文档),它也使用了本质上是 Rust-y 的东西const &
:
struct Book {
isbn: i32,
format: BookFormat,
}
impl PartialEq for Book {
fn eq(&self, other: &Self) -> bool {
self.isbn == other.isbn
}
}
持续 Const
这就引出了下一点——const
随处可见。Rust 确实为我做好了充分的准备。我基本上把它当成了 的反面mut
。这是我的一个类头文件:
class Board
{
int dimension;
std::vector<Cell> receivedShots;
std::vector<Ship> ships;
public:
Board(int boardSize = BOARD_SIZE);
bool doesFit(ShipPlacement sp) const;
char getCharAt(Cell c, bool showShips) const;
Cell getRandomCell() const;
Cell promptCell(const std::string &promptStr) const;
void pushShip(Ship s);
std::vector<Cell> getAllShots() const;
bool receiveFire(Cell target);
int size() const;
lines toLineStrings(bool showShips) const;
};
这玩意儿真是太const
荒谬了。刚开始写代码的时候,我并没有意识到它的用途有多广,也因为害怕看不懂而犹豫要不要用。现在我的经验是,默认把它添加到任何方法中,只有确定用不了的时候才去掉。
天哪,这里满是溪流
C++ 非常依赖流抽象。当我想漂亮地打印一些数据时,我很快就遇到了这个问题。在 Rust 中,我会使用impl Display
,在更面向对象编程(OOP)的语言中,我会覆盖它toString()
或其他什么。
在 C++ 中,你实际上重载了<<
流插入运算符。举一个简单的例子:
enum Direction
{
Left,
Down
};
std::ostream &operator<<(std::ostream &stream, const Direction &d)
{
if (d == Direction::Left)
return stream << "Left";
else
return stream << "Down";
}
现在您可以将其直接弹出到流中,无需调用任何东西:
std::cout << "Direction: " << direction << "!" << std::endl;
这种模式一开始对我来说并不明显,但几天后感觉自然多了。
重载构造函数
我从未使用过能做到这一点的语言,所以它对我来说仍然很新颖,也很简洁。在 Rust 中,你需要使用 trait,这有点笨重。在 C++ 中,我可以直接定义三个构造函数:
class ShipClass
{
// ..
public:
ShipClass();
ShipClass(char c);
ShipClass(ShipClassType sc);
// ..
}
这是获得灵活 API 的一个非常简单的方法。
挫折
并非一切都令人满意。鉴于我之前的学习经验,我基本上已经预料到了 C++ 会给我带来的所有挑战,但还是有一些突出的问题我不确定该如何去喜欢它。
构建工具/包管理
对我来说,C++ 就像狂野的西部。
我还没开始使用CMake和Autotools之类的工具,但这些工具的存在本身就说明了很多问题。直接使用外部库真的很难,所以很多项目根本就无法进行。由于包管理一团糟,所以需要重新发明轮子。在外行人看来,这不是一个健康的生态系统,但语言本身足够强大,或许可以弥补这一点。尽管存在这个缺陷,但它本身也是一个庞大的生态系统,所以我想探索和使用它,但如果它太复杂,我就不会费心了。
还有一些东西boost
本身就是庞然大物。我想,我可能还需要几年时间才能对 C++ 生态系统的强大和质量做出一个合理且有理有据的评价。在那之前,它会让新手很反感。
我已经写过一篇关于的文章make
,这里就不再赘述了。那篇文章中我讲解的第二个示例正是我用来构建这个项目的 Makefile。
这是课程中的第一门 C++ 课程,我想稍后会讲到它,但对于这门课程,教授基本上说“我不关心你如何构建你的代码,只要确保我可以重新创建它,如果你不知道该怎么做,这里有一个下载 Visual Studio 的链接”。
我可能应该找个时间学习一下 Visual Studio,但我觉得一次学一个更容易,所以我还是坚持用我常用的文本编辑器,通过命令行进行编译。我之前在 Linux 上折腾了好几年,已经学会了如何使用 make。我不知道最好的方法是什么。看来在专业环境下,CI/CD 应该会运行所有编译器。
例外
这并不难理解,与 JS 或 Python 中的异常没有什么不同:
try
{
row = stoi(originStr.substr(1, originStr.size() - 1));
}
catch (const std::invalid_argument &ia)
{
std::cerr << "Please enter a number as your second term." << endl;
// ..
}
我想我可能被类型层面的东西惯坏了。我不喜欢这个,它看起来就像意大利面条式的代码,而且为了捕获整个应用的错误,需要写很多不必要的冗长代码。我还没怎么接触过模板,所以这可能是一个熟悉度的问题。它的工作方式看起来和我预期的 Python 或 JS 差不多。
类与结构体
这虽然不是问题,但对我来说仍然不太合理。在 Rust 中,所有东西都是struct
,你可以提供一个impl Struct
块来定义构造函数和/或方法(如果需要),或者只是不用于普通数据。
C++ 有结构体和类,但它们几乎相同。唯一的区别是默认可见性:结构体是公共的,类是私有的,但通过显式注释,它们在功能上是等效的。我尝试将结构体用于纯数据,将类用于其他任何数据,但界限很模糊。如果我有一个仅包装枚举但具有一堆不同 getter 的结构体,例如 achar
和 astring
和 an int
,那么它是类还是结构体?现在我有一个仅包含一行和一列的结构体,还定义了一些构造函数和相等方法。这与类没什么不同。我不知道哪个是正确的,或者这是否重要。当我定义一个新的结构体时,我只是做出了直觉决定,而没有再考虑它,这似乎没有什么区别。
追问:你会用 aunion
来做这种事吗?我还是不太清楚什么时候会用到它,除非我空间特别紧张。
结论
我很高兴我终于撕掉了创可贴并使用 C++ 来做一些更实质性的事情,但是以前从来没有在一开始就如此明显地看到这座大山的浩瀚。
照片由 Matthew Henry 在 Unsplash 上拍摄
文章来源:https://dev.to/decidously/getting-cozy-with-c-4j8n