星海's Blog

老头初学编程

C初级知识(三)

# 用户在命令行输入命令后,一般情况下SHELL后fork并exec该命令。但是Shell的内建命令例外,执行内建命令相当于调用Shell中的一个函数,并不创建新的进程。
# 但如果将命令行下输入的命令用()括起来,那么也会fork出一个子shell执行小括号中的命令。

# 1,Shell文件名代换(Globbing)  : *?[]

##  * 0个或0个任意字符
##  ? 一个任意字符
##  [] 匹配方括号中任意一个字符的一次出现

# 2,命令代换:`或$(),两者等价

# 3,算数代换:$(())
# 如果一个文件名以 -开头,例如 -help, 则 cmd - help 出错,因为各种UNIX命令都把 -号开头的命令行参数当作命令行选项。可以 cmd . / filename,或 cmd -- filename

# 4,''
# 单引号用于保持引号内所有字符的字面值,即使\和回车也不例外。但是字符串中不能出现单引号。

# 5,""
# 双引号除以下情况保持字符串字面值
#   $ ` \$ \` \" \\
# 
# 交互登录Shell(图形界面的窗口登录器和图形下的终端不是登录Shell)
# /etc/profile  ~/.bashrc ~/.bash_login ~/.profile
#
# 交互非登录Shell(图形界面下的终端,或者登录shell提示符下输入bash得到的Shell)
# ~/.bash_profile 
#
# 非交互启动(Shell脚本)
# 启动时执行的脚本文件由环境变量BASH_ENV定义,相当于执行
if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi

# 测试命令
# [ -d DIR ] 如果DIR 存在并且是一个目录则为真
# [ -f FILE ]如果FILE 存在且是一个普通文件则为真
# [ -z STRING ] 如果STRING的长度为零则为真
# [ -n STRING ] 如果STRING的长度非零则为真
# [ STRING1 = STRING2 ] 
# [ STRING1 != STRING2 ] 
# ARG1 和ARG2 应该是整数或者取值为整数的变量,OP 是-eq (等于)  -ne (不等于) -lt (小于)  -le (小于等于)  -gt (大于)   -ge (大于等于)之中的一个
# [ ARG1 OP ARG2 ]
 
# 逻辑命令
# [ ! EXPR ] 逻辑反
# [ EXPR1 -a EXPR2 ] 逻辑与
# [ EXPR1 -o EXPR2 ] 逻辑或
#
# 作为一种好的Shell编程习惯,应当总是把变量取值放在双引号之中
# 
# :是一种特殊的命令,称为空命令,Exit STatus总是为真
# Shell中的 && 和 ||具有C语言的Short-circuit特性,注意他和 -a -o的区别
# 例子:
test "$VAR" -gt 1 -a "$VAR" -lt 3
# 等价于
test "$VAR" -gt 1 && test "$VAR" -lt 3
#case 命令可类比C语言的switch/ case 语句,esac 表示case 语句块的结束。C语言的case 只能匹配整型或字符型常量表达式,而Shell脚本的case 可以匹配字符串和Wildcard,每个匹配分支可以有若干条命令,末尾必须以;;结束,执行时找到第一个匹配的分支并执行相应的命令,然后直接跳到esac 之后,不需要像C语言一样用break 跳出。
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
echo "Good Morning!";;
[nN]*)
echo "Good Afternoon!";;
*)
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1;;
esac
exit 0
#
#
# $0 相当于C语言main 函数的argv[0]
# $1 $2..位置参数, 相当于C语言main 函数的argv[1], argv[2]....
# $@ 参数列表,可用for in遍历
# $$ 当前Shell的进程号

# 和C语言类似,Shell中也有函数的概念,但是函数定义中没有返回值也没有参数列表。例如:
#! /bin/sh
foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"
# 注意函数体的左花括号{和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号}写在同一行,命令末尾必须有;号。

# Shell提供了一些用于调试脚本的选项,如下所示:
# -n    读一遍脚本中的命令但不执行,用于检查脚本中的语法错误
# -v    一边执行脚本,一边将执行过的脚本命令打印到标准错误输出
# -x    提供跟踪执行信息,将执行的每一条命令和结果依次打印出来

# 正则表达式
#
# 1,字符类
. 匹配任意一个字符
[] 匹配括号中任意一个字符
- 在[]中表示字符范围
^ 位于[]开头,匹配除括号中字符之外的任意一个字符
[[:xxx:]]  grep工具预定义的一些命名字符,如[[:alpha:]]   [[:digit:]]

# 2,数量限定符
? 匹配前面单元零次或一次
+ 一次或多次
* 零次或多次
{N} 精确匹配N次
{N,} 至少匹配N次
{,N} 至多匹配N次

# 3,位置限定符
^ 匹配行首的位置
$ 行末
\< 匹配单词开头
\> 匹配单词结尾
\b 匹配单词开头或结尾
\B 匹配非单词开头或结尾


## sed
/pattern/p 打印匹配pattern的行
/pattern/d 删除匹配
/pattern/s/pattern1/pattern2/
查找符合pattern的行,将该行第一个匹配pattern1的字符串改为pattern2

-n只打印匹配的行
pattern2中的&表示源文件的当前行中与pattern1相匹配的字符串
\1表示与pattern1中第一个()内相匹配的内容

## awk
# 自动变量$1、$2分别表示第一列、第二列等,类似于Shell脚本的位置参数,而$0表示整个当前行。
# awk '$2<75 {printf "%s\t%s\n", $0, "REORDER";} $2>=75 {print $0;}'
# awk '/^ *$/ {x=x+1;} END {print x;}' testfile

#awk 常用内建变量
FILENAME 	当前输入文件的文件名,该变量是只读的
NR 		当前行的行号,该变量是只读的,R代表record
NF 		当前行所拥有的列数,该变量是只读的,F代表field
OFS 		输出格式的列分隔符,缺省是空格
FS 		输入文件的列分融符,缺省是连续的空格和Tab
ORS 		输出格式的行分隔符,缺省是换行符
RS 		输入文件的行分隔符,缺省是换行符



C初级知识(二)

/*  改变计算机存储单元里的数据或者输入输出操作都算Side Effect

Implementation - defined:
C标准没有明确定义,但要求编译器必须对此作出明确规定
unspecified:编译器自己可以决定,不必写在编译器文档中。
undefined:完全不缺定。C标准和编译器可能都没又规定怎么处理,甚至没有作出错误检查。例如数组访问越界。

Usual Arithmetic Conversion
如果一边是无符号数,另一边是有符号数。如果无符号数的Rank不低于有符号数,则把有符号数转换成另一边的无符号类型。
如果无符号数的Rank低于有符号数的Rank。如果有符号数类型能够覆盖这个无符号数类型的取值范围,则将无符号数类型转换成另一边的有符号类型。否则,也就是有符号数类型不足以覆盖这个无符号数类型,则将两边都转换成有符号数的Rank对应的无符号数类型。例如32位平台上的unsigned int和long做算数运算,将两边都转换成unsigned long。

如果一个表达式隐含着多个Side Effect,发生顺序未确定。
Sequence Point:
, && || :
? 的第一个操作数求值之后是Sequence Point。
在一个完整的声明末尾是Sequence Point。int a[10], b[20];
a[10]的末尾是,然后是b[20]。

运算优先级:
1,标识符,常量,()
2,后缀运算符,包括[],函数调用(),结构体取成员.,->, 后缀++, 后缀--, (从左到右依次计算) a.name++,先求a.name
3,单目运算符,包括前缀++,前缀--,sizeof,类型转换(),取址&,指针取值*,按位取反~,逻辑非!。如果一个操作数有多个前缀,则从右到左依次计算。例如~!a,先计算!a。
4, + - * / % 从左到右。
5,移位运算符 << , >>
6,关系运算符 > < <= >=
7,相等运算符 == , !=
8,按位与 &
9,按位异或 ^
10,按位或 |
11,逻辑与 &&
12,逻辑或 ||
13,条件运算符
14,条件 : ?
15,赋值 = 和各种复合赋值 *= , -= ,  /= , |= 。在双目运算符中只有这种是右结合的。
16,逗号运算符。
*/

#include 	<stdio.h>

const int A = 10;                      /* .rodata段,只读变量 */
int a = 20;                            /* data段,global符号 */
static int b = 30;                     /* data段,local符号 */

/* 静态变量未初始化,则放在.bss段,初始化为0。 static 变量未初始化,也放在.bss段
.bss段在文件中不占用存储空间,在加载时这个段用0填充*/
int c;

int main ( int argc, char *argv[] )
{
        static int a = 40;                      /* data段 */
        char b[] = "hello world";
        register int c = 50;

        printf("Hello World %d\n", c); /* .rodata段,只读变量 字符串字面值 */

        return 0;
}

/*
 * 作用域(Scope):
 * 1,函数作用域(Function Scope), 只有语句标号属于函数作用域
 * 2,文件作用域(File Scope),全局变量,main,全局中的static
 * 3,函数原型作用域(Function Prototype Scope)
 */

/*
 * 标识符链接属性(Linkage)
 * 外部链接(External Linkage)
 * 内部链接(Internal Linkage) 全局变量,全局中的static变量
 * 无链接(No Linkage)
 * Previous Linkage  extern
 */

/*
 * 链接库
 *
 *
 * 打包成静态库
 *  ar rs libxxx.a aaa.o bbb.o ccc.o
 * 以上命令等价于ar r lib.............. && ranlib libxxx.a
 *
 * gcc main.c -L. -lstatck -Istack -o main
 * -L.告诉编译器在当前目录找库文件
 * -lstatck告诉编辑器要链接libstack库,-I选项告诉编译器去哪个目录找头文件
 *  -static告诉编译器只链接静态库。链接器在链接静态库时,会把静态库中的目标文件取出来和可执行文件真正链接在一起。链接器可以从静态库中只取出需要的部份来做链接。
 *
 *
 * 共享库在编译时要加-fPIC选项,PIC表示生成位置无关代码(Position Independent Code)。
 *
 * 程序运行时,要注意共享库位置。
 * 共享库搜索路径顺序:
 * 1,环境变量 LD_LIBRARY_PATH
 * 2,缓存文件/etc/ld.so.cache。这个缓存文件是ldconfig读取配置文件/etc/ld.so.conf之后生成的
 * 3,默认的系统路径 /usr/lib /lib
 *
 *
 *
 * 共享库的命名惯例:real name, soname 和 linker name
 * real name包含完整的共享库版本。例如 libcap.so.1.10
 * soname是一个符号链接的名字,只包含共享库的主版本号。主版本号一致即可保证接口一致。例如libcap.so.1
 * linker name只在编译链接时使用。有的link name是一个符号链接的名字,有的是一个符号链接
 *
 *  gcc -shared -WL,-soname,libstatck.so.1 -o libstack.so.1.0 aaa.o bbb.o
 *  -WL之后是GCC传递给链接器的选项。本例中
 *  libstack.so.1.0是realname,libstack.so.1是soname
 */

/* 1,虚拟内存管理控制物理内存的访问权限
 * 2,最主要的作用是让每个进程都有独立的地址空间。是指
 * 不同进程中的同一个VA被MMC映射到不同的PA,并且某一个进程中访问任何地址都不能访问到另外一个进程的数据。
 * 3,VA到PA的映射会给分配和释放内存带来方便。不连续的PA可映射为连续的VA。
 */

/* define
 * 如果一个程序文件中重复定义一个宏,C语言规定这些重复的宏定义必须一模一样
 *
 * #运算符用于创建一个字符串,#后应该跟一个形参
 * 注意,实参中如果包含字符串或字符常量,则宏展开之后,字符串的界定符"
 * 要替换成\",其中的\要替换成\\
 *
 * 宏定义中运算符##把前后两个预处理Token连接成一个Token。根据定义,##不能出现在宏定义开头或结尾
 * __VA_ARGS__
 * gcc有一种扩展语法,如果
 * ##运算符用在__VA_ARGS__前面,当__VA_ARGS__是空参数时,##前面的,被吃掉了
 */

#define sh(x) printf("n" #x "=%d, or %d\n", n##x,alt[x])
#define sub_z 26
sh(sub_z);

/* 展开后为 printf("nsub_z = %d, or %d\n",nsub_z, alt[26] */

/* #pragma预处理指示供编译器实现一些非标准的特性。
 * __FILE__展开为当前源文件名的字符串,
 * __LINE__展开为当前代码行的行号。
 * GCC引进了一个特殊的标识符
 * __func__,是一个变量名而不是标识名,不属于预处理。当前函数名的字符串。
 */

/*
 * Makefile
 *
 * 如果make执行的命令前加了@命令,则不显示命令本身而只显示它的结果。命令如果出错,就立刻终止。
 * 如果命令前加了-号,即使这条命令出错,make也会继续执行后续命令。
 *
 * 把clean声明为一个伪目标。 .PHONY: clean
 * make隐含规则(Implicit Rule)
 * #号表示单行注释,
 * $(CC)取变量CC的值,
 * $@ 的取值为规则中的目标,
 * $< 为规则中的第一个条件,
 * $? 为规则中所有比目标新的条件,组成一个列表,以空格分隔。
 * $^ 表示规则中的所有条件。
 * 用=号定义变量的延迟展开特性,有可能写出无限递归的定义,例如 CFLAGS =
 * $(CFLAGS) -O。如果希望make遇到变量定义时立即展开,可以用:=运算符。
 * 如果要定义一个变量的值是空格,可以
NULLSTR :=
SPACE := $(NULLSTR) # 注释前面有个空格。
*/

/* foo ?= $(bar)的意思是,如果foo没有定义过, foo =
 * $(bar),如果foo已经定义,则什么也不做
 *
 * += 运算符可以给变量追加值。+=根据前面是=还是:=再展开。
 * 例如:
 * objects := main.o
 * objects += $(foo)
 * 前面是 :=,则 如果foo没有定义,则 objects =
 * main.o。如果前面foo已经定义,则objects = main.o foo的值
 *
 */

/* 数组名做右值时自动转换成指向首元素的指针
 * 指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针。
 * 如果要定义一个指针指向字符串字面值,这个指针应该是const char * 类型。
 * 错误例子:
 * char *p = "abcd";
 * *p = 'A';
 * p指向 .rodata段,不允许改写,但编译器不会报错,在运行时会出现段错误
 */

int a[10];
int (*pa)[10] = &a;
/* &a[0]表示数组a的首元素的首地址,而&a表示数组a的首地址。数值相同,类型不同。
*/

typedef int F(void);         /* 定义函数类型F */

F f, g;                      /* 正确,int f(void) */
//F h(void);                  /* 错误,不能返回函数类型 */
F *e(void);                  /* 正确,返回一个F*类型的函数指针 */

/* 传入参数,传出参数,Value-result参数 */

/* stdarg.h的一种实现。
 * 原理:可变参数中的参数从右向左依次压栈,第一个参数靠近栈顶,最后一个参数在栈底。对齐!!
 */

#ifndef _STDARG
#define _STDARG

typedef char *va_list;
#define va_arg(ap,T) \
        (* (T*)(((ap) += _Bnd(T, 3U)) - _Bnd(T, 3U)))
#define va_end(ap) (void)0
#define va_start(ap, A) \
        (void)((ap) = (char *)&(A) + _Bnd(A, 3U))
/*_Bnd使之对齐,注意后面的运算符优先级,是 ((sizeof(x) + (bnd)) & (~(bnd)) */
#define _Bnd(X, bnd) (sizeof(X) + (bnd) & ~(bnd))
#endif

void *memset(void *s, int c, size_t n);
void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
/* memmove可以拷贝两个重叠的内存区域。*/

char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr); /* 可重入posix ,saveptr是个 Value-result参数*/

// "r"    只读,文件必须存在
// "w"    只写,文件不存在就创建,截0
// "a"    追加,不存在就创建
// "r+"   读写,文件必须存在
// "w+" "a+",读写,文件不必存在
//

//从终端设备输入时有两种方法,可以表示文件结束。一种是开头输入EOF或行中输入两次EOF,一种是用Shell的Heredoc语法。 ./a.out << endif
// printf %a.b
// a为宽度,格式化后的最小长度。b为精度,对于字符串来说指定了格式化后的最大长度,对于浮点数来说指定了格式化后小数点右边的位数,对于整数来说指定了格式化后的最小位数。
//
//
long int strol(const char *nptr, char **endptr, int base);
double strtod(const char*nptr, char **endptr, int base);
// endptr指向未被识别的第一个字符
//

C最初级知识


#include <stdio.h>
int main(void)
{
	int a, b, c;
	b = 5;
	c = 5;
	a = ++b + c--;		//相当于顺序执行 b=b+1,a=b+c,c=c-1
	printf("%d,%d,%d\n", a, b, c);
	a = b-- - c;		//相当于顺序执行 a=b-c,b=b-1
	printf("%d,%d,%d\n", a, b, c);
	a = -b++ + c;		//相当于执行a=-b+c,b=b+1
	printf("%d,%d,%d\n", a, b, c);
        a-=a-5;
        printf("%d\n",a);
	return 0;
}
/* 给一个正整数, 
* 1,求出这是几位数 
* 2,逆序输出它的每一位数字 
* 3,!!通过逆序输出的每一位数字,重新合成这个正整形数据(不允许用reverse,atoi等函数 以及 数组 )*/ 
#include <stdio.h> 
#include <math.h> 

int main(void) 
{ 
int a, i, temp, count = 1;	//count记录位数 
printf("input a int number : "); 
scanf("%d", &a);	//a为输入的数字 
temp = a; 

while (a / 10 != 0) { 
++count; 
a = a / 10; 
} 
printf("这个数字有%d位数\n", count); 

a = temp; 
temp = 0; 
for (i = 0; i < count; ++i, a = a / 10) { 
printf("%d ", a % 10); 
temp += a % 10 * pow(10, i);	//这里我借助了pow函数,有没有更简洁,不借助其他函数的办法呢? 
} 
printf("\n"); 

printf("这个数字是%d\n", temp); 
return 0; 
} 
minute + 1 = hour;
等号左边的表达式要求表示一个存储位置而不是一个值,这是等号运算符和+ - * /运算符的又一个
显著不同。有的表达式既可以表示一个存储位置也可以表示一个值,而有的表达式只能表示值,不
能表示存储位置,例如minute + 1这个表达式就不能表示存储位置,放在等号左边是语义错误。表
达式所表示的存储位置称为左值(lvalue)(允许放在等号左边),而以前我们所说的表达式的值
也称为右值(rvalue)(只能放在等号右边)。上面的话换一种说法就是:有的表达式既可以做左
值也可以做右值,而有的表达式只能做右值。目前我们学过的表达式中只有变量可以做左值,可以
做左值的表达式还有几种,以后会讲到

如果定义三个变量int a, b, c;,表达式a = b = c是合法的,先求b =
c的值,再把这个值赋给a,而表达式(a = b) = c是不合法的,先求(a = b) 的值没问题,但(a =
b) 这个表达式不能再做左值了,因此放在= c的等号左边是错的。

    在C语言中整数除法取的既不是Floor也不是Ceiling,无论操作数是正是负总是把小数部分截掉,在
数轴上向零的方向取整(Truncate toward Zero)


局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常量表达式
(Constant Expression)初始化。
如果全局变量在定义时不初始化则初始值是0,如果局部变量在定义时不初始化则初始值是不确定的。所以,局部变量在使用之前一定要先赋值,如果基于一个不确定的值做后续计算肯定会引入Bug。

(a/b)*b+a%b的值总是等于a

A<B<C,不同于数学中的 a<b and b<c,而是等同于 a<b,为true或false,即1,0 ,然后判断1或0是否<c

以下哪一个if判断条件是多余的可以去掉?这里所谓的“多余”是指,某种情况下如果本来应该打印Test OK! ,去掉这个多余条件后仍然打印Test OK! ,如果本来应该打印Test failed!,去掉这个多余条件后仍然打印Test failed!。
if (x<3 && y>3)
    printf("Test OK!\n");
    else if (x>=3 && y>=3)
    printf("Test OK!\n");
    else if (z>3 && x>=3)
    printf("Test OK!\n");
    else if (z<=3 && y>=3)
    printf("Test OK!\n");
    else
    printf("Test failed!\n");
这个文件用typedef工具创建了新的类型名字。
比如:uint32_t作为一个具有某种特征的标准类型的同义词或别名。在某系统中这个标准类型可能是unsigned int,另一个系统可能是unsigned long.编译器会提供所在系统相一致的头文件。这个新的名称叫做“确切长度类型”。

使用“确切长度类型”的一个潜在问题是某些系统可能不支持一些选择,例如int8_t类型。C99标准定义了第二组名字结合。这些名字保证所表示的类型至少大于指定长度的最小类型。被称为“最小长度类型”。例如 int_least8_t,他在某些机器的实现也许是16位整数。

“最快最小长度类型” int_fast8_t。
“最大可能整数类型” intmax_t uintmax_t

C99为表示浮点常量新添加了一种十六进制格式。最后是2的指数,而不是10
0xa.1fp10
a是10, 1f为十六进制小数,转2进制为1/16+15/256.  p10表示2的十次方。结果为 (10+1/16+15/256)*2^10
#include	<stdio.h>
						/* 参数传递机制 */
int main(void)
{
	float n1 = 3.0;
	double n2 = 3.0;
	long n3 = 2000000000;
	long n4 = 1234567890;
	long n3_2 = 78888888;

	printf("%ld %ld %ld %ld %ld\n", n1, n2, n3, n4, n3_2);
/* 该调用告诉计算机把变量n1...n4的值传给计算机。计算机把他们放知道被称为堆栈(stack)的一块内存区域中来实现。计算机根据变量的类型而非转换说明符把这些值放到堆栈中。所以,n1 在堆栈中占用8B(float被转换为double),n2 8B,n3/n4 4B。
 * 然后控制转移到printf()函数。它在读取时,根据转换说明符号读取。%ld说明pf()读取4个字节。所以返回结果如下
 * 0 1074266112 0 1074266112
 * 如果我们再声明一个long int数值,读取的应该就是n3的值了。
 */
	return 0;
}
#include	<stdio.h>

void show_array(double ar[], int n)
{
	int i;

	for (i = 0; i < n; i++)
		printf("%8.3f", ar[i]);
	putchar('\n');
}

int main(void)
{
	double rates[5] = { 88.99, 100.12, 59.45, 183.11, 340.5 };
	const double *pd = rates;	/* 指向常量的指针不能用于修改数值,
					   这一行把pd声明为指向 const double的指针 */
	double *const pc = rates;	/* 指针不会指向别处,不可更改 */
//不允许*pd = 29.89;
//      pd[2] = 222.22;

	rates[0] = 99.99;
	pd++;			/* 让pd指向rates[1],这是允许的 */

	const double locked[5] = { 88.99, 100.12, 59.45, 183.11, 340.5 };
	double *pnc;
//只有非常量的地址才可以赋给普通指针   
//非法:   pnc = locked;
//将常量数组作为非常量数组参数会警告或出错
	show_array(locked, 5);
  
	return 0;
}
#include	<stdio.h>

int main(void)
{
	int a[5] = { 5, 2, 3, 1, 4 };
	int *p1;
	const int *p2;
	const int **pp2;

	p2 = a;

	p1 = p2;		/* 非法,把const指针赋值给非const指针 */
	p2 = p1;		/* 合法,把非const指针赋值给const指针 */

	pp2 = &p1;		/* 非法,把非const指针赋值给const指针 */

	return 0;
}
括号成员第一;        //括号运算符[]() 成员运算符.  ->
全体单目第二;        //所有的单目运算符比如++ -- +(正) -(负) 指针运算*&
乘除余三,加减四;  //这个"余"是指取余运算即%
移位五,关系六;    //移位运算符:<< >> ,关系:> < >= <= 等
等于(与)不等排第七;    //即== !=
位与异或和位或;    //这几个都是位运算: 位与(&)异或(^)位或(|)    
"三分天下"八九十;  
逻辑或跟与;            //逻辑运算符| 和 &&
十二和十一;            //注意顺序:优先级(||)  底于 优先级(&&) 
条件高于赋值,      //三目运算符优先级排到 13 位只比赋值运算符和","高//需要注意的是赋值运算符很多!
逗号运算级最低!  //逗号运算符优先级最低
/*-----------------------------------------------------------------------------
 *  typedef建立的一系列相关类型
 *-----------------------------------------------------------------------------*/

typedef int arr5[5];
typedef arr5 *p_arr5;
typedef p_arr5 arrp10[10];

arr5 togs;			/* togs是具有5个元素的int数组 */
p_arr5 p2;			/* p2是一个指针,指向具有5个元素的int数组 */
arrp10 ap;			/* ap是具有10个元素的指针数组,每个指针指向具有5个元素的int数组 */
Initializer中的数据依次赋给结构体的各成员。如果Initializer中的数据比结构体的成员多,编译器会报错,但如果只是末尾多个逗号则不算错。如果Initializer中的数据比结构体的成员少,未指定的成员将用0来初始化,就像未初始化的全局变量一样。
z1必须是局部变量才能用另一个变量x的值来初始化它的成员,如果是全局变量就只能用常量表达式来初始化。这也是C99的新特性,C89只允许在{}中使用常量表达式来初始化,无论是初始化全局变量还是局部变量。
a,如果两边都是有符号数或无符号数,那么较低Rank的类型转换成较高Rank的类型

b,如果一边是符号数另一边是有符号数。
如果无符号数Rank不低于有符号数的Rank,则把有符号数转换为另一边的无符号类型。

如果无符号数Rank低于有符号数的Rank。此时分两种情况。
(1),如果这个有符号数类型能够覆盖这个无符号数类型的取值范围,则把无符号数转成另一边的有符号类型。
(2),否则把两边都转换成有符号数Rank对应的无符号类型。

 

漫谈C语言及如何学习C语言(转)

 

漫谈C语言及如何学习C语言

云风最近写了一篇博客《C语言的前世今生》。作为长期使用C语言开发网络游戏服务器的程序员,云风是有理由写这样一篇文字,不过还是感觉谈的不够深入,C语言在业界使用的现状没有怎么描写,有些意犹未尽。

为什么要学习C语言?

为什么要学习、使用C语言?为什么要学习一个可能比自己都岁数大的编程语言?

我在前面如何学习编程语言的博客文章http://sunxiunan.com/?p=1597 里提到,选择一门编程语言,“为什么而学”这个目的是最重要的,目的不明确就没法学好。这也是为什么很多学生朋友在大学里必修C语言却觉得没学明白的原因。因为学习的目的不明确,学习当然也没有动力。还有一个原因是C语言是工程实践性很强的语言,它不是来自某个研究所某个大学学院,而是实实在在从项目需要中产生,伴随着Unix的兴起而流行,语义简明清晰,功能强大而不臃肿,简洁而又不过分简单,实在是居家旅行工作学习必备之良友。

C语言相比C++的优点之一就是最小惊讶原则,一是一二是二,不会在私底下产生一些莫名其妙的额外产物。用C++做个例子,比如这样一个函数原型void PassWithClassValue(COneClass clsParam1),稍微了解C++的朋友都会知道,如果你没有实现COneClass的拷贝构造函数,编译器会好心的帮你实现一个,而且在调用这个函数PassWithClassValue的时候,偷偷地调用拷贝构造函数产生一个临时对象作为参数传递,对于某些情况,比如编写操作系统这类必须优化性能的情景下,这些自以为是的东西是非常邪恶的事情。

C语言本身只提供必要的语言特性,其它复杂一点功能如文件处理、数学计算等等都以库函数方式提供,甚至连malloc、free这种“必须有”的功能,也是以标准库函数的方式提供,而不是作为C语言核心出现。在伟大的著名的无所不包的《K&R》开头部分就提到了,for其实可以通过while来完成,只不过for可以写的更简洁,言外之意,对于C语言for其实不是必要的。跑题一点说,在其它程序语言中Lua可以说继承了C语言简洁的设计哲学,甚至连continue这种几乎必备的关键字都一直拒绝加入,在Lua的maillist以及wiki里都提到过continue这个问题,Lua语言维护者认为continue对于Lua而言不是必要的,也不考虑在后续版本中添加这个关键字。这种简洁哲学也让C语言的可移植性、便携性特别优秀,也使得很多嵌入式系统依然使用C语言作为主要编程工作语言。

Java语言有一个口号:“一次编写,处处运行”,就是跨平台这个噱头。实际上C语言从早期开始就几乎达到了“一次编写,处处编译”,在ANSI在1989年统一了C语言标准以后(称之为C89),只要特定平台上的编译器完整实现了C89标准,而且你的代码没有使用某些特殊的扩展(GCC以及微软都有自己的编译器特定扩展),那么代码一定可以编译通过,再实现一下操作系统相关的函数库,C语言的移植就是很简单的事情。可以用Lua作为例子,Lua本身是完全遵循C89标准,没有使用任何特定扩展,这也保证了有C语言编译器的平台,都可以编译使用Lua。可以编译运行C语言的硬件平台可以从A排到Z,真是非常有意思的事情。

C语言也是一个比较少见的应用领域极为广泛的语言。比如编写操作系统这种高难问题,只有C++、汇编语言可以做到。C语言可以编写服务器端软件如Apache、Nginx,或者编写GUI程序,如GTK。大多数程序语言的第一版是通过C语言实现,借助前面提到的“一次编写处处编译”,最大的保证了这些程序语言的可移植性。在Web开发领域,C语言的应用相对较少,这也是一种取舍的结果,Web开发需要使用PHP、Ruby、Python这样的动态语言,可以快速上线快速修改,可以最大程度满足用户时时变化的需求,这也是C语言的弱项。如果把程序语言的应用领域从硬件到管理软件、Web程序做一个很粗略从下到上的排列,C语言适合领域是比较底层靠近硬件的部分,而新兴语言比较偏重于高层管理或者Web开发这种相对贴近最终用户的领域。比较流行的混合开发模式是使用C语言编写底层高性能部分代码或后台服务器代码,而使用动态语言如Python做前端开发,充分发挥它们各自的优势力量。

提到C语言的缺点,常常是它缺少这种或者那种特性,比如有人建议加入GC,有人建议加入并行或者并发支持,有人提到没有一个比较完整的类似C++的异常策略。这些特性有的可以通过引入第三方库来实现,但C语言的设计哲学其实决定了它不会像C++那样“非常强大”。即使引入了某些人期望的特性,依然会是某些人喜欢某些人不喜欢的情形,现在的功能对于C语言应用领域来说已经够用,其它特性可以通过特定程序语言实现,并且通过C API与C语言编写的程序进行交互。任何一个工匠都不可能只使用一个工具完成他的工作,不同工具结合起来才能更快更好的完成任务。

提到C API,也稍微介绍一下,我们知道windows操作系统的api也好,Linux的系统api也好,或者是想给Ruby、Python编写扩展模块,C语言形式的函数定义都是唯一的选择。C语言就好像是一个中间层或者是胶水,如果想把不同编程语言实现的功能模块混合使用,C语言是最佳的选择。

提了这么多关于C语言的好处,那么学习C语言是否适合就看你自己的判断了,例如要进行一个嵌入式项目,或者需要进行服务器端开发,或者写一个性能相关的组件等等,C语言都是比较好用的选择。另外也可以在C++的使用过程中有意的使用C语言的思考方式,汲取C语言简洁明快清晰地设计思路,对编程设计水平会有很大的提高。

C语言学习方法

在前面http://sunxiunan.com/?p=1597 曾经提到过一个比较系统学习一门新的编程语言的方式,C语言学习也可以按照类似的顺序:阅读参考书,阅读代码,编写调试实际程序,上网参与讨论,研究高级话题。

学习语言的开始一般是阅读参考书。我建议选择几本非常经典的好书,仔细完整反复阅读几遍,“书读百遍其义自现”。选择C语言学习的好处是,这几本书基本上完整涵盖了C语言编程领域的方方面面,不会像C++那样,即使读完一堆书还是有些糊涂,依然有这样那样难懂的陷阱。

1,参考书籍

在豆瓣上列了一个书单,大家可以直接参考http://book.douban.com/doulist/636329/

在下面简单点评一下,阅读顺序最好参照列出的顺序。

《The C Programming Language》http://book.douban.com/subject/1230004/

image

如果你只想买一本书学习C语言,只需要买这一本就够了。如果你经费足够,建议你多买几本,办公室、家里都放上一本,随手都可以翻翻。用三个词语来形容它就是:经典!经典!经典!这本薄薄的只有二百多页的小书涵盖了C语言的方方面面,前无古人而且后无来者,任何溢美之词都不足以形容它。

《The C Programming Language》(后面称为 K&R)里面包含了一个简单的语法解析器,包含了malloc如何实现,包含了一个完整的操作系统目录浏览程序,这些程序的实用性极高,可以这样说,如果学习任何一门语言能够自己独立动手实现以上的功能,基本上就可以算是入门了。K&R书里面每段都蕴含着非常值得探究的软件开发工程实践经验,如果没有一定的开发经验,其实是看不出来这些冰山下面的内容的,比如开头一章就提出用写完整代码这种方式来教学,而在书中那些C语言的陷阱或者可能出问题的地方,都有提到,但是由于篇幅所限,写的非常简约,很难让人一下就看懂。我正在完整的逐字逐句的阅读此书,希望能稍作注解,写几篇博客分享一下。

《C陷阱与缺陷》http://book.douban.com/subject/2778632/

《C专家编程》http://book.douban.com/subject/2377310/

这两本书也是学习及使用C语言的朋友必备的两本书,比如《C专家编程》,专门用两三个章节详细介绍C语言中数组与指针的不同之处,这两本书在某种程度上算是对K&R略过的地方做了详细补充,强烈推荐。

《C语言参考手册》http://book.douban.com/subject/2132084/

这是最后一本强烈推荐你最好买回家作为案头书必备的参考书。前面几本书或者稍显简略,或者专注某个特定专题,都不适合遇到问题时翻查。这本《C语言参考手册》可以看作是C语言编程的《新华字典》,全面而权威。里面还涵盖了C99的内容,紧跟时代潮流。

下面几本书都可以作为交叉参考,也都很有价值,也是建议大家都买下来,好书如朋友,日久弥新,像是我推荐的这几本书在douban或者amazon上评分都非常高,而且反复再版。

《C和指针》http://book.douban.com/subject/1229973/

指针的重要性如何,学过C语言(或者C++)的朋友都知道,这本书更是把指针拔高到了与C语言平起平坐的地位,其实也是从头开始介绍,作为教学参考书也是可以的。

《C标准库》http://book.douban.com/subject/3775842/

这本书是专门介绍C语言的标准库如何实现的,比如malloc算法,用标准的C语言该如何写?strlen这个函数应该如何实现?尽管书中不少代码与真实的C标准库相差很多(由于标准库需要考虑性能优化,很多函数有一些特定的trick),但是绝对值得参考。

《你必须知道的495个C语言问题》 http://book.douban.com/subject/3422332/

这本书其实就是C-FAQ的印刷版本,C-FAQ在各种编程语言的FAQ中可以称得上质量一流。如果你想应聘或者招聘C语言相关程序员,这本书一定要参考。

《Linux C编程一站式学习》http://book.douban.com/subject/4141733/

学习C语言,一定不能只读书,应该动手练习完成书里面的项目需求(比如编写一个目录浏览器)以及每章的练习题目。这就需要有可以实验的环境,下面针对不同操作系统简单做一下介绍。

2,动手实验环境搭建

也没有调查过,不知道现在学校里学习C语言是不是依然跟着谭浩强老师用TurboC2.0编程,如果还是这个组合的话,那就太差劲了,赶快抛开它们。

下面主要介绍不同操作系统平台下的集成编程环境,基于初学者以及我个人喜好,就不推荐大家命令行下用vim编程了,直接上IDE。

Windows系统下推荐大家使用Code::blocks这个软件。这个软件最大优点是自带了基于mingw的GCC以及GDB,只要下载70M左右软件包,就可以完整支持C++、C语言编程了。各种功能(比如调试功能)也很强大,版本更新也比较快。注意下载选择名字有mingw的文件,比如最新版本是codeblocks-10.05mingw-setup.exe(版本也许有所不同)。

主页:http://www.codeblocks.org/

image

3,网络资源

如果想用十分钟时间了解一下C语言的来龙去脉、前世今生,维基百科这个页面http://en.wikipedia.org/wiki/C_%28programming_language%29 是最佳选择。

从维基百科可以看到,C语言1972年由Dennis Ritchie设计的命令式、结构化范式编程语言。类型为静态的弱类型,需要显式定义。最新国际标准为C99。设计上主要受到了B、ALGOL68、汇编语言、PL/I、FORTRAN的影响,C语言也影响了大量编程语言,如C++、Objective-C、C#、Java、Go、PHP、Python等等(个人觉得受C影响很大的是PHP,基本上有C编程基础的程序员,很容易就能上手PHP了,除了PHP的OO部分)。

在维基百科条目中有很大篇幅介绍了作者认为C语言缺失的特性,比如面向对象、多线程、GC、异常处理等等,当然这有些吹毛求疵,如果需要这些特性,完全可以用其它程序语言。另外一个介绍的重点是“未定义行为”,有些我们认为理所当然的结果,其实在C语言标准中并没有明确定义,假定这些行为应该如何,当程序使用另外的编译器或者不同版本编译器编译运行,都可能有bug产生。

接下来维基百科条目谈到了C语言的用处,必须承认尽管现在编程语言成百上千,能称之为“系统级”的少之又少,新兴语言中只有Go还能称得上。现在大规模软件项目中完全选用C语言可能性不大,但是核心部分完全可以用C搭建,相对C++开发工具的高昂价格,C语言相关的免费辅助开发软件非常丰富,比如splint,valgrind,不少核心库经过长期使用也都非常稳定。

由于C语言广泛支持各种平台以及编译器相对成熟可靠,不少编程语言选择C语言作为一个中间层,比如Glasgow Haskell编译器就是这样做的。

另一个可以找到大量C语言编程相关资料的地方是“美味书签”,通过搜索特定关键字 (C + programming)就可以找到很多值得挖掘的资源http://delicious.com/search?p=c+programming

还可以参考dmoz.org的C语言分类http://www.dmoz.org/Computers/Programming/Languages/C/ 相比美味书签时效性能差点,但是分类比较系统,查找也要容易一些。

程序员往往是懒惰的,“拿来主义”、“拷贝主义”很流行也很有效,当对某个函数或者关键字不是很理解的时候,看看别人是怎么使用的,会非常有启发性。这里介绍几个常用的代码搜索网站,最常用的是google的codesearch:http://codesearch.google.com ,可以通过不同条件及正则表达式搜索特定关键词。另外可以参考维基百科上一个“带有C语言示例的文章”分类,里面代码写的也很不错。还可以在github.com上搜索相关项目。在前面博客文章我还介绍了一个名为罗塞塔代码的网站http://rosettacode.org/ 这个网站上可以找到不同程序语言针对某个问题的解决方案,用于学习比较非常便利。

学习编程也需要大量阅读名家经典代码,与学中文英文需要大量阅读名著一个道理,C语言编程优质项目那是“彩旗飘舞,人山人海”,个人建议可以看看Lua、Sqlite、Nginx这些项目的代码,代码量不多,而且代码质量也都比较高。另外可以看看Linux内核代码,坊间有不少书籍可以帮助解读。关于如何很好的阅读代码,大家可以参考《Code Reading》这本书。

书看了几本,代码写了一些,也略微读了读其他人的代码,就应该用C语言来完成真实工作中碰到的问题,让C语言真正成为你的瑞士军刀。只有当你经常使用C语言来进行编程工作,经常思考如何通过C设计一个优雅高效的系统,才能更深刻的理解C语言设计哲学。

还可以到http://stackoverflow.com 参与回答问题,浏览其他人的问题解答来汲取知识,比如这篇http://stackoverflow.com/questions/2054939/char-is-signed-or-unsigned-by-default 就介绍了一个C语言关于char类型的小陷阱。

C语言学习当中,有一些难点需要多加注意,如pointer与array的不同之处,复杂类型定义如何解读,如何正确使用预处理preprocessor以及宏定义。其实这些内容在前面书籍都是反复提到,如果按部就班学习下来,应该不成问题。

当C语言学习的差不多时候,还可以学习一门动态语言,比如Lua或者Python,试着在实际工作项目中混合使用动态语言与C语言,一加一发挥出来的力量不仅仅是二,而是非常二(说笑一下,哈哈)。

还有什么问题,欢迎留言。

附录

一些有用的C语言网络资源:

C语言标准化组织ISO JTC1/SC22/WG14的主页,在这里可以找到ISO C的文档:http://www.open-std.org/jtc1/sc22/wg14/

《The Development of the C Language》作者Dennis Ritchie,极为经典的论文。 http://cm.bell-labs.com/cm/cs/who/dmr/chist.html

“C语言全景”这个网站内容很全面:http://www.softpanorama.org/Lang/c.shtml

Dan Saks在embedded.com上的专栏Programming Pointer ,里面文章很有深度,值得一读。

http://www.lysator.liu.se/c/c-www.html 这也是一个C语言资源汇总页面。

http://www.ioccc.org/index.html 混乱C语言代码大赛,很著名。

http://en.wikipedia.org/wiki/Underhanded_C_Contest 另外一个C语言编程大赛,主要面向黑客。

comp.lang.c以及c.moderated这两个讨论组推荐订阅,相当于互联网最大的C相关编程问题论坛:

http://groups.google.com/group/comp.lang.c

http://groups.google.com/group/comp.lang.c.moderated

这里对C语言的各种bit操作做了收集整理,不少题目在面试时候经常出现。http://graphics.stanford.edu/~seander/bithacks.html

台湾的惯C达人Jserv博客,建议大家订阅:http://blog.linux.org.tw/~jserv/

一些值得关注及研究的C语言相关项目:

TinyCC,被很多项目用作动态编译C语言的编译器引擎:http://bellard.org/tcc/

GCC的标准库实现:http://en.wikipedia.org/wiki/GNU_C_Library

Glib是GTK的底层辅助编程库,与C标准库是不一样的,在C语言上实现了面向对象机制:http://en.wikipedia.org/wiki/GLib

dietlibc在前面博客文章介绍过,C标准库的另一种实现:http://www.fefe.de/dietlibc/

一些C语言编程时可以使用的工具软件,帮你提高代码质量:

http://www.splint.org/

http://valgrind.org/

http://www.dwheeler.com/flawfinder/

PMD可用于检测重复代码 http://pmd.sourceforge.net/cpd.html

llvm的静态分析项目 http://clang-analyzer.llvm.org/

C语言编程规范编程标准:

http://en.wikipedia.org/wiki/MISRA_C

http://www.eecs.harvard.edu/~ellard/CS50-96/programming-style.html

http://developers.sun.com/solaris/articles/secure.html

cert这个文档国内有中文翻译版本:https://www.securecoding.cert.org/confluence/display/seccode/CERT+C+Secure+Coding+Standard

http://www.cs.utah.edu/dept/old/texinfo/standards/standards_toc.html

C语言编程电子书及教程:

http://publications.gbdirect.co.uk/c_book/ 这一本写的非常详细,你可以把它看成是类似谭浩强版的教科书。

http://www.knosof.co.uk/cbook/cbook.html 这一本云风曾经推荐过,相当深入的介绍了C99标准,深入细节时候需要读读。

http://www.duckware.com/bugfreec/index.html 这本书在网上流传一个中文版本,《编写优化、高效、无错地代码》,另外也有英文影印版《编程精粹》。

http://wangcong.org/blog/?page_id=196 作者王聪,也是相当hard geek,从两个样章看,包含了相当多的内容。

《C语言深度解剖》这本可以在百度文库或google搜到,可以读读,有些参考性。

《C标准和实现》作者姚新颜,他的《深度探索C、C++》算是当年比较有深度的书籍,可惜已经绝版了。这本书也可以在百度文库搜到。这本书也比较值得读。

良葛格C语言学习笔记 http://caterpillar.onlyfun.net/Gossip/CGossip/CGossip.html

C与C++的兼容性问题 http://en.wikipedia.org/wiki/Compatibility_of_C_and_C%2B%2B

另一个文档关于C与C++标准兼容性问题:http://david.tribble.com/text/cdiffs.htm

C Elements of Stylehttp://www.oualline.com/books.free/style/index.html

《Linux安全编程》http://www.dwheeler.com/secure-programs/

《C Craft》电子版 http://crypto.stanford.edu/~blynn/c/

《The function pointer tutorials》函数指针教程。http://www.newty.de/fpt/index.html

C语言编程及Unix系统调用,想用C在Unix或者Linux编程的朋友可以参考。http://www.cs.cf.ac.uk/Dave/C/

优化C、C++代码 http://www.eventhelix.com/RealtimeMantra/Basics/OptimizingCAndCPPCode.htm

图文并茂介绍C语言的指针 http://boredzo.org/pointers/

另外一篇介绍C语言优化的文章 http://www.prism.uvsq.fr/~cedb/local_copies/lee.html

一个C语言教学ppt http://www.slideshare.net/petdance/just-enough-c-for-open-source-programmers

一些Unix下C语言编程相关的文章 http://users.actcom.co.il/~choo/lupg/tutorials/index.html

Unix下如何建立静态、动态C语言函数库 http://users.actcom.co.il/~choo/lupg/tutorials/libraries/unix-c-libraries.html

如何使用GDB http://users.actcom.co.il/~choo/lupg/tutorials/debugging/debugging-with-gdb.html

一些C语言编程技巧 http://users.bestweb.net/~ctips/

Advanced C programming,高级C语言编程,可以提高水平,非常有帮助 http://www.mpi-inf.mpg.de/departments/rg1/teaching/advancedc-ws08/literature.html

C语言问答,这些题目也可用于面试 http://www.gowrikumar.com/c/

初学C编程(1)

a+++++b这个表达式如何理解?应该理解成a++ ++ +b还是a++ + ++b,还是a + ++ ++b呢?应该按第一种方式理解。编译的过程分为词法解析和语法解析两个阶段,在词法解析阶段,编译器总是从前到后找最长的合法Token。把这个表达式从前到后解析,变量名a是一个Token,a后面有两个以上的+号,在C语言中一个+号是合法的Token(可以是加法运算符或正号),两个+号也是合法的Token(可以是自增运算符),根据最长匹配原则,编译器绝不会止步于一个+号,而一定会把两个+号当作一个Token。再往后解析仍然有两个以上的+号,所以又是一个++运算符。再往后解析只剩一个+号了,是加法运算符。再往后解析是变量名b。词法解析之后进入下一阶段语法解析,a是一个表达式,表达式++还是表达式,表达式再++还是表达式,表达式再+b还是表达式,语法上没有问题。最后编译器会做一些基本的语义分析,这时就有问题了,++运算符要求操作数能做左值,a能做左值所以a++没问题,但表达式a++的值只能做右值,不能再++了,所以最终编译器会报错。

阅读全文