第一篇:程序员实验室 - C 基础教程
C++ 基础教程Beta 版
原作:Juan Soulié 翻译:Jing Xu(aqua)英文原版
本教程根据Juan Soulie的英文版C++教程翻译并改编。本版为最新校对版,尚未定稿。如有不明或错误之处,请参考英文原版,并敬请在本站留言指正。版权归作者所有,欢迎链接,请勿转载。
本教程对C++语言进行了深入浅出的介绍,从基础知识到ANSI-C++标准的最新功能,内容涵盖了从数组,类等基本概念到多态、模板等高级概念。教程本着实用的原则,每一小节都结合了可以工作的程序实例,以便读者从第一课开始就可以上手实习。
本翻译版本对许多C++概念中的关键词保留了中英文对照,以便读者增强理解,并方便日后阅读英文原版教材 目录
1.简介
怎样使用本教程 2.C++基础 Basics of C++ 1.C++程序结构
Structure of a program 2.变量和数据类型
Variables and Data types 3.常量 Constants 4.操作符/运算符 Operators 5.控制台交互
Communication through console 3.控制结构和函数
Control structures and Functions 1.控制结构
Control Structures 2.函数I Functions I 3.函数II Functions II 4.高级数据类型 Advanced Data 1.数组 Arrays 2.字符序列
Character Sequences 3.指针 Pointers 4.动态内存分配 Dynamic memory 5.数据结构 Data Structures 6.自定义数据类型
User defined data types 5.面向对象编程
Object-oriented Programming 1.类,构造函数和析构函数,类的指针
Classes.Constructors and Destructors.Pointers to classes.2.操作符重载,this,静态成员
Overloading Operators.this.Static members 3.类之间的关系
Relationships between classes: friend.Inheritance 4.虚拟成员,抽象,多态
Virtual Members.Abstraction.Polymorphism 6.C++高级
Advanced concepts 1.模板 Templates 2.名空间 Namespaces 3.出错处理
Exception handling 4.类型转换高级
Advacned Class Type-casting 5.预处理指令
Preprocessor Directives 7.C++ 标准函数库 C++ Standard Library 1.文件的输入输出
Input/Output with files C++基础教程简介 怎样使用本教程 读者范围
本教程面向所有希望学习C++语言的读者。如果读者有其他编程语言背景或计算机相关基本知识可以帮助更好的理解教程内容,但这并非必须条件。
对于C语言熟悉的读者可将前三章(1.1 到 3.4)当作复习,因为这部分内容主要介绍C++中的C部分。不过某些C++的语法与C还是有些差别,所以建议还是快速的读一下这部分。第四章讲述面向对象编程。
第五章主要介绍ANSI-C++标准中的新增的功能。
本教程结构
教程共分6章,每章分若干小节。你可以直接从主目录进入任意小节,并循每页底部的链接向后浏览。
很多小节含有一页例题介绍该章节主要知识点的使用。建议在进入下一章学习之前最好先阅读这些例题,理解每行代码。
学习和练习一种编程语言的最好办法是自己修改书中例题程序,设法在程序中增加新的功能。不要不敢修改这些例题程序,这正是学习的方法。
兼容性备注
ANSI-C++标准近几年来被接受为国际标准。尽管C++语言从二十世纪80年代即存在,ANSI-C++在1997年才被发表,2003年又被修订过。因此很多编译器不支持ANSI-C++中的部分新功能,特别是那些在此标准发表前即被发布的编译器。
在本教程中,那些ANSI-C++中新增的而老一代C++编译器大多不支持概念将备用如下标志标出:
ANSI C++新增的概念
同样对于C和C++在实现上有明显不同的概念,将备用如下标志标出: C 与 C++不同的地方 编译器
本教程中所有例题程序均为console程序(控制台程序)。此类程序以文本形式与用户交换信息,显示结果。
所有C++编译器均支持console程序的编译。要了解更多关于如何编译的说明,请查询你的编译器用户使用手册。C++编译器和开发环境推荐
很多读者询问编译器和开发环境的问题。除了常用的商用收费的MS Visual Studio, VC++,Borland C++等工具外,还有很多免费的工具也是很好用的。这里推荐两种免费的C++开发软件:
1、Eclipse的CDT开发工具,官方网站在http://www.xiexiebang.com/cdt/
2、开源工具Dev-C++和wxDev-C++ 第一章 C++ 基础知识(Basics of C++)1.C++程序结构
Structure of a program 2.变量和数据类型
Variables and Data types 3.常量 Constants 4.操作符/运算符 Operators 5.控制台交互
Communication through console
1.1 C++程序结构(Structure of a program)
下面我们从一个最简单的程序入手看一个C++程序的组成结构。// my first program in C++ #include
int main(){ cout << “Hello World!”;return 0;} Hello World!上面左侧显示了我们的第一个程序的源代码,代码文件名称为hellowworld.cpp。右边显示了程序被编译执行后的输出结果。编辑和编译一个程序的方法取决于你用的是什么编译器,根据它是否有图形化的界面及版本的不同,编译方法也有可能不同,具体请参照你所使用的编译器的使用说明。
以上程序是多数初学者学会写的第一个程序,它的运行结果是在屏幕上打出”Hello World!”这句话。虽然它可能是C++可写出的最简单的程序之一,但其中已经包含了每一个C++程序的基本组成结构。下面我们就逐个分析其组成结构的每一部分: // my first program in C++ 这是注释行。所有以两个斜线符号(//)开始的程序行都被认为是注释行,这些注释行是程序员写在程序源代码内,用来对程序作简单解释或描述的,对程序本身的运行不会产生影响。在本例中,这行注释对本程序是什么做了一个简要的描述。# include < iostream.h > 以#标志开始的句子是预处理器的指示语句。它们不是可执行代码,只是对编译器作出指示。在本例中这个句子# include < iostream.h > 告诉编译器的预处理器将输入输出流的标准头文件(iostream.h)包括在本程序中。这个头文件包括了C++中定义的基本标准输入-输出程序库的声明。此处它被包括进来是因为在本程序的后面部分中将用到它的功能。using namespace std;C++标准函数库的所有元素都被声明在一个名空间中,这就是std名空间。因此为了能够访问它的功能,我们用这条语句来表达我们将使用标准名空间中定义的元素。这条语句在使用标准函数库的C++程序中频繁出现,本教程中大部分代码例子中也将用到它。int main()这一行为主函数(main function)的起始声明。main function是所有C++程序的运行的起始点。不管它是在代码的开头,结尾还是中间 – 此函数中的代码总是在程序开始运行时第一个被执行。并且,由于同样的原因,所有C++程序都必须有一个main function。main 后面跟了一对圆括号(),表示它是一个函数。C++中所有函数都跟有一对圆括号(),括号中可以有一些输入参数。如例题中显示,主函数(main function)的内容紧跟在它的声明之后,由花括号({})括起来。cout << “Hellow World!”;这个语句在本程序中最重要。cout 是C++中的标准输出流(通常为控制台,即屏幕),这句话把一串字符串(本例中为”Hello World”)插入输出流(控制台输出)中。cout 在的声明在头文件iostream.h中,所以要想使用cout 必须将该头文件包括在程序开始处。注意这个句子以分号(;)结尾。分号标示了一个语句的结束,C++的每一个语句都必须以分号结尾。(C++ 程序员最常犯的错误之一就是忘记在语句末尾写上分号)。return 0;返回语句(return)引起主函数 main()执行结束,并将该语句后面所跟代码(在本例中为0)返回。这是在程序执行没有出现任何错误的情况下最常见的程序结束方式。在后面的例子中你会看到所有C++程序都以类似的语句结束。
你可能注意到并不是程序中的所有的行都会被执行。程序中可以有注释行(以//开头),有编译器预处理器的指示行(以#开头),然后有函数的声明(本例中main函数),最后是程序语句(例如调用cout <<),最后这些语句行全部被括在主函数的花括号({})内。本例中程序被写在不同的行中以方便阅读。其实这并不是必须的。例如,以下程序 int main(){ cout << “ Hello World ”;return 0;} 也可以被写成:
int main(){ cout << “ Hello World ”;return 0;} 以上两段程序是完全相同的。
在C++中,语句的分隔是以分号(;)为分隔符的。分行写代码只是为了更方便人阅读。以下程序包含更多的语句: // my second program in C++ #include
int main(){ cout << “Hello World!”;cout << “I'm a C++ program”;return 0;} Hello World!I'm a C++ program 在这个例子中,我们在两个不同的语句中调用了cout << 函数两次。再一次说明分行写程序代码只是为了我们阅读方便,因为这个main 函数也可以被写为以下形式而没有任何问题: int main(){ cout << “ Hello World!”;cout << “ I'm to C++ program ”;return 0;}
为方便起见,我们也可以把代码分为更多的行来写: int main(){ cout << “Hello World!”;cout << “I'm a C++ program”;return 0;} 它的运行结果将和上面的例子完全一样。
这个规则对预处理器指示行(以#号开始的行)并不适用,因为它们并不是真正的语句。它们由预处理器读取并忽略,并不会生成任何代码。因此他们每一个必须单独成行,末尾不需要分号(;)
注释(Comments)注释(comments)是源代码的一部分,但它们会被编译器忽略。它们不会生成任何执行代码。使用注释的目的只是使程序员可以在源程序中插入一些说明解释性的内容。C++ 支持两中插入注释的方法: // line comment /* block comment */ 第一种方法为行注释,它告诉编译器忽略从//开始至本行结束的任何内容。第二种为块注释(段注释),告诉编译器忽略在/*符号和*/符号之间的所有内容,可能包含多行内容。在以下我们的第二个程序中,我们插入了更多的注释。/* my second program in C++ with more comments */
#include
int main(){ cout << “Hello World!”;// says Hello World!cout << “I'm a C++ program”;// says I'm a C++ program return 0;} Hello World!I'm a C++ program
如果你在源程序中插入了注释而没有用//符号或/*和*/符号,编译器会把它们当成C++的语句,那么在编译时就会出现一个或多个错误信息。1.2 变量和数据类型(Variables and Data types)
你可能觉得这个“Hellow World”程序用处不大。我们写了好几行代码,编译,然后执行生成的程序只是为了在屏幕上看到一句话。的确,我们直接在屏幕上打出这句话会更快。但是编程并不仅限于在屏幕上打出文字这么简单的工作。为了能够进一步写出可以执行更有用的任务的程序,我们需要引入变量(variable)这个的概念。
让我们设想这样一个例子,我要求你在脑子里记住5这个数字,然后再记住2这个数字。你已经存储了两个数值在你的记忆里。现在我要求你在我说的第一个数值上加1,你应该保留6(即5+1)和2在你的记忆里。现在如果我们将两数相减可以得到结果4。
所有这些你在脑子里做的事情与计算机用两个变量可以做的事情非常相似。同样的处理过程用C++来表示可以写成下面一段代码: a = 5;b = 2;a = a + 1;result = a38(7 个数字(7digits))double 8 双精度浮点数(double precision floating point number)1.7e + /308(15 digits)bool 1 布尔Boolean值。它只能是真(true)或假(false)两值之一。true 或 false wchar_t 2 宽字符(Wide character)。这是为存储两字节(2 bytes)长的国际字符而设计的类型。一个宽字符(1 wide characters)
* 字节数一列和范围一列可能根据程序编译和运行的系统不同而有所不同。这里列出的数值是多数32位系统的常用数据。对于其他系统,通常的说法是整型(int)具有根据系统结构建议的自然长度(即一个字one word的长度),而4中整型数据char, short, int, long的长度必须是递增的,也就是说按顺序每一类型必须大于等于其前面一个类型的长度。同样的规则也适用于浮点数类型float, double和 long double,也是按递增顺序。
除以上列出的基本数据类型外,还有指针(pointer)和void 参数表示类型,我们将在后面看到。
变量的声明(Declaration of variables)
在C++中要使用一个变量必须先声明(declare)该变量的数据类型。声明一个新变量的语法是写出数据类型标识符(例如int, short, float...)后面跟一个有效的变量标识名称。例如: int a;float mynumber;以上两个均为有效的变量声明(variable declaration)。第一个声明一个标识为a 的整型变量(int variable),第二个声明一个标识为mynumber 的浮点型变量(float variable)。声明之后,我们就可以在后面的程序中使用变量a和 mynumber 了。
如果你需要声明多个同一类型的变量,你可以将它们缩写在同一行声明中,在标识之间用逗号(comma)分隔。例如: int a, b, c;以上语句同时定义了a、b、c 3个整型变量,它与下面的写法完全等同: int a;int b;int c;整型数据类型(char, short, long 和 int)可以是有符号的(signed)或无符号的(unsigned),这取决于我们需要表示的数据范围。有符号类型(signed)可以表示正数和负数,而无符号类型(unsigned)只能表示正数和0。在定义一个整型数据变量时可以在数据类型前面加关键字 signed 或 unsigned 来声明数据的符号类型。例如: unsigned short NumberOfSons;signed int MyAccountBalance;如果我们没有特别写出signed或 unsigned,变量默认为signed,因此以上第二个声明我们也可以写成:
int MyAccountBalance;因为以上两种表示方式意义完全一样,因此我们在源程序通常省略关键字signed。
唯一的例外是字符型(char)变量,这种变量独立存在,与signed char 和 unsigned char型均不相同。
short 和 long 可以被单独用来表示整型基本数据类型,short 相当于 short int,long 相当于 long int。也就是说 short year;和 short int year;两种声明是等价的。
最后,signed 和 unsigned 也可以被单独用来表示简单类型,意思分别同signed int 和 unsigned int 相同,即以下两种声明互相等同: unsigned MyBirthYear;unsigned int MyBirthYear;下面我们就用C++代码来解决在这一节开头提到的记忆问题,来看一下变量定义是如何在程序中起作用的。
// operating with variables
#include
int main(){ // declaring variables: int a, b;int result;
// process: a = 5;b = 2;a = a + 1;result = ab;cout << result;
return 0;} 6
字符串(strings)字符串是用来存储一个以上字符的非数字值的变量。
C++提供一个string类来支持字符串的操作,它不是一个基本的数据类型,但是在一般的使用中与基本数据类型非常相似。
与普通数据类型不同的一点是,要想声明和使用字符串类型的变量,需要引用头文件
int main(){ string mystring = “This is a string”;cout << mystring;return 0;} This is a string 如上面例子所示,字符串变量可以被初始化为任何字符串值,就像数字类型变量可以被初始化为任何数字值一样。
以下两种初始化格式对字符串变量都是可以使用的: string mystring = “This is a string”;string mystring(“This is a string”);字符串变量还可以进行其他与基本数据类型变量一样的操作,比如声明的时候不指定初始值,和在运行过程中被重新赋值。// C++字符串例题2 #include
int main(){ string mystring;mystring = “This is the initial string content”;cout << mystring << endl;mystring = “This is a different string content”;cout << mystring << endl;return 0;} This is the initial string content This is a different string content 要了解更加详细的C++字符串操作,建议参考Cplusplus上的string类reference。1.3 常量(Constants)
一个常量(constant)是一个有固定值的表达式。字(Literals)
字是用来在程序源码中表达特定的值。在前面的内容中我们已经用了很多的字来给变量赋予特定的值。例如: a = 5;
这句代码中5就是一个字常量。
字常量(literal constant)可以被分为整数(Integer Numbers), 浮点数(Floating-Point Numbers),字符(Characters)和字符串(Strings)。
整数(Integer Numbers)1776 707-273 他们是整型常数,表示十进制整数值。注意表示整型常数时我们不需要些引号(quotes(“))或任何特殊字符。毫无疑问它是个常量:任何时候当我们在程序中写1776,我们指的就是1776这个数值。
除十进制整数另外,C++还允许使用八进制(octal numbers)和十六进制(hexadecimal numbers)的字常量(literal constants)。如果我们想要表示一个八进制数,我们必须在它前面加上一个0字符(zero character),而表示十六进制数我们需要在它前面加字符0x(zero, x)。例如以下字常量(literal constants)互相等值: 75 // 十进制 decimal 0113 // 八进制 octal 0x4b // 十六进制 hexadecimal 所有这些都表示同一个整数: 75(seventy five),分别以十进制数,八进制数和十六进制数表示。
像变量一样,常量也是有数据类型的。默认的整数字常量的类型为int型。我们可以通过在后面加字母u或l来迫使它为无符号(unsigned)的类型或长整型(long)。75 // int 75u // unsigned int 75l // long 75ul // unsigned long
这里后缀u和l可以是大写,也可以是小写。
浮点数(Floating Point Numbers)浮点数以小数(decimals)和/或指数幂(exponents)的形式表示。它们可以包括一个小数点,一个e字符(表示”by ten at the Xth height“,这里X是后面跟的整数值),或两者都包括。
3.14159 // 3.14159 6.02e23 // 6.02 x 10^1023 1.6e-19 // 1.6 x 10^-19 3.0 // 3.0 以上是包含小数的以C++表示的4个有效数值。第一个是PI,第二个是Avogadro数之一,第三个是一个电子(electron)的电量(electric charge)(一个极小的数值)– 所有这些都是近似值。最后一个是浮点数字常量表示数3。
浮点数的默认数据类型为double。如果你想使用float或long double类型,可以在后面加f或l后缀,同样大小写都可以: 3.14159L // long double 6.02e23f // float
字符和字符串(Characters and strings)此外还有非数字常量,例如: 'z' 'p' ”Hello world“ ”How do you do?“ 前两个表达式表示单独的字符(character),后面两个表示由若干字符组成的字符串(string)。注意在表示单独字符的时候,我们用单引号(single quotes(')),在表示字符串或多于一个字符的时候我们用双引号(double quotes(”))。
当以常量方式表示单个字符和字符串时,必须写上引号以便把他们和可能的变量标识或保留字区分开,注意以下例子: x 'x' x 指一个变量名称为 x, 而'x'指字符常量'x'。字符常量和字符串常量各有特点,例如escape codes,这些是除此之外无法在源程序中表示的特殊的字符,例如换行符 newline(n)或跳跃符tab(t)。所有这些符号前面要加一个反斜杠inverted slash()。这里列出了这些escape codes: n 换行符newline r 回车carriage return t 跳跃符tabulation v 垂直跳跃vertical tabulation b backspace f page feed a 警告alert(beep)' 单引号single quotes(')“ 双引号double quotes(”)? 问号question(?)反斜杠inverted slash()例如: 'n' 't' “Left t Right” “onentwonthree” 另外你可以数字ASCII 码表示一个字符,这种表示方式是在反斜杠()之后加以8进制数或十六进制数表示的ASCII 码。在第一种(八进制octal)表示中,数字必需紧跟反斜杠(例如23或40),第二种(十六进制hexacedimal),必须在数字之前写一个x字符(例如x20或x4A)。
如果每一行代码以反斜杠inverted slash()结束,字符串常量可以分多行代码表示: “string expressed in two lines” 你还可以将多个被空格blankspace、跳跃符tabulator、换行符newline或其他有效空白符号分隔开的字符串常量连接在一起:
“we form” “a single” “string” “of characters” 最后如果我们想让字符串使用宽字符(wchar_t),而不是窄字符(char),可以在常量的前面加前缀L:
L“This is a wide character string” 宽字符通常用来存储非英语字符,比如中文字符,一个字符占两个字节。布尔型常量(Boolean Literals)布尔型只有两个有效的值:true和false,其数据类型为bool。
定义常量Defined constants(#define)使用预处理器指令#define,你可以将那些你经常使用的常量定义为你自己取的名字而不需要借助于变量。它的格式是: #define identifier value 例如:
#define PI 3.14159265 #define NEWLINE 'n' #define WIDTH 100 以上定义了三个常量。一旦做了这些声明,你可以在后面的程序中使用这些常量,就像使用其它任何常量一样,例如: circle = 2 * PI * r;cout << NEWLINE;实际上编译器在遇到#define指令的时候做的只是把任何出现这些 常量名(在前面的例子中为PI, NEWLINE或WIDTH)的地方替换成他们被定义为的代码(分别为3.14159265, 'n'和100)。因此,由#define定义的常量被称为宏常量macro constants。
#define 指令不是代码语句,它是预处理器指令,因此指令行末尾不需要加分号semicolon(;)。如果你在宏定义行末尾加了分号(;),当预处理器在程序中做常量替换的时候,分号也会被加到被替换的行中,这样可能导致错误。
声明常量declared constants(const)通过使用const前缀,你可以定义指定类型的常量,就像定义一个变量一样: const int width = 100;const char tab = 't';const zip = 12440;如果没有指定类型(如上面最后例子中最后一行),编译器会假设常量为整型int。1.4 操作符/运算符(Operators)
前面已经学习了变量和常量,我们可以开始对它们进行操作,这就要用到C++的操作符。有些语言,很多操作符都是一些关键字,比如add, equals等等。C++的操作符主要是由符号组成的。这些符号不在字母表中,但是在所有键盘上都可以找到。这个特点使得C++程序更简洁,也更国际化。运算符是C++语言的基础,所以非常重要。
你不需要背下所有这一小节的内容,这些细节知识仅供你以后需要时参考。赋值Assignation(=)赋值运算符的功能是将一个值赋给一个变量。a = 5;将整数5赋给变量a。= 运算符左边的部分叫做lvalue(left value),右边的部分叫做rvalue(right value)。lvalue 必须是一个变量,而右边的部分可以是一个常量,一个变量,一个运算(operation)的结果或是前面几项的任意组合。
有必要强调赋值运算符永远是将右边的值赋给左边,永远不会反过来。a = b;将变量b(rvalue)的值赋给变量a(lvalue),不论a当时存储的是什么值。同时考虑到我们只是将b的数值赋给a,以后如果b的值改变了并不会影响到a的值.例如:如果我们使用以下代码(变量值的变化显示在绿色注释部分): // 赋值符号例子
#include
int main(){ int a, b;// a:?, b:? a = 10;// a:10, b:? b = 4;// a:10, b:4 a = b;// a:4, b:4 b = 7;// a:4, b:7
cout << “a:”;cout << a;cout << “ b:”;cout << b;
return 0;} a:4 b:7 以上代码结果是a的值为4,b的值为7。最后一行中b的值被改变并不会影响到a,虽然在此之前我们声明了a = b;(从右到左规则right-to-left rule)。
C++拥有而其他语言没有的一个特性是赋值符(=)可以被用作另一个赋值符的rvalue(或rvalue的一部分)。例如: a = 2 +(b = 5);等同于: b = 5;a = 2 + b;它的意思是:先将5赋给变量b,然后把前面对b的赋值运算的结果(即5)加上2再赋给变量a,这样最后a中的值为7。因此,下面的表达式在C++中也是正确的: a = b = c = 5;//将5同时赋给3个变量a, b和c。
数学运算符Arithmetic operators(+,-, *, /, %)C++语言支持的5种数学运算符为: ? + 加addition ?5;a /= b;等同于 a = a / b;price *= units + 1;等同于 price = price *(units + 1);其他运算符以此类推。例如: // 组合运算符例子
#include
int main(){ int a, b=3;a = b;a+=2;// 相当于 a=a+2 cout << a;return 0;} 5
递增和递减Increase and decrease 书写简练的另一个例子是递增(increase)运算符(++)和递减(decrease)运算符(--)。它们使得变量中存储的值加1或减1。它们分别等同于+=1和-=1。因此: a++;a+=1;a=a+1;在功能上全部等同,即全部使得变量a的值加1。
它的存在是因为最早的C编译器将以上三种表达式的编译成不同的机器代码,不同的机器代码运行速度不一样。现在,编译器已经基本自动实行代码优化,所以以上三种不同的表达方式编译成的机器代码在实际运行上已基本相同。
这个运算符的一个特点是它既可以被用作prefix前缀,也可以被用作后缀suffix,也就是说它既可以被写在变量标识的前面(++a),也可以被写在后面(a++)。虽然在简单表达式如a++或++a中,这两种写法代表同样的意思,但当递增increase或递减decrease的运算结果被直接用在其它的运算式中时,它们就代表非常不同的意思了:当递增运算符被用作前缀prefix(++a)时,变量a的值线增加,然后再计算整个表达式的值,因此增加后的值被用在了表达式的计算中;当它被用作后缀suffix(a++)时,变量a的值在表达式计算后才增加,因此a在增加前所存储的值被用在了表达式的计算中。注意以下两个例子的不同: 例 1 例 2 B=3;A=++B;// A 的值为 4, B 的值为 4 B=3;A=B++;// A 的值为 3, B 的值为 4 在第一个例子中,B在它的值被赋给A之前增加1。而在第二个例子中B原来的值3被赋给 A然后B的值才加1变为4。
关系运算符Relational operators(==,!=, >, <, >=, <=)我们用关系运算符来比较两个表达式。如ANSI-C++ 标准中指出的,关系预算的结果是一个bool值,根据运算结果的不同,它的值只能是真true或false。
例如我们想通过比较两个表达式来看它们是否相等或一个值是否比另一个的值大。以下为C++的关系运算符: == 相等Equal!= 不等Different > 大于Greater than < 小于Less than >= 大于等于Greater or equal than <= 小于等于Less or equal than 下面你可以看到一些实际的例子:(7 == 5)将返回false.(5 > 4)将返回true.(3!= 2)将返回true.(6 >= 6)将返回true.(5 < 5)将返回false.当然,除了使用数字常量,我们也可以使用任何有效表达式,包括变量。假设有a=2, b=3和c=6,(a == 5)将返回false.(a*b >= c)将返回true 因为它实际是(2*3 >= 6)
(b+4 > a*c)将返回false因为它实际是(3+4 > 2*6)
((b=2)== a)将返回true.注意:运算符=(单个等号)不同于运算符==(双等号)。第一个是赋值运算符(将等号右边的表达式值赋给左边的变量);第二个(==)是一个判断等于的关系运算符,用来判断运算符两边的表达式是否相等。因此在上面例子中最后一个表达式((b=2)== a),我们首先将数值2赋给变量b,然后把它和变量a进行比较。因为变量a中存储的也是数值2,所以整个运算的结果为true。
在ANSI-C++标准出现之前的许多编译器中,就像C语言中,关系运算并不返回值为真true或假false的bool值,而是返回一个整型数值最为结果,它的数值可以为0,代表“false”或一个非0数值(通常为1)来代表“true”。
逻辑运算符Logic operators(!, &&, ||)运算符!等同于boolean 运算NOT(取非),它只有一个操作数(operand),写在它的右边。它做的唯一工作就是取该操作数的反面值,也就是说如果操作数值为真true,那么运算后值变为假false,如果操作数值为假false,则运算结果为真true。它就好像是说取与操作数相反的值。例如:
!(5 == 5)返回false,因为它右边的表达式(5 == 5)为真true.!(6 <= 4)返回true因为(6 <= 4)为假false.!true 返回假false.!false 返回真true.逻辑运算符&&和||是用来计算两个表达式而获得一个结果值。它们分别对应逻辑运算中的与运算AND 和或运算OR。它们的运算结果取决于两个操作数(operand)的关系: 第一个操作数 a 第二个操作数 b 结果 a && b 结果 a || b true true true true true false false true false true false true false false false false 例如 :
((5 == 5)&&(3 > 6))返回false(true && false).((5 == 5)||(3 > 6))返回true(true || false).条件运算符Conditional operator(?)条件运算符计算一个表达式的值并根据表达式的计算结果为真true或假false而返回不同值。它的格式是:
condition ? result1 : result2(条件?返回值1:返回值2)
如果条件condition 为真true,整个表达式将返回esult1,否则将返回result2。7==5 ? 4 : 3 返回3,因为7不等于5.7==5+2 ? 4 : 3 返回4,因为7等于5+2.5>3 ? a : b 返回a,因为5大于3.a>b ? a : b 返回较大值,a 或b.// 条件运算符例子
#include
a=2;b=7;c =(a>b)? a : b;
cout << c;
return 0;} 7 上面的例子中a的值为2,b的值为7,所以表达式(a>b)运算值为假(false),所以整个表达式(a>b)?a:b要取分号后面的值,也就是b的值7。因此最后输出 c 的值为7。
逗号运算符(,)逗号运算符(,)用来分开多个表达式,并只取最右边的表达式的值返回。
例如有以下代码: a =(b=3, b+2);
这行代码首先将3赋值给变量b,然后将 b+2 赋值给变量 a。所以最后变量a 的值为5,而变量b的值为3。
位运算符Bitwise Operators(&, |, ^, ~, <<, >>)位运算符以比特位改写变量存储的数值,也就是改写变量值的二进制表示: op asm Description & AND 逻辑与 Logic AND | OR 逻辑或Logic OR ^ XOR 逻辑异或Logical exclusive OR ~ NOT 对1取补(位反转)Complement to one(bit inversion)<< SHL 左移Shift Left >> SHR 右移Shift Right
变量类型转换运算符Explicit type casting operators 变量类型转换运算符可以将一种类型的数据转换为另一种类型的数据。在写C++中有几种方法可以实现这种操作,最常用的一种,也是与C兼容的一种,是在原转换的表达式前面加用括号()括起的新数据类型: int i;float f = 3.14;i =(int)f;以上代码将浮点型数字3.14转换成一个整数值(3)。这里类型转换操作符为(int)。在C++中实现这一操作的另一种方法是使用构造函数constructor 的形式:在要转换的表达式前加变量类型并将表达式括在括号中: i = int(f);以上两种类型转换的方法在C++中都是合法的。另外ANSI-C++针对面向对象编程(object oriented programming)增加了新的类型转换操作符(参考Section 5.4, Advanced class type-casting).sizeof()这个运算符接受一个输入参数,该参数可以是一个变量类型或一个变量自己,返回该变量类型(variable type)或对象(object)所占的字节数: a = sizeof(char);这将会返回1给a,因为char是一个常为1个字节的变量类型。
sizeof返回的值是一个常数,因此它总是在程序执行前就被固定了。
其它运算符
在本教程后面的章节里我们将看到更多的运算符,比如指向指针的运算或面向对象编程特有的运算,等等,我们会在它们各自的章节里进行详细讨论。
运算符的优先度 Precedence of operators 当多个操作数组成复杂的表达式时,我们可能会疑惑哪个运算先被计算,哪个后被计算。例如以下表达式: a = 5 + 7 % 2 我们可以怀疑它实际上表示:
a = 5 +(7 % 2)结果为6,还是 a =(5 + 7)% 2 结果为0? 正确答案为第一个,结果为6。每一个运算符有一个固定的优先级,不仅对数学运算符(我们可能在学习数学的时候已经很了解它们的优先顺序了),所有在C++中出现的运算符都有优先级。从最从最高级到最低级,运算的优先级按下表排列: 优先级
Level 操作符 Operator 说明 Description 结合方向 Grouping 1 :: 范围 从左到右()[].-> ++--dynamic_cast static_cast reinterpret_cast const_cast typeid 后缀 从左到右 ++--~!sizeof new delete 一元(前缀)从右到左
* & 指针和取地址
+加减 从左到右 8 << >> 位移 从左到右 < > <= >= 关系操作符 从左到右 10 ==!= 等于、不等于 从左到右 11 & 按位与运算 从左到右 12 ^ 按位异或运算 从左到右 13 | 按位或运算 从左到右 14 && 逻辑与运算 从左到右 15 || 逻辑或运算 从左到右 16 ?: 条件运算 从右到左 = *= /= %= +=-= >>= <<= &= ^= |= 赋值运算 从右到左 18 , 逗号 从左到右
结合方向Grouping定义了当有同优先级的多个运算符在一起时,哪一个必须被首先运算,最右边的还是最左边的。
所有这些运算符的优先级顺序可以通过使用括号parenthesis signs(和)来控制,而且更易读懂,例如以下例子: a = 5 + 7 % 2;根据我们想要实现的计算的不同,可以写成: a = 5 +(7 % 2);或者 a =(5 + 7)% 2;所以如果你想写一个复杂的表达式而不敢肯定各个运算的执行顺序,那么就加上括号。这样还可以使代码更易读懂。
1.5 控制台交互(Communication through console)控制台(console)是电脑的最基本交互接口,通常包括键盘(keyboard)和屏幕(screen)。键盘通常为标准输入设备,而 屏幕为标准输出设备。
在C++的iostream函数库中,一个程序的标准输入输出操作依靠两种数据流:cin 给输入使用和cout给输出使用。另外,cerr和clog也已经被实现――它们是两种特殊设计的数据流专门用来显示出错信息。它们可以被重新定向到标准输出设备或到一个日志文件(log file)。因此cout(标准输出流)通常被定向到屏幕,而cin(标准输入流)通常被定向到键盘。通过控制这两种数据流,你可以在程序中与用户交互,因为你可以在屏幕上显示输出并从键盘接收用户的输入。
输出Output(cout)输出流cout与重载(overloaded)运算符<<一起使用:
cout << “Output sentence”;// 打印Output sentence到屏幕上 cout << 120;// 打印数字 120 到屏幕上 cout << x;// 打印变量 x 的值到屏幕上
运算符<<又叫插入运算符(insertion operator)因为它将后面所跟的数据插入到它前面的数据流中。在以上的例子中,字符串常量Output sentence,数字常量120和变量x先后被插入输出流cout中。注意第一句中字符串常量是被双引号引起来的。每当我们使用字符串常量的时候,必须用引号把字符串引起来,以便将它和变量名明显的区分开来。例如,下面两个语句是不同的:
cout << “Hello”;// 打印字符串Hello到屏幕上
cout << Hello;// 把变量Hello存储的内容打印到屏幕上
插入运算符insertion operator(<<)可以在同一语句中被多次使用: cout << “Hello, ” << “I am ” << “a C++ sentence”;上面这一行语句将会打印 Hello, I am a C++ sentence 到屏幕上。插入运算符(<<)的重复使用在我们想要打印变量和内容的组合内容或多个变量时有所体现:
cout << “Hello, I am ” << age << “ years old and my zipcode is ” << zipcode;如果我们假设变量age的值为24,变量zipcode的值为90064,以上句子的输出将为: Hello, I am 24 years old and my zipcode is 90064 必须注意,除非我们明确指定,cout并不会自动在其输出内容的末尾加换行符,因此下面的语句:
cout << “This is a sentence.”;cout << “This is another sentence.”;将会有如下内容输出到屏幕:
This is a sentence.This is another sentence.虽然我们分别调用了两次cout,两个句子还是被输出在同一行。所以,为了在输出中换行,我们必须插入一个换行符来明确表达这一要求。在C++中换行符可以写作n: cout << “First sentence.n ”;cout << “Second sentence.nThird sentence.”;将会产生如下输出: First sentence.Second sentence.Third sentence.另外,你也可以用操作符endl来换行,例如: cout << “First sentence.” << endl;cout << “Second sentence.” << endl;将会输出:
First sentence.Second sentence.当操作符endl被用在buffered streams中时有一点特殊:它们被flushed。不过cout 默认为unbuffered,所以不会被影响。你可以暂时不管这一点。
你可以使用n或endl来指定cout输出换行,请注意前面所讲的两者的不同用法。
输入Input(cin)C++中的标准输入是通过在cin数据流上重载运算符extraction(>>)来实现的。它后面必须跟一个变量以便存储读入的数据。例如: int age;cin >> age;声明一个整型变量age然后等待用户从键盘输入到cin并将输入值存储在这个变量中。cin 只能在键盘输入回车键(RETURN)后才能处理前面输入的内容。因此即使你只要求输入一个单独的字符,在用户按下回车键(RETURN)之前cin将不会处理用户的输入的字符。在使用cin输入的时候必须考虑后面的变量类型。如果你要求输入一个整数,extraction(>>)后面必须跟一个整型变量,如果要求一个字符,后面必须跟一个字符型变量,如果要求一个字符串,后面必须跟一个字符串型变量。// i/o example #include
cin和字符串
我们可以像读取基本类型数据一样,使用cin和>>操作符来读取字符串,例如: cin >> mystring;但是,cin >> 只能读取一个单词,一旦碰到任何空格,读取操作就会停止。在很多时候这并不是我们想要的操作,比如我们希望用户输入一个英文句子,那么这种方法就无法读取完整的句子,因为一定会遇到空格。
要一次读取一整行输入,需要使用C++的函数 getline,相对于是用cin,我们更建议使用getline来读取用户输入。例如:
// 读取字符串例子 #include
int main(){ string mystr;cout << “What's your name? ”;getline(cin, mystr);cout << “Hello ” << mystr << “.n”;cout << “What is your favorite color? ”;getline(cin, mystr);cout << “I like ” << mystr << “ too!n”;return 0;} What's your name? Aqua Hello Aqua.What is your favorite color? blue I like blue too!你可能注意到在上面的例子中,两次调用 getline 函数我们都是用了同一个字符串变量(mystr)。在第二次调用的时候,程序会自动用第二次输入的内容取代以前的内容。
字符串流(stringstream)
标准头文件
int main(){ string mystr;float price=0;int quantity=0;
cout << “Enter price: ”;getline(cin,mystr);stringstream(mystr)>> price;cout << “Enter quantity: ”;getline(cin,mystr);stringstream(mystr)>> quantity;cout << “Total price: ” << price*quantity << endl;return 0;} Enter price: 22.25 Enter quantity: 7 Total price: 155.75 在这个例子中,我们要求用户输入数值,但不同于从标准输入中直接读取数值,我们使用函数getline从标注输入流cin中读取字符串对象(mystr),然后再从这个字符串对象中提取数值price和quantity。
通过使用这种方法,我们可以对用户的输入有更多的控制,因为它将用户输入与对输入的解释分离,只要求用户输入整行的内容,然后再对用户输入的内容进行检验操作。这种做法在用户输入比较集中的程序中是非常推荐使用的。第二章 控制结构和函数
(Control structures and Functions)1.控制结构
Control Structures 2.函数I Functions I 3.函数II Functions II 2.1 控制结构(Control Structures)
一个程序的语句往往并不仅限于线性顺序结构。在程序的执行过程中它可能被分成两支执行,可能重复某些语句,也可能根据一些判断结果而执行不同的语句。因此C++ 提供一些控制结构语句(control structures)来实现这些执行顺序。
为了介绍程序的执行顺序,我们需要先介绍一个新概念:语句块(block of instructions)。一个语句块(A block of instructions)是一组互相之间由分号semicolons(;)分隔开但整体被花括号curly bracket signs: { and }括起来的语句。
本节中我们将看到的大多数控制结构允许一个通用的statement做参数,这个statement根据需要可以是一条语句,也可以是一组语句组成的语句块。如果我们只需要一条语句做statement,它可以不被括在花括号({})内。但如果我们需要多条语句共同做statement,则必须把它们括在花括号内({})以组成一个语句块。
条件结构Conditional structure: if and else 条件结构用来实现仅在某种条件满足的情况下才执行一条语句或一个语句块。它的形式是: if(condition)statement 这里 condition 是一个将被计算的表达式(expression)。如果表达式值为真,即条件(condition)为true,statement 将被执行。否则,statement 将被忽略(不被执行),程序从整个条件结构之后的下一条语句继续执行。
例如,以下程序段实现只有当变量x存储的值确实为100的时候才输出“x is 100”: if(x == 100)cout << “x is 100”;如果我们需要在条件condition为真true的时候执行一条以上的语句,我们可以花括号{}将语句括起来组成一个语句块: if(x == 100){ cout << “x is ”;cout << x;} 我们可以用关键字else 来指定当条件不能被满足时需要执行的语句,它需要和if 一起使用,形式是:
if(condition)statement1 else statement2 例如: if(x == 100)cout << “x is 100”;else cout << “x is not 100”;以上程序如果x的值为100,则在屏幕上打出x is 100,如果x不是100,而且也只有在x不是100的时候,屏幕上将打出x is not 100。
多个if + else 的结构被连接起来使用来判断数值的范围。以下例子显示了如何用它来判断变量 x中当前存储的数值是正值,负值还是既不正也不负,即等于0。if(x > 0)cout << “x is positive”;else if(x < 0)cout << “x is negative”;else cout << “x is 0”;记住当我们需要执行多条语句时,必须使用花括号{}将它们括起来以组成一个语句块block of instructions。
重复结构 Iteration structures 或循环loops 循环Loops 的目的是重复执行一组语句一定的次数或直到满足某种条件。while 循环 格式是:
while(表达式expression)语句statement 它的功能是当expression 的值为真true时重复执行statement。例如,下面我们将用while循环来写一个倒计数程序: // custom countdown using while #include
以上程序的所有处理过程可以用以下的描述来解释: 从main开始: 1.用户输入一个数值赋给n.2.while语句检查(n>0)是否成立,这时有两种可能: o true: 执行statement(到第3步)o false: 跳过statement.程序直接执行第5步.3.执行statement: cout << n << “, ”;--n;(将n 的值打印在屏幕上,然后将n 的值减1).4.语句块结束,自动返回第2步。
5.继续执行语句块之后的程序:打印 FIRE!,程序结束。
我们必须考虑到循环必须在某个点结束,因此在语句块之内(loop的statement之内)我们必须提供一些方法使得条件condition 可以在某个时刻变为假 false,否则循环将无限重复下去。在这个例子里,我们用语句--n;使得循环在重复一定的次数后变为false :当 n 变为0,倒计数结束。do-while 循环 格式:
do 语句statement while(条件condition);它的功能与while 循环一抹一样,除了在do-while循环中是先执行statement 然后才检查条件condition,而不想while循环中先检查条件然后才执行statement。这样,即使条件condition从来没有被满足过,statement 仍至少被执行一次。例如,下面的程序重复输出(echoes)用户输入的任何数值,直到用户输入0为止。// number echoer #include
for(initialization;condition;increase)statement;它的主要功能是当条件condition 为真true时重复执行语句statement,类似while 循环。但除此之外,for 还提供了写初始化语句initialization 和增值语句increase 的地方。因此这种循环结构是特别为执行由计数器控制的循环而设计的。它按以下方式工作:
1.执行初始化initialization。通常它是设置一个计数器变量(counter variable)的初始值,初始化仅被执行一次。
2.检查条件condition,如果条件为真true,继续循环,否则循环结束循环中语句statement 被跳过。
3.执行语句statement。像以前一样,它可以是一个单独的语句,也可以是一个由花括号{ }括起来的语句块。
4.最后增值域(increase field)中的语句被执行,循环返回第2步。注意增值域中可能是任何语句,而不一定只是将计数器增加的语句。例如下面的例子中计数器实际为减1,而不是加1。
下面是用for循环实现的倒计数的例子: // countdown using a for loop #include
n初始值为0,i初始值为100,条件是(n!=i)(即n不能等于i)。因为每次循环n加1,而且i减1,循环的条件将会在第50次循环之后变为假false(n 和 i 都等于50)。分支控制和跳转(Bifurcation of control and jumps)break 语句
通过使用break语句,即使在结束条件没有满足的情况下,我们也可以跳出一个循环。它可以被用来结束一个无限循环(infinite loop),或强迫循环在其自然结束之前结束。例如,我们想要在倒计数自然结束之前强迫它停止(也许因为一个引擎故障): // break loop example #include
continue语句使得程序跳过当前循环中剩下的部分而直接进入下一次循环,就好像循环中语句块的结尾已经到了使得循环进入下一次重复。例如,下面例子中倒计数时我们将跳过数字5的输出:
// continue loop example #include
通过使用goto语句可以使程序从一点跳转到另外一点。你必须谨慎只用这条语句,因为它的执行可以忽略任何嵌套限制。
跳转的目标点可以由一个标示符(label)来标明,该标示符作为goto语句的参数。一个标示符(label)由一个标识名称后面跟一个冒号colon(:)组成。
通常除了底层程序爱好者使用这条语句,它在结构化或面向对象的编程中并不常用。下面的例子中我们用goto来实现倒计数循环: // goto loop example #include
exit是一个在cstdlib(stdlib.h)库中定义的函数。
exit的目的是一个特定的退出代码来结束程序的运行,它的原型(prototype)是: void exit(int exit code);exit code是由操作系统使用或被调用程序使用。通常exit code为0表示程序正常结束,任何其他值表示程序执行过程中出现了错误。
选择结构The selective Structure: switch switch 语句的语法比较特殊。它的目标是对一个表达式检查多个可能常量值,有些像我们在本节开头学习的把几个if 和else if 语句连接起来的结构。它的形式是: switch(expression){ case constant1: block of instructions 1 break;case constant2: block of instructions 2 break;...default: default block of instructions } 它按以下方式执行:
switch 计算表达式expression 的值,并检查它是否与第一个常量constant1相等,如果相等,程序执行常量1后面的语句块block of instructions 1 直到碰到关键字break,程序跳转到switch 选择结构的结尾处。如果expression 不等于constant1,程序检查表达式expression 的值是否等于第二个常量constant2,如果相等,程序将执行常量2后面的语句块block of instructions 2 直到碰到关键字break。
依此类推,直到最后如果表达式expression 的值不等于任何前面的常量(你可以用case语句指明任意数量的常量值来要求检查),程序将执行默认区default: 后面的语句,如果它存在的话。default: 选项是可以省略的。下面的两段代码段功能相同:
switch example if-else equivalent switch(x){ case 1: cout << “x is 1”;break;case 2: cout << “x is 2”;break;default: cout << “value of x unknown”;} if(x == 1){ cout << “x is 1”;} else if(x == 2){ cout << “x is 2”;} else { cout << “value of x unknown”;} 前面已经提到switch的语法有点特殊。注意每个语句块结尾包含的break语句。这是必须的,因为如果不这样做,例如在语句块block of instructions 1 的结尾没有break,程序执行将不会跳转到switch选择的结尾处(}),而是继续执行下面的语句块,直到第一次遇到break语句或到switch选择结构的结尾。因此,不需要在每一个case 域内加花括号{ }。这个特点同时可以帮助实现对不同的可能值执行相同的语句块。例如: switch(x){ case 1: case 2: case 3: cout << “x is 1, 2 or 3”;break;default: cout << “x is not 1, 2 nor 3”;} 注意switch只能被用来比较表达式和不同常量的值constants。因此我们不能够把变量或范围放在case之后,例如(case(n*2):)或(case(1..3):)都不可以,因为它们不是有效的常量。如果你需要检查范围或非常量数值,使用连续的if 和else if 语句。2.2 函数I(Functions I)
通过使用函数(functions)我们可以把我们的程序以更模块化的形式组织起来,从而利用C++所能提供的所有结构化编程的潜力。
一个函数(function)是一个可以从程序其它地方调用执行的语句块。以下是它的格式:
type name(argument1, argument2,...)statement
这里:
? type 是函数返回的数据的类型 ? name 是函数被调用时使用的名
? argument 是函数调用需要传入的参量(可以声明任意多个参量)。每个参量(argument)由一个数据类型后面跟一个标识名称组成,就像变量声明中一样(例如,int x)。参量仅在函数范围内有效,可以和函数中的其它变量一样使用,它们使得函数在被调用时可以传入参数,不同的参数用逗号(comma)隔开.? statement 是函数的内容。它可以是一句指令,也可以是一组指令组成的语句块。如果是一组指令,则语句块必须用花括号{}括起来,这也是我们最常见到情况。其实为了使程序的格式更加统一清晰,建议在仅有一条指令的时候也使用花括号,这是一个良好的编程习惯。下面看一下第一个函数的例子: // function example #include
int main(){ int z;z = addition(5,3);cout << “The result is ” << z;return 0;} The result is 8 记得在我们教程开始时说过:一个C++程序总是从main函数开始执行。因此我们从那里开始。
我们可以看到 main 函数以定义一个整型变量z 开始。紧跟着我们看到调用addition 函数。我们可以看到函数调用的写法和上面函数定义本身十分相似:
参数有明显的对应关系。在main 函数中我们调用addition 函数,并传入两个数值: 5 和3,它们对应函数addition 中定义的参数int a 和int b。当函数在main 中被调用时,程序执行的控制权从main转移到函数addition。调用传递的两个参数的数值(5 和3)被复制到函数的本地变量(local variables)int a 和int b 中。函数addition 中定义了新的变量(int r;),通过表达式r=a+b;, 它把a 加b 的结果赋给r。因为传过来的参数a 和b 的值分别为5 和3,所以结果是8。下面一行代码: return(r);结束函数addition,并把控制权交还给调用它的函数(main),从调用addition的地方开始继续向下执行。另外,return 在调用的时候后面跟着变量r(return(r);), 它当时的值为8, 这个值被称为函数的返回值。
函数返回的数值就是函数的计算结果,因此,z 将存储函数addition(5, 3)返回的数值, 即8。用另一种方式解释,你也可以想象成调用函数(addition(5,3))被替换成了它的返回值(8)。
接下来main中的下一行代码是: cout << “The result is ” << z;它把结果打印在屏幕上。
变量的范围(Scope of variables)你必须考虑到变量的范围只是在定义该变量的函数或指令块内有效,而不能在它的函数或指令块之外使用。例如,在上面的例子里就不可能在main 中直接使用变量a, b 或 r,因为它们是函数addition的本地变量(local variable)。在函数addition中也不可能直接使用变量z,因为它是main的本地变量。
因此,本地变量(local variables)的范围是局限于声明它的嵌套范围之内的。尽管如此,你还可以定义全局变量(global variables),它们可以在代码的任何位置被访问,不管在函数以内还是以外。要定义全局变量,你必须在所有函数或代码块之外定义它们,也就是说,直接在程序体中声明它们。这里是另一个关于函数的例子: // function example #include
int main(){ int x=5, y=3, z;z = subtraction(7,2);cout << “The first result is ” << z << 'n';cout << “The second result is ” << subtraction(7,2)<< 'n';cout << “The third result is ” << subtraction(x,y)<< 'n';z= 4 + subtraction(x,y);cout << “The fourth result is ” << z << 'n';return 0;} The first result is 5 The second result is 5 The third result is 2 The fourth result is 6 在这个例子中,我们定义了函数subtraction。这个函数的功能是计算传入的两个参数的差值并将结果返回。
在 main 函数中,函数subtraction被调用了多次。我们用了几种不同的调用方法,因此你可以看到在不同的情况下函数如何被调用。为了更好的理解这些例子,你需要考虑到被调用的函数其实完全可以由它所返回的值来代替。例如在上面例子中第一种情况下(这种调用你应该已经知道了,因为我们在前面的例子中已经用过这种形式的调用): z = subtraction(7,2);cout << “The first result is ” << z;如果我们把函数调用用它的结果(也就是5)替换,我们将得到: z = 5;cout << “The first result is ” << z;同样的
cout << “The second result is ” << subtraction(7,2);与前面的调用有同样的结果,但在这里我们把对函数subtraction 的调用直接用作cout的参数。这可以简单想象成我们写的是: cout << “The second result is ” << 5;因为5 是subtraction(7,2)的结果。在
cout << “The third result is ” << subtraction(x,y);中,与前面的调用唯一的不同之处是这里调用subtraction 时的参数使用的是变量而不是常量。这样用时毫无问题的。在这个例子里,传入函数subtraction 的参数值是变量x 和y中存储的数值,即分别为5 和3,结果为2。第四种调用也是一样的。只要知道除了 z = 4 + subtraction(x,y);我们也可以写成:
z = subtraction(x,y)+ 4;它们的结果是完全一样的。注意在整个表达式的结尾写上分号semicolon sign(;)。它并不需要总是跟在函数调用的后面,因为你可以有一次把它们想象成函数被它的结果所替代: z = 4 + 2;z = 2 + 4;没有返回值类型的函数,使用void.如果你记得函数声明的格式:
type name(argument1, argument2...)statement 就会知道函数声明必须以一个数据类型(type)开头,它是函数由return 语句所返回数据类型。但是如果我们并不打算返回任何数据那该怎么办呢? 假设我们要写一个函数,它的功能是打印在屏幕上打印一些信息。我们不需要它返回任何值,而且我们也不需要它接受任何参数。C语言为这些情况设计了void 类型。让我们看一下下面的例子:
// void 函数示例 #include
void printmessage(){ cout << “I'm a function!”;}
int main(){ printmessage();return 0;} I'm a function!void还可以被用在函数参数位置,表示我们明确希望这个函数在被调用时不需要任何参数。例如上面的函数printmessage也可以写为以下形式: void printmessage(void){ cout << “I'm a function!”;} 虽然在C++ 中void可以被省略,我们还是建议写出void,以便明确指出函数不需要参数。你必须时刻知道的是调用一个函数时要写出它的名字并把参数写在后面的括号内。但如果函数不需要参数,后面的括号并不能省略。因此调用函数 printmessage 的格式是 printmessage();函数名称后面的括号就明确表示了它是一个函数调用,而不是一个变量名称或其它什么语句。以下调用函数的方式就不对: printmessage;2.3 函数II(Functions II)
参数按数值传递和按地址传递(Arguments passed by value and by reference)到目前为止,我们看到的所有函数中,传递到函数中的参数全部是按数值传递的(by value)。也就是说,当我们调用一个带有参数的函数时,我们传递到函数中的是变量的数值而不是变量本身。例如,假设我们用下面的代码调用我们的第一个函数addition : int x=5, y=3, z;z = addition(x , y);在这个例子里我们调用函数addition 同时将x和y的值传给它,即分别为5和3,而不是两个变量:
这样,当函数addition被调用时,它的变量a和b的值分别变为5和3,但在函数addition内对变量a 或b 所做的任何修改不会影响变量他外面的变量x 和 y 的值,因为变量x和y并没有把它们自己传递给函数,而只是传递了他们的数值。
但在某些情况下你可能需要在一个函数内控制一个函数以外的变量。要实现这种操作,我们必须使用按地址传递的参数(arguments passed by reference),就象下面例子中的函数duplicate:
// passing parameters by reference #include
void duplicate(int& a, int& b, int& c){ a*=2;b*=2;c*=2;}
int main(){ int x=1, y=3, z=7;duplicate(x, y, z);cout << “x=” << x << “, y=” << y << “, z=” << z;return 0;} x=2, y=6, z=14 第一个应该注意的事项是在函数duplicate的声明(declaration)中,每一个变量的类型后面跟了一个地址符ampersand sign(&),它的作用是指明变量是按地址传递的(by reference),而不是像通常一样按数值传递的(by value)。当按地址传递(pass by reference)一个变量的时候,我们是在传递这个变量本身,我们在函数中对变量所做的任何修改将会影响到函数外面被传递的变量。
用另一种方式来说,我们已经把变量a, b,c和调用函数时使用的参数(x, y和 z)联系起来了,因此如果我们在函数内对a 进行操作,函数外面的x 值也会改变。同样,任何对b 的改变也会影响y,对c 的改变也会影响z>。
这就是为什么上面的程序中,主程序main中的三个变量x, y和z在调用函数duplicate 后打印结果显示他们的值增加了一倍。如果在声明下面的函数:
void duplicate(int& a, int& b, int& c)时,我们是按这样声明的:
void duplicate(int a, int b, int c)也就是不写地址符 ampersand(&),我们也就没有将参数的地址传递给函数,而是传递了它们的值,因此,屏幕上显示的输出结果x, y,z 的值将不会改变,仍是1,3,7。
这种用地址符 ampersand(&)来声明按地址“by reference”传递参数的方式只是在C++中适用。在C 语言中,我们必须用指针(pointers)来做相同的操作。
按地址传递(Passing by reference)是一个使函数返回多个值的有效方法。例如,下面是一个函数,它可以返回第一个输入参数的前一个和后一个数值。// more than one returning value #include
int main(){ int x=100, y, z;prevnext(x, y, z);cout << “Previous=” << y << “, Next=” << z;return 0;} Previous=99, Next=101
参数的默认值(Default values in arguments)当声明一个函数的时候我们可以给每一个参数指定一个默认值。如果当函数被调用时没有给出该参数的值,那么这个默认值将被使用。指定参数默认值只需要在函数声明时把一个数值赋给参数。如果函数被调用时没有数值传递给该参数,那么默认值将被使用。但如果有指定的数值传递给参数,那么默认值将被指定的数值取代。例如: // default values in functions #include
int main(){ cout << divide(12);cout << endl;cout << divide(20,4);return 0;} 6 5 我们可以看到在程序中有两次调用函数divide。第一次调用: divide(12)只有一个参数被指明,但函数divide允许有两个参数。因此函数divide 假设第二个参数的值为2,因为我们已经定义了它为该参数缺省的默认值(注意函数声明中的int b=2)。因此这次函数调用的结果是 6(12/2)。在第二次调用中: divide(20,4)这里有两个参数,所以默认值(int b=2)被传入的参数值4所取代,使得最后结果为 5(20/4).函数重载(Overloaded functions)两个不同的函数可以用同样的名字,只要它们的参量(arguments)的原型(prototype)不同,也就是说你可以把同一个名字给多个函数,如果它们用不同数量的参数,或不同类型的参数。例如:
// overloaded function #include
int divide(int a, int b){ return(a/b);}
float divide(float a, float b){ return(a/b);}
int main(){ int x=5,y=2;float n=5.0,m=2.0;cout << divide(x,y);cout << “n”;cout << divide(n,m);cout << “n”;return 0;} 2 2.5 在这个例子里,我们用同一个名字定义了两个不同函数,当它们其中一个接受两个整型(int)参数,另一个则接受两个浮点型(float)参数。编译器(compiler)通过检查传入的参数的类型来确定是哪一个函数被调用。如果调用传入的是两个整数参数,那么是原型定义中有两个整型(int)参量的函数被调用,如果传入的是两个浮点数,那么是原型定义中有两个浮点型(float)参量的函数被调用。
为了简单起见,这里我们用的两个函数的代码相同,但这并不是必须的。你可以让两个函数用同一个名字同时完成完全不同的操作。
Inline 函数(inline functions)
inline 指令可以被放在函数声明之前,要求该函数必须在被调用的地方以代码形式被编译。这相当于一个宏定义(macro)。它的好处只对短小的函数有效,这种情况下因为避免了调用函数的一些常规操作的时间(overhead),如参数堆栈操作的时间,所以编译结果的运行代码会更快一些。
它的声明形式是:
inline type name(arguments...){ instructions...} 它的调用和其他的函数调用一样。调用函数的时候并不需要写关键字inline,只有在函数声明前需要写。
递归(Recursivity)
递归(recursivity)指函数将被自己调用的特点。它对排序(sorting)和阶乘(factorial)运算很有用。例如要获得一个数字n的阶乘,它的数学公式是: n!= n *(n-1)*(n-2)*(n-3)...* 1 更具体一些,5!(factorial of 5)是: 5!= 5 * 4 * 3 * 2 * 1 = 120 而用一个递归函数来实现这个运算将如以下代码: // factorial calculator #include
long factorial(long a){ if(a > 1)return(a * factorial(a-1));else return(1);}
int main(){ long l;cout << “Type a number: ”;cin >> l;cout << “!” << l << “ = ” << factorial(l);return 0;} Type a number: 9!9 = 362880 注意我们在函数factorial中是怎样调用它自己的,但只是在参数值大于1的时候才做调用,因为否则函数会进入死循环(an infinite recursive loop),当参数到达0的时候,函数不继续用负数乘下去(最终可能导致运行时的堆栈溢出错误(stack overflow error)。
这个函数有一定的局限性,为简单起见,函数设计中使用的数据类型为长整型(long)。在实际的标准系统中,长整型long无法存储12!以上的阶乘值。
函数的声明(Declaring functions)
到目前为止,我们定义的所有函数都是在它们第一次被调用(通常是在main中)之前,而把main 函数放在最后。如果重复以上几个例子,但把main 函数放在其它被它调用的函数之前,你就会遇到编译错误。原因是在调用一个函数之前,函数必须已经被定义了,就像我们前面例子中所做的。
但实际上还有一种方法来避免在main 或其它函数之前写出所有被他们调用的函数的代码,那就是在使用前先声明函数的原型定义。声明函数就是对函数在的完整定义之前做一个短小重要的声明,以便让编译器知道函数的参数和返回值类型。它的形式是:
type name(argument_type1, argument_type2,...);它与一个函数的头定义(header definition)一样,除了:
? 它不包括函数的内容,也就是它不包括函数后面花括号{}内的所有语句。? 它以一个分号semicolon sign(;)结束。
? 在参数列举中只需要写出各个参数的数据类型就够了,至于每个参数的名字可以写,也可以不写,但是我们建议写上。例如:
// 声明函数原型
#include
void odd(int a);void even(int a);
int main(){ int i;do { cout << “Type a number:(0 to exit)”;cin >> i;odd(i);} while(i!=0);return 0;}
void odd(int a){ if((a%2)!=0)cout << “Number is odd.n”;else even(a);}
void even(int a){ if((a%2)==0)cout << “Number is even.n”;else odd(a);} Type a number(0 to exit): 9 Number is odd.Type a number(0 to exit): 6 Number is even.Type a number(0 to exit): 1030 Number is even.Type a number(0 to exit): 0 Number is even.这个例子的确不是很有效率,我相信现在你已经可以只用一半行数的代码来完成同样的功能。但这个例子显示了函数原型(prototyping functions)是怎样工作的。并且在这个具体的例子中,两个函数中至少有一个是必须定义原型的。这里我们首先看到的是函数odd 和even的原型: void odd(int a);void even(int a);这样使得这两个函数可以在它们被完整定义之前就被使用,例如在main中被调用,这样main就可以被放在逻辑上更合理的位置:即程序代码的开头部分。
尽管如此,这个程序需要至少一个函数原型定义的特殊原因是因为在odd 函数里需要调用even 函数,而在even 函数里也同样需要调用odd函数。如果两个函数任何一个都没被提前定义原型的话,就会出现编译错误,因为或者odd 在even 函数中是不可见的(因为它还没有被定义),或者even 函数在odd函数中是不可见的。
很多程序员建议给所有的函数定义原型。这也是我的建议,特别是在有很多函数或函数很长的情况下。把所有函数的原型定义放在一个地方,可以使我们在决定怎样调用这些函数的时候轻松一些,同时也有助于生成头文件。
第三章 高级数据类型(Advanced Data)1.数组 Arrays 2.字符序列
Characters Sequences 3.指针 Pointers 4.动态内存分配 Dynamic memory 5.数据结构 Data Structures 6.自定义数据类型
User defined data types 3.1 数组(Arrays)
数组(Arrays)是在内存中连续存储的一组同种数据类型的元素(变量),每一数组有一个唯一名称,通过在名称后面加索引(index)的方式可以引用它的每一个元素。
也就是说,例如我们有5个整型数值需要存储,但我们不需要定义5个不同的变量名称,而是用一个数组(array)来存储这5个不同的数值。注意数组中的元素必须是同一数据类型的,在这个例子中为整型(int)。
例如一个存储5个整数叫做billy的数组可以用下图来表示:
这里每一个空白框代表数组的一个元素,在这个例子中为一个整数值。白框上面的数字0 到4 代表元素的索引(index)。注意无论数组的长度如何,它的第一个元素的索引总是从0开始的。
同其它的变量一样,数组必须先被声明然后才能被使用。一种典型的数组声明显示如下: type name [elements];这里type 是可以使任何一种有效的对象数据类型(object type),如 int, float...等,name 是一个有效地变量标识(identifier),而由中括号[]引起来的elements 域指明数组的大小,即可以存储多少个元素。
因此,要定义上面图中显示的 billy 数组,用一下语句就可以了: int billy [5];备注:在定义一个数组的时候,中括号[]中的elements 域必须是一个常量数值,因为数组是内存中一块有固定大小的静态空间,编译器必须在编译所有相关指令之前先能够确定要给该数组分配多少内存空间。
初始化数组(Initializing arrays)当声明一个本地范围内(在一个函数内)的数组时,除非我们特别指定,否则数组将不会被初始化,因此它的内容在我们将数值存储进去之前是不定的。如果我们声明一个全局数组(在所有函数之外),则它的内容将被初始化为所有元素均为0。因此,如果全局范围内我们声明: int billy [5];那么billy 中的每一个元素将会被初始化为0:
另外,我们还可以在声明一个变量的同时把初始值付给数组中的每一个元素,这个赋值用花括号{ }来完成。例如:
int billy [5] = { 16, 2, 77, 40, 12071 };这个声明将生成如下数组:
花括号中我们要初始化的元素数值个数必须和数组声明时方括号[ ]中指定的数组长度相符。例如,在上面例子中数组billy 声明中的长度为5,因此在后面花括号中的初始值也有5个,每个元素一个数值。
因为这是一种信息的重复,因此C++允许在这种情况下数组[ ]中为空白,而数组的长度将有后面花括号{}中数值的个数来决定,如下例所示。int billy [] = { 16, 2, 77, 40, 12071 };存取数组中数值(Access to the values of an Array)在程序中我们可以读取和修改数组任一元素的数值,就像操作其他普通变量一样。格式如下: name[index] 继续上面的例子,数组billy 有5个元素,其中每一元素都是整型int,我们引用其中每一个元素的名字分别为如下所示:
例如,要把数值75存入数组billy 中第3个元素的语句可以是: billy[2] = 75;又例如,要把数组billy 中第3个元素的值赋给变量a,我们可以这样写: a = billy[2];因此,在所有使用中,表达式billy[2]就像任何其他整型变量一样。
注意数组billy 的第3个元素为billy[2],因为索引(index)从0开始,第1个元素是billy[0],第2个元素是billy[1],因此第3个是 billy[2]。同样的原因,最后一个元素是billy[4]。如果我们写billy[5],那么是在使用billy的第6个元素,因此会超出数组的长度。
在C++ 中对数组使用超出范围的index是合法的,这就会产生问题,因为它不会产生编译错误而不易被察觉,但是在运行时会产生意想不到的结果,甚至导致严重运行错误。超出范围的index 之所以合法的原因我们在后面学习指针(pointer)的时候会了解。学到这里,我们必须能够清楚的了解方括号[ ]在对数组操作中的两种不同用法。它们完成两种任务:一种是在声明数组的时候定义数组的长度;另一种是在引用具体的数组元素的时候指明一个索引号(index)。我们要注意不要把这两种用法混淆。int billy[5];// 声明新数组(以数据类型名称开头)billy[2] = 75;// 存储数组的一个元素 其它合法的数组操作:
billy[0] = a;// a为一个整型变量 billy[a] = 75;b = billy [a+2];billy[billy[a]] = billy[2] + 5;
// arrays example #include
int billy [ ] = {16, 2, 77, 40, 12071};int n, result=0;
int main(){ for(n=0;n<5;n++){ result += billy[n];}
cout << result;return 0;} 12206
多维数组(Multidimensional Arrays)
多维数组(Multidimensional Arrays)可以被描述为数组的数组。例如,一个2维数组(bidimensional array)可以被想象成一个有同一数据类型的2维表格。
jimmy 显示了一个整型(int)的3x5二维数组,声明这一数组的的方式是: int jimmy [3][5];而引用这一数组中第2列第4排元素的表达式为:jimmy[1][3]
(记住数组的索引总是从0开始)。多维数组(Multidimensional arrays)并不局限于2维。如果需要,它可以有任意多维,虽然需要3维以上的时候并不多。但是考虑一下一个有很多维的数组所需要的内存空间,例如: char century [100][365][24][60][60];给一个世纪中的每一秒赋一个字符(char),那么就是多于30亿的字符!如果我们定义这样一个数组,需要消耗3000M的内存。
多维数组只是一个抽象的概念,因为我们只需要把各个索引的乘积放入一个简单的数组中就可以获得同样的结果。例如:
int jimmy [3][5];效果上等价于 int jimmy [15];(3 * 5 = 15)唯一的区别是编译器帮我们记住每一个想象中的维度的深度。下面的例子中我们就可以看到,两段代码一个使用2维数组,另一个使用简单数组,都获得同样的结果,即都在内存中开辟了一块叫做jimmy的空间,这个空间有15个连续地址位置,程序结束后都在相同的位置上存储了相同的数值,如后面图中所示: // multidimensional array #include
int jimmy [HEIGHT][WIDTH];int n,m;
int main(){ for(n=0;n return 0;} // pseudo-multidimensional array #include int jimmy [HEIGHT * WIDTH];int n,m; int main(){ for(n=0;n #define HEIGHT 4 而不需要对程序的其他部分作任何修改。 数组参数(Arrays as parameters) 有时候我们需要将数组作为参数传给函数。在C++ 中将一整块内存中的数值作为参数完整的传递给一个函数是不可能的,即使是一个规整的数组也不可能,但是允许传递它的地址。它们的实际作用是一样的,但传递地址更快速有效。 要定义数组为参数,我们只需要在声明函数的时候指明参数数组的基本数据类型,一个标识后面再跟一对空括号[]就可以了。例如以下的函数: void procedure(int arg[])接受一个叫做arg的整型数组为参数。为了给这个函数传递一个按如下定义的数组: int myarray [40];其调用方式可写为: procedure(myarray);下面我们来看一个完整的例子: // arrays as parameters #include void printarray(int arg[ ], int length){ for(int n=0;n int main(){ int firstarray[ ] = {5, 10, 15};int secondarray[ ] = {2, 4, 6, 8, 10};printarray(firstarray,3);printarray(secondarray,5);return 0;} 5 10 15 2 4 6 8 10 可以看到,函数的第一个参数(int arg[ ])接受任何整型数组为参数,不管其长度如何。因此,我们用了第2个参数来告知函数我们传给它的第一个参数数组的长度。这样函数中打印数组内容的for 循环才能知道需要检查的数组范围。 在函数的声明中也包含多维数组参数。定义一个3维数组(tridimensional array)的形式是: base_type[ ][depth][depth] 例如,一个函数包含多维数组参数的函数可以定义为: void procedure(int myarray[ ][3][4])注意第一对括号[ ]中为空,而后面两对不为空。这是必须的,因为编译器必须能够在函数中确定每一个增加的维度的深度。 数组作为函数的参数,不管是多维数组还是简单数组,都是初级程序员容易出错的地方。建议阅读章节3.3, 指针(Pointers),以便更好的理解数组(arrays)是如何操作的。 3.2 字符序列(Character Sequences) 前面基础知识部分讲C++变量类型的时候,我们已经提到过C++的标准函数库提供了一个string类来支持对字符串的操作。然而,字符串实际就是一串连续的字符序列,所以我们也可以用简单的字符数组来表示它。例如,下面这个数组: char jenny [20];是一个可以存储最多20个字符类型数据的数组。你可以把它想象成: 理论上这数组可以存储长度为20的字符序列,但是它也可以存储比这短的字符序列,而且实际中常常如此。例如,jenny 在程序的某一点可以只存储字符串“Hello” 或者“Merry christmas”。因此,既然字符数组经常被用于存储短于其总长的字符串,就形成了一种习惯在字符串的有效内容的结尾处加一个空字符(null character)来表示字符结束,它的常量表示可写为0 或' '。 我们可以用下图表示jenny(一个长度为20的字符数组)存储字符串“Hello” 和“Merry Christmas” : 注意在有效内容结尾是如何用空字符null character(' ')来表示字符串结束的。后面灰色的空格表示不确定数值。 初始化以空字符结束的字符序列(Initialization of null-terminated character sequences)因为字符数组其实就是普通数组,它与数组遵守同样的规则。例如,如果我们想将数组初始化为指定数值,我们可以像初始化其它数组一样用: char mystring[] = { 'H', 'e', 'l', 'l', 'o', ' ' };在这里我们定义了一个有6个元素的字符数组,并将它初始化为字符串Hello 加一个空字符(null character ' ')。 除此之外,字符串还有另一个方法来进行初始化:用字符串常量。 在前几章的例子中,字符串常量已经出现过多次,它们是由双引号引起来的一组字符来表示的,例如: “the result is: ” 是一个字符串常量,我们在前面的例子中已经使用过。与表示单个字符常量的单引号(')不同,双引号(“)是表示一串连续字符的常量。由双引号引起来的字符串末尾总是会被自动加上一个空字符(' ')。 因此,我们可以用下面两种方法的任何一种来初始化字符串mystring: char mystring [ ] = { 'H', 'e', 'l', 'l', 'o', ' ' };char mystring [ ] = ”Hello“;在两种情况下字符串或数组mystring都被定义为6个字符长(元素类型为字符char):组成Hello的5个字符加上最后的空字符(' ')。在第二种用双引号的情况下,空字符(' ')是被自动加上的。 注意:同时给数组赋多个值只有在数组初始化时,也就是在声明数组时,才是合法的。象下面代码现实的表达式都是错误的: mystring = ”Hello“;mystring[ ] = ”Hello“;mystring = { 'H', 'e', 'l', 'l', 'o', ' ' };因此记住:我们只有在数组初始化时才能够同时赋多个值给它。其原因在学习了指针(pointer)之后会比较容易理解,因为那时你会看到一个数组其实只是一个指向被分配的内存块的常量指针(constant pointer),数组自己不能够被赋予任何数值,但我们可以给数组中的每一个元素赋值。 在数组初始化的时候是特殊情况,因为它不是一个赋值,虽然同样使用了等号(=)。不管怎样,牢记前面标下画线的规则。 给字符序列的赋值 因为赋值运算的lvalue 只能是数组的一个元素,而不能使整个数组,所以,用以下方式将一个字符串赋给一个字符数组是合法的: mystring[0] = 'H';mystring[1] = 'e';mystring[2] = 'l';mystring[3] = 'l';mystring[4] = 'o';mystring[5] = ' ';但正如你可能想到的,这并不是一个实用的方法。通常给数组赋值,或更具体些,给字符序列赋值的方法是使用一些函数,例如strcpy。strcpy(string copy)在函数库cstring(string.h)中被定义,可以用以下方式被调用: strcpy(string1, string2);这个函数将string2 中的内容拷贝给string1。string2 可以是一个数组,一个指针,或一个字符串常量constant string。因此用下面的代码可以将字符串常量”Hello“赋给mystring: strcpy(mystring, ”Hello“);例如: // setting value to string #include int main(){ char szMyName [20];strcpy(szMyName,”J.Soulie“);cout << szMyName;return 0;} J.Soulie 注意:我们需要包括头文件才能够使用函数strcpy。 虽然我们通常可以写一个像下面setstring一样的简单程序来完成与cstring 中strcpy同样的操作: // setting value to string #include void setstring(char szOut [ ], char szIn [ ]){ int n=0;do { szOut[n] = szIn[n];} while(szIn[n++]!= ' ');} int main(){ char szMyName [20];setstring(szMyName,”J.Soulie“);cout << szMyName;return 0;} J.Soulie 另一个给数组赋值的常用方法是直接使用输入流(cin)。在这种情况下,字符序列的值是在程序运行时由用户输入的。 当cin 被用来输入字符序列值时,它通常与函数getline 一起使用,方法如下: cin.getline(char buffer[], int length, char delimiter = ' n');这里buffer 是用来存储输入的地址(例如一个数组名),length 是一个缓存buffer 的最大容量,而delimiter 是用来判断用户输入结束的字符,它的默认值(如果我们不写这个参数时)是换行符newline character('n')。 下面的例子重复输出用户在键盘上的任何输入。这个例子简单的显示了如何使用cin.getline来输入字符串: // cin with strings #include int main(){ char mybuffer [100];cout << ”What's your name? “;cin.getline(mybuffer,100);cout << ”Hello “ << mybuffer << ”.n“;cout << ”Which is your favourite team? “;cin.getline(mybuffer,100);cout << ”I like “ << mybuffer << ” too.n“;return 0;} What's your name? Juan Hello Juan.Which is your favourite team? Inter Milan I like Inter Milan too.注意上面例子中两次调用cin.getline 时我们都使用了同一个字符串标识(mybuffer)。程序在第二次调用时将新输入的内容直接覆盖到第一次输入到buffer 中的内容。你可能还记得,在以前与控制台(console)交互的程序中,我们使用extraction operator(>>)来直接从标准输入设备接收数据。这个方法也同样可以被用来输入字符串,例如,在上面的例子中我们也可以用以下代码来读取用户输入: cin >> mybuffer;这种方法也可以工作,但它有以下局限性是cin.getline所没有的: ? 它只能接收单独的词(而不能是完整的句子),因为这种方法以任何空白符为分隔符,包括空格spaces,跳跃符tabulators,换行符newlines和回车符arriage returns。 ? 它不能给buffer指定容量,这使得程序不稳定,如果用户输入超出数组长度,输入信息会被丢失。 因此,建议在需要用cin来输入字符串时,使用cin.getline来代替cin >>。 字符串和其它数据类型的转换(Converting strings to other types)鉴于字符串可能包含其他数据类型的内容,例如数字,将字符串内容转换成数字型变量的功能会有用处。例如一个字符串的内容可能是”1977“,但这一个5个字符组成序列,并不容易转换为一个单独的整数。因此,函数库cstdlib(stdlib.h)提供了3个有用的函数: ? atoi: 将字符串string 转换为整型int ? atol: 将字符串string 转换为长整型long ? atof: 将字符串string 转换为浮点型float 所有这些函数接受一个参数,返回一个指定类型的数据(int, long 或 float)。这三个函数与cin.getline 一起使用来获得用户输入的数值,比传统的cin>> 方法更可靠: // cin and ato* functions #include int main(){ char mybuffer [100];float price;int quantity;cout << ”Enter price: “;cin.getline(mybuffer,100);price = atof(mybuffer);cout << ”Enter quantity: “;cin.getline(mybuffer,100);quantity = atoi(mybuffer);cout << ”Total price: “ << price*quantity;return 0;} Enter price: 2.75 Enter quantity: 21 Total price: 57.75 字符串操作函数(Functions to manipulate strings)函数库cstring(string.h)定义了许多可以像C语言类似的处理字符串的函数(如前面已经解释过的函数strcpy)。这里再简单列举一些最常用的: ? strcat: char* strcat(char* dest, const char* src);//将字符串src 附加到字符串dest 的末尾,返回dest。 ? strcmp: int strcmp(const char* string1, const char* string2);//比较两个字符串string1 和string2。如果两个字符串相等,返回0。 ? strcpy: char* strcpy(char* dest, const char* src);//将字符串src 的内容拷贝给dest,返回dest。 ? strlen: size_t strlen(const char* string);//返回字符串的长度。注意:char* 与char[] 相同。 关于这个函数库的更多信息,参阅 C++ Reference。 3.3 指针(Pointers) 我们已经明白变量其实是可以由标识来存取的内存单元。但这些变量实际上是存储在内存中具体的位置上的。对我们的程序来说,计算机内存只是一串连续的单字节单元(1byte cell),即最小数据单位,每一个单元有一个唯一地址。 计算机内存就好像城市中的街道。在一条街上,所有的房子被顺序编号,每所房子有唯一编号。因此如果我们说芝麻街27号,我们很容易找到它,因为只有一所房子会是这个编号,而且我们知道它会在26号和28号之间。同房屋按街道地址编号一样,操作系统(operating system)也按照唯一顺序编号来组织内存。因此,当我们说内存中的位置1776,我们知道内存中只有一个位置是这个地址,而且它在地址1775和1777之间。 地址操作符/去引操作符 Address/dereference operator(&)当我们声明一个变量的同时,它必须被存储到内存中一个具体的单元中。通常我们并不会指定变量被存储到哪个具体的单元中—幸亏这通常是由编译器和操作系统自动完成的,但一旦操作系统指定了一个地址,有些时候我们可能会想知道变量被存储在哪里了。 这可以通过在变量标识前面加与符号ampersand sign(&)来实现,它表示”...的地址“(”address of“),因此称为地址操作符(adress operator),又称去引操作符(dereference operator)。例如: ted = &andy;将变量andy的地址赋给变量ted,因为当在变量名称andy 前面加ampersand(&)符号,我们指的将不再是该变量的内容,而是它在内存中的地址。 假设andy 被放在了内存中地址1776的单元中,然后我们有下列代码: andy = 25;fred = andy;ted = &andy;其结果显示在下面的图片中: 我们将变量andy 的值赋给变量fred,这与以前我们看到很多例子都相同,但对于ted,我们把操作系统存储andy的内存地址赋给它,我们想像该地址为1776(它可以是任何地址,这里只是一个假设的地址),原因是当给ted 赋值的时候,我们在andy 前面加了ampersand(&)符号。 存储其它变量地址的变量(如上面例子中的ted),我们称之为指针(pointer)。在C++ 中,指针pointers 有其特定的优点,因此经常被使用。在后面我们将会看到这种变量如何被声明。 引用操作符Reference operator(*)使用指针的时候,我们可以通过在指针标识的前面加星号asterisk(*)来存储该指针指向的变量所存储的数值,它可以被翻译为“所指向的数值”(”value pointed by“)。因此,仍用前面例子中的数值,如果我们写: beth = *ted;(我们可以读作:”beth 等与ted所指向的数值“)beth 将会获得数值25,因为ted 是1776,而1776 所指向的数值为25。 你必须清楚的区分ted 存储的是1776,但*ted(前面加asterisk *)指的是地址1776中存储的数值,即25。注意加或不加星号*的不同(下面代码中注释显示了如何读这两个不同的表达式): beth = ted;// beth 等于 ted(1776)beth = *ted;// beth 等于 ted 所指向的数值(25) 地址或反引用操作符Operator of address or dereference(&)它被用作一个变量前缀,可以被翻译为“„的地址”(”address of“),因此:&variable1 可以被读作 variable1的地址(”address of variable1“)。引用操作符Operator of reference(*)它表示要取的是表达式所表示的地址指向的内容。它可以被翻译为“„指向的数值”(”value pointed by“)。 * mypointer 可以被读作 ”mypointer指向的数值"。继续使用上面开始的例子,看下面的代码: andy = 25;ted = &andy;现在你应该可以清楚的看到以下等式全部成立: andy == 25 &andy == 1776 ted == 1776 *ted == 25 第一个表达式很容易理解,因为我们有赋值语句andy=25。第二个表达式使用了地址(或反引用)操作符(&)来返回变量andy的地址,即 1776。第三个表达式很明显成立,因为第二个表达式为真,而我们给ted赋值的语句为ted = &andy。第四个表达式使用了引用操作符(*),相当于ted指向的地址中存储的数值,即25。 由此你也可以推断出,只要ted 所指向的地址中存储的数值不变,以下表达式也为真: *ted == andy 声明指针型变量Declaring variables of type pointer 由于指针可以直接引用它所指向的数值,因此有必要在声明指针的时候指明它所指向的数据类型。指向一个整型int或浮点型float数据的指针与指向一个字符型char数据的指针并不相同。 因此,声明指针的格式如下: type * pointer_name;这里,type 是指针所指向的数据的类型,而不是指针自己的类型。例如: int * number;char * character;float * greatnumber;它们是3个指针的声明,每一个指针指向一种不同数据类型。这三个指针本身其实在内存中占用同样大小的内存空间(指针的大小取决于不同的操作系统),但它们所指向的数据是不同的类型,并占用不同大小的内存空间,一个是整型int,一个是字符型char,还有一个是浮 软件程序员C方向技能描述 初级 软件程序员初级资格主要培养学生的基本编程能力,应具备以下技能: 1、熟练掌握C程序设计的基础知识、基本概念;掌握过程化程序设计的思想和编程技巧;理解算法的概念以及算法的表示方法,能使用C语言进行简单的程序设计。 2、熟练掌握C++语言的基本语法规则,掌握常用数据类型、函数、类、对象等基础知识;掌握面向对象程序设计的思想和编程技巧;能够使用C++语言进行简单程序设计。 3、掌握面向对象程序设计的基本方法,能进行简单的VC界面设计。 4、掌握基本的数据库知识,熟悉常见数据库在编程语言中的使用。 5、具有规范化、标准化的编程习惯和方法,符合软件工程要求。 中级 中级程序员资格主要培养学生的基本编程能力、数据库应用能力,具备初级的软件工程思想,应具备以下技能。 1、掌握过程化程序设计的思想和编程技巧;理解算法的概念以及算法的表示方法;能够使用C语言按照需求进行编码实现。 2、能够深刻理解和领会面向对象程序设计的特点和风格,掌握其方法和要领。 3、掌握面向对象程序设计的基本方法,熟练使用VC进行界面设计。 4、掌握一定的数据库理论知识,熟悉一种或两种数据库产品的使用,能够进行基本的数据库设计与分析能力,熟悉编程语言与数据库的连接和操作。 5、具备基本的软件工程思想,熟悉软件生命周期和常用开发模型。 6、具有规范化、标准化的编程习惯和方法,符合软件工程要求。 高级 高级程序员资格主要培养学生的数据库编程能力,具备系统的软件工程理论知识,应具备以下能力: 1、熟练掌握过程化程序设计的思想和编程技巧;熟悉数据结构,能够使用C语言能进行较复杂的算法编程。 2、深刻理解和领会面向对象程序设计的特点和风格,具有较高的使用C++语言解决实际问题的能力。 3、能正确运用面向对象的思维方法分析问题和解决问题,能够应用MFC来解决实际复杂的问题,具有创新意识。 4、掌握一定的数据库理论知识,熟悉一种或两种数据库产品的使用,能够进行基本的数据库设计。 6、具备系统的软件工程理论知识,有一定的软件工程实践经验,有较强的沟通能力和协调能力,能够应付软件开发中出现的大多数问题。 C程序员精通Perl 本书帮助有C和C++背景的程序员学习和使用Perl语言。 全书共17章,探讨了Perl语言的所有知识要点。既包括C程序员所熟悉的变量、数据结构、控制语句和子程序,又包括C中所没有的报告编写、文本表格操纵、CGI编程以及简单数据库接口,还讲述了如何充分利用CPAN库中广泛的Perl模块库。 本书可作为C和C++程序员学习Perl编程的参考书。对于Perl语言的初学者,本书可以帮助他们了解C和Perl之间可比较、可结合特性。 一:推理题。 1、一个大院子里住了50户人家,每家都养了一条狗,有一天他们接到通知说院子里有狗生病了,并要求所有主人在发现自己家狗生病的当天就要把狗枪杀掉。然而所有主人和他们的狗都不能够离开自己的房子,主人与主人之间也不能通过任何方式进行沟通,他们能做的只是通过窗户观察别人家的狗是否生病从而判断自己的狗病否。(就是说,每个主人只能看出其他49家的狗是不是生病,单独看自己的狗是看不出来的) 第一天没有枪声,第二天还是没有枪声,第三天传出一阵枪声,问有多少条狗被枪杀。 2、有四个人(A,B,C和D)要在一个月黑风高的夜里过一个很长的独木桥。桥只能一次乘载两个人,就是说每次最多两人同时过桥。过桥要用手电筒,而这四个人只有一只手电筒,也就是说两人共用这只手电筒过桥后,其中一人必须带着手电筒返回(没有其他方法),否则其他人就不能再过了。这四个人由于年龄和身体状况的差异,每个人过桥所需要的时间不同:A需要1分钟,B需要2分钟,C需要5分钟,D需要10分钟。由于共用一只手电筒的原因,当两人一同过桥时,过桥的时间是以其中慢的一人为准,比如A和C一起过桥要用5分钟。 现在问:要所有人过桥,最短要多少分钟,如何安排他们的过桥顺序? 二:编程题 1,用C语言实现一个revert函数,它的功能是将输入的字符串在原串上倒序后返回。用C语言实现函数void * memmove(void *dest,const void *src,size_t n)。memmove函数的功能是拷贝src所指的内存内容前n个字节到dest所指的地址上。用两个栈实现一个队列的功能?要求给出算法和思路!已知链表的头结点head,写一个函数把这个链表逆序 void reverse(node*& head) 写一个函数找出一个整数数组中,第二大的数 int find_sec_max(int data[] , int count) 已知strcpy的函数原型:char *strcpy(char *strDest, const char *strSrc)其中strDest 是目的字符串,strSrc 是源字符串。不调用C++/C 的字符串库函数,请编写函数 strcpy。 三:找错题 11 void test1(){ char string[10];char* str1 = “0123456789”;strcpy(string, str1);} 2: void test2(){ char string[10], str1[10];int i;for(i=0;i<10;i++){ str1[i] = 'a';} strcpy(string, str1);} 3: void test3(char* str1){ char string[10];if(strlen(str1)<= 10){ strcpy(string, str1);} } 试题4: void GetMemory(char *p){ p =(char *)malloc(100);} void Test(void){ char *str = NULL;GetMemory(str);strcpy(str, “hello world”);printf(str);} 试题5: char *GetMemory(void){ char p[] = “hello world”;return p;} void Test(void){ char *str = NULL;str = GetMemory();printf(str);} 试题6: void GetMemory(char **p, int num){ *p =(char *)malloc(num);} void Test(void){ char *str = NULL;GetMemory(&str, 100);strcpy(str, “hello”);printf(str);} 试题7: void Test(void){ char *str =(char *)malloc(100);strcpy(str, “hello”);free(str);...//省略的其它语句 } 编程题 答案1 char *revert(char * str){ int n=strlen(str);int i=0;char c;for(i=0;i { c=str;str=str[n-i];str[n-i]=c;} return str;} 答案2 void * memmove(void *dest,const void *src,size_t n){ assert((dest!=0)&&(src!=0));char * temp=(char *)dest;char * ss=(char *)src;int i=0;for(;i{ *temp++=*ss++;} return temp;} 答案3 设2个栈为A,B, 一开始均为空.入队: 将新元素push入栈A; 出队: 1将栈A中所有元素依次pop出并push到栈B; 2将栈B的栈顶元素pop出; 答案4 void reverse(node*& head) { if((head == 0)||(head->next == 0))return;// 边界检测 node* pNext = 0; node* pPrev = head;// 保存链表头节点 node* pCur = head->next;// 获取当前节点 while(pCur!= 0) { pNext = pCur->next;// 将下一个节点保存下来 pCur->next = pPrev;// 将当前节点的下一节点置为前节点 pPrev = pCur;// 将当前节点保存为前一节点 pCur = pNext;// 将当前节点置为下一节点 } } 链表正常的顺序是前一个节点的NEXT指向后一个节点。反转就是要将后一个节点的next指向前一个节点所以pCur->next = pprev;完成了这一个功能。但这只是完成了两个节点的反转,所以对应的要将当前 节点的next保存下来pNext = pcur->next;,用来当作下一次的当前节点PCur = Pnext;在下一次反转中,当前节点就变成了下一次反转中的前节点。pPrev = pCur;一直到当前节点为NULL,也就是全部转化为止; char *strcpy(char *strDest, const char *strSrc){ if(strDest == NULL || strSrc == NULL)return NULL;if(strDest == strSrc)return strDest;char *tempptr = strDest;while((*strDest++ = *strSrc++)!= ‘/0’);return tempptr;} 分 void strcpy(char *strDest, char *strSrc){ while((*strDest++ = * strSrc++)!= ‘ ’);} 4 分 void strcpy(char *strDest, const char *strSrc)//将源字符串加const,表明其为输入参数,加2 分 { while((*strDest++ = * strSrc++)!= ‘ ’);} 7 分 void strcpy(char *strDest, const char *strSrc){ //对源地址和目的地址加非0 断言,加3 分 assert((strDest!= NULL)&&(strSrc!= NULL));while((*strDest++ = * strSrc++)!= ‘ ’);} 找错题答案: 试题4 传入中GetMemory(char *p)函数的形参为字符串指针,在函数内部修改形参并不能真正的改 变传入形参的值,执行完 char *str = NULL;GetMemory(str);后的str 仍然为NULL; 试题5 中 char p[] = “hello world”;return p;的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。 试题6 的GetMemory 避免了试题4 的问题,传入GetMemory 的参数为字符串指针的指针,但是在 GetMemory 中执行申请内存及赋值语句 *p =(char *)malloc(num);后未判断内存是否申请成功,应加上: if(*p == NULL){...//进行申请内存失败处理 } 试题7 存在与试题6 同样的问题,在执行 char *str =(char *)malloc(100);后未进行内存是否申请成功的判断;另外,在free(str)后未置str 为空,导致可能变成一个“野”指 针,应加上: str = NULL;试题6 的Test 函数中也未对malloc 的内存进行释放。 剖析: 试题4~7 考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60 的 错误。但是要完全解答正确,却也绝非易事。对内存操作的考查主要集中在:(1)指针的理解; (2)变量的生存期及作用范围;(3)良好的动态内存申请和释放习惯。 实习日记 1第一次是来面试,当时感觉不怎么好,面试的人问了一些问题有些没回答上来,这并不奇怪,C#自己学习了2个月,然后就没怎么用,回答不上我觉得可以理解。没想到还能第二次来到公司,由于对环境还比较陌生,所以今天大概熟悉了一点,成都这边有50多个员工,属于研发中心,主要从事无线微波得较大型的设备的再加工,听同事说以前在北京,但由于北京人力资源太贵所以来到成都,这听起来怎么像是在压榨的感觉啊,没关系,只是来实习而已。 对于今天的第一个小任务,要求:界面、数据压入堆栈、简单冒泡排序实现数据有序输出、堆排序输出。让我快速地熟悉了C#,从界面到后台都得到了加强,此外在实现过程中对用到的冒泡排序和堆排序有了更深的理解,并能用C#熟练的实现。 实习日记 2今天对昨天的实例进行了改进和提高,将堆排序和冒泡排序封装在一个动态链接库中,提供函数调用和事件委托。此外加入XML进行配置,在XML中存入相关配置信息,提供一个单独的XmlClass对其进行操作,加深了对xml的理解。为增加程序的性能,采用简单工厂模式对其客户端和算法操作进行分离,有很好的可扩展性,对于C#的动态链接库和XML配置不怎么熟悉,花了很长一段时间看书和网上查资料,才开始动手写代码,不过感觉还是很快,因为自己有编程的能力,可以说掌握了一种快速编程的方法,所以就能驾轻就熟。 实习日记 3今天采用抽象工厂模式对实例进行改进,以达到动态加载算法实例,抽象工厂模式属于设计模式中的内容,自己在学习视频的时候注意到了,所以之前就买了本大话设计模式来学习,可以说了解了大部分设计模式。今天遇到后自己心里也有底,温故一下书就知道怎么做 了,可以说这是我学习的方法。 针对以上,在dll中提供一个接口,两个算法都继承了该接口,对于该dll的操作都通过接口ISort进行,此外,修改工厂为抽象工厂,提供一个接口创建算法,在算法中返回ISort实例,在主函数中使用该实例进行对应的排序算法操作,充分显示了抽象工厂的作用,本例子将反射和抽象工厂模式结合,增加了程序的可扩展性和可维护性。 实习日记 4由于自己编程能力还算可以吧,所以软件组的老大就叫我加入开始做一些这次项目的东西,说了一句“我可是没把你当实习生哦。”,这话一听当时感觉还好,不过后来一想,我却是吃亏了,他不把我当实习生但给我的却是实习生工资,这点有点想不过去,不过没关系,实习嘛就是学习,有基本工资也不错了,关键的公司提供的条件那是在学校里所得不到的:商业性的问题。 今天将上述实例的方法用到本次项目中,开始项目中的一些任务。仍然使用抽象工厂模式,将自定义窗体封装到类库中,将窗体的控件预留到外部进行实施定义;在XML文件中配置窗体的实际参数,利用反射按从XML中读出的参数动态加载窗体。将上述的实例方法运用到了实际项目中。 实习日记 5昨天的任务基本已完成,今天温习了一下加深了对知识的理解,可以说掌握得比较牢固了,今天又有新的任务,可以说每天都在进步。 按组长要求编写RankStyle=Group时的窗体排列方式,在XML中配置控件或窗体的参数,利用XML读接口从XML中读出参数,为了使程序具有灵活性,在组长的提示下将窗体的配置按照窗体的Name属性进行配置,之前是利用字段的方式写在XML中,但是组长说这样的话在程序中查找不方便,可我觉得没什么啊,当然了,对于我这个没有工作经验的人来说看什么都是可以的,所以要多学习学习。在FormControl中利用双层循环将控件容器中的控件按Name与读出的配置进行匹配,如果在XML中配置了该窗体,就设置该窗体的一些属性。个人感觉实现了该功能吧,最后经过组长看了一下审核基本通过。要求: 根据Group,从XML中读取控件配置信息,动态设置控件在主窗体中的显示。 实习日记6 每天都有不同的要求,哎,果然和学校是不一样啊,今天将DLL中的控件更换成窗体,方法仍然采用反射+抽象工厂模式。 虽然自己有一定编程经验,但是遇到问题是不可避免的,今天在编写过程中遇到了一个问题,就是当窗体接口和窗体类库放在一起的时候,在工厂中创建的实例无法装换成接口类型,经过多次测试和上网查阅相关资料,发现控件类实现接口,不能将接口放在同一个类库中,针对以上问题在网上查阅相关资料,结果还是没有相同的主题,没办法,只有硬着头皮在网上找,没有主题就找有没有相关的例子,结果还是没有,差点就无语了,突然看到有类似方法的一个例子,真是柳暗花明又一村啊,仔细看了看,发现他和我的结构有些差异,所以调了一下结构,一编译果然痛过了,就是将两个类库分离,再在接口中加入工厂便可创建实例并转换成接口。在主窗体中只需要引用该接口类库便可达到要求,使程序耦合性降低了。 实习日记7 由于黎甫对DLL的要求发生变化,今天就不断修正DLL程序,并验收,再结合Group的窗口排列方式对DLL中的窗体进行测试。我现在才明白,编写代码是一件很容易的事,但是编写好的代码却是很难的事情,好的代码要有可维护性、可扩展性,这样在修改的话就很简单,可不能像我昨天写得代码,根本谈不上艺术,所以一旦面对客户的要求,改动就很困难,这一点值得我继续学习。 至于可扩展性也是对维护的扩充,因为顾客的要求有可能随时都在改变,我们也得适应客户的要求,如果软件有了可扩展性,那么它的改动就比较小,就能适应顾客的要求。所有上面说的这些都是自己要学习的地方,这样在以后的商业开发中才能游刃有余,应对自如。 实习日记8 回顾前几天所做的练习,对遇到的问题进行解决,在网上查阅相关资料独立解决,并对之前的一些项目练习做好备份。将这几天练习中所用的编程方法都串联了一遍,形成了固定的模式,以便在今后可以快速的使用。 实习日记9 今天算是正式开始做公司的项目,上午黎甫和秦阳在会议室给我讲解了项目的一些功能并进行讨论,大致理解了项目后,分配了自己的一部分任务,整理过后在多次与秦阳的沟通请教下明白了自己的任务,下午秦阳建好VSS,我拷贝项目进行框架整合和测试。在编写代码的过程中也涉及了一些问题,通过看MSDN和上论坛,基本得到解决,同时也了解了一些C#其它的相关知识。 实习日记10 今天继续昨天的任务,做一个小测试,将现今做的项目所有代码进行整合测试,测试要求是在数据库中配置菜单项,动态添加几项菜单项,在不同点击下动态从DLL中加载窗体读取配置文件进行排列,顺利完成。第二篇:程序员C方向技能描述技能描述
第三篇:C程序员精通Perl
第四篇:51CTO下载-C程序员面试题
第五篇:C程序员实习日记