#一些工具
Valgrind : 用于内存调试,内存泄露检测以及性能分析的软件开发工具。
火焰图 : 检查方法快慢
ICU : http://site.icu-project.org/ 编码转换
#命令式编程
命令式编程使用指令式的风格来写,比如运算语句、循环判断等语句.
早期的命令式编程都是计算机本身的机器语言。这些语言指令非常简单
后来才有了允许变量、复杂表达式等的出现。
像C、Java等大部分都是按命令式的编程风格在编程,只不过一个是面向
过程,一个是面向对象。
#函数式的编程
也是一种编程风格,主要的思想是把运算尽量写成一系列的函数调用。
例如这样一个数学表达式:
(1+2) * 3 - 4
传统的命令式面向过程的编程会这样编写:
int a = 1 + 2;
int b = a * 3;
int c = b - 4;
函数式编程风格像这样:
int result = subtract(multiply(add(1,2)),4);
函数式编程强调程序的执行结果比执行过程更重要,倡导利用若干简单
的执行单元让计算结果不断渐进,逐层推到复杂运算,而不是设计一个复
杂的执行过程。
#函数式编程的特点
1.函数是第一类对象(First-class object)和高阶函数
第一类对象:
又称第一类公民(First-class citizen),指函数与其他数据
类型一样,地位平等。一般特性如下:
可被存入变量或其他结构
可被作为参数传递给其他函数
可被作为函数的返回值
可以在执行期间创造,而无需在设计期全部写出
即使没有被关联到某一名称也可以存在
高阶函数,至少满足下列一个条件的函数:
接受一个或多个函数作为输入
输出一个函数
2.只用表达式,不用语句
expression是一个单纯的计算过程,总有返回值;语句是执行某种操作,没有
返回值。
函数式编程一开始就是为了处理运算,不考虑系统的读写(I/O)。实际应用中
不做I/O是不可能的,因此编程过程中,函数式编程只要求把I/O限制到最小,
保持计算过程的单纯性。
3.没有副作用(side effect)
函数要保持独立,所有的行为只是返回一个新值,不会修改外部变量的值,每个
函数都具有幂等性。
#纯函数式编程语言
#强静态类型
Hashell
Miranda
Concurrent Clean
#弱类型
Lazy K
#非纯函数式编程语言
#强静类型
F#
ML
OCaml
Scala
#强动态类型
Erlang
LISP
R
LOGO
Scheme
Clojue
Mathematica
#弱类型
Unlambda
动态类型语言是在运行是确定数据类型的语言.
变量使用之前不需要类型声明,通常变量的类型就是被赋值的类型.
#运算符及其优先级
--------------------------------------------------------------
顺序 运算符 说明
--------------------------------------------------------------
1 ()
[]
. 按对象选择成员
-> 按指针选择成员
2 + - 一元加和一元减,如-89
++ -- 前缀递增和前缀递减
! ~ 逻辑非和按位补
* 取消引用(也称为间接运算符)
& 寻址
sizeof
(type) 强制转换为type,如(int)
3 * / %
4 + -
5 << >>
6 < <= > >=
7 == !=
8 & 按位与
9 ^ 按位异或
10 |
11 && 逻辑与
12 ||
13 ?: 条件运算符
14 = += -= /= *=
%= <<= >>= &=
|= ^=
15 , 逗号运算符
---------------------------------------------------------------
#三字母转义序列
??= #
??/ \
??' ^
??( [
??< {
??! |
??) ]
??> }
??- ~
#极限值
<limits.h>: 定义了数据类型的极限值
类型 下限 上限
int INT_MIN INT_MAX
long LONG_MIN LONG_MAX
<float.h>: 定义了浮点数的极限值
float FLT_MIN FLT_MAX
double DBL_MIN DBL_MAX
#C语言的几种基本数据类型
char 字符型,一个字节
int 整形,通常反映所用机器中整数的最自然长度,至少16位
float 当精度浮点
double 双精度浮点
#限定符
short
long
signed
unsigned
#各个数据类型占用的字节数
---------------------------------------------------------------
类型名称 字节数 后缀
---------------------------------------------------------------
char 1 /
short int 2 /
int 2/4 /
long int 4 L
long long int 8 LL
unsigned char 1 /
unsigned short int 2 /
unsigned int 2/4 U
unsigned long int 4 UL
unsigned long long int 8 ULL
float 4
double 8
long double 12
unsigned float 4
unsigned double 8
unsigned long double 12
--------------------------------------------------------------
#字符转大小写
<ctype.h> 标准库
toupper()
tolower()
#枚举(是一个整数类型)
定义一个枚举,默认每个枚举常量的值是从0开始,依次加1的整形。每个枚举常量的名称是
唯一的,但常量的值可以相同:
enum Weekday {Monday,Tuesday,Wednesday,Thursday,Firday,Saturday,Sunday};
该定义的枚举常量值为0~6
enum color {red=1,orage,yellow=1,green}
常量值分别为(1,2,1,2)
定义完后就可以用指定的枚举常量为枚举赋值了
声明一个枚举变量并赋值
enum Weekday var = Tuesday;
enum Weekday var = 1;
注意:可以给枚举类型指定一组可能的值,但没有检查机制来确保程序只使用这些值.
所以对于 enum Weekday var = 123;也是可行的。
#C11标准的可选函数的使用
实现C11标准可选函数的编译器都会定义__STDC_LIB_EXT1__符号,所以可以根据
该符号来判断是否支持可选函数:
int main(){
#if defined __STDC_LIB_EXT1__
printf("支持");
#else
printf("不支持");
#endif
}
如果支持且要使用string.h中的可选函数,必须在string.h前定义__STDC_WANT_LIB_EXT1__符号
为1才能真正的使用:
#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>
注意:C11的可选函数都以_s结尾
#const用该关键字修饰的变量是只读的(但不是常量)
const int a = 3; //对该变量修改会报错
const char str[] = "hello";
str[0] = 'a'; //错误,变量的内容是只读的
const char *str = "hello";
str = "abc"; //正确,没有修改变量的内容
'\000' 用八进制表示一个字符 如'\013'
'\xhh' 用十六进制表示一个字符 如'\xb'
#按位运算符
& 按位与
| 按位或
^ 按位异或
<< 左移
>> 右移 根据编译系统有以下两种右移方式:
1.逻辑右移(高位补零,无符号右移)
将一个数字转为无符号数据,可确保无符号右移
例如:
int a = -123;
uint_t b = (uint_t)a;
b = b >> 3; //如此,则一定是无符号右移,同java >>>符号
2.算数右移(高位补符号位,有符号右移)
~ 按位求反
#if语句
if (){
...
}else if(){
...
}else{
...
}
#switch语句
switch(num){
case 35:
...
break;
default:
...
break;
}
#C语言没有指定同一运算符的计算顺序
例如x = f() + g(); 两个函数的计算顺序依赖于具体的编译器
#C语言也没有指定函数参数的求职顺序
printf("%d %d\n",++n,power(2,n)); 不确定++n先执行还是pwoer()先执行
a[i] = i++; 存在同样的问题
#函数声明又称函数原型
就像普通变量一样,函数在使用之前也需要先声明;
函数声明的时候只需要使用函数的签名并在尾部加上分号,如:
int abc(int a,int b); //一个函数声明
函数声明的时候,参数名不需要和函数定义的参数名相同,甚至不需要在函数声明中
包含参数名,如:
int abc(int,int); //不包含参数名的函数声明,但是参数的个数和类型要和定义的一直
函数原型的作用域是,从其声明处开始一直到源文件的结尾。
#extern修饰函数和变量
平常如果我们要使用某个函数,需要将包含这个函数的.h头文件include过来,如果
用extern修饰的函数是表示被修饰的函数定义在当前文件外,这样就不用include
这个.h头文件了
例如:
//--------文件hello.c-----------------
#include <stdio.h>
void hello(void){
printf("hello\n");
}
//-------------------------------------
//---------文件main.c-------------------
//声明这是一个外部函数,编译的时候不会检查是否存在
//只有在连接的时候才会用到它
extern void hello(void);
int main(void){
hello();
return 0;
}
//--------------------------------------
我们编译并连接这两个c文件
$gcc -o main hello.c main.c
执行它:
$sh main //执行
$hello //输出hello
注意:如果hello()函数是static修饰的,则表示只有在定义它的源文件中
才能使用和函数声明,在其他文件中是不能声明也不能用extern修饰的。
需要注意的是,extern关键字只表示声明变量或函数,并不是去定义它,意思就是
告诉你有这么一个东西,它在哪儿,具体是什么值是不知道的。
例子:
//-------------main.c----------
#include <stdio.h>
//生明这是一个外部变量
extern int aaa;
int main(){
printf("%d\n",aaa);
}
//------------------------------
//-----------aaa.c-----------
int aaa = 555;
//---------------------------
编译并链接
$gcc main.c
会出现类似"_aaa", referenced from:这样的报错信息。原因是编译通过了,
但是在链接的时候链接器找不到关于aaa变量的定义,所以出错。所以链接的
时候需要aaa.o目标文件。
#函数隐式声明
如果函数没有原型声明,那么函数会在第一次出现的地方被隐式声明。
被声明的函数返回值被假定为int型,但不对参数做类型假设。
#函数的声明和定义必须一致,如果声明和定义在一个文件,则编译器会检查到是否一致。
如果不在同一个文件,则无法检测到,这件会导致声明是int,定义且是其他类型(如double).
所以函数的声明和定义最好在一个文件中。
#函数声明的一个例子
int sum;
//cc 这函数第一次出现,所以隐式声明为 int cc();
//不对参数做假设.所以输入什么参数都可以.
//注意:int cc(); int cc(void); 这两种声明是不一样的,
//int cc()会把后边出现的第一个int cc(...){}视为定义
//int cc(void) 只会把 int cc(){} 视为定义
sum = cc("2.4");
1) int cc(char a[]){}
//如果1) 随后首先出现,那么1) 将被视为函数定义
2) int cc(char a[], char b[]){}
//如果2) 随后首先出现,那么2) 将被视为函数定义
注意:1)和2)不可同时出现,因为名字相同,所以视为重复定义
3) double cc(char a[] , char b[]){}
//如果3) 随后首先出现,那么3) 被视为重复声明, 并不会把这个函数看成一个定义。
//因为前面隐式声明的类型是int,而这里是一个double
//类似这样:int cc(); double cc();
注意:函数声明时按照名字来确定是否已经声明;按照声明的类型和名字
确定是否定义了函数。C语言中不会存在同名的函数。
#声明指针变量和普通变量
int a,*b,c;
其中a,c为普通变量。b是指针变量。
#指针和指针变量
int* x: //int型指针
void* y
任意类型指针,实际用的时候可以强转成具体的类型指针。void*类似于java中的Object
//为指针分配空间并使用
x = malloc(sizeof(int));
*x = 5;
y = malloc(sizeof(int));
*y = 5; //错误,void*类型的指针使用时需要转换成具体类型
x = (int*)y;
x = 5; //正确
#include <stdio.h>
main(){
int x=2;
int* y = &x; //y是一个整型指针(地址)变量,&x是一个整型指针(地址) *y是y这个地址所指向的内容,是个整型.
printf("%d\n",y); //表示地址
printf("%d\n",&x); //表示地址
printf("%d\n",&(*y)); //表示*y这个整型的地址
printf("%d\n",*y); //内存地址指向的内容
printf("%d\n",*(&x)); //内存地址指向的内容
}
#指向常量的指针
不能通过指针的方式,去改变'指针指向'的值;
例子1:
int value = 999;
const int *pv = &value;
这是一个指向常量的指针,也就是说不能通过pv来改变pv指向的值
*pv = 888; //这是错误的
但是可以通过value进行任意更改
value = 777; //这个是可以的,因为没有通过指针去改变这个值
例子2:
int num = 888;
pv = # //pv指向的地址可以改变
指针变量的地址可以改变,但是仍然不能使用指针来改变它指向的值
*pv = 666; //仍然是错误的
num = 555; //Ok
#常量指针
指针中存储的地址不能改变,指向的值可以改变
例子:
int count = 56;
int *const pv = $count; //定义个常量指针并赋值
如果试图改修改指针存储的地址,则会报错
pv = &count; //编译错误,pv是一个只读的变量
#指向常量的常量指针
就是将上面两个概念结合起来:
指针地址不可修改;
不能通过指针的方式去改变它指向的值;
例如:
int value = 30;
const int *const pv = &value; //指向常量的(const int) 常量指针(*const pv)
pv = &value; //编译错误,该指针不可修改(常量指针)
*pv = 89; //编译错误,不能用指针的方式修改其指向的值
value = 20; //合法,因为value是可修改的
#const修饰符的总结
const修饰符仅作用于紧跟其后的类型或变量
#1.当const修饰类型时,表示指针最终指向的值是一个常量,例如:
const int a = 4;
const int *b = &a;
const int **c = &b;
a = 1; //错误,a是个常量
*b = 1; //错误,b指向的值是一个常量
**c = 1; //错误,c指向的是一个指针n,指针n指向的是一个常量
b = &a; //正确,因为指针b不是常量
c = &b; //正确
#2.当const修饰变量时,表示这个变量是一个常量
#例子1: const前面没有*号
int const a = 4;
int const *b = &a;
int const **c = &b;
a = 1; //错误,a是一个常量
*b = 1; //错误,*b 是一个常量
**c = 1; //错误,**c 是一个常量
#例子2: const前面有*号
int t = 4;
int *const a = &t; // *a
int *const *b = &a; // **b
int *const **c = &b; // ***c
a = &t; //错误,a是一个不可变的变量; 这个变量是一个指针
*b = &a; //错误,*b是一个不可变的变量; *b指向的是一个指针
**c = &b; //错误,**c是一个不可变的变量; **c指向的是一个指针
*c = b; //正确,因为const只说**c是不可变的; *c和b都是指向指针的指针
#总结:
当向指定某个指针是变量的时候,需要在const前面加*号;
比如 *****p 这样一个多层级的指针,下面几种写法分别对不同层级的指针进行常量化:
int *const ****p; //****p 这一层的指针不可变
int **const ***p; //***p 这一层的指针不可变
int ***const **p;
int ****const *p;
int *****const p; //这个等同 const int *****p; 都是指定最终的变量值不可通过指针p修改
#更为复杂一点的例子
const int *const ****p; //****p 这一层的指针不可变,且最终指向的变量不可变,即*****p整形值不能变
const int **const ***p; //***p 这一层不变,且最终指向的变量不可变
const int ***const **p;
const int ****const *p;
const int *****const p; //等同 const int *****p; int const *****p;
#函数指针
int (*pfun) (int,int); //声明 pfun是一个指向 int xxx(int,int) 函数的指针
int (*pfuns[10]) (int,int); //函数指针数组
int sum(int a,int b){};
pfun = sum;
pfuns[0] = sum;
pfun(1,3);
pfuns[0](3,5);
#函数的调用方式 函数指针和函数指示符
int test(){}; //test是一个函数指示符
int (*pfunc)(int,int); //pfunc是一个函数指针
其中pfunc是一个指向函数的指针,*pfunc就是这个函数,按照这种解释则
我们在使用函数指针调用函数的时候应该使用 *pfunc 来调用函数,像这样
int c = (*pfunc)(5,6)
历史上,贝尔实验室的C和Unix的开发者使用的就是这种观点,即
(*pfunc)(5,6)
而Berkeyly的Unix的扩展这采用函数指针的形式对其调用,即
pfunc(5,6)
标准C为了兼容性,两种方式都接受。
实际上,编译器在内部都把器转换成了函数指针的形式来使用,这就是默认的function-to-pointer转换。
也就是说内部其实使用的是pfunc(5,6)这种形式,那么 (*pfunc)(5,6)之所以可以使用,是因为编译器
把 *pfunc 这个函数指示符(代表某个函数)按照function-to-pointer规则,转换成了函数指针类型pfunc。
所以依次类推 *(*pfunc)(5,6) 最终也会转换成 pfunc(5,6)的方式进行调用。
另外前面提到的test函数可以这样调用
(*test)()
因为按照function-to-pointer规则,test函数类型被编译器转换成了函数指针(这里可以认为test变成指针了)
那么在函数指针前面加上*则代表这个函数,即*test在编译器内是个函数类型,最后又根据func-to-pointer
将其转换为函数指针来调用,最终test()和(*test)()等价。
#结束程序的函数
abort(): 该函数会清空输出缓冲区,关闭打开的流,是否真的这么做取决于实现.
exit(int): 该函数正常结束程序,清空所有缓冲区,并将其写入目的地,关闭所有
打开的流,将传给函数的值返回给主机环境.
atexit(void xxx(void)); 该函数接收一个函数参数,总共可以注册32个,注册成功
则返回0。
当exit被调用的时候,会从最后一次注册的函数开始,顺序地执行这个注册的函数.
例子:
void clean(void);
int main(void){
atexit(clean); //注册exit退出是要执行的函数
printf("world ");
exit(0);
}
void clean(viod){
printf("hello\n");
}
程序输出结果:
world hello
_Exit(int)
quick_exit()
at_quick_exit(void xxx(void))
#内联函数声明
inline void add(int a,int b){}
用inline修饰函数,表示其他方法调用该函数时,可以直接内联.
#restrict关键字
void sum(char *restrict s1,int a){}
用该关键字告诉编译器,在函数体中,s1引用的字符串仅通过s1这个指针来引用,
编译器可以优化这个s1相关的代码。
#stdnoreturn.h/_Noretrun函数限定符
_Noreturn void end(void){ //指定该函数永远不会返回
exit(EXIT_SUCCESS);
};
#用stdlib.h/malloc()函数分配内存
分配可以容纳25个int型的内存
int *pv = (int *)malloc(25*sizeof(int));
if(pv == NULL){
printf("分配内存失败");
}
#stdlib.h/free()释放内存
释放的地址必须是分配时的地址,否则结果是不确定的,假设pv是分配是的地址
free(pv);
pv = NULL;
#stdlib.h/calloc()分配内存
该函数分配的内存会把所有的位初始化为0,例如分配25个int大小的内存
int *pv = (int *)calloc(25,sizeof(int));
if(pv == NULL){
printf("内存分配失败");
}
#stdlib.h/realloc()重新分配内存
该函数可以扩展以前用malloc()、calloc()、realloc()分配的内存.
两个参数:
一个是以前有上面三个函数返回的指针地址;
另一个是要分配的新内存的字节数,该值可以大于之前的值,也可以小于之前的值.
当扩大一块内存空间时,realloc()视图直接从原空间后面获取连续的附加字节,如果
可以满足则直接扩大空间,否则就需要重新分配一块新的内存,并把原数据考过来.
例子:
#include <stdio.h>
#include <stdlib.h>
int main(){
int *pv = (int *)realloc(NULL,5*sizeof(int));
printf("%p\n",pv); //新分配的内存
int *npv = (int *)realloc(pv,10*sizeof(int));
printf("%p\n",npv);//如果pv后面有连续的5个int型大小的字节,则npv等于pv
int *nnpv = (int *)realloc(npv,8*sizeof(int));
printf("%p\n",nnpv);//缩小内存,所以nnpv等npv
}
realloc()失败时返回NULL,并且原来的内存不变,不释放也不移动
如果第一个参数是NULL,那么它就等同于malloc()函数:
int *npv = (int *)realloc(NULL,50*sizeof(int));
如果第一个参数不是NULL,也不是之前分配的地址,或者指向已释放的内存,
则结果是不确定的。
#C中变量作用域
连接器如何解析多重定义的全局符号
强符号:函数和以初始化的全局变量
弱符号:未初始化的全局变量
1.不允许有多个相同的强符号
2.如果有一个强符号和多个弱符号,那么选择强符号
3.如果有多个相同弱符号,那么从这个弱符号中任意选择一个
在函数外的是全局变量,相同的强符号的全局变量名在整个程序中只能出现一次,
不管这个程序是在一个文件中,还是分散在多个文件中的。
在函数内的是局部变量,函数内的和函数外的相同名字的变量不会冲突。
如果函数内要访问外部变量,如果在同一个文件内,且函数的位置在外部
变量的后面,则可以访问,否则需要用extern进行声明。
#static修饰符可以将变量的访问范围锁定在文件内或函数内,并且该变量一直占用空间。
如果变量 static int sp = 0;在某个文件内,则其他任何文件都无法访问该变量;如果在
函数内,和其他变量的区别是,函数结束后该变量不释放空间和值.
static修饰的变量只会初始化一次,第二次进入改变量的时候不会再次赋值,例如:
int main(void){
for(int i=0; i<10; i++){
static int a = 0;
a = a + 1;
printf("%d\n",a);
}
}
代码输出结果:
--> 1
--> 2
...
--> 10
#宏定义
#define 名字 替换文本
宏定义只是做简单的文本替换:
#define max(A,B) A * B
使用宏max看起来很像是函数调用,但宏调用直接将替换文本插入到代码中。
形式参数的每次出现都将被替换成对应的实际参数。因此,语句:
x = max(a+b,c+d);
将被替换为:
x = a+b*c+d;
在替换文本中,如果参数名以#作为前缀,则参数将被替换成加双引号的字符串
例如语句:
#define print(expr) printf( #expr "=%g\n",expr)
使用语句
print(x/y);
调用时将被替换为:
printf( "x/y" "=%g\n",x/y);
使用语句
print("x/y");
调用时将被替换为:
printf( \""x/y"\" "=%g\n",\"x/y\");
##运算符用于连接两个实际的参数,如:
#define aa(a,b) a ## b
调用语句
aa(name,age);
将被替换为:
nameage; //去掉了##前后的空格
条件包含
defined(exp),如果exp已经被定义,其值是1,否则是0
例句:
#if !defined(HDR) //如果没有定义HDR
#define HDR "aaa.h"
#elif SYSTEM == SYSV //如果系统变量SYSTEM是SYSV
#define HDR "bsd.h"
#else
#define HDR "default.h"
#endif
#include HDR
C语言专门定义的两个预处理命令
#ifdef //如果定义了
#ifndef //如果没定义
宏定义和类型定义例子:
#include <stdio.h>
#define aa int //预处理的时候使用,遇见aa直接替换为int
typedef int bb; //运行时候使用?编译时
int main(int argc, char **argv){
aa cc = 9; // 用预处理声明的int变量
bb hh =10; // 用类型定义声明的int变量
printf("%d\n",hh);
}
C数组指针
int bb[13];
bb是一个指针,但不是指针变量;bb这个指针指向的是一个int型;
&bb和bb的值一样,但意思不一样,&bb指向的是一个长度是13的int型的数组;
所以,如果int占四个字节,那么 bb+1 跨4个字节; (&bb)+1 跨13*4个字节;
int *bb[13]
bb是一个指向指针的指针,数组里面的值bb[i]是一个指针,这个指针(bb[i])指向一个int型;
int (*bb)[13]
bb指向的是一个长度是13的int型的数组;
bb+1 的地址跨度是13*4个字节;
#C语言结构体
定义结构:
struct Point{ //定义了一个叫Point的结构
int x;
int y;
};
声明一个结构变量
struct Point cc; //声明了一个cc变量,这个变量是一个结构,这个结构名字是Point;
使用结构:
cc.x=12; //为结构成员变量赋值
cc.y=15;
匿名结构
struct {int x; int y;} a={3,4},b={7},c;
上面这条语句声明了三个变量,每个变量都是一个结构体,结构体中有两个整形的成员变量;
初始化结构体:
struct Point point = {15,20}; //需要和结构成员顺序一致
struct Point point = {.y=20,.x=15}; //不需要顺序一致
//-----例子---------
typedef struct Point Point;
struct Point{
int x;
int y;
int z;
};
int main(void){
Point p1 = {1,2};
Point p2 = {.y=2,.x=1};
printf("%d %d\n",p1.x,p1.y);
printf("%d %d\n",p2.x,p2.y)
}
C结构和函数
结构在函数传递的过程中仍是按值传递:
#include <stdio.h>
struct point {
int x;
int y;
};
struct point addpoint(struct point p1){
//结构的传递仍然是传值,所以这里的p1是传进来p1的一个副本
//在这里对p1的任何操作,不会对外部操作产生影响
p1.x= p1.x+1;
p1.y= p1.y+1;
return p1;
}
void addpoint_pointer(struct point *p1){
//p1是一个指向结构体的指针,*p就是这个结构体
(*p1).x = (*p1).x +1;
//为了方便,C语言提供了一种结构指针简写的方式,
//可以用 p1->x 代替 (*p1).x
p1->y = p1->y +1;
}
//测试结构传递
main(int argc, char* argv){
struct point a;
a.x=3;
a.y=4;
addpoint(a);
//结构a中成员变量值不变, 保持x=3,y=4
printf("%d %d\n",a.x, a.y);
addpoint_pointer(&a);
//结构a中成员变量改变, x=4,y=5
printf("%d %d\n",a.x, a.y);
}
位字段
为了节省空间可以使用一个char或int对象中的位,来标记某个变量;
例如:某一位是1表示存在某个变量,是0表示不存在等。
定义一个屏蔽码集合 KEY=01 EXT=02 STATIC=04
用一个char长度的二进制表示是这样:
0000 0111 这就表示KEY EXT STATIC都存在
C语言提供了另一种替代的方法,直接定义位字段
struct flags{
unsigned int is_key:1; //位字段宽度是1 可以表示 0,1
unsigned int is_ext:3; //为字段宽度是3 可以表示 000, 001, 011,101等
unsigned int is_sta:6; //位字段宽度是6 可表示 000000, 000001, 000111等
};
struct flags cc;
cc.is_key=0;
cc.is_ext=5; //小于2的三次方
C实现的头文件,在unix系统中一般放在 /usr/include 目录中
#C文件操作
FILE 文件结构,包括:缓冲区位置,缓冲区中当前字符的位置、文件的读写状态、
是否出错或是否已经到达文件结尾等。
FILE *fp; //定义一个执行FILE结构的指针fp
打开一个文件
fp = fopen(char *name, char *mode);
//name是文件名
//mode是读("r")、写("w")、追加("a")、二进制("b")
关闭一个文件
fclose(fp);
从文件读一个字符
int getc(fp); //返回读到的字符,如果结束或出错返回EOF
向文件写一个字符
int putc(c,stdout);//将字符c写入到标准输出,并返回写入的字符,如果出错返回EOF
如果流fp中出现错误,则函数ferror返回一个非0值
int ferror(FILE *fp);//比如输出到磁盘时,磁盘满。
如果文件fp到达文件尾,返回非零值
int feof(FILE *fp);
readfile.c
#include <stdio.h>
//读文件
main(int argc ,char *argv[]){
FILE *fp; //指向文件的指针
void filecopy(FILE *infp,FILE *outfp);
if(argc !=2){
printf("命令错误,请输入文件名!\n");
return 1;
}
fp = fopen(argv[1],"r"); //打开一个文件
if(fp == NULL){ //如果发生错误,fopen返回NULL
printf("文件:%s打开错误!\n",argv[1]);
return 1;
}
int c;
while((c=getc(fp)) != EOF){ //从文件fp中读一个字符
putc(c,stdout); //向文件stdout中写入一个字符
}
fclose(fp); //关闭文件,每个操作系统对程序打开的文件个数都有限制
return 0;
}
行输入和行输出
//从fp指向的文件中读取一行数据(包括换行符)
//line 将读取的数据结尾加上'\0'放入line中
//maxline 读取字符的个数,最多maxline-1个字符?
//返回值 正常情况下返回line,发生错误或到文件结尾,则返回NULL
char *fgets(char *line, int maxline, FILE *fp)
//将字符串line 写入到文件fp中
//正常返回非负值,错误返回EOF
int fputs(char *line, FILE *fp)
函数gets和puts功能与fgets和fputs函数类似,但他们是对stdin和stdout
进行操作。gets在读取字符串时,将删除结尾的'\n';而puts在写入时,将加
上一个'\n'。
#系统接口open、read等和C标准函数库fopen,fgets等函数的区别
fopen是ANSIC标准中得C语言函数,在不同的系统中(window或linux)调用不同
的api。在linux中open是系统函数,fopen是其封装函数,fopen最终还是要调用
底层系统的open函数。
但是fopen函数在window的实现时就需要调用window的系统函数。
linux中open、read等系统函数被称为无缓冲I/O函数,用这个函数读写每次都
要进内核,调用一个系统调用比调用一个用户空间的函数要慢很多,C标准I/O库
函数会再用户空间开辟I/O缓冲区,省去了自己管理I/O缓冲区的麻烦。
空间分配函数
//分配n个字节的空间
void *malloc(size_t n)
使用:
int *a = (int *)malloc(sizeof(int));
*a = 9;
//分配n*size个字节的空间
void *calloc(size_t n, size_t size)
使用:
int *p = (int *)calloc(3,sizeof(int));
*p = 3;
*(p+1) = 5;
*(p+2) = 8;
UNIX系统接口
任何时候对文件的输入输出都是通过文件描述符表示文件,不是通过文件名。
操作系统在打开一个文件的时候,会返回一个小的非负整数,这个整数就是文件描述符。
0、1、2分别代表标准输入、标准输出、标准错误输出;任何程序在运行的时候,都会
打开这个三个文件。
系统接口read和wirte
//fd 文件描述符
//buf 存放读到的字符
//n 指定要读多少个
//n_read 实际读了; 0 表示已到达文件结尾,-1 发生了某种错误
int n_read = read(int fd, char *buf, int n);
//fd 要写入的文件描述符
//buf 要写入的字符
//n 指定要写多少个
//n_writen 写了多少个; 如果n和n_writen不相等,表明发生了错误
int n_writen = write(int fd, char *buf, int n);
例子:将输入写到输出
#include <syscall.h>
main(){
char buf[BUFSIZ];
int n;
//0 是标准输入的文件描述符
while( (n=read(0, buf, BUFISIZ)) > 0 ){
//1 是标准输出的文件描述符
write(1, buf, n);
}
}
系统接口open、creat、close、unlink
//该函数与标准库函数fopen很相似,不同的是该函数返回的是文件描述符(int类型),
//fopen返回的是一个文件指针(FILE *fp)
//name 要打开的文件名
//flags 打开方式,int类型; O_RDONLY 只读方式, O_WRONLY 只写方式, O_RDWR 读写方式
O_DIRECT 绕过pagecache,直接将数据写入到磁盘
//perms
//fd 返回值,如果发生错误,返回-1
int fd = open(char *name, int flags, int perms);
//创建一个文件,若存在则覆盖
//name 要创建的文件的名字
//perms 创建的文件的权限,如0755
//fd 创建的文件描述符,若创建失败则放回-1
int fd = creat(char *name, int perms);
//断开文件描述符和已打开文件之间的关联
//一个程序同时打开的文件数通常是20个
close(int fd);
//通过文件名删除文件
//该函数和标准库中的remove对应
unlink(char *name);
例子:文件复制
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#define PERMS 0666 //对于所有者、所有者组和其他成员均可读写
//系统调用 f1复制到f2
main(int argc, char *argv[]){
int f1,f2,n;
char buf[BUFSIZ];
if(argc != 3){
printf("参数需要两个文件f1 to f2\n");
exit(1);
}
if((f1 = open(argv[1], O_RDONLY, 0)) == -1){
printf("不能打开文件 %s\n",argv[1]);
}
if((f2 = creat(argv[2], PERMS)) == -1){
printf("不能创建文件 %s, mode %03o\n",argv[2],PERMS);
}
while( (n=read(f1,buf,BUFSIZ)) >0 ){
if( write(f2,buf,n) != n){
printf("写文件错误 %s\n", argv[2]);
}
}
return 0;
}
随机访问文件
//fd 文件描述符
//offset 要设置的文件描述符fd的当前位置
//origin 0 offset基于文件开始位置; 1 offset基于文件当前位置; 2 offset基于文件结束位置
//返回值 表示文件的新位置,如果错误返回-1
long lseek(int fd, long offset, int origin);
标准输入stdin是有缓冲区的,比如标准函数 getc(stdin);
例如程序:
int c = getc(stdin);
c = getc(stdin);
第一次访问的时候,因为还没有缓存区,所以会先创建缓存区,再调用系统函数read填充次缓冲区,
这时会等待用户输入数据,比如输入为"abcdef",则会返回第一个字符'a'。
第二次访问的时候,因为缓冲区里面已经有数据了,所以会从缓冲区中取出第二字符'b'。
当缓冲区中的数据被全部取出后,如果再次调用函数getc(stdin),会再次要求用户输入数据。