C-Tips

(欢迎纠错)

1 基本概念

  • CPU:运算器+控制器(向内储存器发送地址流数据)

  • 1 Byte=8 Bit1 K=1024 Byte,每个Byte有一个唯一的地址,一个指针4 Byte

  • 预处理:.c->.i 编译:.i/.c->.s 汇编:.s->.o 链接:.o->可执行程序。通常预处理器与编译器集成在一起

  • 编译:转化成目标代码,但还需要标准启动代码库代码合并起来(链接)才能形成可执行代码

  • 小端存储法: 高地址存高字节、低地址存低字节;(手机ARM、ipad、x86结构、intel/ARM和DSP...)(IBM大型机、网络字节序用大端存储法)

  • Gcc常用编译选项: -Wall 生成警告信息(配合-O使用),-W 额外的警告信息,-pedantic 根据C标准生成警告、避免使用非标准特性, -ansi 禁用非标准C特性,-std=c99 指定编译器版本

  • 泛型编程(generic programming),指没有特定类型、但一旦指定一种类型就可以转换成指定类型的代码

2 格式化输入输出

  • printf的转换说明: %(-)m.pX m为要显示字符最少数量(不够会自动扩充)、p为精度,有负号时左对齐(空格会放在右边、+表示始终加正负号);常用转换说明符: d 十进制整数,e 指数,f 十进制浮点数,g 指数或浮点数、适合范围变化大或无法预知大小的数

  • 读取double%lf,显示用%f

3 表达式与循环

  • 运算符优先级: 最高的是非正式意义运算符(数组下标、成员选择等)、其次是单目运算符(括号>++>*),双目运算符(算术>移位>关系>逻辑>赋值>条件(a?b:c)),最后是逗号运算符

  • 注意各种运算符的优先级、结合性,但没必要记

  • %取余数只用于整数,其他的用fmod函数

  • x+=yx=x+y的区别: 后者对x进行了两次求值,可能引起的副作用也都会出现2次

  • while(i++ < n) 这种写法使程序更简洁

  • 形式参数:formal parameter,实际参数:actual argument

  • switch语句记得加break,否则会从匹配标签开始执行到switch末尾

  • 可以接受的一种goto用法: 出现问题时想要从一组循环嵌套中跳出(用break只能跳出一个循环,比较麻烦)

    goto label;
    ...
    label: statement;
  • 把常量放在判断相等的比较表达式左侧,减少出错:
    while('\t'==c||''==c||'\n'==c){;}   //一旦出错会马上捕获

4 基本类型

  • C99的<stdbool.h>提供了bool宏:bool flag=true; 这样使用

  • typedef可提高程序的可移植性

  • sizeof(short) 函数返回size_t类型,是无符号整型unsigned int的别名。相比于strlen()sizeof把字符串末尾不可见空字符也计算在内

  • 指针的类型:决定指针移动一位时的距离(sizeof(指针类型)

  • sizeof(a=b+1)并没有向a赋值

5 数组

  • 获得数组长度:sizeof(a)/sizeof(a[0]), 注意返回的是一个无符号数

  • 复制数组的一个方式: memcpy(a,b,sizeof(a)),来自<string.h>头的内存复制函数

  • 复合字面量建立无名数组: (int []){1,2,3,4,5};

  • 一维数组中:&a[i]等价于a+i,二维数组中:&a[i][j]等价于a[i]+j

  • 一维数组a[i]中,&a+1代表跨越整个数组的长度,不同于a+1(跨越数组一个元素的长度)

  • 绝大多数情况下,数组名的值是指向数组第1个元素的指针,除了两个例外: sizeof返回整个数组占用的字节、&返回一个指向数组的指针

  • 多维数组作为函数参数:

    int mat[3][4];
    void func(int (*mat)[4]);  //指向数组的指针
    //也可以写成:  void func(int mat[][4]);
    //但不能写成:  void func(int **mat);
    func(mat);
  • 多维数组定义:

    //正确:
    int a[][3]={{1,2,3},{4,5,6}};
    //报错:
    int a[2][]={{1,2,3},{4,5,6}};
    //只有第一维能根据初始化列表缺省地提供,其余的必须显式写出

6 函数

  • exit导致程序退出,return导致函数退出

  • return 0; 控制权交给上一级(直到最初一级、结束程序); exit(0); 直接结束程序

  • 函数名(不带括号)返回该类型函数的地址

  • 递归的工作原理: 函数调用时、变量存在堆栈上,每一次递归、之前调用的变量仍留在堆栈上但不能被访问,直到函数返回前一次调用

  • 递归往往是用简洁性弥补其运行开销(冗余计算增长极快),一些情况用迭代实现更好

  • 调用在后面定义的函数,必须在调用之前声明其函数原型;函数定义时结尾可不加分号、但声明结束时必须加,推荐全都加

  • stdarg实现可变参数:

    #include <stdarg.h>
    double avg(int n, ...) {  // n为省略号中参数个数
    va_list var_arg;        //定义一va_list类型
    int count;
    double sum = 0;
    va_start(var_arg, n);  //初始化va_list
    for (count = 0; count < n; count++) {
     sum += va_arg(var_arg, double);  //按顺序读取va_list中的参数,注意类型一定要一致
    }
    va_end(var_arg);  //结尾要调用
    return sum / n;
    }
    avg(3,1.2,2.3,3.4); //使用方式

7 指针

  • 二维数组中aa[0]的区别: 类型不同,前者为(拿int举例)int (*)[num_cols],后者为int *

  • *p++ 等价于*(p++);相当于a[i++](*p)++表示p所指元素值加1

  • *(a+i)代表a[i]*(a[i]+j)*(*(a+i)+j)都代表a[i][j]

  • *p[4] 指针数组,元素全为指针的数组; (*p)[4] 是一个指针,指向一个4元素数组

  • 字符指针:char *s="Hello";等价于:char *s; s="Hello";而字符数组不可以分开这样做,并且字符数组间不能进行赋值(s输出的是Hello、而*s输出的是H)

8 字符串

  • 单引号:字符常量(代表一个整数); 双引号:字符串(指向无名数组起始字符的指针)

  • 延续字符串到下一行: 前一行用\结尾

  • C语言把字符串字面量作为字符数组(结尾为\0)来处理,把字符常量(单引号)作为整数处理

  • getchar、putchar 每次处理一个字符。getchar不会跳过空格、制表符和换行符,而scanf

    #include<stdio.h>
    ...
    char ch;
    while((ch=getchar())!=EOF){ //stdio.h中定义EOF
     putchar(ch);
    }
    //该程序会每次按下`Enter`输出缓冲区字符、当遇到文件结尾模拟(`Ctrl+D`或`Ctrl+Z`)时停止
  • puts()函数只输出字符串,会自动在末尾加上换行符(类似还有fputs()printf());gets() 读取整行输入直到换行符(尽量别用、已淘汰、可以用fgets()、而scanf()适合读取单词)

  • 字符串最后要有\0,否则为字符数组

  • 常见创建字符串的方式:

    // 数组形式中,数组名st1是地址常量,不能更改st1(不能++st1这样的操作);数组元素是变量、但数组名是常量
    char st1[]="hello world.";
    // 指针形式,字符串字面量为const数据、不能修改st2指向的数据,但可以改变st2的值(可以++st2)
    const char *st2="Hello world too.";
  • atoi() stdlib.h中将字符串转成int,同理有atof()(double)、atol()(long)。另外strtol()也可以

  • 所谓的文本文件不过是二进制文件按照一定规则解码得到的(如ASCII一字节一字节解读)?

  • 字符只是小型整数,所以用一个整型变量容纳字符值不会引起任何问题

  • 两个问号放一起的时候注意“三字母词(trigrph)”

  • signed关键字多用于char类型,因为其他整形类型默认为有符号数,只有char因编译器而异

  • 常用字符串函数:

    size_t strlen(char const *string)   //返回字符串(末尾必须为\0)长度,size_t为无符号数
    //用strlen(x)>=strlen(y)而不是strlen(x)-strlen(y)>=0,无符号数必非负
    char *strcpy(char *dst,char const *src) //复制字符串并返回一份拷贝,注意内存不能重叠或空间不足
    char *strcat(char *dst,char const *src) //连接字符串到dst后面
    int strcmp(char const *s1,char const *s2)   //字符串比较、若相等返回0
    //长度受限的字符串函数:strncpy、strncat、strncmp,多了一个参数size_t len

9 预处理器

  • #define PI 3.1415 编译时替换、末尾不用加分号

  • #if 0 #endif可以用来屏蔽含注释的代码

  • 预处理中的#运算符和黏合剂##运算符:

    #define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x)))
    #define XNAME(n) x ## n
    int y=5,x1=2;
    PSQR(y); //输出为: "The square of y is 25"
    printf(XNAME(1));   //输出为2
  • 带参数的宏:(若是表达式一定记得加括号,否则相邻操作符之间会产生不可预料的作用)

    #define PRINT(FORMAT,VALUE) \
     printf("The vaule of " #VALUE " is " FORMAT "\n",VALUE)
    //注意#argument的用法
    #define ADD(num,value) \
     sum##num+=value
    x=1;
    PRINT("%d",x+3);
    ADD(5,25);  //把sum5这个变量加25
  • #undef移除一个宏定义

  • 条件编译:

    #if constant-expression
     statements
    #elif
     ...
    #endif
    //是否被定义:
    #if defined(symbol) //等价于#ifdef symbol
    #if !defined(symbol2) //等价于#ifndef symbol2

10 大型程序编写要点

  • include中使用宏替换

    #if defined(IA32)
      #define CPU_FILE "ia32.h"
    #elif defined(IA64)
      #define CPU_FILE "ia64.h"
    #endif
    #include CPU_FILE
  • #error检查不该包含的头文件

    //避免把头文件用于旧的非标准编译器
    #ifndef _STDC_
    #error This header requires a Standard C compiler
    #endif
  • echo_eof < words<为重定向运算符、使程序用文件而不是键盘输入,同样>表示输出到文件(覆盖)、而>>表示附加到文件

  • 宏可以完成一些函数做不到的事:

    #define MALLOC(n,type) \
     ((type*)malloc((n)*sizeof(type)) )
    pi=MALLOC(25,int);
  • 模块的客户(main、使用者),模块的接口(头文件),模块的实现(.c文件)。模块的好处:抽象、可复用性、可维护性;模块的特点:高内聚性、低耦合性;模块的类型:数据池、库、抽象对象、抽象数据类型(ADT)

11 结构、联合和枚举

  • 允许把一个结构赋值给另一个结构,也允许把一个结构初始化为相同类型的另一个结构

  • 结构中比较特殊的一种初始化方式: struct book a={.value=10.9}; 即只初始化部分成员(注意声明的结尾加分号)

  • 结构内部不能包含该结构本身,但可以是指向该结构的指针(构造链表、树常用)

  • 利用结构来复制数组: struct {int a[10];} a1,a2; a1=a2;

  • 复合字面量,创建一个临时的结构: part1=((struct part){528,"drive",10});

  • 结构中储存字符串不要用字符指针而要用字符数组(字符指针将字符串储存在编译器储存常量的地方),如果要用请先用malloc()分配储存空间

  • 结构名不是地址(不同于函数、数组),访问地址要加上&

  • 两结构即使成员列表完全相同、也被编译器当做两种截然不同的类型

  • 联合 类型,混合数据类型:

    //该union可储存一个int或一个double或一个char
    union hold{
     int digit;
     double bigfl;
     char letter;
    }
  • 枚举类型(enum)是整数类型,目的是为了提高程序的可读性和可维护性

    enum suit {CLUBS,DIAMONDS,HEARTS,SPADES};
    typedef enum {FALSE,TRUE} Bool;
    enum suit2 {CLUBS=1,DIAMONDS=2,...};    //也可以选择不同数字,推荐加尾逗号

12 动态储存分配和函数指针

  • Segmentation fault 错误,说明程序试图访问未分配的内存

  • void *malloc( unsigned int size ),size是申请字节的大小,常用示例:

    int* p=(int*) malloc(5* sizeof(int) )
    if(p==NULL) {
     printf("内存申请失败,退出");
     return;
  • 动态分配内存详解:

    #include <stdlib.h>  //使用malloc、exit必加
    double *ptd;
    int n = 30;
    ptd = (double *)malloc(
      n * sizeof(double));  //创建一个数组(n可以用变量),分配了一块连续的内存
    if (ptd ==
      NULL) {  //分配失败时返回NULL指针,NULL在stdio.h、stdlib.h、string.h、time.h等中均有定义
      exit(
          EXIT_FAILURE);  // EXIT_FAILURE表示程序异常终止、EXIT_SUCCESS表普通的程序结束(相当于0)
      free(ptd);          //释放malloc分配了的内存
      // calloc和malloc类似,不过还会自动把指针初始化为0:
      double *pt = calloc(n, sizeof(double));
    // realloc能修改分配的内存块大小
  • 灵活数组成员

    struct vstring{
      int len;
      char chars[]; //C99 only , flexible array member}
    };
    struct vstring *str= malloc(sizeof(struct vstring)+n);
    str->len=n;
  • 函数指针的使用(常作为其他函数的参数):
    int f(int x);   //声明函数
    int (*pf)(int)=&f;  //创建并初始化函数指针pf,&可以去掉、因为函数名总是被编译器转换为函数指针
    //函数的调用:
    int ans=f(25);
    int ans=pf(25); // 直接这样调用也行,函数名就是个地址
    int ans=(*pf)(25);
    // int *g() , ()优先级较高,故是个函数,返回类型是int *
    // int (*g)()  , 是个指针,指向一个返回int类型的函数

13 声明、存储类型、作用域、变量类型

  • 声明的语法:
    extern const unsigned int a[10];
    //1. extern、储存类型,有auto、static、extern、register四种
    //2. 类型限定符,const、volatile两种
    //3. 类型说明符,如int、char...
  • 变量的3个性质:

    1. 存储期限。自动存储期:块终止时释放,静态存储期:整个运行期间占有同一个单元
    2. 作用域。 块作用域、文件作用域
    3. 链接(不同部分共享此变量的范围)。 外部链接:可以被程序中几个(或全部)文件共享,内部链接:只能属于单独一个文件,无链接:属于单独一个函数,不能共享
  • 五种存储类别:

    1. 自动变量:自动存储期(函数自动分配的、运行时堆栈)、块作用域、无链接,声明在块或函数头(函数参数部分)中,最常见的类型

      int main(voidl){
       auto int plex;//显示声明成自动变量,注意auto的用法C和C++很不一样
      }
    2. 寄存器变量:不储存在内存而是CPU寄存器(最快可用内存)中,无法获得变量地址,其他特性与自动变量相似(可以对指针用、这样就不必复制指针值、但必须被声明为局部变量?)

      register int quick; //只是一个请求,可能被忽略而变成自动变量
    3. 块作用域的静态变量(静态无链接):和自动变量的作用域相同、但离开后变量不消失(静态存储期,对其他函数隐藏、对当前函数保留),声明在块内

      void trystat(void){//不能在函数形参中用static
       static int stay=1;
      }
    4. 外部链接的静态变量: 有文件作用域、外部链接,未初始化的外部变量会自动设为0,并且只能用常量表达式初始化

      extern int out;//在main之外声明外部变量,若变量定义在别的源文件中则需要加extern(不会分配存储空间,只提示定义在别处),否则不要加
      int main(void){}
    5. 内部链接的静态变量:只能用于同一个文件中的函数、声明在函数外、用static关键词

  • 内存的3个部分:
    1. 静态变量(外部链接、内部链接、无链接)、内存在编译时确定,程序开始时创建、结束时销毁
    2. 自动变量:作为处理、进入定义变量的块时存在、离开块时消失;(按创建顺序加入、相反顺序销毁)
    3. 动态分配的内存:“支离破碎”地分布在已用内存块之间(堆)、比栈内存要慢,malloc时分配free后释放
  • 标识符声明staticexternal只有第一次声明有效,多次声明无法更改其链接属性

  • 如何理解复杂声明:从内往外读、[]()还有.(于是有了->)优先*

    1. 从名字开始读取,按优先级顺序排序

    2. 优先级从高到低是: a. 声明中被括号括起来的部分 b. 后缀操作符、小括号表示函数、方括号表示数组 c. 前缀操作符:*表示指针

    3. const后面若紧跟类型说明符(如int)、则作用与它,否则作用于它左边的星号

      例子:char * const *(*next)()
      首先*next说明是个指针,看外面有个括号说明是函数指针,再有个*得出指针所指内容;再看函数返回类型,是个char * const

  • 例子:int *f()()优先级较高、说明是个函数、*表示返回值是个指针; int (*f)(): 是个指向函数的指针、函数返回值是int;int *(*f)(): 指向函数的指针、函数返回值为整形指针;int *f[]: 是个数组、元素是整形指针;int (*f[])(): 是个数组、数组元素为指针、指针类型为返回int的函数的指针;int *(*f[])(): 是个数组、数组元素为指针、指针类型为返回int*的函数的指针

  • 复杂的类型转换符: 把声明中的变量名和末尾的分号去掉,剩余部分用括号“封装”即可

  • 通过-lname选项告知编译器链接到libname.so函数库;始终将-l函数库选项放在命令行的最右端(首先需要让文件包含未解析的引用)

14 位运算符等

  • 用处:编写系统程序(编译器、操作系统)、加密程序、图形程序、需要高执行速度或高效利用空间的程序

  • C语言中其他进制的表示方式:

  • 移位运算符:<< 左移>> 右移:(为了可移植性,最好只对无符号数移位运算)

    unsigned int i=13; //1101
    unsigned int j=i<<13;   //110100
    unsigned int j=i>>13;   //11
    i<<=2;  //会改变操作数
  • ~按位取反、一元运算符(一定要弄清有多少位)

  • &按位与、^按位异或、|按位或

15 标准库

  • 求余数和商:

    #include <stdlib.h>  //定义了div函数
    #include <iostream>
    int main(int argc, char *argv[]) {
     auto result = div(25, 3);  //返回div_t类型的一个结构
     std::cout << "quot(商): " << result.quot << " , rem(余数): " << result.rem
               << std::endl;
     return 0;
    }
  • 程序运行时间获取:

    #include <time.h>
    clock_t t1=clock()/CLOCKS_PER_SEC; //开始运行到这个位置处花的时间,并转换为s
    //只是近似值

16 输入输出(高级、stdio.h)

  • 文件指针:定义在stdio.h中的派生类型,C语言会自动打开stdinstdoutstderr三个标准文件。

    #include <stdio.h>
    #include <stdlib.h>
    FILE *fp;
    // 返回空指针则说明打开文件失败
    if((fp=fopen("filename","r"))==NULL){
     fprintf(stdout,"Can't open file.\n");
     exit(EXIT_FAILURE);
    }
    while((ch=getc(fp))!=EOF){  //判断是否到了文件末尾
    ;}
    if(fclose(fp)!=0){ // 成功关闭返回0,否则返回EOF
     fprintf(stderr,"Error closing file\n");
    }
  • rewind(fp) 可以让程序回到文件开始处

  • windows下使用fopen等函数时注意路径的写法:使用/或者\\作为目录分隔符

  • 通过argv传递命令行参数:

    // argc为argv长度,argv是指针数组,*argv[0]为程序名,之后如*argv[1]为命令行参数
    int main(int argc, char *argv[]){}
Author: zcp
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source zcp !
评论
Valine utteranc.es
 Previous
东方时尚驾校学车记录
1 前言我从小对车并没有太大兴趣,刚上大学那会寒暑假闲得很也不想去学车,看到朋友们都拿到了驾照也没觉得羡慕啥的。也许就是因为懒吧,当时想做的事太多了,认为考了驾照也基本上不会去开车(家里没车),就把学车这件事一直放着了。 前一段时
Next 
Vim常用命令学习
《Vim使用技巧》里面的东西还是挺多的,适合每天啃一点、来回品味。当然也有一些地方有更优解,总的来说是本好书,基本上书里讲的都能够理解的话vim也用的很熟练了吧。 1 Vim基础 vim -u NONE -N:不加载vimrc、不
  TOC