简介

c 语言程序的创建过程

c 语言程序的结构

预处理指令

符号 # 表示这是一个预处理指令,告诉编译器在编译源代码之前,要先执行一些操作。例如:

#include <stdio.h>

函数

常量和变量

常量

转义字符 含义 转义字符 含义
\n 换行 \t 水平制表
\v 垂直制表 \b 退格
\r 回车(不换行) \f 换页
\a 响铃 \ 反斜线
' 单引号 " 双引号
\ddd 3 位 8 进制代表的字符 \xhh 2 位 16 进制代表的字符

变量

数据类型

整形

短整形 short

长整形 long

浮点型

单精度 float

 +      0.314159    1
 ↑         ↑        ↑
符号     小数部分   指数部分

我们可以用下面的代码查看计算机里,单精度浮点数的范围。

#include <stdio.h>
#include <float.h>

int main() {
    printf("Smallest positive: %.3e\n", FLT_MIN);
    printf("Largest  positive: %.3e\n", FLT_MAX);
}

输出结果

Smallest positive: 1.175e-38
Largest  positive: 3.403e+38

双精度 double

float 溢出

float 的有效位数是 7 位,这里赋值超过了

#include <stdio.h>

int main() {
    float a, b;
    a = 123456.789e5;
    b = a + 20;
    printf("b - a = %f", b - a);
    return 0;
}

输出结果

b - a = 0.000000

溢出部分会被忽略,所以 b 和 a 的结果相同。将 float 改为 double 之后可以得到正确结果

#include <stdio.h>

int main() {
    double a, b;
    a = 123456.789e5;
    b = a + 20;
    printf("b - a = %f", b - a);
    return 0;
}

输出结果

b - a = 20.000000

字符类型 char

介绍

字符的输出

#include <stdio.h>

int main() {
    char a, b;
    a = 'c';  // 输入字符
    b = 121;  // 输入 ascii 码
    printf("a: %c,  b: %c   (char)\n", a, b);  // 打印字符
    printf("a: %d, b: %d (int)\n", a, b);  // 打印 ascii 码
    return 0;
}

输出结果

a: c,  b: y   (char)
a: 99, b: 121 (int)

我们可以对字符的 ASCII 码进行数学运算。

#include <stdio.h>

int main() {
    char a, x;
    int b;
    a = 'a';
    b = 1;
    x = a + b; // increase 'c' by 1
    printf("x is %c\n", x);
    return 0;
}

输出结果

x is b

其他类型

强制类型转换

格式:(类型名) (表达式)

例子

#include <stdio.h>

int main() {
    int a;
    float b = 2.5;
    a = (int) b;  // 类型转换
    printf("(int) b is %d\n", a);
    return 0;
}

输出结果

(int) b is 2

运算符

基本运算符

x + y
x - y
x * y
x / y
x % y  // 取余数

整数和整数的结果是整数

自增、自减运算符

不同数据的混合运算

类型(优先级从高到低)
long double
double
float
long
unsigned int
int
short
char

数据的输入和输出

格式化字符

输入输出字符

顺序结构设计

条件表达式

格式:如果 (a>b) 为真,则取 a,否则取 b

max = (a > b) ? a : b;

while 循环

下面两种写法等价

while

#include <stdio.h>

int main() {
    int i = 1;
    while (i <= 10){
        i++;
    }
    printf("result is %d", i);
    return 0;
}

do ... while

#include <stdio.h>

int main() {
    int i = 1;
    do {
        i++;
    } while (i <= 10);
    printf("result is %d", i);
    return 0;
}

for 循环

下面是 for 循环的语法。注意条件语句中的分号,不是逗号。

#include <stdio.h>

int main() {
    int i = 0;
    for (i = 1; i <=3; i++){
        printf("i = %d\n", i);
    }
    return 0;
}

条件语句

(表达式_1; 表达式_2; 表达式_3)

循环嵌套和跳出循环

使用 break 可以跳出一层循环。下面的代码是一个例子(只有涉及变量 j 的循环被跳出了)。

#include <stdio.h>

int main() {
    int i, j;
    for (i = 0; i < 3; i++){
        for (j=0; j < 3; j++){
            if (j == 2) break;
            printf("i = %d, j = %d\n", i, j);
        }
    }
}

输出结果 (j 没有取 2 因为循环被 break)

i = 0, j = 0
i = 0, j = 1
i = 1, j = 0
i = 1, j = 1
i = 2, j = 0
i = 2, j = 1

数组

一维数组

下面介绍了 C 语言中,创建一个 含有 10 个数字的 数组的语法。

int array[10];

我们可以用 常量表达式 指定数组长度。

int array[2 * 5];

我们也可以用 变量 来指定数组长度。

int n = 10;
int array[n];

但是,我们不能用 变量 来指定数组长度,并且给它赋值。

int n = 5;
int array[n] = {1, 2, 3, 4, 5};

报错

variable-sized object may not be initialized  

当我们给数组赋值的时候,我们可以一次给所有元素赋值,也可以只给 部分元素 赋值。

/* 完整赋值 */
int array[3] = {1, 2, 3};    // -> {1, 2, 3}
/* 部分赋值 */
int array[3] = {1, 2};       // -> {1, 2, 0}
/* 完整赋值,Warning Excess elements in array initializer */
int array[3] = {1, 2, 3, 4}; // -> {1, 2, 3}

二维数组

下面是创建 二维数组 的语法。

类型符   数组名 [常量表达式][常量表达式]
int     matrix[10][10] 

二维数组在内存中的 存放规则 如下。

┌─────────┐
│ a[0][0] │
│ a[0][1] │  第一行
│ a[0][2] │
├─────────┤
│ a[1][0] │
│ a[1][1] │  第二行
│ a[1][2] │
├─────────┤
│ a[2][0] │
│ a[2][1] │  第三行
│ a[2][2] │
└─────────┘

下面是常用的给 二维数组 赋值的方法。

/* 整体赋值 */
int matrix[3][2] = { {1, 2}, {3, 4}, {5, 6} };
int matrix[3][2] = {1, 2, 3, 4, 5, 6};

/* 部分赋值 */
int matrix[3][2] = { {1}, {3}, {5} };

字符数组(字符串)

在 C 语言中,字符串就是「字符组成的数组」。创建字符串的格式如下:

char string[10];

由于 字符串就是数组,给它赋值的方法与 同数组赋值 的方法一致。

/* 完整赋值 */
char string[5] = {"H", "E", "L", "L", "O"};

/* 部分赋值(剩下的元素会被「空字符 \0」填充) */
char string[5] = {"H", "E", "L"};

我们可以在赋值的时候,省略长度。

char string[] = {"H", "E", "L", "L", "O"};

我们也可以用下面的,更简单的方法来赋值。

char string[] = {"hello world"};
char string[] = "hello world";

c 语言编译器在用字符数组存储字符串时,会自动加一个 ’\0’ 作为结束符

我们可以使用下面的两个方法(%sputs)来打印字符串。

#include <stdio.h>

int main() {
    char string[] = "hello world\n";
    printf("%s", string); // method 1
    puts(string); // method 2
    return 0;
}

字符串处理库 string.h

字符串处理库 string.h 提供了很多 方便操作字符串 的函数。

#include <stdio.h>
#include <string.h>

int main() {
    char string[5];
    gets(string);  // 从标准输入获得字符串内容
    puts(string);  // 打印字符串
    return 0;
}
#include <stdio.h>
#include <string.h>

int main() {
    char str1[20] = "hello "; // 长度必须足够包含 strcat 的结果
    char str2[] = "world\n";
    printf("%s", strcat(str1, str2));
    printf("%s", str1);
    return 0;
}

输出结果

hello world
hello world
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "hello world";
    int length = strlen(str);
    printf("String size is %d", length);
    return 0;
}

输出结果

String size is 11
#include <stdio.h>
#include <string.h>

int main() {
    char A[10];
    char B[] = "message";
    strcpy(A, B);  // 将 B 的内容复制到 A
    puts(A);
    return 0;
}

输出结果

message
#include <stdio.h>
#include <string.h>

int main() {
    char A[] = "hello";
    char B[] = "world";
    int result = strcmp(A, B);
    printf("%d", result);
    return 0;
}

输出结果(负数,因为 h < w)

-15

比较规则:将两个字符串自左向右逐个字符比较(按照ASCII码值大小比较),直到出现不同的字符或者遇到’\0’

例如: “A”<“D”,”e”>”E”,”these”>”that”,”computer”>”compare”

比较结果由函数值返回:

指针

指针的基本用法

下面的例子展示了指针的基本用法。

#include <stdio.h>

int main() {
    int a = 100, b =10;
    int *p1, *p2;  // 两个指向「整形」变量的指针
    p1 = &a;  // 将 a 的地址 赋予给 p1
    p2 = &b;  // 将 b 的地址 赋予给 p2
    
    /* 输出变量的 值 */
    printf("a = %d, b = %d\n", *p1, *p2);
    
    /* 输出变量的 地址 */
    printf("a is in %#x, b is in %#x\n", p1, p2);
    return 0;
}

输出结果

a = 100, b = 10
a is in 0xee9b71b8, b is in 0xee9b71b4

运算符

取地址运算符 & 和 指针运算符 * 处于第二优先级,仅次于括号。它们的结合方向是 从右到左。下面的表格展示了二者的差异。

名称 取地址运算符 & 指针运算符 *
功能 取地址 指针运算(简介访问)
操作对象 「变量」或「数组元素」 「指针」或「指针表达式」

我们也可以使用 + / - 运算指针。这样的效果是,让指针指向一个 新的地址。请看下面的例子,

#include <stdio.h>

int main() {
    int a = 1;
    int *p = &a;
    printf("%p\n", p);
    printf("%p\n", ++p);
}

运算结果
  
0x7ffee209e1bc
0x7ffee209e1c0   上面的地址 +4,因为一个整数占用 4 个 bit

在操作数组的时候,我们可以用「下标运算符[]」来获取元素。下标运算符是指针运算符的简写。下面的两种形式是等价的。

p[i]   <---> *(p + i)

由于加法的可交换性,下面的(奇怪的)形式也是等价的。

p[i]  <--->  *(p + i)  <--->  *(i + p)  <--->  i[p]

定义指针

            
│int│*│pointer
│   │ │           指针的名字            
│   │ └─────────────────── 
│   │          指针指向的变量             
│   └─────────────────────
│定义一个指针,指向一个整数变量                  
└─────────────────────────

引用指针

给指针变量赋值的语法如下,使用 &

/* 将变量 a 的地址赋予 pointer */
pointer = &a;

引用指针变量指向的变量,使用 *

/* 「使用」指针指向的变量 */
printf("%d", *pointer);
/* 对 指针指向的变量 「赋值」 */
*pointer = 2;

指向任意类型的指针

C 语言为我们提供了 void *,用于创建指向任意类型的指针。

#include <stdio.h>

int main() {
    int a = 5;
    void *p;
    p = &a;
    printf("%d\n", *p);  // 会报错,因为不知道 p 指向的内容 的类型
    printf("%d\n", * (int*) p);  // 将指针指向的变量定义为 int
}

我们不使用 void 的原因是,在给指针赋值之后,指针不知道自己指向的变量的类型。所以,我们没有办法直接使用 解除引用 之后的指针。

指针的指针

我们也可以创建,指向指针的指针。一个非常简单的例子如下。

int value = 5;
int *ptr = &value; // 普通的指针
int *ptr_to_ptr = &ptr; // 指针的指针

注意,我们不能连续用两次 & 来创建指针的指针。换言之,下面的代码是错误的。

int value = 5;
int **ptr_to_ptr = &&value; // 错误的创建方式

我们可以直接创建一个 空指针 nullptr,方法如下

int **ptr_to_ptr = nullptr;

我们可以使用 指针的指针 来处理「包含指针的数组」。下面代码给出了一个示范。

int **array = new int*[10];  // 分配一个数组,包含 10 个整数指针

指向函数的指针

函数可以 在表达式中 被解读成「指向函数的指针」。我们也可以创建新的指针,来指向某个函数。下面的代码展示了一个 指向函数的指针。

#include <stdio.h>

int sum(int a, int b){
    return a + b;
}

int main() {
    int (*func_ptr) (int, int);  // 创建指针
    func_ptr = sum;  // 给指针赋值
    int result = func_ptr(1, 2);  // 使用指针,不需要解引用
    printf("%d\n", result);
}

我们也可以创建「指向函数的指针 的 数组」。一个例子如下。

int (*func_table[]) (double) = {
  func0,  // 这些函数接受 double 参数,输出整数
  func1,
  func2
};

func_table[0](0.5)  // 使用函数

指针的应用

#include <stdio.h>

int main() {
    void swap(int *p1, int *p2);
    int a = 1, b = 2;
    int *p1 = &a, *p2 = &b;
    swap(p1, p2);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

void swap(int *p1, int *p2){
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

输出结果

a = 2, b = 1
#include <stdio.h>

int main() {
    int *p, a[3] = {1, 2, 3};
    p = a; // same as p = &a[0]
    printf("pointing %d\n", *p);
    p++;
    printf("after p++, pointing %d\n", *p);
    return 0;
}

输出结果

pointing 1
after p++, pointing 2

pointer = array 这个表达式自动把指针指向函数的第一个元素)

char str[5];
str[0] = "I";  // 合法,对字符串的「元素」赋值
str = "hello"; // 非法,str 是一个地址,是常量

二维数组 和 指针的指针

假设我们创建了二维数组 A[m][n], 那么 A[0] 指向第 1 行的地址,A[0][0] 指向第一行第一列。总体上,下面的关系成立。

*A = A[0]
**A = A[0][0]
*(A + i) = A[i]
&A[i][0] = &(*(A[i] + 0)) = A[i]
A[i][j] = *(A[i] + j) = *(*(A + i) + j)

假设 A 是一个二维 int 数组,那么在 C 语言里,A 其实是一个 指针的指针,int**。下面是一些与 A 相关的不同的 地址/指针 类型。

不同指针声明方法

C 语言 含义
int var var 是整形
int var[10] var 是整数的数组
int var[10][3] var 是整数的二维数组
int *var[10] var 是「指向整数的指针」的数组
double (*var)[3]; var 是指向「双精度浮点数数组」的指针
int func(int a); func 是接受整数参数,返回整数的函数
int (*func_p)(int a); func_p 是指向「接受整数参数,返回整数的函数」的指针

C 语言的声明不能按照从左到右(或者从右到左)的顺序解读。我们并没有很好的办法理解 C 语言的声明。

文件操作

打开文件

char filename[10]
FILE *f;
f = fopen(filename, "w");

关闭文件

fclose(f)

写入文件

fputc(input_char, f);

读取文件

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *file_pointer;
    char filename[] = "abc.txt", read_char;
    file_pointer = fopen(filename, "r");
    for (int i=0; i<5; i++){
        read_char = fgetc(file_pointer);
        putchar(read_char);
    }
    fclose(file_pointer);
    return 0;
}

输出结果

Hello

a.txt 内容:Hello World