小小程序员的学习计划

追过很多新技术,迷茫于还有太多东西要学,有些找不到方向、不知所措。常在一个叫酷壳的网站上转悠,渐渐想明白了,其实学习技术也像看电影看书一样,要看就看经典的。我们的生命如此短暂,作为程序员的职业生涯可能会更短。与其追逐各种新潮的技术,不如把有限的精力投入到经典的技术之中。

平时酷爱看各种技术书籍,但缺乏系统的梳理。读书感悟都散落在每本书的字里行间和空白处。于是借着准备从最经典、最基础知识学习的机会,开始由底向上,系统地整理以前所学。
一、计算机数学
大学时认真学习了编程相关的课程,但一些基础课程由于不知道跟编程是什么联系所以失去了兴趣、没有学好。工作了两年,才慢慢发觉大学时一些课程的用处:微积分(分析算法性能时一些求极限)、线性代数(矩阵在三维转换中的作用)、概率统计、编译原理(理解源代码是如何编译、链接,最后被操作系统载入执行的)。现在虽然没有大块时间(其实也没必要)重新复习这些课程,但计算机相关的数学知识基本可以包含在下面两本书中:
具体数学-计算机科学的基础
离散数学及其应用
可以认真学习这这两本书籍。如果阅读过程中发现不懂的,可以去翻阅大学课程中更基础的数学书。这样不会迷失在过多的理论书籍之中。推荐本微积分的课外读物,齐民友的《重温微积分》,看了一点感觉还不错。
二、C语言
学习过计算机的基础数学知识后,可以先不急于去看数据结构和算法。先静下心来,仔细学好C语言。相信对C语言更深入的学习,会加深之后对数据结构和算法知识的理解。
《C程序设计语言(第二版)》
《C专家编程》
《C和指针》
《C陷阱与缺陷》
用心做了《C程序设计语言》中的小习题,算是对C语言编程的热身了。这些书籍都很经典(据说 :),可以先仔细看一遍,以后再回过头来重读,相信到时会有更多的感悟。
三、数据结构与算法
掌握了数学基础知识,也学习了C语言,接下来当然就开始学习编程最核心的部分-数据结构和算法。《算法导论》和《计算机程序设计艺术》三卷当然很经典,但拜读之前可以先学习一些更加入门的教材。
《数据结构与算法分析 C语言版》
《数据结构 C语言版》(严蔚敏)
《C算法:基础、数据结构、排序和搜索》
要多做题,不仅有利于接下来计算机系统的学习,还对日后找工作、做笔试题大有裨益。尽管有些枯燥,但若能坚持下来,绝对能提高自己的分析设计能力。
四、计算机系统
又是有些理论的东西,其实也可以先跳过这部分,直接学习下面更为具体的技术。但要想进一步提高自己,早晚都要回过头来系统的学习这些知识。高手都是有很强的系统性知识嘛~
概述
《深入理解计算机系统》
汇编语言
《汇编语言(第二版)》
《80X86汇编语言程序设计教程》
操作系统
《操作系统概念》
《操作系统设计与实现》
数据库
《数据库系统导论》
《数据库系统概念》
编译原理
《编译原理技术与工具》
网络
《计算机网络》
《TCP/IP详解》
从大学到现在,只有两次感觉自己突破了瓶颈大幅度提高。一次是在一个项目中用了大量的T-SQL特性,解决了很多实际问题,因而对SQL有了更深的理解。另一次就是对汇编语言及操作系统知识的学习。一直使用着Java,对底层系统的知识还停留在大学课堂上。今年初,先读了《深入理解计算机系统》有了个大概了解,之后学习了汇编语言那两本书,紧接着就开始啃那本《自己动手实现操作系统》。虽然没有读完,但这一路走下来,两三个月内对汇编语言的基本语法、80X86 CPU结构、寻址方式、实模式和保护模式、进程信息块、页等等曾经课堂上的概念都有了实实在在的了解。也明白了那句话:真正的程序员是应该懂底层知识的。知道我们每天对着编程的机器是怎样运转的,才会明白对编程的本质。
五、具体平台开发
终于可以在具体平台上开始编程实践了。Windows平台有很多要学的,微软的东西更新换代很快(追的好累,让人又爱又恨),但一定要把握重点。Windows平台上主要学习图形界面程序的结构,界面是怎样布局的,消息是怎样传递的等等。还有一些比较经典的技术像MFC、COM等,也可以简单学习一下。
《Windows程序设计》
《MFC深入浅出》
《COM本质论》
《Windows核心编程》
重头戏是Unix/Linux平台的学习!Unix/Linux平台上有太多优秀的代码值得我们阅读学习。在开始学习Linux平台上编程前,可以先装个Ubuntu虚拟机对Linux有个简单了解。掌握基本的命令,会写简单的Makefile和Shell脚本,会用GCC和GDB编译调试C程序。
《Unix环境高级编程》
《Unix网络编程》
还有本个人认为不错的入门书《Linux C编程一站式学习》。
六、高级语言
最后才是Java和.Net的学习。有了前面的学习过程,此时再学习高级语言应该是轻车熟路了。可惜的是大学时却本末倒置了,学了C++和一些基础课程后都没怎么派上用场,之后就一直用Java开发,导致底层知识一点都不牢固真实遗憾~
高级语言的API、框架很多很多,当然书也很多了,就不推荐了。
N、系统内核
Linux内核源码很多,里面包含很多算法和设计,学习起来真的很难。但挑重点的学习了解一下,对提高编程水平还是有很大帮助的。比如操作系统怎样通过Loader加载,进程的切换和调度等。学习Linux四库全书:《Linux内核设计与分析》、《深入理解Linux内核》、《Linux内核源代码情景分析》、《Linux设备驱动开发》。再推荐一本《Orange’s 一个操作系统的实现》。最重要的一点:read the fucking source code!
N年寒窗苦读后,也许可以到达这个阶段,也许吧~
看到累了也别忘了放松一下,读一些小散文:
《代码之美》、《Java夜未眠》、《Unix编程艺术》、《疯狂的程序员》…
好了,列了这么长的学习计划勉励自己,希望对看到此贴的人会有些许帮助。

 

《C程序设计语言》第一章 导言


1.1 入门
与Windows平台下在Visual Studio中开发的不同。
尽管这个练习很简单,但对于初学语言的人来说,它仍然可能是一大障碍。
因为我们首先必须编写程序文本,然后成功地编译,并加载、运行。
掌握了这些操作细节以后,其他事情才会比较容易。
#include <stdio.h>
main()
{
     printf(“hello, world\n”);
}
练习1-2     做个试验,当printf函数的参数字符串中包含\c时,观察一下会出现什么情况。
答:printf(“hello, \cworld”);
     编译时警告:unknown escape sequence: ‘\c’。
     执行时打印hello, cworld。
1.2 变量与算术表达式
%d          按照十进制整型数打印
%6d        按照十进制整型数打印,至少6个字符宽
%f          按照浮点数打印
%6f         按照浮点数打印,至少6个字符宽
%.2f        按照浮点数打印,小数点后有两位小数
%6.2f       按照浮点数打印,至少6个字符宽,小数点后有两位小数
练习1-3和1-4     编写一个程序打印摄氏温度转换为相应华氏温度的转换表。
答:
#include <stdio.h>
main()
{
float fahr, celsius;
float lower, upper, step;

lower = 0;
upper = 50;
step = 10;

printf(“%s\t%s\n”, “Celsius”, “Fahr”);

celsius = lower;
while (celsius <= upper) {
fahr = celsius * 9.0 / 5.0 + 32.0;
printf(“%3.0f\t%6.1f\n”, celsius, fahr);
celsius = celsius + step;
}
}

1.3 for语句
main()
{
     int fahr;
     for (fahr = 0; fahr <= 300; fahr = fahr + 20)
          printf(“%3d %6.1f\n”, fahr, (5.0/9.0)*(fahr-32));
}
与while循环相比的主要改进:去掉了大部分变量而只使用一个int类型的变量。
     温度的上限、下限、步长都是常量,而计算摄氏温度的表达式变成了printf函数的第三个参数。
以上几点改进中的最后一点是C语言中一个通用规则:在允许使用某种类型变量值的任何场合,
都可以使用该类型的更复杂的表达式。
练习1-5     修改温度转换程序,要求以逆序(从300度到0度的顺序)打印温度转换表。
答:
#include <stdio.h>
main()
{
int fahr;
for (fahr = 300; fahr >= 0; fahr = fahr – 20)
printf(“%3d %6.1f\n”, fahr, (5.0/9.0)*(fahr-32));
}
1.4 符号常量
程序中使用300、20等幻数不是好习惯,可以通过
     #define 名字 替换文本
把符号名(或称为符号常量)定义为一个特定的字符串。
在该定义之后,程序中出现的所有在#define中定义的名字(既没有用引号引起来,
也不是其他名字的一部分)都将用相应的替换文本替换。替换文本可以是任何字符序列,
而不仅限于数字。
#define LOWER 0
#define UPPER 300
#define STEP 20
for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
1.5 字符输入/输出
标准库提供一次读/写一个字符的函数,最简单的是getchar和putchar两个函数。
文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。
文本复制
#include <stdio.h>
main()
{
     int c;
     c = getchar();
     while (c != EOF) {
          putchar(c);
          c = getchar();
     }
}
在声明变量c时,必须让它大到足以存放getchar函数返回的任何值。这里之所以不把c声明成char类型
是因为它必须足够大,除了能存储任何可能的字符外还要能存储文件结束符EOF。EOF定义在头文件
<stdio.h>中,是个整型数,其具体数值是什么并不重要,只要它与任何char类型的值都不相同即可。
对于经验比较丰富的C语言程序员,可以写的更精炼一些。在C语言中,类似于c = getchar()之类的赋值
操作是一个表达式,并且具有一个值,即赋值后左边变量保存的值。
int c;
while ((c = getchar()) != EOF)
     putchar(c);
这段程序使输入集中化,更紧凑。这种方式编写的程序更易阅读。
赋值表达式两边的圆括号不能省略。不等于运算法!=的优先级比赋值运算符=的优先级要高。
相当于:c = (getchar() != EOF)。c的值将被置为0或1(取决于是否碰到文件结束标志)。
练习1-6     验证表达式getchar() != EOF的值是0还是1。
答:
#include <stdio.h>
main()
{
printf(“%d\n”, getchar() != EOF);
}
结果是:1。(输入任意字符,都不等于EOF,表达式为真)
练习1-7     编写一个打印EOF值的程序。
答:
#include <stdio.h>
main()
{
printf(“%d\n”, EOF);
}
结果是:-1。(ASCII中没有值为-1的字符,所以变量c要用int型保存)
字符计数
练习1-8     编写一个统计空格、制表符与换行符个数的程序。
答:
#include <stdio.h>
main()
{
int space = 0, table = 0, enter = 0;
int c;
while ((c = getchar()) != EOF) {
if (c == ‘ ‘)
++space;
else if (c == ‘\t’)
++table;
else if (c == ‘\n’)
++enter;
}
printf(“space: %d, table: %d, enter: %d\n”, space, table, enter);
}
练习1-9     编写一个将输入复制到输出的程序,并将其中连续的多个空格用一个空格代替。
答:
#include <stdio.h>
main()
{
int inspace = 0;
int c;
while ((c = getchar()) != EOF) {
if (c != ‘ ‘) {
if (inspace) {
putchar(‘ ‘);
inspace = 0;
}
putchar(c);
} else if (!inspace)
inspace = 1;
else
;     // discard space
}
putchar(‘\n’);
}
练习1-10    编写一个将输入复制到输出的程序,并将其中的制表符替换为\t,把回退符替换
为\b,把反斜杠替换为\\。这样可以将制表符和回退符以可见的方式显示出来。
答:
#include <stdio.h>
main()
{
int c;
while ((c = getchar()) != EOF) {
if (c == ‘\t’) {
putchar(‘\\’);
putchar(‘t’);
}
else if (c == ‘\n’) {
putchar(‘\\’);
putchar(‘b’);
}
else if (c == ‘\\’) {
putchar(‘\\’);
putchar(‘\\’);
}
else
putchar(c);
}
putchar(‘\n’);
}
单词统计
命令wc的主要部分。
练习1-11     如何测试单词计数程序?如果程序中存在某种错误,那么什么样的输入最可能发现这类错误?
练习1-12     编写一个程序,以每行一个单词的形式打印其输入。
答:
#include <stdio.h>
main()
{
int c;
while ((c = getchar()) != EOF) {
if (c == ‘ ‘ || c == ‘\n’ || c == ‘\t’)
putchar(‘\n’);
else
putchar(c);
}
putchar(‘\n’);
}
1.6 数组
数组下标可以是任何整型表达式,包括整型变量和整型常量。
if (c>= ’0′ && c <= ’9′) 用于判断c中的字符是否为数字。
如果它是数字,那么该数字对应的数值是:c – ’0′
练习1-14     编写一个程序,打印输入中各个字符出现频度的直方图。
答:
#include <stdio.h>
main()
{
int freq[26];
int x;
for (x = 0; x < 26; ++x)
freq[x] = 0;

int c;
while ((c = getchar()) != EOF) {
if (c >= ‘a’ && c <= ‘z’)
++freq[c - 'a'];
}

int i, j;
for (i = 0; i < 26; ++i) {
printf(“%c:\t”, ‘a’ + i);
for (j = 0; j < freq[i]; ++j)
putchar(‘*’);
putchar(‘\n’);
}
}

1.7 函数
函数原型与函数声明中参数名不要求相同。事实上,函数原型中的参数名是可选的。
int power(int m, int n);
int power(int base, int n)
{
     int i, p;
     p = 1;
     for (i = 1; i <= n; ++i)
          p = p * base;
     return p;
}
ANSI C同较早版本C语言之间的最大区别在于函数的声明与定义的不同。
power(base, n)
int base, n;
{
     …
}
参数名在圆括号内制定,参数类型在左花括号之前声明。函数体与ANSI C中形式相同。
函数不一定有返回值。不带表达式的return语句
1.8 参数–传值调用
在C语言中,所有函数参数都是通过值传递的。也就是说:
传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。
被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。
传值调用的利大于弊。参数可看做是初始化好的局部变量,因此额外使用的变量更少。
这样程序可以更紧凑简洁。
int power(int base, int n)
{
     int p;
     for (p = 1; n > 0; –n)
          p = p * base;
     return p;
}
必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用
函数提供变量的地址(就是指向变量的指针)。
1.9 字符数组
int getline(char s[], int lim)
{
int c, i;
for (i=0; i < lim-1 && (c=getchar())!=EOF && c!=’\n’; ++i)
s[i] = c;
if (c == ‘\n’) {
s[i] = c;
++i;
}
s[i] = ‘\0′;
return i;
}
getline函数把字符’\0′插入到数组末尾,以标记字符串的结束。这一约定已被C语言采用。
printf函数中的格式规范%s规定,对应的参数必须是以这种形式表示的字符串。
copy函数的实现正是依赖于输入参数由’\0′结束这一事实。
值得一提的是,即使是这样很小的程序,在传递参数时也会遇到一些麻烦的设计问题。
getline函数无论是否到达换行符,当数组满时它将停止读字符。
main函数可以通过测试行的长度以及检查返回的最后一个字符来判断当前行是否太长。
练习1-16     修改打印最长文本行程序的main函数,使之打印任意长度的输入行长度。
练习1-17     编写一个程序,打印长度大于80个字符的所有输入行。
答:
#include <stdio.h>
#define MAXLINE 1000

int getline2(char line[], int maxline);     //与stdio.h中的getline冲突

main()
{
int len;
char line[MAXLINE];

while ((len = getline2(line, MAXLINE)) > 0)
if (len > 80) {
printf(“%s\n”, line);
}

return 0;
}

练习1-18     编写一个程序,删除每个输入行末尾的空格及制表符,并删除完全是空格的行。
答:(不好确定何时字符结束开始空格,方法是从后向前找,直到最后一个字符)
int remove(char s[])
{
int i = 0;
while (s[i] != ‘\n’)
++i;
–i;     // back off from ‘\n’

// locate last letter
while (i >= 0 && (s[i] == ‘ ‘ || s[i] == ‘\t’))
–i;

if (i >= 0) {
++i;
s[i] = ‘\n’;
++i;
s[i] = ‘\0′;     // terminate the string
}
return i;
}

练习1-19     编写函数reverse(s),将字符串中的字符顺序颠倒过来。
答:
#include <stdio.h>
void reverse(char s[])
{
int i, j;

j = 0;
while (s[j] != ‘\0′)
++j;

char temp;
for (i=0,j=j-1; i<j; ++i,–j) {
temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}

main()
{
char s[] = “abcdefg”;
reverse(s);
printf(“%s\n”, s);
char s2[] = “123456″;
reverse(s2);
printf(“%s\n”, s2);
return 0;
}

1.10 外部变量与作用域
函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失。所以称为自动变量。
可以定义位于所有函数外部的变量,称为外部变量。
在每个需要访问外部变量的函数中,必须声明相应的外部变量,说明其类型。
声明时可以用extern语句显示声明,也可以通过上下文隐式声明。
在源文件中,如果外部变量的定义出现在使用它的函数之前,那么就没有必要使用extern声明。
通常的做法是,所有外部变量的定义都放在源代码的开始处,这样就可以省略extern声明。
如果程序包含在多个源文件中,就需要使用extern声明来建立该变量与其定义之间的联系。
通常把变量和函数的extern声明放在一个单独的文件中(习惯上称为头文件)。
练习1-20     编写程序detab,将输入中的制表符替换成适当数目的空格,使空格充满到下一个制表符终止位的地方。

 

用SSH连接VirtualBox中的Ubuntu

1. 设置虚拟机网络连接方式为桥接模式,共享本机的网卡。
2. 关闭本机操作系统(Win7或者XP)的防火墙。
3. 设置本机和虚拟机IP到同一网段,或者都设置成DHCP自动获取,让路由器去分配。
     在本机和虚拟机中进行ping测试,看网络是否互通。
     并查询各自IP,确认在同一网段下。(Windows下ipconfig -all命令,Ubuntu下ifconfig命令)
     在我的机器上,本机IP是192.168.1.100,虚拟机是192.168.1.102,都是由路由器自动分配。
4. 启动Ubuntu虚拟机,安装SSH服务器端OpenSSH。
安装ssh-server:sudo apt-get install openssh-server
启动ssh-server:sudo /etc/init.d/ssh restart
确认ssh-server已经正常工作: netstat -tlp
5. 在本机安装SSH客户端,如SSH Secure Shell客户端。
6. 在SSH客户端中设置虚拟机IP和Ubuntu的用户名,端口默认为22。
     第一次连接会提示保存公钥,输入用户名的密码后连接成功!

《Windows程序设计》第三章 窗口和消息


总体结构
所谓「Windows给程序发送消息」,是指Windows呼叫程序中的一个函数,该函数的参数描述了这个特定消息。这种位于Windows程序中的函数称为「窗口消息处理程序」。程序建立的每一个窗口都有相关的窗口消息处理程序。这个窗口消息处理程序是一个函数,既可以在程序中,也可以在动态链接库中。Windows通过呼叫窗口消息处理程序来给窗口发送消息。窗口消息处理程序根据此消息进行处理,然后将控制传回给Windows。

HELLOWIN.C源代码

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
     PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT (“HelloWin”) ;
     HWND hwnd ;
     MSG msg ;
     WNDCLASS wndclass ;
     wndclass.style = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc = WndProc ;
     wndclass.cbClsExtra = 0 ;
     wndclass.cbWndExtra = 0 ;
     wndclass.hInstance = hInstance ;
     wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName = NULL ;
     wndclass.lpszClassName = szAppName ;
     if (!RegisterClass (&wndclass))
     {
          MessageBox ( NULL, TEXT (“This program requires Windows NT!”),
               szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     hwnd = CreateWindow( szAppName, // window class name
                                             TEXT (“The Hello Program”), // window caption
                                             WS_OVERLAPPEDWINDOW, // window style
                                             CW_USEDEFAULT, // initial x position
                                             CW_USEDEFAULT, // initial y position
                                             CW_USEDEFAULT, // initial x size
                                             CW_USEDEFAULT, // initial y size
                                             NULL, // parent window handle
                                             NULL, // window menu handle
                                             hInstance, // program instance handle
                                             NULL) ; // creation parameters
      ShowWindow (hwnd, iCmdShow) ;
      UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     HDC hdc ;
     PAINTSTRUCT ps ;
     RECT rect ;
     switch (message)
     {
          case WM_CREATE:
               PlaySound (TEXT (“hellowin.wav”), NULL, SND_FILENAME | SND_ASYNC) ;
               return 0 ;
          case WM_PAINT:
               hdc = BeginPaint (hwnd, &ps) ;
               GetClientRect (hwnd, &rect) ;
               DrawText (hdc, TEXT (“Hello, Windows 98!”), -1, &rect,
                    DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
               EndPaint (hwnd, &ps) ;
               return 0 ;
          case WM_DESTROY:
               PostQuitMessage (0) ;
               return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

全盘考量

1. Windows函数调用
  • GetStockObject 取得一个图形对象(在这个例子中,是取得绘制窗口背景的画刷对象)。 
  • RegisterClass 为程序窗口注册窗口类别。 
  • CreateWindow 根据窗口类别建立一个窗口。 
  • ShowWindow 在屏幕上显示窗口。 
  • UpdateWindow 指示窗口自我更新。 
  • GetMessage 从消息队列中取得消息。 
  • TranslateMessage 转译某些键盘消息。 
  • DispatchMessage 将消息发送给窗口消息处理程序。  
  • BeginPaint 开始绘制窗口。 
  • GetClientRect 取得窗口显示区域的大小。 
  • DrawText 显示字符串。 
  • EndPaint 结束绘制窗口。 
  • PostQuitMessage 在消息队列中插入一个「退出程序」消息。 
  • DefWindowProc 执行内定的消息处理。
2. 大写字母标识符

这些识别字是在Windows表头档案中定义的。有些识别字含有两个字母或者三个字母的字首,
这些字首後头接著一个底线。

CS_HREDRAW, CS_VERDRAW:CS-窗口类别样式
CW_USEDEFAULT:CW-建立窗口
DT_CENTER, DT_SINGLELINE, DT_VCENTER:DT-绘制文字
IDC_ARROW:IDC-游标ID
IDI_APPLICATION:IDI-图示ID
MB_ICONERROR:MB-消息框
SND_ASYNC, SND_FILENAME:SND-声音
WM_CREATE, WM_DESTORY, WM_PAINT:WM-窗口消息
WS_OVERLAPPEDWINDOW:WS-窗口样式

3. 新的数据形态
有时这些新的数据型态只是为了方便缩写。例如,用于WndProc的第二个参数的UINT数据型态只是一个unsigned int (无正负号整数),在Windows 98中,这是一个32位的值。用于WinMain的第三个参数的PSTR数据型态是指向一个字符串的指针,即是一个char *。

WndProc函数传回一个型态为LRESULT的值,该值简单地被定义为一个LONG。WinMain函数被指定了一个WINAPI型态(在表头文件中定义的所有Windows函数都被指定这种型态),而WndProc函数被指定一个CALLBACK型态。这两个标识符都被定义为_stdcall,表示在Windows本身和使用者的应用程序之间发生的函数呼叫的呼叫参数传递方式。

HELLOWIN还使用了Windows表头文件中定义的四种数据结构:MSG-消息结构、WNDCLASS-窗口类别结构、PAINTSTRUCT-绘图结构、RECT-矩形结构。
4. 句柄
最后,还有三个大写标识符,用于不同型态的「句柄」:HINSTANCE-执行实体(程序自身)句柄、HWND-窗口句柄、HDC-设备内容句柄。
句柄在Windows中使用非常频繁。句柄是一个(通常为32位的)整数,它代表一个对象。Windows中的句柄类似传统C或者MS-DOS程序设计中使用的文件句柄。程序几乎总是通过呼叫Windows函数取得句柄。程序在其它Windows函数中使用这个句柄,以使用它代表的对象。代号的实际值对程序来说是无关紧要的。但是,向您的程序提供代号的Windows模块知道如何利用它来使用相对应的对象。
5. 匈牙利表示法
许多Windows程序写作者使用一种叫做「匈牙利表示法」的变量命名通则。这是为了纪念传奇性的Microsoft程序写作者Charles Simonyi。非常简单,变量名以一个或者多个小写字母开始,这些字母表示变量的数据型态。例如,szCmdLine中的sz代表「以0结尾的字符串」。在hInstance和hPrevInstance中的h前缀表示「句柄」;在iCmdShow中的i前缀表示「整数」。
在命名结构变量时,可以用结构名(或者结构名的一种缩写)的小写作为变量名的前缀,或者用作整个变量名。例如,在HELLOWIN. C的WinMain函数中,msg变量是MSG型态的结构;wndclass是WNDCLASSEX型态的一个结构。
由于变量名既描述了变量的作用,又描述了其数据型态,就比较容易避免产生数据型态不合的错误。
6. 注册窗口类别
窗口依照某一窗口类别建立,窗口类别用以标识处理窗口消息的窗口消息处理程序。在为程序建立窗口之前,必须首先呼叫RegisterClass注册一个窗口类别。该函数只需要一个参数,即一个指向型态为WNDCLASS的结构指针。
WINUSER.H定义了WNDCLASSA和WNDCLASSW结构(以及指向结构的指针)以后,表头文件依据对UNICODE标识符的解释,定义了WNDCLASS和指向WNDCLASS的指标。
在WNDCLASS结构中最重要的两个字段是第二个和最后一个,第二个字段(lpfnWndProc) 是依据这个类别来建立的所有窗口所使用的窗口消息处理程序的地址。在HELLOWIN.C中,这个是WndProc函数。最后一个字段是窗口类别的文字名称。程序写作者可以随意定义其名称。在只建立一个窗口的程序中,窗口类别名称通常设定为程序名称。
如果用定义的UNICODE标识符编译了程序,程序将呼叫RegisterClassW。该程序可以在Microsoft Windows NT中执行良好。但如果此程序在Windows 98上执行,RegisterClassW函数并未真地被执行到。函数有一个进入点,但函数呼叫后只传回0,表明错误。
GetLastError函数会帮助您确定在这样的情况下产生错误的原因。GetLastError是Windows中常用的函数,它可以在函数呼叫失败时获得更多错误信息。在Windows 98中呼叫RegisterClassW时,GetLastError将传回120。在WINERROR.H中您可以看到,值120与标识符ERROR_CALL_NOT_IMPLEMENTED相等。
最后,一个老经验是:
     if (!hPrevInstance)
     {
          create and init wndclass;
          RegisterClass(&wndclass);
     }
这是出于「旧习难改」的原因。在16位的Windows中,如果您启动正在执行的程序的一个新执行实体,WinMain的hPrevInstance参数将是前一个执行实体的执行实体句柄。为节省内存,两个或多个执行实体就可能会共享相同的窗口类别。在32位的Windows中,hPrevInstance总是NULL。此程序代码会正常执行,而实际上也没必要检查hPrevInstance。
7. 建立窗口
Windows程序设计新手有时会混淆窗口类别和窗口之间的区别,以及为什么一个窗口的所有特征不能被一次设定好。由窗口类别来负责处理按钮的键盘和鼠标输入,并定义按钮在屏幕上的外观形象。从这一点看来,所有的按钮都是以同样的方式工作的。但是并非所有的按钮都是一样的。它们可以有不同的大小,不同的屏幕位置,以及不同的字符串。后面的这样一些特征是窗口定义的一部分,而不是窗口类别定义的。传递给RegisterClass函数的信息会在一个数据结构中设定好,而传递给CreateWindow函数的信息会在函数单独的参数中设定好。
CreateWindow传回被建立的窗口的句柄,该句柄存放在变量hwnd中,后者被定义为HWND型态(「窗口句柄型态」)。Windows中的每个窗口都有一个句柄,程序用句柄来使用窗口。许多Windows函数需要使用hwnd作为参数,这样,Windows才能知道函数是针对哪个窗口的。如果一个程序建立了许多窗口,则每个窗口均有一个句柄。窗口句柄是Windows程序所处理最重要的句柄之一。
8. 显示窗口
在CreateWindow呼叫传回之后,Windows内部已经建立了这个窗口。这就是说,Windows已经配置了一块内存,用来保存在CreateWindow呼叫中指定窗口的全部信息跟一些其它信息,而Windows稍后就是依据窗口句柄找到这些信息的。
     ShowWindow(hwnd, iCmdShow):第一个参数是刚刚用CreateWindow建立的窗口句柄。第二个参数是作为参数传给WinMain的iCmdShow。它确定最初如何在屏幕上显示窗口,是一般大小、最小化还是最大化。
     UpdateWindow(hwnd):重画显示区域。它经由发送给窗口消息处理程序(即HELLOWIN.C中的WndProc函数)一个WM_PAINT消息做到这一点。
9. 消息循环
呼叫UpdateWindow之后,窗口就出现在视讯显示器上。程序现在必须准备读入使用者用键盘和鼠标输入的数据。Windows为当前执行的每个Windows程序维护一个「消息队列」。在发生输入事件之后,Windows将事件转换为一个「消息」并将消息放入程序的消息队列中。程序通过执行一块称之为「消息循环」的程序代码从消息队列中取出消息:
while (GetMessage(&msg, NULL, 0, 0))
{
     TranslateMessage(&msg);
     DispatchMessage(&msg);
}
     GetMessage(&msg, NULL, 0, 0):传给Windows一个指标,指向名为msg的MSG结构。第二、第三和第四个参数设定为NULL或者0,表示程序接收它自己建立的所有窗口的所有消息。只要从消息队列中取出消息的message字段不为WM_QUIT(其值为0×0012),GetMessage就传回一个非零值。
     TranslateMessage(&msg):将msg结构传给Windows,进行一些键盘转换。(关于这一点将在第六章中深入讨论。)
     DispatchMessage(&msg):又将msg结构回传给Windows。然后,Windows将该消息发送给适当的窗口消息处理程序,让它进行处理。这也就是说,Windows将呼叫窗口消息处理程序。处理完消息之后,WndProc传回到Windows。此时,Windows还停留在DispatchMessage呼叫中。在结束DispatchMessage呼叫的处理之后,Windows回到HELLOWIN,并且接着从下一个GetMessage呼叫开始消息循环。
10. 窗口消息处理程序
一个Windows程序可以包含多个窗口消息处理程序。一个窗口消息处理程序总是与呼叫RegisterClass注册的特定窗口类别相关联。CreateWindow函数根据特定窗口类别建立一个窗口。但依据一个窗口类别,可以建立多个窗口。
     LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)
窗口消息处理程序的四个参数与MSG结构的前四个字段是相同的。第一个参数hwnd是接收消息的窗口的句柄,它与CreateWindow函数的传回值相同。第二个参数与MSG结构中的message字段相同,它是标识消息的数值。最后两个参数都是32位的消息参数,提供关于消息的更多信息。
程序通常不直接呼叫窗口消息处理程序,窗口消息处理程序通常由Windows本身呼叫。通过呼叫SendMessage函数,程序能够直接呼叫它自己的窗口消息处理程序。
窗口消息处理程序在处理消息时,必须传回0。窗口消息处理程序不予处理的所有消息应该被传给名为DefWindowProc的Windows函数。
WM_CREATE消息
     当Windows在WinMain中处理CreateWindow函数时,WndProc接收这个消息。就是说,在HELLOWIN呼叫CreateWindow时,Windows呼叫WndProc,将第一个参数设定为窗口句柄,第二个参数设定为WM_CREATE。WndProc处理WM_CREATE消息并将控制传回给Windows。 Windows然后可以从CreateWindow呼叫中传回到HELLOWIN中,继续在WinMain中进行下一步的处理。
WM_PAINT消息
当窗口显示区域的一部分显示内容或者全部变为「无效」,以致于必须「更新画面」时,将由这个消息通知程序。
窗口无效的情况:
     在最初建立窗口的时候,整个显示区域都是无效的,因为程序还没有在窗口上画什么东西。
     在使用者改变HELLOWIN窗口的大小后,显示区域的显示内容重新变得无效。HELLOWIN中wndclass结构的style字段设定为标志CS_HREDRAW和CS_VREDRAW,这样的格式设定指示Windows,在窗口大小改变后,就把整个窗口显示内容当成无效。
     当使用者将HELLOWIN最小化,然后再次将窗口恢复为以前的大小时,Windows将不会保存显示区域的内容。在图形环境下,窗口显示区域涉及的数据量很大。因此,Windows令窗口无效。
     在移动窗口以致其相互重迭时,Windows不保存一个窗口中被另一个窗口所遮盖的内容。在这一部分不再被遮盖之后,它就被标志为无效。
对WM_PAINT的处理几乎总是从一个BeginPaint呼叫开始,而以一个EndPaint呼叫结束。
在BeginPaint呼叫中,如果显示区域的背景还未被删除,则由Windows来删除。它使用注册窗口类别的WNDCLASS结构的hbrBackground字段中指定的画刷来删除背景。BeginPaint呼叫令整个显示区域有效,并传回一个「设备内容句柄」。设备内容是指实体输出设备(如视讯显示器)及其设备驱动程序。
WM_DESTROY消息
这一个消息指示,Windows正在根据使用者的指示关闭窗口。该消息是使用者单击Close按钮或者在程序的系统菜单上选择 Close时发生的。PostQuitMessage(0)在程序的消息队列中插入一个WM_QUIT消息。前面提到过,GetMessage对于除了WM_QUIT之外的从消息队列中取出的所有消息都传回非0值。而当GetMessage得到一个WM_QUIT消息时,它传回0。这将导致WinMain退出消息循环,并终止程序。
Windows程序设计的难点
Windows程序所作的一切,都是响应发送给窗口消息处理程序的消息。这是概念上的主要难点之一,在开始写作Windows程序之前,必须先搞清楚。实际上,从程序外呼叫程序内的例程这一种做法,在传统的程序设计中并非前所未闻。C中的signal函数可以拦截Ctrl-C中断或操作系统的其它中断。为MS-DOS编写的老程序中经常有拦截硬件中断的程序代码。但在Windows中,这种概念扩展为包括一切事件。窗口中发生的一切都以消息的形式传给窗口消息处理程序。
消息能够被分为「队列化的」和「非队列化的」。队列化的消息是由Windows放入程序消息队列中的。在程序的消息循环中,重新传回并分配给窗口消息处理程序。非队列化的消息在Windows呼叫窗口时直接送给窗口消息处理程序。任何情况下,窗口消息处理程序都将获得窗口所有的消息–包括队列化的和非队列化的。
队列化消息基本上是使用者输入的结果,以击键(如WM_KEYDOWN和WM_KEYUP消息)、击键产生的字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)和鼠标按钮(WM_LBUTTONDOWN)的形式给出。队列化消息还包含时钟消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。
非队列化消息则是其它消息。在许多情况下,非队列化消息来自呼叫特定的Windows函数。例如,当WinMain呼叫CreateWindow时,Windows将建立窗口并在处理中给窗口消息处理程序发送一个WM_CREATE消息。当WinMain呼叫ShowWindow时,Windows将给窗口消息处理程序发送WM_SIZE和WM_SHOWWINDOW消息。当WinMain呼叫UpdateWindow时,Windows将给窗口消息处理程序发送WM_PAINT消息。

 

《Windows程序设计》第二章 Unicode简介

字符集简史

先天即被ANSI 束缚的C程式设计语言通过对宽字元集的支援来支援Unicode
开发ASCII 的过程中,在字元长度是6 位元、7 位元还是8 位元的问题上产生了很大的争议。从可靠性
的观点来看不应使用替换字元,因此ASCII 不能是6 位元编码,但由於费用的原因也排除了8 位元版本的方案(当时每位元的储存空间成本仍很昂贵)。这样,最终的字元码就有26 个小写字母、26 个大写字母、10 个数字、32 个符号、
33 个代号和一个空格,总共128 个字元码。

ASCII 有许多优点。例如,26 个字母代码是连续的(在EBCDIC 代码中就不
是这样的);大写字母和小写字母可通过改变一位元资料而相互转化;10 个数
位的代码可从数值本身方便地得到(在BCDIC代码中,字元「0」的编码在字元
9」的後面!)

因为遵循了ANSI 草案和ISO 标准,纯
Windows字元集被称作「ANSI字元集」。 ANSI草案和ISO标准最终成为ANSI/ISO
8859-1-1987
,通常也简写为「Latin 1」。

MS-DOS 3.319874 月发行)向IBM PC 用户引进了内码表code page
的概念,Windows也使用此概念。

白Unicode DBCS 之间的区别很重要。Unicode 使用(特别在C 程式设
计语言环境里)「宽字元集」。「Unicode 中的每个字元都是16 位元宽而不是
8 位元宽。」在Unicode 中,没有单单使用8 位元数值的意义存在。相比之下,
在双位元组字元集中我们仍然处理8 位元数值。
宽字符和C
Unicode或者宽字符都没有改变char数据型态在C中的含义。char继续表示1个字节的储存空间,sizeof (char)继续返回1。
C中的宽字符基于wchar_t数据型态,wchar_t数据型态与无符号短整数型态相同,都是16位宽。
wchar_t c = ‘A’;
wchar_t c = L’A';     // 通常这是不必要的,C编译器会对该字符进行扩充,使它成为宽字符
wchar_t * p = L”Hello!”;
注意紧接在第一个引号前面的大写字母L(代表「long」)。这将告诉编译器该字符串按宽字符保存-即每个字符占用2个字节。通常,指针变量p要占用4个字节,而字符串变量需要14个字节-每个字符需要2个字节,末尾的0还需要2个字节。
宽字符和链接库函数
wchar_t * p = L”Hello!”;
iLength = strlen(pw);
首先,C编译器会显示一条警告消息:incompatible types – from ‘unsigned short *’ to ‘const char *’
仍然可编译并执行该程序,但您会发现iLength等于1。
字符串「Hello!」中的6个字符占用16位:
0×0048 0×0065 0x006C 0x006C 0x006F 0×0021
Intel处理器在内存中将其存为:
48 00 65 00 6C 00 6C 00 6F 00 21 00
假定strlen函数正试图得到一个字符串的长度,并把第1个字节作为字符开始计数,但接着假定如果下一个字节是0,则表示字符串结束。
事实上并不是每个C语言链接库函数都需要重写,只是那些有字符串参数的函数才需要重写,而且也不用由您来完成。它们已经重写完了。strlen函数的宽字符版是wcslen。
宽字符和Windows

Windows NT从底层支援Unicode。这意味着Windows NT内部使用由16位字符组成的字符串。因为世界上其它许多地方还不使用16位字符串,所以Windows NT必须经常将字符串在操作系统内转换。Windows NT可执行为ASCII、Unicode或者ASCII和Unicode混合编写的程序。即,Windows NT支持不同的API函数呼叫,这些函数接受8位或16位的字符串。相对于Windows NT,Windows 98对Unicode的支持要少得多。只有很少的Windows 98函数呼叫支持宽字符串(包括MessageBox)。
一个Windows程序包括表头文件WINDOWS.H。该文件包括许多其它表头文件,包括WINDEF.H,该文件中有许多在Windows中使用的基本型态定义,而且它本身也包括WINNT.H。WINNT.H处理基本的Unicode支持。WINNT.H的前面包含C的表头文件CTYPE.H,这是C的众多表头文件之一,包括wchar_t的定义。WINNT.H定义了新的数据型态,称作CHAR和WCHAR。当您需要定义8位字符或者16位字符时,推荐您在Windows程序中使用的数据型态是CHAR和WCHAR。
typedef char CHAR;
typedef wchar_t WCHAR;     // wc, 注释是匈牙利标记法的建议

前缀N和L表示「near」和「long」,指的是16位Windows中两种大小不同的指标。在Win32中near和long指标没有区别。WINNT.H表头文件进而定义了可用做8位字符串指针的六种数据型态和四个可用做const 8位字符串指针的数据型态。


typedef WCHAR * PWCHAR, * LPWCH, * PWCH, * NWPSTR, * LPWSTR, * PWSTR ;  

typedef CONST WCHAR * LPCWCH, * PCWCH, * LPCWSTR, * PCWSTR ;

#ifdef UNICODE
typedef WCHAR TCHAR, * PTCHAR ;
#else
typedef char TCHAR, * PTCHAR ;  
#endif

#define __TEXT(quote) L##quote
#define __TEXT(quote) quote  
#define TEXT(quote) __TEXT(quote)
这些定义可使您在同一程序中混合使用ASCII和Unicode字符串,或者编写一个可被ASCII或Unicode编译的程序。如果您希望明确定义8位字符变量和字符串,请使用CHAR、PCHAR(或者其它),以及带引号的字符串。
为明确地使用16位字符变量和字符串,请使用WCHAR、PWCHAR,并将L添加到引号前面。
对于是8位还是16位取决于UNICODE标识符的定义的变量或字符串,要使用TCHAR、PTCHAR和TEXT宏。


Windows函数调用
从Windows 1.0到Windows 3.1的16位Windows中,MessageBox函数位于动态链接库USER.EXE。
     int WINAPI MessageBox(HWND, LPCSTR, UINT);
实际上,有两个进入点,一个名为MessageBoxA(ASCII版),另一个名为MessageBoxW(宽字符版)。用字符串作参数的每个Win32函数都在操作系统中有两个进入点!
在Windows程序中不能使用printf,Windows对标准输入和标准输出没有概念。仍然可以使用sprintf及sprintf系列中的其它函数来显示文字。这些函数除了将内容格式化输出到函数第一个参数所提供的字符串缓冲区以外,其功能与printfI相同。然后便可对该字符串进行操作(例如将其传给MessageBox)。
     char szBuffer[100];
     sprintf(szBuffer, “The sum of %i and %i is %i”, 5, 3, 5+3);
     puts(szBuffer);


《Windows程序设计》第一章 起步


这些程
式使用C 语言撰写并原原本本的使用Windows API 来开发程式。我将这种方法称作「古典」Windows 程式设计。这是我们在1985 年为Windows 1.0 写程式的方法,它今天仍是写作Windows 程式的有效方法。

一般而言,Windows API Windows 1.0 以来一直保持一致,没什么重大改变。

Windows API和它的语法的最大变化来自於从16位元架构向32位元架构转
化的过程中。

使用C 语言和原始的API 不是编写Windows 98程式的唯一方法。然而,这
种方法却提供给您最佳的性能、最强大的功能和在发掘Windows 特性方面最大
的灵活性。可执行档案相对较小且运行时不要求外部程式库(自然,Windows DLL
自身除外)。

虽然我认为学习古典的Windows 程式设计对任何Windows 程式写作者都是
重要的,我没有必要建议使用C API 编写每个Windows 应用程式。许多程式
写作者,特别是那些为公司内部开发程式或在家编写娱乐程式的程式写作者喜
欢轻松的开发环境,例如Microsoft Visual Basic 或者Borland Delphi(它结
合了物件导向的Pascal 版本)。

在专业程式写作者中——特别是那些开发商业应用程式的程式写作者——
Microsoft Visual C++Microsoft Foundation Class LibraryMFC)是近年
来流行的选择。MFC在一组C++物件类别中封装了许多Windows程式设计中的琐
碎细节。

最近,Internet World Wide Web 的流行大力推广著Sun Microsystems
Java,这是一个受C++启发却与微处理器无关的程式设计语言,而且结合了
可在几个作业系统平台上执行的图形应用程式开发工具组。

在原始的Windows API 之上的任何软体层都必定将您限制在全部功能的一
个子集内。您也许发现,例如,使用Visual Basic 编写应用程式非常理想,然
而它不允许您做一个或两个很简单的基本工作。在这种情况下,您将不得不使
用原始的API 呼叫。API 定义了作为Windows程式写作者所需的一切。没有什么
方法比直接使用API 更万能的了。

MFC 尤其问题百出。虽然它大幅简化了某些工作(例如OLE),我却经常发
现要让它们按我所想的去工作时,会在其他特性(例如Document/View 架构)
上碰壁。MFC 还不是Windows 程式设计者所追求的灵丹妙药。

编写第一个Windows程序

#include <stdio.h>
int main()
{
     printf(“Hello, world\n”);
     return 0;
}
同样效果的Windows程序:

#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance,
     HINSTANCE hPrevInstance,
     PSTR szCmdLine,
     int iCmdShow)
{
     MessageBox(NULL, TEXT(“Hello, Windows 98!”), TEXT(“HelloMsg”), 0);
     return 0;
}

创建过程:打开VS,新建VC++下的Win32 Application。在建好的解决方案中的源代码文件夹下添加C++文件,取名为HelloMsg.c。拷贝以上代码并运行,得到提示框:Hello, Windows 98。

表头档案

WINDOWS.H 是主要的含入档案,它包含了其他Windows表头档案,这些表头
档案的某些也包含了其他表头档案。这些表头档案中最重要的和最基本的是:
WINDEF.H 基本型态定义。
WINNT.H 支援Unicode 的型态定义。
WINBASE.H Kernel 函式。
WINUSER.H 使用者介面函式。
WINGDI.H 图形装置介面函式。

程序进入点

正如在C 程式中的进入点是函数main 一样,Windows 程式的进入点是
WinMain

编译、连结和执行

在编译阶段,编译器从C 原始码档案产生一个.OBJ(目标)
档案。在连结阶段,连结程式结合.OBJ 档案和.LIB(库)档案以建立.EXE(可
执行)档案。

KERNEL32.LIB
USER32.LIB GDI32.LIB。这些是三个主要Windows 子系统的「引用程式库」。
它们包含了动态连结程式库的名称以及放进.EXE 档案的引用资讯。Windows 使
用该资讯处理程式对KERNEL32.DLLUSER32.DLLGDI32.DLL 动态连结程式库
中函数的呼叫。

Vmware 8下Mac OS Lion安装

1. 从Verycd下载压缩包[Mac.OSX操作系统].Mac.OS.X.Lion.10.7.2.Vmware.Workstation.8.With.iReSign.zip

2. 按照里面的Readme.txt的说明,首先安装Vmware 8,已附上序列号。

3. 重启机器并在进程管理器里停掉所有Vmware服务,大概五六个。

4. 拷贝压缩包里的补丁文件夹到C盘(如果Vmware装在C盘的话)。

5. 已管理员权限运行CMD.exe,并执行补丁中的install.cmd。

6. 重启机器后打开Vmware,并导入Mac OS虚拟机。

《Linux C一站式编程》第八章 数组


1. 数组的基本概念
数组(Array)也是一种复合数据类型,它由一系列相同类型的元素(Element)组成。
int count[4];
和结构体成员类似,数组count的4个元素的存储空间也是相邻的。结构体成员可以是基本数据类型,也可以是复合数据类型,数组中的元素也是如此。根据组合规则,我们可以定义一个由4个结构体元素组成的数组:
struct complex_struct {
     double x, y;
} a[4];
struct {
     double x, y;
     int count[4];
} s;
使用数组下标不能超出数组的长度范围,这一点在使用变量做数组下标时尤其要注意。C编译器并不检查count[-1]或是count[100]这样的访问越界错误,编译时能顺利通过,所以属于运行时错误。但有时候这种错误很隐蔽,发生访问越界时程序可能并不会立即崩溃,而执行到后面某个正确的语句时却有可能突然崩溃(在第 4 节 “段错误”我们会看到这样的例子)。
C语言的设计精神是:相信每个C程序员都是高手,不要阻止程序员去干他们需要干的事,高手们使用count[-1]这种技巧其实并不少见,不应该当作错误。)
数组也可以像结构体一样初始化,未赋初值的元素也是用0来初始化,例如:

int count[4] = { 3, 2, };
数组和结构体虽然有很多相似之处,但也有一个显著的不同:数组不能相互赋值或初始化。例如这样是错的:
int a[5] = { 4, 3, 2, 1 };
int b[5] = a;
既然不能相互赋值,也就不能用数组类型作为函数的参数或返回值
void foo(int a[5])
{
     …
}
int array[5] = { 0 };
foo(array);
编译器也不会报错,但这样写并不是传一个数组类型参数的意思。对于数组类型有一条特殊规则:数组类型做右值使用时,自动转换成指向数组首元素的指针。所以上面的函数调用其实是传一个指针类型的参数,而不是数组类型的参数。这也解释了为什么数组类型不能相互赋值或初始化,例如上面提到的a = b这个表达式,ab都是数组类型的变量,但是b做右值使用,自动转换成指针类型,而左边仍然是数组类型,所以编译器报的错是error: incompatible types in assignment
习题
1、编写一个程序,定义两个类型和长度都相同的数组,将其中一个数组的所有元素拷贝给另一个。既然数组不能直接赋值,想想应该怎么实现。
答:
int main(void)
{
int array1[10] = { 3, 5, 1, 2, 2, 9, 1, 3, 4, 8 };
int array2[10];

int i;
for (i = 0; i < 10; i++) {
array2[i] = array1[i];
}

for (i = 0; i < 10; i++) {
printf(“%d, “, array2[i]);
}
return 0;
}



2. 3. 数组应用实例
调用C标准库得到的随机数其实是伪随机(Pseudorandom)数,是用数学公式算出来的确定的数,只不过这些数看起来很随机,并且从统计意义上也很接近均匀分布(Uniform Distribution)的随机数。C标准库中生成伪随机数的是rand函数,使用这个函数需要包含头文件stdlib.h,它没有参数,返回值是一个介于0和RAND_MAX之间的接近均匀分布的整数。RAND_MAX是该头文件中定义的一个常量,在不同的平台上有不同的取值,但可以肯定它是一个非常大的整数。
0~10的随机数:int x = rand() % 11;

把上面的程序运行几遍,你就会发现每次产生的随机数都是一样的,不仅如此,在别的计算机上运行该程序产生的随机数很可能也是这样的。这正说明了这些数是伪随机数,是用一套确定的公式基于某个初值算出来的,只要初值相同,随后的整个数列就都相同。实际应用中不可能使用每次都一样的随机数,例如开发一个麻将游戏,每次运行这个游戏摸到的牌不应该是一样的。因此,C标准库允许我们自己指定一个初值,然后在此基础上生成伪随机数,这个初值称为Seed,可以用srand函数指定Seed。通常我们通过别的途径得到一个不确定的数作为Seed,例如调用time函数得到当前系统时间距1970年1月1日00:00:00的秒数,然后传给srand

srand(time(NULL);

然后再调用rand,得到的随机数就和刚才完全不同了。调用time函数需要包含头文件time.h,这里的NULL表示空指针。

习题
1、用rand函数生成[10, 20]之间的随机整数,表达式应该怎么写?
答:

1. 补完本节直方图程序的main函数,以可视化的形式打印直方图。
答:
int main(void)
{
gen_random(UPPER);

int i, histogram[UPPER] = {0};
for (i = 0; i < N; i++)
histogram[a[i]]++;

for (i = 0; i < UPPER; i++)
printf(“%d\t”, i);
printf(“\n”);
do {     // 实际上只可能循环N次,因此外层while循环可改为for 0-> N-1,变量breakLoop也可以省了
int breakLoop = 1;
for (i = 0; i < UPPER; i++) {
if (histogram[i] > 0) {
printf(“%c\t”, ‘*’);
histogram[i]–;
breakLoop = 0;
} else {

                                printf(“\t”);
                        }
}
printf(“\n”);
if (breakLoop)
break;
} while (1);

return 0;
}

2、定义一个数组,编程打印它的全排列。比如定义:
#define N 3
int a[N] = { 1, 2, 3 };

则运行结果是:
     
$ ./a.out
1 2 3
1 3 2
2 1 3
2 3 1
3 2 1
3 1 2

最后再考虑第三个问题:如果要求从N个数中取M个数做组合而不是做排列,就不能用原来的递归过程了,想想组合的递归过程应该怎么描述,编程实现它。
答:

4. 字符串
字符串可以看作一个数组,它的每个元素是字符型的。
注意每个字符串末尾都有一个字符'\0'做结束符,这里的\0是ASCII码的八进制表示,也就是ASCII码为0的Null字符,在C语言中这种字符串也称为以零结尾的字符串(Null-terminated String)。数组元素可以通过数组名加下标的方式访问,而字符串字面值也可以像数组名一样使用,可以加下标访问其中的字符:
char c = “Hello, world.\n”[0];
编译错误,字符串字面值是只读的。
“Hello, world.\n”[0] = ‘A’;
char str[10] = “Hello”;
相当于
char str[10] = { ‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0′ };
最好让编译器自己计算:
char str[] = “Hello, world.\n”;

printf(“string: %s\n”, str);

printf会从数组str的开头一直打印到Null字符为止,Null字符本身是Non-printable字符,不打印。
如果数组str中没有Null字符,那么printf函数就会访问数组越界,后果可能会很诡异:有时候打印出乱码,有时候看起来没错误,有时候引起程序崩溃。
5. 多维数组
int a[3][2] = { 1, 2, 3, 4, 5 };

图 8.3. 多维数组

多维数组
也可以:int a[][2] = { {1, 2} , {3, 4}, {5,} };
结构体数组
struct complex_struct {
     double x, y;
} a[4] = { [0].x = 8.0; };

结构体元素是数组
struct {
     double x, y;
     int count[4];
} s = { .count[2] = 9 };

多维字符串数组
char days[8][10] = { “”, “Monday”, “Tuesday” … “Sunday” };
printf(“%s\n”, days[day]);

图 8.4. 多维字符数组

多维字符数组

这个程序和例 4.1 “switch语句”的功能其实是一样的,但是代码简洁多了。简洁的代码不仅可读性强,而且维护成本也低,像例 4.1 “switch语句”那样一堆caseprintfbreak,如果漏写一个break就要出Bug。这个程序之所以简洁,是因为用数据代替了代码。具体来说,通过下标访问字符串组成的数组可以代替一堆case分支判断,这样就可以把每个case里重复的代码(printf调用)提取出来,从而又一次达到了“提取公因式”的效果。这种方法称为数据驱动的编程(Data-driven Programming),写代码最重要的是选择正确的数据结构来组织信息,设计控制流程和算法尚在其次,只要数据结构选择得正确,其它代码自然而然就变得容易理解和维护了,就像这里的printf自然而然就被提取出来了。

石头剪刀布游戏问题:
留给读者思考的问题是:(man - computer + 4) % 3 - 1这个神奇的表达式是如何比较出0、1、2这三个数字在“剪刀石头布”意义上的大小的?

 

学习计划的一些补充

1. 并发编程

随着单机上CPU核数的不断增多,以及互联网中大型网站每天产生的海量数据,并发编程将会成为程序员必不可少的技能。

学习《Erlang程序设计》和Scala,理解函数式编程,搞清Actor模式以及并发编程的消息传递方式。

2. 汇编语言

懂一些计算机底层的知识才会明白咱们每天使用的计算机硬件是怎样运行的,操作系统是怎样启动的,写好的源代码是怎样编译成机器码被操作系统加载的。

学习《汇编语言》(第二版)、《80×86汇编语言程序设计教程》,了解计算机的构造CPU、内存、硬盘等,搞明白CPU是怎样寻址,什么是保护模式,保护模式和实模式有什么不同。

另外推荐《程序员的自我修养》、《天书夜读-从汇编语言到Windows内核编程》、《琢石成器:Windows32位汇编语言程序设计》。

3. 操作系统内核

Linux内核源码很多,里面包含很多算法和设计,学习起来真的很难。但挑重点的学习了解一下,对提高编程水平还是有很大帮助的。比如操作系统怎样通过Loader加载,进程的切换和调度等。

学习Linux四库全书:《Linux内核设计与分析》、《深入理解Linux内核》、《Linux内核源代码情景分析》、《Linux设备驱动开发》。再推荐一本《Orange’s 一个操作系统的实现》。

最重要的一点:read the fucking source code!

4. 游戏开发

游戏尤其是3D游戏开发是学习数学和算法的一个好途径,由于3D游戏通常很耗费资源,因此也是学习代码优化的好方法。比如怎样渲染图形的表面,怎样把空间坐标上的3D图形是怎样通过矩阵运算投射到摄影机屏幕上的,比如学习分析计算关键代码段的大O值并试着优化。

推荐《3D数学基础:图形与游戏开发》,以及DirectX的学习。

5. 数学和算法

这是编程的基础,编程一路学到底层东西时,自然而然就想补一补自己的数学和算法知识。

数学:《什么是数学》、《具体数学》。

算法:《计算机程序设计艺术》、《算法导论》。

通过做题检验自己,书后习题以及各大公司的笔试题。带着问题去看这些书,也许会增加一些动力。

6. 散文杂文

学累了,该放松一下了。

《代码之美》、《Java夜未眠》、《Unix编程艺术》、《疯狂的程序员》

看着看着就睡着了,梦到有一天自己成为武林中的编程高手。。。

《Linux C一站式学习》第七章 结构体


1. 复合类型与结构体
在编程语言中,最基本的、不可再分的数据类型称为基本类型(Primitive Type),例如整型、浮点型;根据语法规则由基本类型组合而成的类型称为复合类型(Compound Type),例如字符串是由很多字符组成的。
struct complex_struct {
     double x, y;
};
struct complex_struct {
     double x, y;
} z1, z2;
struct complex_struct z3, z4;
struct complext_struct z = { 3.0, 4.0 }
Initializer中的数据依次赋给结构体的各成员。如果Initializer中的数据比结构体的成员多,编译器会报错,但如果只是末尾多个逗号则不算错。如果Initializer中的数据比结构体的成员少,未指定的成员将用0来初始化,就像未初始化的全局变量一样。
这样是错误的:
struct complex_struct z1;
z1 = { 3.0, 4.0 };
Designated Initializer是C99引入的新特性,用于初始化稀疏(Sparse)结构体和稀疏数组很方便。有些时候结构体或数组中只有某一个或某几个成员需要初始化,其它成员都用0初始化即可,用Designated Initializer语法可以针对每个成员做初始化(Memberwise Initialization),很方便。
struct complex_struct z1 = { .y = 4.0 };
结构体类型用在表达式中有很多限制,不像基本类型那么自由,比如+ – * /等算术运算符和&& || !等逻辑运算符都不能作用于结构体类型,if语句、while语句中的控制表达式的值也不能是结构体类型。
结构体变量之间使用赋值运算符是允许的:z1 = z2;

注:结构体作为变量声明、参数、返回值都要加上struct关键字。


2. 数据抽象
1、在本节的基础上实现一个打印复数的函数,打印的格式是x+yi,如果实部或虚部为0则省略,例如:1.0、-2.0i、-1.0+2.0i、1.0-2.0i。最后编写一个main函数测试本节的所有代码。想一想这个打印函数应该属于上图中的哪一层?
答:
void print_complex(struct complext_struct z)
{
     if (z.x == 0 && z.y == 0) {
          printf(“0″);
     } else if (z.x == 0 && z.y != 0) {
          printf(“%.1f i”, z.y);
     } else if (z.x != 0 && z.y == 0) {
          printf(“%.1f”, z.x);
     } else {
          printf(“%.1f + %.1f i”, z.x, z.y);
     }
}
2、实现一个用分子分母的格式来表示有理数的结构体rational以及相关的函数,rational结构体之间可以做加减乘除运算,运算的结果仍然是rational。注意要约分为最简分数,例如1/8和-1/8相减的打印结果应该是1/4而不是2/8,可以利用第 3 节 “递归”练习题中的Euclid算法来约分。
答:
#include <stdio.h>
#include “gcd.c”
struct rational
{
     int x;
     int y;
};     //分号不能忘!
struct rational add(struct rational a, struct rational b)
{
     struct rational c;
     c.x = a.x + b.x;
     c.y = a.y + b.y;
     return simply(c);
}
// 变量和函数不能重名 int gcd会出错
struct rational simply(struct rational a)
{
     int gcdnum;

if (abs(a.x) > abs(a.y)) {
gcdnum = gcd(abs(a.x), abs(a.y));
} else {
gcdnum = gcd(abs(a.y), abs(a.x));
}
a.x = a.x / gcdnum;
a.y = a.y / gcdnum;
return a;

}
void print(struct rational a)
{
     printf(“%d/%d \n”, a.x, a.y);
}
int main(void)
{
     struct rational a = { -1, 8 };
     struct rational b = { -1, 8 };
     struct rational c = add(a, b);
     print(c);
     return 0;
}
3. 数据类型标志
enum coordinate_type { RECTANGULAR, POLAR };
struct complex_struct {
     enum coordinate_type t;
     double a, b;
};
枚举类型的成员是常量,它们的值由编译器自动分配,例如定义了上面的枚举类型之后,RECTANGULAR就表示常量0,POLAR表示常量1。如果不希望从0开始分配,可以这样定义:
enum coordinate_type { RECTANGULAR = 1, POLAR };
枚举常量也是一种整型,其值在编译时确定,因此也可以出现在常量表达式中,可以用于初始化全局变量或者作为case分支的判断条件。
虽然结构体的成员名和变量名不在同一命名空间中,但枚举的成员名却和变量名在同一命名空间中,所以会出现命名冲突。例如这样是不合法的:
int main(void)
{
     enum coordinate_type { RECTANGULAR = 1, POLAR };
     int RECTANGULAR;
}
习题
2. 编译运行下面这段程序:
#include <stdio.h>

enum coordinate_type { RECTANGULAR = 1, POLAR };

int main(void)
{
     int RECTANGULAR;
     printf(“%d %d\n”, RECTANGULAR, POLAR);
     return 0;
}

结果是什么?并解释一下为什么是这样的结果。
答:3719156 2。main函数中的局部变量作用域覆盖了外部的枚举。
4. 嵌套结构体
struct segment {
     struct complex_struct start;
     struct complex_struct end;
};
Initializer也可以嵌套,因此嵌套结构体可以嵌套地初始化:
struct segment s = { { 1.0, 2.0 }, { 4.0, 6.0 } };
访问嵌套结构体的成员要用到多个.运算符,例如:
s.start.t = RECTANGULAR;
s.start.a = 1.0;