How To Think Like A Computer Scientist

How To Think Like A Computer Scientist C++ Version

Posted by Lucas on April 22, 2016

还差近1/4的内容,待续

书中的几个错误

《像计算机科学家一样思考C++》中文译本(人民邮电出版社,2013年6月第一版)属于入门读本,但是发现些许错误,这里只提出一部分。小白看的时候要注意甄别。书比较老了,很多知识陈旧了,内容不全,当作补充是可以的。但是凭借这本书还是无法入门我觉得。

错误一、位于1.5 第一个程序

#include<iostream.h>
int main() {
	cout << "Hello World";
	return 0;
} 

在Dev-C++5.11版本中,iostream.h头文件不存在,是iostream头文件 另外缺少了命名空间using namespace std;的声明语句,不能直接使用cout方法。 文中很多程序代码都是没有声明命名空间的。

错误二、位于3.2 从double转换为int

里面提到从double类型数值隐式转换为int类型时需要发生截断,但是C++并不会自动实现这一操作。通过代码可以发现是会发生截断的。但是这里对“C++并不会自动实现这一操作”不甚理解。是翻译错误吗。

错误三、位于3.3 数学函数

log(1/10) = -1 计算机中1/10 == 0 所以log(0) == inf,无穷值。 另外,C++中log的底默认是e而不是10,log10()才是默认底为10 甚至文中使用了log作为变量名称,简直给跪了。在C++中变量名和函数名相同是合法的,但是log是我们math.h中引用的函数,在同一个作用域中不能同时出现log和log()

但是不得不说,这本书对于结构体的描述比较精彩,一些代码也写得优美。

读书摘记:

自然语言和形式语言

自然语言:人类表达的语言,比如英语和汉语。不是由人类设计(尽管人类尝试对其强加某些命令)的,而是通过自然演化的。 形式语言:人类为了某些特殊应用而设计的语言。例如,数学中使用记号法就是一种特别擅长表示数字和符号间关系的形式语言。化学家使用某种形式语言来表示分子间的化学结构。编程语言是一种用于表达计算过程的形式语言。

类型的自动转化

C++中要避免出现类型的自动转化,一因为这个可能会造成混乱之源。 比如 double value = 1/3得到的value是0.0,计算时应该为double value = 1.0/3.0 自动类型转化是设计编程语言的一个常规问题。因为在设计的几个形式之间是有冲突的。设计编程语言的目标是形式语言应该有简单的规则和较少的异常情况,更具便利性,同时它还要容易使用。但是通常,便利性会取胜,这对专家级程序员来说是个好消息。

main函数的实例

main函数的实例是空的,因为main函数不包含任何参数或者局部变量??

一个有趣的重载例子

已知两点的位置,已两点之间长度为半径计算圆的面积 计算两点,求之间的长度 distance()

double distance(double x1,double y1,double x2,double y2) {
	return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}

已知半径,计算圆的面积

double area(double radius) {
	return M_PI * radius * radius;  //需要先#define _USE_MATH_DEFINES才能使用M_PI
}

已知两点位置,计算圆的半径

double area(double x1,double y1,double y1,double y2) {
	return area(distance(x1,y1,x2,y2));
}

这看起来像是一个递归函数,但它不是。这个版本的函数area调用了另外一个版本的的area函数。 如果两个函数做了同样的事情,很自然就给它们起同样的名字。多个函数使用同一个函数名就称之为重载。

4 bool与int

有趣的是,当C++输出bool类型的值的时候,他不是显示true或false,而是整数1或0。应当注意到在C++中,bool值有时是会自动转化为int类型的,比如在输出的时候或者计算的时候。

赋值语句

尽管多次赋值语句通常情况下都很有用,但是你仍然需要谨慎使用。如果变量的值经常在程序的不同部分修改,这将会使程序变得十分难以阅读和调试。

++

i = ++i与i = i++有很大的不同,前者自增后再赋值,后者赋值后再自增

结构体

struct Point{
    double x,y;
};

Point point = {1,2};       //正确
Point point;
point = {1,2};             //警告,可能是编译器不知道赋值符号有点的表达式是什么类型的。如果添加一个类型转化,如下图所示:
Point point;
point = (Point){1,2} ;

这样就可以了。

引用传递

void swap(int& x, int& y) {
	int tmp = x;
	x = y;
	y = tmp;
}

调用 swap(i,j+1); //错误,只能将变量作为引用出传递,j+1是表达式。

cin.good()

int x;
cin >> x;
if (cin.good()) {
	//cin.good()是用于判断输入语句是否成功
}

对象函数

纯函数(pure function):接收对象或者基本操作类型作为参数但是并不对对象进行修改,返回值是基本类型或者在函数内创建的新对象。 修改器(modifier):接收对象作为参数并对部分或全部参数进行修改。一般无返回值。 填写函数(fill-in function):参数中有一个空对象,并在函数内部进行填充。从技术角度来说,这是一种类型的修改器。

const参数

传递参数中的传值方式的优点是调用函数和被调用函数都有很好的封装性。 传递参数中的按引用传递不会对参数进行复制,通常来说效率更高。而且,C++有一个很好的特性叫const(常量),可以使引用参数与值参数一样安全。 void print(const int& number) { //这样的话如果函数内部试图改变参数,编译器会警告或报错 }

算法

人类凭借本能做的一些毫无困难并且无意识的行为,是最难用算法表达的。对自然语言的理解力就是一个很好的例子。每个人都会,但是目前为止没人可以解释人类是怎么做到的,甚至没有办法用算法的形式解释。

代码优化

在一定大小范围内生成大量随机数,储存每个随机数出现的次数

//生成随机数
vector<int> randomVector(int n,int upperBound) {
	vector<int> vec(n);
    for (int i = 0; i < n; i++) {
        //random函数可以生成伪随机数,返回在0~RAND_MAX之间的一个整数,RAND_MAX代表一个很大的数
        //不同电脑中的值不同。
        vec[i] = random() % upperBound;
    }
    return vec;
}
//计算每个随机数出现的次数
int howMany(const vector<int> &vec,int value) {
	int count = 0;
    for(int i = 0; i < vec.length(); i++) {
        if (value == vec[i]) {
        count++;
        }
    }
    return count;
}
//在一定大小范围内生成大量随机数,储存每个随机数出现的次数
int numValues = 100000;
int upperBound = 10;
vector<int> vector = randomVector(numberValues,upperBound);

vector<int> histogram(upperBound);
for (int i = 0; i < upperBound; i++) {
	int count = howMany(vector,i);
	histogram[i] = count;
}

将vector命名为histogram是因为这是一个统计术语,表明了这个vector中的值统计了某个范围内数字出现的次数。 但是上面的代码效率并不是很高,每次调用howMany的时候,程序都会遍历整个vector,这个例子中需要遍历十遍。

单次遍历的解决方案

vector<int> histogram (upperBound,0);
for (int i = 0; i < numValues; i++) {
	index = vec[i];
	histogram[index]++;
}

题外话,伪随机数的一个特点是:如果他们从一个相同的起点开始,那么他们将生成相同的值序列。这个起点称为种子。默认情况下,当多次运行程序时,C++总是会使用相同的种子。每次生成相同序列值有助于程序的调试。在这种方式下,当修改了程序之后,依然可以比较修改之前和修改之后的输出。 如果你想为随机数生成器指定不同的种子,就可以使用srand函数,该函数接受一个整形参数,该参数只能介于0~RAND_MAX之间。 对于许多像游戏这样的应用程序来说,我们更希望在程序每次运行时,产生的随机序列都是不同的。为了达到这个目的,通常的方式都是首先调用诸如gettimeofday这样的库函数生成在理论上随机和不重复的值,例如从前一秒到现在所经历的毫秒数,将这个值作为种子。如何实现取决于环境。

第11章 成员函数

结构体通过添加成员函数之后变得如此强大,感觉看起来像类一样。这点我需要再专研一下。 注意需要在结构体定义内部声明新函数,即描述函数的接口。

成员函数通过this指针显式声明或者通过隐式变量访问。this实际上是一个指向结构体的指针,而不是结构体本身,需要通过指针操作符*将结构体指针转换为结构体。而隐式变量访问指的是,我们并不需要创建一个局部变量来作为对当前对象实例的引用,可以省略”.”操作符,C++默认指向当前对象。

使用大括号声明和初始化结构体还是使用构造函数声明和初始化变量,这两种函数分别代表了不同编程风格和C++历史的不同时期。可能是因为这个原因,C++编译器通常要求在同一个程序中只使用一种。幸运的是,可以使用重载函数的方法重载构造函数。