嵌入式C++实战教程(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-28 11:44:20

点击下载

作者:陈志发,周中孝

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

嵌入式C++实战教程

嵌入式C++实战教程试读:

前言

C++不仅是一种很重要的高级编程语言,而且代表了一种编程思想。它的思想已经被其他编程语言继承并发扬光大。现代每种新语言的诞生,都可以找到C++的影子。所以说,若精通了C++,再学习别的语言就很容易了,比如Java、C#等。

C++在目前使用非常广泛,很多大型的程序都是用C++写出来的,学好C++编程语言是很有用的。但是很多初学者学习过C++语言后认为C++很难学,学习了好几本书,有不少地方还是不理解。C++难道真的这么难吗?学习C++真的需要阅读这么多教材和资料吗?

C++其实不难学,那是由于好多书籍、资料的知识点组织结构和讲解方式等不够合理,无意中增加了初学者学习C++的难度。无论是谁,学习一种新的知识,好的教材是非常重要的。讲解清晰易懂、内容科学合理的教材有助于初学者迅速掌握知识体系和精髓,在学习时间相同的情况下,学习效果会更好。

现在市面上一些C++书籍不分主次轻重,比如,在初学者根本不知道模板是什么的时候,该书却对STL过早地讲解。而一些相对简单的基础概念,却放到后面,进而影响前面其他基础语法知识的学习,这也违背了先易后难的原则。而且表述语言过于专业化,专业术语太多,对于普通知识点的讲解也写得复杂深奥,非常不直观。这样的后果是初学者在按照那本书学习C++的时候,需要不断前后跳跃式阅读,就像在查字典,不但花费很多时间,而且学习效果也不好,人为增加了学习C++的难度。对于初学者来说,这样的字典式图书是不合适的,他们需要一本循序渐进、快速、扎实讲解C++语言的书。

本书是以初学者最易懂的方式来阐述C++知识的,能让从未学习过编程语言的初学者也能成为高手;同时讲解深刻细致,能让专业C++程序员阅读本书后仍然有质的飞跃。本书是我在百忙的工作之中抽出大量业余时间完成的,其中的辛苦自然不用多说。但是让我感到安慰的是,本书确实能让学习C++的初学者少走弯路,并迅速提高。

本书从一个最简单的C++程序讲起,然后通过这个程序引出一系列相关知识,让初学者循序渐进地学习。同时书中的示例程序都是经过精心设计的。本书特点是实用性强,章节安排合理,清晰易懂,重点突出,深入浅出。相信读者阅读本书后,一定会有很大的提升,能够达到短期内掌握C++语言的效果。

本书的出版,离不开深圳信盈达电子有限公司所有同事们的支持和帮助,在此向他们表示衷心的感谢。另外,感谢我的父母、亲人和朋友,是他们给予我精神上的支持和鼓励。感谢电子工业出版社,是他们认真专业的审核,让本书由粗糙的初稿变成了精美的图书。

由于时间仓促,编著者水平有限,书中可能有不恰当的地方,希望广大读者批评指正,联系邮箱:niusdw@163.com,欢迎来信交流。陈志发2014年10月

第1章 初识C++

1.1 C++简介

C++这个词在中国大陆的程序员圈子中通常被读作“C加加”,而西方的程序员通常读作“C Plus Plus”,它的前身是C语言。C++是在C语言的基础上开发的一种集面向对象编程、泛型编程和过程化编程于一体的编程。1980年,美国贝尔实验室的Bjarne Stroustrup博士及其同事在C语言的基础上,从Simula67中引入面向对象的特征,开发出一种将过程性与对象性相结合的程序设计语言,最初被称为“带类的C”,1983年取名为C++。此后,C++经过了许多次改进、完善,发展成为现在的C++。目前的C++具有两方面的特点:其一,C++是C语言的超集,因此它能与C语言兼容;其二,C++支持面向对象的程序设计,这使它被称为一种真正意义上的面向对象程序设计语言。C++支持面向对象的程序设计方法,特别适合中型和大型的软件开发项目。从开发时间、费用,到软件的重用性、可扩充性、可维护性和可靠性等方面,C++均具有很大的优越性。

1.2 C++的发展过程

C++语言发展大概可以分为三个阶段。第一阶段是从20世纪80年代到1995年,这一阶段的C++语言基本上是传统类型上的面向对象语言,并且凭借着接近C语言的效率,在工业界使用的开发语言中占据了相当大份额。第二阶段是从1995年到2000年,这一阶段由于标准模板库(STL)和Boost等程序库的出现,泛型程序设计在C++中占据了越来越多的比重。当然,同时由于Java、C#等语言的出现和硬件价格的大规模下降,C++受到了一定的冲击。第三阶段是从2000年至今,由于以Loki、MPL等程序库为代表的产生式编程和模板元编程的出现,C++出现了发展历史上又一个新的高峰,这些新技术的出现以及和原有技术的融合,使C++已经成为当今主流程序设计语言中最复杂的一员。

1.3 C++和C的区别以及C++新增特性

1.3.1 C和C++的区别

对于初学者来说,仅需要明白:C++是在扩充了C面向对象过程功能的基础上,又增加了面向对象的功能。下面列举了二者的一些区别。

从机制上:C是面向过程的(但C也可以编写面向对象的程序);C++是面向对象的,提供了类。但是用C++编写面向对象的程序比C容易。

从适用的方向:C适合要求代码体积小、效率高的场合,如嵌入式;C++适合更上层、复杂的场合。Linux核心大部分是用C写的,因为它是系统软件,要求极高的效率。

从名称上也可以看出,C++比C多了两个“+”,说明C++是C的超集。那为什么不叫C+而叫C++呢?这是因为C++比C扩充的东西太多了,所以就在C后面放上两个“+”,于是就成了C++。

C语言是结构化编程语言,C++是面向对象的编程语言。

C++侧重于对象而不是过程,侧重类的设计而不是逻辑的设计。

C++是从C语言派生的,它与C语言是兼容的。

C++在C语言基础上新增了一些保留字:class、friend、virtual、inline、private、public、protected、const、this、string;也新增了一些运算符:new、delete、operator::。

1.3.2 C++新增特性

C++新增特性包括:● 引用;● const常量定义;● 函数的默认参数;● 内联函数;● 函数重载;● 强制类型转换;● 输入/输出流。1.引用“引用”就是“变量”的别名,对引用的操作与对变量直接操作的作用是完全一样的。比如一个人有个正式名字叫张三,还有一个小名叫小红。那么她和朋友在一起的时候,朋友叫她张三或叫她小红,都是指向同一个人。

引用的声明:数据类型 & 引用名 = 初始值(初始值是变量名)。

引用在定义时必须初始化。注意此处的“&”并不是取地址符,而是“引用说明符”。声明一个引用并不是定义了一个新的变量,只表示给变量取了一个别名,不能再把该引用名作为其他变量名的别名。编译器会给它分配内存空间,因此引用本身占据存储单元,但是引用表现出来给用户看到的不是引用自身的地址,而是目标变量的地址,也就是说对引用取地址就是目标变量的内存地址。

C++的函数允许利用引用进行参数传递,具有高效性和安全性。

引用作为函数参数的特点如下:(1)在进行实参和形参的结合时,不会为形参分配内存空间,而是将形参作为实参的一个别名。使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率高,且所占空间小。(2)用引用能达到用指针传递一样的效果,则函数内对形参的操作相当于直接对实参的操作,即形参的变化会影响实参。引用相对指针的优点如下:利用指针传递时,在被调用函数中同样要给形参分*配存储单元,且需要重复使用“指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差。另一方面,在主调函数的调用点处,必须用变量的地址作为实参,则引用更容易使用,更清晰。

示例1:引用作为形式参数

示例2:函数返回引用类型2.const常量定义

const在英文中是固定不变的意思,用const修饰的常量不能修改,常用来定义一个符号常量。

在编程中,为了代码更容易维护,通常把一些只读的变量定义为常量形式,这样可以防止程序员在编程过程中由于不小心而导致的对不应该修改的变量进行修改。比如,标准C库中的字符串复制函数原型是:

细心的读者可能会发现,第2个参数是前面增加了const关键字修饰。这是因为这个函数中dest是目标指针,存放新的内容;src是数据源指针,只提供源数据;执行函数并不会修改src源指针指向的内存单元内容,所以为提高代码的可读性和安全性,把它定义为常量指针(指向的内存单元是常量,不可修改)。

在形式参数和普通变量上增加const关键字修饰,在C++和C中的特性是一样的。但是是在C++语言中,对const的应用进行了扩展。比如,在类成员函数上使用const修饰具备不同特殊含义,这在后面的类学习中将会讲述。下面列举常见的const修饰用法。

常量:const 类型说明符 变量名

例子:

定义常量时一定要进行初始化,因为后面不能再对var进行修改。

常量引用:const 类型说明符 &引用名

例子:

常量对象:类名 const 对象名

#include <iostream>

#include <string>

using namespace std;

例子:

常量成员函数:类名::函数名(函数形参) const 说明:这个是C++特有用法

例子:

上面class MyClass的int get_x(void) const;函数内容只负责返回类成员x的值,并没有对类中的任何成员进行修改,所以在函数后面添加了const修饰。这样在函数体的对像类中任何成员的写操作,在编译时都会报错,提高了代码的安全性。

常量数组:类型说明符const数组名[大小]*

常量指针: const 类型说明符 指针名*

指针常量:类型说明符 const 指针名

指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。注意,指针常量很容易和常量指针混淆,指针常量意思是指针本身是一个常量,其指向地址不能修改,并不是它指向地址的内存单元是常量,所以指针常量在定义时一定要对它进行初始化。

例子:3.函数的默认参数

在C++中,对C语言的函数特征进行了扩展,其中增加的一个新特性就是函数可以有默认值。所谓的默认值,就是在调用时可以不写某些参数的值,编译器会自动把默认值传递给调用语句。默认值可以在声明或定义中设置;也可在声明或定义时都设置;但在都设置时要求默认值是相同的。

对于新的事物,以一个代码为例子来学习会比较快。以下是示例代码:

代码中使用的cout<<是C++的输出流运算符号,没有C++基础的读者可以先不用理解,把它看成相当于标准C的printf函数一样,能在计算机屏幕上输出内容。代码中定义了一个函数default_parameter_func,另外声明时给形式参数设置了默认值。“void default_ parameter_ func(int num1=def_var_1,int num2 = 3,char *ch = '');”在主程序中调用default_ parameter_func函数时就可以有多种调用方式。拥有默认参数的,调用时可以不传递,就像上面代码中一样,三个都有默认参数,调用时甚至可以一个参数都不传递,这时函数使用声明中设定的参数。

运行结果如图1.1所示。图1.1 默认参数测试运行结果

以下总结一下默认参数的语法与使用:(1)在函数声明或定义时,直接对参数赋值,这就是默认参数。(2)在函数调用时,省略部分或全部参数,这时可以用默认参数来代替。(3)默认参数只可在函数声明中设定一次。只有在没有函数声明时,才可以在函数定义中设定。(4)如果一个参数设定了默认值,其右边的参数都要有默认值。如:(5)默认参数在调用时,则遵循参数调用顺序,自左到右逐个调用。这一点要与第(2)条分清楚,不要混淆。

上述示例代码如下:(6)默认值可以是全局变量、全局常量,甚至是一个函数,但不可以是局部变量。因为默认参数的调用是在编译时确定的,而局部变量的位置与默认值在编译时是无法确定的。4.内联函数

内联函数从代码形式上看,有函数的结构;但是在编译后,却不具备函数的性质。编译时,如同#define宏,内联函数通过避免被调用的开销来提高执行效率,一般在代码中用inline修饰。

内联函数和宏很类似,但区别在于:宏是由预处理器对其进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样展开,所以取消了函数的参数压栈,减少了调用的开销。用户可以像调用函数一样来调用内联函数,而不必担心会产生处理宏所带来的一些负面问题。

内联函数的定义:

当用户定义一个内联函数时,在函数定义前加上inline关键字,并且将定义放入头文件就可以了。内联函数示例:

在C++中,还存在另外一种隐式的内联函数定义规则:在类定义体内部直接实现代码的成员函数都会被自动认为是内联函数。示例如下:

上面的add( )、dec( )、get_num( )三个函数都是内联函数。在C++中,在类的内部定义了函数体的函数,被默认为是内联函数,而不管是否有inline关键字。5.函数重载

函数重载是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是用同一个同名函数完成不同的功能。

示例代码如下:

编译器通过参数的个数和类型以及顺序来确定调用重载函数的哪个定义,不能仅仅通过函数的返回值类型的不同来实现函数重载。只有对不同的数据集完成基本相同任务的函数才应重载。

优点:● 不必使用不同的函数名;● 有助于理解和调试代码;● 易于维护代码。

运行结果如图1.2所示。图1.2 函数重载示例运行结果6.强制类型转换

C++的强制类型转换与C语言略有不同。

C语言中的强制类型转换常常采用:

C++中的强制类型转换常常采用:

在C++中,除上面的常规数据类型的显式转换外,还新增加了4个显式转换方式。

static_cast:

用法:static_cast < new_type > ( expression )

说明:该运算符把expression转换为new_type类型,但没有运行类型检查来保证转换的安全性。

static_cast可以用在指针和引用上,还可以用在基础类型和对象上进行转换,但是static_cast只能用来处理两者具有一定关系的数据间转换。而在这几种使用场合中,static_cast真正用处并不在指针和引用上,而在基础类型和对象的转换上,这是dynamic_cast,reinterpret_cast,const_cast这3个运算符号所不能实现的。

示例代码:

dynamic_cast :

用法:dynamic_cast < new_type > (expression)

说明:该运算符把expression转换成new_type类型的对象。*new_type必须是类的指针、类的引用或者void。dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。

在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。当对一个指针使用dynamic_cast时,先尝试转换:如果成功,就返回新类型的合法指针;如果dynamic_cast失败,返回空指针。dynamic_cast<>转换还有一个要求,需要类成为多态,即包括“虚”函数。

示例代码:

该示例代码中Base是Derived的基类,使用了两个dynamic_cast进行这两类对象指针的相互转换:第一,“pd = dynamic_cast<*Derived>(pba);”把一个子类对的指针转换到基类对象指针,这是可*以行的;第二,“pd = dynamic_cast<Derived>(pbb);”把一个基类对象指针转换到子类对象指针,这会导致失败,因为子类对象可能存在基础不存在的成员,而dynamic_cast会在运行时进行检查。如果使用static_cast可以转换成功,就会造成不安全因素,当使用到基类不存在的成员时可能会造成程序崩溃。

运行结果:

const_cast:

用法:const_cast< new_type > (expression)

说明:该运算符用来修改类型的const或volatile属性。除了const或volatile修饰之外,new_type和expression的类型是一样的。

示例代码:

运行结果:

结果分析:

从上面输出可以发现,即使是使用const_cast把const属性去除了,原来所指向的const对象的值并不会被修改;但是使用const_p,modifier指针取出的值却是修改了。这一点符合C++的逻辑;但是最后面输出&var, const_p,modifier的值,竟然是相同的。这一点从常理上推导却是不合理的。IBM的《C++指南》称呼以上代码的*“modifier = 7;”为“未定义行为(Undefined Behavior)”。所谓未定义,是说这个语句在标准C++中没有明确的规定,由编译器来决定如何处理。所以,这个运算符号并不能修改const变量的属性。那这个运算符号存在的作用是什么呢?实际上这个运算符号的用处是在函数调用时的实际参数转换上,示例代码如下:*

void disp(int var)函数原型参数是非const,但是实现调用时传入了一个const参数。直接写会编译错误,然而实际应用中却常常会有这样的需求,所以要使用const_cast运算符号进行修饰。

reinterpret_cast :

用法:reinterpret_cast<new_type> (expression)

说明:reinterpret_cast运算符是用来处理无关类型之间的转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原来类型的指针,还可以得到原先的指针值),还可以进行不同类型指针之间的转换。

IBM的C++指南(Web网页)中明确告诉我们reinterpret_cast应该在什么地方用作转换运算符:(1)从指针类型到一个足够大的整数类型;(2)从整数类型或者枚举类型到指针类型;(3)从一个指向函数的指针到另一个不同类型的指向函数的指针;(4)从一个指向对象的指针到另一个不同类型的指向对象的指针;(5)从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针;(6)从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针。

备注:所谓“足够大的整数类型”取决于操作系统的参数:如果是32位的操作系统,就需要整型(int)以上的;如果是64位的操作系统,则至少需要长整型(long)。具体大小可以通过sizeof运算符来查看。

随意在不同类型之间使用reinterpret_cast,有可能造成程序的破坏和不能使用,比如下面的代码:

上面的代码先是定义一个函数指针的数据类型funp_t;然后定义一个funp_t类型的函数指针fun_p,并使用reinterpret_cast运算符号把变量value的地址强制转换为函数指针,赋值给fun_p;最后使用fun_p函数指针来调用函数,代码编译没有问题,但是在运行到“fun_p(value);”时却会报错,因为fun_p所指向的地址并不是一个有效的函数存放地址,所以运行异常。图1.3所示是运行异常的界面,这个错误要使用单步调试才能看到,全速运行并不能出现这个运行异常的界面。图1.3 reinterpret_cast导致程序运行异常的提示

由此可知,错误地使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回它的原始类型,这样才是正确使用reinterpret_cast方式。7.输入/输出流

流是字符集合或数据流的源或目的地。

有两种流:● 输出流,在类库里称为ostream;● 输入流,在类库里称为istream。

同时处理输入和输出,由istream和ostream派生双向流iostream,如表1.1所示。表1.1 输入/输出流(1)输出流。

cout输出到屏幕,对应ostream类:

输出结果:*

cout提供:put输出字符,write(char buf,int n)输出字符串。

输出结果:

在输出数据时,为简便起见,往往不指定输出的格式,由系统根据数据的类型采取默认的格式,但有时希望数据按指定的格式输出,如要求以下十六进制或八进制形式输出一个整数,对输出的小数只保留两位小数等。有两种方法可以达到此目的:一种是使用控制符,如表1.2所示;另一种是使用流对象的有关成员函数。表1.2 用控制符控制输出格式

注意:这些控制符是在头文件iomanip中定义的,因而程序中应当包含头文件iomanip。通过下面的例子可以了解使用它们的方法。

示例代码:(2)输入流

cin是istream类的对象,它从标准输入设备(键盘)获取数据,变量通过流提取符“>>”从流中提取数据。当通过“>>”从流中提取数据时,通常忽略输入流中的空格、Tab键、换行符等空白字符;所以在连续的变量输入中,可以使用空格、Tab键、换行符来实现输入分割符号。比如,从键盘上读取两个整数,存放在变量a、b中:

使用get( )获取一个字符:

利用cin.sync( )清空缓冲区,类似于C语言的fflush(stdin)。

使用cin.read做无格式读取一串字符:

以上几个知识点的综合示例代码如下:

运行结果:

程序中要使用标准输出/输入流,要包含iostream或iostream.h。iostream.h与iostream的区别:iostream.h是为兼容旧有系统而保留的版本,是从C继承下来的,不存在命名空间问题,其实在VS2012中已经不再支持这个文件了;而iostream是新的C++标准支持的,最好是用iostream,然后用using namespace std。

1.4 C++编译器版本

鉴于本书所有例程都是在Microsoft Visual Studio 2012环境下测试成功的,希望读者安装此版本的C++编译器。Microsoft Visual Studio 2012是收费软件,官方提供30天试用版,对学习本书已经足够使用了;如果觉得该软件不错,请购买正式版本使用。

第2章 一个简单的C++入门程序

在编程界,几乎所有的编程语言学习,都是从一个简单而伟大的入门程序开始的,这个程序就是向屏幕中输出“hello world!”。下面我们也以这个简单的C++程序入门,开始我们的C++学习之路。

2.1 入门级的C++程序

入门C++示例程序:

运行结果:

这是一段输出“hello world!”的小程序。

学习过C语言的读者可能会发现,这个程序的结构和C语言的程序结构没有什么区别。但是,其中有的函数在C语言中却没有,同时C++也可以使用C语言中的标准C库函数,比如像代码中的printf("hello world!\n")语句,就是属于标准C库的函数。

为了照顾没有编程基础的读者,顺便解释一下这个程序的组成。程序中#include <iostream>是预处理代码,表示引入标准库iostream的头文件。在C语言中,标准库头文件扩展名是.h,但是在C++中标准库头文件是不带.h扩展名的。

iostream(输入/输出流)是C++的一个标准库,包含很多功能函数和对象等。库中每个函数都有其自身的作用,比如示例中的cout对象,它就是这个标准库函数中的一部分,负责向屏幕输入内容的。

using namespace std表示使用std命名空间。命名空间是为了解决大程序开发时,函数名、变量名重名的问题。在本书后面的章节中会进行更加详细的介绍。

int main (void){ }函数:“main”的意思是“主函数”,每个C++程序都必须有一个main函数,但是其中的函数体{ }代码的具体内容由编程者决定。本例中用来在屏幕上输出“hello world!”。前面的int表示main函数的返回值类型,对于main函数,其返回值是操作系统的,用于告诉操作系统main函数的执行情况:一般情况下返回0表示程序执行功能,其他表示程序执行异常。所以在main{ }函数后面有一条代码return 0,程序从上往下执行,执行到最后,告诉操作系统程序执行成功。当然,如果main函数做的事情很多,我们会在代码中分析判断什么情况下出现错误。当出现错误的时候,可以提前结束程序,给操作系统返回一个非0值,初学者不要求掌握这个内容。

2.2 输出语句的使用

下面来学习一个C++的输出语句。在程序调试中,使用最频繁的就是输出语句了,常常要以各种形式打开输出变量值或一些调试提示信息,所以掌握常用的输出格式是非常必要的,这也是学习C++的必备知识。

先来看一个程序,示例代码如下:

运行结果:

上述代码中using namespace std使用std命名空间,意思是下面的对象或变量,没有显式声明是哪个命名空间中的成员,则默认表示是std空间中的成员。比如,代码中的cout成员对象,前面就没有添加std::限定,由于此处指明了使用std,则没带限定符号的都是std空间的成员。

从上述代码中还可以看到,C++要输出字符到屏幕,使用的左移运算符,并且输出可以连续书写。其中的endl表示输出回车换行符,类似标准C中的“\n”字符。

另外,对整型数据的输出可以设置为十进制、十六进制、八进制方式输出,只要在输出数据前先输出这几种进制对应的标志:dec、hex、oct,再输出对应的变量即可,详细请参考上面的示例代码。

2.3 std::介绍

在前面的代码中,文件上方有一行using namespace std,其实也可以不加这一行,而直接在cout和endl前面添加“std::”。但当不加这一行时,添加std::cout是必要的,否则编译器会报错。

示例代码如下:

由于在程序上方没有了“using namespace std;”声明,程序中使用std命名空间,所以代码中不能直接写cout和endl,而需要用std::来限定。

关于命名空间成员引用,除上面提到的使用全局的“using namespace std;”声明或者使用std命名空间在每一个成员前面添加std::限定外,还在另外一种定法,其实就是把“using namespace std;”修改为“using std::cout; using std::endl;”,主程序中使用cout<<endl时也不用在前面添加std::限定符号。使用“using std::cout; using std::endl;”意思是释放出std命名空间中的cout<<endl成员,而“using namespace std;”把std命名空间中成员全部释放出来,这样写法简单,但是在大型程序中有可能存在名字冲突的风险。至于使用哪一种,由读者根据自己的需要去决定。修改后的代码如下:

上述代码中使用了“using std::cout; using std::endl;”语句把std命名空间中的cout<<endl对象释放出来,所以主程序中不再添加std::限定;但是并没有把进制输出格式控制成员dec、hex、oct释放出来,所以代码中使用这几个成员时还是要加std::限定。

2.4 iostream与iostream.h的区别

前面在介绍std::的用法时,同时介绍了iostream头文件,而iostream头文件是C++一个标准库的头文件。也许在以后的学习、工作中还会发现有的代码并没有使用std::限定cout,也没有使用using namespace来声明,头文件也不是使用iostream.h,而是使用iostream.h。这是历史的原因,C++是从C语言衍生而来的,前期是为了让程序员能够平稳过渡到C++开发中,对C采取了兼容做法,那时候的C++也不存在命名空间这个特征。所以在比较老的资料或代码中,看到的将会是带.h扩展名的标准库头文件,并且也不会存在命名空间问题。现在比较新的编译器已经不再兼容这种带.h扩展的标准库头文件了(注意:用户自己编写的头文件还是带.h)。

命名空间这个新概念从C++开始才真正引入,下面介绍命名空间的作用。

C++引入命名空间(namespace)的目的是为了减少和避免命名冲突。当程序较大时,就很难避免重名,特别是多人合作的情况下。过去C语言中的解决方法是靠人为的注意,并且加长名字,避免重名。这样做会使得一些名字看上去没有意义或者难以理解。而程序员在编写程序时,也会受到这个问题的限制,不能自由地命名自己要使用的变量或者函数。通过使用namespace,可以解决这一问题,这就是C++引入namespace这个概念的好处。

补充说明:由于C++是从C衍生而来的,又具备兼容C语言的特点,对标准C库实现的库函数也是支持的,但是其包含的头文件却有点不同。比如,标准C有一个头文件叫stdio.h,而在C++中这个文件演变成cstdio;C语言中有标准字符串处理库string.h,而在C++中这个文件演变成cstring;其他的头文件同理。细心的读者可能已经发现规律了,其实只是在原来的文件名前边添加一个字母c,用于表示这个库来源于标准C库,然后再把.h扩展名去除,就符合C++对标准库头文件的书写规则了。

2.5 重名问题

前面章节引出了标准命名空间std,知道了C++之所以引入命名空间这个概念,是为了避免发生重名问题。那么我们如何定义自己的命名空间,以及如何使用呢?下面通过实例来学习这个内容。

C++有两种形式的命名空间——有名的和无名的。

有名字命名空间的定义格式:

无名字命名空间定义格式:

命名空间的成员是指在命名空间定义中的花括号内声明了名称。可以在命名空间的定义内定义命名空间的成员(内部定义);也可以只在命名空间的定义内声明成员,而在命名空间的定义之外定义命名空间的成员(外部定义)。

例如,相关程序如下:

运行输出:

代码分析:(1)往已经存在的命名空间中添加新成员,只需按照命名空间的定义方式那样,把命名空间名写成和原来的名字一样就可以了;对于同名命名空间,编译器会自动合并在一起。比如下面中的代码:(2)命名空间中可以只做函数的声明,而在空间外部进行函数的实现,具体写法如下:(3)全局命名空间是一个匿名的空间,没有名字,访问其成员使用::做限定符号,如代码中的cout << ::namespace_test_var <<std::endl ;(4)一个命名空间要访问另外一个命名空间的成员,要添加目标命名空间的空间名限定符号,如:

接下来测试一下同名冲突的情况是什么样的,以加深对命名空间的理解。测试代码只是在上一例子代码的基础上,在main函数前面添加了两条语句using namespace A和using namespace B,其他的代码不变,这样会使程序编译不能通过,出现编译错误。修改后的示例代码如下:

该程序段编译后显示一条错误信息:

编译错误的原因很直观明了,因为程序中定义了三个namespace_test( )函数,分别位于不同命名空间中,在主程序上方写了“using namespace A;”和“using namespace B;”把这两个空间的成员都释放出来了,这时全局空间中同时存在3个同名的函数,编译器就不知道应该使用哪一个了,所以直接中止编译。

第3章 C++数据类型和运算符

绝大部分的高级编程语言(有些没有数据类型,比如shell就没有)都会制定有不同的数据类型,用于存储不同的数据。所谓的数据类型,其实从本质上讲,就是占用的存储空间不一样,以及存储的方式不一样。在C++中定义了多种基础数据类型,在本章中将对这些基础数据类型进行学习。一般把用数据类型名限定的词称为变量,它会开辟一定大小的内容空间来存放数据;至于空间的大小,由变量类型决定。

3.1 C++基本数据类型

C++语言的基本数据类型有四种:整型,说明符为int;字符型,说明符为char;浮点型(又称实型),说明符为float(单精度)和double(双精度);空值型,说明符为void,用于函数和指针。

为了满足各种情况的需要,除了空值型外,其他三种类型前面还可以加上修饰符改变原来的含义。修饰符分别为:signed:表示有符号;unsigned:表示无符号;long:表示长型;short:表示短型。这四种修饰符都适用于整型和字符型,只有long还适用于双精度浮点型。

ANSI C/C++基本数据类型及取值范围如表3.1所示。表3.1 ANSI C/C++基本数据类型

表3.1中总结了C++内置基础数据类型所占用空间大小以及取值范围。可以使用sizeof( )函数测试数据类型占用的内存空间大小,示例程序如下:

输出结果:

小结:(1)类型修饰符signed、unsigned、short和long用于修饰字符型和整型。(2)当用signed和unsigned、short和long修饰int整型时,int可省略。(3)其中bool和wchar_t是C++特有的。对于条件判断,零为假,非零为真;对bool变量可赋非0、非1的其他真值,但是一般使用true和false来赋值。(4)float的精度是6位有效数字,通常是不够的;double类型可以保证10位有效数字,能够满足大多数计算的需要。使用double类型基本不会出错,在float类型中存在隐式的精度损失。默认的浮点数字常量为double类型,在数值后面加上F或f表示单精度,如3.14159F。浮点数float、double的存储设计,和其他的基础数据类型不同。可参考IEEE754浮点数表示标准。(5)C/C++都可以自定义枚举enum、联合union和struct结构体类型。(6)以上数据类型长度是在Windows 8的64位平台上使用sizeof( )测试的,其中某些类型数据的字节数和数值范围由操作系统和编译平台决定的。

比如16位机上,sizeof(int) = 2,而32位机上sizeof(int) = 4;32位机上sizeof(long) = 4,而64位机上sizeof(long) = 8。除此之外,注意64位机上的pointer占8字节。(7)void的字面意思是“无类型”,不能用来定义变量。void真正发挥的作用在于:

a.对函数返回和函数参数的限定,例如自定义既不带参数也无返回值的函数:*

b.定义无类型通用指针void ,指向任何类型的数据。

3.2 布尔型变量

布尔型变量(Boolean Variable)是有两种逻辑状态的变量,它包含两个值:真和假。如果在表达式中使用了布尔型变量,那么将根据变量值的真假而赋予整型值1或0。要把一个整型变量转换成布尔型变量,如果整型值为0,则其布尔型值为假;如果整型值为非0,则其布尔型值为真。布尔型变量在运行时通常用作标志,比如进行逻辑测试以改变程序流程。

可以这样定义一个布尔型变量:

对这个布尔型变量bool_var的赋值:

在C++中true用来代表1,false用来代表0,因此我们可以赋值:

它与bool_var = 1是相同的。

另外我们也可以在定义时进行初始化,如:

关于布尔型变量用法的例子如下:

输出:

程序分析:

main函数中定义了一个bool变量bool flag = true,初始化为真,接下来的while( flag ){ }循环执行,每次减1,直到num值减到1时,把flag变量设置为假,这时终止循环,在屏幕上输出1~10的累加和。

3.3 wchar_t双字节型变量

wchar_t是C/C++的字符数据类型,它是一种扩展的字符存储方式。wchar_t类型主要用在国际化程序的实现中,但它不等同于unicode编码。unicode编码的字符一般以wchar_t类型存储。char是8位字符类型,最多只能包含256种字符,而许多外文字符集所含的字符数目超过256个,char型无法表示,像存储汉字、韩文与日文却不可以,因为汉字、韩文与日文都占据2字节。为了解决这个问题,C++又提供了wchar_t类型,也就是双字节类型,又叫宽字符类型。标准C++中的wprintf( )函数以及iostream类库中的类和对象能提供wchar_t宽字符类型的相关操作。

宽字符的定义:

宽字符的输出:

上述语句定义了一个wchar_t类型的数组变量wc,它用来保存中文字符“中”,大写字母L告诉编译器为“中”字分配2字节的空间。

标准C++的iostream类库包含了可以支持宽字符的类和对象,如wcout对象可以替代cout对象来执行对宽字符的输出,例子如下:

输出:

程序分析:

第7行定义了一个宽字符类型数组wc,它保存了“中”字,第8行使用wcout替代cout来输出宽字符。由于“中”是个汉字,因此第16行调用setlocale函数将本机的语言设置为中文简体;而setlocale函数在头文件locale中定义,因此第2行添加了头文件locale。

对于初学者来说该程序有很大的难度,因此不要求掌握,只要了解即可。

3.4 常量

所谓常量,就是固定不变的意思。C++中定义常量有两种方式:使用const关键字修改和使用#define定义。

使用const定义常量:

比如:const float price = 12.1234;定义了名字为price的常量,初始值是12.1234,程序中的任何地方都不能再修改price的值。

使用#define定义常量:

比如:#define PRICE 12.1234;定义了名字为PRICE的常量,值是12.1234,编译前,编译器会把程序中所有使用到PRICE的代码直接以文本替换的方式替换成为12.1234进行编译,同样这种定义也不能对PRICE进行赋值操作。

这两种常量之间的区别:(1)#define定义的常量,除了字符串字面常量外都不占内存,所以无法取常量的地址,仅仅是宏替换而已。

本质是字符串字面常量,会占用“静态存储区”。

本质是整型的字面常量,不会分配内存。(2)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查;而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。(3)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。(4)基本数据类型的const常量,编译器会重新在内存中创建一个它的备份(真正的基本数据类型的const常量会被编译器放到符号表中而不分配内存空间),通过其地址访问到的就是这个备份而不是原始的符号常量。(5)构造类型的const常量,实际上是编译时不允许修改的常量,是占内存空间的。因此,如果能绕过编译器的静态类型安全检查机制,就可以在运行时修改其内存单元。

如果不使用符号常量,而是直接在程序中填写数字或字符串,将会有哪些麻烦呢?(1)程序的可读性(可理解性)变差。程序员会忘记那些数字或字符串是什么意思,用户则更加不知它们从何处来、表示什么。在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。(2)如果要修改数字或字符串,则会在很多地方改动,维护成本高。

3.5 枚举类型

enum枚举类型:在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期只有七天,一年只有十二个月。如果把这些量说明为整型,字符型或其他类型显然是不妥当的。为此,C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值,并说明为该“枚举”类型的变量取值不能超过定义的范围。应该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。

枚举的定义

一个枚举是一个类型,可以保存一组由用户设定的值,枚举的值的范围就像是整数的一个子集一样,只不过其中的值由编程来决定。枚举的定义具有以下形式,即以关键词enum开头,接着是一个可选的枚举名,然后是由大括号{ }包含着一个由逗号分隔的枚举子列表:

enum是枚举关键字,enumeration_name是枚举类型名字,iteam1, iteam12是枚举成员名,在程序中直接使用它来代替其对应的值。例如,以下定义一个表示城市的枚举类型:

其中的Guangzhou赋值是1,Shenzhen赋值是3,Hongkong , Shanghai , Beijing , Chongqi后面的没有赋值,则其值依次类推,分别是Hongkong =4, Shanghai=5 , Beijing=6 , Chongqi=7。

定义好枚举类型cities后,下面我们就可以使用它来定义相应枚举类型的变量了,这种变量的值都只能限定在enum cities定义的数值范围中。示例代码如下:

运行结果:

程序分析:

从上述代码运行结果可以得到证实,枚举确定对应的是一个整数,但是又和整数是两个不同的数据类型,比如代码中的“// city_var = 7;”如果打开了,会导致编译失败,可以知道整型并不能直接赋值给枚举类型,尽管枚举定义中有值为7的枚举成员。要把一个整数赋值给枚举类型,则到进行显式的数据类型转换,如代码中的city_var = enum cities(7),编译就完全没有问题。另外,如果使用了显式数据类型转换把一个整数转换为枚举类型后赋值给一个枚举变量,则不需要这个整数在枚举类型定义的值的范围内,程序可以正常编译、运行,比如代码中city_var = enum cities(80),但是这样做没有什么好处,不建议这么做。

3.6 C++的运算符和表达式概述

C++语言中的运算符、表达式绝大多数和C语言中的是一样的。为了知识的完整性,现对其做一些简要说明。

顾名思义,运算符是指具有运算意义的符号,比如加运算符(+),减运算符(-)等;表达式是编译器能读懂的计算机语句,由运算符和操作数按一定语法规则组合而成,根据运算符决定对操作数进行何种运算,并得出唯一的运算结果。

按照操作数的个数可以将运算符分为:单目运算符(++、--),*双目运算符(+、-、),三目运算符(?:运算符)。通常有如下的运算符:算术运算符、逻辑运算符、关系运算符、条件运算符、位运算符、赋值运算符、逗号运算符、sizeof运算符以及其他运算符。*

算术运算符:加(+),减(-),乘(),除(/),取余(%)和正负号(+、-),其中前面5个的结合性是从左到右,负号的结合性是从右到左。

注意:在做除法操作时,如果被除数和除数都为整型,那么结果一定为整型,舍去小数部分。在做取余的操作时,只能用于整型数据。

逻辑运算符:C语言中提供了3个逻辑运算符,分别是&&(与)、||(或)和!(非)。!为单目操作符,结合顺序为从右向左;&&和||为双目运算符,结合顺序为从左向右。逻辑运算符要求其操作数为bool型,即有true和false两种取值;但实际上,非0表示true,0表示false。逻辑运算符的真值表如表3.2所示(1表示true,0表示false)。表3.2 逻辑运算符真值表注:由&&和||运算符组成的逻辑表达式中,C语言中规定只对能够确定整个表达式值所需的最少数目的子表达式进行计算。也就是说,当计算出一个子表达式的值后便可确定整个逻辑表达式的值时,后面的子表达式就不需要再计算了,这种表达式也称为短路表达式。

在“表达式1 && 表达式2”中,若表达式1为false,整个表达式为false,则表达式2不执行;而“表达式1 || 表达式2”中,若表达式1为true,整个表达式为true,则表达式2不执行。

示例:

结果是b++和++a都不会得到执行,a、b的值不变。

关系运算符:又叫比较运算符。主要包括:小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、等于等于(==)、不等于(!=)。关系运算符全部都是从左到右的结合性。关系运算主要用于一些程序的分支判断中,其中变量与零值的比较是应用最广的,也是最容易出错的地方。● 整型变量是否为0的判断:if(x == 0)。● 浮点型变量是否为0的判断:if(x >= -0.000001 && x <=

0.000001)。● 布尔型变量是否为0的判断:if(x)表示x为真。● 指针型变量是否为0的判断:if(p == NULL)。● 字符型变量是否为0的判断:if(c == ‘\0’)。

条件运算符:就是三目运算符,一般形式为A1 ? A2 : A3,表示若A1为真,则执行A2,否则执行A3。

位运算符:数据都是由0和1来存储的,可以通过位运算符改变内存中某单元的某一位的值。位逻辑运算符主要有:&(按位与),^(按位异或),|(按位或),~(按位取反)。移位运算符:<<(左移),>>(右移)。

赋值运算符:由赋值运算符组成的表达式叫赋值表达式。赋值运算符的结合性为从右到左。赋值运算符涉及一个左值的概念,就是能放在等号左边的表达式。

++和--运算符:它们都是单目运算符,结合性为从右到左。它分为两种形式:● 前缀形式(++a,--a)表示先将a加上或减去1之后再赋给左值;● 后缀形式(a++,a--)表示先将a赋给左值,a再加上或减去1。

示例:

逗号运算符:逗号运算符就是用“,”作为操作符,它可以将多个表达式连接起来形成逗号表达式。它是双目运算符,其优先级是C语言所有运算符中最低的,具有左结合性(从左到右)。它的结果是最右边表达式的值,其类型也是最后一个表达式的类型。

sizeof运算符:sizeof是个单目运算符,其结合性为右结合性(从右到左)。它用来求出运算对象在计算机的内存中所占用的字节数。例如:sizeof(short)→2,sizeof(int)→4。

运算符的优先级和结合性如表3.3所示,优先级的数字越小,优先级别越高。表3.3 运算符的优先级和结合性

总结:(1)操作数越多的运算符其优先级越低:单目-双目(不包含赋值运算符)-三目-赋值-逗号。(2)双目运算符个数最多:算术运算符-移位运算符-关系运算符-位运算符-逻辑运算符。(3)位运算符:~、&、^、|。(4)逻辑运算符:!、&&、||。(5)所有的赋值运算符都具有相同的优先级。

3.7 C++的类型转换

C++的类型转换和C语言差不多,只是稍有不同。

C语言的类型有很多,当进行不同数据类型之间的混合运算时,可能会引发混乱,为此引入类型转换机制(一种类型转换为另一种类型)。若参与运算的数据类型不同,先要将其转换为相同的类型,然后再进行运算。运算转换规则如图3.1所示。图3.1 运算转换规则

自动转换规则:图3.1中横向的箭头表示必须转换。例如,即使是两个float型参与运算,两者仍要先转换为double型再进行运算。纵向箭头表示系统会将类型由低向高自动转换。

赋值转换:将赋值运算符右侧表达式的类型转换为左侧变量的类型。

浮点型→整型:将浮点型(包括单、双精度)转换为整型时,将舍弃浮点数的小数部分,只保留整数部分。

单、双精度浮点型相互转换:将double型转换为float型时,先截取双精度的前7位有效数字,然后再赋给单精度类型的变量。

字符型与整型的相互转换:将整型赋给字符型时,只将低8位的赋给字符,高位的全部舍弃。

无符号与有符号的相互转换:若占据同样长度存储单元的有符号整型、无符号整型相互转换,则原样赋值,内部的存储方式不变,但外部值可能发生变化。例如,在将-1赋给一个unsigned int时,我们知道数据在计算机中是以补码的形式存储的,-1 → 11111111 11111111 11111111 11111111,则变成了4294967295。

强制类型转换:将一种数据类型强制转换为另外一种数据类型。C语言的强制类型转换的一般格式:(类型说明符)(表达式)。C++在此基础上有了新的格式:类型说明符(表达式)。例如,C语言的(float)a, (int)(x + y);C++的float(a) int(x + y)。

注意:不管是自动转换还是强制类型转换,都只是为了本次运算的需要而对变量的值进行临时转换,并不会改变变量的值。

第4章 C++程序的流程控制语句

C++虽然是面向对象设计的高级语言,但是,当把一个复杂功能进行分解成一个一个简单的对象后,要做的工作是使用面向过程的编程思想去实现这一个个基本的对象,最后再按照对象的组合关系,适当地把已经实现好的小对象进行组合,从而解决实际中的复杂问题。因此,不管是什么高级语言,流程控制语言是必不可少的,本章就开始C++语言的基本流程控制语句的学习。

任何程序逻辑都可以用顺序、选择和循环三种基本结构来表示。分别为:分支语句(if…else…语句和switch语句)、循环语句(for循环和while循环和do…while循环)以及转向控制语句(continue、break、goto)。

4.1 if( ){ }else{ }选择结构

C++语言的if分支语句有三种基本形式。

1)第一种形式为基本形式的if(表达式)语句

基本形式:

其语义是:如果表达式的值为真,则执行其后的语句,否则不执行该语句。例如,用户输入两个整数,当第一个比第二个大时,输出提示信息,否则什么都不输出。

运行结果:

if( ){ }语句比较简单,只有当if(表达式)成立时候才执行{ }中的代码,除此不会做任何事情。

2)第二种形式为if( ){ } else{ }形式

其语义是:如果表达式的值为真,则执行语句1,否则执行语句2。例如,比较两个整数,var_max为其中的大数,并用if-else语句判别exp_y, exp_z的大小,若exp_y大,则var_max = exp_y,否则输出var_max = exp_z。

输出结果:

输出结果比较简单,if( ){ }else{ }结构就是不管if( )是否成立,都会执行一种情况。这与上面程序执行情况一样,不管第一个数大还是第二个数字大,最终都会提示输出。

3)第三种形式为if( ){ } else if( ) else if( ){ } ……else{ }形式

前两种形式的if语句一般都用于两个分支的情况。当有多个分支选择时,可采用if( ){ } else if( ) else if( ){ } ……else{ }语句,其一般形式为:

其语义是:

依次判断表达式的值,当出现某个值为真时,则执行其对应的语句。然后跳到整个if语句之外继续执行程序。如果所有的表达式均为假,则执行语句n。然后继续执行后续程序。执行流程如图4.1所示。图4.1 if-else if-else执行流程

示例代码如下:

输出结果:

程序分析:

从输出结果可以知道,if( ){ }else if( ){ }else{ }判断是从上到下,一旦有成立的,则后面的不再执行。利用这个特殊功能可以实现数值的全空间判断,比如上面例子代码中就是这样,判断学生成员的等级,先从小到大进行排列,实现起来非常简洁。

使用if语句应注意以下问题:(1)在这三种形式的if语句中,在if关键字之后均为表达式。该表达式通常是逻辑表达式或关系表达式,但也可以是其他表达式,如赋值表达式等,甚至还可以是一个变量。例如:if(a=5)语句,但是需要注意,这样写实际上没有什么意义,因为if(a=5)是永远成立的;if(b)语句,也是允许的。只要表达式的值为非0,即为“真”。如在“if(a=5)…;”中,表达式的值永远为非0,所以其后的语句总是要执行的。(2)在if语句中,条件判断表达式必须用括号括起来,在语句之后必须加分号。(3)在if语句的这三种形式中,所有的语句应为单个语句,如果在满足条件时执行一组(多个)语句,则必须把这一组语句用{ }括起来组成一个复合语句。但是需要注意,在“}”之后不能再加分号。

4.2 switch结构

switch结构的基本形式:

当程序执行到switch( )语句时,首先查看开关表达式(只能是(unsigned) int,(unsigned) long或(unsigned) char型的值或表达式)的值。在下面的case语句中一直查看是否有该值存在:若找到,则执行相应的语句序列,然后执行break跳出,后面的case语句不再执行;如果没有,则执行default。这个分支结构语句中的break是有必要的,如果没有,则程序会在第一次匹配后,再继续往下运行,这样的效果不是我们需要的。下面通过示例代码来说明这一问题。

示例1:switch语句(没有break)

执行结果:

程序分析:

从输出结果很容易看出来,switch语句是从上往下执行,switch(开关表达式)一旦遇到匹配的case语句,并且没有遇到break语句,会把匹配处的case语句下面的代码都执行,而不管下面的case语句值是否和switch(开关表达式)的值相同。代码顺序:case 1,case 3,case 7,default,所以当输入1时,case 1匹配,只要没有遇到break会把其下面的代码都执行一次,所以输出:

当输入3时,前面的case 1不匹配,没有执行,而在case 3处匹配了,从而把case 3下边的代码都执行一次,输出:

当输入7时,前面的case 1,case3不匹配,没有执行,而在case 7处匹配了,从而把case 7下边的代码都执行一次,输出:

当输入2时,前面的case 1,case3,case 7不匹配,没有执行,default处匹配了,从而把default的代码都执行一次,输出:

上述这种代码结构在实际应用中是没有什么价值的,之所以列出来,是为了让读者更加深刻地认识switch语句,以及使用时不要忘记在case语句块中添加break语句。下面修改一下上面的例子程序,看一下正确的执行效果是怎么样的。

示例2:switch-case-break语句:

执行结果:

程序分析:本代码在上一程序的每个分支结构基础上增加了break语句,这样在switch(开关表达式)和case常量表达式匹配后,程序会立即退出switch( ){ }语句块,这样得到的结果就是我们需要的。

if( ){ }else if( ){ }…else{ }结构和switch结构的比较

switch只能进行相等或不等的判断,而if…else…可以进行大于、小于等范围上的判断;switch无法处理浮点数,一般只能进行整数的判断,而且case标签必须是常量,如果涉及浮点和变量的判断,应当使用if…else…;在switch和if…else…结构都适用的情况下,看看分支数目,如果分支数目大于3,推荐使用switch结构。

4.3 for循环结构

for循环结构需要重复执行某个动作,其基本形式:

执行过程:首先计算初始化表达式(循环初值),然后判断判断表达式(循环条件)是否为真:若为真,则进入循环执行语句块(循环体),并修改修正表达式(循环变量);否则退出循环。

示例代码:

运行结果:

程序分析:上述程序中,使用了3个for语句,但是两个for语句写法不相同,第一个for ( i = 0;i < 5; i++)语句中三个表达式都写了,代码执行5次循环;第二个for ( ;i < 5; )语句中只写了中间一个表达式,但是在for( )前面已经修改了i值:i=0,循环体中写了i++,所以效果也是一样的,循环执行了5次;第三个for ( ;; )语句中3个表达式都省略了,这样的结果是无限循环,但是可以在循环体修改循环终止条件,并且判断终止条件是否已经成立,比如程序中使用的代码片段:

使用了break来跳出for(;;){ }这个无限循环体。

4.4 while循环结构

前面学习了for的循环语句,用来实现一些重复的代码,C++中还提供了while( ){ }语句来实现循环语句,其效果和for实现的效果是相同的。

基本形式:

执行过程:当程序执行到while结构时,首先判断表达式是否为真,为真则执行语句块,否则跳出循环。

下面把上面的代码使用while结构来实现,示例程序如下:

输出结果:

程序分析:

代码中把for语句修改为使用while( ){ }语句实现,一样的效果。同样,while(1){ }会导致程序无限循环,如果有终止循环,使用的方法和for(;;){ }一样,在循环体中修改循环条件值,并使用break语句跳出while(1)语句。

4.5 do{ }while{ }循环结构

上面学习了while( ){ }结构的循环语句,这个循环结构特点是先判断循环条件是否成立,如果成立再执行循环体。do{ }while( )语句也是循环语句,但是其特点是先执行循环体,再判断循环体条件是否成立,如果成立则再次执行循环体,并如此循环。简单地说,do{ }while( )结构不管循环条件是否成立都会执行最少一次的循环体代码。

基本形式:

示例程序如下:

运行结果:

程序分析:

代码中最开始执行了

尽管while(0)表示表达式不成立,但是还是执行了一次do{ }循环体,输出i:0。接下来的代码分别使用了do{ }while( )和while( ){ }语句实现相同的效果。

4.6 break流程转向控制语句

break用于两种结构中:

1)switch语句的每一个case语句块,用于跳出switch语句。

2)提前从循环结构中跳出。当有while结构嵌套时,break只能往外跳一层。

由于break在前面的switch( ){ }语句的示例程序中已经使用了,此处仅演示从嵌套循环中跳出循环的特征,示例程序如下:

输出结果:

程序分析:

代码中使用了两层的while( ){ }语句,这种就属于嵌套循环语句,内层循环每次调用break语句时,只终止了本层循环体,不能终止外层循环体。其实外层循环、内层循环都可以使用前面学习过的三种循环结构语句:for( ){ }、while( ){ }、do{ }while( ),这几种结构可以混合着使用,这里不再举例,读者自行写代码测试。

4.7 continue流程转向控制语句

continue主要用于结束本次循环(注意:是结束本次,不是终止整个循环语句往下执行),继续下次循环(如下次没有循环条件成立还会再次执行循环体代码)。

示例程序如下:

运行结果:

程序分析:

代码中内层循环while (j++ < 4)表示会执行循环体4次,但是循环体中if (j == 2){ continue; }提前结束了本次循环体代码,从而输出结果中少了“这是内层循环 j:2”这句的输出。这也正是break和continue语句的区别。

4.8 goto流程转向控制语句

goto语句可以让程序员自由地将流程转向程序的任何地方,程序员只要在程序的某一行前加以标号,便可使用“goto 标号”的形式跳转到该标号处。在现代编程中,已经不再提倡使用goto语句,因为它破坏了程序的结构。但是它有一个好处:当要跳出多重循环时,它可以一次就跳转到外面,不用写多个break。

示例程序如下:

执行结果:

程序分析:

这个代码是用break语句的测试程序稍作修改,即把break修改为goto语句得到的程序。结果却和break测试大不相同,goto语句直接就跳出了两层循环,这也是在用于终止循环体时,两种语句的差异。

其实对goto语句的用法,存在许多争论,而且持反对意见的居多,就笔者认为,任何语句,存在就是合理的,看编程者如何使用。比如跳出多层循环就是一个很好的用法,而且在Linux系统源码中,驱动代码程序的编程可以随处看到goto语句的影子,但是并没有使代码难于阅读,反而层次分明。

4.9 exit( )程序终止函数

exit( )程序是C语言函数库stdlib.h中的一个函数(在新的C++标准中,头文件为cstdlib),它的功能是终止程序的执行,并在退出前对程序占用的资源进行必要的清理。exit( )程序也是一个无返回值的函数,其参数称为退出码,用来通知操作系统当前程序正常终止(一般为0)还是非正常终止(一般为-1)。

示例代码如下:

运行结果:

程序分析:

在程序的后面写了cout << "程序将不能运行到这里!" <<endl;语句,在输出结果上也没有确定不会执行到这里,所以程序是在执行exit(-1);时已经结束。注意,这就是exit( )程序和break语句的区别。

第5章 数组

C++中的数组和C语言的类似,但为了知识体系的完整性,这里简要介绍一下数组的用法。

5.1 数组的引入

C++中的数据类型除了在前面的章节中所介绍的基本类型(如整型、实型、字符型等)外,还有一种称为构造类型。数组就是构造类型的一种,它是由一组数目固定、类型相同的若干个数据构成的有序集合,并在内存中连续存放。数组中的每一个数据称为一个元素,通过数组名和下标来访问。那么为什么需要数组?在解决一些实际的问题时,往往需要面对成批的数据。例如,要求输入全班50个学生某一门课程的成绩,计算并输出全班学生的平均成绩,同时统计并输出低于平均成绩的人数。在解决这类问题时,如果仍然用基本数据类型来处理,由于在计算平均成绩时要用到每个学生的成绩,在统计低于平均成绩的学生人数时,也要用到每个学生的成绩,这样就必须定义50个变量来保存50个学生的成绩。这样无疑增加了程序的复杂度和代码量。因此需要引入数组,它是学生成绩的集合,可以用数组名和下标访问每一个学生的成绩。

5.2 一维数组

1.概念

一维数组也称为向量,用来组织具有一维顺序关系的一组同类型数据。形象地说,可以把一维数组看成一个一行多列的表格,每个单元格大小相同。2.一维数组定义格式

格式为:数组元素类型 数组名[常量表达式];

说明:数组元素类型可以是任何合法的数据类型,如int、float、char以及指针、结构体、共用体,以及自己定义的类类型;数组名和变量名一样,遵循变量命名规则;常量表达式表示数组元素的个数,必须在编译时能有一个确定的值,不能是一个数值可变化的变量。

例如一维整型数组int age[10],编译器将一次性开辟10个存放int*类型的连续的内存空间,也就是总共占用104字节连续空间(int在32位平台上占用4字节),就不用定义10个int类型的变量了。3.一维数组初始化

类型 数组名[常量表达式]={值1,值2,值3,...,值n},如int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}。4.一维数组访问方式

一维数组中元素的访问方式:数组名[下标]。下标的值表示元素在数组中的位置。C/C++语言规定任何数组的下标都是从0开始的。例如,int age[5]表示age有5个元素,这5个元素分别为age[0]、age[1]、age[2]、age[3]、age[4]。不要去访问age[5],因为会出现下标越界的错误。通常在针对数组进行操作时,最有效的途径是使用循环结构。5.注意事项

除了在定义数组时可以用初始化列表为数组整体赋值之外,其他地方不能对数组整体赋值。在对数组的所有元素赋初值时,可以在定义数组时不指定元素的个数。例如,int a[5] = {3, 0, 4, 9, 6}等价于int a[ ] = {3, 0, 4, 9, 6}。系统在编译程序时,根据初值的个数确定元素的个数,并为它分配相应大小的空间。如果没有为数组变量赋初值,则在定义数组变量时就不能省略数组的大小。例如,int a[ ],就是错误的。

对数组的部分元素赋初值,若常量表达式的个数小于数组中元素的个数,则未指定的数组元素自动变为0。例如,int a[5] = {3, 4, 5},等价于int a[5] = {3, 4, 5, 0, 0}。

示例如下:

运行结果:

输出结果比较简单,不再做分析,读者只要需要明白如何定义,如何访问数组元素就可以了。

5.3 二维数组

1.概念

二维数组的数据结构是一个二维表,相当于数学中的矩阵。形象地说,可以把二维数组看成一个有多行的表格,而每一行又可以有一列或多列(单元格),所分配的内存空间和一维数组一样是连续的。2.二维数组定义格式

格式为:数据类型 数组名[常量表达式1][常量表达式2]。例如二维数组:int a[3][4]表示3行4列的矩阵,总共12个元素,每一个元素都是整型数据。二维数组在内存中是按行的顺序存储的,即先存放第0行的各列数据,再存放第1行的,以此类推。3.二维数组初始化

示例:int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}或int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}。也可以对二维数组的部分元素进行初始化:int a[3][4] = {{2, 5}, {6}, {14, 0, 12}},等价于int a[3][4] = {{2, 5, 0, 0}, {6, 0, 0, 0}, {14, 0, 12, 0}}。还可以省略一维的大小:int a[2][3] = {3, 4, 5, 6, 8, 9},等价于int a[ ][3] = {3, 4, 5, 6, 8, 9};第二维的大小不能省略。4.二维数组使用

数组名[下标1][下标2],下标1为行下标,下标2为列下标。

示例如下:

运行结果:

从输出可以很容易地发现,二维数组的访问其实和一维数组访问差不多,其左边的下标表示行号,右边的下标表示列号,用一张表格来比喻,就是想定位一个单元格。即先定位目标单元格所在的行,再定位目标单元格所在的列,行和列交叉点处就能锁定一个单元格。

5.4 字符数组

1.字符数组概念

用来存放字符量的数组称为字符数组。

前面学习过整型数组,字符数组形式与前面介绍的数值数组相同。例如:

因为字符型和整型通用,也可以定义为int chr[10],但这时每个数组元素占2字节的内存单元,这样浪费了内存空间。

字符数组也可以是二维或多维数组。例如:

这样就定义了一个有5行,并且每行有10个字符类型元素的二维字符数组。2.字符数组的初始化

字符数组初始化方式有两种,一种是定义时候初始化,另一种是在代码运行时逐个初始化。

定义时初始化也存在两种方式,第一种是单个元素进行初始化,如下:

这种初始化方式,字符数组在定义时只对部分元素进行初始化,则未赋初值的元素默认的初值是‘\0’。这个结束标志对字符串来说有很重要的意义,像前面所认识的printf函数,如何知道字符串已经输出完成,就是依赖‘\0’做结束标志,下面尝试使用printf在屏幕上输出一个没有结束标志的字符数组内容,则很有可能会导致程序运行异常(不一定会,看当前运行的时候数组所在内存区域数据内容而不同)。

示例程序:

运行结果:

程序分析:

本例子使用的是VS2012测试,读者的测试结果不一定会和图上的相同,原因上面已经说过了。可以发现,输入结果中有乱码。原因很简单,printf函数会一直输出到遇到‘\0’,也就是遇到数值为0的内存单元才停止输出,而上面的定义char chr[5] = {'C','h','i','n','a'};中5个元素都已经初始化为非0值,而数组元素chr[4]后面的的相邻内存单元的数值也不是0,数值还可能是随机的,所以导致了输出乱码的结果。知道了原因,要解决问题就很容易了,只需要把char chr[5] = {'C','h','i','n','a'}修改为char chr[6] = {'C','h','i','n','a'},注意,把元素多添加一个,定义时候只初始化5个元素,没有初始化的默认是0,所以输出正常。

第二种初始化方式,在定义时把一个字符串直接赋值给数组,如下:

这种初始化方式,会在数组后面添加一个ASCII码‘\0’,其实际数值是0,表示字符串的结束标志。把上面的示例程序进行简单修改并测试,修改后的程序如下:

运行结果如下:

可以发现,数组chr[ ]占用的内存空间是6字节,并自动加了一个‘\0’做结束标志,运行输出非常正常。

在C语言中没有字符串变量的概念,通常是利用字符数组或者指针来存储字符串。编译器把每个字符串理解为一个以‘\0’(ASCII为0)为结束符的一维字符数组,这种类型的字符数组常被称为C风格字符串。3.字符数组的访问方式

字符数组的访问方式和前面学习的普通整型数组的访问方式相同,可以通过下标进行访问,在此不再举例。4.字符串的输入

1)使用cin输入字符串

cin使用空白(空格、制表符和换行符)来定字符串的界。这意味着cin在获取字符数组输入时只读取一个单词,在读取该单词后,cin将该字符串放入数组中,并自动在结尾添加空字符。这样,后一个字符串将不会输入到数组中。

当输入字符串长度比存放目标数组的长度大时,将会出现内存溢出问题。比如定义一个20个元素的数组,却输入了大于20个字符后再回车,就会出现程序运行异常,这是一个潜在的风险,通常不使用这种方式。

2)使用行读取函数getline( )

在C++程序中有两个getline函数,一个是在string头文件中,定义的一个全局的函数,函数声明是istream & getline ( istream & is, string & str, char delim )与istream & getline ( istream & is, string & str )。另一*个则是istream的成员函数,函数声明是istream & getline (char s, *streamsize n )与istream & getline (char s, streamsize n, char delim ),注意第二个getline是将读取的字符串存储在char数组中,而不可以将该参数声明为string类型,因为C++编译器无法执行此默认转换。(1)istream & getline ( istream & is , string & str , char delim );

将输入流is中读到的字符存入str中,直到遇到终结符delim才结束。对于第一个函数中的delim是可以由用户自己定义的终结符;对于第二个函数中的delim默认为‘\n\’(换行符)。

函数在输入流is中遇到文件结束符(EOF)或者在读入字符的过程中遇到错误都会结束。在遇到终结符delim后,delim会被丢弃,不存入str中。在下次读入操作时,将在delim的下个字符开始读入。(2)istream & getline ( istream & is, string & str )

将输入流is中读到的字符存入str中,遇到‘\n’就结束。*(3)istream & getline (char s, streamsize n, char delim )

这是成员类函数,作用是将输入流中读到的字符存入s中,最多读取n-1个字符,如果输入字符出现了终结符delim,则还没有到n-1个字符,也会提前结束输入。delim是可以由用户自己定义的终结符,delim默认为‘\n’(换行符)。在遇到终结符delim后,delim会被丢弃,不存入s中。在下次读入操作时,将在delim的下个字符开始读入。*(4)istream & getline (char s, streamsize n, char delim )

这是成员类函数,作用是将输入流中读到的字符存入s中,直到遇到‘\n’或文件结束符EOF。

示例程序:

输出结果:

程序分析:

程序中分别使用了4种方式从键盘读取字符串,测试的结果和前面讲解的特征完全相同。

3)使用get( )函数读取

get( )函数和getline函数一样,都有不同的版本,下面分别讲解。

① 单字符读取函数

int get( ):从输入流中读取单字符,把读取到的单字符以函数值的形式返回。

istream & get (char& c):从输入流中读取单个字符,保存到c中。

② C字符串读取*

istream & get (char s, streamsize n):从输入流中读取n-1个字符存放到s中,会自动添加上‘\n’做结束标志。*

istream & get (char s, streamsize n, char delim):从输入流中读取n-1个字符存放到s中,或当读取到delim时提前结束读取,最后会在字符串后面自动添加上‘\0’做结束标志。

示例程序如下:

运行结果:

程序分析:**

其中istream & get (char s, streamsize n);和istream & get (char s, *streamsize n, char delim);的效果和istream & getline (char s, *streamsize n )与istream & getline (char s, streamsize n, char delim )完全相同。

4)使用scanf( )函数读取

scanf( )函数是由C语言继承来的,不能输入包含空格的句子,并且输入格式需要和指定输入格式完全相同,否则会出错,得不到预期的效果,对于这个函数需要注意的地方是比较多的,下面将会举例说明容易出错的地方。

常规字符串读取示例程序:

运行结果:

下面逐个讲解scanf( )函数在使用过程中需要注意的地方。(1)当格式化字符串中用空白符结尾时,scanf会跳过空白符去读下一个字符,所以必须多输入一个数才能结束scanf函数。这里的空白符包括空格、制表符、换行符、回车符和换页符。所以用scanf("%d ",&a)也会出现同样的问题。

示例程序:

运行结果:

程序分析:

第一个scanf("%d\n",&chr[0])语句,从需求来说,只想要一个整型数据,但实际测试时,输入12再回车,却没有结束scanf( )函数,还要再输入一个数35(可以是任意数),这样才结束输入,原因前面已经说过。第二个scanf("%d\n%d",&chr[0],&chr[1])语句中也有‘\n’,但是不是在后面,所以运行时输入两个整数直接回车后就能正常工作。第三个scanf("%d\n%d ",&chr[0],&chr[1])语句,注意"%d\n%d "最后是有一个空格的,所在当输入两个数后,和第一个scanf语句一样不能结束输入,还得再输入一个数字后回车才能结束输入。(2)在连续读取时\n会留在缓冲区中,从而导致读取不正确的现象。

示例程序:

运行结果:

程序分析:

代码中for(int i = 0; i < 5; i++)scanf("%c",&chr[i]);表示是读入5个字符,但是,实际测试是输入a,b,c三个字符后就结束了scanf。是否是提前结束了?输入a和第一个回车后,a和这个回车符都留在缓冲区中。第一个scanf读取了a,但是输入缓冲区里面还留有一个‘\n’,第二个scanf读取这个‘\n’。然后输入b和第二个回车,同样的,第三个scanf读取了b,第四个scanf读取了第二个回车符。第五个读取了c。所以五个scanf都执行了,并没有提前结束,只是有的scanf读取到了回车符而已,从输入结果可以看出读取到的是‘\n’,运行结果如下:

在解决这个问题时,只要想办法去除缓冲中的回车符就可以了。把程序:

修改为:

实际上就是每次读取完一个字符后,使用cin.sync( )或fflush(stdin)同步缓冲区,这样可以清空存在的残留信息。修改后,后面的测试完全正常了。(3)scanf( )函数的参数输入类型不匹配。

scanf( )读取数据,类型必须相同,一旦遇到第一个类型不匹配的数据时,则会把后面的参数都忽略,但是类型匹配错误时的输入数据还是存留在缓冲区中,下次如有从相同流读取数据的代码时就会马上得到残留在缓冲区中的数据,而不用等待用户输入。这种错误是很致命的,在程序中必须避免。以下通过示例代码形式来说明。

示例程序:

运行结果:

程序分析:

程序中scanf("%d,%c",&var_b,&var_a)语句第一个参数%d是整型,但是测试时输入了字符a,在读取第一个输入时就已经不匹配了,所以后面的第二个参数%c直接忽略了,马上输出,这个输入语句后面代码是scanf("%c",&var_c),按照正常执行来说,应该等待用户输入一个字符才结束输入,但是却没有等待用户输入,直接往下执行,输出的结果是字符a,这个a刚刚好是前面输入的。这告诉我们一个信息,如果输入数据和当前scanf参数不匹配时,则输入的数据还会留在缓冲区中,所以当执行到scanf("%c",&var_c)时,直接从缓冲中读取到上次输入的a,从而出现了没有等待用户输入的现象。

要修正这个问题,可以像上一个例子一样,在发生错误时同步一次缓冲区,就可以了。那如何知道错误了?可以利用scanf函数返回值,其返回值是scanf函数成功读取的参数个数,用这个返回值和期望参数个数作比较,就可以知道scanf函数是否执行正确的输入。

5)使用gets( )函数读取数据

使用gets( )函数,能输入完整的句子,弥补了scanf函数不能输入包含空格的句子的不足。当输入一行字符时,可以用Enter键作为结束符。在向字符数组赋值时,自动将‘\n’转化为‘\0’,作为字符串的结束标志。例如char str[30]; gets(str),当输入“How are you”时,可以接收全部。但是要注意,输入字符串不要超过程序中定义的最大长度,否则程序将运行异常。

示例程序:

运行结果:

程序分析:

输入“acsdv sd1213 edrfdf4443 dfbdf”有空格,也有Tab键,回车后把读取的数据输出,和输入的内容完全相同。5.字符串的输出

标准输出流cout:依次输出字符串中的每个字符,直到遇到字符串结束符‘\0’。若标准输出流输出项的字符数组中不止一个‘\0’,则输出时遇到第一个‘\0’就结束。输出字符串后不会自动换行。

printf( )函数:依次输出字符串中的每个字符,直到遇到字符串结束符‘\0’。若printf( )函数输出项的字符数组中不止一个‘\0’,则输出时遇到第一个‘\0’就结束。输出字符串后不会自动换行。

puts( )函数:将一个字符串(以‘\0’结束的字符序列)输出到终端。在输出时,将字符串的结束符‘\0’自动转化为‘\n’,即输出完字符串之后自动换行。

示例程序:

输出结果:

程序分析:

比较简单,唯一要强调的是puts(chr)函数最后输出完要自动换行,其他的两种不自动会换行。

第6章 C++函数

函数是完成一定功能的代码片段,通常把可以重复利用的功能性代码写成函数,这样有利于代码的重用,以及便于后期对代码的维护。

函数的使用包含3个重要部分:函数声明、函数定义和函数调用。

6.1 函数的定义和使用

函数在C/C++程序设备中占据着非常重要的地位,标准C/C++库提供了很多编程常用得到的功能函数。比如printf函数,这个函数在C程序使用频率是非常高的,其功能就是向屏幕格式化输出一个字符串。除标准C/C++库提供的功能函数外,程序员在编程过程中也需要设计自己的功能函数。毕竟,标准库函数提供的都只是一些通用性比较强的函数,在具体软件设计中,需要根据自己的软件设计功能。

下面用一个简单的函数示例代码作为学习函数的入门程序。

示例程序:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载