mituh's notes


  • 首页

  • 归档

邓俊辉<<数据结构>>-公开课-01-F

发表于 2018-01-10

动态规划

Make it work \
Make it right - 递归
Make it fast - 迭代
– Kent Beck

动态规划: 通过递归找出算法本质,并且给出了初步解,再讲其等效成迭代形式

int fib(int n) {
return (2 > n) ? n : fib(n-1) + fib(n-2);
}

work, right, but not fast.

封底估算

$^36 = 2^25, ==> $^43 = 2^30 = (2^10)^3 = 10^9 flo = 1sec
$^5 = 10, ==> $^67 = 10^14 flo = 10^5 sec = 1 day

递归跟踪

效率低的原因是递归实例被重复调用。

解决方法A(记忆:memoization)

int fib_memoization(int n, int mem_lst[]) {
// 将已经计算的结果, 制成表备查
if (is_exist(mem_lst[n])) {
return mem_lst[n];
} else {
if (2 > n) {mem_lst[n] = n;} else {
mem_lst[n] = fib_memoization(n-1, mem_lst) +
fib_memoization(n-2, mem_lst);
return mem_lst[n];
}
}
}

解决方法B 动态规划
上楼梯。


int fib_dynamic(int n) {
int f = 0, g = 1; // fib(0) = 0; fib(1) = 1;
while (0 < n--) {
g = g + f;
f = g - f;
}
return g;
}

不太能理解, 先放着

最长公共子序列

子序列(Subsequence): 有序列中若干字符,按原相对次序构成

最长公共子序列(Longest common subsequence),两个序列公共子序列的最长者

可能有多个,可能有歧义

实现

暂时还实现不了

动态规划

理解了一下,方法上有差异,思想上为自下而上的求解,有那么点在gh中做的时候的思想在里面。

邓俊辉<<数据结构>>-公开课-01-E

发表于 2018-01-10

迭代与递归

分而治之:分解成子问题,递归式的求解。

空间复杂度:指不包含算法的输入本身所占的空间之外,所需要的另加的用于计算所必须的空间总量。

减而治之

 合                合并
----> 问题 --------
^ / \ ^
| 缩减/ \ 平凡 |
| / \ |
-> 子问题 子问题 <---
! ! ! !
------- ------
治 治

-—例子1—–
求n个总数之和

int sum(int A[], int n) {
// A为整数数组, n为问题规模, 返回数组中整数的总和
return
(n < 1) ?
0 : sum(A, n-1) + A[n-1];
}

分析:

  • 该问题分解为一个缩减问题sum(A, n-1) 和 一个平凡问题A[n-1]
  • 最后规模小到一定程度时, 缩减问题变为 平凡问题
  • 将两个问题合并得到结果

复杂度如何?

递归跟踪(recursion trace)分析 (用于简单的递归)

  • 检查每个递归实例
  • 累积所需要时间(调用语句本身抹去,计入递归实例)
  • 其总和是算法执行时间
    || ||
    vv vv
    线性递归:得出上述递归是线性递归,复杂度渐进O(n)

递推方程分析 (用于复杂的递归)
上述例子中:
T(n) = T(n-1) + O(1) // recurrence
T(0) = O(1) // base

T(n) - n = T(n-1) - (n-1) = T(n-2) - (n-2) ...
= T(2) - 2
= T(1) - 1
= T(0) - 0 = O(1)

T(n) = O(1) + n = O(n)

-——例子2—————–
任给数组A[0,n), 将其前后颠倒 // 更一般的子区间[lo, hi]
统一接口 : void reverse(int * A, int lo, int hi);

递归版

规模缩小两个单位。

void reverse(int* A, int lo, int hi) {
// 输入指向数组A的指针, A中要转置的左区间lo, A中要转置的右区间lo
// 无返回值,改变指针A所指向的数组, 使其倒序
if (lo < hi) {
swap(&(A[lo]), &(A[hi]));
if (((hi-lo) == 1) || ((hi - lo) == 0)) return;
reverse(A, lo + 1, hi - 1);
}
}

分析时间复杂度:

递归跟踪(recursion trace)

int main();
reverse(A[n], lo, hi);
reverse(A[n-2], lo+1, hi-1);
reverse(A[n-4], lo+2, hi-2);
...

reverse(A[1], lo+(n-1)/2, hi+(n-1)/2);
or
reverse(A[0], lo + n/2, hi+ n/2);
(n-1)/2 , n是奇数
/
O(1)* = O(n)
\n / 2, n是偶数

递推方程

T(n) = T(n) + O(1);
T(n) - n = T(n-1) - (n-1)
T(n) - n = T(2) - 2
= T(0) - 0
T(n) = T(0) + n = O(n)

<span style=”color”:blue”>感觉不怎么正确??

课后推敲:

迭代原始版本

void reverse_iterate_original(int* A, int lo, int hi) {
// 迭代原始版本
next:
if (lo < hi)
{swap(&A[lo], &A[hi]); lo++; hi--; goto next;}
}

使用next作为分支标记,goto跳转,真的能让代码运行,从来没用过

迭代精简版

void reverse_iterate(int* A, int lo, int hi) {
// 迭代版本
while (lo < hi) swap(&A[lo++], &A[hi--]);
}

分而治之(divide-and-conquer)

分解为多个或两个子问题,得到解后归并。
-—–二分递归———-

int mid_sum(int A[], int lo, int hi) {
// 数组求和 :二分递归
if (lo == hi) return A[lo];
int mid = (lo + hi) >> 1;
return mid_sum(A, lo, mid) + mid_sum(A, mid + 1, hi);
}

注意 mid + 1

// 分析:
// 被分解成两个相似问题,mid_sum(n/2)
// 规模每次缩减一半,最后到达递归基
// 将多个问题结果合并

分析复杂度:

递归跟踪(几何归纳)

以2为倍数的几何级数,总和与末项同阶。

递推方程(代数运算)

两个问题都是n/2
累加O(1)时间
递归基O(1)时间返回

递推关系
T(n) = 2* T(n/2) + O(1)
T(1) = O(1)

..
T(n) = O(n)

Max2: 迭代1

从数组区间A[lo, hi)中找出最大的两个整数A[x1]和A[x2] // A[x1] > A[x2]
比较次数要尽可能的少

int max2_three_iters(int A[], int lo, int hi) {   // 1 < n = hi - lo
int max1 = 0, max2 = 0;
int x1, x2;
if (hi < lo) return -1;
for (int i = lo; i <= hi; i++)
if (max1 < A[i]) {max1 = A[i]; x1 = i;} // hi-lo-1 = n-1

if (x1 != lo) {
for (int i_lo = lo; i_lo < x1; i_lo++)
if (max2 < A[i_lo]) {max2 = A[i_lo]; x2 = i_lo;} // x1-lo-1
}
if (x1 != hi) {
for (int i_hi = x1+1; i_hi <= hi; i_hi++)
if (max2 < A[i_hi]) {max2 = A[i_hi]; x2 = i_hi;} // hi-x1-1
}
int max_array[2] = {max1, max2};
std::cout << " A[x1] = " << A[x1] << '\n'
<< " A[x2] = " << A[x2] << std::endl;
}

总共比较n-1+n-2 =2n-3

int max2(int A[], int lo, int hi) {
// 遍历一次,改变指针
int* x1 = &lo;
int lo_next = lo + 1;
int* x2 = &lo_next;
if (A[*x1] < A[*x2]) {x1 = &lo_next; x2 = &lo;}
for (int i = lo + 2; i <= hi; i++) {
if (A[*x2] < A[i]) { // 索引i的对象比较小的值大
if (A[*x1] < A[i]) { // 索引i的对象甚至超过了较大值
x2 = &(*x1); x1 = &i;
break;
}
*x2 = i; // x1指针指向的元素赋值成i
}
}
std::cout << " A[*x1] " << A[*x1] << '\n'
<< " A[*x2] " << A[*x2] << std::endl;
}

最好情况: 1 + (n-2)1 = n-1
最坏情况: 1+ (n-2)
2 = 2n-3

即使在最坏情况,也更高效的改进算法

  • 分而治之
  • 实现退化情况
void max2(int A[], int lo, int hi, int & x1, int & x2) {     // [lo, hi)
if (lo + 2 == hi) {
if (A[lo] < A[lo+1]) {
x1 = A[lo+1]; x2 = A[lo];
} else {
x2 = A[lo+1]; x1 = A[lo];
}
return;
} // T(2) = 1
if (lo + 3 == hi) { // lo, lo+1, lo+2, lo+3; 19, 2, 3, -1;
x1 = lo, x2 = lo+1;
if (A[x1] < A[x2]) {x1 = lo+1; x2 = lo;}
for (int i = lo+2; i < hi+1; i++) {
if (A[i] > A[x2]) {
if (A[i] > A[x1]) {
int tmp = x1;
x1 = i; x2 = tmp;
break;
}
x2 = i;
}
}
return;
} // T(3) <= 3
int mid = (lo + hi) >> 1;
int x1L, x2L; max2(A, lo, mid, x1L, x2L);
int x1R, x2R; max2(A, mid+1, hi, x1R, x2R);
if (A[x1L] > A[x1R]) {
x1 = x1L; x2 = (x2L < x1R) ? x1R:x2L;
} else {
x1 = x1R; x2 = (x2R < x1L) ? x1L:x2R;
}
} // 1 + 1 = 2

最坏情况: T(n) = 2 * T(n/2) + 2 <= 5n/3 -2

递推方程推导过程: ?

最好情况复杂度: ?

总结

两种重要算法策略:减而治之,分而治之

两种分析方法:递归跟踪和递推方程

邓俊辉<<数据结构>>-公开课-01-C

发表于 2018-01-10

渐进分析: 大O记号

好读书不求甚解。

考察DSA(考察人):

  • 长远
  • 主流,

渐进分析(Asymptotic Analysis) n >> 2, 对于规模为n输入,算法

  • 需要执行的基本操作数: T(n) = ?
  • ..存储单元: S(n) = ? // 通常不考虑?教材

教材P33

  • 空间复杂度不会超过常数规模,纵然是新开辟的,算法所需的空间总量,也不过与基本操作的次数同阶。从这个意义上,时间复杂度是空间复杂度的天然上限。
  • 但两种情况下会有意义:
    • 对空间效率也异常在乎(时间复杂度的平凡上界难以令人满意)
    • 数据的输入规模大。

big-O()比T(n), f(n)更简洁,但依然反应增长趋势(长远)

  • 常系数可忽略(主流):O(f(n)) = O(c*f(n))
  • 低次项可忽略(主流)

O(1)
常数

  • 2 = 2013 = 2013*2013 = O(1)
  • 效率: 最高效
  • 出现的情况,需要具体分析
    不含循环,不含分支转向,一定不能有(递归)调用?
    
    教材

O(logn)
对数

  • 常底数无所谓
  • 常数次幂无所谓
  • 多项式
  • 效率: 接近于常数

O(n^c)
多项式

  • 一般 取最高次
    线性:所有O(n)函数
    从O(n)到O(n^2):编程习题主要覆盖范围
    效率: 已经可令人满意。

O(2^n)
指数
效率: 算法成本增长极快,通常不可忍受。
n^x->2^n, 是从有效算法到无效算法的分水岭。

2-Subset
直觉算法:逐一枚举S的每一个子集,并统计其中元素总和
定理:2-Subset is NP-complete
意即:就目前模型而言,不存在在多项式时间内回答此问题的算法。
除非,添加条件:分布规律啦, 票的总数啦,

复杂度增长速度表格。

邓俊辉<<数据结构>>-公开课-01-B

发表于 2018-01-10

功能:
算法:
规范:图灵机复位h的原因。 => 在软件中叫做接口

RAM: Random Access Machine
共同之处:无限的空间。
寄存器:

  • 常数赋值给RAM
  • RAM之间直接赋值 R[i] < R[j]
  • RAM之间间接赋值 R[i] <- R[R[j]]
  • RAM+-
  • 判断0跳转 IF R[i] = 0 GOTO l
  • 判断正跳转 > 0
  • 跳转
  • 停止 STOP

概括:
对计算工具抽象后的简化。
独立于环境和平台,可评判效率。

  • 将执行时间,转化为操作次数。T(n) = 算法为求求解规模为n问题的,所需执行次数
  • 这个次数是清晰的,可度量的。

RAM实例:Floor
向下取整除法, 0 <= c, 0 < d
c%d = max {x | dc <= c}
= max {x | d
x < 1+c}

R[0] = c+1, R[1] = d

0, R[0] <- c // int c = c;
1, R[1] <- d // int d = d;
2, R[2] <- 0 // int x = 0;
3, R[3] <- 1 // int a = 1;
4, R[0] <- R[0] + R[2] // c++
6, R[0] <- R[0] - R[1] // c-=d
7, R[2] <- R[2] + R[3] // x++
8, IF R[0] > 0 GOTO 4 // if c > 0 goto 4
9, R[0] <- R[2] - R[3] // else x– and;
9, STOP // return R[0] = x

src:

<<游戏之旅>>笔记

发表于 2018-01-04

最近读了云风大大的书, 感谢云风, 对我有所帮助。
选读 第1章, 第2章, 3.1, 3.2, 3.3, 3.5.2, 4.4.4, 8.1, 第9章, 13, 14

对我有所启发的points

  • 写email的建议
  • C和C++的关系
  • C++的开发经验
  • 学习C++阅读次序
  • template的技巧
  • 开发的成功和失败的经验
  • 游戏编程几个反思
  • 后记中编程实践和思考方法
  • 致谢中云风父亲的背景以及教育方法

2. 算法

2.1 程序 = 算法+数据结构

计算机解决的问题,看成一个需要求解的函数,

  • 算法:将输入转化为输出的方法。
  • 数据结构: 需要解决问题中的信息用计算机进行的数字描述方法,以及相应的对这些信息的操作。

2.1.1 算法

时间和空间之间寻求平衡。

  • 空间换时间的做法,非常广泛,即预处理
  • 时间换空间,不容忽视,即使重复计算。

依赖平台特性:

  • 非并行计算机,不考虑用并行来提速。
  • 空间允许随机访问,不是纸带机的顺序。

2.1.2 数据结构

简单定义:对数据的组织方法,还有对数据组织方式的处理方法。
C++已经提供了常见的数据结构,为什么还要学?

  • 学蕴含思想
  • 根据实际情况对数据结构进行改造,更高效。
1. 线性表
有限逻辑上的有序数列,有确定的前驱和后驱;
分为成:数组和链表;

数组:
2. 堆栈、队列和串
3. 树、二叉树及其他

树:
有层次的数据集的组织方式。

GUI界面,通常用树来组织;
游戏中的对象管理,用树来解决管理上的层次问题。

二叉树:
严格来说不算是树。
表达式计算,数据压缩,排序查找方面有很多用途。

四叉树、八叉树:
空间状态划分,

  • 四叉树:平面
  • 八叉树: 空间

空间: 场景空间+ 调色盘算法等(向量空间)

图:
节点没有父子关系,纯粹的点和边的集合。
节点和节点之间允许加上一些与它有关的数,称为权(weight), 带权图称为网络。
节点和节点之间可以有方向,也可以无方向。

图应用于现代网络游戏中,多服务器设计,或者三维游戏中的大场景描述,值得开发者好好研究。

映射表
std::map,STL最复杂的容器。

禁忌思想,

  • 模拟人的记忆过程,从某点开始,想临近区域扩展解。经过的地方一概进入禁忌状态,为了短期搜索回来,造成循环搜索,会有一个记忆寿命,超过若干步骤后解决禁忌。这个步骤叫做Tabu Length, 禁忌长度。禁忌长度过大会导致计算量增加,过小则会进入循环搜索。
  • 蚂蚁队伍在附近找最高点。任意选择一个起点,一开始就保持队伍胡乱地向周围爬。蚂蚁队伍遵循一个原则,就是不能回到最近经过的位置。

算法一种思想,自己动手实践,了解这些方法,慢慢地就可以真正运用它到实际中去。不光光要会描述一个问题,以适应不同的算法;还要了解更多的算法会让我们更快解决棘手问题。

2.4 优化

  • 数学方法的改进
  • 预运算来节省时间(空间换时间避免重复运算)或是重复运算来节省空间
  • 简化算法求得近似来取代精确解(或最有解)
  • 改进数据组织方式,用更少的操作处理更多的数据,甚至避免冗杂数据的处理。

碰到棘手的问题,不要先急于找Google,或者问别人,应该自己思考最好的解决方法。每个人都有自己无数的解决方法,每次独立的思考,都是对思维的一次开阔。

<>

C
C语言本身是简洁的。语言本身仅仅只是提供了一种用计算机角度实现算法的符号。
C语言函数,让问题分而治理它。
全局变量,可以让函数之间不通过输入参数来访问一些公有的数据。
最让人诟病就是指针的设计。从高级语言的角度来讲,程序员不需要指针。我们只需要有可以指带数据的标识之物。

BASIC

比较解释语言和C语言的不同

解释型语言:

  1. 每运行一次,解释器就读一次,对符号进行翻译
  2. 中间状态以严格规定保存起来,在后面的语句需要时被重新加载
    C语言
  3. 在编码后需要多做一些工作,即他们会被翻译成为机器码,运行时就不需要翻译。
  4. 编译器对代码同类向合并,最后的机器码会很简洁。

逻辑学教育,BASIC,适合理解编程。

3. 编程语言

3.3 C++

C++的不同理解

第一印象可能是为了面向对象的设计,实际上C++是一种支持各种编程范式的语言。它支持面向过程的编程,基于对象的编程,面向对象的编程,以及使用template实现的泛型编程。

C对于C++的优势

简洁,而非高效;适合做小内存的嵌入式系统开发。

但大多数游戏平台,C++比C更适合

3.4 汇编语言

合适场合使用汇编,能让程序跑得更快。
了解汇编,能让你了解代码最终会以什么形态运行于CPU,对理解高级语言有所帮助,
在紧要关头帮忙,程序出现莫名错误,又不能在源码级调整程序。

4. 前Windows时代

4.3 保护模式下的开发工具

个人主页建立之初,我罗列出了个人兴趣 :…很多。但是大而全的效果是,我一样都做不好,而且和其他人的个人主页相比没有特色。稍加考虑,立刻砍掉了除了游戏开发之外的所有项目。

  • 翻译allgero文档:自己做了一个辅助翻译的工具;细致了解; 交朋友
  • 写关于游戏编程技术方面的小文,各处转载

  • 利用假期,走访网上认识的从事游戏开发的朋友,慢慢踏上了游戏制作的道路。

学习新的技术,翻译一本相关的英语著作可以算是捷径。

  • 比囫囵吞枣的读一遍英文原文要有效的多。
  • 因为有责任感,必须用心搞清楚每一个句子的意思,以免错误而误导阅读你的译作的人们。
  • 即使对此有所了解,翻译后也能更上一层楼。
  • 英语水平也会提高。

4.4 闲话allegro

4.4.4 几何图形和3D

图形引擎提供,可以绘制一些几何图形,点,线,多边形等等,但制作平面游戏这些并不是必须的,因为游戏中的一切,事先都可以用预先制作好的图片代替掉。

3D游戏,却是基于多边形。通过空间多边形的网络信息,把一些平面的图片,做一些变形,映射到多边形网格上。
相关的:

  • 透视映射
  • 矩阵,三维空间中变换的工具, 自带一套矩阵运算。
  • 实现了一套定点数的运算该改善游戏中需要的数学运算速度。

1,定点数
巧妙的利用定点数,可以避免许多浮点误差问题,是很有价值的。

浮点数:计算机用一种纯小数加指数的形式表示实数。
定点数:利用整数运算来模拟小数的方法,那就是定点数来表示小数。

高16位,表示整数位;低16位,表示小数部分;实际是60000+倍,小数点的位置是固定的。

精读在1/6*10^4, 十进制小数点后四位的精度。

定点数的一些计算技巧和优势:

  • 原本复杂的三角函数计算,可以依照所需要的精度,预先制作好查询表,以空间换取时间。
  • 角度表示方式不是360和2pi, 而是64表示直角, 256表示圆周角。

4.5

云风关于写email的建议:

  • 回复收到的每封email, 即使没空写, 也让对方知道自己已读。
  • 使用纯文本,发送过大附件前请求
  • 合适的标题,能概括内容;偏题时,修改标题。
  • 删除尽可能多的引信,尤其是对方的签名,对主题表达无意义。
  • 合理分段,表达自己意见,别赞同两字多写一点。
  • 发出之前,读一遍,修改掉错别字和语法错误,核对一次收件人是否正确
  • 不要随意公开转载私人信件,即使转载也注明出处。

8. 引擎中的优化

“风魂”中很多蹩脚的编码方法,不成熟的整体架构、非系统的编码规范和不严谨的接口定义,但对于自己干了这件贻笑大方的事情,自己却不后悔。敢于把自己的无知展现给世人,是一种勇气。

只要保持真诚谦逊,错误的存在就能得到理解;整体上或许不完善,但还是有后来者,依旧可以从中学到闪光点,避免踏上弯路。

9. C和C++

9.1 从C到C++

1, C只提供了CPU本来就提供的操作,把他映射到更容易让人理解和描述的书写形式上。为了让程序员使用,引入了栈上临时变量,和堆上动态变量的概念,而不是直接面向寄存器。

2, C对大块的数据,使用数据指针来表达。

3, C一个个函数组织起来,函数之间按层次调用,去处理那些不同结构的数据

4, C是对汇编语言的一种抽象, 正如汇编语言是对机器底层指令的一个最小幅度的抽象,为了人类方便控制机器。C程序员要做问题描述和机器模型之间的桥梁。

5, C是容易学的,它非常接近机器模型,而机器模型非常简洁,有条理;有效率,接近机器模型;困难性,离实际描述太远,在问题描述和机器模型之间找到对应关系,会随着问题的复杂性提高而急剧增加。

困难性的说明,
高级语言往往改为对问题本身抽象,把问题归为特定的类别,然后,语言本身只解决这些被抽象的问题,来简单编程的难度,但往往只能针对特定的类型。

C++面向对象的说明

  • 既然保持强大的抽象能力,又保持底层符合机器模型的优势,在设计变得简单的情况下不失效率。
  • 将对象分为不同的类型,每个对象都是这种对象的实例。类型的设计是有层次的,就好像动物是一种大类型,而哺乳动物和爬行动物都属于动物。

9.2 C vs C++, 效率至上

C++中混有C语言的好处:

1, 更广泛的移植性
2, C的思考方式,会让不致于陷入面向对象和泛型编程的泥沼,只是说很容易钻进设计方法的牛角尖
3, 良好的C语言接口可能使得模块的使用方法更容易理解,对多人合作和多语言编程是一种好事
4, C简洁, 编译速度快

9.3 优雅的C++

将C细化,又不增加额外的开销;
不对程序员有过多限制。

9.3.1 宏

宏的作用

1, 定义常数
2, 代码生成
3, 内联代码
4, 对编译流程作出选择

1, 定义常数

#define PI 3.1415926f;
const float PI;

C++会选择inline函数+template定义常数,最终编译器会把它优化成一个常数,而没有对函数调用的消耗。

经典的如:

#define min(a, b) ((a<b)?(a):(b))

template <typename T> const T& min(const T &a, const T &b) {
return a<b ? a:b;
}

9.3.2 const修饰以及类型转换

const修饰

const chat 而不是 C 的 char

将函数参数写成const;
成员变量修饰成const, 只有在构造的时构建他们;
为成员函数增加一个const, 表达这个函数不会修改类的成员变量

类型转换

C中几乎任何类型都可以任意转换,缘于C汇编的根。
C++用static_cast 以模板的语法表达看起来可以互相转换的类型互换

const和const之间的转换,只能通过const_cast转换。

严格描述每个对象的const性质,可以帮助在编译期发生错误减少。

9.3.3 隐藏实现

好的C++程序会把所有数据都声明成private的,尽量在同一类中暴露过多的public方法,而protected慎用,至少尽量不用在成员数据上。

作为良好的面向对象设计,继承类很少去扩展基类的功能,这样子作为具体实现,被隐藏于接口后,结构变得干净整洁。

9.3.4 引用而非指针

指针是导致资源被遗忘释放的罪魁祸首。

  • 当旧的指针被赋予新值,旧的值消失,如果消失的值是唯一指向某个对象的地址,那么这个对象控制的资源将永远留在内存中,直到进程结束被系统回收。

引用和指针在实现本质上是完全相同的,但引用的语法只能在构造的那一刻被赋值,并永远不能修改。

C++优雅设计中,会用引用代替成员变量中的指针,而减少实现类的时候不小心犯错误。

9.3.5 命名空间

引用外部的名字空间到当前位置。可以防止不同库之间的名字冲突。

9.4 C++的误区

自己的一条准则: 尽可能地使用结构最简单的工具来完成任务,直到这个工具不合适。

举例:

  • 能用C写的程序,不用C++;能用C的原生数组的情况不用std::vector。能用std::vector的情况不要用std::map, 能自己写的代码不用第三方库。

解释:

  • 没有完美的程序,也没有完美的原则。
  • 不主张写C++的标准库,重写MFC或者广为大众所使用的代码。大多数C++程序员,没到达这个技术理解。

9.4.1 类层次过细

会造成间接调用引起的损耗。

9.4.2 滥用操作符重载

为了让用户写出的类适用于现存的模板,让用户构造的对象和C++原生的类型有相同的表现,操作符重载是有意义的。
如果仅仅是让程序看起来紧凑,或者是类使用起来“好玩”,那就是对其的滥用了。

9.4.3 滥用标准容器

用std::vector就不想再碰语言中的原生数组,用过std::string, 就不知道 const char* 是为何物,这是许多C++程序员的通病。

std::map可以这样子做,
如果只是为了创建一张key-value的对应表可供查询,完全可以用
key-value对应的关系记录下来,只做一次排序,在检索的时候可以使用二分查找查找,一种快捷又节省内存的方法。

9.4.5 滥用多重继承

会造成实现过于复杂,几乎所有的多重继承问题,都可以化为组合方式来解决。

9.4.6 忽视C++高级特性的复杂度

如果把两种复杂度高的特性运用于项目中,其复杂度不是两倍,而是平方。

9.4.7 学习C++

我现在无法把自己学会的东西,理解的东西借助文字教给入门者,因为许多的知识需要自己在实践中领悟。过多的细节,太快地展现出来反而会增加学习的复杂度,难以消化。

因此,需要

1,学
2, 用
3, 思考
2, 回到1

如果期望C++成为自己开发中的利器,应该尽量多用C++做项目,尽量可能地体验更多的设计方法,用心去写程序,而不是单单去实现而已。自己写出的代码,多多思考,对感觉不好的部分重新设计。

推荐一个阅读次序:

1, 先学会C,只是也是C++子集的部分。
2, <>
3, <> 和 <>
4, C++ 标准文档(1998定制,电子档),遇到问题不是去查某本教材,而是直接翻阅文档。
5, <>,深入了解C++设计的根源以及C++编译器实现方法入手.
6, <<深度探索C++物件模型>>对追求高效的C++程序员尤为受用。

进阶学习: 需要一定的开发经验才能阅读

  • <>, <>, <>
  • 泛型编程<>,有点难,但是花上一定的时间一定有收获。
  • <>template使用自信,对泛型编程感觉良好,
  • <>想更精通标准库,C++程序员案头必备的参考手册。
  • <>, 与前面的effcive系列一样

9.5.3 再论动态内存分配

//…

理解动态内存的捷径,自己写一个内存分配器。从一大块给定的内存上,分配出用户提交的内存。

9.6 template

template最初只为了取代C++语言的宏设计,后来被赋予了实例化的特性,可以针对某些特别的类型做特殊的操作。

9.6.1 封装C++的成员函数的调用

9.6.4 避免重复代码

大部分程序错误的根源:

  • 在多个地方表达相似的概念,意味着日后改动一个地方,就必须记得改动相似的所有地方。而直接复制这些代码,导致编译器并不知道这些地方的相似性,不能为你提供帮助。而人,随着项目扩大,几乎不可能记住做过多少次这种复制动作。

没有意识到在重复,可以用加强对代码糟糕味道的嗅觉敏感度和提高作为编码者的责任心来达到。

举个blit()例子

9.6.5 选择最佳的容器

很多语言都提供了一种未定义类型的变量,可以用来保存各种不同类型的变量。而C++没有提供这样的类型,但是可以用template来模拟一个,也就是设计一个容器存放不同类型的对象。

对于一个容器到底是保存

  • 对象指针
  • 对象值

  • 对于体积较大的对象,保存指针, 复制很快

  • 对于体积较小的对象,保存对象值, 减少间接;减少指针空间的占用

可以利用模板在编译期间由编译器自动进行;

9.6.5 延迟计算

由于涉及到了关于重载运算符的内容,暂且通读。大概是讲,可以自定义一种运算方式,但是又要对这种运算方式进行优化,所以需要模板的帮助,在编译期就对他进行不同函数的选择。

9.6.7 编译时的计算游戏(未完成)

三个数排序
+

9.7 小结

早犯一天错误,就可以早一天改正错误。

13. 开发方法

13.1 失败的经验

1, 过多的工作压力压到一个人身上。
2, 过分的弹性工作制
3, 没完没了的变化和返工
4, 没有及时的测试
5, 项目的主导严重偏向了某一职位上

1, 过多的工作压力压到一个人身上。
迫于压力,无法学习新的东西,使用好方法解决问题。独揽大局,只能解决眼前碰到的bug, 耦合度太高。

2, 过分的弹性工作制
兴奋时效率高,停下来几天没有进展也可能发生。到了项目后期,bug重重,受到挫折之后,失去新鲜感,假借弹性工作制之名,导致怠工拖垮项目。放弃的时候,并非没有压力,可能只是不知道下一步该怎么做,或是问题太多,无法入手,项目已经失控。

3, 没完没了的变化和返工
想法太多,什么都想加入进来,导致最后漏洞很多。

> 4, 没有及时的测试
很少有严格的测试,而是把错误积累。

5, 项目的主导严重偏向了某一职位上
程序员,策划,美术,盲目的跟从和固执的坚持自己都可能会影响整个项目.

13.2 成功的经验

1, 引擎和实现的分离
2, 结对编程(XP 极限编程)
3, 随时方便地测试
4, 尽早发现结构上的问题, 并尽早重构
5, 其他

1, 引擎和实现的分离
早期的程序员都是研究图形显示的技术开始的,总想表达更绚丽的图案,游戏程序变成了代码的集合。
许多程序员都是模块见耦合度过高,对于软件的整体稳定性不利。
和图形图像打交道的人,整个团队一个人就够了,他不需要去管任何的游戏的逻辑。

其他还包括,图像处理模块,声音,网络,时钟,文件读写,windows窗口控制,需要和操作系统直接打交道的东西,不管多简单都应该分离。

2, 结对编程
找个水平差的不太远的程序员和自己配成一对,只有一台计算机,大家选一个人坐在键盘前,另外一个人口述。两个人需要不断的交流,频率不应该低于一分钟一次。整个设计思想是由后面只动口不动手的人主导,而由键盘操作的人实现。由于人的思维速度是快于键盘输入的速度的,那么观看的人有空闲的时间可以用来思考,很容易看出代码和结构的问题。

潜在问题的代码,在XP极限编程中,被称做代码的坏味道。

好处:

1, 促进参与项目的程序员的自身的提高。水平较低的学习新东西,水平较高的把思路说出来整理思路。
2, 参与项目人员互换位置,使得维护繁杂的文档不再那么重要,一旦有人离开,项目不会受到影响。大家的交流更顺畅,关系更融洽。
3, 提高工作效率。单独工作遇到问题刷网站,而这种方法是交流解决问题。互相监督和激65励。

3, 随时方便地测试
测试应该从开发者开始,从项目一开始就开始。

4, 尽早发现结构上的问题, 并尽早重构

5, 其他

脚本和版本控制。
软件开发的素养。

14. 编程和游戏

技术派在挖空心思模拟出更真实,更绚丽的画面;创意派在为自己构思的游戏中的一个绝妙的主意沾沾自喜。
而忽略了一些更重要的东西。

14.1 操作

任何一个游戏都是玩家和程序之间通过操作设备进行交互产生乐趣。

14.2 角色设定

生动的角色,RPG,增加游戏气氛

14.3 操作技术

3D和2D

  • 应该由游戏本身的需要决定。
  • 3D技术问题是通过一些近似算法而不是数学上严格的方案,得到令人满意的效果。
  • 2D技术怎样控制和管理不断膨胀的图片数量

14.4 浅谈网络游戏

社会性
除了传统的游戏它自身的娱乐性之外,还存在一种社会性。
因为社会性,就更需要在游戏中设计完备的经济体系,追踪货币的流通,实物的交换,弄清游戏社会中的经济是如何运作的。

看似不是程序的事情,但非程序的策划很难去做:

  • 大规模的数字采集和统计的工作
  • 大规模所要求的服务器架构,需要减轻数据库负担,减轻服务器压力,必须在设计上针对硬件水平作出精简。只有熟悉软件架构的人才能去做。

平衡性

  • 传统游戏中渡过来的方法,多数是依靠经验而不是数学推算来解决,慢慢的会变得不那么有效果。
  • 程序员有数学和计算机运算工具的能力,可以完善为平衡而更理论化的工具。

14.5 小结

一个好的游戏程序员和策划之间的界限往往很模糊,所以游戏程序员除了编程方面的修养,各个领域的只是都应该有所涉猎,以提高自身的修养。

后记

技术问题,固然本源的方法,理论变化不大。但计算机这种偏重工程的实践,一旦实际化到具体上,却有日新月异的变化,想要搞清楚问题,不是朝夕之功,只好借着自己对旧知识的理解去参悟新的知识。

致谢

记得在初中毕业前夕,我曾经萌发过放弃上重点高中乃至上大学的想法。但是,面对盛怒之后,老泪纵横的父亲,第一次看到父亲的眼泪时,我理解了,理解了一个求知欲如此强烈的人却由于时代的错误,甚至连高中都没有机会去读,在繁忙的工作中,熬夜自学考上大学的人,怎能不对自己的儿子在学业上有更高的期望呢。

关于文档学习方法和图形学学习的随想

发表于 2017-12-28

上篇笔记OpenGL绘制三角形附练习来来回回写了三天,当然,除了学习外,我还去练习了网球,还去参加了一次grasshopper草猛歌会;学习openGL之外,我同时在学习dsa, 看了云风的<<游戏之旅>>, 查阅了OpenGL与图形引擎的相关资料,

BLOG整理文档学习方法

从云风的书中偶得一段话,

学习新的技术,翻译一本相关的英语著作可以算是捷径。

  • 比囫囵吞枣的读一遍英文原文要有效的多。
  • 因为有责任感,必须用心搞清楚每一个句子的意思,以免错误而误导阅读你的译作的人们。
  • 即使对此有所了解,翻译后也能更上一层楼。
  • 英语水平也会提高。

发现产生了一些共鸣。
其中之一,是之前学习Grasshooper的一段经历,我学grasshopper已经是0.76版本了, NCF那一批人开始学习也是10左右的事情了,那个时候他们组织学运算器的方式也跟这个很相似,就是翻译Help, 所以有了<<grasshopper运算器手册>>一书。不知道是否这种是共通的?还是因为他们中也有程序员,亦或者是因为grasshopper变成了程序爱好者,理解了这种方式?

之二是石老师推荐学生翻译<<Form Space & Order>>, <<Parten Language>>, <<A New Kind of Science>>等书来学习数字化技术。

我自己也享受到了这些方法的好处,现在又被云风清晰说出了那种模糊的感觉。便开始尝试在写笔记的时候,结合着文档理解下函数原理,并把它们用心整理好。这也是最近开始写博客来的好处,虽然之前一直用evernote记笔记写日记,但是用一种这是要整理给别人看的心态去做一件事情,效果便也很不同,总之这对于技术学习来说是很好的。

图形学学习

另外,这几天在跟着OpenGL的教程学习,开始对OpenGL和图形学有了模糊的认识:

  • 图形学是研究用计算机如果画出一个图像的学科。
  • OpenGL教程会教我如何去用封装代码去渲染一个场景,以及背后没展开的图形学原理。
  • 他的封装性,与我原来想象的能够研究一些与计算几何相关的图形问题算法有较大出入。
  • 但是学习封装性能够让我先去了解一个大概,以及算法的具体应用性,等到发现其中核心算法的学习路径,开始学习也为时不晚,一是对图形学有了些理解,二是明白各自使用方向,三是dsa的课程也是那些算法实现的前提,四是每天都能够编写C++找到乐趣和问题。

下面是摘自 Milo Yip 叶神在知乎上的回答,要借鉴这个学习轨迹, 并以此勉励自己:

如何开始用 C++ 写一个光栅化渲染器?

Milo Yip
计算机图形学、编程、C++ 等 7 个话题的优秀回答者
228 人赞同了该回答
@空明流转 开发过高大上的SALVIA, @Yong He 则提到了Larrabee。我来提供另一些观点。

首先,如果从学习角度出发,不必一开始完全根据现时GPU的架构及概念,来用软件复制一遍。现时的GPU主要是基于三角形光栅化及z-buffer。

如果我们从图形学的历史进程来学习,可以这样做练习:

2D部分:

1, 光栅化2D点(就是在二维数组上画点,了解色彩基本原理,并解决影像输出问题)
2, 光栅化2D直线(布雷森漢姆直線演算法、吴小林直线算法等)
3, 2D直线的剪切算法(见Line clipping)
4, 光栅化2D三角形(scan conversion)。避免重复光栅化相邻三角形边界的像素(edge equation)。
5, 光栅化简单/复杂多边形
3D部分:
1, 把顶点从三维世界空间变换至二维屏幕空间,绘画顶点(如银河星系数据),操控摄像机旋转模型。
2, 在剪切空间进行3D直线的剪切算法,把顶点连线(如各种三维正多面体)光栅化成wire frame模型
3, 以多边形来定义三维模型。使用画家算法来光栅化那些多边形。
改为使用深度缓冲。
4, 实现简单的纹理映射,先做屏幕空间的插值,然后实现简单的perspective-correct texture mapping。
5, 实现简单的顶点光照,使用顶点颜色插值实现Gouraud shading。
6, 通过顶点法线插值,实现Phong shading。
7, 实现其他贴图技术,如mipmapping(也可试Summed area table)、bilinear/trilinear filtering、bump mapping、normal mapping、environment mapping等。

邓俊辉<<数据结构>>-公开课-01-D

发表于 2017-12-28

我把邓老师课上对自己有需要的笔记写下来,并在看完视频后补充教材和课后习题的内容.

算法分析

运用DSA

算法分析的两个任务

  • 正确性(不变性 * 单调性)
  • 复杂度增长速度表格

C++等高级语言的基本指令,等效于常数条RAM的基本指令;渐进意义下,相当
分支转向:goto // 算法灵魂;出于结构考虑,被隐藏
迭代循环:for(), while().. // 本质上 “if+goto”
调用+递归 // 本质上也是 “goto”

复杂度分析方法:

  1. 迭代: 级数求和
  2. 递归: 递归跟踪 + 递推方程
    猜测 + 验证

级数

算术级数:与末项平方同阶

幂方级数:比幂次高出一阶

几何级数(a > 1):与末项同阶

收敛级数:O(1)

未必收敛,但长度有限

h(n) = 1 + 1/2 + 1/3 + … + 1/n = O(logn) // 调和级数
log1 + log2 + log3 + … + logn = log(n!) = O(nlogn) // 对数级数

推荐书籍

<<Concrete Math>> 具体数学

循环 vs 级数

没有耦合的二层循环

for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
O1Operation(i, j);

算术级数: n * n = O(n^2)
等效:矩形被填充的过程,时间复杂度等于矩形面积。

耦合的二层循环

for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
O1Operation(i, j);

算术级数: n(n-1) / 2 = O(n^2)
等效:三角形被填充,复杂度等于矩形面积。

递增不为1的二层循环

for (int i = 0; i < n; i++)
for (int j = 0; j < i; j += 2013)
O1Operation(i, j);

算术级数:O(n^2)

外循环左移一位(加倍)

for (int i = 1; i < n; i <<= 1)
for (int j = 0; j < i; j++)
O1peration(i, j);

几何级数:O(n) // ??

更复杂的实例

for (int i = 0; i <= n; i++)
for (int j = 1; j < i; j += j)
O1peration(i, j);

习题解析

取非极端元素、冒泡排序

取非极端元素

算法:

1, 从S中取出三个元素{x, y, z}
2, 确定并排除其中的最小值和最大值
3, 输出剩下的元素z

int ordinaryElements(int A[], int n) {
// 从n >= 3个互异整数中,除最大、最小者以外,任取一个“常规元素”
// 先比较a,b;再确定c对于(a,b)区间的关系
int a = A[0], b = A[1], c = A[2]; // 从特定单元读取元素O(3)
// 统一成区间(a, b), 用于c对其判断
if (a < b) { } else {
swap(&a, &b);
}
if (c < a) return a;
else if (c > b) return b;
else
return c;
// return 输出非极端数O(1)

// =======================================
// T(n) = O(3) + O(3) + O(1) = O(7) = O(1)
}

结论:无论输入规模有多大,所需执行该算法的执行时间都不变。

起泡排序问题

void bubblesort(int A[], int n) {
for (bool sorted = false; sorted = !sorted; n--)
for (int i = 1; i < n+1; i++) { // 自左向右逐对检查[0,n)各相邻元素
if (A[i-1] > A[i]) { // 若逆序,则
swap(&A[i-1], &A[i]); // 令其交换位置
sorted = false; // 消除全局有序标记
}
}
}

算法分析

不变性:经k轮扫描交换后,最大的k个元素必然就位
单调性:经k轮扫描交换后,问题的规模缩减至n-k
正确性:经至多k轮扫描后,算法必然终止,且能给出正确答案

基本且重要的技巧:通过挖掘不变性和单调性,证明算法的正确性

封底估计 Back-Of-The-Envelope Calculation

不需要工具

787km 占据整个周长的1/50 => 整个周长4wkm

抓住问题的主要方面,简洁得出总体规律

在复杂度分析中,对象是时间。

封底估计实例

  • 一天: = 24hr 60min 60sec = 25 * 4000 = 10^5 sec
  • 一生: = 1世纪 = 100yr 365 = 3 10^4 = 3 * 10^9 sec
  • “50年” = 1.6 * 10^9 sec
  • 三生三世: 300yr = 10^10 = (1 googel)^(1/10) sec
  • 宇宙大爆炸至今: 10^21 = 10^(10^10)^2 sec
    三生三世是10^10s
    三生三世中的一天,相当于在一天中的1s
    整个宇宙中的三生三世,就是在三生三世中的0.1s

1亿 = 10^9

复杂度和浮点运算能力flops相除,能得到某算法的时间。

人口普查 n = 10^9
\=====================
普通PC 1Ghz 10^9 flops
Bullesort: O(n^2) ==> (10^9)^2 = 10^18

时间: 10^18 / 10^9 = 10^9
310^9 = 100yr, 10^9 = 30年
\=====================
普通PC 10^9 flops
Mergesort: O(n\
logn) => (10^9)*log(10^9) = 30 * 10^9

时间; 30 * 10^9 / 10^9 = 30s
\=====================
天河1A 10^15 flops
Bullesort:

时间:20min: 10^3s
\=====================

gitignore-invalid

发表于 2017-12-27

OpenGL绘制三角形附练习

发表于 2017-12-25

代码已托管在/timtingwei/LearnOpenGL

图形管线渲染有哪几个阶段?

顶点数据(Vertex data[]) ->

1, 顶点着色器(Vertex Shader) ->
2, 形状(图元)装配(Shape Assembly) ->
3, 几何着色器(Geometry Shader) ->
4, 光栅化(Rasterization) ->
5, 片段着色器(Fragment Shader) ->
6, 测试与混合(Test And Blending)

三个着色器可以自己定义。

顶点数据(Vertex data):

  • Vertex是一个3D坐标的集合
  • Vertex Data是用顶点属性(Vertex Attribute), 如位置Position, 颜色Color

顶点着色器(Vertex Shader):

  • 把3D坐标转换成另外一种3D坐标
  • 允许对Vertex Attribute做基本处理

片段着色器(Fragment Shader):

  • 计算一个像素最终的颜色
  • 着色器包含3D场景数据(光照,阴影,光的颜色)

Alpha测试和混合(Blending):

  • 测试深度,反应前后。
  • 检查Alpha并混合,(同个片段着色器,渲染多个三角形时候的颜色可能不同)

补充: 图元(Primitive):

  • 任何一个绘制指令的调用,都把图元传递给OpenGL。
  • 如GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。

绘制绘制三角形

顶点着色器

使用GLSL(OpenGL Shading Language)编写顶点着色器

#version 330 core          // 每个着色器起始于版本声明, core代表核心模式

layout (location=0) in vec3 aPos; // 设定输入变量的位置值

void main() {
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

为什么需要layout (location=0)设定位置值?

  • glVertexAttributPointer函数第一个参数指定配置顶点属性,而**layout (location=0) 定义了顶点某变量的position属性。

相关内容in, out, uniform, type类型, 向量组合, main函数,会在下一节,着色器中讲解

编译顶点着色器

编写着色器源码后,

1, 先创建着色器对象;
2, 再把源码附加到着色器对象上;
3, 编译并检查

1, 创建顶点着色器对象

unsigned int vertexShader;                         // ID引用
vertexShader = glCreateShader(GL_VERTEX_SHADER); // 传递着色器类型参数

2, 附源码

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);

glShaderSource函数的几个参数:

0, 着色器对象
1, 传递的源码字符数量
2, 顶点着色器的真正源码
3, 先设置为NULL

3, 编译

glCompileShader(vertexShader);
// 检查编译是否成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
// 如果不成功,用glGetShaderInfoLog获取信息, 存储再InfoLog中,打印
if {!success} {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

片段着色器

同理顶点着色器,编写着色器源码

#version 330 core

out vec4 FragColor;

void main() {
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

片段着色器只有一个vec4作为out, 表示最终的输出颜色, 最后一个值是alpha量

编译片段着色器

1, 创建对象
2, 附加源码
3, 编译并检查

自己重新写一遍, 并检查错误
1, 创建对象尝试

unsigned int fragmentShader;
glCreateShader(fragmentShader, GL_FRAGMENT_SHADER); // ERROR

改正

unsigned int fragmentShader;
glCreateShader(GL_FRAGMENT_SHADER); // DEBUG::不需要用着色器对象作为函数的参数,

查阅khronos.org - glCreateShader

  • Like buffer and texture objects, the name space for shader objects may be shared across a set of contexts, as long as the server sides of the contexts share the same address space. If the name space is shared across contexts, any attached objects and the data associated with those attached objects are shared as well.
    会绑定上下文中的ID创建object, 而且同一ShaderType会被联系起来。

2, 附源码尝试

glShaderSource(1, fragmentShader, fragmentSource, NULL)   // ERROR

改正

glShaderSource(fragmentShader, 1, fragmentShaderSource, NULL);  // DEBUG::交换参数位置

  1. 着色器对象
  2. 传递源码字符的数量

3, 尝试编译

bool success;    // ERROR
glCompileShader(GL_FRAGMENT_SHADER, fragmentShader, success); // ERROR
unsigned char* infoLog[512]; // ERROR
if (!success) {
// 失败把报错内容储存再infoLog中输出
glGetShaderinfoLog(fragmentShader, &success, &infoLog); // ERROR
std::cout << "COMPILE::ERROR::FRAGEMENT::SHADER"
<< infoLog << std::endl;
}

改正

glCompileShader(fragmentShader);     // DEBUG::只要着色器对象一个参数
int success; // DEBUG:: success是一个int类型
char infoLog[512]; // DEBUG:: char 并非 unsigned cha*
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); // DEBUG::获取success在先,才能对success做判断
if (!success) {
// 失败输出
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}

创建着色器程序

绘制图元时,使用的是glUseProgram(shaderProgram), 因此,先要创建着色器程序

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

链接着色器对象

两个着色器对象要链接。

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

编译着色程序

与之前的着色器编译原理相同,只是调用函数和参数变量改变了。

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n"
<< infoLog << std::endl;
}

之后可以glUseProgram函数,激活程序对象。激活后,着色器调用和渲染调用都会使用这个程序对象。

删除着色器

完成以上几步后不要忘记删除着色器

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

输入顶点以及索引

// 顶点输入
GLfloat vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};

赋值array的时候, 不要忘记{};

// 顶点索引
GLuint indices[] = {
0, 1, 3 // 第一个三角形
// ... // 可根据vertices数组, 建立多组索引
};

创建和绑定顶点数组对象(VAO)

VAO = Vertex Array Object

功能:

  • 随后的顶点属性调用都会储存在这个VAO之中。
  • 使得在不同顶点数据和属性设置之间切换变得简单。

存储内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

使用注意:

  • 先VAO,再绑定和设置VBO, EBO 以及设置顶点属性指针
GLuint VAO;
glGenVertexArrays(1, &VAO); // 创建VAO对象
glBindVertexArray(VAO); // 绑定顶点数组对象

创建和绑定顶点缓冲对象(VBO)

OpenGL如何解释顶点数据的内存,并指定其如何发送给显卡。

作用:

  • 通过VBO(Vertex Buffer Object)管理这个内存,它会在GPU内存中存储大量顶点。
  • 一次性发送大量数据到显卡上,而不是每个顶点发送一次。CPU发到显卡上速度慢,因此,一次发送多个比较好。而发送过去之后顶点着色器又能够立即访问。

我想这个过程应该是发生图形管线渲染的第一个阶段,是顶点数据和顶点着色器如何对接?就是依靠CPU发送到显卡上

GLuint VBO;
glGenBuffer(1, &VBO); // 创建
// 把顶点数组复制到缓存中提供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

索引缓冲对象(EBO)

可以创建不同的索引数组,而使用同一个顶点数组。如下:

unsigned int indices[] = { // 注意索引从0开始! 
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

GLuint EBO;
glGenBuffer(1, &EBO); // 创建
// 把顶点索引复制到缓存中提供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

设置顶点属性

vertex_attribute_pointer

  • 位置数据被储存为32位(4字节)浮点值 => siezeof(flaot) = 4 types = 32bits
  • 每个位置包含3个这样的值
  • 没有空隙, 紧密排列。
  • 数据中的第一个值再缓冲开始位置。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexattribArray(0); // 开启位置为0的顶点属性

glVertexAttribPointer参数

1, 要配置的顶点属性的位置值。输入的参数顶点属性的位置值。还记得之前的 layout (location=0) in vec3 xx 吗?
2, 顶点属性的大小。 vec3 => 3个值组成
3, 指定数据类型。 (GLSL中vec*都是由浮点数值组成的)
4, 是否被标准化。 GL_TRUE代表是,所有数据(对有符号数据是-1)映射到0到1之间;
5, 步长,这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节。因为是紧密排列设置成0也可以,OpenGL自己会设置。

  1. 偏移量。位置数据在数组开头就设置成0

顶点属性默认是禁用的,要用glEnableVertexAttribArray函数开启。
输入的参数顶点属性的位置值。layout (location=0) in vec3 xx

绘制图元

在主循环内部绘制图元。

1, 激活程序对象
2, 绑定缓存对象
3, 绘制
4, 解绑缓存对象

绘制两种方式:
glDrawArrays();

  • 使用当前绑定的顶点缓存对象进行绘制
    glDrawElements()
  • 使用当前绑定的索引缓冲对象中的索引进行绘制

第一种,

// 激活程序对象
glUseProgram(shaderProgram);
// 绑定顶点数组对象
glBindVertexArray(VAO);
// 使用当前绑定的顶点缓存对象进行绘制
glDrawArrays(GL_TRIANGLES, 0, 3);
// 解绑缓存对象
glBindVertArray(0);

第二种,

// 激活程序对象
glUseProgram(shaderProgram);
// 绑定顶点数组对象
glBindVertexArray(VAO);
// 使用当前绑定的索引缓冲对象中的索引进行绘制
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// 解绑缓存对象
glBindVertArray(0);

VAO有种结合VBO+EBO的意思
再回顾一遍它的存储内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

退出循环后删除对象,释放缓存

glDeleteVertexArray(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);


课后练习

实现完成一个三角形/矩形的绘制后,对很多问题不是很清楚。如,

1, 索引缓冲对象EBO绘制和顶点缓冲绘制VBO的区别和联系?
2, 顶点着色器和片段着色器之间的关系?
3, 设置顶点属性glVertexAttribPointer();函数的几个参数并不理解?

1. 添加加更多顶点到数据中,使用glDrawArrays,尝试绘制两个彼此相连的三角形

glDrawArrays();
根据数组数据中渲染图元,可以指定渲染图元的类型,给定第一个索引和绘制点的个数,从而确定哪些点被绘制。

// 先设置顶点数组
GLfloat vertices[] = {
// 第一个triangle
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
0.0f, 0.0f, 0.0f, // 连接处
// 第二个triangle
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f, // 左上角
0.0f, 0.0f, 0.0f // 连接处
};
// 激活程序对象
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 6); // 从顶点数组索引0开始依次绘制6个顶点

glDrawElements();
根据索引的数组数据渲染图元,可以指定渲染图元的类型,给定一个数量,索引的类型以及指向索引的指针

// 顶点输入
GLfloat vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};

GLuint indices[] = { // 索引从0开始
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

还需要在绑定VAO后绑定VBO和EBO

// ...

// 激活程序对象
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) // 索引数组绘制6个顶点

这里并没有给出索引数组的指针,我估计是绑定VAO后,Draw函数调用会从上下文获得这个指针。不知道这个猜想是否正确。

练习1源码 - github


2. 创建相同的两个三角形,但对它们的数据使用不同的VAO和VBO

// 不同VAO和VBO的ID各自放在同一个数组中,可以以数组创建,但需要单独绑定,复制缓存,设置顶点属性,以及单独绘制。

glGenBuffers(GLsizei n,
GLuint buffers);
生成缓存对象的名字,输入缓存名字的数量n以及缓存对象名字存在的数组,返回n个缓存对象名字在*Buffers
中。直到调用glBindBuffer(),生成的缓存名字才和某一缓存对象绑定在一起。

glGenVertexArrays(GLsizei n,
GLuint* arrays);
返回n个顶点数组对象名字,存储到arrays指针中。

glBindBuffer(GLenum target,
GLuint buffer);

绑定一个已经命名的缓存对象。

target
----------------------------------------------------------
GL_ARRAY_BUFFER | 顶点属性
GL_ELEMENT_ARRAY_BUFFER | 顶点数组索引
GL_TEXTURE_BUFFER | 纹理数据缓存
GL_UNIFORM_BUFFER | Uniform块存储
----------------------------------------------------------

buffer
缓存对象的名字

glBindVertexArray(GLuint array);
用名字array绑定一个顶点数组对象。array是从之前glGenVertexArrays()返回的,或者是0意味着接触当前的绑定。

glBufferData(GLenum target
GLsizeiptr size
const GLvoid * data
GLenum usage);
创建或者初始化一个缓冲对象的数据的储存。

target
这里是GL_ARRAY_BUFFER

size
新建的缓冲对象大小,以字节(type)类型表示,可以使用sizeof(data)得到

data
用于拷贝和初始化新的data对象的,指向data的指针,如果是NULL的话,没有数据被拷贝

usage
储存好的数据的使用模式。 GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, or GL_DYNAMIC_COPY有这么几种。这里只用于静态绘制

glVertexAttribPointer(GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid * pointer);
上面有所解释。现在就够用了。

glEnableVertexAttribArray(GLuint index);
启用设置好的顶点属性数组,
glDisableVertexAttribArray(GLuint index);
关闭

index
顶点属性的序号,即位置的序号。与顶点着色器的(location=n)相关.

glBindVertexArray();
用名字array绑定一个顶点数组对象。array是从之前glGenVertexArrays()返回的,或者是0意味着接触当前的绑定。

glDrawArrays(GLenum mode,
GLint first,
GLsizei count);
练习1中已经出现过,调用之前先要绑定Array;
根据数组数据中渲染图元,可以指定渲染图元的类型,给定第一个索引和绘制点的个数,从而确定哪些点被绘制。

代码实现如下:

// 输入两个三角形各自的顶点数组
GLfloat firstTriangleVertices[] = {
// 第一个triangle
0.5f, 0.5f, 0.0f, // 右上角
0.1f, -0.5f, 0.0f, // 右下角
-0.5f, 0.3f, 0.0f, // 左下角
};

GLfloat secondTriangleVertices[] = {
// 第二个triangle
-0.5f, -0.5f, 0.0f, // 左下角
-0.1f, 0.5f, 0.0f, // 左上角
0.5f, -0.3f, 0.0f // 右下角
};

1, 共同以VAOs和VBOs以数组创建

GLuint VBOs[2], VAOs[2];
glGenBuffers(2, VAOs);
glGenVertexArrays(2, VAOs);

2, 单独绑定, 复制, 设置顶点属性

glBindVertexArray(VAOs[0]);
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glBufferData(GL_GL_ARRAY_BUFFER, sizeof(firstTriangleVertices),
firstTriangleVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);

// 同理VAOs[1], VBOs[1]..
glBindVertexArray(VAOs[1]);
glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]);
glBufferData(GL_GL_ARRAY_BUFFER, sizeof(secondTriangleVertices),
secondTriangleVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);

3, 单独绘制

// ==========firstTriangle=========
glUseProgram(shaderProgram);
glBindVertexArray(VAOs[0]);
glDrawArrays(GL_TRIANGLES, 0, 3);
// ==========secondTriangle========
glBindVertexArray(VAOs[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);

练习2源码

3.创建两个着色器程序,第二个程序使用一个不同的片段着色器,输出黄色;再次绘制这两个三角形,让其中一个输出为黄色

之前提出了一个问题,顶点着色器和片段着色器之间的关系?在这个练习中,有所涉及。

// 放在同一个数组中,但需要各自绑定和各自绘制, 也可以匹配不同的顶点数组
// 注意:片段着色器不能放在数组里。
// 顶点着色器,片段着色器attach+link成program, 着色器需要创建,绑定源码,编译,检查编译四个步骤。

1, GLSL编写橙色和黄色着色器代码
2,分别附着,编译,检查着色器,并attach到不同的program
3, 用不同的program进行draw

1, GLSL编写橙色和黄色着色器代码

// 片段着色器0
const GLchar* fragmentShaderSource0 = "#version 330 core\n"
"out vec4 color;\n"
"void main() {\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" // orange
"}\n\0";

// 片段着色器1
const GLchar* fragmentShaderSource1 = "#version 330 core\n"
"out vec4 color;\n"
"void main() {\n"
"color = vec4(1.0f, 1.0f, 0.0f, 1.0f);\n" // yellow
"}\n\0";

2,分别附着,编译,检查着色器,并attach到不同的program

// 创建片段着色器
GLuint fragmentShaderOrange;
GLuint fragmentShaderYellow;
GLuint shaderProgramOrange;
GLuint shaderProgramYellow;
fragmentShaderOrange = glCreateShader(GL_FRAGMENT_SHADER);
fragmentShaderYellow = glCreateShader(GL_FRAGMENT_SHADER);
shaderProgramOrange = glCreateProgram();
shaderProgramYellow = glCreateProgram();
// ===========编译第一个片段着色器===================
// 源码附加到着色器对象上
glShaderSource(fragmentShaderOrange, 1, &fragmentShaderSource0, NULL);
glCompileShader(fragmentShaderOrange);
// 检测glComplieShader是否编译成功
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
<< infoLog << std::endl;
}

// ===========编译第二个片段黄色着色器===================
// 源码附加到着色器对象上
glShaderSource(fragmentShaderYellow, 1, &fragmentShaderSource1, NULL);
glCompileShader(fragmentShaderYellow);
// 检测glComplieShader是否编译成功
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
<< infoLog << std::endl;
}
// 把两个着色器对象链接到用来渲染的两个着色程序中
glAttachShader(shaderProgramOrange, vertexShader);
// =====连接第一个片段着色器=====
glAttachShader(shaderProgramOrange, fragmentShaderOrange);
glLinkProgram(shaderProgramOrange);
glGetProgramiv(shaderProgramOrange, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgramOrange, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n"
<< infoLog << std::endl;
}
// =====连接第二个片段着色器=====
glAttachShader(shaderProgramYellow, vertexShader);
glAttachShader(shaderProgramYellow, fragmentShaderYellow);
glLinkProgram(shaderProgramYellow);
glGetProgramiv(shaderProgramYellow, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgramYellow, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n"
<< infoLog << std::endl;
}
// 删除着色器
glDeleteShader(vertexShader);
glDeleteShader(fragmentShaderOrange);
glDeleteShader(fragmentShaderYellow);

3, 用不同的program进行draw

// =====激活程序对象绘制第一个triangle======
glUseProgram(shaderProgramOrange); // 使用第一个片段着色器连接的program
glBindVertexArray(VAOs[0]);
glDrawArrays(GL_TRIANGLES, 0, 3); // 从索引0开始依次绘制3个顶点

// =====激活程序对象绘制第二个triangle======
glUseProgram(shaderProgramYellow); // 使用第二个片段着色器连接的program
glBindVertexArray(VAOs[1]);
glDrawArrays(GL_TRIANGLES, 0, 3); // 从索引0开始依次绘制3个顶点

练习3源码

=======

refwebsite:
你好三角形 - learnopengl-CN

(转载)谈编程语言

发表于 2017-12-24

文章目录
Bruce Eckel:编程生涯
Peter Norvig:十年学会编程

转载自编程随想 - program think整理的, 关于编程的心态定位和实践方法的文章,比较经典, 对我有所启发的两篇。


Bruce Eckel:编程生涯

作者 Bruce Eckel 是编程界的大牛,著有大名鼎鼎的《Thinking in C++》和《Thinking in Java》。
本文是他对程序员(尤其是新手)的忠告。

================================

大家总是问一个错误的问题:“我应该学习C++还是Java?”在本文中,我将告诉大伙儿:对于选择编程生涯真正需要关注的是哪些问题。

请注意,这篇文章的目标读者并不是那些已经做出自己选择的人。(对于这些人而言)你会继续自己的编程生涯,而不管别人会怎么说。因为它已经渗透到你的血液中,你已经无法摆脱。你已经知道答案:C++、Java、Shell脚本、Python、还有其它一大堆的语言和技术,你都理所当然地会去学习。甚至有可能你才仅仅14岁,就已经知道好几种不同的语言。

问我这样的问题的人可能来自其他行业,或者来自诸如Web开发之类的领域。他们知道HTML是一种类编程语言,而且想尝试构建某些更大型的应用。但我特别希望,当你在问这个问题时,你已经意识到了想要在计算机领域取得成功,你需要掌握自学能力,而且永不停息。

在这个领域做得越多,我越觉得软件开发比任何行业都更接近于写作。 我们从来不知道是什么造就了优秀的作者,我们只知道什么时候我们会喜欢某个人的文字。编程不是一种工程,仅需要把东西从入口倒进去,然后再转动手柄。把软件开发看成确定性的,是一个诱人的想法。因为这个想法,人们总想搞出一些工具来帮我们开发出想要的软件。但是我的经验告诉我,事实并非如此——人的重要性远高于流程。而软件是否运行在一部精确的机器上已经越来越不重要了——这犹如测不准原理对人类的影响。

我的父亲是造房子的,小时候我偶尔会帮忙打下手,放放砖块之类。他和他的木工告诉我,他们是为我好才让我干这些活——这样我就不至于走入这个行业。事实确实是这样。

我们不妨把软件开发比作盖房子。造房子的人当然不可能完全一样。这些人里面有:混凝土工、屋顶工、管道工、电工、砖瓦工、水泥工、瓦片工、搬运工、粗木工、细木工。当然,还有工头。每个工种都需要相应的技能,这些技能都需要花时间和精力去掌握。跟软件开发一样,造房子也是一个“建立/推翻”的过程。如果你想很快地获得回报,你可能从搬运工和砖瓦工开始做,这样的话,你无需太多的学习曲线就可以获得回报。当需求很多时,你的工作会很稳固,甚至收入也可能提升——如果没有足够的人手的话。但是,一旦行情不妙,木匠甚至工头就可能把砖瓦工一脚踢开。

当互联网刚刚兴起时,仅仅是花一点时间学习HTML,你就可以得到一份薪水丰厚的工作。但是当形势惨淡时,对于技能的要求更高了——HTML程序员(就像搬运工和砖瓦工一样)第一个被抛弃了,而拥有更高技能的程序员则留了下来。

我想说的是: 除非你准备活到老学到老,不然的话,不要进入这个行业!编程看起来似乎是一个高收入而又稳定的工作。但要做到这一点,唯一的途径是:始终让自己更有价值。

当然,你总能找到例外。总有那么一些人,仅仅学了一门编程语言,就可以胜任留在一个岗位上,而不需要增长他的技能。但他们只是幸免于难而已,他们最终无疑是很脆弱的。为了不让自己变得脆弱,你需要持续的提高自己,通过阅读、加入用户组、参加研讨会…… 你学得越深入,你就越有价值,也就意味着你有更好的职业前景,可以配得上更高的薪水。

另一个方法是:先大致了解这个领域,找到最适合你的地方。打个比方:我的兄弟对软件很感兴趣,也入了这行,只不过他的工作是安装、维修、升级电脑。他总是一丝不苟,所以当他把电脑搞好,一定会很完美——不光是软件,连电线都会被仔细地捆好。他总是生意兴隆,远超出他的精力所能及。他甚至都不用担心 .com 泡沫的崩溃。显然他的饭碗不容易被抢走。

我在高校里待了很久,甚至还在UCLA(加州大学洛杉矶分校)进修博士学位,后来又幸运地终止了。我说“幸运”是因为我不再喜欢呆在学校,而我之前在高校待了那么久,只是因为我很享受它。但我所享受的,基本上是不务正业的东西——艺术和舞蹈课,在校报工作,还有一小撮计算机课程(之所以说计算机课程“不务正业”,是因为我本科是物理专业,研究生才是计算机专业)。虽然我在学术上远谈不上卓越(有意思的是很多当时也许不会接受我这个学生的学校现在却用我的书做教材)。我真的很享受作为学生的日子,当我完成博士课程,也许会以一个教授的身份终老一生。

但就如现在看到的,我在学校里最大的收获恰恰来自我那些“不务正业”的课程,它们拓展了我的思维,使之超越了“我们已经知道的东西”。在计算机领域中,你总是为某种目标而编程。你对目标了解得越多,你就做得越好。我遇到过一些欧洲的研究生,他们需要结合其它专业领域来搞编程,他们的论文需要解决这个专业领域的特定的问题。

了解编程之外的领域,将会极大得提高你解决问题的能力 (就如同多学几种编程语言将极大地提高你的编程技能)。很多时候,我发现仅仅学习计算机专业的学生,比那些(除了计算机之外)拥有其它背景的学生,在思维上有更多的局限性。因为后者有着更严谨的思维,也不那么容易想当然。

有一次我组织了一次会议,其中一个议题是:理想的应聘者有哪些特征:
◇把学习当成生活方式。比如:你应该知道不止一种语言,没有什么比学习一门新语言更能让你开阔眼界了。

  • 知道如何获取知识
  • Study prior art
  • 善用工具
  • 学会把事情简化
  • 理解业务
  • 为自己的错误负责。“我就是这样的”是不能接受的托词。能找到自己的失误。
  • 成为一个领导者,善于沟通和激励。
  • 搞清楚你在为谁服务
  • 没有绝对正确的答案(更好的方法总是存在的)。展示并讨论你的代码,不要带着感情因素——你的代码并不等于你本人。
  • 明白完美是渐进的

适当尝试一些冒险——尤其是能令人感到害怕的冒险。当你尝试之后,将体会到出乎意料的兴奋。(在冒险的过程中)最好不要刻意去计划某个特定的结果。当你过于注重结果,你往往会错过那些真正有价值的问题。我的冒险往往是这样开始的——“我们先做些试验,看看它会把我们带到什么地方”。

或许某些人会对我的回答感到失望,并回复我说:“是的,这很有趣也很有用。但我到底应该学什么?C++还是Java?” 我再重复一次:并不是所有的问题都有一个唯一的简单的答案。问题的关键不在于选择某个编程语言,然后掌握之。问题的关键在于:持续学习,并且很多时候,有不止一个选择。 相信我所说的,你的生活会更精彩!

原文出处:
http://www.artima.com/weblogs/viewpost.jsp?thread=259358


Peter Norvig:十年学会编程

作者 Peter Norvig 是计算机科学家,Google 的研究总监。
在本文中,Peter Norvig会告诉你:为什么急功近利地学习软件开发技术是没效果滴?

================================

为啥都想速成?

随便逛一下书店,你会看到《7天自学Java》等诸如此类的N天甚至N小时学习Visual Basic、Windows、Internet的书。我用亚马逊网站的搜索功能,出版年份选1992年以后,书名关键词是:“天”、“自学”、“教你”,查到248个结果,前78个是计算机类图书,第79个是《30天学孟加拉语》。我用“天”换成“小时”,结果更惊人,有多达253本书,前77本是计算机图书,第78是《24小时自学语法句式》。在前200名中,96%是计算机的书。

结论就是:要么人们急于学习电脑,要么计算机比其他东西学起来要异常简单。没有任何书是关于几天学习贝多芬或量子物理的,甚至连犬类装扮都没有。费雷森(Felleisen)等人在其著作《如何设计程序》中同意这个趋势,其中提到:“坏设计很简单,笨蛋才用21天学,尽管他们还是真傻。”

让我们看看《三日学会C++》这个书名意味着什么:

◇学习:
三天内你可能没有时间写出有意义的程序,或者从中积累经验。你不可能有时间去跟职业编程者一起去理解在C++环境下的状况。简而言之,你没有充足的时间学很多。所以这本书只能说肤浅的知识。正如亚历山大·波普(Alexander Pope)所言:一知半解是很危险的。

◇C++:
三天内你可能学会C++的句法(如果你已经了解其他的语言),但你还不会使用它。打个比方,假如你是个Basic程序员,你可能写出Basic风格的C++程序,而无法理解C++的真实好处。那要点是什么?艾伦·佩里斯(Alan Perlis)曾经说过:“一门不能影响你编程观点的语言不足学的。”有可能你学了一点点C++(或者诸如Javascript、Flex之类),因为你需要和现成的工具接口以完成手头的任务。这种情况下,你不是在学习如何编程,只是在学习如何完成任务。

◇三日:
不幸地是,这远远不够,下一部分会详细讲。

如何用十年掌握编程

研究人员(Bloom (1985), Bryan & Harter (1899), Hayes (1989), Simmon & Chase (1973))得出结论:想要在诸多领域达到职业水平需要十年,比如国际象棋,作曲,电报操作,绘画,弹钢琴,游泳,网球以及神经心理学和拓扑学的研究。关键是精心练习,只是一遍一遍地重复是不够的,必须挑战恰好超越你能限的事情,尝试并思考你的表现,并自我矫正。周而复始。这并无捷径!4岁的音乐奇才莫扎特用了13年才能创作世界级的音乐。另外,披头士乐队似乎在1964年的埃德·苏利文( Ed Sullivan show)演出中一炮而红,但是他们自从1957年就在利物浦和汉堡的酒吧演出,在取得广泛关注后,第一部重量级作品《佩珀军士》(Sgt. Peppers)是在1967年发行。马尔科姆·格拉德威尔(Malcolm Gladwell)撰文描述了一项针对柏林音乐学院学生的研究,他们被分为尖子,中等和不足三类,并被问到他们练琴的情况:
所有三组中的人,开始学琴的年龄大概相差无几,五岁左右。在刚开始的几年,所有人练习量也差不多,一周两三个小时。自八岁开始,实质性变化就有了。那些精英学生开始比其他人练习更多:九岁的时候一周六个小时,十二岁的时候一周八个小时,十四岁的时候一周十六个小时,一直到二十岁的时候一周要超过三十小时。截止到二十岁,在他们的生涯里已经有总计一万小时练琴。仅仅表现可以的那部分学生加起来是八千小时,那些未来的音乐老师有四千小时。

所以,更确切地说,一万小时,而非十年,是个神奇之数。萨缪尔·约翰逊(Samuel Johnson, 1709-1784)认为还需更长时间:“卓越乃一生之追求,而非其它”。 乔叟(Chaucer, 1340-1400)抱怨道”the lyf so short, the craft so long to lerne.” (生之有限,学也无涯)。希波克拉底(Hippocrates, c. 400BC)因这句话被世人所知:”ars longa, vita brevis”(译注:拉丁语,意为“艺无尽,生有涯”),更长的版本是 “Ars longa, vita brevis, occasio praeceps, experimentum periculosum, iudicium difficile”,翻译成英文就是 “Life is short, (the) craft long, opportunity fleeting, experiment treacherous, judgment difficult.” (生有涯,艺无尽,机遇瞬逝,践行误导,决断不易)。

我的编程成功秘笈是:

  • 首先要对编程感兴趣,能从编程中得到乐趣。一定要让它足够有趣,因为你要保持你的兴趣长达十年。

  • 与别的程序员交流;阅读别人的代码——这比看任何书或参加培训课都重要。

  • 实践。最好的学习乃实践。俗话说:“编程的至高境界一定要通过充分的实践才能达到,而个人的能力可通过不懈努力获得显著提升。” (p. 366) “最有效率的学习需要明确的目标,适当的难度,知识回馈,并容许重复或修正错误。” (p. 20-21) 《实践认知:每日的思维、数学及文化》(Cognition in Practice: Mind, Mathematics, and Culture in Everyday Life) 在这方面可做参考。

  • 如果你愿意,花四年学习大学课程(或者再加上读研)。这将给你赢得某些工作机会,并给予你在该领域的深层见解。但如果你不喜欢学校的学习,你同样可以在工作中获得相似的经验。无论如何,单靠书本是远远不够的。“学习计算机科学不会让你成为编程专家,如同学习绘画和色彩理论不会让你成为画家一样”。这是埃里克·雷蒙德(Eric Raymond)说的,他是《新黑客字典》(The New Hacker’s Dictionary)的作者。我雇用过的最优秀程序员,只有高中文凭。但他开发过许多伟大软件,有自己的新闻组,通过公司认股赚的钱就让他买下了自己的夜店。

  • 和其他程序员一起参与工程项目。在某些项目中担当最优秀程序员,在另一些项目中充当最差劲程序员。充当领头羊的时候,你要测试你领导一项工程的能力,并用你的视野来激发他人;如果在项目组中垫底,就应该学习其它牛人在做些啥,以及他们不喜欢做的(看他们把哪些活让给你做)。

  • 继续别人的工程项目。去理解先前程序员写的程序。学习如何理解并解决先前程序员没有考虑到的问题。思考你的程序该如何设计以便让之后的程序员更容易维护。

  • 至少学6种程序语言。其中包括一种支持类抽象的(Java和C++),一种支持函数抽象的(如Lisp或ML),一种支持语义抽象的(Lisp),一种支援声明规范的(如Prolog或C++模板),还有一种支援协程的(Icon或Scheme),另外一种支持并发的(Sisal)。

  • 记住,在“计算机科学”里有“计算机”一词。理解计算机执行你的代码的时候花费的时间。比如:从内存中取一个字(考虑有无缓存未命中情形),连续从磁盘读字,或者在磁盘中定位。

  • 参加语言标准化工作。这可能是有关 ANSI C++ 委员会,也可能是决定你编码风格是两格缩进或四格缩进。无论如何,你要知道其他人对语言的喜好程度,有时还要想想他们为什么喜欢这样。

  • 知道自己应该在何时脱身于语言标准化

所有上述这些,很难通过书本的学习来达到。我头一个孩子出生时,我读了所有的“如何做”(How To)系列的书籍,却依然对育婴毫无头绪。30个月后,我第二个孩子出生,我还需要温习一下那些书吗?绝对不!相反,我完全可以参照个人经验,而结果相当有效。这更让我确信:我的经验胜过那些专家们写的上千页文字。

弗雷德·布鲁克斯(Fred Brooks)在《没有银弹》(No Silver Bullet)一书给出了寻找顶级设计师的三条建议:

  • 尽早系统地识别出顶级设计师。
  • 分配一个人作为其职业规划的导师。
  • 给予机遇让成长中的设计师互相磨砺。

此处假定有部分人已经有成为伟大设计师的潜质,你所需的就是要诱导他们。艾伦·佩里斯(Alan Perlis)一针见血地指出:“假如人人都可以学雕刻,那就得教米开朗基罗如何不去干雕刻。对于伟大程序员,也是如此。”

所以,简单地买一本Java书,你或许能找到些有用的东西,但绝不会让你在24小时内甚至24天抑或24月内,成为行家里手。

原文出处:
http://norvig.com/21-days.html

1…161718

mituh

179 日志
99 标签
© 2018 mituh
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4