分类目录归档:Think in C++笔记与心得

第三章:C++中的C

在使用C++编写程序的时候,我们会发现其中很多地方所使用的语句与C语言十分相似,其实从其命名可以看出来,C++与C语言有着不一般的关系。C++可以认为是为了使C语言更容易被开发,具有更强大的工程能力而被制造出来的。其中的基础语句以及函数的调用等等,都与标准C非常的相似,但是也有很多不同的地方,这些细节也都是C++为了解决一些问题而改进的地方,正因为”++”在C++里边有着自增的含义,所以C++也可以理解为在C的基础上更上一层楼。其实在使用C++编译器时,几乎可以将C语言代码原封不动的放置进来编译,不过C++也加入了很多独有的特性,同时也改进了很多C语言当中的问题,所以这里仅着重介绍一些两者之间的不同以及编写程序的时候值得注意的地方。

首先关于函数的创建,在C和C++当中,有一个特征叫函数原型,或者叫函数接口,它的意思是对函数返回值类型和参数列表的一个声明。而编译器则可以利用这个原型去检查函数的调用及返回并及时发现错误。在声明函数原型的时候,可以只依次写出函数参数列表的参数类型,而不必使用标识符,当然使用标识符可以使程序员获得更多信息有助于编写程序,但是编译器并不会检查函数声明和定义时的标识符是否匹配。而在函数定义时,标准C认为所有参数都必须具有标识符才可以被调用,所以在标准C当中定义函数时必须给所有变量指定标识符,而C++认为在函数开发过程中可能需要具有一些保留位来提高兼容性,所以其参数列表允许不使用标识符,仅做占位用途,当然该位置输入的参数也不能被直接调用,不过,删去不使用的标识符可以消除编译器的警告。在不清楚具体需要输入几个参数的时候,C语言提供了一种解决方法,即在参量表中输入省略号(…)或者直接将参量表留空,这两者在C中表示的意思都是可变参数参量表,即编译器不会对输入的参数类型进行检查。但是在C++当中,由于有函数重载这样的方法,可以尽量避免使用可变参量表,因为可变的参量表总是会导致很多bug。在C和C++当中,声明一个函数都需要声明其返回值类型,如果不进行声明编译器将会按照int型进行检查。

与C语言非常相似的是,在C++当中的几乎所有执行控制语句都与C当中的一模一样,比如if-else语句、switch-case语句、while语句、do-while语句、for语句甚至goto语句,说到goto语句,这是一个十分不推荐使用的语句,因为goto语句在C++里边可以引起严重的混乱。由于C++具有实时定义的特点,所以在C++当中处处都有着严格的作用域控制,即一个变量在声明之后,只会在离它最近的包裹着它的花括号范围内起作用,脱出这个范围之后,这个变量即会失去作用,也就是说它被释放掉了。但是如果使用goto跨越了作用域的边界,其并不会触发变量的释放,因此就会引发一系列错误。

再来说说实时定义,这是较C语言方便太多了的一个特性,它允许程序员在代码中任何地方声明定义变量,不必像C语言那样必须将变量在作用域的开始位置定义。一方面在编写代码的过程中不停的来回插入所需变量十分的麻烦,另一方面在读者阅读代码的时候由于变量的定义与使用地方距离太远,不容易对应观察其含义。所以实时定义是一个非常重要且好用的特性。

在变量方面C++与C语言并没有什么显著的区别,不过在常量方面,C++与C具有很大差距,在C语言当中,建立一个常量有时只能使用C语言的预处理器特性,使用#define来定义一个宏。而在C++当中则还可以选择使用const关键字。在这个方面C++要显得人性化很多,因为它的编译器会对const类型的数据进行很多的优化,比如如果程序所有的地方都只是对const的值进行使用,则编译器会将const进行变量折叠,即不给它进行内存分配,而是直接写入到程序的符号表里边去。而如果有对其地址的请求的话,编译器则会将其放在内存中,并且保证其不被改变,任何改变行为都会引发编译错误。还有一点就是编译器会对const类型数据进行类型检查,保证const在作为参数被传递的时候的安全性。而#define则只是简单的文字替换,容易引发很严重的问题。后来在C语言当中也引入了const的用法,不过不同的是,在C语言当中,const只能表示一段不能被改变的内存,由编译器来保证其不被改变。

C语言与C++的运算符在基础类型操作中表现的一模一样,其也是由右至左进行等式的赋值等等,其中几乎所有的逻辑运算符、位运算符、关系运算符以及特殊的运算符在基础应用情况下均是相同的。不过不同的是,在C++当中运算符似乎不仅仅再是运算符那么简单。在C++当中,运算符是可以被重载的,比如在使用C++的标准输出cout的时候,用法是cout << “str”;其中”<<“即是移位操作符,不过在此处它并不是移位符的意思,而是一种函数调用,对于上面那个表达式来说,cout和”str”分别为函数的两个参数,而表达式的值则是函数的返回值。而重载的意思就是在C++当中,可以用同一个名字来作为多个函数的名字,只要他们的参量表不完全相同,编译器就可以对函数的调用进行区分。具体的细节在之后的内容会专门讲到。而运算符也可以作为一类特殊的函数。

当然在进行运算编写的过程中,我们经常由于各种原因需要对不同类型的变量进行转换,在C语言当中,一些相对比较常见而且一般不会导致数据丢失等现象的转换会由编译器隐式完成,不过在一些较为容易出现问题或者转换目的不明确的时候,编译器会提示错误,并且需要程序员专门输入转换方式进行显式转换即用括号在变量名前冠上需要转换到的类型。而在C++当中,虽然它支持相同的转换方式,不过它也给出了一种显式转换的方法来替替代旧的风格,即static_cast, const_cast, reinterpret_cast, dynamic_cast这几种转换方式分别控制了几种不同的转换过程,static_cast是静态转换,就是进行一些定义明确的但是可能具有危险的,或者甚至编译器都会进行自动转换的类型转换。而const_cast则是将const类型的常量转换为非const类型的变量,或者将volatile类型的变量转换成非volatile类型的变量。(这里volatile变量指的是“不知何时会改变的变量”,它在内存当中可能会被除了本段代码以外的其它东西所改变,所以编译器会对这个变量不进行任何优化,比如将其加载到寄存器中进行连续赋值操作等。)reinterpret_cast指的是按照位拷贝的方式进行转换,一般情况下这种转换非常少用,除非在进行一些特殊的操作或算法时,因为按位拷贝转换成其他变量之后,大多数情况下需要再转换回来才能正常使用该变量,当然如果进行一些位操作的话倒没有什么太大的影响。最后dynamic_cast则是用于对类的派生与其基类之间的转换,在多态方面有着重要的应用,这些概念在书中的15章有详细的阐述。值得注意的一点是,在编写程序的时候尽量还是应该少使用类型转换,它们极易引发程序的漏洞,而C++将类型转换的方法设计的尤为复杂,也是在时时刻刻提醒着程序员,尽量少使用类型转换,而且一旦出现了问题,只需要再程序中检索cast关键字便可以迅速排查程序中出现的类型转换问题。

最后,C++的结构体联合体以及枚举等特殊用法也和C语言几乎一模一样,不过有一点不同的是,在C++当中结构体中的内容不一定必须是数据的集合,也可以将函数添加进去,而类(class)的概念也是从这个基础之上诞生的,将数据和函数封装成为一个集合,并且由内部的函数完成一些数据的操作,最后达到一种黑箱的效果,这也是所谓面向对象的一个最直观形象的一个解释。

总而言之,C++其中几乎包含了C语言的所有用法,并且对其中很多不甚妥当的地方进行了改进,使它在代码性能丝毫不逊色C语言的情况之下,更加的贴近程序员的思路,并且加速了开发的流程,而且使得可以由更多的人进行同一个项目的开发不会变得混乱不堪。这些都是C++的优点,也是它流行起来的原因之一。其相较C语言更上一层楼可谓当之无愧。

第二章:对象的创建与使用 (2) (利用C++标准库创建对象)

在我们进行编程的时候,并不需要每次都从零开始构建一个程序,往往我们可以使用一些由其他人或者先前的工程师进行精心测试过得代码来快速构建我们自己的程序,这样我们可以节省很多很多的时间和精力,同样在大多数情况下其性能也会出色很多。而在C和C++当中通过调用库函数来实现这一点。所谓库即是由别的公司或者个人将它们编写测试好的代码进行编译之后得到的一个或多个文件,这些文件当中含有的函数可以由连接器连接到用户所编写的程序当中去。而由于库中往往含有多个函数和变量,用户去一一声明自己所使用到的函数将会十分麻烦,然而C和C++当中的“头文件”很好的解决了这个问题,即函数库的提供者同时提供一份头问件,其中包含用户可以使用的所有的变量及函数的声明,如此一来用户只需要使用#include预处理器命令将对应文件插入到用户所需的地方即可,从而免去了用户繁杂的声明工作。

对于#include预处理器命令,有些值得注意的地方:首先双引号和尖括号包含文件名有着不同的意义。尖括号表示预处理器以特定的方式来寻找文件,一般是环境目录中或者编译器命令行中指定的某种路径,具体寻找路径跟随机器、系统、编译器不同而不同,而双引号则表示从当前目录开始寻找,如果没有找到则再按照尖括号的形式来寻找。

#include <header.h>

#include “header.h”

关于标准库中头文件的引用,在早期不同的编译器厂商选用了不同的扩展名,而后为了增加源代码的可移植性,则使用了一种标准引用,去除了扩展名,比如

#include <iostream>

而从C语言继承下来的库则可以去除文件后缀后在开头加入字母”c”。即

#include <cstdio>

#include <cstdlib>

当然传统的后缀包含法依然可用。只不过两种引用方式稍有不同,对于标准C++库来说

#include <iostream.h>

相当于

#include <iostream>

using namespace std;

如果使用新式的引用方式,则必须显式声明使用名字空间std(“standard”之意)名字空间为C++为了解决C语言当中程序规模庞大后的函数以及变量命名困难问题,即将函数包含进名字空间,使用某个名字空间中的函数时需要在函数或变量名前边加上作用域解析符(“::”双冒号),比如:

std::cout << “hello” << std::endl;

当然也可以使用using namespace std;使得以下的语句等同于上边这条语句。

cout << “hello” << endl;

当然如果只是想单独不加作用域解析符使用其中一个函数或变量,则可以:

using std::cout;

cout << “hello” << std::endl;

当然在使用名字空间的时候尽量在源代码文件当中使用,不要将其包含在头文件当中,因为在头文件当中包含using的话会导致所有引用这个头文件的源代码都出现混乱。很可能造成编译失败或者难以发现的函数及变量调用错误。

对于字符串输出这里,C的预处理器对长字符串做出了一些优化,即它可以将相邻的几个字符串拼接成为一个长字符串,于是在输出长文本时我们不必将其完全写在一行当中,比如:

std::cout << “Hello World “

“I feel good \n”;

以下为C++标准库当中一些头文件的简单介绍:

Reference-C

图片来源:http://www.cplusplus.com/reference/

其中最常用的莫过于标准容器库,所谓容器,其介绍如下:

A container is a holder object that stores a collection of other objects (its elements). They are implemented as class templates, which allows a great flexibility in the types supported as elements.

The container manages the storage space for its elements and provides member functions to access them, either directly or through iterators (reference objects with similar properties to pointers).

参考来源:http://www.cplusplus.com/reference/stl/

这段话的含义是:容器是储存其他对象(它的元素)的东西,他们使用具有对类型兼容性很高的模板类来实现。容器管理元素存储所需空间并且提供操作访问它们的函数,当然也可以通过直接访问和迭代器访问(一个和指针类似的指向性类)

根据我的理解,容器即是一个可以容纳几乎任何类型数据的管理器,它将我们平时使用的存储模型(向量存储,链表存储,图存储,堆栈,队列等)进行了模板化处理,并且提供了标准的操作函数,还有标准的访问工具(迭代器)这样让我们可以在开发程序的时候不用太在意如何去实现这样一个模型,去考虑更多更高层面的问题,而且标准库中提供的实现会更加严谨全面和高性能。而且所有的容器都可以自动的管理分配存储空间,比如向量容器,与我们使用的数组十分相似,但是其可以在数组大小将尽的时候自动扩展数组上限以容纳更多元素。

总之,学会基本语法以及如何使用标准函数库之后,就可以做出属于自己的第一份C++代码了,书中有详细的模板使用方法,并给出了很多样例代码,(主要以vector即向量模板为例)可以参照它做出自己的第一个C++程序,万事开头难,至此便启程C++之旅。

第二章:对象的创建与使用 (1) (基础知识与基本语法)

可以说,从这里开始算是C++的起步了,在接触对象之前,需要了解如何在一段代码当中创建以及使用一个对象。而在这之前,也需要一些基础的知识来作为铺垫。

首先,无论是什么语言,它们最终都会通过一系列的变化,从一种我们人类易于理解的形式,变化成计算机可以执行的形式。而中间这个转化的过程就是翻译,执行翻译操作的工具叫做翻译器。翻译器分为两种,一种是解释器,一种是编译器。解释器通过读取源代码并将其转化成可执行的机器指令,一般情况下,解释器会将自身先加载到内存,然后通过读取一行或者一小段源代码,然后将其在内存中解释成为机器指令,再对其进行执行,然后再读取下一行或者一小段代码。这样会使代码的执行效率非常低下,但是从写代码到执行代码中间并不需要太多时间,而且一旦发生了错误,解释器便可以很快速的指出错误所在。所以解释器的优点在于“较好的交互性和适用于快速程序开发”。当然也有一些解释语言为了提高自身效率,会先将整个程序转化成某种中间语言,然后用更为快速的解释器来执行它。而且解释器还有一个很明显的优点就是可以一定程度上直接将源代码复制到不同的平台使用,甚至可以不用经过太多的处理,基于不同平台的解释器会将代码解释为本平台可以运行的指令并运行它。解释器的工作模式可以总结为边翻译边运行。而编译器则不同,编译器会将源代码直接编译为汇编语言或者机器指令,这个过程比较复杂,一般分多步来执行,关于这方面的知识可以参见编译原理方面的书籍。至少可以看出,这样一来程序节省了运行时的翻译所需时间,大大的提高了程序的运行效率。其工作模式可以总结为先翻译后运行。但是弊端也显而易见,首先从源代码转换到机器指令所耗费的时间将是很大的,而且修改一处将会导致所有的源码都需要重新编译,一旦发生了错误还很难发现错误的所在地,调试程序也成为了一件很难的事情。所幸的是,聪明的工程师们总能解决几乎所有可以被提出来的问题,例如C语言便采用了分段编译这样一个方法来解决第一个问题,将源代码分为一个一个区块,将每一块单独测试完成后便可以进行单独的编译,最后再使用连接器将各个区块的代码连接起来,加上可以被操作系统装载的部分指令,便生成了一个可执行程序。而想要对代码进行调试的话,便需要编译器在编译后的机器指令中间加入一些与源代码有关的信息,以便于一些源代码层的调试器来跟踪程序的执行情况,而在最后源代码可以正常工作后则可以由编译器可以产生不含这些信息的机器指令。我们常用的Visual C++可以通过在工程选项中选择生成debug版本或者release版本来控制这些信息的有无,(当然除此之外一般情况下release版本也会包含更多的优化,在编译中生成更快更短更少冗余的机器指令是编译过程中优化器所做的的工作),而在GNU C++编译器当中我们也可以通过添加debug参数来实现加入调试信息。有了这些东西很多时候会使解释性语言显得不那么的具有诱惑力,然而编译器编译后的程序的工作平台往往是局限的,想要适应更多的平台就必须使用编译器针对不同平台重新编译。这也是为了运行效率所作出的一点牺牲吧。

上一段中提到了C语言采用了分段编译来解决程序“牵一发而动全身”的问题,当然C++当中也采用了这一具有优势的特性,由其在大型工程中,很有可能一次全局的编译要耗费好几个小时甚至好几天的时间。但是在分段编译的时候,也有一些问题需要思考,首先对于一个代码块,当它要引用其他代码块或者数据块的时候,最起码要知道一些其他代码块或者数据块的必要信息,这样编译器才能为他正确的预留一个“接口”让连接器将它引用的代码块连接上来,并且提供给它正确的信息。对于一个数据块这个基本信息就是它的名字和类型组成,对于一个代码块则是它的名字还有它需要的信息类型以及执行完这个代码块之后可以得到的信息类型。当然在C和C++当中这些代码块的最小的单位为函数,而引用这些函数的动作被称为函数的调用,函数调用的时候便需要传递给函数它所需要的参数,并且读取函数执行完之后传递回来的返回值。编译器为了保证能够正确的为一个函数的调用预留接口,并且可以让编译器检查我们传入数据的正确性,于是要求我们在调用这个函数之前一定要对这个函数进行声明,这样编译器才可以获得足够的信息来避免错误。而这个函数的本体可以放置在另外的编译单元当中,而这包含了代码块的函数本体则称为函数的定义。可以由编译器对其进行单独的编译,只要正确的配置了连接器,连接器总能正确的将它们拼接起来。

“声明”是一个很有意思的概念,它会告诉编译器有这样一个名字的数据块或者函数,它是什么什么样子的,你可以在接下来的代码当中根据这些信息预留出它的接口,并让连接器将它的函数体连接上去,接下来的代码便可以正确运行。而“定义”则是告诉编译器下边花括号当中的代码块即是这个名字,或者请在程序运行至这个位置时为这个名字的变量分配空间。在C和C++当中,我们可以在很多地方进行相同的声明,但是定义只能出现在一个地方,如果一个函数或者变量具有多个定义的话,连接器将会报告出错误。

以下为一些详细的使用方法,几乎全部摘自书上。

首先声明一个函数:

double func1(int, float);

其中int表示函数返回值类型,func1为函数名称,可以是除了保留字以外的任何字符串,括号当中是函数的参数表,依次为函数输入参数的类型。当然函数声明的时候也是可以给参数命名的只不过这里的命名对于编译器来书毫无意义,它会将其忽略掉,不过给它命名会给程序员阅读代码带来很多便利。正如下面的声明和上面的声明意义完全相同。

double func1(int a, float b);

当然值得注意的一点是在C和C++当中空的参数列表具有不同的意思,对于C语言来说一个空的参数列表意味着这个函数可以匹配任意的参量表,而对于C++来说这意味着这个函数不能接受任何参数。当然如果需要在C语言当中表示这个函数不能接受任何参数的话需要在括号内加入void,而在C++当中表示这个函数可以接受任何参数的话需要在括号内加入省略号。示例如下:

int func2();

int func2(void);

int func2(…);

而函数的定义也不是特别复杂,定义一个函数即是把函数声明部分的分号替换成为花括号就好:

double func1(int a, float b) {}

花括号中间为函数的主体部分,值得注意的是,如果函数当中使用到了参数表中的某个参数,那么这个参数必须具有一个名字,即标识符,而如果始终没有用到某个参数,则这个参数可以只写它的参数类型,这样的参数称为占位符。一个用途是函数更新后参数需求减少时可以进行占位而不必让编译器弹出警告。

声明一个变量则稍稍有点复杂,这是一个耐人寻味的问题,因为当我们声明一个变量的时候,编译器便获取到了足够的信息可以对它进行内存分配,所以如果我们单独写出一行类似于:

int a;

这样的代码之后,编译器默认这是一条声明和定义。但是如果我们不想在这个地方定义变量的话,我们需要向编译器特别说明。通过在前边加上extern关键字来实现这一点:

extern int a;

这样就可以区分定义和声明一个变量了,当然函数也可以用相同的方法区分,不过会显得多此一举。

以上便是一些关于C++的基础的知识以及基础的语法,通过了解本章后半部分关于库及模板的介绍之后,便可以构建一个简单的C++程序了。我将它分为两部分来叙述。