面向C程序员的现代C++(二)
注意 本人的博客都迁移到本人自己搭建的博客地址,通过此处可查看。
欢迎回来!在第一部分中,我讨论了std: string
和std::vector
如何与C交互,包括与C标准库qsort调用交互。我们还发现C++std::sort
比Cqsort
快40%,因为C++能够内联比较函数
在这一部分中,我们将继续使用C++特性,您可以使用这些特性来为代码“逐行添加”,而不必立即使用所有1400页的“C++编程语言”。
这里讨论的各种代码示例可以在GitHub上找到。
如果你有任何你喜欢的东西,你希望看到讨论或问题,请联系@PowerDNS_Bert或bert.hubert@powerdns.com
命名空间
命名空间允许名称相同的事物同时存在。由于C++定义了许多函数和类,这些函数和类可能会与您在C中使用的名称发生冲突,因此这对我们来说是非常重要的。因此,C++库就在std:: namespace
中,使编译C代码变得更容易。
为了节省大量的输入,可以导入整个std::namespaces
通过using namespace std
,或者选择单个名称:using std::thread
。
C++本身确实有一些关键字,比如class、throw、catch和reintrepret_cast,它们可能会与现有的C代码发生冲突。
类
C++又一个名称是“带类的C”,由一个转换器把这个新的C++转换成普通的C语言。有趣的是,这个翻译本身就是用“带有类的C语言编写的”。
大多数高级C项目已经使用了与C++几乎完全相同的类。在最简单的形式中,类只不过是具有一些调用约定的结构体。(继承和虚函数使情况变得复杂,这些可选技术将在第3部分中讨论)。
典型的现代C代码将定义一个描述某样东西的结构,然后有一组函数接受指向该结构的指针作为第一个参数:
struct Circle
{
int x, y;
int size;
Canvas* canvas;
...
};
void setCanvas(Circle* circle, Canvas* canvas);
void positionCircle(Circle* circle, int x, int y);
void paintCircle(Circle* circle);
许多C项目实际上甚至会使这些结构(部分)变得不透明,这表明API用户不应该看到这些内部结构。这是通过在.h中向前声明一个结构体来实现的,但是从来没有定义它。sqlite3句柄就是这种技术的一个很好的例子。
一个C++类就像上面的struct一样布局,实际上,如果它包含了方法(成员函数),那么这些内部调用的方式完全相同:
class Circle
{
public:
Circle(Canvas* canvas); // "constructor"
void position(int x, int y);
void paint();
private:
int d_x, d_y;
int d_size;
Canvas* d_canvas;
};
void Circle::paint()
{
d_canvas->drawCircle(d_x, d_y, d_size);
}
如果我们看“水下”Circle::position(1,2)实际上被称为Circle::position(Circlethis,int x, int y)。没有比这个更神奇的(或头顶上的)了。此外,Circle::paint和Circle::position*函数在范围内具有d_x、d_y、d_size和d_canvas。
两者之间区别之一是这些“私有成员变量”不能从外部访问。例如,当x中的任何更改需要与画布协调时,这可能是有用的,而且我们不希望用户在不知情的情况下更改x。如前所述,许多C项目都通过技巧实现同样的不透明——这只是一种更简单的方法。
到目前为止,类仅仅是语法糖和一些范围规则。然而. .
资源获取是初始化(RAII)
大多数现代语言需要执行垃圾收集是因为很难跟踪内存。这导致周期性的GC运行,有可能“阻止世界”。尽管技术正在改进,GC仍然是一个令人担忧的问题,尤其是在一个多核心的世界中。
尽管C和C++不进行垃圾收集,但仍然是非常困难的,在所有(错误)条件下跟踪每个内存分配是非常困难的。C++有复杂的方法来帮助您,这些方法构建在称为构造函数和析构函数的原语之上。
SmartFP 就是一个例子,我们将在接下来的章节中对其进行补充,这样它就会变得更加有用和安全:
struct SmartFP
{
SmartFP(const char* fname, const char* mode)
{
d_fp = fopen(fname, mode);
}
~SmartFP()
{
if(d_fp)
fclose(d_fp);
}
FILE* d_fp;
};
注意: 结构体和类是一样的,除了所有的东西都是“公共的”。
典型的使用SmartFP:
void func()
{
SmartFP fp("/etc/passwd", "r");
if(!fp.d_fp)
// do error things
char line[512];
while(fgets(line, sizeof(line), fp.d_fp)) {
// do things with line
}
// note, no fclose
}
就像这样编写的,当SmartFP对象实例化时,就会发生对fopen()的实际调用。它调用构造函数,构造函数的名称与结构本身相同:SmartFP。
然后,我们可以像往常一样使用存储在类中的文件*。最后,当fp超出范围时,调用它的析构函数SmartFP::~SmartFP(),如果d_fp在构造函数中成功打开,它将为我们fclose()。
这样写,代码有两个巨大的优点:1)文件指针永远不会泄漏2)我们确切地知道何时关闭。带有垃圾收集的语言也保证了“1”,但交付“2”需要努力或需要真正的工作。
这种使用带有构造函数和析构函数的类或结构来拥有资源的技术称为资源获取初始化或RAII,它被广泛使用。对于更大的c++项目来说,在构造函数/析构函数对之外不包含对new或delete(或malloc/free)的单个调用是很常见的。或者事实上。
智能指针
内存泄漏是每个项目的祸害。即使使用垃圾收集,也可以将千兆字节的内存用于显示聊天消息的单个窗口。
C++提供了许多所谓的智能指针,它们都有各自的优势。最“做我想做的”智能指针是std:: shared_ptr
,它最基本的形式是:
void func(Canvas* canvas)
{
std::shared_ptr<Circle> ptr(new Circle(canvas));
// or better:
auto ptr = std::make_shared<Circle>(canvas)
}
第一个表单显示了使用C++编写malloc的方法,在本例中,为Circle实例分配内存,并使用canvas参数构造它。如前所述,大多数现代C++项目很少使用“naked new”语句,而是将它们封装在负责(de)分配的基础设施中。
第二种方法不仅打字更少,而且效率更高。
不过,shared_ptr有更多的妙招:
// make a vector of shared pointers to Circle instances
std::vector<std::shared_ptr<Circle> > circles;
void func(Canvas* canvas)
{
auto ptr = std::make_shared<Circle>(canvas)
circles.push_back(ptr);
ptr->draw();
}
这首先定义了一个std::shared_ptrs
的向量,然后创建一个shared_ptr并将其存储在Circle向量中。当func返回时,ptr超出了范围,但是由于它的副本在矢量圆中,所以Circle对象仍然是活着的。因此shared_ptr是一个引用计数智能指针。
shared_ptr还有一个很好的功能:
void func()
{
FILE *fp = fopen("/etc/passwd", "r");
if(!fp)
; // do error things
std::shared_ptr<FILE> ptr(fp, fclose);
char buf[1024];
fread(buf, sizeof(buf), 1, ptr.get());
}
在这里,我们使用名为fclose的自定义删除器创建shared_ptr。这意味着如果需要,ptr知道如何在自己之后进行清理,并且使用一行创建了一个引用计数文件句柄。
有了这个,我们现在就能明白为什么我们之前定义的SmartFP不是很安全了。可以对它进行复制,一旦该复制超出范围,它也将关闭相同的文件*。shared_ptr使我们不必考虑这些事情。
std::shared_ptr的缺点是它使用内存进行实际的引用计数,这对于多线程操作也是安全的。它还必须存储一个可选的自定义删除程序。
c++还提供了其他智能指针,其中最相关的是std::unique_ptr。我们通常不需要实际的引用计数,只需要“在超出范围时进行清理”。这就是std::unique_ptr所提供的,几乎为零开销。还有一些工具可以将std: unique_ptr“移动”到存储中,以便它在范围内。我们稍后再讨论这个问题。
线程、原子
每次我用C或更老的C++创建一个带有pthread_create的线程时,我都会感觉很糟糕。必须填满所有的数据,才能通过一个空指针来启动线程,这感觉很傻很危险。
C++在本机线程系统之上提供了一个强大的层,以使这一切更容易和更安全。此外,它还可以轻松地从线程中获取数据。
一个小示例:
double factorial(unsigned int limit)
{
double ret = 1;
for(unsigned int n = 1 ; n <= limit ; ++n)
ret *= n;
return ret;
}
int main()
{
auto future1 = std::async(factorial, 19);
auto future2 = std::async(factorial, 12);
double result = future1.get() + future2.get();
std::cout<<"Result is: " << result << std::endl;
}
如果不需要返回代码,那么启动一个线程就像:
std::thread t(factorial, 19);
t.join(); // or t.detach()
与C11一样,C++也提供原子操作。这些就像定义std::atomic<uint64_t>
packetcounter一样简单。packetcounter上的操作是原子性的,如果需要特定的模式(例如构建无锁数据结构),可以使用一套广泛的方法来询问或更新packetcounter。
注意,与在C中一样,将计数器从多个线程中声明为volatile没有任何用处。需要全原子或显式锁定。
Looking
就像跟踪内存分配一样,确保在所有代码页上释放锁是很困难的。像往常一样,赖伊伸出援手:
std::mutex g_pages_mutex;
std::map<std::string, std::string> g_pages;
void func()
{
std::lock_guard<std::mutex> guard(g_pages_mutex);
g_pages[url] = result;
}
上面的守护对象将在需要的时候将g_pages_mutex锁定很长时间,但是无论是否通过错误,在func()完成时总是会释放它。
错误处理
老实说,错误处理在任何语言中都是一个难以解决的问题。我们可以用检查来对代码进行谜语,每次检查时,我都想知道“如果失败了,程序实际上应该做什么”。选项很少是好的——忽略、提示用户、重新启动程序或记录消息,希望有人阅读它。
C++提供的异常在任何情况下都比检查每个返回代码有一些好处。异常的好处是,与返回代码不同,它在默认情况下不会被忽略。首先让我们更新SmartFP,它会抛出异常:
std::string stringerror()
{
return strerror(errno);
}
struct SmartFP
{
SmartFP(const char* fname, const char* mode)
{
d_fp = fopen(fname, mode);
if(!d_fp)
throw std::runtime_error("Can't open file: " + stringerror());
}
~SmartFP()
{
fclose(d_fp);
}
FILE* d_fp;
};
如果我们现在创建了一个SmartFP,并且它没有抛出异常,我们知道使用它是很好的。对于错误报告,我们可以捕获异常:
void func2()
{
SmartFP fp("nosuchfile", "r");
char line[512];
while(fgets(line, sizeof(line), fp.d_fp)) {
// do things with line
}
// note, no fclose
}
void func()
{
func2();
}
int main()
try {
func();
}
catch(std::exception& e) {
std::cerr<< "Fatal error: " << e.what() << std::endl;
}
这显示了一个从SmartFP::SmartFP
中抛出的异常,该异常随后“通过”func2()和func()以在main()中被捕获。关于fallthrough的好处是,错误总是会被注意到,而不像简单的返回代码可以忽略。然而,不利的一面是,异常可能会在离抛出异常的地方很远的地方被“捕获”,这可能会导致意外。这通常会导致良好的错误记录。
结合RAII,异常是一种非常强大的技术,可以安全地获取资源并处理错误。
可以抛出异常的代码比不能抛出异常的代码稍慢一些,但它几乎不会出现在概要文件中。实际上抛出异常是相当重的,所以只在错误条件下使用它。
大多数调试器可以在抛出异常时中断,这是一种强大的调试技术。在gdb中,这是通过接球完成的。
如前所述,没有任何错误处理技术是完美的。一个看起来很有希望的东西是std::expect
工作或boost::expect
,它创建的函数具有返回代码或抛出异常(如果不查看它们)。
总结
在“C++为C程序员”的第2部分中,我们展示了类是如何在C中得到很好的应用的,除了C++使它更容易。此外,C++类(和结构)可以有构造函数和析构函数,这些函数对于确保在需要时获取和释放资源非常有用。
基于这些基本类型,C++提供了各种智能和开销的智能指针,涵盖了大多数需求。
此外,C++为线程、原子和锁定提供了良好的支持。最后,异常是处理错误的一种强大方法。
如果你有任何你喜欢的东西,你希望看到讨论或问题,请联系@PowerDNS_Bert或bert.hubert@powerdns.com
请继续关注第3部分!
**注:**原文MODERN C++ FOR C PROGRAMMERS: PART 1.