按照我所了解的当代中国本科计算机教育,大多人工科学生学习的第一门编程语言应该是C,接下来如果还有需要的话,就是C++或者Java了。不幸的是,鄙人也是这样过来的,幸运的是,鄙人天资愚鲁,学了半年C++后知难而退,转战了实战主义的Shell Script和Python,这一年来闹中取静,为了探寻MapReduce算法模型的本原,啃了几本Lisp的书,回过头来,总算对C++的诸多繁杂特性有了一些全局性的认识,这种认识来自于跨语言的佐证和思考,而非来自于C++语言本身。事实上C++中的很多概念在C++中是无法学到通透的,比如:
C++11的lambda:你可以不知道Alonzo Church, 也可以不会Lambda calculusHigher-order function
STL:STL几乎就是C++经典库和设计的代名词,通过迭代器将算法和组件分离可惜你不知道的是,迭代器并不是一个高层次的抽象机制,迭代器的本质是一种迭代遍历操作,至于通过什么手段来迭代遍历,这些本不应该是使用者所应该关心的细节问题。所以对于下面的这段c++伪代码:
for (vector::iterator itr = v.begin(); itr != v.end(); ++itr)
{
do_something(*itr)
}
其高阶抽象代码应该是:
for_each(v, do_something)
稍微了解一点Lisp的读者都能想到如下的等价伪代码
(mapcar #'do_something v)
每次你写"v.begin(), v.end()"这样的代码时,你就不知不觉地降低了自己的抽象层次,使自己脱离问题域而转向去纠结于实现域,不要小看这种力量, 软件工程的一切欢乐和痛苦,只是聚沙成塔的两个极端而已。
事实上STL本身包含很多函数式编程的思想,比如说 functor这种组件,其实质是将在c++中作为second-class的函数通过类封装的手段提升至first-class,如此一来,函数的核心操作摇身一变成为functor的时候,就可以像函数式语言里面的Higher-order function一样,可以用类成员变量来模拟实现闭包,可以被当做普通参数传递返回(这样就不用费力去写令很多新手语法不过关的函数指针了),甚至可以通过std::bind1st/std::bind2nd这种奇技淫巧实现一个蹩脚的线性代数级别的函数映射与变换。
STL里面大量的算法都是基于迭代器的抽象而进行序列的批量化操作,同种算法多种容器的核心技术是 基于C++模板实现的静多态 ,"It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures",从这个角度上来讲,STL算法和Lisp中针对sequence类型数据的各种函数(mapcar/remove/remove_if/member等)有异曲同工之妙。
最后来八一八STL之父Alexander Stepanov,其实人家是莫斯科大学数学系毕业的高材生,所以STL背后有着很深的数学思想,"Elements of Programming"或许是解开这个谜题的钥匙。另外,Alexander是反对OOP的。
泛型与模板:这大概是Modern C++中最重口味的话题了,也是很多C++初学者的噩梦。我认为C++模板足够强大,但同时也足够扭曲且非人道,布满了大大小小的地雷和陷阱。探究起来,C++模板之所以有那么多坑,其历史原因在于C++模板是一种被发现而非被发明的技术C++模板的本质在于用编程的手段显式地控制编译器的代码生成。没错,聪明的你已经想到,Lisp的macro做的也是同样的事情。但是不同于Lisp的macro,由于C++模板的先天不足和C++静态类型系统的限制,C++在语言层面上对模板编程的支持非常有限。荣耀先生有一篇非常精炼的PPT《C++模板元编程技术与应用》, 基本上概括了C++模板编程的核心机制和语言实现,我摘录了一些如下:
模板元编程使用静态C++语言成分,编程风格类似于函数式编程,其中不可以使用变量、赋值语句和迭代结构等。
在模板元编程中,主要操作整型(包括布尔类型、字符类型、整数类型)常量和类型。被操纵的实体也称为元数据(Metadata)。所有元数据均可作为模板参数。
由于在模板元编程中不可以使用变量,我们只能使用typedef名字和整型常量。它们分别采用一个类型和整数值进行初始化,之后不能再赋予新的类型或数值。如果需要新的类型或数值,必须引入新的typedef名字或常量。
编译期赋值通过整型常量初始化和typedef语句实现。例如:
enum { Result = Fib::Result + Fib::Result};
static const int Result = Fib::Result + Fib::Result;
成员类型则通过typedef引入,例如:
typedef T1 Result;
条件结构采用模板特化或条件操作符实现。如果需要从两个或更多种类型中选其一,可以使用模板特化,如前述的IfThenElse
静态C++代码使用递归而不是循环语句。递归的终结采用模板特化实现。如果没有充当终结条件的特化版,编译器将一直实例化下去,一直到达编译器的极限
而正是由于底层支撑性语言机制的匮乏,使得C++模板编程非常的冗长、丑陋,甚至有些扭曲乃至非人道有时候你想要舞蹈的时候,要低头看看,你的脚上是否带着不必要的镣铐。 C++的静态类型系统对于泛型编程而言,就是这样的镣铐。
引用、指针、const、static等:除了以上比较“重口味”的C++语言特性,C++里还有各种各样的语言小尾巴,而且这个尾巴一般都拉的特别长。当然,尾巴长的好处之一就是可以养活很多语言专家,什么effective啊、exceptional啊、faq啊啥的,在所有的编程语言中,C++这点绝对是独树一帜。其实每个语言特性的背后都有值得深究的知识, 没有任何事情是想当然的。 const够简单了吧?可是你知道const pointer和pointer to const的区别吗?你知道什么时候用const引用传参什么时候返回const引用什么时候返回值吗?你知道const成员函数吗?你知道为什么会有初始化成员列表的存在吗?再来说说引用这个概念,其本质上就是一种受限指针加上编译器层面上的语法糖修饰,按理说不太难,但是什么时候传引用返回引用确是值得深究的好问题,搞清楚了这点,你就会搞明白C++中的copy constructor/copy assignment operator,Java中的Object.clone(),Python中的"is"、和Lisp中的eq/eql/equal。传引用/指针还是传值涉及到深刻的程序语言原理,并不是你想象的那么简单而已。
以上谈了这么多,读者可能会问,既然C++如此繁杂,还要不要学习C++?学,当然要学,否则你怎么批判呢?怎么学?批判地学。要去学习语言机制的根源和本质而不要迷失在语言特性的森林里
最后,还是回到面试题上,还是放上鄙人的C++代码,也好和Lisp/Python版的程序做一个小对比:
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
struct vertex
{
int index; /// the vertex index, also the vertex name
vertex* prev; /// the prev vertex node computed by bfs and bfs_shortest
int dist; /// the distance to the start computed by bfs
/// and bfs_shortest
vector adj; /// the adjacency list for this vertex
vertex(int idx)
: index(idx) {
reset();
}
void reset() {
prev = NULL;
dist = numeric_limits::max();
}
};
class graph
{
public:
graph() { }
~graph();
void add_edge(int start, int end);
void bfs(int start);
void bfs_shortest(int start);
list get_path(int end) const;
void print_graph() const;
protected:
vertex* get_vertex(int idx);
void reset_all();
list get_path(const vertex &end) const;
private:
/// disable copy
graph(const graph &rhs);
graph& operator=(const graph &rhs);
typedef map > vmap;
vmap vm;
};
graph::~graph() {
for (vmap::iterator itr = vm.begin(); itr != vm.end(); ++itr)
{
delete (*itr).second;
}
}
/**
* return a new vertex if not exists, else return the old vertex, using std::map
* for vertex management
*
* @param idx vertex index
*
* @return a (new) vertex of index idx
*/
vertex* graph::get_vertex(int idx) {
/// cout << "idx: " << idx << "\tvm.size(): " << vm.size() << endl;
vmap::iterator itr = vm.find(idx);
if (itr == vm.end())
{
vm[idx] = new vertex(idx);
return vm[idx];
}
return itr->second;
}
/**
* clear all vertex state flags
*
*/
void graph::reset_all() {
for (vmap::iterator itr = vm.begin(); itr != vm.end(); ++itr)
{
(*itr).second->reset();
}
}
/**
* add an edge(start --> end) to the graph
*
* @param start
* @param end
*/
void graph::add_edge(int start, int end) {
vertex *s = get_vertex(start);
vertex *e = get_vertex(end);
s->adj.push_back(e);
}
/**
* print the graph vertex by vertex(with adj list)
*
*/
void graph::print_graph() const {
for (vmap::const_iterator itr = vm.begin(); itr != vm.end(); ++itr)
{
cout << itr->first << ": ";
for (vector::const_iterator vitr = itr->second->adj.begin();
vitr != itr->second->adj.end();
++vitr)
{
cout << (*vitr)->index << " ";
}
cout << endl;
}
}
/**
* traversal the graph breadth-first
*
* @param start the starting point of the bfs traversal
*/
void graph::bfs(int start) {
if (vm.find(start) == vm.end())
{
cerr << "graph::bfs(): invalid point index " << start << endl;
return;
}
vertex *s = vm[start];
queue q;
q.push(s);
s->dist = -1;
while (!q.empty()) {
vertex *v = q.front();
cout << v->index << " ";
q.pop();
for (int i = 0; i < v->adj.size(); ++i)
{
if (v->adj[i]->dist != -1)
{
q.push(v->adj[i]);
v->adj[i]->dist = -1;
}
}
}
}
/**
* the unweighted shortest path algorithm, using a std::queue instead of
* priority_queue(which is used in dijkstra's algorithm)
*
* @param start
*/
void graph::bfs_shortest(int start) {
if (vm.find(start) == vm.end())
{
cerr << "graph::bfs_shortest(): invalid point index " << start << endl;
return;
}
vertex *s = vm[start];
queue q;
q.push(s);
s->dist = 0;
while (!q.empty()) {
vertex *v = q.front();
q.pop();
for (int i = 0; i < v->adj.size(); ++i)
{
vertex *w = v->adj[i];
if (w->dist == numeric_limits::max())
{
w->dist = v->dist + 1;
w->prev = v;
q.push(w);
}
}
}
}
/**
* get the path from start to end
*
* @param end
*
* @return a list of vertex which denotes the shortest path
*/
list graph::get_path(int end) const {
vmap::const_iterator itr = vm.find(end);
if (itr == vm.end())
{
cerr << "graph::get_path(): invalid point index " << end << endl;
return list();
}
const vertex &w = *(*itr).second;
if (w.dist == numeric_limits::max())
{
cout << "vertex " << w.index << " is not reachable";
return list();
}
else {
return get_path(w);
}
}
/**
* the internal helper function for the public get_path function
*
* @param end
*
* @return a list of vertex index
*/
list graph::get_path(const vertex &end) const {
list l;
const vertex *v = &end;
while (v != NULL) {
l.push_front(v->index);
v = v->prev;
}
return l;
}
class chessboard {
private:
struct point {
int x;
int y;
point(int px, int pb)
: x(px), y(pb) { }
};
public:
chessboard(int s);
void solve_knight(int x, int y);
protected:
bool is_valid(const point &p);
point next_point(const point &p, int i);
private:
graph board;
int size;
};
/**
* constructor, build a underlying graph from a chessboard of size s
*
* @param s
*/
chessboard::chessboard(int s)
: size(s) {
for (int i = 0; i < size; ++i)
{
for (int j = 0; j < size; ++j)
{
int start = i * size + j;
point p(i, j);
for (int k = 0; k < 8; ++k)
{
/// the next possible knight position
point np = next_point(p, k);
if (is_valid(np))
{
int end = np.x * size + np.y;
/// add edges in both directions
board.add_edge(start, end);
board.add_edge(end, start);
}
}
}
}
}
/**
* find and print a path from (x, y) to (size, size)
*
* @param x
* @param y
*/
void chessboard::solve_knight(int x, int y) {
int start = (x-1) * size + (y-1);
int end = size * size - 1;
board.bfs_shortest(start);
list l = board.get_path(end);
int count = 0;
for (list::const_iterator itr = l.begin(); itr != l.end(); ++itr)
{
cout << "(" << *itr/size + 1 << ", " << *itr%size + 1<< ")";
if (count++ != l.size() - 1)
{
cout << " -> ";
}
}
cout << endl;
}
/**
* whether or not the point is valid in the chessboard
*
* @param p
*
* @return true for valid
*/
bool chessboard::is_valid(const point &p) {
if (p.x < 0 || p.x >= size - 1 || p.y < 0 || p.y >= size - 1)
{
return false;
}
return true;
}
/**
* the next possible position, every has 8 next possible position, though not
* all 8 position is valid
*
* @param p the original knight position
* @param i
*
* @return
*/
chessboard::point chessboard::next_point(const point &p, int i) {
int knight[8][2] = {
{2, 1}, {2, -1},
{-2, 1}, {-2, -1},
{1, 2}, {1, -2},
{-1, 2}, {-1, -2}
};
return point(p.x + knight[i][0], p.y + knight[i][1]);
}
int main(int argc, char *argv[])
{
if (argc != 4)
{
cerr << "Wrong arguments! Usage: knight.bin N x y" << endl;
return -1;
}
int N = atoi(argv[1]);
int x = atoi(argv[2]);
int y = atoi(argv[3]);
chessboard chess(N);
chess.solve_knight(x, y);
return 0;
}