C语言学习笔记

#一些工具
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 = &num;   //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),会再次要求用户输入数据。
                                                                                                                                                                                                   

你可能感兴趣的:(C语言学习笔记)