C++编程自学宝典

978-7-115-49240-1
作者: [英]理查德·格里姆斯(Richard Grimes)
译者: 邓世超
编辑: 胡俊英
分类: C++

图书目录:

详情

本书旨在通过全面细致的内容和代码示例,带领读者更加全方位地认识C++语言。全书内容共计10章,由浅入深地介绍了C++的各项特性,包括C++语法、数据类型、指针、函数、类、面向对象特性、标准库容器、字符串、诊断和调试等。本书涵盖了C++11规范及相关的C++标准库,是全面学习C++编程的合适之选。

图书摘要

版权信息

书名:C++编程自学宝典

ISBN:978-7-115-49240-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [英] 理查德•格里姆斯(Richard Grimes)

译    邓世超

责任编辑 胡俊英

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Copyright ©2017 Packt Publishing. First published in the English language under the title Beginning C++ Programming.

All rights reserved.

本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


作为一门广为人知的编程语言,C++已经诞生30多年了,这期间也出现并流行过许多种编程语言,但是C++绝对是经得起考验的。如此经典的编程语言,值得每一位编程领域的新人认真学习,也适合有经验的程序员细细品味。

本书旨在通过全面细致的内容和代码示例,带领读者更加全方位地认识C++语言。全书分为10章,由浅入深地介绍了C++的各项特性,包括C++语法、数据类型、指针、函数、类、面向对象特性、标准库容器、字符串、诊断和调试等。本书涵盖了C++11规范及相关的C++11标准库,是全面学习C++编程的合适之选。

本书适合C++零基础读者,但是希望读者有一些编程经验。通过书中丰富、典型的代码示例,读者将快速把握C++的特性和编程技巧。


Richard Grimes有20多年C++编程经验,曾经致力于汽车制造业远程设备的金融分析和科学控制等多个项目。他在70多场微软技术(其中包含C++和C#)的国际会议上发表重要讲话,共编写了8本书,在编程期刊上发表了150多篇文章,主讲了5期微软培训课程。他连续10年(1998—2007)获得微软MVP认证,对微软.net框架和C++的深刻理解以及对新技术的坦率评价使其在业内享有盛誉。


Angel Hernandez是一位备受瞩目的高级解决方案提供商、架构师,并且拥有超过15年的软件开发经验,擅长咨询领域。他曾经连续11年获得微软Visual Studio和开发技术(之前是Visual C++)类别的MVP称号,他目前是微软MVP重新连接计划的成员,同时还是一名TOGAF从业者。他对微软和开源技术(*nix系统)有深入的了解,也是托管和原生语言专家,最大的爱好是C#和C++。

致我的妻子Ellinor:只有你的爱和支持能让我无往不胜。


C++已经问世30多年了。在此期间,很多新的语言来了又走,但是C++经得起考验。本书背后的一个大问题就是:为什么选择C++?答案就分布于读者将要看到的本书的10章内容中。但作为一个“搅局者”,C++是一门灵活、强大的语言,并且拥有丰富、庞大的标准库提供支持。

C++一直是一门强大的语言,可以让用户直接访问内存,同时提供大量的高级特性,比如创建新类型和类的能力,以及重载运算符以满足用户需求。然而,更现代的C++标准添加了不少特性:通过模板进行泛型编程,通过函数对象和lambda表达式进行函数式编程。用户可以根据需要充分地利用这些特性,也可以使用抽象接口指针或类C过程代码编写事件驱动代码。

在本书中,我们将介绍C++11规范以及通过该语言提供的标准库。本书使用简短的代码片段解释了如何使用这些特性,每一章包含一个实用示例来解释这些概念。在本书的最后,读者将了解该语言的所有功能以及C++标准库可以实现的功能。假定读者是初学者,本书将引导和提示读者从零开始使用C++。

第1章“初识C++”介绍了用于编写C++应用程序的文件、文件引用依赖以及基本的C++项目管理知识。

第2章“语言特性简介”涵盖了C++语句、表达式、常量、变量和运算符,以及如何在应用程序中控制执行流程。

第3章“C++类型探秘”描述了C++内置类型、聚合类型、类型别名、初始化器列表以及类型之间的转换。

第4章“内存、数组和指针”介绍了在C++应用程序中如何分配和使用内存、如何使用内置类型、C++引用的角色以及如何使用C++指针访问内存。

第5章“函数”解释了如何定义函数、如何使用可变数目的参数通过值和引用传递参数、创建和使用函数指针以及定义模板函数和重载运算符。

第6章“类”介绍了如何通过类定义新类型以及在类中使用多种专一化函数,如何将类实例化为对象以及如何将其销毁,如何通过指针访问对象以及如何编写模板类。

第7章“面向对象编程简介”介绍了继承和组合技术,以及它们如何影响指针、引用对象和类成员访问层级的使用,它们如何继承成员。本章还介绍了如何通过虚方法实现多态、通过抽象类实现继承编程。

第8章“标准库容器”介绍了C++标准库容器类,以及如何将它们和迭代器、标准库算法搭配使用,以便用户可以访问容器中的数据。

第9章“字符串”介绍了标准C++字符串类的特性、数字和字符串之间的转换、国际化字符串,以及如何使用正则表达式搜索和操作字符串。

第10章“诊断和调试”介绍了如何准备代码以便诊断和调试、如何优雅地终止应用程序以及如何使用C++异常机制。

本书涵盖了C++11规范以及相关的C++标准库。对于本书的绝大多数内容,任何符合C++11规范的编译器都是适合的,这些编译器的厂家包括Intel、IBM、Sun、Apple和Microsoft,以及开源的GCC编译器。

本书采用的开发环境是Visual C++ 2017社区版,因为它是一个功能齐全的编译器和开发环境,是可以免费下载的。这是作者的个人选择,不过也不会限制读者选择其他编译器。第10章的某些部分介绍了专属于Microsoft的一些特性,但是这些部分都已清楚地标记出来。

本书适用于有一定编程经验,但还是C++新手的程序员。希望读者在阅读本书之前已经知道什么是高级语言以及相关的基本概念,比如模块化代码和程序控制执行流程。

在本书中,读者将发现一些用于区分不同信息的文本样式。以下是这些样式的一些示例及其含义的解释。

文本形式的代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、简单的URL地址、用户输入、引用段落如下所示:“We can include other contexts through the use of the include directive”。

代码块设置如下所示:

class point
{
public:
    int x, y;
};

当我们希望某些代码片段引起读者注意时,相关的行或元素将以粗体表示:

class point
{
public:
    int x, y;
    point(int _x, int _y) : x(_x), y(_y) {}
};

任何命令行输入或输出如下所示:

C:\> cl /EHsc test.cpp

新术语或关键字都以粗体显示。读者在屏幕上看到的单词,比如在菜单或者对话框中,会以如下文本显示:“Clicking the Next button moves you to the next screen”。

 警告 

警告或需要特别注意的内容。

 

 提示 

提示或者技巧。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

要获得以上配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


为什么选择C++?从读者自身的实际情况来看,原因有很多。

读者选择C++可能是因为必须为一个C++项目提供技术支持。在超过30年的生命周期中,该项目中已经包含了数百万行C++代码,并且大部分流行的应用程序和操作系统是使用C++编写的,或者是使用了与之有关的组件和库。几乎不可能找到一台不包含C++代码的电脑。

或者读者打算使用C++编写新的代码。这可能是因为项目代码中将会用到一个使用C++编写的程序库,而且有成千上万的程序库可供选择:开源的、共享的和商业软件。

或者读者可能是被C++强大的功能和灵活性所吸引。现代高级程序语言的目标是将程序员从繁复的编程工作中解放出来。同时,C++还允许用户和机器保持尽可能紧密的联系,使得用户可以直接访问计算机内存(有时是比较危险的)。通过类和重载这些语言特性,C++是一门灵活的语言,我们可以对它进行功能扩展,并编写可复用的代码。

不论读者选择C++的理由是什么,这个决定都是非常明智的,本书可以作为读者入门的起点。

本书是一本实用性的书,读者可以对其中的代码输入、编译和运行。为了编译代码,你将需要一个C++编译器和链接器,在本书中它们是指提供Visual C++的Visual Studio 2017社区版程序。选择该编译器是因为我们可以免费下载它,它符合C++标准规范,并且包含大量能够提高编程效率的工具。Visual C++提供了对C++11语言特性的支持,并几乎兼容C++14和C++17的所有语言特性。Visual C++还包含了C99运行时库、C++11标准库和C++14标准库。上述所有规范意味着读者在本书中将要学习的代码,将能够被其他所有标准的C++编译器编译。

本章将从如何获取和安装Visual Studio 2017社区版程序的细节开始。如果你已经拥有了一个C++ 编译器,那么可以跳过本小节。本书大部分内容是与编译器和链接器无关的。但是第10章介绍调试和诊断技术时会涉及一些专属于Microsoft的功能特性。Visual Studio是一款功能齐全的代码编辑器,所以即使你不使用它来管理项目文件,也仍然会发现它对于编辑代码来说是非常有用的。

在介绍完程序安装之后,读者将学习一些C++的基础知识:如何组织源码文件和项目,以及如何管理可能存在几千个文件的项目。

最后,本章将以一个循序渐进的结构化示例作为结尾。这里读者将学习如何使用C++标准库编写简单的函数以及一种管理项目文件的机制。

C++的前身是C语言,C语言是由Dennis Richie供职于贝尔实验室时设计的,于1973年首次发布。C语言曾经广受青睐,并且用于编写早期的Unix和Windows版本。事实上,大部分操作系统的程序库和软件开发包仍然包含C语言接口。C语言功能很强大,因为使用它编写的代码可以被编译成一种紧凑格式,它采用了静态类型系统(因此编译器可以进行类型检查),并且该语言的类型和结构支持直接访问内存的计算机架构。

不过C语言是过程式的并且基于函数,虽然它包含能够封装数据的记录类型(struct),但是它不包含类似对象的行为来表现被封装的状态。显然,用户迫切希望有一种语言既拥有C语言的强大功能,又拥有面向对象的类的灵活性和可扩展性,也就是一种支持类的C语言。1983年,Bjarne Stroustrup发明了C++,++符号来自C语言的增量运算符++。

 警告 

严格来说,在作为变量后缀时,++运算符表示变量执行自增操作,但返回的变量值是它执行自增操作之前的。因此在C语言代码语句“int c = 1; int d = c++;”当中,变量d获得的返回值是1,变量c的值是2。从这一点来看,它并没有明确地表达C++是C的增量这一理念。

Microsoft的Visual Studio Community 2017包含Visual C++编译器、C++标准库和一组可以帮助我们编写和维护C++项目的工具。本书不是专门讲述如何编写Windows代码的,而是主要讲述如何编写标准C++程序和如何使用C++标准库的。本书中的所有示例都能够在命令行中运行。选择Visual Studio的原因是它可以免费下载(当然你还必须使用一个e-mail地址注册一个Microsoft账户),并且它是符合标准的。如果读者已经安装了C++编译器,那么可以跳过本小节。

在开始安装上述程序之前,有一点读者需要注意,那就是将Visual Studio作为Microsoft的社区版程序的一部分安装时,你应该拥有一个Microsoft账号。我们可以在首次运行Visual Studio程序时创建一个Microsoft账号,如果跳过这个步骤,将获得为期30天的试用期。Visual Studio在一个月之内将会正常运行,但是如果你希望在此之后继续使用Visual Studio ,则必须提供一个Microsoft账号。Microsoft账号不会要求用户承担任何义务,并且在用户登录后使用Visual C++时,相关的代码仍然在其本地计算机上,无需将它们发送给Microsoft公司。

当然,如果你在一个月之内读完本书,那么将能够在不需要使用Microsoft账号的情况下使用Visual Studio,并且可以将之作为努力读完本书的一种动力!

读者可以到Visual Studio官网下载其安装包,当单击Visual Studio Community 2017的“Download”按钮后,浏览器会自动下载一个名为vs_community__1698485341. 1480883588.exe的程序,其大小约为1MB。当运行该程序后,它会要求你选择希望安装的语言和程序库,然后下载和安装所有必需的组件。

Visual Studio 2017会将Visual C++视为一个可选组件,所以我们必须显式声明希望通过自定义选项安装。当你首次执行这个安装程序时,将看到图1-1所示的对话框。

图1-1

当你单击“Continue”按钮后,应用程序将配置安装程序,如图1-2所示。

对话框顶部有3个选项卡,分别是“Workloads”“Individual Components”和“Language Packs”。你务必确保选择的是“Workloads”选项卡(如图1-2所示),然后选择名为“Desktop development with C++”的复选框。

图1-2

安装程序将会为你选定的项目检查本地计算机是否拥有足够的磁盘空间。安装Visual Studio所需的最大磁盘空间是8GB。当然,对于Visual C++来说所需的磁盘空间会小很多。当你选择“Desktop development with C++”项目后,将会发现对话框的右侧发生了变化,其中列出了已经选择的项目和所需的磁盘空间大小,如图1-3所示。

图1-3

对于本书来说,请保留安装程序默认选择的内容,然后单击右下角的“Install”按钮即可。安装程序将会下载所有必需的内容,并通过图1-4所示的对话框显示安装进度。

图1-4

安装完成后,Visual Studio Community 2017程序将包含“Modify”和“Launch”两个按钮,如图1-5所示。

图1-5

Modify”按钮允许我们添加更多组件,单击“Launch”将启动Visual Studio的首次运行。

首次运行Visual Studio时,它会要求用户通过图1-6所示对话框登录Microsoft的服务。

图1-6

如果用户不愿意,并不一定必须注册Visual Studio。这种情况下,Visual Studio将只能正常工作30天。注册Microsoft账号并不会让用户承担任何义务。如果用户愿意注册,那么可以马上注册。单击“Sign in”按钮提供你的Microsoft账号信息,如果你还没有账号,可以单击“Sign up”按钮注册一个账号。

 提示 

当我们单击“Launch”按钮后,一个新的窗口将会打开,但是安装程序窗口仍然是打开状态。你会发现安装程序窗口挡住了“Welcome”窗口,因此应检查Windows任务栏,看另一个窗口是否处于打开状态。Visual Studio程序启动之后,你就可以将安装程序窗口关闭。

现在我们可以使用Visual Studio编辑代码了,并且Visual C++的编译器和链接器也安装到了计算机上,因此能够在Visual Studio或命令行中编译C++代码。

C++项目中可以包含几千个文件,并且管理这些文件甚至可以成为一个单独的工作任务。当构建项目时,如果应该编译某个文件,那么选择哪种工具编译它?文件应该按照什么顺序编译?这些编译器生成的输出结果又是什么?编译后的文件应该如何组织到一起构造可执行文件?

编译器工具还拥有大量的选项,比如调试信息、优化类型、为多种语言特性提供支持以及处理器特性。编译器选项的不同组合将会用于不同场景(比如版本构建和版本调试)。如果用户是在命令行上执行编译任务的,那么务必确保选择了正确的选项,并在编译所有源代码的过程中始终应用它们。

文件和编译器选项的管理可以变得很复杂。这也是用户应该使用一款构建工具处理即将上线的产品代码的原因。与Visual Studio一起安装的构建工具有:MSBuildnmake两款。当用户在Visual Studio环境下构建一个Visual C++项目时,将使用MSBuild,并且会把编译规则存放在一个XML文件中。用户甚至可以在命令行中调用MSBuild,将XML项目文件传递给它。nmake是Microsoft在多个编译器之间维护程序多个版本的实用性工具。在本章中,读者将学习如何充分利用nmake的实用性编写一个简单的makefile文件。

在介绍项目管理的基础知识之前,我们必须先了解用户通常会在C++项目中找到哪些文件以及编译器会如何处理这些文件。

C++是一门高级程序语言,旨在为用户提供丰富的语言特性,以及为用户和其他开发人员提供良好的可读性。计算机的处理器执行底层代码,并且这也是编译器将C++代码转化成处理器的机器码的主要目的。单个编译器也许可以兼容多种处理器,如果代码是符合C++规范的,那么它们还可以被其他编译器编译,以便兼容其他处理器。

不过,编译器的功能远不止于此。如第4章所述,C++允许用户将代码分割成若干函数,这些函数可以接收参数并返回一个值,因此编译器可以配置内存来传递这些数据。此外,函数可以声明只在函数内部使用的变量(第5章将介绍更多细节),并且它将只在函数被调用时才存在。编译器配置的内存称为栈帧(stack frame)。编译器中包含如何创建栈帧的选项,比如Microsoft的编译器选项/Gd/Gr/Gz决定了函数参数被推送到堆栈上的次序,以及调用方函数或被调用函数在调用结束时是否应该从堆栈中移除这些参数。当我们编写的代码需要和其他人共享时,这些选项将非常重要(不过基于本书的目的,应该会使用默认的堆栈结构)。这只是冰山一角,不过编译器选项为用户提供的强大功能和灵活性应该会让读者印象深刻。

编译器编译C++代码,如果遇到代码中的错误,将向用户发送编译器错误提示信息。它是对代码的语法检查。这对于确保用户从语法角度编写完美的C++代码非常重要,不过这仍然可能是在做无用功。编译器的语法检查对于检查代码来说非常重要,不过用户应该总是使用其他方法检查代码。比如下列代码声明了一个整数类型变量并为它赋值:

int i = 1 / 0;

编译器将向用户提示C2124错误:divide or mod by zero(除数不能为0)。不过,下列代码将使用额外的变量执行相同的操作,但是编译器不会报错:

int j = 0;
int i = 1 / j;

当编译器提示出现错误时将停止编译。这意味两件事:首先,你将无法得到编译输出结果,因此将不会在一个可执行文件中找到该错误;其次,如果源代码中存在其他错误,我们只有在修复当前错误重新编译代码时才会发现它。如果你希望对代码执行语法检查并退出编译,可以使用/Zs选项开关。

编译器还会生成警告信息。一个警告意味着代码将被编译,但是代码中的某个问题可能会对生成的可执行文件产生潜在的不良影响。Microsoft编译器将警告分为4个级别:级别1是最严重的(应该立刻解决),级别4是信息性的。警告通常用于向用户声明被编译的语言特性可以正常运行,不过它需要的某个特定编译器选项,开发者并没有使用。

在开发代码期间,我们将会经常忽略警告信息,因为这可能是在测试某些语言特性。

不过,当开发的代码准备上线发布时,你最好对警告信息多加留意。默认情况下,Microsoft编译器将显示1级警告信息,你可以使用/W选项和一个数字来声明希望看到的警告信息级别(比如,/W2表示用户希望看到2级警告以及1级警告)。在正式上线的产品代码中,你可能会使用/Wx选项,这是告知编译器将警告信息也当作错误来看待,我们必须修复所有问题,以便能够顺利编译代码。你还可以使用pragma编译器(pragma的概念将稍后介绍),并且编译器的选项还可以忽略特定警告信息。

编译器将生成一个输出。对于C++代码来说,这将是对象代码,不过你可能还会得到一些其他的编译器输出,比如被编译的资源文件。对于它们自身来说,这些文件无法被执行,尤其是操作系统需要设置特定的结构时。一个C++项目将始终包含两个阶段:先将源代码编译成一个或者多个对象文件,然后将这些对象文件链接到一个可执行程序中。这意味着C++编译器将提供另外一种工具,即链接器。

链接器也有决定它如何工作并指定输出和输入的选项供用户选择,并且它还会向我们发出错误和警告信息。与编译器类似,Microsoft的链接器也有一个选项/WX,它可以将预览版程序中的警告信息当作错误来处理。

在最基本的层面,一个C++项目将只包含一个文件,即C++源代码文件。该文件一般是以cpp或者cxx后缀结尾的。

1.一个简单示例

一个最简单的C++程序如下:

#include <iostream>

//  程序的入口点
int main()
{
    std::cout << "Hello, world!n";
}

第一点需要注意的是,以//开头的行是注释。编译器将忽略直到该行末尾的所有文本。如果你希望使用多行注释,则注释的每行都必须以//开头。你还可以使用C语言风格的注释。一个C语言风格的注释是以/*开头、以*/结尾的,这两个标识符之间的内容就是一个注释,包括换行符。

C语言风格的注释是一种对部分代码进行快速说明解释的方式。

大括号{}表示一个代码块。在这种情况下,C++代码就是函数main。我们可以根据基本的格式判断这是一个函数,首先,它声明了返回值类型,然后具有一对括号的函数名,括号中常用于声明传递给该函数的参数(和它们的类型)。在这个示例中,函数名是main,括号内是空的,说明该函数没有参数。函数名之前的标识符(int)表示该函数将返回一个整数。

C++中约定名为main的函数是可执行文件的入口,即在命令行中调用该可执行程序时,该函数将是项目代码中首个被调用的函数。

 提示 

这个简单示例函数会立即让读者陷入到可能激怒其他语言程序员的一个状态:该语言可能有一定规则,但是不一定总是需要遵循这些规则。在这种情况下,main函数被声明为返回一个整数,但是相关代码却没有返回值。C++中的相关规则是,如果函数声明了返回值,那么它必须返回一个值。不过,该规则存在一个例外情况:如果main函数没有返回值,那么系统默认会将0作为它的返回值。C++包含很多类似的奇怪约定,不过你将很快了解这些内容并对此习以为常。

main函数只包含一行代码:这个单条语句是以std开头,然后以一个分号(;)作为结尾的。C++中空格的使用非常灵活,与之相关的详情将在下一章介绍。不过,有一点读者必须特别留意,那就是在使用文本字符串时(比如本文中使用的),每个语句都是用分号分隔的。语句末尾缺少分号是编译器错误的常见来源。一个额外的分号只表示一个空语句,因此对于新手来说,项目代码中分号太少的问题比分号过多更致命。

示例中的单个语句会在控制台上打印输出字符串“Hello, world!”(以及一个换行符)。我们知道这是一个字符串,因为它是用双引号标记包裹起来的("")。该语句的含义是使用运算符<<将上述字符串添加到流对象std::cout中。该对象名中的std表示一个命名空间,实际上代表一组包含类似目的的代码集合,或者来自单个供应源。在这种情况下,std表示cout流对象是C++标准库的一部分。双冒号::是域解析运算符,并表示你希望访问的cout对象是在std命名空间下声明的。你还可以定义属于自己的命名空间,并且在一个大型项目中用户应该定义自己的命名空间,因为它允许我们使用可能已经存在于其他命名空间的名称进行变量定义,并且这种语法使我们可以消除标识符的歧义。

对象coutostream类的一个实例,并且在main函数被调用之前已经创建。<<表示一个名为运算符<<的函数被调用,并传递了相关的字符串(它是一个字符型数组)。该函数会将字符串中的每个字符打印输出到控制台上,直到遇到一个NUL字符。

这是一个演示C++灵活性的示例,即被称为运算符重载的特性。运算符<<经常会与整数一起使用,它被用于将某个整数向左移动指定数目的位置;x << y将返回一个将x向左移动y位后的值,实际上返回的值是x乘以2y后的值。不过,在上述代码中,代替整数x的是流对象std::cout,并且代替左移索引的是一个字符串。很明显,运算符<<在C++中的定义并未生效。当运算符<<出现在一个ostream对象的左边时,C++规范已经高效地对它进行了重新定义。此外,代码中的运算符<<将在控制台上打印输出一个字符串,因此它会接收位于右边的一个字符串。C++标准库中还定义了其他的<<运算符,使得用户可以将其他类型的数据打印输出到控制台。它们的调用方式都是一样的,编译器会根据使用的参数类型来决定使用哪个函数。

如前文所述,std::cout对象已经作为ostream类的一个实例被创建,但是没有告知用户这是如何发生的。这将引出我们对这个简单源码文件没有解释的最后一个部分:以#include开头的第一行代码。这里#会高效地向编译器声明某种类型的信息。

可供发送的信息有多种(比如#define#ifdef#pragma,本书后续的内容将会涉及它们)。在这种情况下,#include告知编译器在此处将特定文件的内容拷贝到该源代码文件中,实际上这意味着上述文件的内容也将被编译。这种特定的文件也叫头文件,并且在文件管理和通过库复用代码方面很重要。

文件<iostream>是标准库的一部分,可以在C++编译器附带的include目录下找到。尖括号(<>)表示编译器应该到用于存储头文件的标准目录中查找相关内容,不过我们可以通过双引号("")提供头文件的绝对路径(或者当前文件的相对路径)。C++标准库按照惯例不使用文件的扩展名。你在命名自己的头文件时,最好使用h(或者hpp,但很少使用hxx)作为文件的扩展名。C运行时库(也可以在C++代码中运行)中对它的头文件也会使用h作为其扩展名。

2.创建源文件

首先在“开始”菜单中找到Visual Studio 2017文件夹,然后单击“Developer Command Prompt for VS2017”项。这个操作将会启动一个Windows命令提示符并为Visual C++ 2017配置环境变量。不过遗憾的是,它还会将命令行程序停留在Program Files目录下的Visual Studio文件中。如果你希望进行程序开发工作,将会希望将命令行程序从该文件夹移动到其他文件夹中,以便在创建和删除文件时不会对上述目录下的文件造成不良影响。在执行此操作之前,请转到Visual C++目录下,并列出其中文件:

C:\Program Files\Microsoft Visual Studio\2017\Community>cd
%VCToolsInstallDir%
C:\Program Files\Microsoft Visual
Studio\2017\Community\VC\Tools\MSVC\14.0.10.2517>dir

因为安装程序将把C++文件放在一个包含当前版本编译器的文件夹中,所以为了确保系统采用了最新版本的程序(目前的版本号是14.0.10.2517),通过环境变量VCToolsInstallDir要比声明特定的版本安全得多。

有几件事是需要留意的。首先是C++项目文件中的binincludelib目录,关于这3个文件夹的用途如表1-1所列。

表1-1  

文件夹

用  途

bin

它间接包含了Visual C++的可执行文件程序。bin目录下将根据用户使用的CPU类型包含若干独立的文件目录,因此用户必须导航到该目录下才能获取包含可执行程序的实际文件夹。两个主要的可执行文件分别是作为C++编译器的cl.exe和作为链接器的link.exe

include

该文件夹下包含C运行时库和C++标准库的头文件

lib

该文件夹下包含C运行时库和C++标准库的静态链接库文件。此外,还有专属于不同种类CPU的文件,本章后续的内容将详细介绍它们

本章后续的内容还会涉及这些文件夹。

另外要指出的是位于文件夹VC\Auxillary\Build下的vcvarsall.bat文件。当我们在“开始”菜单上单击“Developer Command Prompt for VS2017”项时,这个批处理文件将被执行。如果希望在一个现有的命令提示符中编译C++代码,那么可以通过运行这个批处理文件进行设置。该批处理文件中3个最重要的操作是设置环境变量PATH,以便其中包含bin文件的路径,然后将环境变量INCLUDELIB分别指向includelib文件夹。

现在导航到根目录下,新建一个名为Beginning_C++的文件夹,并导航到该目录下。接下来为本章创建一个名为Chapter_01的文件夹。现在你可以切换到Visual Studio。如果该程序还未启动,则可以从“开始”菜单中启动。

在Visual Studio中,单击“文件”菜单,然后单击“新建”按钮,之后弹出一个新的对话框,在左边的树形视图中单击Visual C++项目。在该面板中间你可以看到C++ File (.cpp)Header File (.h)两个选项以及打开文件夹时的C++属性项,如图1-7所示。

图1-7

前两种文件类型主要用于C++项目;第三种类型将创建一个JSON文件辅助Visual Studio实现代码自动补全功能(帮助我们输入代码),本书将不会使用这个选项。

单击这些选项中的第一项,然后单击“Open”按钮。该操作将创建一个名为Source1.cpp的空白文件,为了将它以simple.cpp的形式另存到本章项目文件夹下,可以通过单击“File”按钮,然后选择“Save Source1.cpp As”项,导航到上述新建的项目文件目录下,在单击“Save”按钮之前,在文件名输入框中将之重命名为simple.cpp

现在我们可以在该空白文件中输入简单程序的代码,代码内容如下:

#include <iostream>

int main()
{
    std::cout << "Hello, world!n";
}

当完成上述代码的输入后,可以通过单击“File”菜单,单击其中的“Save simple.cpp”项保存该文件。接下来我们就可以编译代码了。

3.编译代码

转到命令行提示符下,然后输入命令cl /?。因为环境变量PATH配置引用了bin文件夹的路径,你将看到编译器的帮助页面。可以通过按下“回车”键对这些帮助信息进行滚动浏览,直到返回命令提示符。其中大多数选项的用途超出了本书的范围,但是我们将讨论表1-2中所列的编译器开关选项。

表1-2  

编译器开关

用  途

/c

仅编译,不链接

/D<symbol>

定义常量或宏<符号>

/EHsc

启用C++的异常处理机制,但声明不处理外部“C”函数(通常是操作系统的函数)引发的异常

/Fe:<file>

提供要链接的可执行文件的名称

/Fo:<file>

提供要编译的对象文件名称

/I <folder>

提供用于搜索引用文件的文件夹名称

/link<linker options>

将<链接器选项>传递给链接器,但必须位于源文件名和编译器开关之后

/Tp <file>

将<文件>作为C++文件进行编译,即使该文件不包含.cpp或者.cxx文件扩展名

/U<symbol>

删除先前定义的宏或常量

/Zi

启用调试信息

/Zs

仅限检查语法,不编译或者链接

对于某些选项需要注意,在开关和选项之间需要包含空格,有些选项则不能有空格,而对于其他选项,空格是可选的。一般来说,如果文件或者文件夹的名称中包含空格,那么最好使用双引号将它们引起来。在使用一个开关之前,我们最好查看相关的帮助文件,了解它们是如何处理空格的。

在命令行中,输入cl simple.cpp命令,你将发现编译器发出的警告信息C4530C4577。这是因为C++标准库会使用异常机制,但是用户没有为编译器声明应该为异常机制提供必需的代码。可以通过开关/EHsc解决这个问题。在命令行中,输入命令cl /EHsc simple.cpp。如果输入正确无误,则将看到如下结果:

C:\Beginning_C++\Chapter_01>cl /EHsc simple.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.25017 for x86
Copyright (C) Microsoft Corporation. All rights reserved

simple.cpp

Microsoft (R) Incremental Linker Version 14.10.25017.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:simple.exe

simple.obj

默认情况下,编译器会将源代码文件编译成一个对象文件,然后将该文件传递给链接器,并将之链接为一个与C++源文件同名的命令行可执行文件,不过其文件扩展名为.exe。上述信息行指出/out:simple.exe是由链接器生成的,/out是一个链接器选项。

列出文件夹中的内容,你将会发现3个文件:源码文件simple.cpp;编译器生成的对象文件simple.obj;可执行文件simple.exe,即链接器将对象文件和相应的运行时库链接之后生成的可执行文件。你可以通过在命令行中输入simple来运行这个可执行文件:

C:\Beginning_C++\Chapter_01>simple
Hello, World!

4.在命令行和可执行文件之间传递参数

如前所述,读者发现main函数会返回一个值,默认情况下该值是0。当应用程序执行完毕后,可以向命令行返回一个错误代码。这使得你可以在批处理文件和脚本中使用可执行程序,并且可以在脚本中使用上述返回值控制程序流。一般来说,当运行一个可执行程序时,可以在命令行上传递相关参数,这将对可执行程序的行为产生影响。

通过在命令行上输入simple命令来运行这个简单的应用程序。在Windows中,错误代码是通过伪环境变量ERRORLEVEL获取的,因此可以通过ECHO命令获得这个值:

C:\Beginning_C++\Chapter_01>simple
Hello, World!

C:\Beginning_C++\Chapter_01>ECHO %ERRORLEVEL%
0

为了演示上述值是通过该应用程序返回的,可以将main函数的返回值修改为一个大于0的值(本示例中是99,并且予以加粗表示):

int main()
{
    std::cout << "Hello, world!n";
    return 99;
}

编译上述代码并运行它,然后打印输出与前文类似的错误代码。你将看到现在输出的错误代码是99。

这是一种非常基础的交流机制:它只允许传递整数值,脚本调用代码时必须知道每个整数值代码的具体含义。

我们更有可能将参数传递给应用程序,这些参数将通过main函数的形式参数进行传递。将main函数替换成如下形式:

int main(int argc, char *argv[])
{
    std::cout << "there are " << argc << " parameters" <<
    std::endl;
    for (int i = 0; i < argc; ++i)
    {
        std::cout << argv[i] << std::endl;
    }
}

当我们编写可以从命令行接收参数值的main函数时,按照约定它会包含两个参数。

第一个参数通常称为argc。它是一个整数,并表明了传递给应用程序的参数格式。这个参数非常重要。因为我们将通过一个数组访问内存,该参数将对所访问的内存做一定限制。如果访问内存时超出了此限制,那么将会遇到麻烦。最好的情况是访问未初始化的内存,最糟糕的情况是出现访问冲突。非常重要的一点是,每当访问内存时,都要了解可以访问的内存数量,并确保在其限制范围之内。

第二个参数通常称为argv,它是一个指向内存中C字符串的指针数组。第4章将详细介绍指针数组,第9章将详细介绍字符串,因此我们在这里不对它们进行深入讨论。

方括号([])表示参数是一个数组,并且数组中每个成员的类型是char **表示数组的每个元素是指向内存的指针。一般来说,这将被解析为一个指向单个给定类型元素的指针,不过字符串比较特别:char *表示内存中的指针指向的是以NUL字符()结尾的0个或者多个字符。字符串的长度是根据字符数目到NUL字符的总数得出的。

上述代码中的第三行表示在控制台上打印输出传递给应用程序字符的长度。在这个示例中,我们将使用流std::endl替代转义换行符(n)来添加一个新行。有不少运算符可供选择,与之有关的详情将在第6章深入介绍。std::endl运算符将把新行添加到输出流中,然后对流中的内容进行刷新。

该行表示C++允许将输出运算符<<链接到一起并添加到流中,该行也向用户表明<<输出运算符被重载了,不同类型的参数对应的运算符版本也各不相同(有3种情况:一种是接收整数(用于argv参数),另一种是接收字符串参数,还有一种是接收运算符作为参数),不过这些运算符的语法调用几乎是一样的。

最后,用于打印输出argv数组中每个字符串的代码块如下:

for (int i = 0; i < argc; ++i)
{
    std::cout << argv[i] << std::endl;
}

for语句表示该代码块在变量i的值小于argc的值之前会一直被调用,每次循环迭代成功后,变量i的值自动加1(在它前面使用自增运算符)。数组中的元素是通过方括号进行访问的([])。传递的值是数组中的索引。

需要注意的是,变量i的起始值是0,因此访问第一个元素是通过argv[0]进行的,因为for循环完成后,变量i中包含的是argc的值,这意味着访问数组中最后一个元素是通过argv[argc-1]实现的。数组的一种典型用法是:第一个索引是0,如果数组中包含n个元素,那么最后一个元素的索引就是n-1

如前文所述,编译并运行这些代码,并且不提供任何参数:

C:\Beginning_C++\Chapter_01>simple
there are 1 parameters
simple

注意,即使你没有提供任何参数,程序本身也会认为你提供了一个参数,即可执行程序的名称。事实上,它不仅是程序名称,而且是命令行中调用可执行程序的命令。在这种情况下,输入simple命令(没有扩展名),会返回文件simple的值并将其作为参数打印输出到控制台上。再试一次,不过这次使用文件全名simple.exe。现在你将会发现第一个参数是simple.exe

尝试使用一些实际的参数调用该代码。在命令行上输入命令simple test parameters

C:\Beginning_C++\Chapter_01>simple test parameters
there are 3 parameters
simple
test parameters

这次程序执行结果表明存在3个参数,并且使用空格对它们进行了分隔。如果你希望在单个参数中使用空格,那么可以将整个字符串放到双引号中:

C:\Beginning_C++\Chapter_01>simple "test parameters"
there are 2 parameters
simple
test parameters

请记住,argv是一个字符串的指针数组,因此如果你希望在命令行中传递一个数字类型的参数,则必须通过argv对它的字符串进行类型转换。

C++编译器编译源文件需要经过几个步骤。顾名思义,编译器的预处理器位于这个过程的开始部分。预处理器会对头文件进行定位并将它们插入到源文件中。它还会替换宏和定义的常量。

1.定义常量

通过预处理器定义常量主要有两种方式:通过编译器开关和编写代码。为了了解它的运行机制,我们将修改main函数以便打印输出常量的值,其中比较重要的两行代码予以加粗显示:

#include <iostream>
#define NUMBER 4

int main()
{
    std::cout << NUMBER << std::endl;
}

#define开头的代码行是一条预处理器指令,它表示代码文本中任意标记为NUMBER的符号都应该被替换成4。它是一个文本搜索和替换,但是只会替换整个符号(因此如果文件中包含一个名为NUMBER99的符号,则其中的NUMBER部分将被替换)。预处理器完成它的工作之后,编译器将看到如下内容:

int main()
{
    std::cout << 4 << std::endl;
}

编译原始代码并运行它们,将发现该程序会把4打印输出到控制台。

预处理器的文本搜索和替换功能可能会导致一些奇怪的结果,比如修改main函数,在其中声明一个名为NUMBER的变量,如下列代码所示:

int main()
{
    int NUMBER = 99;
    std::cout << NUMBER << std::endl;
}

现在编译代码,你将发现编译器报告了一个错误:

C:\Beginning_C++\Chapter_01>cl /EHhc simple.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.25017 for x86
Copyright (C) Microsoft Corporation. All rights reserved.

simple.cpp
simple.cpp(7): error C2143: syntax error: missing ';' before 'constant' 
simple.cpp(7): error C2106: '=': left operand must be l-value

这表示第7行代码中存在一个错误,这是声明变量新增的代码行。不过,由于预处理器执行了搜索和替换工作,编译器看到的代码将如下列内容所示:

int 4 = 99;

这在C++程序中是错误的!

在所输入的代码中,很明显导致该问题的原因是你在相同文件中拥有一个该标识符的#define伪指令。在实际开发过程中,我们将引用若干头文件,这些文件可能会引用其自身,因此错误的#define伪指令可能会在多个文件中被重复定义。同样,常量标识符可能会与引用的头文件中的变量重名,并可能会被预处理器替换。

使用#define定义全局常量并不是一种好的解决方案,C++中有更好的方法,与之有关的详情将在第3章深入介绍。

如果你认为预处理器替换标识符过程中可能存在问题,那么可以通过检查经过预处理器处理后传递给编译器的源文件来确认自己的判断。为此,在编译时需要搭配开关/EP一起使用。这将中断实际的编译过程,并将预处理器的执行结果输出到命令行窗口中。需要注意的是,这将生成大量的文本,因此最好将输出结果另存为一个文件,然后使用Visual Studio编辑器查看该文件。

为预处理器提供所需的值的方式是通过编译器开关传递它们。编辑上述代码并将以#define开头的代码行删除。像往常一样对代码进行编译(cl /EHsc simple.cpp)并运行它,然后确保打印输出到控制台上的值是99,即分配给变量的值。现在再次通过下列命令对代码进行编译:

cl /EHsc simple.cpp /DNUMBER=4

注意,开关/D与标识符之间没有空格。这会告知预处理器使用4替换每个NUMBER符号,并且这会导致与前文所述相同的错误,这表明预处理器正尝试使用你提供的值替换相关符号。

Visual C++这类工具和nmake项目将提供一种机制通过C++编译器定义符号。开关/D只能用来定义一个符号,如果你希望定义其他符号,将需要提供与之相关的/D开关。

你现在应该理解为什么C++会拥有这样一个看上去只会引起歧义的古怪特性。一旦明白了预处理器的工作机制,那么符号定义功能将非常有用。

2.宏

宏是预处理器符号非常有用的特性之一。一个宏可以包含参数,并且预处理器将确保使用宏的参数搜索和替换宏中的符号。

编辑main函数如下列代码所示:

#include <iostream>

#define MESSAGE(c, v)
for(int i = 1; i < c; ++i) std::cout << v[i] << std::endl;

int main(int argc, char *argv[])
{
    MESSAGE(argc, argv);
    std::cout << "invoked with " << argv[0] << std::endl;
}

main函数调用了一个名为MESSAGE的宏,并将命令行参数传递给了它。该函数会将第一个命令行参数(调用命令)打印输出到控制台上。MESSAGE并不是一个函数,而是一个宏,这意味着预处理器将使用先前定义的两个参数文本替换出现的每个MESSAGE,使用传递给宏的第一个实际参数替换参数c,使用传递给宏的第二个实际参数替换参数v。预处理器处理完毕整个文件后,main函数的内容如下:

int main(int argc, char *argv[])
{
    for(int i = 1; i < argc; ++i)
        std::cout << argv[i] << std::endl;
    std::cout << "invoked with " << argv[0] << std::endl;
}

注意,在宏定义中,反斜杠(\)表示行连接符,因此我们可以定义包含多行的宏。通过一个或者多个参数编译和运行这些代码,然后确保MESSAGE能够打印输出命令行参数。

3.标识符

我们可以定义一个不包含值的标识符,并且预处理器可以被告知测试验证某个标识符是否被定义。最常见的应用场景是编译调试版本的不同代码,而不是发布版程序。

编辑上述代码并添加加粗显示的代码行:

#ifdef DEBUG
#define MESSAGE(c, v)
for(int i = 1; i < c; ++i) std::cout << v[i] << std::endl;
#else
#define MESSAGE
#endif

第一行代码告知预处理器去查找DEBUG标识符。如果该标识符已经定义(不管其值是什么),则第一个MESSAGE宏定义将被使用。如果该标识符未定义(一个预览版构建),则MESSAGE标识符将被定义,不过它不执行任何操作,本质上来说,就是将代码中出现的包含两个参数的MESSAGE删除。

编译上述代码并通过一个或者多个参数运行该程序,比如下列内容:

C:\Beginning_C++\Chapter_01>simple test parameters
invoked with simple

这表示代码已经在不包含DEBUG定义的情况下被编译,因此MESSAGE的定义将不会执行任何操作。现在再次编译代码,不过这次使用/DDEBUG开关来定义DEBUG标识符。再次运行该程序之后,用户将发现命令行参数被打印输出到控制台上:

C:\Beginning_C++\Chapter_01>simple test parameters
test parameters
invoked with simple

上述代码使用了宏,不过我们可以通过条件编译在任意C++代码中使用标识符。这种标识符的使用方式允许编写灵活的代码,并且可以通过在编译器命令行中定义一个标识符来选择将要被编译的代码。此外,编译器自身也将定义一些标识符,比如DATE将包含当前日期、TIME将包含当前时间、FILE将包含当前文件名。

 提示 

Microsoft和其他编译器厂商提供了一长串标识符供访问,建议在帮助手册中了解详情。你可能会发现以下几个非常有用:cplusplus将专门用于C++源代码(但是不适用于C文件)文件的定义,因此我们可以识别需要编译的C++代码。_DEBUG是用于设置调试构建过程的(注意它前面的下划线),_MSC_VER包含当前Visual C++编译器的版本信息,因此我们可以为多个版本的编译器使用相同的源。

4.pragma指令

与标识符和条件编译有关的是编译器指令#pragma once。pragma是专属于编译器的指令,不同编译器支持的pragma也不尽相同。Visual C++定义的#pragma once指令是为了解决多个头文件重复引用相同头文件的问题。该问题可能导致相同元素被重复定义一次以上,并且编译器会将之标记为错误。有两种方法可以执行此操作,并且<iostream>头文件下采用了这两种技术。你可以在Visual C++的include文件夹下找到该文件。在该文件顶部将看到如下代码:

// ostream standard header
#pragma once
#ifndef _IOSTREAM_
#define _IOSTREAM_

在该文件底部,将看到如下代码行:

#endif /* _IOSTREAM_ */

首先是条件编译。该头文件的名称首次被引用,标识符_IOSTREAM_还未被定义,所以该标识符会被定义,然后其余的文件将被引用直到#endif代码行。上述过程演示了条件编译时的最佳实践。对于每个#ifndef,都有一个#endif与之对应,并且它们之间包含数百行代码。当使用#ifdef或者#ifundef时,为相应的#else#endif提供注释说明信息是比较推荐的做法,这样做的目的是声明标识符引用的目标。

如果文件被再次引用,则标识符_IOSTREAM_将被定义,这样一来#ifndef#endif之间的代码将被忽略。不过,非常重要的一点是,即使已经定义该标识符,头文件仍然将被载入和处理,因为相关的操作指令是被包含在文件中的。

#pragma once标识符会对条件编译执行相同的操作,不过它解决了可能存在的标识符重复定义的问题。如果你将这行代码添加到了自己的头文件顶部,将指示预处理器载入和处理该文件一次。预处理器维护着一份已经处理过的文件列表,如果后续的某个头文件尝试载入一个已经处理过的文件,则该文件将不会被载入和处理。这种做法可以减少项目预处理过程所需的时间。

在关闭<iostream>文件之前,可以查看该文件的代码行数。对于版本是v6.50:0009<iostream>,它包含55行代码。这是一个小型文件,不过它引用的<istream>文件有(1157行),引用的<ostream>文件有(1036行),引用的<ios>文件有(374行),引用的 文件有(1630行)。预处理的结果可能意味着你的源代码文件中将引用数万行代码,即使程序只包含一行代码!

一个C++项目将生成一个可执行文件或者库,它们是由链接器根据对象文件构建的。可执行文件或者库依赖于这些对象文件。一个对象文件是由一个C++源代码文件(可能包含一个或者多个头文件)编译而来的。对象文件依赖于这些C++源代码文件和头文件。理解这些依赖关系非常重要,它可以帮助我们了解项目代码的编译顺序,并且允许我们通过只编译已更改的文件来加快项目构建的速度。

1.库

当你在自己的源代码文件中引用一个文件时,头文件中的代码将能够访问代码。我们引用的文件可能包含整个函数或者类的定义(与之有关的详情将在后续章节介绍),不过这将导致出现前面提及的问题:某个函数或者类被重复定义。相反,你可以声明一个类或者函数原型,这可以指示代码如何调用函数而不进行实际定义。显然,代码将在其他地方定义,这可能是在一个源文件或者库中,不过这对编译器来说很有利,因为它只看到了一个定义。

库就是已经定义的代码,它经过完全的调试和测试,因此将不需要访问源代码。C++标准库主要是通过头文件的形式共享的,这有助于调试项目代码,但是你必须抵制住任何临时编辑这些代码的诱惑。其他库将以已编译程序库的形式提供。

编译程序库一般有两种:静态库和动态链接库。如果使用的是静态库,那么编译器将从静态库中拷贝我们所需的代码,并将它们集成到可执行程序中。如果你使用的是动态链接(共享)库,那么链接器将在程序运行过程中(有可能是在可执行程序被加载后,或者可能被推迟到函数被调用时)添加一些信息,以便将共享库加载到内存中并访问其功能特性。

 提示 

Windows使用lib作为静态库的文件扩展名,用dll作为动态链接库的文件扩展名。GNU gcc使用a作为静态库的文件扩展名,使用so作为共享库的文件扩展名。

如果你所需的代码在某个静态库或者动态链接库中,编译器将需要精确地知道你调用函数的信息,以便确保函数调用时使用正确的参数个数和类型。这也是函数原型的主要用途:它在不提供实际函数体的情况下,为编译器提供了调用函数所需的信息,即函数定义。

本书将不会涉及如何编写程序库的细节,因为它是特定于编译器的,也不会详细介绍调用程序库代码,因为不同操作系统共享代码的方式也各不相同。一般来说,C++标准库将以标准头文件的形式引入项目中。C运行时库(将为C++标准库提供一些代码)将以静态链接库的形式引入,不过如果编译器提供了动态链接版本程序,那么我们可以通过编译器选项来使用它。

2.预编译头文件

当我们将一个文件引入到源代码文件中时,预处理器将引入该文件的内容(在执行完所有条件编译指令之后),并且以递归的方式添加所有该文件引用的任意文件。如前所述,最终的结果可能涉及数千行代码。在程序开发过程中,我们将经常编译项目代码,以便对代码进行测试。每次编译代码时,在头文件中定义的代码也会被编译,即使头文件中的代码没有发生任何变化。对于大型项目,这使得编译过程需要耗费很长时间才能完成。

为了解决这个问题,编译器通常会提供一个选项,对没有发生变更的头文件进行预编译。预编译头文件的创建和使用是特定于编译器的。比如GNU C++编译器gcc,如果编译的某个头文件是一个C++源代码文件(使用/x开关),该编译器会创建一个扩展名为gch的文件。当gcc编译源代码文件需要用到该头文件时,它会去搜索该gch文件。如果它找到了该预编译头文件,将使用它;否则,它会使用对应的头文件。

在Visual C++中该过程稍微有点复杂,因为必须在编译器编译源代码文件时,告知编译器去查找某个预编译头文件。Visual C++项目的约定是提供一个名为stdafx.cpp的源文件,其中包含一行引用stdafx.h文件的代码。你可以在stdafx.h文件中引用所有性能稳定的头文件。然后可以通过编译stdafx.cpp文件来创建一个预编译头文件,同时使用/Yc编译器选项声明所有性能稳定并且需要被编译的头文件都包含在了stdafx.h文件中。这将创建一个pch文件(一般来说,Visual C++将在项目名称之后附加相关的名称),其中包含经过编译的所有stdafx.h头文件中引用的代码。其他源代码文件中必须将stdafx.h头文件作为第一个引用的文件进行引用,不过它们还可以引用其他文件。当编译源代码文件时,可以使用/Yu开关声明性能稳定的头文件(staafx.h),编译器将使用预编译pch文件替代相关的头文件。

当在浏览大型项目文件时,经常会发现其中采用了不少预编译头文件。如你所见,它会改变项目的文件结构。本章后续的示例将向读者演示如何创建和使用预编译头文件。

3.项目结构

将项目代码进行模块化组织非常重要,这使得我们可以高效地对项目代码进行维护。第7章将介绍面向对象编程技术,它是一种组织和复用代码的方式。不过,即使你正在编写类似C语言的过程式代码(也就是说,代码是以线性的方式进行函数调用的),仍然可以将它们组织成模块化的形式,继而从中获益。比如,代码中的某些函数是与操作字符串有关的,其他函数是与文件访问有关的,那么我们可以将字符串函数的定义放在某个单独的源码文件中,即string.cpp;与文件函数定义有关的内容放在其他文件中,即file.cpp。这样一来,就可以方便项目文件中的其他模块调用这些文件,你必须在某个头文件中声明这些函数的原型,并在调用这些函数的模块中引用上述头文件。

在头文件和源代码文件之间包含函数的定义在语言层面并没有绝对的规则。你可能在string.cpp的函数中引用了一个名为string.h的头文件,或者在file.cpp的函数中引用了一个名为file.h的头文件。又或者我们可能只有一个utilities.h文件,其中包含上述两个文件中所有函数的声明。我们必须遵守的唯一规则是,在编译时,编译器必须能够通过某个头文件或者函数定义本身,在当前的源代码文件中访问函数的定义。

编译器在源代码中将不会向前查找,因此如果函数A准备调用函数B,那么在同一源代码文件中函数B必须在函数A调用它之前就已经被定义,否则必须存在一个对应的原型声明。这导致了一个非常典型的约定,即每个包含头文件的源代码文件中包含函数的原型声明,并且该源文件引用上述头文件。当编写类时,这一约定变得更加重要。

4.管理依赖项

当通过构建工具构建项目时,将先检查构建的输出是否存在,如果不存在,则执行适当的构建操作。构建步骤的输出的通用术语一般称为目标,构建步骤的输入(比如源代码文件)是目标的依赖项。每个目标的依赖项是用于构建它们的文件。依赖项也可能自身就是某个构建动作的目标,并且拥有它们自己的依赖项。

比如,图1-8展示了一个项目的依赖关系。

在这个项目中,有main.cppfile1.cppfile2.cpp三个源代码文件。它们引用了相同的头文件utils.h,它可以被预编译(因为有第四个源代码文件utils.cpp,它只引用了utils.h头文件)。所有源代码文件都依赖于utils.pch文件,而utils.pch文件又依赖于utils.h文件。源代码文件main.cpp包含main函数,并调用了存在于file1.cppfile2.cpp两个源代码文件的函数,而且是通过头文件file1.hfile2.h访问这些函数的。

图1-8 项目依赖项关系

在第一次编译时,构建工具将发现可执行程序依赖于4个对象文件,因此它将根据查找规则来构建每个对象文件。存在3个C++源代码文件的情况下,这意味着需要编译源代码文件,不过因为utils.obj是用于支持预编译头文件的,因此构建规则将与其他文件不同。当构建工具生成这些对象文件时,它将使用任意库代码将它们链接到一起(这里未显示)。

随后,如果你修改了file2.cpp文件,然后构建该项目,构建工具将发现只有file2.cpp文件被修改,并且因为只有file2.obj文件依赖于file2.cpp文件,需要构建工具做的所有工作就是编译file2.cpp文件,然后使用现存的对象文件链接新的file2.obj文件,以便创建可执行程序。如果你修改了头文件file2.h,构建工具将发现有两个文件依赖于该头文件,即file2.cppmain.cpp,因此构建工具将编译这两个源代码文件,然后使用现有的对象文件链接新生成的对象文件file2.objmain.obj,以便生成可执行文件。但是,如果预编译的头文件util.h发生了变化,这意味着所有源代码文件都必须重新编译。

对于小型项目来说,依赖关系的管理还比较容易。如你所见,对于单个源文件项目,我们甚至不需要为调用链接器操心,因为编译器会自动执行。但随着C++项目规模不断增大,依赖项的管理会变得越来越复杂,这时诸如Visual C++这样的开发环境就会变得至关重要。

5.makefile文件

如果你正在维护一个C++项目,则很有可能会遇到makefile文件。它是一个文本文件,其中包含用于构建目标文件的目标、依赖项以及项目构建规则。makefile是通过make命令进行调用的,其中Windows平台的工具是nmake、类Unix平台的工具是make。一个makefile文件就是一系列与下列内容类似的规则:

targets : dependents
 commands

目标是一个文件还是多个文件取决于其依赖项(也可能是多个文件),因此如果有一个或者多个依赖项比目标文件中的版本更新(并且目标自上次构建之后已经发生变更),那么将需要再次构建目标文件,这些操作是通过运行相关命令完成的。可能有多个命令,每个命令都是以制表符为前缀处于单个行上。一个目标可能不包含任何依赖项,在这种情况下,这些命令仍然将被调用。

比如,使用上述示例时,可执行文件test.exe的构建规则如下:

test.exe : main.obj file1.obj file2.obj utils.obj
    link /out:test.exe main.obj file1.obj file2.obj utils.obj

因为对象文件main.obj依赖于源代码文件main.cpp、头文件File1.hFile2.h、预编译头文件utils.pch,所以该文件的构建规则如下:

main.obj : main.cpp file1.h file2.h utils.pch
    cl /c /Ehsc main.cpp /Yuutils.h

编译器被调用时使用了/c开关选项,这表明相关的代码会被编译成对象文件,但是编译器将不会调用链接器。/Yu开关选项和头文件utils.h搭配使用,是告知编译器使用预编译头文件utils.pch。其他两个源代码文件的构建规则与此类似。

生成预编译头文件的构建规则如下:

utils.pch : utils.cpp utils.h
    cl /c /EHsc utils.cpp /Ycutils.h

/Yc开关是告知编译器使用头文件utils.h创建一个预编译头文件。

实际开发中makefile通常比上述内容更复杂。它们将包含宏,即组目标、依赖项和命令行开关。它们还会包含目标类型的一般规则,而不是这里描述的具体规则,而且它们还将包含条件测试的内容。如果需要维护或者编写makefile,那么你应该详细了解构建工具帮助手册中的所有选项。

这个项目将向读者演示在本章学到的C++语言和项目管理的特性。该项目将使用若干源代码文件,以便读者可以了解到依赖项的作用,以及构建工具是如何管理源代码变更的。这个项目非常简单,它将要求你输入名字,然后在命令行上打印输出名字以及相关的时间和日期。

该项目包含3个函数:main函数,它会调用其他另外两个函数print_nameprint_time。它们分别位于3个独立的源代码文件中,因为main函数将调用位于其他源代码文件中的两个函数,这意味着main函数源代码文件中必须包含这些函数的原型声明。在本示例中,这意味着引用每个文件的头文件。该项目还会使用一个预编译头文件,这表示存在一个源代码文件和一个头文件。总之,项目中将包含3个头文件和4个源代码文件。

该代码将使用C++标准库的流对象进行信息的输入和输出,因此我们将使用<iostream>头文件。代码中将使用C++的字符串类型处理信息输入,因此还将使用<string>头文件。最后,它会访问C运行时库的timedate函数,因此代码中还将用到<ctime>头文件。这些都是标准的头文件,我们在进行程序开发时无需对它们进行修改,因此它们是预编译的理想目标。

在Visual Studio中创建一个C++头文件,然后在其中添加如下代码:

#include <iostream>
#include <string>
#include <ctime>

将它另存为utils.h

新建一个C++源代码文件,添加一行代码来引用刚才创建的头文件:

#include "utils.h"

将其另存为utils.cpp,我们将需要为这个项目创建一个makefile,因此在新建文件对话框中,选择文本文件作为文件类型。添加以下规则来构建预编译头文件:

utils.pch utils.obj :: utils.cpp utils.h
    cl /EHsc /c utils.cpp /Ycutils.h

将它另存为文件名为“makefile.”的文件,注意要在文件末尾加一个句点(.)符号。因为我们是以文本文件的形式创建该文件的,Visual Studio将自动为它添加一个txt文件后缀,但是因为我们不希望使用该文件后缀,所以需要添加一个句点以表明该文件无后缀名。其中第一行表示utils.pchutils.obj依赖于特定的源代码文件和头文件。第二行(以一个制表符作为前缀)告知编译器去编译C++文件,但是不调用链接器,同时还告知编译器将预编译代码保存到utils.h文件中。该命令将创建utils.pchutils.obj文件,即声明的两个目标。

make实用程序发现有两个目标时,默认的动作(当目标和依赖项之间使用单个冒号时)是为每个目标都调用一次命令(你可以使用宏来决定哪个目标需要被构建)。这意味着相同的编译器命令将执行两次。这是我们不希望看到的,因为两个目标是通过调用一次命令构建的。双冒号::是一个变通方案,它告知nmake不要采用为每个目标调用命令的行为。最终的结果就是,当make程序调用命令一次,并生成了utils.pch文件,然后它会尝试生成utils.obj文件,不过它发现该文件已经被生成了,所以不需要再次调用命令。

现在测试输出结果。在命令行中,导航到项目文件夹下,输入nmake命令。

如果没有提供makefile的名称,程序维护工具将自动使用名为makefile的文件(如果你希望使用一个其他名字的makefile,可以使用/f 开关并指定文件名):

C:\Beginning_C++\Chapter_01\Code>nmake
Microsoft (R) Program Maintenance Utility Version 14.00.24210.0
Copyright (C) Microsoft Corporation. All rights reserved.

cl /EHsc /c utils.cpp /Ycutils.h
Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24210 for x86
Copyright (C) Microsoft Corporation. All rights reserved.

utils.cpp

查看文件目录列表,以确认utils.pchutils.obj文件是否已经被创建。

现在创建一个C++源代码文件并添加如下代码:

#include "utils.h"
#include "name.h"
#include "time.h"

void main()
{
    print_name();
    print_time();
}

将此文件另存为main.cpp

其中引用第一个文件是标准库头文件的预编译头文件。其他两个文件提供了被main函数调用的另外两个函数的函数原型声明。

现在用户需要为main文件添加一条规则到makefile文件。将下列加粗显示的代码行添加到文件顶部:

main.obj : main.cpp name.h time.h utils.pch
 cl /EHsc /c main.cpp /Yuutils.h

utils.pch utils.obj :: utils.cpp utils.h
    cl /EHsc /c utils.cpp /Ycutils.h

新增的一行表示目标文件main.obj依赖于两个头文件:一个源代码文件main.cpp和一个预编译头文件utils.pch。目前main.cpp文件将无法编译,因为相关的头文件还不存在。所以我们可以测试makefile文件,创建两个C++头文件。在第一个头文件中,添加函数原型声明:

void print_name();

将该文件另存为name.h。在第二个头文件中,添加函数原型声明,例如:

void print_time();

将该文件另存为time.h

现在我们可以运行make程序,它将只编译main.cpp文件。测试它的输出结果:通过在命令行中输入del main.obj utils.obj utils.pch 命令来删除所有目标文件,然后再次运行make程序。这次,用户将发现make程序首先编译了utils.cpp文件,然后编译了main.cpp文件。这样的编译顺序是因为首个目标是main.obj文件,但是因为它依赖于utils.pch文件,所以make程序在返回创建main.obj文件规则之前,移动到了下一个规则并采用此规则生成了预编译头文件。

需要注意的是,我们还没有定义print_nameprint_time函数,不过编译器并不会对此有异议。因此编译器只创建对象文件,链接器负责链接函数。头文件中的函数原型声明满足了编译器的要求,该函数将在其他对象文件中被定义。

目前为止,我们已经了解了如何通过cout对象将数据输出到控制台。标准库还提供了一个cin流对象,允许读取命令行中输入的值。

创建一个C++源代码文件,并添加如下代码:

#include "utils.h"
#include "name.h"

void print_name()
{
    std::cout << "Your first name? ";
    std::string name;
    std::cin >> name;
    std::cout << name;
}

将该文件另存为name.cpp。

其中首先引用的文件是预编译头文件,这将引用两个标准库头文件,即<iostream><string>,因此我们可以在这些文件中使用类型声明。

函数中第一行的含义是在控制台上打印输出字符串“Your first name? ”。

注意,在该问题末尾有一个空格,因此光标将保持在同一行,等待输入信息。

接下来的一行声明了一个C++字符串对象变量。字符串可以包含0个或者多个字符,并且每个字符都会占用内存。字符串类会处理字符串相关的分配和释放内存的所有工作,该类将在第9章详细介绍。cin重载了运算符>>以便获取控制台的输入信息。当用户按下“回车”键之后,运算符>>将把输入内容传递给变量name(将空格字符视为分隔符)。

该函数会将变量name中的内容打印输出到控制台,并且不带换行符。

现在为该源代码文件添加一条规则到makefile中,即添加如下代码到该文件的顶部:

name.obj : name.cpp name.h utils.pch
    cl /EHsc /c name.cpp /Yuutils.h

保存该文件,然后运行make工具,以确认它是否生成了name.obj目标文件。

最终的源代码文件将会获取时间,并将它打印输出到控制台。创建一个C++源代码文件并添加如下代码行:

#include "utils.h"
#include "time.h"

void print_time()
{
    std::time_t now = std::time(nullptr);
    std::cout << ", the time and date are "
              << std::ctime(&now) << std::endl;
}

std::timestd::gmtime这两个函数是C函数,并且std::time_t是一个C类型,所有这些都是通过C++标准库获得的。std::time函数获取的时间是1970年1月1日午夜以来的秒数。该函数会返回一个std::time_t类型的值,即一个64位整数。如果用户通过指针传递变量在存储中的存储位置,则该函数可以将上述整数值拷贝到另外一个变量中。

在这个示例中,我们不需要这个工具,因此我们传递一个C++的nullptr给该函数,以声明不需要执行拷贝操作。

接下来,我们需要将秒数转换成包含时间和日期的字符格式,以方便用户理解。这也是std::ctime函数的主要用途,它会接收一个指向保存秒数变量的指针作为参数。变量now保存了秒数,运算符&用于获取变量的内存地址。第4章将详细介绍变量和指针的细节。std::ctime函数会返回一个字符串,不过我们还没有为该字符分配任何内存,也不应该尝试为该字符串分配内存。std::ctime函数创建了一个静态分配内存缓冲区,它将被运行在当前执行线程的所有代码共享使用。每次在相同执行线程上调用std::ctime函数时,使用的内存地址是一样的。不过内存中的内容可能会发生变化。

此函数说明检查帮助手册,查看谁负责分配和释放内存是非常重要的。第4章将详细介绍分配内存的细节。

std::ctime返回的字符被打印输出到控制台,并且是通过调用若干次运算符<<对输出结果进行格式化的。

现在给makefile添加一条构建规则,即添加如下规则到该文件顶部:

time.obj : time.cpp time.h utils.pch
    cl /EHsc /c time.cpp /Yuutils.h

保存该文件并运行make工具,然后确认该构建过程是否生成了time.obj目标文件。

现在我们已经拥有了项目所需的所有对象文件,因此下一个任务是将它们链接到一起。为此,将下列代码添加到makefile文件顶部:

time_test.exe : main.obj name.obj time.obj utils.obj
    link /out:$@ $**

这里的目标是可执行文件,并且依赖项是4个对象文件。命令行中为了构建可执行文件会调用链接器并使用特殊的语法。标识符$@会被make工具解析为使用目标,所以/out开关实际的内容是/out:time_test.out。标识符$**会被make工具解析为使用所有依赖项,因此所有依赖项都将被链接。

保存该文件并运行make程序。用户将发现只有链接器被调用,并且它将链接所有对象文件继而创建可执行文件。

最后,添加一条规则来清理项目。提供一种机制移除编译过程中生成的文件,只保留源代码文件,从而保持项目结构的整洁是一个非常好的习惯。在链接对象文件的代码行之后,添加如下代码:

time_test.exe : main.obj name.obj time.obj utils.obj
    link /out:$@ $**
clean : @echo Cleaning the project...
 del main.obj name.obj time.obj utils.obj utils.pch
 del time_test.exe

clean任务是一个伪目标,实际上并没有生成文件,因此也不存在依赖项。这说明了make程序的一个特性,如果调用nmake工具,并指定目标名称,那么该程序将只生成该目标。如果没有声明目标,那么该程序将生成makefile提及的第一个目标,在这种情况下是time_test.exe

clean伪目标包含3个命令:第一个命令会将“Cleaning the project...”打印输出到控制台,标识符@会告知命令行不要将该命令打印输出到控制台;第二个和第三个命令会调用命令行工具del删除文件。现在可以在命令行上输入nmake clean命令清理项目,然后确认项目目录下是否只包含头文件、源代码文件和makefile文件。

再次运行make程序,以便构建可执行文件。在命令行中,可以通过输入time_test命令运行示例程序。你将被要求输入姓名;执行此操作,然后按“回车”键,此时将发现自己的名字、当前时间和日期被打印输出到控制台上:

C:\Beginning_C++\Chapter_01>time_test
Your first name? Richard
Richard, the time and date are Tue Sep 6 19:32:23 2016

现在读者对基本的项目结构有所了解,通过makefile,你可以对文件进行修改,并确保重新构建项目时,只对发生变更的文件进行编译。为了说明这一点,将修改name.cpp中的print_name函数,以更客气的方式询问你的姓名。修改函数体中第一行代码,比如下列代码中加粗显示的代码行:

void print_name()
{
    std::cout << "Please type your first name and press [Enter] ";
    std::string name;

保存该文件,然后运行make工具。这一次只有源代码文件main.cpp被编译,生成的name.obj文件会与已有的对象文件链接到一起。

现在修改头文件name.h,并在其中添加一个注释信息:

// 更客气的版本
void print_name();

构建该项目。读者发现了什么?这一次,有两个源代码文件被编译,即name.cppmain.cpp。并且它们与已有的对象文件链接到一起,从而创建了可执行文件。为了细究这两个文件被编译的原因,可以查看makefile中的依赖项规则。唯一发生变更的文件是name.h,并且该文件的名字出现在了ame.objmain.obj依赖项列表上,因此这两个文件被重新构建。由于这两个文件出现在了time_test.exe的依赖项列表上,所以该可执行文件也将被重新构建。

本章是对C++进行温和但彻底的介绍。读者学习了选择使用该语言的原因,以及如何从某个厂商那里获取编译器并对其进行安装。同时学习了C++项目的组织结构、源代码文件和头文件,以及如何通过程序库进行代码共享。然后学习了如何使用makefile维护项目,并通过一个简单示例获得了编辑和编译代码的实践经验。

读者现在已经有了编译器、编辑器和项目管理工具,那就已经准备好进一步了解C++的更多详细信息。从下一章开始,本书将向读者介绍C++语句和应用程序中的执行流控制。


相关图书

代码审计——C/C++实践
代码审计——C/C++实践
CMake构建实战:项目开发卷
CMake构建实战:项目开发卷
C++ Templates(第2版)中文版
C++ Templates(第2版)中文版
C/C++代码调试的艺术(第2版)
C/C++代码调试的艺术(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
Qt 6 C++开发指南
Qt 6 C++开发指南

相关文章

相关课程