云计算百科
云计算领域专业知识百科平台

【C语言】初识 C 语言(点到为止)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

C 语言诞生于 20 世纪 70 年代。它的语法并不复杂,却能直接操作内存、组织底层资源,因此至今仍广泛用于操作系统、嵌入式、编译器、驱动和高性能程序。

从第一性原理看,程序只有两件事:保存数据,处理数据。C 语言提供了类型、变量、运算符、流程控制、函数、数组、指针和结构体,帮助我们把这两件事表达清楚。

本文不追求一次讲完所有细节,而是沿着一条主线,走完 C 语言最重要的基础概念。


1. 第一个 C 语言程序

#include <stdio.h>

int main(void) {
printf("Hello, C!\\n");
return 0;
}

main 是程序的入口函数。一个可执行的 C 程序必须有且只有一个 main 函数,程序从这里开始执行。

逐行看:

  • #include <stdio.h>:包含标准输入输出库的声明,printf 的声明就在这里。
  • int main(void):定义一个名为 main 的函数,它返回 int,且不接收参数。
  • printf(…):把文字输出到终端。
  • return 0:告诉操作系统程序正常结束。

C 源文件不会被计算机直接执行。编译器会先把源代码翻译成目标代码,再由链接器把目标代码和库连接起来,最终生成可执行文件。

在这里插入图片描述


2. 数据类型:数据应该如何解释

计算机内存本质上只是一串字节。数据类型告诉编译器:这些字节代表什么,以及要占用多少空间。

C 语言常见的基本类型如下:

类型用途典型大小*
char 字符或小整数 1 字节
short 短整型 2 字节
int 常用整型 4 字节
long 长整型 4 或 8 字节
long long 更大范围的整型 8 字节
float 单精度浮点数 4 字节
double 双精度浮点数 8 字节
bool 真或假 通常 1 字节

具体大小由编译器和平台决定,32 位平台小一些(4 字节),64 位平台大一些(8 字节)。

声明变量时,需要同时写出类型和名字:

#include <stdio.h>

int main(void) {
char ch = 'A';
int count = 10;
double price = 1.23;

printf("%c %d %.2f\\n", ch, count, price);
printf("int: %zu bytes\\n", sizeof(int));
return 0;
}

sizeof 计算对象或类型占用的字节数,结果类型是 size_t,通常使用 %zu 输出。

类型不仅决定大小,也决定运算规则。例如,两个 int 相除仍是整数除法:5 / 2 的结果是 2;如果希望得到小数,需要让至少一个操作数参与浮点运算:5.0 / 2。

*注:在 C99 至 C17 中,使用 bool、true 和 false 需要包含头文件:

#include <stdbool.h>

bool finished = false;

这些版本中的 bool 实际上由标准库映射到内置类型 _Bool。从 C23 开始,bool、true 和 false 可以直接使用。日常代码写 bool 即可,认识 _Bool 是为了能看懂标准和旧代码。


3. 变量与常量

程序需要保存数据。数据在执行过程中可能改变,就用变量;不应改变或希望明确表达固定含义,就用常量。

3.1 变量的定义

int age = 18;
double score = 95.5;
char grade = 'A';

一条变量定义可以理解为:向编译器申请一块符合指定类型的存储空间,给它起一个名字,并在需要时写入初始值。

定义但没有初始化的局部变量,其值是不确定的,使用前必须先赋值:

int n; // 不要直接读取 n
n = 10; // 先赋值,再使用

3.2 局部变量与全局变量

#include <stdio.h>

int value = 100; // 全局变量

int main(void) {
int value = 300; // 局部变量,与全局变量同名
printf("%d\\n", value);
return 0;
}

输出是 300。当局部变量和全局变量同名时,在该局部作用域内,局部变量会遮蔽全局变量。

3.3 作用域与生命周期

  • 作用域:名字可以被使用的代码范围。块内定义的局部变量通常只在该块中可见;普通全局变量从定义处开始,在整个源文件可见,也可以通过 extern 跨文件声明。
  • 生命周期:对象实际存在的时间。普通局部变量进入代码块时创建,离开代码块时销毁;全局变量从程序启动一直存在到程序结束。

3.4 使用输入输出

#include <stdio.h>

int main(void) {
int a = 0;
int b = 0;

printf("请输入两个整数:");
if (scanf("%d %d", &a, &b) == 2) {
printf("%d + %d = %d\\n", a, b, a + b);
}
return 0;
}

printf 负责输出,scanf 负责输入。scanf 需要把输入结果写入变量,所以要传入变量的地址:&a、&b。关于地址和指针,后文会详细解释。

3.5 常量

C 中常见的“固定值”有四种形式:

10 // 字面常量
const int days = 7; // const 对象:不能通过 days 修改
#define MAX 100 // 宏常量:预处理阶段进行文本替换
enum color { RED, GREEN, BLUE }; // 枚举常量

const 表示通过这个名字不能修改对象,但它不等同于 C++ 意义上的编译期常量;需要预处理替换时才使用 #define,并注意宏没有类型检查。


4. 字符、字符串与转义字符

4.1 字符与字符串

字符常量用单引号表示,例如 'A';字符串字面量用双引号表示,例如 "Hello"。

C 没有独立的字符串类型。字符串本质上是一个以空字符 \\0 结尾的 char 数组:

char text[] = "Hello";

内存中实际存放的是 6 个字符:H、e、l、l、o、\\0。

!c-string-memory.svg

如果没有 \\0,像 printf("%s", text) 这样的字符串函数就不知道应该在哪里停止读取。

4.2 转义字符

反斜杠 \\ 引出转义字符,用来表示换行、制表或源代码中不方便直接写出的字符。

printf("Hello\\nWorld\\n");
printf("A\\tB\\n");
printf("c:\\\\code\\\\test.c\\n");

输出目录时必须把反斜杠写成 \\\\。否则,\\t 会被解释为制表符:

printf("c:\\code\\test.c"); // 错误示例:\\t 会变成 Tab
printf("c:\\\\code\\\\test.c"); // 正确

常用转义字符:

写法含义
\\0 空字符,字符串结束标志
\\n 换行
\\t 水平制表符
\\\\ 反斜杠
\\" 双引号
\\' 单引号
\\r 回车,将光标移到当前行开头
\\b 退格
\\a 响铃(是否有声音取决于终端)

C 还支持八进制 \\ooo 和十六进制 \\xhh 表示字符,例如 \\x41 表示字符 A。

4.3 注释

注释只给人阅读,编译器会忽略它们:

/* C 风格注释,可以跨多行 */
// C99 及之后支持的单行注释


5. 选择语句:根据条件决定路径

C 中,条件表达式的结果为 0 时表示假,非 0 时表示真。

#include <stdio.h>

int main(void) {
int score = 85;

if (score >= 60) {
printf("及格\\n");
} else {
printf("不及格\\n");
}
return 0;
}

if 先计算条件,再执行一个分支。else if 可以继续添加条件:

if (score >= 90) {
printf("A\\n");
} else if (score >= 60) {
printf("B\\n");
} else {
printf("C\\n");
}

当要根据同一个表达式匹配多个固定值时,可以使用 switch:

switch (choice) {
case 1:
printf("开始\\n");
break;
case 2:
printf("退出\\n");
break;
default:
printf("未知选项\\n");
break;
}

break 用来结束当前 switch。如果省略它,程序会继续执行下一个 case,这叫“贯穿”。


6. 循环语句:重复执行一段代码

循环的核心是:先判断是否满足条件,满足就执行一次,再回到判断处。

6.1 while

int count = 1;
while (count <= 10) {
printf("%d ", count);
count++;
}

while 先判断条件,因此一次也可能不执行。

6.2 for

for (int count = 1; count <= 10; count++) {
printf("%d ", count);
}

for 把初始化、条件和迭代写在一行,适合计数循环。上面的代码等价于:

int count = 1;
while (count <= 10) {
printf("%d ", count);
count++;
}

6.3 do…while

int count = 1;
do {
printf("%d ", count);
count++;
} while (count < 1);

do…while 先执行一次,再判断条件,因此至少执行一次。注意结尾的分号不能省略。

循环中还常用两个控制语句:

  • break:立即结束当前循环。
  • continue:跳过本轮剩余代码,进入下一轮判断。

7. 函数:给一段操作命名

函数是完成特定任务的一段代码。它可以接收输入(参数),经过处理后返回结果,从而避免重复编写相同逻辑。

#include <stdio.h>

int add(int x, int y) {
return x + y;
}

int main(void) {
int x = 0;
int y = 0;

printf("请输入两个整数:");
scanf("%d %d", &x, &y);
printf("结果:%d\\n", add(x, y));
return 0;
}

函数定义包含返回类型、函数名、参数列表和函数体。如果函数定义写在调用之后,需要先声明函数原型:

int add(int x, int y); // 函数声明

C 的参数传递默认是值传递:调用函数时,实参的值会复制给形参。函数内修改形参,不会改变调用者的变量;如果要修改调用者的变量,需要传递地址,这正是指针的用途。


8. 数组:连续存放同类型数据

数组是一组类型相同、在内存中连续存放的元素。数组长度在定义时确定,元素下标从 0 开始。

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

for (int i = 0; i < 5; i++) {
printf("nums[%d] = %d\\n", i, nums[i]);
}

如果初始化列表少于数组长度,剩余元素会被初始化为 0:

int nums[5] = {1, 2}; // 实际为 {1, 2, 0, 0, 0}

访问越界不会自动报错,结果是未定义行为。数组长度必须由程序员保证。

在数组仍是数组的作用域内,可以用下面的方式计算元素个数:

size_t length = sizeof nums / sizeof nums[0];

数组作为函数参数时会退化为指针,此时 sizeof 得到的是指针大小,不能再用上面的公式计算长度,通常需要额外传入长度。


9. 操作符:对数据执行操作

操作符连接一个或多个操作数,产生一个结果,或改变对象的值。

类别操作符作用示例
算术 + – * / % 加、减、乘、除、取余 a + b
自增自减 ++ — 加一、减一 i++
关系 < <= > >= == != 比较大小或相等 a == b
逻辑 `&& !`
位运算 `& ^ ~ << >>` 按位与、或、异或、取反、移位
赋值 = += -= *= /= %= … 给变量赋值 a += 1
指针 & * 取地址、解引用 &a、*p
成员访问 . -> 访问结构体成员 s.age、p->age
下标与调用 [] () 访问数组、调用函数 arr[i]、f()
条件 ?: 根据条件返回两个结果之一 a > b ? a : b
其他 sizeof、(type)、, 求大小、类型转换、逗号表达式 sizeof(int)

9.1 最容易混淆的符号

  • = 是赋值,== 是比较:a = 3 把 3 写入 a,a == 3 判断 a 是否为 3。
  • & 可能表示按位与,也可能表示取地址,要看上下文:a & b 是按位与,&a 是取地址。
  • 可能表示乘法、声明指针或解引用:a * b、int *p、*p。
  • | 是按位或,|| 是逻辑或;& 是按位与,&& 是逻辑与。

9.2 优先级

常见优先级从高到低大致是:

  • ()、[]、.、>
  • 一元运算符:!、~、++、-、&、、sizeof
  • 、/、%
  • +、
  • 移位:<<、>>
  • 关系比较与相等比较
  • 位运算:&、^、|
  • 逻辑运算:&&、||
  • ?:
  • 赋值运算符
  • ,
  • 不确定优先级时,直接加括号。括号不仅能改变求值顺序,也能让代码更容易阅读。


    10. 关键字

    关键字是 C 语言保留的单词,不能用作变量名、函数名或结构体成员名。常见关键字按用途可以分为:

    类别关键字
    类型 char、short、int、long、float、double、void、signed、unsigned、struct、union、enum
    流程控制 if、else、switch、case、default、for、while、do、break、continue、goto、return
    存储与链接 auto、register、static、extern
    类型与限定 const、volatile、sizeof、typedef

    不同 C 标准定义的关键字略有差异,例如 C99 引入了 _Bool、inline 和 restrict,C23 又将 bool、true 和 false 纳入语言。实际编程时,优先掌握上表,再根据项目采用的 C 标准查阅完整列表。

    10.1 static:改变生命周期或可见范围

    static 放在不同位置,含义不同。

    使用位置作用
    局部变量前 生命周期延长到整个程序结束,但作用域不变
    全局变量前 只允许当前源文件访问(内部链接)
    函数定义前 只允许当前源文件调用(内部链接)
    static 修饰局部变量

    #include <stdio.h>

    void normal(void) {
    int count = 0;
    printf("normal: %d\\n", ++count);
    }

    void persistent(void) {
    static int count = 0;
    printf("static: %d\\n", ++count);
    }

    int main(void) {
    normal(); normal(); normal();
    persistent(); persistent(); persistent();
    return 0;
    }

    language/c/1intro 󰊢 master !?./main
    normal: 1
    normal: 1
    normal: 1
    static: 1
    static: 2
    static: 3

    输出中,normal 每次都是 1;persistent 依次是 1、2、3。静态局部变量只初始化一次,之后保留上一次调用结束时的值。静态存储期对象如果没有显式初始化,默认初始化为 0。

    static 修饰全局变量或函数

    先看一个可以正常跨文件访问的例子。下面是两个源文件:

    // counter.c
    int g_count = 0;
    void reset(void) { g_count = 0; }

    // main.c
    #include <stdio.h>

    extern int g_count;
    void reset(void);

    int main(void) {
    g_count = 42;
    reset();
    printf("g_count = %d\\n", g_count); // 0
    return 0;
    }

    编译时把两个源文件一起交给编译器:

    cc counter.c main.c -o demo

    这里的 g_count 和 reset 都具有外部链接,main.c 可以通过声明使用它们。

    如果把 counter.c 中的变量改成 static:

    // counter.c
    static int g_count = 0;

    void reset(void) {
    g_count = 0;
    }

    此时 g_count 只在 counter.c 内可见。reset 仍然可以被其他文件调用,但 main.c 中的 extern int g_count; 将无法链接到它。通常的做法是:把数据设为 static,只对外提供操作数据的函数。

    同理,如果把函数定义为 static:

    // counter.c
    static void reset(void) {
    /* 只能在 counter.c 中调用 */
    }

    那么 main.c 即使写出 void reset(void);,调用时也会在链接阶段报“未定义引用”。

    因此,文件作用域的 static 并不是改变变量的生命周期,而是把变量或函数的链接属性改为“仅当前源文件可见”。
    !c-static-linkage.svg


    11. #define:预处理阶段的替换

    #define 属于预处理指令,不是变量定义。预处理器会在编译前,把宏名替换成宏体。

    #include <stdio.h>

    #define MAX 100
    #define ADD(x, y) ((x) + (y))

    int main(void) {
    printf("%d\\n", MAX);
    printf("%d\\n", ADD(1, 2));
    return 0;
    }

    宏参数和整个宏体都应尽量加括号,避免运算优先级导致意外结果。宏没有类型检查,也可能重复计算参数,因此复杂逻辑通常更适合写成函数。


    12. 指针:保存地址的变量

    12.1 先理解内存

    程序运行时,变量需要放在内存中。内存可以看成许多连续的小格子,每个格子至少有一个地址;在 C 语言里,地址是一个用于定位对象的数值。

    需要区分两个概念:地址是位置,指针是保存地址的对象(通常就是指针变量)。 地址本身不是指针变量。

    int a = 10;
    printf("%p\\n", (void *)&a);

    &a 是取地址运算,得到变量 a 的地址。%p 用于输出指针,按 C 标准应转换为 void *。

    !c-variable-address.svg

    12.2 定义和使用指针

    #include <stdio.h>

    int main(void) {
    int a = 10;
    int *p = &a;

    printf("a = %d\\n", a);
    printf("address = %p\\n", (void *)p);
    printf("*p = %d\\n", *p);
    return 0;
    }

    int *p 表示 p 是一个“指向 int 的指针”;p 中保存 a 的地址。*p 是解引用运算,表示沿着 p 保存的地址访问那块内存,因此 *p 的值就是 a 的值。

    指针类型很重要。int *、double * 虽然都保存地址,但解引用时,编译器会按不同类型解释内存,并使用不同的步长。指针变量自身的大小由平台决定,可以用 sizeof p 查询,不要假定永远是 4 或 8 字节。

    未指向有效对象的指针不能解引用。没有目标时,应初始化为空指针:

    int *p = NULL;

    12.3 为什么需要指针

    C 的参数传递是值传递。下面的 x 是 a 的副本:

    void change(int x) {
    x = 20;
    }

    int main(void) {
    int a = 10;
    change(a);
    printf("%d\\n", a); // 10
    return 0;
    }

    !c-pass-by-value.svg

    如果把地址传进去,函数就能找到并修改原变量:

    #include <stdio.h>

    void change(int *pa) {
    *pa = 20;
    }

    int main(void) {
    int a = 10;
    change(&a);
    printf("%d\\n", a); // 20
    return 0;
    }

    这里发生了三步:

  • &a 得到 a 的地址;
  • pa 保存这个地址;
  • pa = 20 根据地址找到 a,把 20 写回原来的内存。
  • !c-pointer-mutation.svg

    可以把 *pa 暂时理解为“通过 pa 找到的那个变量”,在本例中就是 a。


    13. 结构体:把相关数据放在一起

    数组只能保存一组相同类型的数据。现实中的一个对象往往由不同类型的数据组成,例如学生同时有姓名、年龄和学号。结构体用于描述这样的复合数据。

    #include <stdio.h>

    struct student {
    char name[20];
    int age;
    char id[15];
    };

    int main(void) {
    struct student s = {"张三", 18, "20260001"};
    struct student *ps = &s;

    printf("name = %s, age = %d, id = %s\\n",
    s.name, s.age, s.id);
    printf("name = %s, age = %d, id = %s\\n",
    ps->name, ps->age, ps->id);
    return 0;
    }

    • s.name:通过结构体变量访问成员。
    • ps->name:通过结构体指针访问成员。
    • ps->name 等价于 (*ps).name。括号不能省略,因为 . 的优先级高于 。

    结构体成员按声明顺序排列,但编译器可能为了对齐在成员之间插入填充字节,因此 sizeof(struct student) 不一定等于各成员大小的简单相加。

    如果不想每次都写 struct student,可以用 typedef 起别名:

    typedef struct {
    char name[20];
    int age;
    char id[15];
    } Student;

    Student s = {"张三", 18, "20260001"};


    结语:把知识串起来

    现在可以用一条主线回顾 C 语言:

    • 类型规定数据如何解释;
    • 变量给内存中的数据命名;
    • 运算符处理数据;
    • 选择和循环控制执行路径;
    • 函数组织可复用的操作;
    • 数组连续保存同类型数据;
    • 指针保存地址,让程序能够间接访问和修改内存;
    • 结构体把不同类型的数据组合成一个更大的对象;
    • static、extern 和宏控制名字的可见范围、生命周期和编译前替换。

    理解这些概念后,再学习动态内存、文件、函数指针和数据结构,就不再是记忆孤立的语法,而是在同一套“数据、内存和操作”的模型上继续展开。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【C语言】初识 C 语言(点到为止)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!