C语言基础

基本概念

位与字节

位(bit,b),字节(byte,Byte,B)

1
2
3
4
5
1byte = 8 bit 
​​1KB   = 1024B 
1MB   = 1024KB 
1GB   = 1024MB
1TB   = 1024MB

位运算

异或(^) 异者或

左移运算:m<<n表示把m左移n位,最左边的n位被丢弃,右边补上n个0。00001010<<2=00101000

右移运算:m>>n表示把m右移n位,最右边的n位被丢弃,左边的情况分两种:

  1. 如果数字是一个无符号数值,则用0填补最左边的n位。00001010>>2=00000010

  2. 如果数字是一个有符号数值,则用数字的符号位填补 最左边的n位。(如果数字是正数,右移之后左边补0,如果是负数,左边补1)。10001010>>3=11110001

typedef & #define

typedef允许为各种数据起新的名字。

1
2
typedef char *ptr_to_char
ptr_to_char a;  //声明a是一个指向字符的指针
应该用typedef而不是#define来创建新的类型名,因为#define不能正确处理指针类型。
1
2
#define d_ptr_to_char char *
d_ptr_to_char a,b;  //b被声明为一个字符而不是指针
define指令是另一种创建名字常量的机制。
1
2
3
4
#define MAX_ELEMENTS 50
int const max_elements = 50

char hello[MAX_ELEMENTS];   //const变量只能使用于允许使用变量的地方

const关键字

以下2条语句都把a声明为一个整数,它的值不能被修改。(选择一种好理解的并一直坚持下去)

1
2
int const a = 15;   
const int a = 15;   
由于a的值无法被修改,也无法被任何东西赋值,有2种方法可以使它获得值:

  1. 在声明的时候对它初始化

  2. 在函数中声明const的形参在函数被调用时会得到实参的值

const与指针的结合会影响指针变量或者它所指向的实体

1
2
3
4
int *pi;                 //普通指针
int const *pci;          //指向整型常量的指针,可以修改指针的值,但不可以修改它所指向的值
int * const cpi;         //指向整型的常量指针,指针是常量不可修改,它所指向的整型值可修改
int const * const cpci;  //指针本身与它所指向的值都不可被修改

作用域

编译器可以确认4种不同类型的作用域–文件作用域、函数作用域、代码块作用域和函数原型作用域。

链接属性

链接属性:external(外部)、internal(内部)、node(无)。

关键字extern和static用于在声明中修改标识符的链接属性。

当extern关键字用于源文件中一个标识符的第一次声明时,它指定该标识符具有external链接属性。但是,如果它用于该标识符的第2次或以后的声明时,它并不会更改由第一次声明所指定的链接属性。

1
2
3
4
5
6
7
8
static int i;   //----1
int func()
{
    int j;  
    extern int k;
    extern int i;   //----2
}
//声明2并不修改声明1所指定的变量i的链接属性

存储类型

变量的存储类型(storage class)是指存储变量值得内存类型。决定变量何时创建、何时销毁以及它的值将保持多久。

存储变量的三个地方:普通内存、运行时堆栈、硬件寄存器。

变量的缺省存储类型取决于它的声明位置。

凡是在任何代码块之外声明的变量总是存储于静态内存中,这类变量称为静态(static)变量(main函数里声明的变量也是局部变量)。

在代码块内部声明的变量的缺省存储类型是自动的(automatic),存储在堆栈中,称为自动(auto)变量。

代码块内部声明的变量加上static关键字,则使它的存储类型从自动变为静态。注意:修改变量的存储类型并不表示修改该变量的作用域。

函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。

static关键字

  1. 当static关键字用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。
  2. 当static关键字用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁

操作符与表达式

++&–操作符

++&–操作符需要一个变量而不是表达式作为它的操作数。操作符实际只要求操作数必须是一个‘左值’,只能作用于可以位于赋值符号左边的表达式。

前缀和后缀的增值操作符都复制一份变量值得拷贝。这些操作符的结果不是被它们所修改的变量,而是变量值的拷贝。

1
++a=10;
++a的结果是a值得拷贝,并不是变量本身,无法向一个值进行赋值。

逻辑操作符为“短路求值”。

左值&右值

左值:可以存储结果值的地点 右值:指定一个值

1
2
3
int a, *pi;
pi=&a;
*pi=20;
第二条语句是一个合法的左值,指针pi的值是*内存中某个特定位置的地址,操作符使机器指向那个位置。当它作为左值使用时,这个表达式指定需要进行修改的位置。当它作为右值使用时,它就提取当前存储于这个位置的值。

语句

C最简单的语句是空语句,他本身只含有一个分号。

1
for(;;)
当你需要循环体至少执行一次时,选择do。
1
2
3
4
5
6
do
{
  func();
  break;
  ...
}while(0);

指针

名字与内存位置之间的关联并不是硬件提供的,它是由编译器为我们实现的。

变量的值就是分配给该变量的内存位置所存储的数值,即使是指针变量也不例外。

1
2
int *a;
*a=12;  //指针未初始化
如果不知道指针将被初始化为什么地址,就把它初始化为NULL;
1
2
3
4
int a;
int *d=&a;
*d = 10-*d;     //-----------1合法
d=10-*d;        //-----------2非法
右边的间接访问作为右值使用,它的值是d所指向的位置所存储的值。

左边的间接访问作为左值使用,所有d所指向的位置把赋值符右侧的表达式的计算结果作为它的新值。

和任何变量一样,指针变量也可以作为左值使用。对指针执行间接访问操作所产生的值也是个左值,因为这种表达式标识一个特定的内存位置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
char ch='a';
char *p=&a;

/*--------------*/
ch
//右值:表达式的值'a'
//左值:所代表的内存的地址而不是该地址的值
/*--------------*/
&ch
//右值:是变量ch的地址
//左值:非法
/*--------------*/
cp
//右值:cp的值(cp的内存中的地址)
//左值:cp所在的内存位置
/*--------------*/
&cp
//右值:指针变量的地址
//左值:非法
/*--------------*/
*cp
//右值:值
//左值:地址
/*--------------*/
*cp+1
//右值:值的拷贝并把它+1
//左值:非法
/*--------------*/
*(cp+1)
//右值:ch之后的内存位置的值
//左值:位置本身
/*--------------*/
++cp
//右值:增值后的指针的一份拷贝,指向下一个位置
//左值:非法
/*--------------*/
cp++
//右值:先返回cp值的一份拷贝然后在增加cp的值
//左值:非法地址
/*--------------*/
*++cp
//右值:增值后的指针的拷贝上,右值是ch后面那个内存地址的值
//左值:那个位置本身
/*--------------*/
*cp++
//后缀++操作符的优先级高于*操作符
//1) ++操作符产生cp的一份拷贝
//2) ++操作符增加cp的值
//3) 在cp的拷贝上执行间接访问操作
//右值:ch的值
//左值:ch的内存位置
/*--------------*/
++*cp
//操作符的结合性是从右向左
//1) 先执行间接访问操作
//2) cp所指向的位置的值增加1
//3) 表达式的结果是这个增值后的值的一份拷贝
//右值:增值后值的拷贝
//左值:非法
/*--------------*/
(*cp)++
//右值:与上面相同
//左值:非法
/*--------------*/
++*++cp
/*
1. 这些操作符的结合性从右向左
2. 首先执行++cp,指向下一个位置
3. 对拷贝的值进行间接访问,访问ch后面的位置(因为下一个操作数把它当左值使用)
4. 在这个位置进行++操作,增加他的值
*/
//右值:如上所述
//左值:非法
/*--------------*/
++*cp++
//右值:间接访问操作符访问的是cp所指向的位置而不是cp所指向位置的后面的那个位置
//左值:非法

函数

C函数的所有参数均以“传值调用”方式进行传递,这意味着函数将获得参数值的一份拷贝。

“传址调用”数组名的值实际上是一个指针,传递给函数的是这个指针的一份拷贝。

尾部递归:递归函数内部所执行的最后一条语句就是调用自身。

数组

在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第1个元素的地址。

它的类型取决于数组元素的类型。

只有当数组名在表达式中使用时,编译器 才会为它产生一个指针常量(不是指针变量)。

只有2种情况下,数组名并不用指针常量来表示:

  1. 数组名作为sizeof操作符
  2. 单目操作符&的操作数

下标绝不会比指针更有效率,但指针有时会比下标更有效。

指针和数组并不是相等的

1
2
3
4
5
6
7
8
int a[5];
/*声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值时一个常量,指向这段空间的起始位置。*/
//*a 合法
//a++ a的值是一个常量
int *b
/*声明一个指针变量时,编译器只为指针本身保留存储空间,并不为任何整型值分配内存空间。*/
//*b 不合法,没有初始化
//b++ 可编译通过

字符串

1
2
3
4
5
char *strncpy(char *dst,char const *src,size_t len);
//如果strlen(src)的值大于等于len,那么只有len个字符被复制到dst中。注意!!它的结果不会以NUL字节结尾,它的结果可能不是一个字符串。
char buffer[BSIZE];
strncpy(buffer,name,BSIZE);
buffer[BSIZE-1]='';
strncat总是在结果字符串后面添加一个NUL字节,而且它不会像strncpy那样对目标数组用NUL字节进行填充。

结构和联合

声明结构时的一种良好技巧:

1
2
3
4
5
6
typedef struct{
    xx
    xx
} Simple;
Simple x;
Simple y[20],*z;
点操作符的结合性时从左向右。

注意如下陷阱:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typedef struct {
    int a;
    SELF_REF3 *b;
    int c;
}SELF_REF3;
//类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。
typedef struct SELF_REF3_TAG {
    int a;
    struct SELF_REF3_TAG *b;
    int c;
}SELF_REF3;
所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置。

向函数传递结构参数是低效的,尽量传递结构指针,提升效率。

分配给联合的内存数量取决与它的最长成员的长度。

高级指针话题

1
2
int f;  /*一个整形变量*/
int *f; /*一个指向整形的指针*/
1
int f();
它把f声明为一个函数,它的返回值是一个整数。
1
int *f();
首先执行的是函数调用操作符(),应为它的优先级高于间接访问操作符。因此,f是一个函数,它的返回值类型是一个指向整型的指针。
1
int (*f)();
第一对括号只起聚合作用,它迫使间接访问在函数调用之前进行,第二对括号是函数调用操作符。所以,f是一个函数指针,它所指向的函数返回一个整型值。
1
int *(*f)();
f是一个函数指针,只是所指向的函数的返回值是一个整型指针。
1
2
3
int f[];    /*f是一个整型数组*/
int *f[];   /*下标优先级更高,f是一个数组,它的元素类型是指向整型的指针*/
int (*f)[]; /*指向数组的指针,数组的每个元素都是整型*/
1
int f()[];
非法声明函数只能返回标量值,这个f函数返回一个整型数组。
1
int f[]();
非法声明f似乎是一个数组,它的元素类型是返回值为整型的函数。但是数组元素必须具有相同的长度,但不同的函数显然可能具有不同的长度。
1
int (*f[])();   
f是一个元素为某种类型的指针的数组。数组元素的类型是函数指针,它所指向的函数的返回值是一个 整型值。
1
int *(*f[])();
这是一个指针数组,指针所指向的类型是返回值为整型指针的函数。

回调函数与转换表

回调函数实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>
#include "node.h"
Node* search_list(Node *node,void const *value, int (*compare)(void const *,void const *))
{
    while(node != NULL)
    {
        if(compare(&node->value,value)==0)
        {
            break;
        }
        node=node->link;
    }
    return node;
}

int compare_ints(void const *a,void const *b)
{
    if(*(int *)a==*(int *)b)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

desired_node=search_list(root,&desired_value,compare_ints);

转移表jump table

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
···
double (*oper_func[])(double, double)={
    add,sub,mul,div,...
};

result=oper_func[oper](op1,op2);

预处理器

预处理(preprocessing)阶段:删除注释、插入被#include指令包含的文件内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。

预定义符号

1
2
3
4
5
__FILE__    进行编译的源文件名
__LINE__    文件当前的行号
__DATE__    文件被编译的日期
__TIME__    文件被编译的时间
__STDC__    遵循ANSI C,其值为1

命令行定义

1
2
3
4
int array[ARRAY_SIZE]
//-Dname 值为1
//-Dname=stuff 值为stuff
cc -DARRAY_SIZE=100 prog.c

文件包含

1
2
3
4
5
6
#ifndef _HELLO_
#define _HELLO_
/*
**All the stuff that you want in the header file
*/
#endif

输入&输出函数

文本流&二进制流

文本流:标准规定文本行至少运行254个字符。在MS-DOS系统中,文本行的结束方式以一个回车符和一个换行符结尾。UNIX系统中只使用一个换行符结尾。

二进制流:二进制流中的字节将完全根据程序编写它们的形式写入到文件或设备中,而且完全根据它们从文件或设备读取的形式读入到程序中。并未做任何改变。

FILE是一个数据结构,用于访问一个流。stdin,stdout,stderr都是一个指向FILE结构的指针。

I/O函数以三种基本的形式处理数据:单个字符、文本行和二进制数据。

数据类型 输入 输出 描述
字符 getchar putchar 读取(写入)单个字符
文本行 gets/scanf puts/printf 文本行的输入输出(未格式化/格式化)
二进制 fread fwrite 读取(写入)二进制数据

标准函数库

随机数

1
2
3
int rand(void);
void srand(unsigned int seed);
srand((unsigned int)time(0));

日期与时间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 处理器时间
clock_t clock(void);   
//程序开始执行起处理器所消耗的时间,可以两个时间相减获取精确时间。
//返回时钟滴答次数,转换秒,除以CLOCKS_PER_SEC

//当天时间
time_t time(time_t *returned_value);    //time函数返回当前的日期和时间

//日期与时间转换
char *ctime(time_t const *time_value);  //转换为字符串,asctime(localtime(time_value));

double difftime(time_t time1, time_t time2); //计算time1-time2的差,结果转换为秒

struct tm *gmtime(time_t const *time_value);    //UTC时间
struct tm *localtime(time_t const *time_value);     //当地时间

char *asctime(struct tm const *tm_ptr);     //tm结构的时间转换为字符串
size_t strftime(char *string,size_t maxsize, char const *format, struct tm const *tm_ptr);  //tm结构转换为一个根据某个格式字符串而定的字符串

time_t mktime(struct tm *tm_ptr);   //把一个tm结构转换为一个time_t值