C语言知识笔记1
此文章基于《C Programming Language (2nd Edition)》,主要是本人对 C 语言的具体认识以及对 C 程序语言的阅读感想。
A Tutorial Introduction
We have to concentrate on the basics: variables and constants, arithmetic, control flow, functions, and the rudiments of input and output.
The C programming language include pointers, structures, most of C’s rich set of operators, several control−flow statements, and the standard library.
简单粗暴的来说 C 语言的基础包含变量与常量,控制流,函数以及一些入门的输入输出。语言性质还包含了指针,结构体,算子集,以及标准库。
C 语言是一个面向过程的编程语言!
Getting Started
In C, the program to print hello,world is
1 |
|
you should create the program in a file whose name ends in .c
such as hello.c
compile it with the command
1 | cc hello.c |
当你照做后,你会发现在 GCC 编译器中:
1 | hello.c:4:1: error: return type defaults to ‘int’ [-Wimplicit-int] |
为什么会报错呢?因为在旧的 C 编译器中是允许这样的写法,但是随着 C 语言语法的规范化逐渐不再使用这种写法,为了代码的可移植性和清晰性,推荐写成
int main()
或者int main(int argc, char *argv[])
Our example is a function named main. Normally you are at liberty togive functions whatever names you like, but
main
is special − your program begins executing at thebeginning of main. This means that every program must have a main somewhere
1 |
这个是 C 的头文件,我们接下来一点一点讲解这行代码。#include
叫做文件包含命令,用来引入对应的头文件(.h 文件)。#include 也是 C 语言预处理命令的一种。
有两种常用的 #include 方式:
- 尖括号方式
< >
:
1 |
- 双引号方式
“ ”
:
1 |
使用双引号时,编译器首先会在当前源文件所在的目录查找头文件。如果没有找到,再去标准系统目录中查找。常用于引入用户自定义的头文件。
使用尖括号时,编译器会在标准系统目录(如编译器自带的头文件路径)中查找头文件。常用于引入标准库或第三方库的头文件。
C 语言一切都是从 main 开始的,main 是主函数,是程序开始的接口,任何函数都是从这里开始的。
The statements of a function are enclosed in braces { }. The function main contains only one statement
1 | printf("hello,world\n"); |
函数printf()
是一个库函数负责打印输出,在双引号中是要输出的字符串。\n
是转义字符,在这里是为了自动换行。
Notice that \n represents only a single character. An escape sequence like \n provides a general andextensible mechanism for representing hard−to−type or invisible characters. Among the others that C providesare \t for tab, \b for backspace, " for the double quote and \ for the backslash itself
Variables and Arithmetic Expressions
Any characters between /_ and _/are ignored by the compile
Comments may appear anywhere where a blank, tab or newline can.
这里提到了多行注释,用/* */
包括注释块,编译器会无视它。
In C, all variables must be declared before they are used, usually at the beginning of the function before anyexecutable statements. A declaration announces the properties of variables; it consists of a name and a list of variables, such as
1 |
|
在使用 C 语言的变量之前,必须先声明,一般在要使用的函数或可执行语句之前,C 读取程序是按照顺序一行一行读取的,所以变量一定要提前声明好。
常用的变量类型如下表所示:
类型关键字 | 描述 |
---|---|
char |
存储单个字符,占用 1 字节 |
int |
存储整数,通常占用 4 字节(具体大小取决于编译器和系统) |
float |
存储单精度浮点数,遵循 IEEE 754 标准 |
double |
存储双精度浮点数,比 float 有更高的精度和更大的范围 |
unsigned |
无符号类型,可以存储正数和零,通常比有符号类型有更大的范围 |
short |
短整型,通常占用 2 字节 |
long |
长整型,通常占用 8 字节(在某些系统上可能是 4 字节) |
long long |
长长整型,至少占用 64 位 |
void |
表示没有类型,用于函数没有返回值或指向无类型数据的指针 |
_Bool |
布尔类型,存储值为 0 或 1,定义在 C99 标准中 |
_Complex |
复数类型,用于存储复数,定义在 C99 标准中 |
Each line of the table is computed the same way, so we use a loop that repeats once per output line; this is thepurpose of the while loop
1 | while (i < j) |
如果每行都运行相同的程序,会显得很麻烦,可以使用 while 循环,在循环结构中,我们习惯性的按Tab
键缩进,虽然 C 不会在意程序的外观,但为了程序的可读性,这是需要的。我们用{}
包含循环内的程序,如果这里不用{}
,那么 C 就只会默认执行 while 内的第一行程序。
printf is a general−purpose output formatting function.Its first argument is a string of characters to be printed, with each % indicating where one of the other (second, third, …) arguments is to be substituted, and in what form it is to be printed.
1 | printf("%d\t%d\n", fahr, celsius); |
关于printf()
里面的%d
,以及类似的占位符请看下表
占位符 | 描述 |
---|---|
%d |
打印十进制整数(有符号) |
%i |
与 %d 相同 |
%u |
打印十进制整数(无符号) |
%o |
打印八进制整数 |
%x |
打印十六进制整数(小写字母) |
%X |
打印十六进制整数(大写字母) |
%c |
打印单个字符 |
%s |
打印字符串 |
%f |
打印浮点数(默认精度为 6 位小数) |
%lf |
打印双精度浮点数 |
%e |
打印科学计数法表示的浮点数 |
%g |
打印一般浮点数(根据数值的大小自动选择 %f 或 %e ) |
%p |
打印指针(地址) |
%% |
打印百分号字符 % |
%n |
打印到目前为止输出的字符数(用于计数) |
By the way, printf is not part of the C language; there is no input or output defined in C itself. printf is just a useful function from the standard library of functions that are normally accessible to C programs. The behaviour of printf is defined in the ANSI standard, however, so its properties should be the same with anycompiler and library that conforms to the standard.
这段话大致讲述了printf()
和 C 的关系,C 并非包含printf()
,而是 C 的标准库函数,所以需要引入标准库stdio.h
(standard input output)。
关于浮点数,以下占位符请见下表:
占位符 | 描述 | 示例 | 输出 |
---|---|---|---|
%f |
打印浮点数,默认精度为 6 位小数 | printf("%.2f\n", 123.456789); |
123.46 |
%e |
打印科学计数法表示的浮点数 | printf("%.2e\n", 123.456789); |
1.23e+02 |
%g |
打印一般浮点数,自动选择 %f 或 %e |
printf("%.2g\n", 123.456789); |
123.46 |
%lf |
打印双精度浮点数(与 %f 相同) |
printf("%.2lf\n", 123.456789); |
123.46 |
%.2f |
打印浮点数并保留两位小数 | printf("%.2f\n", 123.456789); |
123.46 |
%.3f |
打印浮点数并保留三位小数 | printf("%.3f\n", 123.456789); |
123.457 |
%.1f |
打印浮点数并保留一位小数 | printf("%.1f\n", 123.456789); |
123.5 |
The for statement
1 |
|
The for statement is a loop, a generalization of the while. If you compare it to the earlier while, it soperation should be clear.
for 语句有三个部分:
1 | fahr = 0 |
第一个部分在循环开始前执行,将0
赋值给fahr
1 | fahr <= 300 |
第二个部分,是循环的条件,条件满足时,循环工作,当fahr
达到300
时,代表循环结束
1 | fahr = fahr + 20 |
第三个部分相当于一个步长,每次循环 fahr 都会加 20
这三个部分都会控制循环的次数
Symbolic Constants
A #define line defines a symbolic name or symbolic constant to be aparticular string of characters
1 |
LOWER
UPPER
STEP
这些都是符号常量,注意,不是变量,而且这些值相当于是预处理命令,用#define
来设置。
Character Input and Output
The standard library provides several functions for reading or writing one character at a time, of which
getchar
andputchar
are the simplest. Each time it is called, getchar reads the next input character from a text stream and returns that as its value.
1 | c = getchar(); |
getchar()
用于从键盘中检测输入的字符,注意不是字符串putchar()
用于输出字符,虽然有的地方会写用puts()
函数
File Copying
这里有一个复制一个字符的例子:
1 |
|
这里的
!=
的意思是“不等于”
EOF
是一个表示文件结束(End of File)的常量。它用于标识当程序从输入流中读取到文件或数据流的末尾时的状态。EOF 通常被定义为 -1,并且是在 stdio.h 头文件中定义的宏。
这个程序的意思是读取一个字符存入c
,检查c
是否为EOF
,不是的话将c
输出,继续读取字符,直到EOF
条件达成,循环停止
File Counting
接下来看一下这个计数的程序
1 |
|
The statement
++nc
persents a new operator++
, whitch means increment by one.
这样的操作可以避免写 nc = nc+1
,这种写法不仅简单,还更有效率。
在这个程序中涉及到了 long
这种变量,而不是int
,这里的long
是一个长整型变量,它的存储比整型更高位的数字,需要使用%ld
告诉printf()
这个变量是长整型
可以把这个程序改成 for 循环类型,以及双精度浮点数
1 |
|
The body of this for loop is empty, because all the work is done in the test and increment parts. But thegrammatical rules of C require that a for statement have a body. The isolated semicolon, called a nullstatement, is there to satisfy that requirement. We put it on a separate line to make it visible.
Line Counting
这是一个记录行数的代码
1 |
|
C 语言是支持嵌套的,程序中while()
里面嵌套了if
。==
这里是等号,为了和=
(赋值)作区分
Word Counting
1 |
|
state = OUT;
:初始状态为 OUT,即程序开始时不在单词内。nl = nw = nc = 0;
:行数、单词数和字符数均初始化为 0。
#define IN 1
和 #define OUT 0
:定义两个符号常量,IN 表示程序处于一个单词的内部,OUT 表示程序处于一个单词的外部。这种定义方式也被成为宏定义。
if (c == ' ' || c == '\n' || c == '\t') state = OUT;
:如果读取到空格、换行符或制表符,则表示不在单词内部,state 设置为 OUT。||
代表着或逻辑,所有参数中只要有一个参数为True
,所有的参数都返回True
。
Arrays
Let is write a program to count the number of occurrences of each digit, of white space characters (blank, tab,newline), and of all other characters. This is artificial, but it permits us to illustrate several aspects of C in one program:
1 | main() |
输出:digits = 9 3 0 0 0 0 0 0 0 1, white space = 123, other = 345
The declaration
int ndigit[10];
declares ndigit to be an array of 10 integers. Array subscripts always start at zero in C, so the elements are ndigit[0], ndigit[1], …, ndigit[9]. This is reflected in the for loops that initialize and print the array
int ndigit[10];
:定义一个数组,数组大小为 10,每个元素类型为整型。数组的[]
我们称为下标(subscript),数组的下标都是从 0 开始,所以数组的元素为ndigit[0]
,ndigit[1]
,…,ndigit[9]
。for (i = 0; i<10; ++i)
:初始化数组,将数组所有元素初始化为 0。if(c >= '0' &c <='9')
:判断当前字符是否为数字,如果是,则将当前字符对应的数组元素加 1。else if (c == ' ' || c == '\n' || c == '\t')
:判断当前字符是否为空格、换行符或制表符,如果是,则将nWhite
加 1。else
:判断当前字符是否为其他字符,如果是,则将nother
加 1。printf("digits = ");
:输出”digits = “。for(i = 0;i<10; ++i)
:输出数组中的元素,格式为%d
,表示整型。printf((", white space = %d, other = %d\n",nwhite,nother));
:输出nwhite
和nother
的值,格式为%d
,表示整型。
- 题外话:
数组是最基础的数据结构之一,提到数据结构,往往会想到增、删、查、改四个操作,我们可以通过下标访问数组中的元素,也可以通过下标修改数组中的元素。
- 首先初始化数组
1
2
3
4
int data[MAX_SIZE]; // 定义数组
int count = 0; // 当前存储的数据数量 - 添加元素(增)
添加元素时,如果数组已满,则无法直接添加。可以通过检查当前计数器是否小于最大容量来判断是否可以添加。1
2
3
4
5
6
7void add(int value) {
if (count < MAX_SIZE) {
data[count++] = value;
} else {
printf("数组已满,无法添加。\n");
}
} - 删除元素(删)
由于数组是连续的,删除特定值时,不会自动移位,需要遍历数组找到该值并将其后的所有元素向前移动一位。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// 从数据集中删除指定值的函数
// 参数 value: 要删除的值
// 注意:这个函数假设有一个全局变量 'data' 用于存储数据,以及一个全局变量 'count' 表示当前数据集中的元素数量
void remove(int value) {
// 初始化遍历索引
int i;
// 遍历数据集查找要删除的值
for (i = 0; i < count; i++) {
// 如果找到了要删除的元素
if (data[i] == value) {
// 结束循环,i现在指向要删除的元素
break;
}
}
// 如果找到了要删除的元素
if (i < count) {
// 将所有在要删除元素后的元素向前移动一位
while (i < count - 1) {
data[i] = data[i + 1];
i++;
}
// 减少数据集的计数,表示一个元素已被删除
count--;
// 输出删除成功的消息
printf("删除成功。\n");
} else {
// 输出未找到该元素的消息
printf("未找到该元素。\n");
}
} - 查找元素(查)
查找元素可以直接通过索引访问,也可以搜索某个值是否存在。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 通过索引查找
int find_by_index(int index) {
if (index >= 0 && index < count) {
return data[index];
} else {
return -1; // 假设-1表示未找到
}
}
// 搜索值
int find_value(int value) {
for (int i = 0; i < count; i++) {
if (data[i] == value) {
return i; // 返回索引
}
}
return -1; // 未找到
} - 修改元素(改)
修改元素通常也是基于索引来操作。1
2
3
4
5
6
7void update(int index, int new_value) {
if (index >= 0 && index < count) {
data[index] = new_value;
} else {
printf("索引超出范围。\n");
}
}
Functions
A function provides a convenient way to encapsulate some computation, which can then be used without worrying about its implementation.
函数提供了一种封装计算,使其可以方便使用的方式。函数可以被重复使用,而无需关心其内部实现。
之前我们就已经学会使用一些函数了比如getchar()
,putchar()
,printf()
等。现在,是时候自己来写一个函数了。
C最早是无法像Fortran或者Python那样用**
来进行幂运算,所以下面的程序我们自定义一个函数用来求幂。
1 |
|
函数的定义遵循以下格式:
1 | return_type function_name(parameter declaration if any) |
函数的声明格式为:
1 | return_type function_name(parameter declaration if any); |
在函数声明时是可以不写出参数的声明,但是函数定义时必须写出参数的声明。
如果函数没有返回值,可以省略return_type
,直接写void
。
Arguments - Call by Value
In C, all function arguments are passed
by value
. This means that the called function is given the values of its arguments in temporary variables rather than the originals. This leads to some different properties than are seen withcall by reference
languages like Fortran or with var parameters in Pascal, in which the called routine has access to the original argument, not a local copy.
在C中,所有函数参数都是通过值传递的。这意味着被调用的函数被赋予了参数的临时变量而不是原始值。这导致一些与call by reference
语言(如Fortran或Pascal中的var参数)中看到的不同特性。
- 函数参数是临时变量:在函数调用时,函数参数被赋予临时变量,而不是原始参数。
- 函数参数是独立的:函数参数是独立的,对函数参数的修改不会影响原始参数。
- 函数参数是只读的:函数参数是只读的,不能修改函数参数的值。
请看下面程序
1 | /* 函数参数是临时变量 */ |
输出为:
a = 10
a = 20
a = 10
1 | /* 函数参数是独立的 */ |
输出为:
a = 10
a = 20
a = 20
a = 20
1 | /* 函数参数是只读的 */ |
输出为:
a = 10
a = 20
a = 10
Character Arrays
The most common type of array in C is the array of characters. To illustrate the use of character arrays and functions to manipulate them, let’s write a program that reads a set of text lines and prints the longest. The outline is simple enough:
1 | while ( there's another line) |
External Variables and Scope
外部变量是在函数外部定义的变量,可以在整个程序中访问和修改。
在C语言中,外部变量的声明和定义必须在同一个作用域内,否则会报错。
- 在函数内部定义的变量是局部变量,只能在函数内部访问和修改。
- 在函数外部定义的变量是外部变量,可以在整个程序中访问和修改。
- 在函数内部定义的变量和函数外部定义的变量可以重名,但作用域不同。
请看下面程序1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a = 10;
void func()
{
int a = 20;
printf("a = %d\n", a);
}
int main()
{
printf("a = %d\n", a);
func();
return 0;
}输出为:
a = 10
a = 20
关于外部链接(extern)
在C语言中,外部链接(extern)用于声明一个外部变量或函数,使得该变量或函数可以在多个文件中被引用。
- 在一个文件中定义一个外部变量或函数,然后在另一个文件中声明该变量或函数,使得两个文件中的变量或函数可以互相访问。
- 在一个文件中定义一个外部变量或函数,然后在另一个文件中定义同名的变量或函数,使得两个文件中的变量或函数可以互相访问,但作用域不同。
请看下面程序在实际开发中,还是尽量避免使用全局变量,因为全局变量容易造成命名冲突,难以维护和理解。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
// 声明全局变量 a
extern int a = 10;
// 函数 func 修改全局变量 a 的值并打印
void func()
{
printf("In func: a = %d\n", a);
a = 20;
printf("After modification in func: a = %d\n", a);
}
int main()
{
// 打印初始值
printf("Initial value of a: a = %d\n", a);
// 调用 func 函数
func();
// 打印修改后的值
printf("Final value of a: a = %d\n", a);
return 0;
}
第一章总结
第一章是一个整体的介绍,开门见山地讲了C是有什么语法,有什么数据结构,变量有什么定义方式,程序结构是什么,表达式和运算符怎么用等等,是比较基础的一些内容,但没有对语言的细节进行介绍比如函数参数的指针的用法,结构体等等。这一章主要目的是让读者快速了解 C 语言的基本框架和核心概念,为后续章节的学习打下基础。所以第一章并没有深入探讨一些更复杂的语言特性,例如:
函数参数的指针用法:如何通过指针传递参数,以及如何在函数中修改传入的变量。
- 结构体:如何定义和使用结构体来组织复杂的数据。
- 指针操作:如何使用指针进行内存操作和动态分配。
- 高级控制结构:如 switch 语句、goto 语句等更复杂的控制流。
- 文件操作:如何读写文件,进行更复杂的 I/O 操作。
这些更详细的语言特性和高级用法将在后续章节中逐步展开。第一章的主要目标是帮助读者快速入门,并为后续学习奠定基础。