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(元素类型)。
浙公网安备 33010602011771号