C语言

1 数据类型‌

int、char、float、double 及其取值范围、平台相关性(32/64位)、无符号与符号

2 变量与常量

3 输入输出

4 运算符

5 控制结构‌

6 表达式与语句‌

表达式求值顺序、逗号运算符、语句结束符

7 函数

7.1 示例

#include <stdio.h>

// 定义一个函数,专门用来打印分隔线
void printSeparator() {
    printf("--------------------------\n");
}

int main() {
    printf("Hello, World!\n");
    
    // 调用函数
    printSeparator(); 
    
    printf("This is C Language.\n");
    
    // 再次调用,体现复用性
    printSeparator(); 
    
    return 0;
}

7.2

带参数和返回值的函数

#include <stdio.h>

// 定义函数:接收两个 int,返回较大的那个 int
int getMax(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

int main() {
    int x = 10;
    int y = 20;
    
    // 调用函数,并将返回值赋值给变量 max
    int max = getMax(x, y);
    
    printf("The maximum of %d and %d is %d\n", x, y, max);
    
    // 也可以直接在 printf 中调用
    printf("Direct call result: %d\n", getMax(5, 8));
    
    return 0;
}

7.3 值传递

C语言中‌所有参数都是值传递‌:调用函数时,实参的‌值被复制‌给形参,函数内部修改的是副本,不影响原变量。

7.3.1 值传递的典型行为

#include <stdio.h>

void modify(int x) {           // x 是形参,接收的是实参的副本
    x = 100;                   // 修改的是副本,不影响原变量
    printf("函数内 x = %d\n", x);
}

int main() {
    int a = 10;
    printf("调用前 a = %d\n", a);
    modify(a);                 // 传入 a 的值(10),不是地址
    printf("调用后 a = %d\n", a);  // 输出仍是 10!
    return 0;
}

7.4 函数声明与作用域

在 C 语言中,编译器是从上往下读取代码的。如果在 main 函数之后定义了一个函数,且在 main 之前没有告诉编译器这个函数的存在,就会报错。

7.4.1 先声明,后定义

#include <stdio.h>

// 1. 函数声明(Prototype):告诉编译器有这么个函数
// 注意:声明以分号 ; 结尾
int add(int a, int b); 

int main() {
    // 编译器知道 add 存在,所以可以调用
    printf("Result: %d\n", add(3, 5));
    return 0;
}

// 2. 函数定义:具体的实现
int add(int a, int b) {
    return a + b;
}

7.4.2 将函数定义放在 main 之前

#include <stdio.h>

// 直接定义在 main 上面
int multiply(int a, int b) {
    return a * b;
}

int main() {
    printf("Result: %d\n", multiply(3, 5));
    return 0;
}

8 数组

  • 数组是相同数据类型元素的有序集合。

相同类型:不能既放整数又放浮点数。

有序:每个元素都有固定的位置(索引/下标)。

连续内存:在内存中,数组元素是紧挨着存放的(这点非常重要,后续讲指针时会用到)。

语法:类型 数组名[长度]

8.1 示例

#include <stdio.h>

int main() {
    // 1. 先声明,后赋值
    int ages[5]; 
    ages[0] = 18;
    ages[1] = 20;
    ages[2] = 22;
    ages[3] = 19;
    ages[4] = 21;

    // 2. 声明同时初始化(推荐)
    // 如果指定了长度,必须确保初始值个数不超过长度
    int scores[5] = {90, 85, 78, 92, 88};

    // 3. 自动推断长度
    // 编译器会根据大括号里的元素个数自动确定数组长度为 4
    double prices[] = {10.5, 20.0, 5.99, 100.0};

    // 4. 部分初始化
    // 未指定的元素会自动初始化为 0
    int nums[5] = {1, 2}; 
    // nums 的内容实际上是: {1, 2, 0, 0, 0}

    printf("第一个学生年龄: %d\n", ages[0]);
    printf("第三个分数: %d\n", scores[2]);
    printf("nums[4] 的值: %d\n", nums[4]); // 输出 0

    return 0;
}
  • 下标从 0 开始:长度为 N 的数组,下标范围是 0 到 N-1。访问 arr[N] 是越界。

  • 未初始化的局部数组:如果在函数内部声明 int arr[10]; 而不初始化,里面的值是垃圾值(随机数),不要直接使用。

  • 方括号必须放在变量后面。下面是错误示范。

    • double [] prices= {10.5, 20.0, 5.99, 100.0};

8.2 数组越界

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    
    // 正常访问
    printf("arr[4] = %d\n", arr[4]); // 输出 5
    
    // 危险!越界访问
    // 下面的行为是“未定义行为”(Undefined Behavior)
    // 可能输出垃圾值,可能导致程序崩溃
    printf("arr[5] = %d\n", arr[5]); 
    printf("arr[100] = %d\n", arr[100]);
    
    return 0;
}

8.3 数组遍历

通常使用 for 循环来访问数组中的每一个元素。

#include <stdio.h>

int main() {
    int arr [6]={32,24,64,647,76,5};
    int length =sizeof(arr)/sizeof(int);
    for (size_t i = 0; i < length; i++){
        printf("arr[%d]:%d\n",i,arr[i]);    
    }
    return 0;
}

8.4 二维数组

声明与初始化

语法:类型 数组名[行数][列数];

示例:

#include <stdio.h>

int main() {
    // 定义一个 3 行 4 列的整数数组
    // 可以想象成 3 个班级,每个班级 4 个学生
    int matrix[3][4] = {
        {1,  2,  3,  4},   // 第 0 行
        {5,  6,  7,  8},   // 第 1 行
        {9,  10, 11, 12}   // 第 2 行
    };

    // 访问特定元素:第 1 行 第 2 列的元素
    // 注意:行下标 1,列下标 2
    printf("matrix[1][2] = %d\n", matrix[1][2]); // 输出 7

    // 遍历二维数组:需要嵌套循环
    printf("整个矩阵:\n");
    for (int i = 0; i < 3; i++) {       // 外层循环控制行
        for (int j = 0; j < 4; j++) {   // 内层循环控制列
            printf("%4d", matrix[i][j]); // %4d 用于对齐输出
        }
        printf("\n"); // 每打印完一行,换行
    }

    return 0;
}
  • 内存布局:C 语言中二维数组是按行存储的。先存第 0 行的所有元素,再存第 1 行……它们在内存里依然是一段连续的空间。

8.4 数组长度

sizeof()

  • 本质:sizeof 是 C 语言的一个单目运算符。

  • 作用:它用于计算数据类型或变量在内存中占用的字节数(Bytes)。

  • 返回值类型:size_t(一种无符号整数类型,通常在 <stddef.h> 或 <stdio.h> 中定义)。

示例

#include <stdio.h>

int main() {
    // 注意:sizeof 后面的括号对于类型名是必须的,对于变量名可选
    printf("Size of char:   %lu bytes\n", sizeof(char));
    printf("Size of int:    %lu bytes\n", sizeof(int));
    printf("Size of float:  %lu bytes\n", sizeof(float));
    printf("Size of double: %lu bytes\n", sizeof(double));
    
    // 也可以对变量使用
    int a = 10;
    printf("Size of variable a: %lu bytes\n", sizeof a);   // 合法,括号可省略
    printf("Size of variable a: %lu bytes\n", sizeof(a));  // 合法,推荐写法

    //计算数组长度
    int arr [6]={32,24,64,647,76,5};
    printf("arr size:%d",sizeof(arr));//输出:24
    return 0;
}

9 字符串‌与字符数组

  • 在 C 语言中,没有原生的“字符串”类型。C 语言的字符串,本质上就是以空字符 \0 结尾的字符数组。

  • 字符数组 char arr[5] 和整型数组 int arr[5] 在物理上没有任何区别。

    • 本质:它就是内存中一段连续的空间(连续的5个房间),每个房间里面可以装任何数值(0~255)

9.1 示例

纯物理视角的字符数组

#include <stdio.h>
int main() {
    char arr[5];
    // 物理赋值:随便塞数字,编译器绝不阻拦你
    arr[0] = 65;  // 65是字母'A'的ASCII码
    arr[1] = 66;  // 66是'B'
    arr[2] = 0;   // 就是数字0,不是字符'0'('0'的ASCII是48)
    arr[3] = 97;  // 97是'a'
    arr[4] = 255; // 超出标准ASCII范围,就是一个纯字节
    // 用 %c 按字符打印,用 %d 按数字打印
    for(int i = 0; i < 5; i++) {
        printf("位置%d: 字符是 %c, 底层数字是 %d\n", i, arr[i], arr[i]);
    }
    return 0;
}

9.2 字符串的本质

  • 以 \0(数字0)作为结尾标志的字符数组。

  • 核心关键:\0 不是字符串的内容,是边界线。而且,这个 \0 必须占用一个房间(1个字节)!

9.2.1 示例

手工捏造一个字符串

#include <stdio.h>
int main() {
    // 注意:数组大小必须是 3!因为要给 '\0' 留个位置
    char str[3]; 
    str[0] = 'H';
    str[1] = 'i';
    str[2] = '\0'; // 关键!没有这个,它只是个字符数组,有了这个,它才升华为字符串
    // %s 的底层逻辑:从你给的地址开始,一个个读字符,直到读到 '\0' 才停下
    printf("%s\n", str); // 正常输出 Hi
    return 0;
}

反面示例

没有 \0

#include <stdio.h>
int main() {
    char bad_str[2] = {'H', 'i'}; 
    // 读到什么乱码就打印什么,直到碰巧遇到一个 0 才停下,或者把程序搞崩溃。
    printf("%s\n", bad_str); 
    return 0;
}

9.3 双引号

每次都要写 \0 太烦,C语言提供了语法糖:用双引号 ""。

#include <stdio.h>
int main() {
    // 写法一:老老实实写
    char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
    // 写法二:用双引号语法糖,最后一个是 \0
    char str2[6] = "Hello";
    // 写法三:省略数组大小(编译器会自动数:5个字母 + 1个\0 = 6)
    char str3[] = "Hello"; 
    printf("str1: %s\n", str1);
    printf("str2: %s\n", str2);
    printf("str3: %s\n", str3);
    // 验证本质:str3 到底多大?
    printf("str3 的物理长度: %zu\n", sizeof(str3)); // 输出 6,不是 5,因为\0 占了1个坑
    return 0;
}
#include <stdio.h>
#include <string.h>
int main() {
    //初始化str字符数组,意思就是在内存里有一段连续的空组,放着10个字符,第4个字符开始是空,最后一个是边界符
    // 65,66,67,0,0,0,0,0,0,\0
    // {'A','B','C',,,,,,,\0}
    char str[10] = "ABC"; 
    printf("sizeof(str) = %zu\n", sizeof(str)); // 输出 10
    // strlen,从开头数,有几个字符,遇到 \0 为止
    printf("strlen(str) = %zu\n", strlen(str)); // 输出 3
    //特殊例子
    char str2[] = "ABC\0DEF"; // 中间硬插一个 \0
    printf("sizeof(str2) = %zu\n", sizeof(str2)); // 输出 8 (A,B,C,\0,D,E,F,\0)
    printf("strlen(str2) = %zu\n", strlen(str2)); // 输出 3!遇到中间的 \0 就停了,后面的DEF它看不见
    return 0;
}
#include <stdio.h>
int main() {
    // 【数组方式】:自己家有房子
    char arr[] = "Hello"; 
    // 本质:在栈区建了6个房间的房子,把 H,e,l,l,o,\0 搬进去。你可以随意修改房子里的东西。
    arr[0] = 'h'; // 完全合法!
    printf("数组方式: %s\n", arr);
    // 【指针方式】:租了别人的房子
    const char *ptr = "Hello"; // 注意:现代C语言强烈建议加 const
    // 本质:"Hello" 作为字符串常量,被存放在内存的只读区(别人建好的死胡同)。
    // ptr 只是一个指针(纸条),记下了这个死胡同的门牌号。
    // ptr[0] = 'h'; // 绝对错误!试图修改只读内存,程序直接崩溃(段错误)!
    // 但是,你可以换一张纸条,指向别的地方(改变指针的指向)
    ptr = "World"; // 合法
    printf("指针方式: %s\n", ptr);
    return 0;
}

10 指针

指针是一个变量,存的是内存地址。在计算机内存中,每个字节都有一个唯一的编号,这就是地址。

int a = 10; -> 变量 a 的值是 10。
&a -> 这是变量 a 在内存中的地址(比如 0x7ffd...)。

指针运算符:

& 运算符获取变量的地址

* 解引用运算符,代表该地址指向的值

10.1 示例

#include <stdio.h>

int main() {
    int a = 100;
    printf("变量 a 的值是:%d\n", a);        // 输出:100
    printf("变量 a 的地址是:%p\n", &a);     // 输出:0x7ffee4b2c9ac(示例地址)
    return 0;
}

10.2 示例

  • 要把int p 理解成 int p,p是变量,p的类型是指针,是int指针。

  • 32位计算机里指针长4个字节这么长,64位里8个字节这么长。

#include <stdio.h>

int main() {
    int a = 200;
    int *p;           // 声明一个指向int的指针p
    p = &a;           // 把a的地址赋给p,p现在“指向”a

    printf("a 的值:%d\n", a);           // 200
    printf("p 存储的地址:%p\n", p);     // 和 &a 一样。存储的地址:000000000061FE10
    printf("p 指向的值:%d\n", *p);      // 200 * 是“解引用”

    *p = 300;         // 通过指针修改a的值
    printf("修改后 a 的值:%d\n", a);    // 300
    return 0;
}

10.3 指针参数和指针函数

10.3.1 指针作为函数的参数

交换两个变量

#include <stdio.h>

// 参数是指针,接收的是地址
void swap(int *a, int *b) {
    int temp = *a; // 取出 a 指向的值
    *a = *b;       // 把 b 指向的值赋给 a 指向的位置
    *b = temp;     // 把 temp 赋给 b 指向的位置
}

int main() {
    int x = 10;
    int y = 20;

    printf("交换前: x = %d, y = %d\n", x, y);

    // 传入地址 &x 和 &y
    swap(&x, &y);

    printf("交换后: x = %d, y = %d\n", x, y);

    return 0;
}

10.3.1 指针函数

返回值为指针的函数

//全局变量
int b=10;

int* getP(){
    return &b;
}

int main() {

    int *b_p=getP();
    printf("%d\n",b_p);
    return 0;
}

11 指针与数组‌

在 C 语言中,数组名本质上就是一个常量指针,指向数组的第一个元素。

  • arr 等价于 &arr[0]。
  • arr[i] 等价于 *(arr + i)。

12 指针数组与数组指针

C语言数组指针详解

什么是数组指针?

数组指针(Pointer to Array)是指向整个数组的指针,而不是指向数组中的某个元素。它的本质是一个指针,但该指针指向的是一个数组类型。

普通指针:     int *p;        // 指向一个 int
数组指针:     int (*p)[N];   // 指向一个包含 N 个 int 的数组

⚠️ 注意区分:指针数组 int *p[N](数组,每个元素是指针) vs 数组指针 int (*p)[N](指针,指向数组)


5种典型用法

用法1:指向一维数组,遍历访问

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    // p 是指向"含5个int的数组"的指针
    int (*p)[5] = &arr;
    
    // 通过数组指针访问元素
    for (int i = 0; i < 5; i++) {
        printf("%d ", (*p)[i]);   // 解引用后得到数组,再用下标访问
        // 等价于: p[0][i]
    }
    // 输出: 10 20 30 40 50
    
    return 0;
}

关键点*p 得到原数组 arr(*p)[i] 等价于 arr[i]


用法2:作为函数参数,传递二维数组

#include <stdio.h>

// 形参是数组指针:指向含4个int的数组
void printMatrix(int (*matrix)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", matrix[i][j]);  // 自然使用二维下标
        }
        printf("\n");
    }
}

int main() {
    int mat[3][4] = {
        {1,  2,  3,  4},
        {5,  6,  7,  8},
        {9, 10, 11, 12}
    };
    
    // 二维数组名退化为指向第一行的指针(即数组指针)
    printMatrix(mat, 3);
    
    return 0;
}

关键点:这是数组指针最常见、最实用的场景。二维数组 int arr[M][N] 作为参数时,必须用 int (*p)[N] 接收,列数 N 不可省略。


用法3:动态分配二维数组

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

int main() {
    int rows = 3, cols = 4;
    
    // 分配一个"含3行、每行4个int"的连续二维数组
    // malloc 返回 void*,强制转换为数组指针类型
    int (*matrix)[4] = (int (*)[4])malloc(rows * sizeof(int[4]));
    
    if (matrix == NULL) {
        perror("malloc failed");
        return 1;
    }
    
    // 像普通二维数组一样使用
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }
    
    // 打印验证
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
    // 输出:
    // 0 1 2 3
    // 4 5 6 7
    // 8 9 10 11
    
    free(matrix);  // 一次释放即可(连续内存)
    return 0;
}

关键点:相比指针的指针 int **,这种方式内存连续、只需一次 malloc/free,缓存友好。


用法4:数组指针的算术运算(跳行)

#include <stdio.h>

int main() {
    int arr[3][4] = {
        {1,  2,  3,  4},
        {5,  6,  7,  8},
        {9, 10, 11, 12}
    };
    
    // p 指向第一行(即 arr[0] 这个含4个int的数组)
    int (*p)[4] = arr;
    
    printf("p[0][0] = %d\n", p[0][0]);   // 1
    printf("p[1][0] = %d\n", p[1][0]);   // 5  (跳过一整行)
    printf("p[2][2] = %d\n", p[2][2]);   // 11
    
    // 指针算术:p + 1 跳过 4 * sizeof(int) = 16 字节
    printf("sizeof one row = %zu bytes\n", sizeof(*p));  // 16
    
    // 手动移动指针
    p = p + 1;       // 现在指向第二行
    printf("After p+1: (*p)[0] = %d\n", (*p)[0]);  // 5
    
    return 0;
}

关键点p + 1 不是移动一个 int,而是移动整个数组(一行)的大小。步长 = N * sizeof(元素类型)


用法5:通用化处理不同长度的行(配合结构体或灵活使用)

#include <stdio.h>

// 使用数组指针处理"固定列数"的多组数据
void processRows(int (*data)[5], int rowCount) {
    for (int i = 0; i < rowCount; i++) {
        int sum = 0;
        for (int j = 0; j < 5; j++) {
            sum += data[i][j];
        }
        printf("Row %d sum = %d, avg = %.2f\n", i, sum, sum / 5.0);
    }
}

int main() {
    // 场景:处理多个学生的5门课成绩
    int scores[][5] = {
        {90, 85, 78, 92, 88},
        {76, 82, 91, 70, 85},
        {95, 90, 88, 93, 91}
    };
    
    int numStudents = 3;
    processRows(scores, numStudents);
    
    // 输出:
    // Row 0 sum = 433, avg = 86.60
    // Row 1 sum = 404, avg = 80.80
    // Row 2 sum = 457, avg = 91.40
    
    return 0;
}

关键点:当数据具有"固定列数、可变行数"的特征时,数组指针作为函数参数非常合适,代码清晰且高效。


核心对比总结

┌─────────────────┬──────────────────────┬──────────────────────┐
│                 │   普通指针 int *p     │   数组指针 int(*p)[N] │
├─────────────────┼──────────────────────┼──────────────────────┤
│ 指向目标         │ 单个 int             │ 含N个int的整个数组    │
│ p + 1 步长       │ sizeof(int)          │ N * sizeof(int)      │
│ 典型用途         │ 一维数组/单个变量     │ 二维数组/多维数组     │
│ 解引用 *p        │ 得到一个 int         │ 得到一个数组(可下标)   │
└─────────────────┴──────────────────────┴──────────────────────┘

记忆口诀

括号优先int (*p)[N] — 先读 *p(p是指针),再读 [N](指向含N个元素的数组),最后读 int(元素类型)。

posted @ 2026-04-23 20:38  cdc321  阅读(8)  评论(0)    收藏  举报