目录

  • 前言 preface
  • 第一章 C++编程基础 Basic C++ Programming
    • 简介
    • 读书笔记
      • 1.1 如何撰写C++程序
      • 1.2 对象的定义与初始化
      • 1.3 撰写表达式
      • 1.4 条件语句和循环语句
      • 1.5 如何运用Array和Vector
      • 1.6 指针带来弹性
      • 1.7 文件的读写
    • 习题解答
      • 第一章
  • 第二章 面向过程的编程风格 Procedural Programming
    • 读书笔记
    • 习题解答
  • 第三章 泛型编程风格 Generic Programming
    • 读书笔记
    • 习题解答
  • 第四章 基于对象的编程风格 Object-Based Programming
    • 读书笔记
    • 习题解答
  • 第五章 面向对象编程风格 Object-Oriented Programming
    • 读书笔记
    • 习题解答
  • 第六章 以template进行编程 Programming with Templates
    • 读书笔记
    • 习题解答
  • 第七章 异常处理 Exception Handling
    • 读书笔记
    • 习题解答

前言 preface

  之所以选择这本《Essential C++ 中文版》而不是谭浩强的《C++程序设计》或C++圣经——《C++ Primer》作为正式学习C++的第一本书籍原因有以下几点。
  首先,正如《Essential C++》前言所说,这本书只有薄薄276页,而《C++ Primer》却厚达1237页。对于想要快速入门C++的我来说,这本短小精悍的轻量级书籍再适合不过。虽然轻薄短小的书籍让我很是开心,但难免产生一些困惑:这本书真的能让我入门语法复杂、概念繁多、内容广泛的C++语言吗?当我浏览完这本书的前言和目录后,我的困惑烟消云散。就是它了!
  本书的译者侯捷在译序中谈到,作为一本优秀教科书,轻薄短小不是重点,素材选择与组织安排,表达的精确与阅读的顺畅,才是重点。起步固然可从轻松小品开始,但如果碰上大部头巨著就退避三舍逃之夭夭,面对任何技术只求快餐速成,学编程语言却从来不编写程序,那就绝对没有成为高手乃至专家的一天。啊,感同身受,恨不得一天就学完这本书,去啃《C++ Primer》了。
  其次,笔者在大一上C语言程序设计这门课时用的就是谭浩强的书,再配上半吊子老师的讲授直接毁了我对学会编程的美好愿景,从此就对谭深恶痛绝。毫不夸张的讲,这本书击垮了当时物理学院所有人对编程的信心,之后对编程更是敬而远之,哎,学物理的不会编程,能走多远呢?好在期末复习时,偶然获得一份PDF版《C primer plus》,短短三天,从无到有,笔试和上机考取了物理学院最高分数。
  最后,谭浩强的这本书好像还是C++03,在2021年学习这个版本真的不合适。

第一章 C++编程基础 Basic C++ Programming

简介

  本章从一例小程序开始,通过此程序复习C++程序语言的基本组成。其中包括:
  1.基本数据类型:布尔值(Boolean)、字符(character)、整数(integer)、浮点数(floating point)。
  2.算术运算符、关系运算符以及逻辑运算符,用来操作1中基础数据类型。包含常简单的基本运算符:加法运算符(+)、相等运算符(==)、小于等于运算符(<=)、赋值运算符(assignment,=),还包含一些特殊运算符:递增运算符(++)、条件运算符(?:)、复合赋值运算符(+=,-=,*=等)运算符。
  3.条件分支语句(if else)和循环控制语句(for,while),用以改变程序控制流程。
  4.一些复合类型,如指针(重点)和数组。指针用来间接参考一个已经存在的对象。
  5.一套标准的通用的抽象化库,例如字符串(string)和向量(vector)。

读书笔记

1.1 如何撰写C++程序

  1.C++程序都是从一个名为main的函数开始执行,它是由用户自行撰写的函数,其通用形式如下:

int main (){//用户编写的程序代码在此处
//...
}

  2.int 是C++程序语言的关键字(keyword,由程序语言预先定义的一些具有特殊意义的名称),int 表示语言内置的整数数据类型。

  3.函数(function)是一块独立的程序代码序列(code sequence),能够完成一些功能。其包含四个部分:返回值类型(return type)、函数名称、参数列表(parameter list)和函数体(function body)。

  函数返回值通常用来表示函数运算结果。1.中提到的main函数返回整数类型(int main),习惯上,程序执行无误时我们令main函数返回0,若返回非零值,表示程序在执行过程中发生了错误。

  函数名称由程序员决定,最好可以清楚提供此函数的功能。main并非是程序语言定义的关键字,但在C++的编译系统中会假定存在main()函数,如果没有定义则程序无法执行。

  参数列表用来表示函数执行时,调用者可以传给函数的类型列表。列表之中用逗号隔开各个类型。例如:

int min(int val1,int val2){//程序代码置于此处return (val1<val2)?:val1:val2;//返回最小值
}

  函数体由大括号标出,其中含有提供此函数运算的程序代码(如上例所示)。

  4.类(class)是用户自定义的数据类型(user-defined data type)。class机制让我们得以将数据类型加入我们的程序中,并有能力识别他们。class机制,赋予了我们增加程序内之类型抽象化层次的能力。面向对象的类层次体系(class hierarchy)定义了整个家族体系的各相关类型如终端与文件输入设备,终端与文件输出设备等。关于类和面向对象的程序设计(object-oriented programming)这两个课题,本书在之后会大量涉及。
  关于class本书有个很形象的例子:照相机可以用七个浮点数来表示,其中六个分别组成了两组x、y、z坐标,剩下一个aspect ratio来描述照相机窗口宽高比。以class这种方式我们不再直接面对七个浮点数,而是转为对Camera class的操作。
  class的定义一般分为两个部分,分别写在不同的文件中,其中之一是所谓“头文件(head file)”,用来声明该class所提供的各种操作行为(operation)。另一个是文件是程序代码文件(program text),包含这些操作内容的实现内容(implemention)。

  5.数据的输入输出并非C++语言本身定义的一部分,而是C++的一套面向对象的类层次对象体系提供支持,并作为C++标准库(standard library)的一员。
  C++标准的输入输出库名为iostream,其中包含了相关的整套class,用以支持对终端和文件的输入和输出。欲使用class,我们必须现在程序中包含其头文件,头文件可以让程序知道class的定义。欲使用iostream,则必须包含iostream库的相关头文件,例如:

#include <iostream>

  将信息写到终端可利用已经定义好的cout对象,而output运算符(<<)可将数据定向到cout,例如:

cout<< "Please enter your first name:";

  在此之后就可以在用户终端看见如下信息:
    Please enter your first name:

  6.定义一个对象用来储存用户的first name,较适合的数据类型是标准库中的string class:

string user_name;//声明语句(declaration statement)

  这样就定义了一个user_name对象,它属于string class。只这样声明还是不够,因为还必须让此程序知道string class 的定义,因此还必须在程序中包含string class的头文件:

#include <string>

  7.通过input运算符(>>)将输入内容定向到具有适当类型的对象身上:

cin>>user_name;

  于是在用户终端上显示如下:
    Please enter your first name:anna
  如果想要换行输出即将输出位置(屏幕上的光标)调到下一行起始处。将换行符(newline)字符常量写至cout即可:

cout<<'\n';

  想要输出用户的名字(已储存在user_name这个string 对象中)可以这样:

cout<<user_name;
cout<<"good bye";

  一般来说,所有内置类型都可用同样的方式来输出,只需换掉output运算符右方的即可,例如:

cout<<"3+4=";
cout<<3+4;
cout<<'\n';
//如果嫌连续数行的输出语句烦人,也可以将数段内容连成单一的输出语句
cout<<"3+4=";<<3+4<<'\n';

  8.using namespace std。using和namespace都是C++关键字,std是标准库所驻之命名空间(namespace)的名称。标准库所提供的任何事物(诸如string class 以及cin、cout这两个iostream类对象)都被封装在命名空间std内。
  命名空间是一种将库名称封装起来的方法,通过这种方法可以避免和应用程序发生命名冲突的问题。
  命名冲突是指应用程序内两个不同实体(entity)具有相同名称,导致程序无法区分两者。命名冲突发生时,程序必须等到该命名冲突获得解析(resolve)之后,才得以继续执行。
  命名空间就像是在众多名称的可见范围之间竖起的一道道围墙。
  若要在程序中使用string class以及cin、cout这两个iostream类对象,不仅需要包含string和iostream头文件,还要让命名空间std内的名称曝光,using namespace std就是让命名空间曝光的最简单方法。

  至此,我们可以将所有程序片段组合在一起了,便写出我们第一个完整的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 good bye!\n";return 0;//表示程序未出错误。
}

1.2 对象的定义与初始化

  1.定义对象要为它命名,并赋予它数据类型。对象名称可以是任何字母、数字、下划线的组合,区分大小写,但不可由数字开头。当然,任何命名都不能和程序语言本身的关键字完全一致。

  2.另外有一种不同的初始化语法,称为“构造函数语法”(constructor syntax):

int num_tries(0);

  为什么要有两种不同的初始化语法呢?用assignment 运算符(=)进行初始化这个操作沿袭自C语言。如果对象属于内置类型,或者对象可以单一值加以初始化,这种方式就没有问题。例如string class:

string sequence_name="Fibonacci";

  但如果对象需要多个初值,这种方式就无法完成任务了。比如说标准库中的复数(complex number)类,它就需要两个初值(实部虚部)。于是便引入用来处理“多值初始化”的构造函数初始化语法(constructor initialization syntax):

#include <complex>
complex<double> purei(0,7);

  complex后的<>,表示complex是一个template class(模板类)。本书之后会对模板类有更详尽的讨论。这里只需知道template class 允许我们在不必指明data member类型的情况下定义class。template class 机制使程序员得以直到使用template class时才决定真正的数据类型。程序员可以事先插入一个代名,稍后才绑定至实际的数据类型。
  当内置数据类型与程序员自行定义的class类型具备不同的初始化语法时,我们无法编写出一个template使它同时支持内置类型和class类型。

  3.C++支持三种浮点数类型,分别是以关键字float表示的单精度(single precision)浮点数,以关键字double表示的双精度(double precision)浮点数,以及连续两个关键字long double表示的长双精度(extend precison)浮点数。

  4.关键字char表示字符(character)类型。单引号括住的字符代表所谓的字符常量,例如’a’,‘7’,’;’。此外还有一些特别的内置字符常量(有时也称为“转义字符(escape sequence)”,例如:

转义字符 含义
‘\n’ 换行符(newline)
‘\t’ 制表符(tab)
‘\0’ null
’ \’’ 单引号(single quote)
‘\’’ ’ 双引号(double quote)
‘\\’ 反斜线(backslash)

  举个例子,我们想要打印用户姓名前,前换行并跳过一个制表符的(tab)距离,下面这行就可以办到:

cout<<'\n'<<'\t'<<user_name;
cout<<'\n\t'<<user_name;//另一种写法是将两个不同的字符合并为一个字符串

  我们常常会在字符串常量中使用这些特殊字符。例如在Windows操作系统下以下字符串常量表示文件路径时,必须用转义字符“(escape sequence)”来表示反斜线字符:
  “F:\essential\progarms\chapter1\ch1_main.cpp”;
  由于反斜线字符已经用作转义字符的起头字符,因此连续两个反斜线即表示一个真正的反斜线字符。

  5.C++提供了内置的Boolean类型,用以表示真假值(true/false)。我们的程序中可以定义Boolean对象来控制是否要显示下一组数列:

bool go_for_it=true;

  6.目前我们所定义的对象,其值都会在程序执行过程中改变。go_for_it最终会被设置成false,用户每次猜完数字后,user_morede的值也可能会改变。
  但有时我们需要一些用来表示常量的对象:比如用户最多猜多少次,等永恒不变的数。这些对象的内容在程序执行过程中不应该变动。怎么做呢?C++的const关键字:

const int max_tries=3;
const double pi=3.14159;

  被定义为const的对象,在获得初值后将不会再有任何变动。如果你企图为const对象指定新的值,就会产生编译错误:

max_tries=42;//错误,这是const对象

  看下面这个需求:我要显示某数列中的两个数字,然后要求用户回答下一个数字是什么。如果用户答对,我们就打印出信息恭喜他答对,并询问他是否愿意试试另一个数列,如果答错,我们就询问他是否愿意再试一次。为了提升程序的趣味性,我们用用户答对的次数除以回答的总次数,以此作为评价标准。
  让我们来慢慢分析,我们的程序看起来至少需要五个对象:一个string对象记录用户的名字,三个整数对象分别存储用户回答的数值,用户回答的次数,以及用户答对的次数,此外还需要一个浮点数,记录用户的评分。
  当我们询问是否再试一次?以及是否愿意回答其他数列问题?,我们还必须将用户的回答(yes,no)记录下来。用字符(char)对象就绰绰有余了。
稍作总结,我们要完成的任务代码如下:

#include <iostream>
#include <string>
using namespace std;int main(){string user_name;cout<<"Please enter your name:";cin>>user_name;int user_val;//用来存储用户输入的值 int num_tries(0);//用来记录用户回答的次数 (构造函数法) int num_right=0;//用来记录用户回答正确的次数//也可以在单一声明语句中一并定义多个对象,其间用逗号分隔 //即int num_tries=0,num_right=0;double user_score=0.0;//浮点数记录用户评分const int max_tries=3;//用户最多可尝试三次 char user_more;//记录用户是否想要尝试其他数列bool go_for_it =true;//是否继续显示下一个数列(三次) cout<<user_name<<",Try another sequence? Y/N";cin>>user_more; cout<<user_more; return 0;//表示程序未出错误。
}

1.3 撰写表达式

  1.条件运算符的一般使用形式如下:
  expr
    ?如果expr为true,就执行这里
    :如果expr为false,则执行这里

  如果expr的运算结果为true,那么紧接着在’?‘之后的表达式会被执行,如果expr的运算结果为false,那么’:'之后的表达式会被执行。

  2.介绍一个余数运算的用处:假设我们希望打印的数据每行不超过八个字符串;尚未八个字符串时,就在字符串之后打印一个空格。如果已满八个字符串,就在字符串之后输出换行符。以下便是实现办法:

const int line_size=8;
int cnt=1;
//以下语句将被我执行多次,每次a_string的内容
//都不相同;每次执行完后,cnt的值都会加1
cout<<a_string<<(cnt%line_size ? ' ':'\n');

  当cnt恰为line_size的整数倍时,运算结果为零,反之则非零。

  3.复合赋值运算符(compound assignment)运算符是一种简便的表达方法,如果我们在对象身上使用某个运算符,然后将结果重新赋值给该对象时,可能会这样写:

cnt=cnt+1;

  但C++程序员通常会这么写:

cnt+=1;

  复合赋值运算符可以和每个算术运算符结合,形成+=、-=、*=、/=和%=。

  4.若想使对象值递增或递减,C++程序员会使用递增(increment)运算符和递减(decrement):

cnt++;//cnt的值递增1;
cnt--;//cnt的值递减1;

  递增和递减运算符都有前置(prefix)和后置(postfix)两种形式,
    前置形式:原值先递增(或递减)后才被拿来使用。
    后置形式:原值先被使用,之后才递增(或递减)。

  5.关系运算符包括以下六个:

关系运算符 含义
== 相等
!= 不等
< 小于
> 大于
<= 小于等于
>= 大于等于

  任何一个关系运算符(relational operator)的求值结果不是true 就是false。所以我们可以利用相等(equality)运算符来检验用户的回答:

bool user_more=true;
char user_rsp;
//询问用户是否愿意继续下一个问题
//将用户的回答存储于user_rsp中
if(user_rsp=='N') user_more=false;//还可以写成user_rsp!='Y

  程序只检验user_rsp的值是否为’N’,而用户却有可能输入‘n’,一种解决方法是else语句:

if(user_rsp=='N') user_more=false;
elseif(user_rsp=='n')user_more=false;

  另一种解决方式是OR逻辑运算符(||),它可以同时检验多个表达式的结果:

if(user_rsp=='n'||user_rsp=='N')user_more=false;

  只要左右两个表达式有一方为true,OR逻辑运算符的求值结果变为true。左侧表达式会被先求值,如果其值为true,剩下的另一个表达式就不需要再被求值(短路求值法)。

  AND逻辑运算符(&&)则在左右两个表达式均为true时,其求值结果才为true。左侧表达式会被先求值,如果其值为false,其余表达式不会再被求值。

  NOT逻辑运算符(!)可以对运算结果取反,例如:

if(user_more==false);
//可以改写成
if(!user_more)

  6.运算符的优先级
  我们将目前介绍过的运算符优先级简列于下。位置在上者的优先级高于位置在下者。同一行的各种运算符具有相同的优先级,其求值次序取绝于它在该表达式中的位置(由左至右)
    逻辑运算符NOT
    算术运算符(*,/,%)
    算术运算符(+,-)
    关系运算符(<,>,<=,>=)
    关系运算符(==,!=)
    逻辑运算符AND
    逻辑运算符OR
    赋值运算符(assignment)

举个例子,当我们想判断ival是否为偶数时,可能会这么写:

!ival%2//错误
!(ival%2)//正确

  我们的想法是利用余数运算符%来检验其结果,若余数运算结果为0,则说明为偶数,进行逻辑运算NOT之后则为true。若余数运算结果不为0,则说明为奇数,进行逻辑运算符NOT之后则为false。
  第一种做法,除非ival等于0,否则表达式结果总是false。因为逻辑运算符NOT具有较高的优先级,!ival将被最先求值。

1.4 条件语句和循环语句

  1.条件语句,让我们接着上节的代码继续探讨:

if(user_rsp=='n'||user_rsp=='N')go_for_it=false;//单条语句

  如果想执行多条语句则要在if之后以大括号将这些语句括住(称为一个语句块):

if(user_guess==next_elem)
{num_right++;got_it=true;//多条语句的语句块
}

  if语句也可以配合else子句来使用:

if(user_guess==next_elem)
{   //猜对了
}
else
{   //猜错了
}

  使用else子句的第二种方式,便是将它和两条(或多条)if语句结合:

if(num_tries==1)cout<<"Oops!Nice guess but not quite it.\n";
if(num_tries==2)cout<<"Hmm.Sorry.Wrong a second time.\n"
if(num_tries==3)cont<<"Ah,this is harder than it looks,isn't it?\n"

  以上代码可以改写成:

if(num_tries==1)cout<<"Oops!Nice guess but not quite it.\n";
else if(num_tries==2)cout<<"Hmm.Sorry.Wrong a second time.\n";
else if(num_tries==3)cont<<"Ah,this is harder than it looks,isn't it?\n";
else cout<<"It mst be getting pretty frustrating by now!\n";

  如果num_tries的值大于3,也就是所有条件都不成立则最后的else将被执行。
  嵌套的(nested)的if-else语句子句有个很容易令人困惑的地方,那就是要正确组织其逻辑其实是比较难的一件事。比如我们想要利用if-else语句将程序的执行分为两种情形:(1)用户猜对(2)用户猜错 ,错误的方式五花八门,正确的方式只此一种:

if(user_guess==next_elem)
{//用户猜对了
}
else//用户猜错了
{   if(num_tries==1)cout<<"Oops!Nice guess but not quite it.\n";else if(num_tries==2)cout<<"Hmm.Sorry.Wrong a second time.\n";else if(num_tries==3)cont<<"Ah,this is harder than it looks,isn't it?\n";else cout<<"It mst be getting pretty frustrating by now!\n";cout<<"Want to try again?(Y/N)";char user_rsp;cin>> user_rsp;if(user_rsp=='n'||user_rsp=='N')go_for_it=false;
}

  有时候多个if-else语句会显得我们很蠢,所以当测试条件值属于整数类型,我们还可以改用switch语句来代替:

//等同于上面的if-else语句
switch(num_tries)
{case 1:cout<<"Oops!Nice guess but not quite it.\n";break;
case 2:cout<<"Hmm.Sorry.Wrong a second time.\n";break;
case 3:cont<<"Ah,this is harder than it looks,isn't it?\n";break;
default:cout<<"It mst be getting pretty frustrating by now!\n";break;

  关键字switch后紧跟一个由小括号括住的表达式(对象名称也可视为表达式),该表达式的值必须为整数。switch之后是一组case标签,每一个标签之后都指定有一个常量表达式。在switch之后的表达式值被计算出来后,便依次和每个case标签的表达式相比较。如果找到符合的case标签就执行该case标签之后的语句,如果找不到吻合者,但有default标签,便执行default标签之后的语句。如果default标签也没有,就不执行任何操作。
  那每个case标签后的break语句有何作用呢?如果不加break语句,则在执行完当前语句后,会一直执行到switch语句的最下面。也就是说,当某个标签和switch的表达式值吻合时,该case标签之后的所有case标签也都会被执行,除非我们明确使用break来结束执行。

2.循环语句
  只要条件表达式不断成立(运算结果为true),循环语句就会不断得执行单一语句或整个语句块。

bool next_seq=true;
bool go_for_it=true;
bool got_it=true;
int num_tries=0;
int num_right=0;while(next_seq==true)//仍显示下一个数列
{//为用户显示下一个数列while((got_it=false)&&(go_for_it==true))//用户猜错了且仍想继续(让用户可以猜一个数列多次){int user_guess;cin>>user_guess;num_tries++;if(user_guess==next_elem)//用户猜对了{got_it=true;num_tries++;}else{cout<<"sorry,you are wrong.";char user_rsp;cin>>user_rsp;//用户猜错了,告诉用户你猜错了,询问用户是否想再试一次if(user_rsp=='N'||user_rsp='n')go_for_it=false;}}cout>>"Want to try another sequence?(Y/N)";//询问用户是否想猜下一个数列char try_again;cin>>try_again;if(try_again=='N'||try_again='n')next_seq=false;
}

  在上面我们介绍过break语句,在while循环同样可以使用。当执行循环内的语句遇上break时,便会退出循环。以下面的程序为例子:

int max_tries=3;
int tries_count=0;
while(tries_count<max_tries)
{int user_guess;cin>>user_guess;if(user_guess==next_elem)break;//猜对就退出循环tries_count++;
}

  我们也可以利用continue语句来遽然终止循环的当前迭代(currentiteration)。例如以下程序,所有长度小于四个字符的单词都会被舍弃:

string std;
const int min_size=4;
while(cin>>word)
{if(word.size()<min_size)continue;//舍弃这个单词process.text(word);//只有长度大于min_size的单词才会执行此语句
}

1.5 如何运用Array和Vector

  书接上回,如果用户答对一次答案,那么他就很容易找出所有答案。这就丧失了很大部分的趣味性。所以我们应该在程序主循环的每次迭代中,挑选不同的数列。
  现在,我们要显示最多六组元素对(element pair ):每一组来自不同的数列。我们希望在显示每组元素的同时,不必知道正在显示的是哪一种数列。每次迭代都必须存取三个数:元素对中的两个元素值,以及数列中出现的第三个元素值。
  为解决这个问题,需要使用的就是可以连续存储整数值的容器(container)类型。这种类型不仅允许我们以名称(name)取用容器中的元素,也允许我们以容器中的位置(下标)来取用元素。
  在容器内放入18个数值,分为6组。每一组的前两个数值用于显示,第三个数值表示数列中的下一元素值。在每次迭代过程中,我们令索引(index)每次增加3,这样就可以依次走访6组数据。
  C++允许我们以内置的array(数组)类型或标准库中的vector类来定义容器。但一般来说,建议使用vector而不是array。
  要定义array,我们必须指定array的元素类型,还得给予array一个名称,并指定其尺寸大小。array的大小必须是个常量表达式(constant expression),也就是一个不需要在运行时求值的表达式。例:

const int seq_size=18;
int pell_seq[seq_size];

  要定义vector object,我们首先要包含vector头文件。vector是个class template,所以我们必须在类名之后的尖括号内指定其元素类型,其大小则写在小括号中;与array不同的是,此处给予的大小并不一定得是常量表达式。下列程序将pell_seq定义为一个vector object,可储存18个int元素,每个元素的初值为0。

#include <vector>;
vector<int> pell_seq(seq_size);

  无论是vector还是array,我们都可以通过索引来访问该位置上的元素。索引操作(indexing)是通过下标运算符([])达成的。要依次访问容器中的元素,我们通常会采用C++中的另一种循环语句——for循环:

for (int ix=2;ix<seq_size;++ix)pell_seq[ix]=pell_seq[ix-2]+2*pell_seq[ix-1];

  for循环通常包括以下几个组成部分:

for(init-statement;condition;expression)statement;

  其中的init-statement会在循环开始执行前被执行一次。condition被用于循环控制,其值会在每次循环迭代前就被计算出来。如果condition为true,statement便会被执行。statement可以是单一语句,也可以是语句块({})。如果condition第一次求值即为false,则statement一次也不会被执行。expresstion会在每次迭代结束之后被求值。通常它用来更改两种对象的值:一个是init-statement中被初始化的对象,另一个是在condition中被检验的对象。如果condition第一次求值即为false,那么expresstion不会被执行。
  如果想打印出每一个元素值,可以对整个集合进行迭代(iterate):

cout<<"The first"<<seq_size<<"element of the Pell Series:\n\t";
for(int ix=0;ix<seq_size;++ix)cout<<pell_seq[ix]<<' ';
cout<<'\n';

  如果我们愿意,也可以将init-statement或expression甚至condition处留空,不写任何东西。例如:

int ix=0;
for( ;ix<seq_size;++ix)//...

  其中分号是必要的,因为必须利用它来表示init-statement留空。
  我们设计的容器存储了六个数列中每一个数列的第二、三、四个元素。我们应该怎样把数值填进去呢?即初始化。
  如果是array,我们可以指定初始化列表(initialization list),用逗号分隔每一个值,这些值就成为了array的全部或部分元素:

int elem_seq[seq_size]={1,2,3,//Fibonacci3,4,7,//Lucas2,5,12,//Pell3,6,10,//Triangular4,9,16,//Sequare5,12,22//Pentagonal
};

  初始化列表里给出的元素个数不能超过array的大小,但可以小于。如果前者的元素数量个数小于array的大小,其余的元素值将会被初始化为0。如果我们愿意,可以让编译器根据初值的数量,自行计算出array的大小。

  vector不支持上述这种初始化列表。有一个冗长的写法可以为每个元素指定其值:

vector <int> elem_seq(seq_size);
elem_seq[0]=1;
elem_seq[1]=2;
//...
elem_seq[17]=22;

  另一种写法是利用一个已经初始化的array作为该vector的初值:

int elem_vals[seq_size]={1,2,3,//Fibonacci3,4,7,//Lucas2,5,12,//Pell3,6,10,//Triangular4,9,16,//Sequare5,12,22//Pentagonal
};
//以elem_vals的值来初始化elem_seq
vector<int> elem_seq(elem_vals,elem_vals+seq_size);

  上例中我们传入两个值给elem_seq。array和vector存在着一些差异:vector知道自己的大小是多少。之前我们以for循环迭代的方式array的做法,如果应用于vector之上,情况将稍有不同:

//elem_seq.size()会返回elem_seq这个vector所包含的元素个数
cout<<"The first"<<elem_seq.size()<<"element of the Pell Series:\n\t";
for(int ix=0;ix<elem_seq.size;++ix)cout<<pell_seq[ix]<<' ';

  下面我们以cur_tuple表示要显示的元素的索引值。首先将它初始化为0,每次迭代循环时,我们将其值累加3,使它能够索引到下一个数列的第一个元素。

int cur_turple=0;
bool next_seq=true;
while(next_seq==true&&cur_turple<seq_size){cout<<"The first two elements of the sequence are: "<<elem_seq[cur_turple]<<","<<elem_seq[cur_turple+1]<<"\nWhat is the next element?";int user_guess;cin>>user_guess;if(user_guess==elem_seq[cur_turple+2])cout<<"You are right!";char user_rsp;cin>>user_rsp;if(user_rsp=='N'||user_rsp=='n')next_seq=false;elsecur_turple+=3;
}

  将目前进行中的数列类别记录下来,应该颇有用处。首先每个数列的名称都用string类型对象储存起来:

const int max_seq=6;
string seq_names[max_seq]={"Fibonacci","Lucas""Pell""Triangular""Sequare""Pentagonal"
};

  然后我们就可以这样来运用seq_names;

if(user_guess==elem_seq[cur_turple+2]){++num_cor;cout<<"Very good.Yes,"<<elem_seq[cur_turple+2]<<" is the next element in the "<<seq_names[cur_turple/3]<<"sequence.\n";
}

  cur_turple/3这一表达式会依次产生0,1,2,3,4,5,通过seq_names来索引出一个字符串,以此代表猜数游戏中正在进行的数列名称。

1.6 指针带来弹性

  前一节的代码有一些缺陷:1.猜数游戏的上限为六个数列,如果用户猜完了,程序就会无预期的结束。2.用array和vector来存储这些数,每次都已同样的顺应显示6组元素。怎么样才能增加程序的弹性呢?
  用一种解法是这样:同时维护六个vector,每个数列使用一次,每个vector储存一些数量的元素。每一次循环迭代,从不同的vector取出一组元素值。当第二次用到相同的vector时,便用不同的索引值取出vector内的元素。这样便可解决上述缺陷。
  和上节的解法一样,我们希望透明的访问不同的vector。上一节的做法是通过索引来访问每个元素,借此达到透明化的目的。每次循环迭代,我们将索引累加3。
  而这一节我们通过指针(primer),舍弃以名称指定的方式,间接的访问每个vector,借此达到透明化的目的。指针为程序引入了一层间接性,我们可以通过操作指针(代表某特定内存地址),而不再是直接操作对象。指针可以增加程序本身的弹性,但也增加了直接操作对象时所没有的复杂性。 在接下来的程序中,我们将定义一个可以对整数寻址的指针,每一次循环迭代更改指针值,使它定位到不同的vector,随后指针的操作行为不需要更改。
  **指针内含有某个特定类型对象的地址。**当我们要定义某个特定类型的指针时,必须在类型名称前加上*号:

int *pi;//pi是个int类型对象的指针

  当我们以对象类型来执行求值操作,例如:

ival;//计算ival的值,即得到ival所存的值

  如果想要取得对象所在的内存地址而非对象的值,则应该使用取址运算符(&):

&ival;//计算ival所在的内存地址

  那我们要想为指针设定初值该如何操作呢?下述写法可以将pi的初值设为ival所在的内存地址:

int *pi=&ival;//①

  如果要访问一个由指针所指的对象,我们必须对该指针进行提领(dereference)操作——也就是取得位于该指针所指内存地址上的对象。在指针之前使用*号,就可以达到这个目的:

//提领pi,借以访问pi所指的对象
if(*pi!=1024)//读取ival的值*pi=1024;//写值进入ival②
//若pi所指int对象的值不为1024,则写1024进入pi所指int对象

  指针的复杂性,如你所见,来源于其令人困惑的语法。本例中可能让你感到困惑的地方就是指针所具有的双重性质:既可以让我们操作指针所包含的内存地址①,又可以让我们操作指针所指的对象值②。
  如果我们这么写:

pi;//计算pi所持有的内存地址

  此举形同操作“指针对象”本身

  而如果我们这么写:

*pi;//求ival的值

  此举等同于操作pi所指的对象
  指针第二个令人感到复杂的地方是,指针可能并不指向任何对象。 当我们写*pi时,这种写法可能会(也可能不会)使程序在运行时产生错误。如果pi定位到某个对象,则对pi进行提领操作不会有错误。但如果pi不指向任何对象(一个未指向任何对象的指针,其地址值为0,有时我们称它为null指针,任何指针都可以被初始化,或是令其值为0。),则提领pi会导致未知的执行结果。**这意味着我们在使用指针时,必须在提领操作之前就先确定它的确指向某个对象。**那该如何做呢?
  以下这个代码在上一个代码的基础上加了检验pi指针所持有的地址是否为0的功能:

if(pi&&*pi!=1024)*pi=1024;

  之前我们提到过,if(pi&&…)这个表达式只有在pi持有一个非0值时,其结果才有可能为true。若pi所持有的地址确实为0,即false,则&&之后的表达式都不会被求值。
  欲检验某个指针是否为null,我们通常使用逻辑运算符NOT(!):

if(!pi)//当pi值为0时,此表达式为true

  以下为我们将要用到的六个vector对象(代表六个数列):

vector<int> fibonacci,lucas,pell,triangular,square,pentagonal;

  当我们需要一个指针,指向一个元素类型为int的vector时,该指针应该如何写呢?通常指针具有以下形式:

    (type_of_object_pointed_to)(*) (name_of_pointer_object)
  由于我们所需要的指针是用来指向vector<int>,因此我们把它命名为pv,并给定初值0:

vector<int> *pv=0;

  pv可以以此指向每一个表示数列的vector,也可以明确的将数列的内存地址赋值给它。但这种赋值会牺牲程序的透明性。更好的方案是将每个数列的内存地址存入另外一个vector中,这样我们就可以通过索引的方式,透明的访问这些数列:

const int seq_cnt=6;
//一个指针数组,大小为seq_cnt,
//每个指针都指向vector<int>对象
vector<int>*seq_addrs[seq_cnt]={&fibonacci,&lucas,&pell,&triangular,&square,&pentagonal};

  seq_addrs是一个array,其元素类型为vector<int>*。seq_addrs[0]所持有的值是Fibonacci vector的地址。这样以来,我们就通过一个索引值而非数列名称来访问各个vector:

vector<int>*current_vec=0;//当前访问的数列
vector<int>*seq_addrs[seq_cnt]={&fibonacci,&lucas,&pell,&triangular,&square,&pentagonal};
for(int ix=0;ix<seq_cnt;++ix){current_vec=seq_addrs[ix];//所有要显示的元素都通过current_vec间接访问到
}

  最后就剩一个问题还没解决:要给用户猜测的数列,总是按照固定的顺序出现。所以我们希望让数列出现的顺序随机化(randomize)。这一点可以通过C语言标准库中的rand()和srand()两个函数达成:

#include<cstdlib>
srand(seq_cnt);
seq_index=rand()%seq_cnt;
current_vec=seq_addrs[seq_index];

  rand()和srand()都是标准库提供的所谓伪随机数(pseudo-random-number)生成器。这两个函数的声明位于cstdlib的头文件里。srand()参数是所谓随机数生成器种子(seed)。每次调用rand()都会返回一个介于0和int所能表示的最大整数间的一个整数。对于本问题我们需要将该值限制在0-5。
  使用class object的指针,和使用内置类型的指针略有不同。这是因为class object关联了一组我们可以调用(invoke)的操作(operation)。举例来说,如果我们想检查fibonacci vector的第二个元素是否为1,我们可能会这么写:

if(!fibonnacci.empty() &&(fibonacci[1]==1))

  fibonnacci.empty(),中间的句点,称为dot成员选择运算符(number selection operator),用来选择我们想要进行的操作。如果要通过指针来选择操作,必须改用arrow而非dot成员选择运算符:

  但如果想要间接通过pv达到同样的效果该如何实现呢?

!pv->empty();

  就像上文所说,有些指针可能未指向任何对象,所以在调用empty()之前,应该先检验pv是否为非零值:

pv&&!pv->empty();

  最后如果要使用下标运算符(subscript operator),我们必须要先提领pv。由于下标运算符的优先级比较高,因此pv提领操作的两旁必须加上小括号

if(pv&&!pv->empty()&&((*pv)[1]==1));

  我们将在第三章深入讨论Standard Template Library(STL),并在第六章设计和实现二叉树(binary tree)时,回头讨论指针相关议题。

1.7 文件的读写

  如果我们想多次执行这个程序,那我们必须达到以下几个要求:1.每次执行结束,将用户名和会话的某些数据写入文件。2.在程序开启另一个会话时,将数据从文件中读回。
要对文件进行读写操作,首先要包含fstream头文件:

#include<fstream>

  为了打开一个可供输出的文件,我们要定义一个ofstream(供输出用的file stream)对象,并将文件名传入:

//以输出模式开启seq_data.txt
ofstream outfile("seq_data.txt");

  声明outfile的同时,会发生什么?当指定的文件并不存在时,便会有一个文件被创建并打开供输出使用。当指定的文件已经存在,这个文件会被打开用于输出,而文件中原有的数据会被丢弃。那如果我们不想丢弃原文件中的原有内容,而是希望将新数据增加到该文件,那么我们必须以追加模式(append mode)打开文件。为此我们提供第二个参数ios_base::app给ofstream对象。(先用之后再了解原理)

//以追加模式打开seq_data.txt
//新数据会被加到文件末尾
ofstream outfile ("seq_data.txt",ios_base::app);

  文件有可能打开失败,因此在写入操作之前,我们必须确定文件的确打开成功。最简单的是检验class object的真伪:

//如果outfile的求值结果为false,表示此文件并未成功打开
if(!outfile)

  如果文件未能成功打开,则该ofstream对象的求值结果为false。本例中我们将信息写入cerr。cerr代表标准错误设备(standard error)。和cout一样,cerr将其输出结果定向到用户终端。两者的唯一差别是,cerr的输出结果并无缓冲(bufferred)情形——它会立即显示于用户终端。

if(!outfile)cerr<<"oops! Unable to save session data!\n";//因某种原因,文件无法开启
else outfile<<user_name<<' '//<<num_tries<<' '<<num_right<<endl;

  如果文件顺利打开,我们便将信息定向到该文件,就像写入cout已经cerr这两个iostream对象一样。在上面的代码中,endl是事先定义好的所谓操纵符(manipulator),由iosstream library提供。
  操纵符并不会将数据写到iostream,也不会从中读取数据,其作用实在iostream上执行某些操作。endl会插入一个换行符,并清楚输出缓冲区(output buffer)的内容。除了endl,还有一些事先定义好的操纵符,例如hex(以十六进制显示整数)、oct(以八进制显示整数)、setprecision(n)(设定浮点数的显示精度为n)。
  如果要打开一个可供读取的文件,就定义一个ifstream(供输入的file steram)对象,并将文件名传入。如果文件未能成功打开,该ifstream对象就为false。如果成功,该文件的写入位置就会被设定在起始处。

//以读取模式(input mode)打开infile
ifstream infile("seq_data.txt);
int num_tries=0;
int num_cor=;
if(!infile){//由于某种原因,文件无法被打开//我们将假设这是一位新用户
}
else{//ok:读取档案的每一行,检查这个用户是否玩过//每一行的格式是://name num_tries num_correct//nt:猜过的总次数(num_tries)//nc:猜对的总次数(num_correct)string name;int nt;int nc;while(infile>>name){infile>>nt>>nc;if(name==user_name){//找到这个用户了cout<<"Welcome bcak,"<<user_name<<"\nYour current score is "<<nc<<"out of "<<nt << "\nGood luck!\n";num_tries=nt;num_cor=nc;}}
}

  上述代码中的while循环每次迭代都会读取文件的下一行内容。

infile>>name;

  这句语句的返回值就是从infile读到的class object。一旦读到文件末尾,对读入class object的求值结果就会是false。因此我们可以在while循环的表达式中以此作为结束条件。
  文件的每一行都包含一个字符串和两个整数。
  如果想要同时读写同一个文件,我们得定义一个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);string name;int nt;int nc;while(infile>>name){infile>>nt>>nc;if(name==user_name){//找到这个用户了cout<<"Welcome bcak,"<<user_name<<"\nYour current score is "<<nc<<"out of "<<nt << "\nGood luck!\n";num_tries=nt;num_cor=nc;}}
}

  当我们以追加模式来打开文件时,文件位置会位于末尾。如果我们没有先重新定位,就试着读取文件内容,那么立刻就会遇到文件结束的状况。seekg()可将iofile重新定位至文件的起始位置。由于此文件时以追加模式开启,所以任何写入操作都会将数据添加在文件末尾。
  iostream library提供的功能很丰富在此不做介绍。

习题解答

第一章

练习 1.1
试着在你的系统上编译并执行这个程序。

#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 good bye!\n";return 0;//表示程序未出错误。
}

练习 1.2
将string头文件注释掉后,会发生什么?
  重新编译程序仍可执行。
将using namespace std;注释掉后,又会发生什么?

练习 1.3
  将函数名main()改为my_main(),然后重新编译有何结果。

练习 1.4
  试着扩充这个程序的内容:(1)要求用户同时输入名字和姓氏,(2)修改输出结果,同时打印形式和名字。

#include <iostream>
#include <string>
using namespace std;int main(){string user_first_name;string user_last_name;cout<<"Please enter your first name :";cin>>user_first_name;cout<<"Please enter your last name :";cin>>user_last_name;cout<<'\n'<<"Hello, "<<user_first_name<<' '<<user_last_name<<"...and good bye!\n";return 0;//表示程序未出错误。
}

练习 1.5
  编写一个程序,能够访问用户的姓名,并读取用户所输入的内容。(保证用户输入的名称长度大于两个字符)如果用户输入了有效名称,就响应一些信息。请以两种方式实现:第一种使用C-style字符串,第二种使用string对象。
首先介绍一下C-style字符串和string对象之间的主要差异:
  1.string对象会动态地随字符串长度的增长而增加其储存空间,C-style字符串却只分配固定的空间,并期望这个固定的空间可以容纳对应的字符串。
  2.C-style字符串并不会记录自身的长度,如果想得到C-style字符串的长度,我们得遍历每一个元素,直到null字符出现。而string对象可以先包含cstring头文件再用用标准库中的strlen()就可以获取string对象的长度。

int strlen(const char*);

  本书建议C++初学者舍弃C-style字符串,改用string class。

#include <iostream>
#include <string>
using namespace std;int main()
{string user_name;cout<<"Please enter your name";cin>>user_name;switch(user_name.size()){case 0:cout<<"Ah,the user with no name."<<"Well,user with no name.";break;case 1:cout<<"A 1-character name?"<<"hello,"<<user_name<<endl;break;default://字符串长度超过一个字符cout<<"Hello,"<<user_name<<"--happy to make you acquaintance!\n";break;}return 0;
}

  如果使用C-style字符串,写法就完全不一样了。首先我们要先决定user_name的长度;随便选一个128。然后利用标准库里的strlen()函数获得user_name的长度。cstring头文件里由strlen()的声明。要是用户输入的字符串长度大于127个字符,则没有空间存放终止字符’\0’(null字符,C++ Primer)。为了避免用户的过多输入,我们用iostream操纵符(manipulator)setw()保证不会读入超过127个字符。由于用到了setw()操纵符,因此还得包含iomanip头文件。

#include <iostream>
#include <iomanip>
#include <cstring>
using namespace std;int main()
{//用const 变量固定一个大小为128的空间const int nm_size=128;char user_name[nm_size];cout>>"Please enter your name";cin>>setw(nm_size)>>user_name;//先这么用,之后有机会再解释switch (strlen(user_name)){case 0:cout<<"Ah,the user with no name."<<"Well,user with no name.";break;case 1:cout<<"A 1-character name?"<<"hello,"<<user_name<<endl;break;//处理方法同上case 127://可能已经被setw()舍弃掉部分内容cout<<"That is a very big name"<<"we may have needed to shorten it!"//此处不加break继续向下执行default://如果符合前述条件,也会执行到此处,因为之前没有breakcout<<"hello,"<<user_name<<"--happy to make your acquaintance!\n";break;}return 0;
}

练习 1.6
  编写一个程序,从标准输入设备读取一串整数,并将读入的整数依次存放到array及vector中,然后再遍历这两种容器,求其数值总和。将总和及平均值输出至标准输出设备。
array和vector也像C-style字符串与string的差异一样:
  1.array大小必须固定,而vector可以动态地随元素的插入而拓展储存空间。
  2.array并不存储自身大小。大小固定就意味着我们要考虑到对它的访问可能造成溢出(overflow)。不过array和C-style字符串不同的是,array并没有像C-style 中null字符(’\0’)这样的“标兵”来表示已到达末尾。
  本书建议初学者用vector代替array。
以下是vector的解法:

#include <iostream>
#include <vector>
using namespace std;int main()
{vector<int> ivec;int ival;while(cin>>ival)ivec.push_back(ival);//其实在输入环节就可以计算总和。for(int sum=0,ix=0;ix<ivec.size();++ix)sum+=ivec[ix];int average=sum/ivec.size();//sum是属于for循环的局部变量,//此处会导致undefined symbol编译错误。//这么写不符合C++ standard规范(但某些编译器如visualC++却可以让它过关 )//所以我们应该在for循环之前声明全局变量sum。cout<<"sum of " <<ivec.size()<<"elements:"<<sum<<".average:"<<average<<endl;

以下是array的解法:

#include <iostream>
using namespace std;
int main()
{const int array_size=128;
int ia[array_size];
int ival,icnt=0;//cnt用来记录array目前有多少元素while(cin>>ival&&icnt<array_size)ia[icnt++]=ival;//熟练这种写法
int sum=0;//解决了上面程序的问题
for(int ix=0;ix<icnt;++ix)sum+=ia[ix];
int average=sum/icnt;
cout<<"sum of " <<icnt<<"elements:"<<sum<<".average:"<<average<<endl;

练习 1.7
  使用你最熟悉的编辑工具,输入两行或更多文字并存盘。然后编写一个程序,打开该文本文件,将其中每一个字都读取到一个vector<string>对象中。遍历该vector,将内容显示到cout。然后用泛型算法sort(),对所有文字排序。用法如下:

#include<algorithm>
sort(container.begin(),container.end() );

最后将排序的结果输出到另一个文件。

  读入文字及输出排序结果之前,我们先打开用来输入输出的文件。虽然可以之后才打开输出文件,但如果因为某种原因而无法打开该输出文件,会发生什么呢?所有的计算将付诸东流。我们把文件路径写死在程序代码中,并使用Windows命名方式。头文件algorithm内有算法sort()的声明。

#include<iostream>
#include<fstream>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;
int main(){ifstream in_file("D:\\My Documents\\text.txt");if(!in_file){cerr<<"oops! unable to open input file \n";//输入文件打开失败 return -1;}ofstream out_file("D:\\My Documents\\text.sort");if(!out_file){cerr<<"oops! unable to open output file \n";//输出文件打开失败 return -2;}string word;vector<string> text;while(in_file>>word)//输入 text.push_back(word);int ix;cout<<"unsorted text:\n";for(ix=0;ix<text.size();++ix)//在命令行显示输入 cout<<text[ix]<<' ';cout<<endl;sort(text.begin(),text.end() );//排序 out_file<<"sorted text:\n";//在输出文件中写入 for(ix=0;ix<text.size();++ix)//在输出文件中写入排序好的字符串 out_file<<text[ix]<<' ';out_file<<endl;return 0;
}

练习 1.8
  1.4节的switch语句让我们得以根据用户答错的次数提供不同的安慰语句。请你用array储存不同的字符串信息,并以用户答错次数作为array的索引值,以此方式来显示安慰语句。
  首先要定义一个字符串数组作为索引的对象。比较好的方法是将这些信息封装在一个显示函数中,这个函数可以根据我们传入的用户猜错次数来返回合适的安慰语句。
  这里需要注意的是,用户的猜错次数是从1开始的,而用来储存安慰信息的字符串数组却是从位置0开始的。为此我们在位置0存储完全猜对的用户,这样就可以使其他信息都可以用猜错次数来索引了。
  另外我们只打算提供四种不同的安慰信息,但用户猜错次数可能会超过4次,如果我们不经过判断就将>=4的值用来索引数组内容,便会超出数组的边界。除此之外我们还得防范其他的无效值,例如负数等。

const char* msg_to_user(int num_tries){const int rsp_cnt=5;static const char* user_msgs[rsp_cnt]={"Go on,make a guess.","Oops!nice guess but not quite it.","Hmm.Sorry ,wrong a secend time.","Ah,this is harder than it looks,no?","It must be getting pretty frustrating by now!"};if(num_tries<0)//传入的猜错次数为负数按0处理 num_tries=0;else if(num_tries>=rsp_cnt)//传入的猜错次数大于等于rsp_cnt(上限),//按最后一个rsp_cnt-1处理 num_tries=rsp_cnt-1;return user_msgs[num_tries];
}

第二章 面向过程的编程风格 Procedural Programming

读书笔记

习题解答

第三章 泛型编程风格 Generic Programming

读书笔记

习题解答

第四章 基于对象的编程风格 Object-Based Programming

读书笔记

习题解答

第五章 面向对象编程风格 Object-Oriented Programming

读书笔记

习题解答

第六章 以template进行编程 Programming with Templates

读书笔记

习题解答

第七章 异常处理 Exception Handling

读书笔记

习题解答

《Essential C++ 中文版》 读书笔记及习题解答相关推荐

  1. 《Essential C++》读书笔记 之 泛型编程风格

    <Essential C++>读书笔记 之 泛型编程风格 2014-07-07 3.1 指针的算术运算(The Arithmetic of Pointer) 新需求1 新需求2 新需求3 ...

  2. 《Essential C++》读书笔记 之 基于对象编程风格

    <Essential C++>读书笔记 之 基于对象编程风格 2014-07-13 4.1 如何实现一个class 4.2 什么是Constructors(构造函数)和Destructor ...

  3. CSAPP读书笔记与习题作业练习-第3章

    CSAPP读书笔记与习题作业练习-第3章 疑问 疑问一 练习题3.26中为什么通过循环+异或可以判断二进制数中1的个数的奇偶性(已解决) 习题 练习题3.1 练习题3.2 练习题3.3 练习题3.4 ...

  4. 快学Scala学习笔记及习题解答(5-8类、对象、包和继承)

    本文scala使用的版本是2.11.7 第五章 类 5.1 基本操作 class Person {// Scala会生成一个私有的final字段和一个getter方法,但没有setterval tim ...

  5. 《Effective STL》中文版 读书笔记

    50条有效使用STL的经验 第一条 慎重选择容器类型(20190713) 第二条 不要试图编写独立于容器类型的代码(20190713) 第三条 确保容器中的对象副本正确而高效(20190713) 第四 ...

  6. Unix网络编程卷1 第一章读书笔记以及习题

    1 下载源码并且配置环境 对于想学习网络编程的同学来说,<UNIX网络编程>这书肯定是不二选择.所谓实践是检验真理的唯一标志,特别是对于编程来讲,再多的理论经验也比不过code一次.< ...

  7. UNIX网络编程第三版(卷1)读书笔记以及习题-章节1

    1. 几个关键点: client使用read读取socket返回的数据的时候,总是使用一个循环包裹,最主要的原因就是tcp/ip协议传输的时候,数据会根据实际情况进行分节,因此只有循环才能保证读出全部 ...

  8. 深入 Python :Dive Into Python 中文版 读书笔记 第13,14,15单元测试

    2019独角兽企业重金招聘Python工程师标准>>> 第 13 章 单元测试 13.4. 正面测试 (Testing for success) 13.5. 负面测试 (Testin ...

  9. 【读书笔记】Java NIO (中文版) 读书笔记

    概述 这本书讲解的一般吧,主要是讲解了 缓冲区 通道 选择器 正则表达式 字符集 主要是讲解了api的使用,以及部分系统知识,比较底层了,而且大部分都是代码的源码讲解,或者api使用的讲解,太细致了. ...

最新文章

  1. MinGW-w64安装教程——著名C/C++编译器GCC的Windows版本
  2. Spring Cloud系列:不重启eureka,清除down掉的服务
  3. mysql cpu占用率过高,MySQL高CPU使用率
  4. python中静态方法可以访问对象变量_python 面向对象-实例变量、类变量、实例方法、类方法、静态方法...
  5. azure blob_使用Azure Blob存储托管Maven工件
  6. mysql从入门到精通之数据库基本概念理解
  7. HDU1799 循环多少次?
  8. 在 Linux 命令行发送邮件的 5 种方法
  9. 固定 顶部_抗拉固定球铰支座优点与施工步骤
  10. 小米笔记本linux无线网卡驱动,小米笔记本在Kali Linux下所遇问题部分解决方案
  11. 常用c语言代码大全,C经典程序代码大全.docx
  12. [RTOS]--uCOS、FreeRTOS、RTThread、RTX等RTOS的对比之特点
  13. 安川ga700变频器故障码集_安川变频器故障代码
  14. linux,centos7环境下,安装RabbitMQ
  15. 135编辑器代码是html吗,不会代码,你也能做背景样式!!!
  16. aw36515闪光灯驱动ic调试
  17. 前一个标签自动增加,后面的标签自动减小,如微信的群发功能
  18. “超级计算机”——GPU云服务器
  19. 关于UWB汽车钥匙介绍
  20. java 设置背景色_背景颜色的设置

热门文章

  1. 伪类和伪元素的区别,以及伪元素的妙用(上)
  2. ExternalInterface的call函数返回null的原因分析(AS2) 1
  3. Flash与Javascript交互时可用ExternalInterface类
  4. wps如何插入Word里的行前小黑圆点
  5. 有些事一转身就是一辈子(转载)
  6. 宽带上行速率和下行速率的区别
  7. 台州学院第七届“星峰杯”大学生程序设计竞赛
  8. ffmpeg文件名含特殊字符引起的问题
  9. Opencv——目标跟踪Tracker
  10. 奥地利采用漱口检测方法对抗新冠病毒