C语言笔记:基础

自动类型转换

自动类型转换的规则如下:

  • 在表达式里,charshort类型自动转换为int,需要时转换为unsigned int
  • 在传递给printf("%f")时,float自动转换为double
  • 在包含两种数据类型的运算中,两个值被转换成两种类型里较高级的类型。
  • 在赋值语句中,计算的最后结果被转换成被赋值的变量的类型(可能导致降级)。

控制语句:循环

while循环

while (expression) statement;

statement部分既可以是带有分号的简单语句,也可以是使用花括号包围的一个复杂语句,也可以是一条空语句。

只有expression为真,statement才会执行。

空语句示例:

while( scanf("%d", &num) == 1 )
    // 跳过整数输入
;

for循环

for(ctrl_1; ctrl_2; ctrl_3) statement;

控制表达式1在循环开始前执行;控制表达式2为真时开始一次迭代;控制表达式3在一次迭代完成后执行。

do while循环

do statement while(expression);

循环辅助手段

  • continue 循环的其余部分被忽略,开始下一次循环;对于for循环,从控制段的更新部分开始;对于while循环,从判断循环表达式开始。
  • break 跳出一层循环;对于for循环,不执行控制段的更新部分。

分支和跳转

if... else...

if(epression) statement

if(expression) statement1 else statement2

if(expression1) staement1 else if(expression2) statement2 else statement3

多重选择 switch

switch(integer expression)
{
    case constant1:
        statements
        break;
    case constant2:
        statements
        break;
    default:
        statements
}

case标签必须为整型。如果没有break标签,匹配标签之后的语句都会被执行,直到遇到break标签或switch结构结束。

goto

  • label: statement
  • goto label;

函数

函数原型

retrun-type function-name(arguments-list);

函数原型中函数的参数可以省略变量名。

形式参数与实际参数

函数定义中头部的参数是形式参数,函数调用时使用的参数是实际参数。实参复制给形参。实参与形参相互独立。

函数声明

函数可以在全局声明也可以在函数中声明。在函数中声明的函数在全局有效。函数在调用前需要先声明。

函数可以声明为无参数或不确定参数。

递归

函数可以递归调用。在return语句之前的递归称为尾递归。

数组和指针

数组

声明和初始化:

type identifer[N] = {elem1, elem2, ..., elemN};

N可被省略,编译器将根据指定元素的个数初始化数组;部分初始化数组,数组其余元素会被自动初始化为0。

使用{[1]=elem1, ..., elemN}语法来指定初始化。如果指定初始化元素后有更多元素,则它们被用于后续数组元素的初始化。如果多次对一个元素初始化,最后一次有效。

使用花括号对数组初始化的语法仅在数组声明时有效。不能将数组作为一个整体进行赋值。

多维数组

type identifier[X][Y];

identifier 是一个由x个元素组成的数组,而它的每一个元素是y个type类型元素组成的数组。

可以使用花括号语法声明和初始化数组:

type identifier[X][Y] = {
    {elem11, elem12, elem13, ..., elem1y},
    {elem21, elem22, elem23, ..., elem2y},
    ...,
    {elemx1, elemx2, elemx3, ..., elemxy}
}

使用花括号声明时也可以去掉内部的花括号,前面的元素优先得到赋值。

使用指针操作多维数组,复杂度随数组维数的增加而增加。地址的地址和指针的指针是双重间接(double indirection)的一个典型例子。

区分指针的数组和数组的指针。int * ptr[2]是有两个int类型指针的数组而int (* ptr) [2]是指向一个拥有两个int类型数据的数组的指针。方括号的结合性高于*(可以借助“靠近标识符的先结合”来判断类型)。

ptr[2] 是一个含有两个元素的数组 → * ptr[2] 是一个含有两个元素的数组,数组的元素是指针 → int * ptr[2] 是一个含有两个元素的数组,数组的元素是指向int类型数据的指针;

(* ptr) 是一个指针 → (* ptr) [2] 是一个指针,指向拥有两个元素的数组 → int (* ptr) [2] 是一个指针,指向拥有两个int类型元素的数组。

声明数组参数

在声明形参时,可以使用int ar[]代替int * ar。使用这样的形式可以提示 ar 是指向int数组中一个元素的指针。无论怎样声明,处理数组的函数实际上是使用指针作为形式参数的。

如果不希望改变数组内容,可以用const修饰形参中的指向数组的指针。

声明处理多维数组的函数的形式参数,可以使用int (* ptr) [n][x][y]和它的等价形式int ptr[][x][y](第一个方括号留空表示它是指针,如果第一个方括号没有留空,效果上等价于留空的)。

变长数组(VLA)

变长数组必须是自动储存类的(也就必须在函数内部或者),不能在初始化时进行声明。

声明处理变长数组的函数,变长数组参量需要在数组之前声明。如int sum2d(int rows, int cols, int ar[rows][cols]);,可以省略一般形参的名称和变长数组的维数,如int sum2d(int, int, int ar[*][*]);

指针

地址运算符 &

间接运算符 *

ptr = &bah; val = * ptr; 等价于 val = bah;

使用 type * identifier; 来声明指针。

不要对未初始化的指针取值。

自增运算符与间接运算符存在微妙的优先级关系,试区分*ptr++*++ptr*(ptr++)*(++ptr)(*ptr)++。其中*ptr++等价于*(ptr++)

使指针自增1,指针指向下一个储存单元(而不是下一个字节)。

如果指针被const修饰,那么指针指向的数据不应该被改变。不能把const指针赋给非const指针(这样指针指向的数据可能会改变),但可以把非const指针赋给const指针,前提是只进行一层间接运算(即被赋值的const指针不能是指针的指针)。

下面的例子演示了如果忽略上述前提,在双重间接的情况下const内容被修改的例子:

const int ** pp;
int * p;
const int n = 10;

pp = &p;    // 不允许
*pp = &n;   // 使p指向n
*p = 11;    // n被修改

基本操作

  • 赋值(Assignment) 将地址赋给指针,可以使用地址运算符或数组名。还可以使用类型指派来为不兼容的指针赋值。
  • 求值(value-finding)或取值(derefering) 使用间接运算符。
  • 取指针地址 使用地址运算符。
  • 指针的整数加法和减法
  • 求差值(differencing) 差值的单位是相应类型的大小,而不是字节。有效差值运算的前提是两个指针指向同一个数组
  • 比较 可以使用关系运算符比较两个相同类型的指针。

一个指针不能另一个指针相加或相乘。

指针与数组

在表达式中使用数组名,等同于使用该数组首元素的地址,即数组名是指向首元素的(只读)指针。

指针变量和数组标识符都可以使用方括号语法和指针语法:ar[n]等价于*(ar + n)。区别在于,指针可以通过自增自减运算符(如ar++)来修改指向的地址,而数组名虽是指针,却是指向数组首元素的只读指针,不能被修改,即ar++是无效的。

复合文字

复合文字(Compound literal)是表示数组和结构内容的字面值。它没有标识符。

(int [2]){1, 2}

上面的表达式创建了一个含有2个int元素的数组。表达式的值是指向数组首元素的指针。

可以省略元素的个数:

(int []){1, 2, 3}

也适用于多维数组,但只能省略第一个维度。

(int [][4]) {
    {1, 2, 3, 4},
    {5, 6, 7, 8}
}

可以使用复合文字初始化一个指针,或者使用复合文字作为实际参数传递给参量形式匹配的函数。

储存类、链接和内存管理

作用域

  • 代码块作用域
  • 函数原型作用域(如VLA)
  • 函数作用域(仅适用于goto语句使用的标签)
  • 文件作用域(全局变量)

链接

  • 外部链接
  • 内部链接(在外部定义中使用了储存类说明符static)
  • 空连接

储存时期

  • 静态储存时期
  • 动态储存时期

自动变量不会被自动初始化。外部变量只能使用常量表达式来初始化。(只要类型不是一个数组,sizeof()就是一个常量表达式)

使用 register 关键字声明寄存器变量。

如果变量在其他文件声明,必须使用 extern 关键字来声明。

malloc()free()

double * ptr = (double *)malloc(n * sizeof(double)); 成功分配内存则返回一个指向第一个分配内存空间的指针,否则返回空指针。应当显式地进行类型指派。

free(ptr); 释放占用的内存空间

malloc() 的等价语句是 calloc()

double * ptr = (double *)calloc(n, sizeof(double)); 返回值与 malloc() 相似。

realloc(ptr, n * sizeof(double)) 可用于重新分配内存空间,ptr指向的空间需由malloccalloc或者realloc分配,且不需要也不能调用free

类型限定词

volatile

当变量除了被程序改变之外还可能被其他代理改变时。一个变量可以同时是 constvolatile

restrict

只用于指针,表示该指针是访问数据块的唯一方式。

在函数原型中,参数的 restrict 要求保证指针是它指向的内容的唯一的访问方式。 void * memcpy(void * restrict s1, const void * restrict s2, size_t n);void * memmove(void * s1, const void * s2, size_t n); memcpy() 要求两个位置之间不重叠, memmove() 允许重叠。

static

double stick(double ar[static 20]);

函数原型中的 static 表示数组至少具有20个元素。

函数原型中的const和restrict

void ofmouth(int * const a1, int * restrict a2, int n); 的等价形式是:

void ofmouth(int a1[const], int a2[restrict], int n);