本文适合:期末复习阶段的在校学生和 C 语言初学者;觉得不错的同学可以点个赞哦,对算法竞赛、OJ 刷题感兴趣的读者,可关注作者或者订阅专栏获取更多竞赛向内容。

一、程序基本结构

C程序由头文件主函数函数定义/声明语句组成,其中主函数(main)是程序入口,有且仅有一个。

  • 最小程序示例(Hello World)
#include <stdio.h>  // 头文件:引入标准输入输出库

// 主函数:程序执行的起点
int main() {
    // 语句:输出字符串,以分号结尾
    printf("Hello World!\n");  // \n是换行符
    return 0;  // 主函数返回值,0表示程序正常结束
}
  • 结构说明
#include <stdio.h>:预处理指令,引入stdio.h库才能使用printf、scanf等输入输出函数;

int main():主函数声明,int表示函数返回值类型为整数;

{}:代码块边界,包裹函数体内容;

printf():库函数,用于输出内容;

;:语句结束标志,C语言中所有语句必须以分号结尾;

return 0:主函数的返回值,返回给操作系统,0代表执行成功。

二、数据类型

C语言数据类型分为基本类型(整型、字符型、浮点型)和构造类型(数组、结构体等)

  • 基本数据类型表格
类型 占用字节 取值范围
char 1 -128 到 127
unsigned char 1 0 到 255
short (或 short int) 2 -32,768 到 32,767
unsigned short 2 0 到 65,535
int 4 -2,147,483,648 到 2,147,483,647
unsigned int 4 0 到 4,294,967,295
long (或 long int) 4 -2,147,483,648 到 2,147,483,647
unsigned long 4 0 到 4,294,967,295
float 4 大约 ±3.4e-38 到 ±3.4e+38 (大约6-7位有效数字)
double 8 大约 ±1.7e-308 到 ±1.7e+308 (大约15位有效数字)
long double 8 或 12 精度和范围比 double 更高 (取决于编译器实现)

注意:

  • 上表中的字节大小和取值范围是 典型值,并非所有系统都完全一致。例如,在64位系统中,long 类型通常为8字节。
  • 可以使用 sizeof 运算符来获取任何数据类型在当前编译环境下的确切字节大小,例如 sizeof(int)
  • 整型(char, short, int, long)默认是 signed(有符号)类型,可以存储正数、负数和零。使用 unsigned 关键字修饰后,它们只能存储非负数(0和正数),但正数的表示范围会扩大一倍。
  • 类型使用示例
#include <stdio.h>

int main() {
    // 整型
    int age = 20;
    // 无符号整型
    unsigned int score = 95;
    // 字符型(本质是ASCII码值)
    char ch = 'A';  // 注意用单引号
    // 单精度浮点型
    float height = 1.75f;  // 加f表示单精度,否则默认double
    // 双精度浮点型
    double weight = 65.5;
    
    // 输出各类型值
    printf("年龄:%d\n", age);          // %d是int的格式符
    printf("分数:%u\n", score);        // %u是unsigned int的格式符
    printf("字符:%c(ASCII值:%d)\n", ch, ch);  // %c输出字符,%d输出ASCII值
    printf("身高:%f\n", height);       // %f输出float/double
    printf("体重:%lf\n", weight);      // %lf也可用于double
    return 0;
}

注意:字符型char实际存储的是ASCII码值,因此可以参与数值运算,例如’A’ + 32会得到’a’ASCII码。

三、变量与常量

(1)变量

在程序设计中,变量 是指一个用于存储数据值的命名内存位置。它的值在程序运行期间可以被改变。

定义格式:

数据类型 变量名 = 初始值;

示例:
// 定义一个整型变量 age 并初始化为 18
int age = 18;

// 定义一个单精度浮点型变量 price,但未初始化
float price; 

// 可以在后续代码中为 price 赋值
price = 99.9f; 

重要提示:在定义变量时,如果省略了初始值(如上面的 float price;),变量将包含一个不确定的、随机的“垃圾值”。这是一个常见的程序错误来源。因此,始终在定义变量时进行初始化是一个非常好的编程习惯。

// 定义时即初始化
int count = 0;
char initial = 'A';

变量命名规则:

  1. 合法字符:只能由 字母 (a-z, A-Z)、数字 (0-9) 和 下划线 (_) 组成。

  2. 不能以数字开头:变量名的第一个字符必须是字母或下划线。

  3. 区分大小写:C语言是大小写敏感的,这意味着大写字母和小写字母被视为不同的字符。

  4. 不能使用关键字:不能使用C语言保留的关键字作为变量名。关键字是语言内部有特殊含义的词。

(2)常量

常量是程序运行中值不可改变的量,分为字面常量宏定义常量const修饰常量

  • 常量使用示例

    #include <stdio.h>
    
    // 1. 宏定义常量:预处理阶段替换,无类型
    #define PI 3.14159  // 注意没有分号
    #define MAX_AGE 120
    
    int main() {
        // 2. 字面常量
        printf("字面常量:%d\n", 100);  // 100是整型字面常量
        
        // 3. const修饰常量:有类型,编译阶段检查
        const float g = 9.8f;  // 重力加速度,不可修改
        
        // 使用常量计算圆的面积
        float r = 5.0f;
        float area = PI * r * r;
        printf("圆面积:%.2f\n", area);  // %.2f表示保留2位小数
        
        printf("最大年龄:%d\n", MAX_AGE);
        printf("重力加速度:%f\n", g);
        
        // g = 10.0f;  // 错误:const常量不能修改
        return 0;
    }
    

四、运算符与表达式

(1)算术运算符

用于执行数值计算。

  • + (加)
  • - (减)
  • * (乘)
  • / (除): 对于整数除法,结果会舍弃小数部分(向零取整)。
  • % (取余/模): 计算两个整数相除的余数。结果的符号与被除数相同。
  • ++ (自增): 将变量的值加1。
  • -- (自减): 将变量的值减1。

自增/自减的两种形式:

  • 前置: ++a--a。先将变量的值加/减1,然后使用新的值参与表达式的运算。
  • **后置: a++a--。先使用变量当前的值参与表达式运算,然后再将变量的值加/减1。

示例:

#include <stdio.h>

int main() {
    int a = 10, b = 3;
    
    printf("a + b = %d\n", a + b);  // 输出: 13
    printf("a - b = %d\n", a - b);  // 输出: 7
    printf("a * b = %d\n", a * b);  // 输出: 30
    printf("a / b = %d\n", a / b);  // 输出: 3 (整数除法,小数部分被舍弃)
    printf("a %% b = %d\n", a % b); // 输出: 1 (10除以3的余数是1)
    
    // 自增自减:前置先运算后取值,后置先取值后运算
    int c = a++;  // 后置自增:先把a(10)赋值给c,然后a自己变成11
    int d = ++b;  // 前置自增:先把b自己变成4,然后把新值(4)赋值给d
    
    printf("a = %d, c = %d\n", a, c);  // 输出: a = 11, c = 10
    printf("b = %d, d = %d\n", b, d);  // 输出: b = 4, d = 4
    
    return 0;
}

(2)关系运算符

用于比较两个操作数的大小关系。表达式的结果是一个整数:1 代表 真 (true)0 代表 假 (false)

  • == (等于)
  • != (不等于)
  • > (大于)
  • < (小于)
  • >= (大于等于)
  • <= (小于等于)

(3)逻辑运算符

用于组合或修改逻辑表达式,其操作数通常是关系表达式。结果同样是 1 (真) 或 0 (假)。在C语言中,任何非零值都被视为“真”,只有零被视为“假”。

  • && (逻辑与): a && b,当且仅当 ab 都为真时,结果才为真。
  • || (逻辑或): a || b,当 ab 至少有一个为真时,结果就为真。
  • ! (逻辑非): !a,如果 a 为假 (0),结果为真 (1);如果 a 为真 (非0),结果为假 (0)。

示例 (关系与逻辑):

#include <stdio.h>

int main() {
    int x = 5, y = 0;
    
    // 关系运算
    printf("x == 5: %d\n", x == 5);  // 1 (真)
    printf("x > y: %d\n", x > y);    // 1 (真)
    
    // 逻辑运算
    // &&:x>0为真,但y>0为假,所以整体为假
    printf("x>0 && y>0: %d\n", x > 0 && y > 0);  // 0 (假)
    
    // ||:x>0为真,满足一个条件即可,所以整体为真
    printf("x>0 || y>0: %d\n", x > 0 || y > 0);  // 1 (真)
    
    // !:y是0(假),取非后为真
    printf("!y: %d\n", !y);                  // 1 (真)
    
    return 0;
}

(4)赋值运算符

用于将一个值赋给一个变量。

  • = (简单赋值): a = b,将b的值赋给a
  • 复合赋值运算符: 将算术运算和赋值结合起来,使代码更简洁。
    • += (例如 a += b 等价于 a = a + b)
    • -= (例如 a -= b 等价于 a = a - b)
    • *= (例如 a *= b 等价于 a = a * b)
    • /= (例如 a /= b 等价于 a = a / b)
    • %= (例如 a %= b 等价于 a = a % b)

代码:

#include <stdio.h>

int main() {
    int a = 10;
    
    a += 5;  // 等价于 a = a + 5; 此后 a 的值为 15
    printf("a after += 5: %d\n", a); // 输出: 15

    a *= 2;  // 等价于 a = a * 2; 此后 a 的值为 30
    printf("a after *= 2: %d\n", a); // 输出: 30
    
    return 0;
}

五、控制语句

(1)选择

用于根据一个或多个条件来选择性地执行代码块。

if-else 语句

这是最基本的条件判断语句,可以单独使用 if,也可以组合成 if-elseif-else if-else 的形式。

示例:

#include <stdio.h>

int main() {
    int score = 85;
    
    if (score >= 90) {
        printf("优秀\n");
    } else if (score >= 80) {  // 上一个if条件不满足时,再判断此条件
        printf("良好\n");
    } else if (score >= 60) {
        printf("及格\n");
    } else {  // 如果以上所有条件都不满足,则执行这里
        printf("不及格\n");
    }
    return 0;  // 输出:良好
}

注意:

  • else ifelse 都是可选的。

  • 程序会从上到下依次检查条件,一旦找到一个满足的 ifelse if,就会执行其代码块,然后跳过整个 if-else 结构余下的部分。

switch 语句

用于对一个 整型字符型 表达式的值进行多分支判断。

  • 特点:适用于对一系列离散的、特定的值进行判断,代码结构比多个 else if 更清晰。

示例:

#include <stdio.h>

int main() {
    char grade = 'B';
    
    switch (grade) {
        case 'A':  // 匹配到字符 'A' 时执行
            printf("得分90-100\n");
            break;  // 执行完毕后,跳出整个switch结构
        case 'B':
            printf("得分80-89\n");
            break;
        case 'C':
            printf("得分60-79\n");
            break;
        default:  // 如果以上所有case都未匹配成功,则执行default部分
            printf("不及格\n");
    }
    return 0;  // 输出:得分80-89
}

说明:

  • switch 的表达式结果必须是整型或可以提升为整型(如 char)。

  • case 后面必须跟一个常量表达式。

  • break 至关重要:如果没有 break,程序会发生“穿透”,即继续执行下一个 case 的代码,直到遇到 breakswitch 结束。

  • default 是可选的,用于处理所有未列出的情况,建议总是加上以增强程序的健壮性。

(2)循环

用于重复执行一段代码,直到满足某个退出条件。

for 循环

最常用的循环结构,非常适合在 循环次数已知 的情况下使用。

  • 格式for (初始化表达式; 循环条件; 更新表达式) { 循环体 }

示例:

#include <stdio.h>

int main() {
    // 打印从 1 到 5 的数字
    for (int i = 1; i <= 5; i++) {  // i从1开始,当i<=5时循环,每次循环结束时i加1
        printf("%d ", i);
    }
    printf("\n"); // 换行
    // 输出:1 2 3 4 5 
    return 0;
}

while 循环

先判断条件,如果条件为真,则执行循环体。

  • 特点:适合在 循环次数未知,仅依赖某个条件来决定是否继续循环的场景。

示例:

#include <stdio.h>

int main() {
    // 同样打印从 1 到 5 的数字
    int i = 1;
    while (i <= 5) { // 先判断 i <= 5 是否为真
        printf("%d ", i);
        i++;  // 必须在循环体内手动更新条件变量,否则会造成死循环
    }
    printf("\n");
    // 输出:1 2 3 4 5 
    return 0;
}

do-while 循环

while 循环类似,但它是先执行一次循环体,然后再判断条件。

  • 特点保证循环体至少被执行一次

示例:

#include <stdio.h>

int main() {
    int i = 1;
    do {
        printf("%d ", i);
        i++;
    } while (i <= 5); // 先执行循环体,再判断条件。注意这里末尾有分号!
    printf("\n");
    // 输出:1 2 3 4 5 
    return 0;
}

(3)跳转语句

用于改变程序的正常执行流程,直接跳转到另一处。

  • break:立即终止并跳出当前所在switch 语句或最内层的循环(for, while, do-while)。
  • continue:立即结束本次循环,跳过循环体中尚未执行的语句,直接开始下一次循环的迭代。

示例:

#include <stdio.h>

int main() {
    // break 示例:在循环中提前退出
    printf("break示例:");
    for (int i = 1; i <= 5; i++) {
        if (i == 3) {
            break;  // 当 i 等于 3 时,立即跳出 for 循环
        }
        printf("%d ", i);
    }
    // 输出:1 2 
    
    printf("\n\n"); // 分隔
    
    // continue 示例:跳过某次循环
    printf("continue示例:");
    for (int i = 1; i <= 5; i++) {
        if (i == 3) {
            continue;  // 当 i 等于 3 时,跳过本次循环中后面的 printf,直接进入 i=4 的循环
        }
        printf("%d ", i);
    }
    printf("\n");
    // 输出:1 2 4 5 
    
    return 0;
}

六、函数

函数就是一个能完成 特定任务、可以 重复使用 的代码块。

核心三要素:

  1. 定义函数

    • 想好函数的功能,给它起个名字。
    • 确定它需要什么“材料”(参数),以及它要产出什么“成品”(返回值)。

    格式:

    返回值类型 函数名(参数列表) {
        // 实现功能的代码
        return 返回值; // 把成品交出去
    }
    

    例子:加法器

    // 返回一个整数,名叫add,需要两个整数
    int add(int a, int b) {
        return a + b; // 直接返回它俩的和
    }
    
  2. 调用函数

    • main 函数或其他地方,通过函数名来使用它。
    • 把实际的“材料”传给它。

    例子:用上面写好的加法器

    #include <stdio.h>
    
    // 函数定义(通常放在 main 前面)
    int add(int a, int b) {
        return a + b;
    }
    
    int main() {
        // 调用 add 函数,把 10 和 20 传进去
        int sum = add(10, 20);
    
        printf("结果是: %d\n", sum); // 输出:结果是: 30
        return 0;
    }
    

注意:

  • void:如果函数不返回任何值,返回值类型写 void。如果不需要参数,括号里也写 void,不推荐括号里留空。

七、数组

数组是存储同一类型数据的连续内存空间,通过下标访问(下标从0开始)。‘

  • 一维数组
#include <stdio.h>

int main() {
    // 1. 定义并初始化
    int arr1[5] = {1, 2, 3, 4, 5};  // 长度5,元素为1-5
    // 2. 部分初始化,未赋值元素为0
    int arr2[5] = {1, 2};  // 元素:1,2,0,0,0
    // 3. 省略长度,由初始化元素个数决定
    int arr3[] = {10, 20, 30};  // 长度3
    
    // 访问数组元素:下标从0开始
    printf("arr1[0] = %d\n", arr1[0]);  // 1
    printf("arr1[2] = %d\n", arr1[2]);  // 3
    
    // 遍历数组(用for循环)
    printf("arr1所有元素:");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr1[i]);
    }  // 输出:1 2 3 4 5
    
    // 修改数组元素
    arr1[3] = 40;
    printf("\n修改后arr1[3] = %d\n", arr1[3]);  // 40
    return 0;
}
  • 二维数组
#include <stdio.h>

int main() {
    // 定义2行3列的二维数组并初始化
    int arr[2][3] = {{1,2,3}, {4,5,6}};
    
    // 访问元素:行下标、列下标
    printf("arr[0][1] = %d\n", arr[0][1]);  // 2(第1行第2列)
    
    // 遍历二维数组
    printf("二维数组所有元素:\n");
    for (int i = 0; i < 2; i++) {  // 遍历行
        for (int j = 0; j < 3; j++) {  // 遍历列
            printf("%d ", arr[i][j]);
        }
        printf("\n");  // 换行
    }
    /* 输出:
    1 2 3 
    4 5 6 
    */
    return 0;
}

八、指针

指针是C语言的灵魂,也是其强大和灵活的根源。从本质上讲,指针就是一个存储内存地址的变量

1. 核心概念:地址与指向

  • 内存地址:计算机的内存被划分为许多个最小单元(通常是字节),每个单元都有一个唯一的编号,这个编号就是内存地址。
  • 指针变量:它不存储普通数据,而是专门用来存储这些内存地址。
  • 指向:当一个指针变量存储了某个普通变量的地址时,我们就说这个指针“指向”了那个变量。

类比

  • 内存地址 就像酒店的 房间号
  • 变量中存储的数据 就像房间里住的 客人
  • 指针 就像一张记录着某个房间号的 门禁卡。通过这张卡,你可以直接找到并进入对应的房间,与里面的客人互动。

2. 指针的定义与基本操作

定义指针的格式为:数据类型 *指针变量名;

这里的 * 表明这是一个指针变量。而前面的 数据类型 则规定了该指针 指向的内存区域中存放的是什么类型的数据。这决定了指针在进行解引用和算术运算时的行为。

两大核心操作符:

  • & (取地址符):获取一个变量的内存地址。
  • * (解引用/间接访问符):通过指针存储的地址,访问或修改其指向的内存中的数据。

注意:定义指针时的 int *p; 和使用指针时的 *p = 10; 中,* 的含义不同。

  • 定义时 (int *p;)* 是类型说明符,表示 p 是一个指针。
  • 使用时 (*p = ...)* 是运算符,表示“获取指针 p 指向的地址中的数据”。

【代码示例:指针的基本使用】

#include <stdio.h>

int main() {
    // 1. 定义一个普通变量
    int a = 10;
    printf("变量 a 的值:%d\n", a);          // 输出: 10
    printf("变量 a 的地址:%p\n", &a);      // %p 用于打印地址 (十六进制)
    
    // 2. 定义指针并指向变量 a
    int *p;  // 定义一个指向 int 类型的指针 p
    p = &a;  // 将 a 的地址赋值给 p,现在 p 指向 a
    // 上面两步可合并为一步初始化:int *p = &a;
    
    printf("指针 p 中存储的地址:%p\n", p);     // 输出与 &a 相同的地址
    printf("通过指针 p 访问到的值:%d\n", *p); // 解引用 p,获取 a 的值,输出: 10
    
    // 3. 通过指针修改变量的值
    *p = 20; // 解引用 p,将 20 存入 p 指向的内存(即变量 a 的内存)
    printf("通过指针修改后,变量 a 的值:%d\n", a); // a 的值被改变,输出: 20
    
    return 0;
}

3. 指针的大小

一个指针变量自身占用多少内存,仅取决于操作系统的寻址能力(位数),而与它指向的数据类型无关。

  • 32位 系统上,内存地址是32位的,因此任何类型的指针都占用 4字节
  • 64位 系统上,内存地址是64位的,因此任何类型的指针都占用 8字节

【代码示例:验证指针大小】

#include <stdio.h>

int main() {
    int *p_int;
    char *p_char;
    double *p_double;
    
    // sizeof 返回 size_t 类型,推荐用 %zu 格式化输出
    printf("int* 指针的大小:%zu 字节\n", sizeof(p_int));
    printf("char* 指针的大小:%zu 字节\n", sizeof(p_char));
    printf("double* 指针的大小:%zu 字节\n", sizeof(p_double));
    // 在64位系统上,输出将全部是 8
    // 在32位系统上,输出将全部是 4
    return 0;
}

4. 指针作为函数参数(地址传递)

默认情况下,C语言函数参数是“值传递”,即函数内部操作的是外部实参的一个副本,无法改变实参本身。

通过传递指针(即变量的地址),函数可以直接访问并修改实参的内存,从而实现对实参的修改。这通常被称为“地址传递”。

【代码示例:使用指针交换两个变量的值】

#include <stdio.h>

// 参数是两个 int 指针,它们接收的是外部变量的地址
void swap(int *x, int *y) {
    int temp = *x;  // 读取 x 指向的地址中的值 (即 a 的值)
    *x = *y;        // 将 y 指向的值赋给 x 指向的内存 (a = b)
    *y = temp;      // 将暂存的值赋给 y 指向的内存 (b = temp)
}

int main() {
    int a = 10, b = 20;
    printf("交换前:a = %d, b = %d\n", a, b);
    
    // 调用 swap 时,传入 a 和 b 的地址
    swap(&a, &b);
    
    printf("交换后:a = %d, b = %d\n", a, b);
    return 0;
}

5. 指针与数组

在C语言中,指针和数组关系紧密:

  • 数组名就是数组首元素的地址。它是一个地址常量,不能被赋值修改。
  • int arr[5]; int *p = arr; 这两行代码等价于 int *p = &arr[0];
  • 对指针进行算术运算(如 p++)时,指针会移动“它所指向的数据类型”的大小。例如,一个 int* 指针 p++ 后,地址会增加 sizeof(int) 个字节。
  • arr[i]*(arr + i) 在效果上是等价的。

【代码示例:使用指针遍历数组】

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr; // 指针 p 指向数组首元素 arr[0]
    
    printf("使用指针遍历数组:");
    for (int i = 0; i < 5; i++) {
        // 第一次循环 *p 是 arr[0],p++ 后 p 指向 arr[1],以此类推
        printf("%d ", *p);
        p++; // 指针移动到下一个元素
    }
    printf("\n");

    // 验证数组下标与指针算术的等价性
    printf("arr[2] 的值是 %d\n", arr[2]);          // 输出: 30
    printf("*(arr + 2) 的值是 %d\n", *(arr + 2)); // 同样输出: 30

    return 0;
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐