简介
c 语言程序的创建过程
- 编辑: 修改源代码的过程
- 编译
- 编译器可以将源代码转换成机器语言
- 编译器能找出程序中很多无效和无法识别的错误,以及结构错误,例如程序的某个部分永远不会执行。
- 编译器输出的结构成为对象代码,存放他们的文件成为对象文件。
- 在linux中这些文件的扩展名通常是.o,在windows下面这些文件的扩展名通常是.
- 如果编译成功就会生成一个文件,它与源文件同名。但扩展名为.o或者.obj
- 链接
- 链接器将源代码文件中由编译器产生的各种对象模块组合起来,再从** c 语言提供的程序库中添加必要的代码模块,将他们组合成一个可执行文件**。
- 链接器也可以检测和报告错误,例如程序中引用了一根本不存在的库组件。
- 链接一旦成功,就会生成可执行文件,在windows下面可执行文件的扩展名是.exe,在linux下面,可执行文件没有扩展名,但它是可执行的文件类型。
- 执行
c 语言程序的结构
预处理指令
符号 # 表示这是一个预处理指令,告诉编译器在编译源代码之前,要先执行一些操作。例如:
#include <stdio.h>
函数
- 函数的结构
- 函数首部: 函数的第一行
- 函数体: 花括号内的部分
- 主函数
- 每个c程序都由一个或多个函数组成,但每个c程序都必须有一个
main()函数 - 每个程序总是从
main()函数开始执行。
- 每个c程序都由一个或多个函数组成,但每个c程序都必须有一个
常量和变量
常量
- 整形
- 实形
- 1.1
- 1.1e5 = 1.1 * 10^5
- 字符
- 普通字符: 单撇号括起来的一个字符
- 转义字符
| 转义字符 | 含义 | 转义字符 | 含义 |
|---|---|---|---|
| \n | 换行 | \t | 水平制表 |
| \v | 垂直制表 | \b | 退格 |
| \r | 回车(不换行) | \f | 换页 |
| \a | 响铃 | \ | 反斜线 |
| ' | 单引号 | " | 双引号 |
| \ddd | 3 位 8 进制代表的字符 | \xhh | 2 位 16 进制代表的字符 |
变量
- 形式: <类型名称> <变量名称>变量名称>类型名称>
- 介绍
- 变量代表一个有名字的、具有特殊属性的一个存储单元。
- 变量可以用来保存数据。
- 变量的值是可以改变的。
- 标识符
- 变量的名字
数据类型
整形
- 编译系统分配给 int 类型数据2个字节或者4个字节(由具体的c编译系统自行决定)。
- 我们使用的gcc编译器为每个整数类型分配四个字节(32个 bit)
- 在存储单元中的存储方式是:用整数的补码形式存放。所以当4个字节的整数类型取值范围是 -2^31 ~ (2^31-1) 。
- 无符号基本整型表示为 unsigned int ,和 int 类型占有的字节数相同,取值范围是0~2^32-1。
短整形 short
- gcc 编译系统分配给
short类型为 2 个字节,存储方式和int类型一样,也是补码的形式存储,取值范围是 -2^15 ~ 2^15-1 - 无符号短整型
unsigned short取值范围是0~2^16-1
长整形 long
- gcc 编译系统分配给 long 类型为 8 个字节,存储方式和
int类型一样,也是补码的形式存储,取值范围是 -2^63 ~ 2^63-1 - unsigned long 取值范围是 0 ~ 2^64-1
浮点型
单精度 float
- gcc编译系统为每一个
float型变量分配4个字节,数值以规范化的二进制数指数形式存放在存储单元中。 - 在存储时,系统将实型数据分成小数部分和指数部分两个部分、分别存储。一个具体的例子如下,
+ 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
- 为了能扩大数字的范围,用 8 个字节(64位)存储一个
double型数据,可以得到15位有效数字
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
介绍
- c语言中,字符型的基本类型符是
char。 - 所有的编译系统中都规定以 1 个字节(8 位)来存放一个字符。
- 字符型数据在存储时,并不是把该字符本身存放到内存单元中,而是把该字符相应的 ASCII 码值存放到该存储单元中。
字符的输出
#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
其他类型
- 构造类型
- 数组
- 字符串
- 结构体 struct
- 共用体 union
- 枚举类型
- 指针类型
- 空类型
void
强制类型转换
格式:(类型名) (表达式)
例子
#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 // 取余数
整数和整数的结果是整数
自增、自减运算符
++i: 先给 i +1,再使用 ii++:先使用 i,再给 i +1--i: 先给 i -1,再使用 ii--: 先使用 i,再给 i -1
不同数据的混合运算
- 基本规则: 混合运算的时候,把低精度转换到高精度
| 类型(优先级从高到低) |
|---|
| long double |
| double |
| float |
| long |
| unsigned int |
| int |
| short |
| char |
数据的输入和输出
格式化字符
%d: 带符号 - 十进制 - 整数%md:以m指定的字段宽度输出,右对齐%ld:输出长整型数据%mld:输出指定宽度的长整型数据
%o: 无符号 - 八进制 - 整数%x: 无符号 - 十六进制 - 整数%u: 无符号 - 十进制 - 整数%f: 小数点形式 - 单/双精度实数%α.βf:数据占α列,其中小数点占β列
%e: 指数形式 - 单/双精度实数%g: 选用输入/输出宽度较小的格式输出实数%c: 字符 - 单个字符%s: 字符 - 字符串
输入输出字符
scanf- 格式:
scanf("%[fdcs]", &var) - 用
%c格式声明输入字符时,空格字符和转义字符都是作为有效字符输入 - 输入的时候,要连续按下三个字母,中间不能有空格
- 用其它格式字符输入的时候,数字之间用空格隔开
- 格式:
putchar- 格式:
putchar(c) - 输出变量 c 所代表的一个字符
- 格式:
getchar- 从输入设备上得到的字符
- 注意也可以输入回车字符
顺序结构设计
条件表达式
格式:如果 (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)
- 表达式_1: 对一个值初始化。该表达式可以省略,但是它后面的「分号」不能省略。
- 表达式_2: 当该表达式为 真 的时候,循环继续。该表达式可以省略,但是它后面的「分号」不能省略。省略它之后,
for循环会无限循环。 - 表达式_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’ 作为结束符
我们可以使用下面的两个方法(%s 和 puts)来打印字符串。
#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;
}
- 连接两个字符串
strcat
#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
- 测字符串长度的函数
strlen
#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
- 字符串
strcpy
#include <stdio.h>
#include <string.h>
int main() {
char A[10];
char B[] = "message";
strcpy(A, B); // 将 B 的内容复制到 A
puts(A);
return 0;
}
输出结果
message
- 比较字符串
strcmp
#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’
- 如果全部字符相同,则认为两个字符串相同。
- 若出现不同的字符,则以第 1 对不相同的字符的比较结果为准。
例如: “A”<“D”,”e”>”E”,”these”>”that”,”computer”>”compare”
比较结果由函数值返回:
- 字符串1 = 字符串2,则函数值为 0
- 字符串1 > 字符串2,则函数值为一个「正」整数
- 字符串1 < 字符串2,则函数值为一个「负」整数
指针
指针的基本用法
下面的例子展示了指针的基本用法。
#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 相关的不同的 地址/指针 类型。
&A:整个「指针的指针」的地址。&A[0]:与 A 相同,是「第一行」的地址。&A[0][0]:第一行第一列的地址,是一个int。A:指向指针的指针,相当于int (*) [n]*A: 指向整数的指针,相当于int *
不同指针声明方法
| 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