目录
1.1如何撰写C++程序(How to Write a C++ Program)
- 每个C++程序都是从一个名为
main
的函数开始执行。 - 程序无误时我们令
main()
返回0;若返回一个非0值,表示程序执行过程中发生了错误。 - 类(
class
)是用户自定义的数据类型(user-defined data type)。class机制让我们得以将数据类型加入我们的程序中,并有能力识别它们。面向对象的类层次体系(class hierarchy)定义了整个家族体系的各相关类型,例如终端与文件输入设备、终端与文件输出设备等。 - C++事先定义了一些基础数据类型:➀布尔值(Boolean)、➁字符(character)、➂整数(integer)、➃浮点数(floating point)。
- class的定义一般来说分为两部分,分别写在不同的文件中。其中之一是所谓的“头文件(header file)”,用来声明该class所提供的各种操作(operation)。另一个文件,程序代码文件(program text),则包含了这些操作行为的实现内容(implemetation)。
- C++标准的“输入/输出”名为
iostream
,其中包含了相关的整套class,用以支持对终端和文件的输入与输出。我们必须包含iostream
的库的头文件,才能够使用它:#include <iostream>
。
第一个C++程序:
#include <iostream>
#include <string>
using namespace std;
int main() {
string user_name;
cout << "Please enter your first name:";
cin >> user_name;
cout << '\n'
<< "Hello, "
<< user_name
<< " ... and goodbye!\n";
return 0;
}
Please enter your first name: |anna
Hello, anna ... and goodbye!
- 所谓
字符常量
(character literal)系由一组单引号括住。
第一类可打印字符,例如英文字母(‘a’、‘A’)、数字、标点符号(’;’、’-’)。
另一类是不可打印字符,例如换行符(’\n’)或制表符(tab, ‘\t’)。由于不可打印字符并无直接的表示法,所以必须以两个字符所组成的字符序列来表示。 using
和namespace
都是C++中的关键字。std是标准库所驻之命名空间(namespace)的名称。标准库所提供的任何事物(诸如string
class以及cout
、cin
这两个iostream
类对象)都被封装在命名空间std内。- 所谓
命名空间(namespace)
是一种将库名称封装起来的方法。通过这种方法,可以避免和应用程序发生命名冲突的问题。(所谓命名冲突
是指在应用程序内两个不同的实体[entity]具有相同的名称,导致程序无法区分两者。命名冲突发生时,程序必须等到命名冲突获得解析[resolve]之后,才得以继续执行。)命名空间像是在众多名称的可见范围之间竖起的一道道围墙。 - 若要在程序中直接使用
string
class以及cout
、cin
这两个iostream
类对象,不仅要包含< string>、< iostream>头文件,还得让命名空间std
内的名称曝光:using namespace std;
否则,得这么使用:
std::string user_name;
std::cout << "Please enter your first name:";
std::cin >> user_name;
1.2对象的定义与初始化(Defining and Initializing a Data Object)
- C++支持三种浮点数类型,分别是
float
:单精度(single precision)浮点数、double
:双精度(double precision)浮点数、long double
:长双精度(extended precision)浮点数。
例:以标准库中的复数(complex number)类为例,它就需要两个初值,一为实部,一为虚部。于是便引入了用来处理“多值初始化”的构造函数初始化语法(constructor initialization syntax)。
#include <complex>
complex<double> purei(0, 7);
出现于complex之后的尖括号,表示complex是一个模板类(template class)。尖括号里面是double,即是将complex类的成员绑定至double类型。
- 关键字
char
表示字符(character)类型。有一些特别的内置字符常量——转义字符[escape sequence],例如:
‘\n’——换行符(newline)
‘\t’ ——制表符(tab)
‘\0’——null
‘’’——单引号(single quote)
‘"’——双引号(double quote)
‘\’——反斜线(backslash)
- C++提供内置的Boolean类型。Boolean对象系由关键字
bool
指出,其值为true或false常量。如:
bool go_for_it = true;
- 对于像圆周率这类永恒不变的值,为避免无意间更改此类对象的值,C++的
const
关键字可以派上用场:
const int max_tries = 3;
const double PI = 3.14159;
运算符的优先级
优先级 | 运算符(从上到下,从左到右优先级由高到低) |
---|---|
*1 | [] () . -> |
2 | -(负号) |
2 | (类型)(强制类型转换) |
2 | ++ - - |
2 | *(取值运算符) |
2 | &(取地址运算符) |
2 | ! ~ sizeof |
3 | / * % |
4 | + - |
5 | << >> |
6 | > >= < <= |
7 | == != |
8 | &(按位与) |
9 | ^(按位异或) |
10 | |(按位或) |
11 | && |
12 | ∥(逻辑或) |
13 | ?: |
14 | =(赋值运算符) /= *= %= += -= <<= >>= &= ^= |= |
15 | ,(逗号运算符) |
// 判断ival是否为偶数
bool isEven = !ival % 2;// 错!
bool isEven = !(ival % 2);// 正确的写法
false在算术表达式中被视为0,true被视为1。根据C++默认的运算符优先级,除非ival的值为0,否则上述错误的表达式便成了0%2。(!0=1[true],!123=0[false])。要注意在C/C++中,非0==true,0=false。
1.4条件语句和循环语句
switch (num_tries) {
case 1:
cout << "Excellent!";
break;
case 2:
cout << "Also Good! Wrong a second time." << endl;
break;
case 3:
cout << "Normal! Wrong a third time." << endl;
break;
default:
cout << "Lost your mind, right?";
break;
}
如果num_tries的值为2,且不加break;
语句,输出如下:
Also Good! Wrong a second time.
Normal! Wrong a third time.
Lost your mind, right?
可以看到,如果不加break;则不判断是否值相等而是直接进入到下面case语句。
下面这个例子可以说明这种**“向下穿越”**的合理性:
switch (next_char) {
case 'a': case 'A':
case 'e': case 'E':
case 'i': case 'I':
case 'o': case 'O':
case 'u': case 'U':
++vowel_cnt;
break;
// ...
}
1.5如何运用Array和Vector(How to Use Arrays and Vectors)
以下是六种数列的前八个元素值:
Fibonacci: 1,1,2,3,5,8,13,21
Lucas: 1,3,4,7,11,18,29,47
Pell: 1,2,5,12,29,70,169,408
Triangular: 1,3,6,10,15,21,28.36
Square: 1,4,9,16,25,36,49,64
Pentagonal: 1,5,12,22,35,51,70,92
使用可存放连续整数值的容器(container)类型
。这种类型不仅允许我们以名称(name)取用容器中的元素,也允许我们以容器中的位置来取用元素。
C++允许我们以内置的数组(array)类型或标准库提供的vector
类来定义容器。一般而言,建议使用vector甚于array。不过,大量现存的程序代码都使用array。因此,了解如何善用这两种方式便相当重要。
// 定义数组(array)
const int seq_size = 18;
int pell_seq[seq_size];
// 定义vector object
#include <vector>
vector<int> pell_seq(seq_size);
定义vector object,首先必须包含vector头文件。vector是一个class template,所以必须在类名之后的尖括号内指定其元素类型,其大小则写在小括号中。此处所给予的大小并不一定得是个常量表达式。
常量表达式(constant expression): 一个不需要在运行时求值的表达式。即在编译期间就能确定其值。
对于Pell数列,用array实现:
int pel_seq[seq_size] = {1,2};// 初始化列表
for(int ix=2; ix<seq_size; ++ix)
pell_seq[ix] = pell_seq[ix-2] + 2 * pell_seq[ix-1];
vector不支持初始化列表。有个冗长的写法:
vector<int> elem_seq(seq_size);
elem_seq[0] = 1;// 或elem_seq.push_back(1)
elem_seq[1] = 2;
...
elem_seq[17] = 22;
另一个方法是利用一个已初始化的array作为该vector的初值:
int elem_vals[seq_size] = {
1,2,3,// Fibonacci
3,4,7,// Lucas
2,5,12,// Pell
3,6,10,// Triangular
4,9,16,// Square
5,12,22// Pentagonal
}
vector<int> elem_seq(elem_vals, elem_vals + seq_size);
上例中传入的两个参数值都是实际内存位置,它们标示出“用以将vector初始化”的元素范围(size=end-begin)。本例中标示出了elem_vals内的18个元素,并将它们复制到elem_seq。
与array不同的是vector知道自己的大小:elem_seq.size()返回elem_seq这个vector所包含的元素个数。elem_seq.empty()还可判断vector容器是否为空。
cout << "The first " << elem_seq.size() << " elements of the Pell Series:\n\t";
for(int ix=0; ix<elem_seq.size(); ++ix)
cout << pell_seq[ix] << ' ';
1.6指针带来弹性(Pointer Allow for Flexibility)
如何增加以上数列程序的弹性?一种可能的解法是同时维护6个vector,每个数列一个。
这里我们使用指针(pointer),舍弃以名称指定的形式,间接地访问每个vector,借此达到透明化的目的。
指针为程序引入了一层间接性。我们可以操作指针(代表某特定内存地址),而不再直接操作对象。
我们定义一个可以对整数vector寻址的指针。每一次循环迭代,便更改指针值,使它定位到不同的vector。
本例中,指针主要做两件事:➊增加程序本身的弹性;➋同时也增加了直接操作对象所没有的复杂度。
int ival = 1024;// 定义一个int对象,并赋值1024
int *pi;// pi是一个[int类型对象]的指针
ival;// 计算ival的值
&ival;// 计算ival所在内存的地址
int *pi = &ival;// 将pi的初值设置为ival所在的内存地址
pi;// 计算pi所持有的内存地址(此举形同操作“指针对象”本身)
*pi;// 求ival的值(等于是操作pi所指的对象)
- 使用指针时,必须在
提领(dereference)
之前先确定它的确指向某对象。否则写*pi时,可能会使程序在运行时产生错误。 - 一个未指向任何对象的指针,其地址为0。称之为
null指针
。任何指针都可以被初始化,或是令其值为0。
// 初始化每个指针,使它们不指向任何对象
int *pi = 0;
double *pd = 0;
string *ps = 0;
为了防止对null指针进行提领操作,我们可以检验该指针所持有的地址是否为0:
if (pi && *pi != 1024)// 只有在pi持有一个非0值时,求值结果才为true
*pi = 1024;
开始写我们的程序:
vector<int> fibonacci, lucas, pell, triangular, square, pentagonal;
vector<int> *pv = 0;
// pv可以依次指向每一个表示数列的vector
pv = &fibonacci;
...
pv = &pentagonal;
以上这种赋值为牺牲程序的透明性。
程序的透明性: 就是说程序内部的东西是不可见的。比如一个函数,只需知道它的功能和输入何种参数调用,而不需要了解函数内部的处理方法。
另一种方案是将每个数列的内存地址存入某个vector中,这样就可以通过索引的方式,透明地访问这些数列:
const int seq_cnt = 6;
vector<int> *seq_addrs[seq_cnt] = {// *优先级低于[],所以seq_addrs是一个存放指针的数组
&fibonacci, &lucas, &pell, &triangular, &square, &pentagonal
}
vector<int> *current_vec = 0;
...
for (int ix=0; ix<seq_cnt; ++ix) {
current_vec = seq_addrs[ix];
// 所有要显示的元素都通过current_vec间接访问到
}
产生伪随机数(pseudo-random number)
可以通过C语言标准库中的rand()和srand()两个函数达成:
#include<cstlib>
srand(seq_cnt);
seq_index = rand() % seq_cnt;// 使随机数介于0~5,以便成为本例中的一个有效索引
current_vec = seq_addrs[seq_index];
srand()的参数是随机数生成器种子(seed)。
rand()每次调用都返回一个在[seed, INT_MAX)区间的一个整数。
函数一:int rand(void);
产生随机值,从srand(seed)中指定的seed开始,返回一个[seed, INT_MAX)间的随机整数。
函数二:void srand(unsigned seed);
参数seed是rand()的种子,用来初始化rand()的起始值。
可以认为rand()在每次被调用的时候,它会查看:
1) 如果用户在此之前调用过srand(seed),给seed指定了一个值,那么它会自动调用srand(seed)一次来初始化它的起始值。
2) 如果用户在此之前没有调用过srand(seed),它会自动调用srand(1)一次。
通常可以利用系统时间来改变系统的种子值,即srand(time(NULL))。
// 打印int最大值、long最小值(C)
#include<limits.h>
printf("%d %d", INT_MAX, LONG_MIN);
// 打印int最大值、double最小值(C++)
#include<limits>
int max_int = numeric_limits<int>::max();
double min_dbl = numeric_limits<double>::min();
使用class object的指针,和使用内置类型的指针略有不同。这是因为class object关联了一组我们可以调用(invoke)的操作(operation)。例如,检查fibinacci vector的第二个元素是否为1,可以这么写:
if (!fibonacci.empty() && (fibonacci[1] == 1))
fibonacci与empty()之间的句点,称为dot成员选择运算符(member selection operator),用来选择我们想要进行的操作。
如果要通过指针来选择操作,必须改用arrow成员选择运算符:
if (pv && !pv->empty() && ((*pv)[1] == 1))
// 运算符优先级(从左往右优先级由高到低):[] () . -> * !
1.7文件的读写(Writing and Reading Files)
我们应该让用户的分数可以在不同的“会话(session)”累计使用。为达到这个目的,我们必须:
(1)每次执行结束,将用户的姓名与会话的某些数据写入文件;
(2)在程序开启另一个会话时,将数据从文件中读回。
要对文件进行读写操作,首先得包含fstream
头文件:#include<fstream>
。
为打开一个可供输出的文件,定义一个ofstream
(供输出的file stream)对象,并将文件名传入:
// 以输出模式开启seq_data.txt
ofstream outfile("seq_data.txt");
如果指定的文件不存在,便会生成一个文件并打开供输出使用。
如果指定的文件已存在,便会丢弃原文件数据并打开供输出使用。
如果指定的文件已存在,不想丢弃原文件数据并以追加模式(append mode)打开这个文件,就得提供第二个参数ios_base::app
给ofstream对象:
// 以追加模式(append mode)打开文件,新数据会被添加到末尾
ofstream outfile("seq_data.txt, ios_base::app);
文件有可能打开失败。在进行操作前,必须确定文件的确打开成功。最简单的方法是检验class object的真伪:
if (!outfile)// 如果outfile的求值结果为false,表示此文件未成功打开
cerr
代表标准错误设备(standard error)。和cout
一样,cerr将其输出结果定向到用户终端。两者的唯一差别是,cerr的输出结果并无缓冲(buffered)情形——它会立即显示于用户终端中。cerr和clog
都是标准错误流,区别在于cerr不经过缓冲区,直接向显示器输出信息,而clog中的信息默认会存放在缓冲区,缓冲区满或者遇到endl时才输出;默认情况下,写到clog的数据是被缓冲的。clog通常用于报告程序的执行信息,存入一个日志文件中。
if (!outfile)
cerr << "Oops! Unable to save session data!\n";
else
outfile << usr_name << ' ' << num_tries << ' ' << num_right << endl;
seq_data.txt中的内容格式如下:
anna 24 19
danny 16 12
...
endl
是事先定义好的操纵符(manipulator)
,由iostream library提供。其作用是会插入一个换行符,并清除输出缓冲区(output buffer)的内容。还有一些事先定义好的操纵符,如dec
、hex
、oct
、setprecision(n)
(设定浮点数显示精度为n)。
#include <iostream> // std::cout, std::dec, std::hex, std::oct
int main () {
int n = 70;
std::cout << std::dec << n << '\n';// 70(十进制)
std::cout << std::hex << n << '\n';// 46(十六进制)
std::cout << std::oct << n << '\n';// 106(八进制)
return 0;
}
如果要打开一个可供读取的文件,定义一个ifstream
(供输入的file stream)对象,并将文件名传入。
// 以读取模式(input mode)打开infile
ifstream infile("seq_data.txt");
int num_tries = 0;
int num_cor = 0;
if (!infile) {
// 由于某种原因,文件打不开......
// 假设这是一位新用户......
} else {
// 检查这个用户是否曾经玩过,每一行格式是:name num_tries num_correct
string name;
int nt;// 猜过的总次数(num_tries)
int nc;// 猜对的总次数(num_correct)
while (infile >> name) {
infile >> nt >> nc;
if (name == usr_name) {
// 找到他了
cout << "Welcome back, " << usr_name
<< "\nYour current score is " << nc
<< " out of " << nt
<< "\nGood Luck!\n";
num_tries = nt;
num_cor = nc;
}
}
}
infile >> name;
这条语句的返回值就是从infile读到的class object。一旦读到文件末尾,对读入class object求值结果就会是false。
为什么会ifstream读取文件时会跳过空格和换行符呢?
先看下面一个上面有提到的操纵符(manipulator)的例子(ws
、skipws
、noskipws
操纵符):
#include <iostream> // std::cout, std::skipws, std::noskipws
#include <sstream> // std::istringstream
int main () {
char a, b, c;
std::istringstream iss (" 123");
iss >> std::skipws >> a >> b >> c;
std::cout << a << b << c << '\n';
iss.seekg(0);
iss >> std::noskipws >> a >> b >> c;
std::cout << a << b << c << '\n';
return 0;
}
输出:
123
1
#include <iostream> // std::cout, std::noskipws
#include <sstream> // std::istringstream, std::ws
int main () {
char a[10], b[10];
std::istringstream iss ("one \n \t two");
iss >> std::noskipws;
iss >> a >> std::ws >> b;
std::cout << a << "," << b << '\n';
return 0;
}
输出:
one,two
官方http://www.cplusplus.com/reference
对std::ws
的解释:
从输入序列的当前位置提取尽可能多的空白字符。一旦发现非空白字符,提取就停止。这些被提取的空白字符将被丢弃。basic_istream对象默认设置了skipws标志。
对std::skipws
的解释:
设置了skipws格式标志后,将从流中读取并丢弃所需的所有空白字符,直到之前找到一个非空白字符为止。制表符、回车符和空格都被认为是空格。
对std::noskipws
的解释:
清除str流的skipws格式标记。当没有设置skipws格式标志时,流上的所有操作都将初始空白字符视为要提取的有效内容。制表符、回车符和空格都被认为是空格。
对于字符流"one \n \t two",先是设置操纵符noskipws,遇到空白字符停止读取,这样读取到字符数组a中的内容就是"one",然后设置操纵符为ws,遇到空白字符丢弃,直到遇到非空白字符开始正式读取,这样读取到字符数组b中的内容就是"two"。
函数
int isspace(int c);
可用于检测字符c是否是空字符:
空白字符包括:
’ ’ (0x20) space (SPC)
‘\t’ (0x09) horizontal tab (TAB)
‘\n’ (0x0a) newline (LF)
‘\v’ (0x0b) vertical tab (VT)
‘\f’ (0x0c) feed (FF)
‘\r’ (0x0d) carriage return (CR)
如果想要同时读写同一个文件,得定义一个fstream
对象。为了以追加模式(append mode)打开,我们得传入第二个参数值为ios_base::in|ios_base::app
:
fstream iofile("seq_data.txt", ios_base::in|ios_base::app);
if (!iofile)
// 由于某些原因,文件无法打开!
else {
// 开始读取之前,将文件重新定位至起始处
iofile.seekg(0);
// 其他操作都和先前讨论的相同
}
类层次结构如下: