数据类型¶
变量和常量¶
变量¶
- 作用:开一块内存放数据
- 定义:
- C89:变量必须在程序一开始全部定义
- C99:任何位置都可以定义变量
-
名字(标识符)
规则:字母数字下划线组成;数字不能是第一个字符;不能是关键字
-
赋值:动态局部变量若不赋值则随机值
全局变量¶
生存期和作用域独立于函数,都在全局。
初始化:
- 可以初始化,但要求是只能用编译时刻已知的值来初始化它,这里,全局变量的初始化在main函数之前。
- 不初始化:数字得到0;指针得到NULL
示例:
int gAll = 12; // 编译通过
int gAll = f(); // 编译错误
int gAll = 12;
int g2 = gAll; // 编译错误
const int gAll;
int g2 = gAll; // 编译通过,但是不建议这样,尤其在大程序中。
int g2 = gAll;
const int gAll; // 编译错误
(全局)变量的覆盖
在更小的地方定义的变量,会将更大的地方定义的同名变量覆盖
-
函数内部本地变量会掩盖全局覆盖
-
函数内部再开一个块({ }),在里面定义一个变量,他会覆盖这个块外面的同名变量。
静态本地变量¶
语法: 变量定义前面加上 static
修饰符。
特点:
-
函数离开时,保留其值
-
和全局变量在相同的内存区域(很小的地址),而动态本地变量的地址很大
-
生存期:全局;作用域:函数内部(只有函数内部可以访问,这也是
static
的意思:局部作用域。
使用
-
函数返回本地变量的地址 / 的值 会有warning :这个变量在函数结束后就没了
-
不要使用全局变量在函数之间传递参数和结果
-
尽量避免使用全局变量,使用全局变量和静态本地变量的函数是线程不安全的。
其他
- C语言有很多区放变量:常量区、静态区、栈区、堆区
- 动态变量 auto
- 特点:调用一次分配一次内存,调用结束就释放了,重来
- 定义:在C语言中,
auto
关键字用于声明自动存储期的局部变量。它告诉编译器,该变量应该在栈上分配,并且其生命周期仅限于声明它的函数或代码块。然而,auto
关键字实际上是可选的,因为在函数内部声明的变量默认就是自动存储期的。 - 在C99标准之前,
auto
关键字是必需的,但在C99及以后的标准中,它变得可选。
- 静态变量 static
- 特点:整个程序运行期间存在,等程序结束后释放
- 静态全局变量
- 作用:只可以使其在声明的文件中可见,避免与其他文件中同名变量冲突
- 静态函数
static int function_name(parameter_type parameter)
- 作用:只能在所声明的文件中调用,其他文件不可使用:辅助函数、实用函数限制在特定文件中
- 静态局部变量
- 定义:函数内部定义的静态变量
- 作用:类似全局变量,函数调用结束后,其值不会被销毁,而是保持存在
- 用处:重复使用,这一次的接着上一次的值用
#include<stdio.h>
//三个变量都赋初始值0
int global = 0; //global为全局变量
void stc()
{
int n = 0; //n 为局部变量
static int sta = 0; //sta为静态局部变量
n++; sta++; global++; //函数每次执行都对三个变量+1
printf("%d ", n); printf("%d ", sta); printf("%d ", global);
}
int main()
{
int i;
for(i = 1; i <= 5; i++){
stc(); printf("\n"); //三次调用函数
}
return 0;
}
/* 输出:
1 1 1
1 2 2
1 3 3
1 4 4
1 5 5
全局变量和静态局部变量都延续了上次调用的结果继续+,局部变量从初始值开始
*/
常量¶
定义
-
const
只读变量可全局/局部,eg:
const double PI = 3.1415;
-#define
宏eg:
#define PI 3.1415
字符串常量
__func__
:表示当前函数的名称。
- 它在函数的任何地方都可以使用,不需要传递或定义额外的变量。
- __func__的值是编译器在编译时自动填充的,且是一个预定义标识符,故不能被重新定义。
基本数据类型总表:
类别 | 名称 | 类型名 | 数据长度 | 取值范围 | 格式说明符 |
---|---|---|---|---|---|
整型 | [有符号] 整型 | int | 32 位 | \(-2^{31}\) ~ \(2^{31}-1\) | %d |
[有符号] 短整型 | short [int] | 16 位 | \(-2^{15}\) ~ \(2^{15}-1\) | %hd | |
[有符号] 长整型 | long [int] | 32 位 | \(-2^{31}\) ~ \(2^{31}-1\) | %ld | |
无符号整型 | unsigned [int] | 32 位 | \(0\) ~ \(2^{32}-1\) | %u | |
无符号短整型 | unsigned short [int] | 16 位 | \(0\) ~ \(2^{16}-1\) | %hu | |
无符号长整型 | unsigned long [int] | 32 位 | \(0\) ~ \(2^{32}-1\) | %lu | |
字符型 | 字符型 | char | 8 位 | \(0\) ~ \(255\) | %c |
浮点型 | 单精度浮点型 | float | 32 位 | 约 \(10^{-38}\) ~ \(10^{38}\) | %f |
双精度浮点型 | double | 64 位 | 约 \(10^{-308}\) ~ \(10^{308}\) | %lf |
注:
-
方括号中的内容可以省略。
-
bit位数:0 ~ 长度-1 位;符号位:最高位(第长度-1位)
-
十进制Decimal 八进制Octal(%o) 十六进制Hexadecimal(%x)
16进制:X对应A~F; x对应a~f
整型¶
定义
- 整型常量 == 整数数字
- 整型变量
存储
让计算机用特定byte存数字:加后缀eg:123L
,123UL
,但是短整型没有类似表示
数字编码
-
补码:
使得数据的表示唯一:主要是0和-0统一;使得减法用加法做
内存中按补码存储
正数:
三个码相同:符号位为 0,其余为其二进制
负数:
- 原码:符号位1,其他位与为绝对值的二进制
- 反码:符号位1不变,除符号位其他为原码的反
- 补码:反码 + 1
负数三码转换
-
转换原因:内存中补码存储/运算;知道原码才知道数字是多少
-
原码 —— 补码:
- 负数 取反(符号位不变) + 1
-
补码转原码:
- 负数法一:减 1,按位取反,符号位不变。
- 负数法一:补码的补码是原码
溢出:会有进位,溢出最高位舍
示例
int a = -1;
printf("%d, %u", a, a);
//结果:-1, 4294967295
printf("%o, %x", a, a);
//结果:37777777777, ffffffff
直接利用格式说明符完成进制转换:
实型/浮点型¶
定义
表示形式:小数/科学计数法( %e )
数据精度 与 取值范围是两个不同的概念:
float x = 1234567.89; x虽在取值范围内,但无法精确表达。
float y = 1.2e55; y 的精度要求不高,但超出取值范围。
存储
IEEE 754标准
浮点数分三个部分,依次是:
-
符号位(Sign bit):1位,表示数值正负,0正1负数。
-
指数位(Exponent):存储指数部分,单精度为8位,双精度为11位。指数部分采用偏移量(bias)表示,单精度的偏移量为127,双精度的偏移量为1023。
-
尾数位(Mantissa,也称为有效数字或 significand):存储有效数字部分,单精度为23位,双精度为52位。尾数部分通常不包括最高位的1(隐含的前导1),除了特殊情况。
举例
假设有一个双精度浮点数 3.14
,其二进制表示为 1.1001000110100010100011110100110110
(不包括隐含的前导1),则其在内存中的存储方式如下:
- 符号位:0(因为3.14是正数)
- 指数:计算指数部分,3.14的二进制科学计数法为
1.1001000110100010100011110100110110 * 2^1
,指数为1,加上偏移量1023,得到1024,二进制为10000000000
。 - 尾数:
1001000110100010100011110100110110
(不包括隐含的前导1)
因此,3.14在内存中的存储为:
特点:不能精确存储
字符型¶
定义
-
字符型变量
-
字符型常量:单个字符,
'A'
'\n'
是正确的定义方式,得加上单引号
存储
存ASCII
转义字符
-
多行字符串:
-
包含特殊字符的字符串:
-
使用退格符:
这里,\b
是退格符,用于删除前一个字符。输出结果将是: -
使用空字符:
这里,\0
是空字符,它将字符串在Hello
后面截断,所以World
不会被打印出来。输出结果将是: -
使用回车符和换行符:
这里,\r
是回车符,将光标移动到行首,然后打印Second line.
,覆盖了First line.
。输出结果将是:
所有字符都可以用转义字符表示(即:使用8 or 16 进制转义序列) eg:打印A
关系: 整型和字符型可以按ASCII随便交换
类型转换¶
零、情况
- 不同类型数据的混合运算
- 整型数据除法需要得到小数
一、自动
(一)、非赋值运算
(二)、赋值运算 1. 理论

- 示例: 数值溢出时:
截断:由于数值超出了 short
类型的范围,编译器会将这个数值截断到 short
类型能表示的范围。具体来说,它会取这个数值的低16位。0x12345678L
的二进制表示是 0001 0010 0011 0100 0101 0110 0111 1000
,取低16位是 0101 0110 0111 1000
,即 0x5EF8
。
二、强制类型转换
语法: (类型名) 表达式
示例:
printf("(double)3 = %.1f\n", (double)3); // 3.0
printf("(int)3.8 = %d\n", (int)3.8); //3
printf("(int)-1.6 = %d\n", (int)-1.6); //-1
printf("(double)(5/2) = %.1f\n", (double)(5 / 2)); //2.0
printf("(double)5/2 = %.1f\n", (double)5 / 2); //2.5
//后两个:看优先级:()比(类型名)高。所以倒数第二个:先5/2 = 2,再(double)2 = 2.0 ,最后一个先(double)5 = 5.0,再5.0/2 = 2.5(发生了自动类型转换)
int i; // Integer variable
double x; // Double variable
x = 3.8; // Assign 3.8 to x
i = (int)x; // Cast x to int, truncating 3.8 to 3
printf("x = %f, i = %d\n", x, i);
// Output: x = 3.800000, i = 3
printf("(double)(int)x = %f\n", (double)(int)x);
// Cast x to int (3) and then cast back to double (3.0)
// Output: (double)(int)x = 3.000000
printf("x mod 3 = %d\n", (int)x % 3);
// Cast x (3.8) to int (3), then compute 3 % 3 = 0
// Output: x mod 3 = 0
枚举¶
定义&规则:
- C内部
enum
其实就是int
,可以当作int
做运算 - 枚举名:是枚举类型的名字(可选,因为一般不用)。
- 枚举常量:是一组合法的标识符,(必要,需要用)类型是
int
常量,默认从0
开始依次递增。 -
枚举中的常量必须唯一
-
不能直接输入枚举常量的名字:
枚举常量不能通过输入输出函数直接读写。例如,不能用
scanf
读取枚举类型,必须通过整数变量赋值。 -
类型安全性:
枚举变量的值可以超出定义的范围(尽管不推荐),因为底层实现是整数。例如:
enum Color { RED, GREEN, BLUE };
int main() {
enum Color myColor = 5; // 合法,但不推荐
printf("%d\n", myColor); // 输出 5
return 0;
}
意义:
命名的一组需要排列的整数常量,用于固定类别、状态和选项的表示。可读性,易于维护。
使用
这里定义了一个枚举类型 Color
,它包含了三个枚举常量:RED
、GREEN
和 BLUE
。这些常量的默认值分别是 0
、1
和 2
。
为枚举常量指定值
可以显式为枚举常量指定值。如果未指定值,则该常量的值为前一个常量值加 1
。
enum Day {
MON = 1, // MON 的值是 1
TUE, // TUE 的值是 2
WED = 5, // WED 的值是 5
THU, // THU 的值是 6
FRI = 10, // FRI 的值是 10
SAT, // SAT 的值是 11
SUN // SUN 的值是 12
};
定义枚举变量
enum Color { RED, GREEN, BLUE };
int main() {
enum Color myColor; // 定义一个枚举变量
myColor = GREEN; // 为变量初始化枚举常量
printf("myColor = %d\n", myColor); // 输出 1
scanf("%d", &myColor); // 输入 5
printf("%d", myColor); // 输出 5
printf("%d", GREEN); // 输出 1
return 0;
}
//GREEN的值不会变成5啊,因为枚举常量的值是固定的,且在编译时已经确定。赋值给枚举变量(如 myColor)时,只是改变了变量的值,不会修改枚举常量的值。
匿名枚举
如果不需要多次引用枚举类型名称,可以省略枚举类型的名字,直接使用枚举常量。
enum { MON, TUE, WED, THU, FRI, SAT, SUN };
int main() {
int today = WED; // 直接使用枚举常量
printf("Today is day number: %d\n", today); // 输出 2
return 0;
}
计数
Color{c1, c2, c3, c4, cnt};
这里cnt
值是4,记录了前面枚举变量的个数,可以用它来循环……
具体使用场景
在底层,枚举类型的常量其实是整数(int
类型)。因此,枚举常量可以用于任何需要整数值的地方。
enum Status { OFF, ON };
int main() {
enum Status light = OFF;
if (light == OFF) {
printf("Light is off\n");
}
return 0;
}
//状态开关机
enum State { INIT, RUNNING, STOPPED };
void checkState(enum State s) {
switch (s) {
case INIT: printf("Initializing...\n"); break;
case RUNNING: printf("Running...\n"); break;
case STOPPED: printf("Stopped.\n"); break;
default: printf("Unknown state!\n");
}
}
结构¶
结构是一种数据类型,它在语法上与python中的字典、类都有相似之处。
想不清楚时,将其当作int类型。因为本质上其都是一种数据类型。
示例:
#include <stdio.h>
#include <string.h>
struct student {
char name[50];
int age;
long int id;
char gender[10];
double height;
char bloodtype;
}; //这是一条C语言定义变量的语句,别忘了最后的分号;另外这段代码的作用是“声明结构类型”
int main() {
struct student students[3]; //这是在定义一个结构变量
// 设置第一个学生的属性
strcpy(students[0].name, "Alice");
students[0].age = 20;
students[0].id = 123456789;
strcpy(students[0].gender, "female");
students[0].height = 1.65;
students[0].bloodtype = 'A';
// 设置第二个学生的属性
strcpy(students[1].name, "Bob");
students[1].age = 22;
students[1].id = 987654321;
strcpy(students[1].gender, "male");
students[1].height = 1.80;
students[1].bloodtype = 'B';
// 设置第三个学生的属性
strcpy(students[2].name, "Charlie");
students[2].age = 19;
students[2].id = 555555555;
strcpy(students[2].gender, "male");
students[2].height = 1.75;
students[2].bloodtype = 'O';
// 输出每个学生的信息
for (int i = 0; i < 3; i++) {
const char *pronoun;
if (strcmp(students[i].gender, "male") == 0) {
pronoun = "his";
} else {
pronoun = "her";
}
printf("%s, a %d-year-old student, %s ID is %ld.\n", students[i].name, students[i].age, pronoun, students[i].id);
}
return 0;
}
语法¶
1. 初始化:
- 对于结构体数组,不可
students[0] = {"Alice", 20, 12345, female, 1.65, 'A'};
类似这样初始化,只能像上面范例程序一样一个一个初始化。如果想这样,只能在定义时就这样初始化,而非定义后再赋值,像这样:
示例:
#include <stdio.h>
#include <string.h>
// 定义结构体
struct Person {
char name[50];
int age;
float height;
};
// 通过函数初始化结构体
void initializePerson(struct Person* p, const char* name, int age, float height) {
strcpy(p->name, name);
p->age = age;
p->height = height;
}
int main() {
// 直接赋值初始化
struct Person person1;
person1.age = 25;
strcpy(person1.name, "Alice");
person1.height = 5.7;
// 使用初始化列表初始化(C99标准及以上支持)
struct Person person2 = {"Bob", 30, 6.0};
// 使用初始化列表初始化,并使用类似“关键词传参”的方式
struct Person person3 = {"zrz", .age = 18, .height = 8.0}
// 列表初始化,若不给某int变量传值,默认0
// 通过函数初始化
struct Person person4;
initializePerson(&person4, "Charlie", 22, 5.9);
return 0;
}
2. 访问结构成员
语法:结构变量.结构成员
概念
结构类型:虚的,一开始定义的那个是结构类型,例如上面的Person 结构变量:基于结构类型定义了许多结构变量,例如上面的person1, person2 ……
3. 结构运算
重点注意其与数组的区别
- 可以用结构变量的名字访问整个结构
-
对于整个结构,可以整体赋值,
-
整个结构,可以取地址
注意,结构变量名不是他的地址,取地址必须得用
&
-
整个结构,可以作为参数传递给函数
结构与函数¶
整个结构可以作为参数传入函数,这时是在函数内新建一个结构变量,并赋值传入的那个结构的值
结构可以作为函数的返回值
输入结构:
结构与数组的区别:
-
在传入函数时,结构与普通的int变量类似,要在函数内部更改它,得传地址。因为传入函数,函数接收到的实际是结构的值,不是这个变量,与原结构无关。
-
数组在传入函数时,传入的是这个数组变量。
解决方案1:在函数内部copy一个一样的临时的结构变量,讲这个结构返回
struct point inputStruct(void)
{
struct point p{/*代码块,要求与main函数里面的一样*/};
/*代码块*/
return p;
}
int main()
{
struct point dest{/*代码块*/};
dest = inputStruct();
printf(/*语句*/)
}
解决方案2:
K&R
“lf a large structure is to be passed to a function, it is generally more efficient to pass a pointer than to copy the whole structure”
指向结构的指针
指针与函数
常用:一个函数的参数是指针,返回值也是指针;目的是将这个值的指针输入,在函数内部更改完之后再返回出去,好处是返回的指针可以再用于其他代码,例如作为其他函数的参数,反复调用。
语法:pointer -> member
,代表 “这个指针所指的那个结构变量里的那个成员”。
示例:
/*定义一个结构变量today,里面有一个成员是month*/
struct date today;
struct date* ptoday = &today;
(*p).month = 12;
p -> month = 12;
使用示例:
#include<stdio.h>
struct date {
int month;
int day;
};
struct date* getStruct(struct date *p);
void printSturct(const struct date* p);
int main()
{
struct date today;
printSturct(getStruct(&today));
*getStruct(&today) = (struct date){12, 18};
printSturct(&today);
getStruct(&today) -> day = 19;
printSturct(&today);
return 0;
}
struct date* getStruct(struct date *p)
{
scanf("%d %d", &(p -> month), &(p -> day));
return p;
}
void printSturct(const struct date* p)
{
printf("%d-%d\n", p -> month, p -> day);
}
结构数组¶
结构数组的本质是数组,只不过该数组的元素是结构体类型的数据
语法
struct class {
char* name;
int age;
long int id;
};
int main()
{
struct class student[26] = {{"zrz", 18, 3240105996}, {"hhh", 18, 3240100000}};
}
结构中的结构¶
语法:还是按照普通变量来理解
示例:
struct point {
int x;
int y;
};
struct rectangle {
struct point pt1;
struct point pt2;
};
int main()
{
struct rectangle rt1;
struct rectangle *prt1 = &rt1;
scanf("%d %d", &(rt1.pt1.x), &(rt1.pt1.y));
scanf("%d %d", &(prt1 -> pt2.x), &(prt1 -> pt2.y));
}
另外,结构和数组等可以无限组合嵌套,例如结构中的结构的数组
联合¶
定义语法:与结构很像
存储:
-
所有成员共享一个内存空间
-
同一时间只有一个成员是有效的
即填入另一个成员的值即将前面的冲掉
-
联合的大小
sizeof(union)
是其最大的成员
初始化:对第一个成员初始化
应用场景
-
节省内存:
在这个例子中,
联合最常见的用途之一是节省内存。例如,在处理需要多种数据类型但只会使用其中一种的场景下,使用联合可以避免每种数据类型都占用单独的内存区域。Data
联合只需要足够存储最大的成员(char str[20]
)的内存,而不需要分别为int
、float
和char[]
分配内存空间。 -
简化存储不同格式的数据:
联合可以存储不同类型的数据,在需要存储多种不同数据格式但只需存储一个格式的情况下特别有用。例如,读取网络数据时,我们可能会在同一位置存储整数、浮点数或字符串等不同数据类型。 -
实现类型安全的多态(polymorphism):
在没有面向对象支持的语言(如C语言)中,可以使用联合类型模拟多态。联合类型允许函数根据需要选择合适的数据类型来操作。通过使用union Value { int intVal; float floatVal; }; void printValue(union Value v, int isInt) { if (isInt) { printf("Integer: %d\n", v.intVal); } else { printf("Float: %.2f\n", v.floatVal); } }
isInt
标识符,函数printValue
可以根据需要处理不同类型的数据。 -
嵌入式系统中的硬件寄存器操作: 在嵌入式开发中,联合可以方便地访问硬件寄存器的不同部分。例如,一个32位的硬件寄存器可以被分解为多个8位字段。
通过使用联合,程序可以以不同的方式访问寄存器的内容(如整个32位寄存器或分解后的各个字节)。 -
类型转换(Type casting): 联合在C语言中也常用于类型转换,尤其是在需要通过不同类型的视图来查看同一块内存数据时。例如,在实现某些特定算法时,可能需要通过联合来在整数和浮点数之间进行转换。
???为啥?到底是几?
-
实现简易的自定义数据结构: 联合可以用于实现自定义的数据结构。例如,当数据结构中有多个可能的数据类型时,可以使用联合来减少内存占用。
在这个例子中,Shape
结构体根据type
字段的不同值来区分它是圆形还是矩形,并通过联合在同一内存位置存储不同的数据类型。 -
解析复杂的数据格式: 联合类型非常适合用来解析复杂的二进制数据格式。不同的数据字段可以用不同的方式解读。使用联合可以更方便地从一个字节流中提取不同类型的数据,常见于文件解析或网络数据包的处理。
union DataPacket { uint32_t integerData; float floatData; char stringData[16]; }; union DataPacket packet; packet.integerData = 0x12345678; // 作为整数处理 printf("Integer: %u\n", packet.integerData); packet.floatData = 3.14159f; // 作为浮点数处理 printf("Float: %.4f\n", packet.floatData); packet.stringData[0] = 'H'; // 作为字符串处理 packet.stringData[1] = 'i'; packet.stringData[2] = '\0'; printf("String: %s\n", packet.stringData);
-
用于操作不同数据结构: 联合可以用来处理包含多种不同数据结构的数据,例如,某个函数可能需要在不同情况下处理不同的数据结构。通过联合,你可以在同一个内存空间内实现这些数据结构的共享。
在这个例子中,union Data { struct { int x; int y; } point; // 用于存储坐标 struct { int width; int height; } rectangle; // 用于存储矩形尺寸 };
Data
联合可以存储一个坐标点或一个矩形的尺寸,但不能同时存储两者。 -
优化内存池管理: 在内存池管理中,联合可用于构建高效的内存分配方案。例如,在一个内存池中管理不同类型的对象时,使用联合类型可以为每个对象节省内存。
这里的内存池union Object { int intValue; float floatValue; char strValue[50]; }; struct MemoryPool { union Object objects[100]; };
MemoryPool
可以用来管理100个对象,每个对象可能是一个整数、浮点数或字符串,而不必为每种类型分配不同的内存块。 -
简化图像处理中的像素表示: 在图像处理或图形编程中,像素的数据表示可以使用联合来简化处理。例如,一个像素可能包含RGB(红、绿、蓝)三个颜色通道,可以将其表示为一个整数,也可以将每个通道分开存储。
-
模拟操作系统中的进程状态: 在操作系统中,可以使用联合来模拟进程的不同状态。一个进程的状态可能包含不同类型的数据结构(如寄存器值、程序计数器、堆栈指针等)。通过联合,可以根据需要访问不同的状态信息。
通过使用联合,操作系统内核可以方便地访问进程的寄存器内容或程序计数器与堆栈指针。 -
模拟设备控制寄存器: 在硬件编程中,联合类型常用于模拟设备的控制寄存器,其中不同的控制位可能对应不同的控制功能。通过联合,可以以不同的视角来访问同一寄存器。
在这个例子中,union ControlRegister { uint32_t regValue; // 32位寄存器值 struct { unsigned bit0 : 1; // 控制位0 unsigned bit1 : 1; // 控制位1 unsigned bit2 : 1; // 控制位2 unsigned bit3 : 1; // 控制位3 unsigned bit4 : 1; // 控制位4 unsigned reserved : 27; // 其他保留位 }; };
ControlRegister
联合让你可以直接访问控制寄存器的32位值,也可以单独操作每个控制位。
总结
C语言中的联合类型是一种有效的节省内存的工具,特别适合在程序中存储和操作多种类型的数据。它能够用于实现多态、硬件寄存器的高效操作、类型转换、数据结构优化等多种实际应用。通过共享内存空间,联合可以极大地减少内存消耗,尤其是在内存资源有限的嵌入式系统和其他高效计算场景中。
自定义数据类型(typedef)¶
语法:
例如,typedef int Length;
:这样,Length
成为 int
的别名,可以代替 int
。 再如;
Date
代表 typedef
和 Date
中间所有东西,Date
\(\Leftrightarrow\) struct adate
typedef
用于给现有类型定义新的别名或创建易于使用的自定义类型。
1. 简化复杂类型 为指针类型创建别名,简化代码的书写和阅读。
#include <stdio.h>
// 为指针类型定义别名
typedef char* String;
int main() {
String name = "Alice";
printf("Name: %s\n", name);
return 0;
}
解析:String
是 char*
的别名,简化了定义指针变量的代码。
2. 定义结构体的别名 为结构体类型创建更简洁的名称。
#include <stdio.h>
// 定义结构体和别名
typedef struct {
int x;
int y;
} Point;
int main() {
Point p = {10, 20};
printf("Point: (%d, %d)\n", p.x, p.y);
return 0;
}
解析:使用 typedef
后,定义变量时无需再写 struct
,直接使用 Point
。
3. 定义枚举类型的别名 为枚举类型起一个更直观的名字。
#include <stdio.h>
// 定义枚举类型和别名
typedef enum { RED, GREEN, BLUE } Color;
int main() {
Color favoriteColor = GREEN;
printf("Favorite color code: %d\n", favoriteColor);
return 0;
}
解析:Color
是枚举类型的别名,代码更直观。
再例,
4. 定义自定义数据类型 将常用的基础类型替换为更具语义的名字。
#include <stdio.h>
// 定义一个长度类型
typedef unsigned int Length;
int main() {
Length len = 100;
printf("Length: %u\n", len);
return 0;
}
解析:Length
是 unsigned int
的别名,用于表示逻辑上的长度,增加代码语义。
5. 定义函数指针的别名 为函数指针类型定义别名,简化函数指针的声明和使用。
#include <stdio.h>
// 定义函数指针类型
typedef int (*Operation)(int, int);
// 函数定义
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
Operation op; // 使用别名定义函数指针变量
op = add;
printf("Add: %d\n", op(5, 3));
op = multiply;
printf("Multiply: %d\n", op(5, 3));
return 0;
}
解析:Operation
是函数指针 int (*)(int, int)
的别名,简化了函数指针的声明。
6. 定义数组类型的别名 为数组定义一个别名,用于统一管理数据类型。
#include <stdio.h>
// 定义数组类型别名
typedef int Matrix[3][3];
int main() {
Matrix mat = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", mat[i][j]);
}
printf("\n");
}
return 0;
}
解析:Matrix
是一个 3x3 整数数组的别名,简化了矩阵类型的声明。
7. 定义位域类型的别名 为包含位域的结构体定义一个易用的别名。
#include <stdio.h>
// 定义位域类型
typedef struct {
unsigned int isAvailable : 1;
unsigned int isReadOnly : 1;
unsigned int isHidden : 1;
} FileAttributes;
int main() {
FileAttributes file = {1, 0, 1};
printf("Available: %u, ReadOnly: %u, Hidden: %u\n",
file.isAvailable, file.isReadOnly, file.isHidden);
return 0;
}
解析:FileAttributes
是带有位域的结构体的别名,用于简化属性定义。
8. 定义通用数据类型 为便于跨平台开发,将特定平台的基础类型定义为通用类型。
#include <stdio.h>
// 定义跨平台的整型别名
typedef unsigned long long int U64;
int main() {
U64 largeNumber = 1234567890123456789ULL;
printf("Large number: %llu\n", largeNumber);
return 0;
}
解析:U64
是 unsigned long long int
的别名,用于表示 64 位无符号整数。
总结
typedef
的用法非常灵活,常见场景包括: 1. 简化复杂类型的声明。 2. 提高代码可读性和语义性。 3. 增强代码的可维护性和可移植性。
在不同的场景中,根据需求使用 typedef
可以使代码更加清晰简洁。
杂项¶
ASCII
字符 | 中文名称 | 十进制 | 十六进制 | 八进制 | 二进制 |
---|---|---|---|---|---|
NUL | 空字符 | 0 | 0x00 | 000 | 00000000 |
SOH | 标题开始 | 1 | 0x01 | 001 | 00000001 |
STX | 正文开始 | 2 | 0x02 | 002 | 00000010 |
ETX | 正文结束 | 3 | 0x03 | 003 | 00000011 |
EOT | 传输结束 | 4 | 0x04 | 004 | 00000100 |
ENQ | 请求 | 5 | 0x05 | 005 | 00000101 |
ACK | 确认 | 6 | 0x06 | 006 | 00000110 |
BEL | 响铃 | 7 | 0x07 | 007 | 00000111 |
BS | 退格 | 8 | 0x08 | 010 | 00001000 |
TAB | 水平制表符 | 9 | 0x09 | 011 | 00001001 |
LF | 换行 | 10 | 0x0A | 012 | 00001010 |
VT | 垂直制表符 | 11 | 0x0B | 013 | 00001011 |
FF | 换页符 | 12 | 0x0C | 014 | 00001100 |
CR | 回车 | 13 | 0x0D | 015 | 00001101 |
SO | 禁止切换 | 14 | 0x0E | 016 | 00001110 |
SI | 允许切换 | 15 | 0x0F | 017 | 00001111 |
SP | 空格 | 32 | 0x20 | 040 | 00100000 |
! | 感叹号 | 33 | 0x21 | 041 | 00100001 |
" | 双引号 | 34 | 0x22 | 042 | 00100010 |
# | 井号 | 35 | 0x23 | 043 | 00100011 |
$ | 美元符 | 36 | 0x24 | 044 | 00100100 |
% | 百分号 | 37 | 0x25 | 045 | 00100101 |
& | 和号/商标符 | 38 | 0x26 | 046 | 00100110 |
' | 单引号 | 39 | 0x27 | 047 | 00100111 |
( | 左括号 | 40 | 0x28 | 050 | 00101000 |
) | 右括号 | 41 | 0x29 | 051 | 00101001 |
* | 星号 | 42 | 0x2A | 052 | 00101010 |
+ | 加号 | 43 | 0x2B | 053 | 00101011 |
, | 逗号 | 44 | 0x2C | 054 | 00101100 |
- | 减号 | 45 | 0x2D | 055 | 00101101 |
. | 句号 | 46 | 0x2E | 056 | 00101110 |
/ | 斜杠 | 47 | 0x2F | 057 | 00101111 |
0 | 数字0 | 48 | 0x30 | 060 | 00110000 |
1 | 数字1 | 49 | 0x31 | 061 | 00110001 |
2 | 数字2 | 50 | 0x32 | 062 | 00110010 |
3 | 数字3 | 51 | 0x33 | 063 | 00110011 |
4 | 数字4 | 52 | 0x34 | 064 | 00110100 |
5 | 数字5 | 53 | 0x35 | 065 | 00110101 |
6 | 数字6 | 54 | 0x36 | 066 | 00110110 |
7 | 数字7 | 55 | 0x37 | 067 | 00110111 |
8 | 数字8 | 56 | 0x38 | 070 | 00111000 |
9 | 数字9 | 57 | 0x39 | 071 | 00111001 |
: | 冒号 | 58 | 0x3A | 072 | 00111010 |
; | 分号 | 59 | 0x3B | 073 | 00111011 |
< | 小于号 | 60 | 0x3C | 074 | 00111100 |
= | 等号 | 61 | 0x3D | 075 | 00111101 |
> | 大于号 | 62 | 0x3E | 076 | 00111110 |
? | 问号 | 63 | 0x3F | 077 | 00111111 |
@ | 电子邮件符 | 64 | 0x40 | 100 | 01000000 |
A | 字母A | 65 | 0x41 | 101 | 01000001 |
B | 字母B | 66 | 0x42 | 102 | 01000010 |
C | 字母C | 67 | 0x43 | 103 | 01000011 |
D | 字母D | 68 | 0x44 | 104 | 01000100 |
E | 字母E | 69 | 0x45 | 105 | 01000101 |
F | 字母F | 70 | 0x46 | 106 | 01000110 |
G | 字母G | 71 | 0x47 | 107 | 01000111 |
H | 字母H | 72 | 0x48 | 110 | 01001000 |
I | 字母I | 73 | 0x49 | 111 | 01001001 |
J | 字母J | 74 | 0x4A | 112 | 01001010 |
K | 字母K | 75 | 0x4B | 113 | 01001011 |
L | 字母L | 76 | 0x4C | 114 | 01001100 |
M | 字母M | 77 | 0x4D | 115 | 01001101 |
N | 字母N | 78 | 0x4E | 116 | 01001110 |
O | 字母O | 79 | 0x4F | 117 | 01001111 |
P | 字母P | 80 | 0x50 | 120 | 01010000 |
Q | 字母Q | 81 | 0x51 | 121 | 01010001 |
R | 字母R | 82 | 0x52 | 122 | 01010010 |
S | 字母S | 83 | 0x53 | 123 | 01010011 |
T | 字母T | 84 | 0x54 | 124 | 01010100 |
U | 字母U | 85 | 0x55 | 125 | 01010101 |
V | 字母V | 86 | 0x56 | 126 | 01010110 |
W | 字母W | 87 | 0x57 | 127 | 01010111 |
X | 字母X | 88 | 0x58 | 130 | 01011000 |
Y | 字母Y | 89 | 0x59 | 131 | 01011001 |
Z | 字母Z | 90 | 0x5A | 132 | 01011010 |
[ | 左方括号 | 91 | 0x5B | 133 | 01011011 |
\ | 反斜杠 | 92 | 0x5C | 134 | 01011100 |
] | 右方括号 | 93 | 0x5D | 135 | 01011101 |
^ | 插入符号 | 94 | 0x5E | 136 | 01011110 |
_ | 下划线 | 95 | 0x5F | 137 | 01011111 |
` | 反引号 | 96 | 0x60 | 140 | 01100000 |
a | 字母a | 97 | 0x61 | 141 | 01100001 |
b | 字母b | 98 | 0x62 | 142 | 01100010 |
c | 字母c | 99 | 0x63 | 143 | 01100011 |
d | 字母d | 100 | 0x64 | 144 | 01100100 |
e | 字母e | 101 | 0x65 | 145 | 01100101 |
f | 字母f | 102 | 0x66 | 146 | 01100110 |
g | 字母g | 103 | 0x67 | 147 | 01100111 |
h | 字母h | 104 | 0x68 | 150 | 01101000 |
i | 字母i | 105 | 0x69 | 151 | 01101001 |
j | 字母j | 106 | 0x6A | 152 | 01101010 |
k | 字母k | 107 | 0x6B | 153 | 01101011 |
l | 字母l | 108 | 0x6C | 154 | 01101100 |
m | 字母m | 109 | 0x6D | 155 | 01101101 |
n | 字母n | 110 | 0x6E | 156 | 01101110 |
o | 字母o | 111 | 0x6F | 157 | 01101111 |
p | 字母p | 112 | 0x70 | 160 | 01110000 |
q | 字母q | 113 | 0x71 | 161 | 01110001 |
r | 字母r | 114 | 0x72 | 162 | 01110010 |
s | 字母s | 115 | 0x73 | 163 | 01110011 |
t | 字母t | 116 | 0x74 | 164 | 01110100 |
u | 字母u | 117 | 0x75 | 165 | 01110101 |
v | 字母v | 118 | 0x76 | 166 | 01110110 |
w | 字母w | 119 | 0x77 | 167 | 01110111 |
x | 字母x | 120 | 0x78 | 170 | 01111000 |
y | 字母y | 121 | 0x79 | 171 | 01111001 |
z | 字母z | 122 | 0x7A | 172 | 01111010 |
{ | 左花括号 | 123 | 0x7B | 173 | 01111011 |
| | 竖线 | 124 | 0x7C | 174 | 01111100 |
} | 右花括号 | 125 | 0x7D | 175 | 01111101 |
~ | 波浪号 | 126 | 0x7E | 176 | 01111110 |
DEL | 删除 | 127 | 0x7F | 177 | 01111111 |
单个数字0(0)(ASCII的0位)就是'\0';带引号的字符0('0') 是ASCII的48位
运算符和表达式¶
牢记几句话
表达式的值
表达式定义:
运算符 + 运算对象
运算对象:常量、变量和函数等表达式
分类
算术表达式、赋值表达式、关系表达式、逻辑表达式、条件表达式和逗号表达式
优先级¶
最好都加上括号
-
逻辑表达式:C赋值:真1 假0
-
循环判断:非零真
-
++ --
- 数据的值一样,表达式的值不一样
- a++:表达式:a+1之前的值,把表达式的值赋给其他
- ++a:表达式:a+1之后的值,把表达式的值赋给其他
- 数据的值一样,表达式的值不一样
-
只能对变量不能对表达式用
-
左右结合:先从哪边算
-
x = y = 3 : 不能在变量定义处用,可以后面用,结合顺序
-
复合赋值:i+=1:更高效
-
逻辑运算短路:
- ||:前面真后面不算
- &&:前面假后面不算
-
逗号运算符:值是最后一个子表达式的值
位运算¶
结果是表达式的值
重点
计算机中,数字的储存、运算都是以 补码 形式
Ctrl F 数字编码
位逻辑运算¶
~按位取反
&
按位与
特点: 全1才1
应用:
-
让某一位或者某些位为0
示例:
x & 0xFE
:将最后一位变成0 -
取一个数中的一段
示例:
x & 0xFF
:取出最后两个字节中的内容
|
按位或
特点: 有1则1
应用:
-
将某些位变为1:或上那一位为1的数
示例:
x | 0x01
最右边一位为1 -
将两个数拼起来
示例:
0x00FF | 0xFF00
逻辑运算(&&
、||
)相当于将所有非0值都变成1,然后做按位对应运算
^
按位异或
特点 :
- 两个位相同得0,不同得1
x ^ y ^ y 变回到 x
移位运算¶
箭头朝向即为移动方向
移动的位数为正
i << j
对x的所有位左移j个位置,右边填入0
-
x <<= n
大多数情况下等价于x *= $2^n$
-
当左移的位数超过位宽(
sizeof(int) * 8
)或有符号数符号位变化时,将导致未定义行为 -
位移导致舍弃前面的位的情况也不是
-
i >> j
对x的所有位右移j个位置
- 对于unsigned类型,左边填0
- 对于signed类型,符号位保持不变,原来的高位移到地位
x >>= n
大多数情况下等价于x /= $2^n$
直接遗弃超出范围的位
复合位赋值运算¶
&=
,|=
,^=
,>>=
,<<=
a &= b
等价于 a = a & b
; a >>= 3
等价于 a = a >> 3
应用¶
-
伪转换二进制
-
单片机
位段¶
语法:
- 可以直接用位段的成员名访问
注意事项:
-
存储大小与对齐:
位段的存储大小通常以 int 或 unsigned int 为单位,且会受编译器的内存对齐策略影响。
多个位段成员可能共享同一个存储单元(如果其总位宽小于存储单元大小),但如果超出单元大小,会分配下一个单元。
-
即,所需的位超过一个int时会采用多个int
-
因此,访问位段成员的地址的操作是有语法错误的
-
-
数据类型的限制:
位段成员通常只能是 int 或 unsigned int,具体限制依赖于编译器。
-
移植性问题:
不同编译器和平台对位段的实现(如存储顺序、对齐方式)可能不同,可移植性较差。
-
位宽限制:
位宽不能超过存储单位的大小。例如,如果 int 是 32 位,位宽不能大于 32。
示例:
struct U0 {
unsigned int leading: 3;
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 27;
};
void toBinary(int num)
{
unsigned int mask = 1u << 31;
for(; mask; mask >>= 1){
printf("%d", num & mask ? 1 : 0);
}
}
int main()
{
struct U0 num1, num2;
num2.leading = 2;
num2.FLAG1 = 1;
num2.FLAG2 =0;
num2.trailing = 0;
printf("please enter your number_1: ");
scanf("%x", &*(int*)&num1);
printf("the binary of your number_1 is: ");
toBinary(*(int*)&num1);
printf("\n");
printf("the binary of your number_1 is: ");
toBinary(*(int*)&num2);
printf("\n");
return 0;
}
应用场景: 底层,对硬件的操作
-
嵌入式开发:
在嵌入式系统中,用位段模拟硬件寄存器的各个位字段。 - 网络协议:
用位段解析网络协议的标志字段(如 TCP/IP 报头)。 - 标志集合:
用位段压缩存储多个布尔标志,以节省内存。
循环和分支¶
循环:
- for
- while
- do while
语法¶
do while
的语法
> 0
当 while
做出错时,试试 do while
灵活:
-
for内部:
(起始条件; 条件判断; 结束条件)
- 内部三处均可按需省略,保留两个分号即可
- 不一定只能有一个变量,几个条件不一定必须是只含
i
的逻辑表达式
-
何时开始何时结束
#include <stdio.h> #include<stdlib.h> #include<string.h> int main(void) { int i, n = 0; char *color[20], str[15]; scanf("%s", str); while(str[0] != '#') { color[n] = (char *)malloc(sizeof(char)*(strlen(str)+1)); strcpy(color[n], str); n++; scanf("%s", str); } for(i = n-1; i >= 0; i--) printf("%s ", color[i]); return 0; }
示例¶
写无限循环遇到某条件跳出:
分支
if
switch
示例:
模拟一个内存管理操作系统的命令行界面,提供查看内存状态、分配内存、释放内存和退出操作。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_BLOCKS 10 // 最大内存块数
typedef struct {
int id; // 内存块 ID
size_t size; // 内存块大小
int in_use; // 是否正在使用(1 表示使用中,0 表示未使用)
} MemoryBlock;
MemoryBlock memory[MAX_BLOCKS]; // 模拟内存块数组
void initialize_memory() {
for (int i = 0; i < MAX_BLOCKS; i++) {
memory[i].id = i;
memory[i].size = 0;
memory[i].in_use = 0;
}
}
void view_memory() {
printf("\n=== 内存状态 ===\n");
for (int i = 0; i < MAX_BLOCKS; i++) {
printf("块 ID: %d, 大小: %zu 字节, 状态: %s\n",
memory[i].id,
memory[i].size,
memory[i].in_use ? "已分配" : "未分配");
}
}
void allocate_memory() {
int id;
size_t size;
printf("请输入要分配的块 ID (0-%d): ", MAX_BLOCKS - 1);
scanf("%d", &id);
if (id < 0 || id >= MAX_BLOCKS) {
printf("无效的块 ID!\n");
return;
}
if (memory[id].in_use) {
printf("块 ID %d 已被占用!\n", id);
return;
}
printf("请输入要分配的大小 (字节): ");
scanf("%zu", &size);
memory[id].size = size;
memory[id].in_use = 1;
printf("内存块 ID %d 分配成功,大小为 %zu 字节!\n", id, size);
}
void free_memory() {
int id;
printf("请输入要释放的块 ID (0-%d): ", MAX_BLOCKS - 1);
scanf("%d", &id);
if (id < 0 || id >= MAX_BLOCKS) {
printf("无效的块 ID!\n");
return;
}
if (!memory[id].in_use) {
printf("块 ID %d 未被分配,无需释放!\n", id);
return;
}
memory[id].size = 0;
memory[id].in_use = 0;
printf("内存块 ID %d 已成功释放!\n", id);
}
int main() {
int choice;
initialize_memory(); // 初始化内存状态
while (1) {
// 打印菜单
printf("\n=== 内存管理系统 ===\n");
printf("1. 查看内存状态\n");
printf("2. 分配内存\n");
printf("3. 释放内存\n");
printf("4. 退出\n");
printf("请输入你的选择 (1-4): ");
scanf("%d", &choice);
switch (choice) {
case 1:
view_memory();
break;
case 2:
allocate_memory();
break;
case 3:
free_memory();
break;
case 4:
printf("退出内存管理系统。\n");
return 0;
default:
printf("无效的选择,请输入 1 到 4 之间的数字。\n");
}
}
return 0;
}
运行示例
初始化时查看内存状态:
分配内存块:
查看内存状态:
=== 内存状态 ===
块 ID: 0, 大小: 0 字节, 状态: 未分配
块 ID: 1, 大小: 0 字节, 状态: 未分配
块 ID: 2, 大小: 1024 字节, 状态: 已分配
...
释放内存块:
查看内存状态:
退出程序:
代码解析
-
MemoryBlock
结构体: - 模拟内存块的信息,包括块 ID、大小和状态。 -
initialize_memory
函数: - 初始化所有内存块为未分配状态。 -
view_memory
函数: - 显示当前内存的分配状态。 -
allocate_memory
和free_memory
函数: - 分配或释放内存块,并更新状态。 -
switch
语句: - 实现不同操作之间的选择。
函数¶
定义、声明与调用¶
声明¶
编译器一行一行编译,故调用之前应该让编译器知道函数的返回类型、参数、名称
建议:用声明:先读main函数干什么
位置
- C89:写在main里面也可
- C99:写在main前面
规则: 定义声明一致
实际上,声明中可以只写变量类型,变量名称也可以和函数定义头部不一样。因为编译器检查只检查定义和声明变量类型是否一样
参数和值¶
核心:
要在函数内部对主函数的变量进行操作,则必须得把主函数中的那个变量or其地址传入函数。
形参实参
- 参数 & 值:实参 —— 参数;形参 —— 值(参数的值):就是传值
数组作为函数的参数
变量空间:¶
每个函数都有他自己的变量空间; 离开一个函数f到另一个函数g里面,则会跳出f的变量空间,来到g的变量空间; 在一个函数g里面对变量操作,不会影响f里面的变量,因为不在一个变量空间;
局部变量¶
前置
- 生存期:变量多会出现,多会消亡
- 作用域:在代码的什么范围内可以访问这个变量(这个变量起作用)
定义
局部变量(==本地变量、自动变量)
概念:
每次函数运行,都产生一个独立的变量空间,这个空间中的变量是函数这次运行独有的
分类:定义在函数内部的 & 参数
规则:
定义在块内,即一个大括号{ },进入块,变量存在;离开块,变量消失 - 函数的块内 - 语句的块内 - 甚至以单拉一个大括号定义变量
内部的变量外部不可以访问,外部的变量内部可以访问
同名变量: - 内外同名:内部掩盖外部的 - 内部同名:编译错误redefination
本地变量不会默认初始化,但是参数进入函数时已经被初始化(参数的传递)
调用函数¶
传递的值(实参):常量、变量、表达式(的值)
类型不匹配: C会发生自动类型转换:即将传入的参数类型转换为定义中说的那个类型。Java、C++会严格检查类型的匹配。
传值: 永远是传值给函数,即参数传递的单向性:实参的值传给形参,形参的值改变了,也不会影响实参。还是变量空间的问题,swap的例子。
返回值¶
return作用:
- 结束执行函数
- 返回一个值,将这个值给到调用它的地方:调用它的地方那里写的函数调用就是代表该函数的返回值。
规则:
return;
,return 表达式;
将表达式的值传出去- 函数里面可以多个return,也可以不在函数最后。
- 单一出口理念:最好函数只有一个出口即只有一个return。
杂项¶
-
没有参数
f(void)
: 不传参数,声明时写上void,别不写 -
逗号运算符
f(a, b)
: 逗号是标点符号不是运算符,这是传了俩参数f((a, b))
:(a, b)
是一个表达式,值是b,则这句代表传的是这个表达式的值:b - 函数中不能定义函数,可以声明 - 函数声明可以放在自己的定义里面 - main函数return 0;返回0:正确;返回非0:异常
bash :
echo $?
: 可查看main运行结束的返回值(return -1 则 stdout:255)
标准库¶
stdio.h¶
1. printf
功能:格式化输出到标准输出(屏幕)。
函数原型:int printf(const char *format, ...);
示例:
2. scanf
功能:从标准输入读取格式化数据。
函数原型:int scanf(const char *format, ...);
示例:
scanf的工作原理
scanf
的工作原理与输入缓冲区的影响
- 标准输入缓冲区的作用 - 当用户输入内容并按下 Enter 键时,输入的所有字符(包括换行符
\n
)会被存储在标准输入缓冲区中。 -scanf
从缓冲区中读取数据,根据指定的格式(如%s
、%c
等)提取需要的部分,剩余的内容仍保留在缓冲区中。
scanf
格式说明符的行为
%s
(读取字符串)scanf("%s", buffer)
会跳过缓冲区中所有的空白字符(包括空格、换行符、制表符)作为起始位置。- 然后从第一个非空白字符开始读取,直到遇到下一个空白字符(或缓冲区结尾),并将读取的字符存储到
buffer
中。 -
剩下的内容(包括分隔字符串的空白字符,如换行符)留在缓冲区中。
-
%c
(读取单个字符) scanf("%c", &ch)
不会跳过空白字符,而是直接读取缓冲区的下一个字符,包括换行符或空格。- 如果缓冲区中还有未处理的换行符,
%c
就会直接读取它。
程序的输入流程分析
假设我们运行以下代码并输入数据:
char matrix[5][100];
char ch;
for (int i = 0; i < 5; i++) {
scanf("%s", matrix[i]);
}
scanf("%c", &ch);
输入内容:
详细流程:
-
第一轮循环读取字符串 - 用户输入 "Apple" + 按下 Enter。 - 输入缓冲区内容为:
Apple\n
-scanf("%s", matrix[0]);
:- 跳过缓冲区中的空白字符(无)。
- 读取
Apple
到matrix[0]
。 - 停止在第一个空白字符(
\n
),缓冲区变为:\n
。
-
第二轮循环读取字符串 - 用户输入 "Banana" + 按下 Enter。 - 输入缓冲区内容为:
\nBanana\n
(上轮剩下的\n
+ 新输入的Banana\n
)。 -scanf("%s", matrix[1]);
:- 跳过缓冲区中的空白字符(
\n
)。 - 读取
Banana
到matrix[1]
。 - 停止在第一个空白字符(
\n
),缓冲区变为:\n
。
- 跳过缓冲区中的空白字符(
-
重复过程 - 对每一轮字符串输入,
scanf("%s", ...)
都会读取用户输入的内容,同时将末尾的换行符\n
留在缓冲区。 -
读取字符
ch
- 第五次循环结束后,缓冲区中剩下一个换行符\n
(用户按下 Enter)。 -scanf("%c", &ch);
不跳过空白字符,直接读取到这个换行符\n
。 -ch
存储的不是用户期望的字符,而是换行符。
-
解决方法
-
清理缓冲区 在读取字符前手动清除输入缓冲区:
-
修改读取方式 改用
getchar
或更高级的输入方法: -
结合
fgets
使用 改用fgets
一次性读取一整行数据,减少缓冲区问题:
3. fprintf
功能:格式化输出到指定文件流。
函数原型:int fprintf(FILE *stream, const char *format, ...);
示例:
4. fscanf
功能:从指定文件流读取格式化数据。
函数原型:int fscanf(FILE *stream, const char *format, ...);
示例:
5. fopen
功能:打开文件。
函数原型:FILE *fopen(const char *filename, const char *mode);
示例:
6. fclose
功能:关闭文件流。
函数原型:int fclose(FILE *stream);
示例:
7. fgetc
功能:从文件流中读取单个字符。
函数原型:int fgetc(FILE *stream);
示例:
8. fputc
功能:将单个字符写入文件流。
函数原型:int fputc(int c, FILE *stream);
示例:
9. fgets
功能:从文件流读取一行。
函数原型:char *fgets(char *str, int n, FILE *stream);
示例:
10. fputs
功能:将字符串写入文件流。
函数原型:int fputs(const char *str, FILE *stream);
示例:
11. getchar
功能:从标准输入读取单个字符。
函数原型:int getchar(void);
示例:
12. putchar
功能:输出单个字符到标准输出。
函数原型:int putchar(int c);
示例:
13. gets
(已不推荐使用,存在安全隐患)
功能:从标准输入读取字符串。
函数原型:char *gets(char *str);
示例:
14. puts
功能:输出字符串到标准输出。
函数原型:int puts(const char *str);
示例:
15. feof
功能:检查文件流是否到达末尾。
函数原型:int feof(FILE *stream);
示例:
16. ferror
功能:检查文件流是否有错误。
函数原型:int ferror(FILE *stream);
示例:
17. rewind
功能:将文件流位置指针重置到文件开头。
函数原型:void rewind(FILE *stream);
示例:
18. ftell
功能:获取文件流当前位置。
函数原型:long ftell(FILE *stream);
示例:
19. fseek
功能:设置文件流位置指针。
函数原型:int fseek(FILE *stream, long offset, int whence);
示例:
20. clearerr
功能:清除文件流的错误标志和 EOF 标志。
函数原型:void clearerr(FILE *stream);
示例:
string.h¶
1. strlen
功能:返回字符串的长度(不包括终止符 \0
)。
函数原型:size_t strlen(const char *str);
示例:
2. strcpy
功能:将字符串复制到另一个字符串。
函数原型:char *strcpy(char *dest, const char *src);
底层:第一个参数代表目标的起始地址,第二个参数代表源头的起始地址。
示例:
3. strncpy
功能:复制指定长度的字符串到另一个字符串。
函数原型:char *strncpy(char *dest, const char *src, size_t n);
示例:
4. strcat
功能:将字符串追加到另一个字符串的末尾。
函数原型:char *strcat(char *dest, const char *src);
示例:
5. strncat
功能:将指定长度的字符串追加到另一个字符串的末尾。
函数原型:char *strncat(char *dest, const char *src, size_t n);
示例:
6. strcmp
功能:比较两个字符串(区分大小写)。
函数原型:int strcmp(const char *str1, const char *str2);
示例:
7. strncmp
功能:比较指定长度的两个字符串。
函数原型:int strncmp(const char *str1, const char *str2, size_t n);
示例:
8. strchr
功能:查找字符串中首次出现的指定字符。
函数原型:char *strchr(const char *str, int c);
示例:
char *pos = strchr("Hello", 'e');
if (pos) {
printf("Found at index %ld\n", pos - "Hello");
}
/*
查找字符第一次出现的位置。如果找到了,函数会返回指向该字符的指针;如果找不到,则返回 NULL。
"Hello" 是字符串的起始地址。
pos 是目标字符 'e' 的地址。
将两个指针相减,结果就是目标字符相对于字符串起始位置的索引。
*/
9. strrchr
功能:查找字符串中最后一次出现的指定字符。
函数原型:char *strrchr(const char *str, int c);
示例:
10. strstr
功能:查找字符串中首次出现的子串。
函数原型:char *strstr(const char *haystack, const char *needle);
示例:
11. strtok
功能:分割字符串(以指定分隔符为界)。
函数原型:char *strtok(char *str, const char *delim);
示例:
char str[] = "Hello,World";
char *token = strtok(str, ",");
while (token) {
printf("%s\n", token);
token = strtok(NULL, ",");
}
/*
char *strtok(char *__restrict__ __s, const char *__restrict__ __delim)
Divide S into tokens separated by characters in DELIM.
*/
/*
1. **`char str[] = "Hello,World";`**
- 定义了一个字符串数组 `str`,其中包含内容 `"Hello,World"`。
- 字符串 `str` 可被修改(不同于字符串常量)。
2. **`char *token = strtok(str, ",");`**
- `strtok` 函数用于将字符串 `str` 按分隔符 `","`(逗号)进行分割。
- 第一次调用时,`strtok` 将会:
1. 查找第一个分隔符 `','`。
2. 将分隔符替换为 `'\0'`(字符串结束符)。
3. 返回指向第一个子字符串(即 `Hello`)的指针。
3. **`while (token) {`**
- 只要 `token` 不为 `NULL`,就继续循环。
- `strtok` 会返回每个子字符串的指针,直到字符串末尾时返回 `NULL`。
4. **`printf("%s\n", token);`**
- 打印当前的子字符串(token)。
5. **`token = strtok(NULL, ",");`**
- 继续查找下一个子字符串:
- 第二次及之后的调用中,传入的第一个参数必须为 `NULL`,表示继续处理上一次的字符串。
- 查找到下一个分隔符,返回对应的子字符串指针。
*/
12. memset
功能:将内存的某一部分设置为指定值。
函数原型:void *memset(void *s, int c, size_t n);
示例:
char buffer[10];
memset(buffer, 'A', 10);
buffer[9] = '\0';
printf("%s\n", buffer); // 输出 "AAAAAAAAA"
13. memcpy
功能:复制内存区域。
函数原型:void *memcpy(void *dest, const void *src, size_t n);
示例:
14. memmove
功能:在内存区域重叠时安全地复制内存。
函数原型:void *memmove(void *dest, const void *src, size_t n);
示例:
15. memcmp
功能:比较两个内存区域。
函数原型:int memcmp(const void *s1, const void *s2, size_t n);
示例:
16. strdup
(POSIX标准,部分实现中提供)
功能:复制字符串并返回新分配的副本。
函数原型:char *strdup(const char *str);
示例:
17. strcspn
功能:返回在字符串中找到的第一个不属于指定字符集的字符位置。
函数原型:size_t strcspn(const char *str1, const char *str2);
示例:
18. strspn
功能:返回字符串中连续包含指定字符集的字符数。
函数原型:size_t strspn(const char *str1, const char *str2);
示例:
19. strpbrk
功能:查找字符串中第一个包含在指定字符集中的字符。
函数原型:char *strpbrk(const char *str1, const char *str2);
示例:
20. strrev
(非标准函数,部分实现中提供)
功能:反转字符串。
函数原型:char *strrev(char *str);
示例:
ctype.h¶
1. isalnum
功能:检查字符是否为字母或数字。
函数原型:int isalnum(int c);
示例:
2. isalpha
功能:检查字符是否为字母。
函数原型:int isalpha(int c);
示例:
3. isdigit
功能:检查字符是否为数字(0-9)。
函数原型:int isdigit(int c);
示例:
4. islower
功能:检查字符是否为小写字母。
函数原型:int islower(int c);
示例:
5. isupper
功能:检查字符是否为大写字母。
函数原型:int isupper(int c);
示例:
6. isspace
功能:检查字符是否为空白字符(如空格、换行、制表符)。
函数原型:int isspace(int c);
示例:
7. iscntrl
功能:检查字符是否为控制字符(如回车、删除等)。
函数原型:int iscntrl(int c);
示例:
8. isprint
功能:检查字符是否为可打印字符(包括空格)。
函数原型:int isprint(int c);
示例:
9. isgraph
功能:检查字符是否为可打印字符(不包括空格)。
函数原型:int isgraph(int c);
示例:
10. ispunct
功能:检查字符是否为标点符号。
函数原型:int ispunct(int c);
示例:
11. tolower
功能:将字符转换为小写(若可能)。
函数原型:int tolower(int c);
示例:
12. toupper
功能:将字符转换为大写(若可能)。
函数原型:int toupper(int c);
示例:
13. isxdigit
功能:检查字符是否为十六进制数字(0-9, A-F, a-f)。
函数原型:int isxdigit(int c);
示例:
14. isblank
(C99标准新增)
功能:检查字符是否为空格或制表符。
函数原型:int isblank(int c);
示例:
15. isascii
(非标准函数,部分实现中提供)
功能:检查字符是否为ASCII字符(0-127)。
函数原型:int isascii(int c);
示例:
16. toascii
(非标准函数,部分实现中提供)
功能:将字符转换为ASCII值。
函数原型:int toascii(int c);
示例:
stdlib.h¶
1. malloc
功能:分配动态内存。
函数原型:void *malloc(size_t size);
示例:
2. calloc
功能:分配并初始化动态内存。
函数原型:void *calloc(size_t num, size_t size);
示例:
int *ptr = (int *)calloc(5, sizeof(int));
if (ptr) {
printf("Memory allocated and initialized to zero\n");
free(ptr);
}
3. realloc
功能:调整动态内存大小。
函数原型:void *realloc(void *ptr, size_t size);
示例:
int *ptr = (int *)malloc(5 * sizeof(int));
ptr = (int *)realloc(ptr, 10 * sizeof(int));
if (ptr) {
printf("Memory resized\n");
free(ptr);
}
4. free
功能:释放动态内存。
函数原型:void free(void *ptr);
示例:
5. abs
功能:计算整数的绝对值。
函数原型:int abs(int n);
示例:
6. labs
功能:计算长整数的绝对值。
函数原型:long int labs(long int n);
示例:
7. llabs
(C99标准新增)
功能:计算长长整数的绝对值。
函数原型:long long int llabs(long long int n);
示例:
8. atoi
功能:将字符串转换为整数。
函数原型:int atoi(const char *str);
示例:
9. atof
功能:将字符串转换为浮点数。
函数原型:double atof(const char *str);
示例:
10. atol
功能:将字符串转换为长整数。
函数原型:long int atol(const char *str);
示例:
11. atoll
(C99标准新增)
功能:将字符串转换为长长整数。
函数原型:long long int atoll(const char *str);
示例:
12. strtol
功能:将字符串转换为长整数,支持指定进制。
函数原型:long int strtol(const char *str, char **endptr, int base);
示例:
13. strtod
功能:将字符串转换为双精度浮点数。
函数原型:double strtod(const char *str, char **endptr);
示例:
14. rand
功能:生成伪随机数。
函数原型:int rand(void);
示例:
15. srand
功能:设置随机数种子。
函数原型:void srand(unsigned int seed);
示例:
16. system
功能:执行系统命令。
函数原型:int system(const char *command);
示例:
17. bsearch
功能:在排序数组中执行二分查找。
函数原型:
void *bsearch(const void *key, const void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));
int arr[] = {1, 2, 3, 4, 5};
int key = 3;
int *res = (int *)bsearch(&key, arr, 5, sizeof(int), compare);
if (res) printf("Found: %d\n", *res);
18. qsort
功能:对数组进行快速排序。
函数原型:
int cmp(const void *a, const void* b) // 这里在qsort函数原型中就是const void *的指针,所以不能变
{
const char *ca = (const char*)a; // 根据要排序的数组的类型 选择强制类型转换的目标类型
const char *cb = (const char*)b; // 强制类型转换
return *ca - *cb;
}
int arr[] = {5, 2, 3, 1, 4};
qsort(arr, 5, sizeof(int), compare);
for (int i = 0; i < 5; i++) printf("%d ", arr[i]);
19. exit
功能:终止程序执行。
函数原型:void exit(int status);
示例:
20. div
功能:执行整数除法并返回商和余数。
函数原型:div_t div(int numerator, int denominator);
示例:
21. labs
功能:计算长整数的绝对值。
函数原型:long int labs(long int n);
示例:
22. getenv
功能:获取环境变量的值。
函数原型:char *getenv(const char *name);
示例:
23. _Exit
(C99标准新增)
功能:立即退出程序,不执行清理操作。
函数原型:void _Exit(int status);
示例:
math.h¶
1. sqrt
功能:计算平方根。
函数原型:double sqrt(double x);
示例:
2. pow
功能:计算 x 的 y 次幂。
函数原型:double pow(double x, double y);
示例:
3. fabs
功能:计算绝对值(浮点数)。
函数原型:double fabs(double x);
示例:
4. ceil
功能:向上取整。
函数原型:double ceil(double x);
示例:
5. floor
功能:向下取整。
函数原型:double floor(double x);
示例:
6. round
功能:四舍五入到最近的整数值。
函数原型:double round(double x);
示例:
7. fmod
功能:计算浮点数的余数。
函数原型:double fmod(double x, double y);
示例:
8. exp
功能:计算 e 的 x 次幂。
函数原型:double exp(double x);
示例:
9. log
功能:计算自然对数(以 e 为底)。
函数原型:double log(double x);
示例:
10. log10
功能:计算常用对数(以 10 为底)。
函数原型:double log10(double x);
示例:
11. sin
功能:计算弧度值的正弦值。
函数原型:double sin(double x);
示例:
12. cos
功能:计算弧度值的余弦值。
函数原型:double cos(double x);
示例:
13. tan
功能:计算弧度值的正切值。
函数原型:double tan(double x);
示例:
14. asin
功能:计算反正弦(弧度值)。
函数原型:double asin(double x);
示例:
15. acos
功能:计算反余弦(弧度值)。
函数原型:double acos(double x);
示例:
16. atan
功能:计算反正切(弧度值)。
函数原型:double atan(double x);
示例:
17. atan2
功能:计算 y/x 的反正切值(弧度值,考虑象限)。
函数原型:double atan2(double y, double x);
示例:
18. hypot
功能:计算欧几里得距离(sqrt(x^2 + y^2)
)。
函数原型:double hypot(double x, double y);
示例:
19. cbrt
(C99标准新增)
功能:计算立方根。
函数原型:double cbrt(double x);
示例:
20. round
功能:返回最接近的整数(四舍五入)。
函数原型:double round(double x);
示例:
21. trunc
功能:截断小数部分,保留整数部分。
函数原型:double trunc(double x);
示例:
22. modf
功能:将浮点数分解为整数和小数部分。
函数原型:double modf(double x, double *iptr);
示例:
double intpart, fracpart;
fracpart = modf(3.14, &intpart);
printf("Integer: %f, Fraction: %f\n", intpart, fracpart); // 输出 "Integer: 3.000000, Fraction: 0.140000"
23. fmax
(C99标准新增)
功能:返回两个浮点数中的较大值。
函数原型:double fmax(double x, double y);
示例:
24. fmin
(C99标准新增)
功能:返回两个浮点数中的较小值。
函数原型:double fmin(double x, double y);
示例:
25. copysign
(C99标准新增)
功能:将 y 的符号赋值给 x。
函数原型:double copysign(double x, double y);
示例:
数组¶
一维数组¶
定义和引用
- 数组长度必须得是一个数字,不能放变量,即使那个变量有值也不行。 即:
-
引用时只能引用单个值,不能一次引用整个数组。引用其实就是访问和操作那个东西
-
数组下标越界:不可越界访问;越界访问,随意赋值
-
在内存中的存放 与前后数据:见下神奇的try:不一定 内部:index从小到大地址依次增大。从下到上排
初始化
- C语言规定只能对静态存储的数组初始化,但是课本允许对动态数组+静态数组初始化 eg:
-
不初始化:
静态数组
static arr[5]
:不初始化则全是0动态数组
arr[5]
:不初始化则随机数 - 部分初始化:初始化前几个,后面没有初始化的元素默认赋值为0
-
全部元素都赋值则可以省略数组长度(不建议)
神奇的try看着玩玩
//按照书上的标准,这个定义方式是错的,但是不带了改代码了......
#include<stdio.h>
int main()
{
int a = 4;
int arr[a];
for(int i = 0; i < a; i++){
arr[i] = i;
}
int b = 3;
printf("%p\n", &a); printf("%p\n", &b); printf("%p\n", &arr); //打印三个的地址
printf("%p\n",&arr[0]); printf("%p\n",&arr[3]); printf("%p\n",&arr[4]); printf("%p\n",&arr[7]); //越界访问,且发现&arr[7] ==&a
printf("%d\n",*&arr[7]);
printf("%d\n",*&a); //发现上面那个之后试试a和arr[7]是什么,发现arr[7]被赋值为a的值4
printf("%d\n",arr[9]); printf("%d\n",arr[10]); //越界访问,随意赋值
int try[5] = {1, 2};
printf("%d\n", try[3]); //初始化部分元素,后面的自动赋值0
return 0;
}
/*
输出:
a's ptr:0x7ffc2289afe4 b's ptr:0x7ffc2289afe8 arr's ptr0x7ffc2289afd0 arr[0]'s ptr:0x7ffc2289afd0 arr[3]'s ptr:0x7ffc2289afdc
arr[4]'s ptr:0x7ffc2289afe0 arr[7]'s ptr:0x7ffc2289afec
arr[7]:4 *&a:4
arr[9]:0 arr[10]:579448784
try[3]:0
示例
-
用数组计算斐波那契数列,每行打印5个数字,最后一行不满5个也要换行
```c
include
¶
约 24436 个字 2258 行代码 18 张图片 预计阅读时间 110 分钟
# define MAXN 46 /* 定义符号常量MAXN */
int main(void)
{
int i, n;
int fib[MAXN] = {1, 1}; /* 初始化前两个 */
printf ("Enter n: ");
scanf ("%d", &n);
if(n >= 1 && n <= 46 ){
for(i = 2; i < n; i++){
fib[i] = fib[i - 1] + fib[i - 2];
}
/* 学学人家怎么输出:*/
for(i = 0; i < n; i++){
printf("%6d", fib[i]);
if((i + 1) % 5 == 0){
/* 每5个换行:循环变量i+1是5的倍数;注意细节i+1 */
printf("\n");
}
}
if(n % 5 != 0){
/* 最后不满5个换行:数学转化:最后一行和总数mod5同余 */
printf("\n");
}
}else{
printf("Invalid Value.\n");
}
return 0;
}
```
-
选择法排序
字符串¶
定义与初始化
-
结束符
- 有效长度:不包含‘\0' : 计算字符串长度不包括末尾0
-
结束符:'\0'
char arr[] = {'h', 'i'}
不是字符串char arr[] = {'h', 'i', '\0'}
orchar arr[] = {'h', 'i', 0}
是字符串 -
定义字符串长度 >= 字符串有效长度 + 1
因为有结束符'\0'
开大数组:只对前面的赋值,'\0'后面的与字符串无关,字符串到'\0'即结束,故不会影响字符串的处理。
-
定义方法
-
数组 & 指针:
char str[]
orchar* str
都可以用来定义数组,但是两个不一样。如果要用指针:不能之后再赋值,否则将导致segmentation fault ~ -
放在一维数组中
-
使用字符串常量
-
字符串常量:就是双引号括起来的一个字符串,两种定义方式:
char str[]
orchar* str
char* str
:只读char str[]
:也是只读,不过在堆栈区会copy一份跟他一样的字符串,这个是可以修改的 -
字符串常量不能修改(但是不能写
const
) - 相同的字符串字面量初始化两不同名字的字符串常量,在一样的地址上(在代码段,是只读的)
字符串常量
在C语言中,用数组和指针定义的字符串的区别主要在于它们的存储位置和是否可以修改。
-
char* str1 = "hi";
- 这里str1
是一个指向字符的指针,它指向一个字符串字面量。字符串字面量存储在程序的只读数据段中,这意味着你不能修改str1
指向的内容。尝试修改str1
指向的字符串将导致未定义行为,通常是程序崩溃。 - 例如,以下代码是不允许的: -
char str2[] = "hi";
- 这里str2
是一个字符数组,它在栈上分配了足够的空间来存储字符串 "hi" 及其结尾的空字符\0
。str2
存储的是数组的第一个元素的地址,这意味着你可以修改str2
中的字符。 - 例如,以下代码是允许的:
-
字符串字面量(
str1
):字符串字面量存储在只读数据段中,这是C语言规范的一部分。编译器将字符串字面量放在只读内存区域,以防止程序修改它们。这样做的好处是可以节省内存,因为多个相同的字符串字面量可以共享同一块内存区域。 -
字符数组(
str2
):字符数组是在栈上分配的,它们的生命周期仅限于定义它们的函数或代码块。字符数组的内容可以被修改,因为它们不是存储在只读内存区域。
在你提供的两个例子中:
char* str1 = "hi";
-
这里的
"hi"
是一个字符串常量,而str1
是一个指向这个字符串常量的指针。 -
char str2[] = "hi";
- 这里的
"hi"
也是一个字符串常量,但str2
是一个字符数组,它在栈上分配了空间,并且包含了字符串常量的内容。尽管str2
本身不是一个字符串常量,它包含了字符串常量的内容,并且这些内容在数组中是可以被修改的。
为什么
str2
不是字符串常量?str2
不是字符串常量,因为它是一个字符数组,它包含了字符串常量的内容,但存储在栈上,并且其内容是可以被修改的。字符串常量本身是存储在只读内存区域的,而str2
只是包含了这些内容的一个可修改的副本。 -
访问
-
可以用数组,可以用指针,多用指针
-
通常涉及'\0',用它来控制
输入输出
- 用
getchar()
,特定字符控制
-
直接读一个字符串
-
先读一个在读一个字符串
- 用
scanf()
-
读取(可能)含有空格的字符串:目的是通过含有空格字符串的测试点
这种方法不可以用于读取单个字符
-
scanf读一个单词:到空格/tab/回车 为止,即见到那三个就停止读入了
- %ns (n为一个整数):这次输入最多输入\(n\)个值,其他的内容停在输入流中,等待下一个scanf,这些scanf依然遵循上一条char str1[8]; char str2[8]; scanf("%3s", str1); scanf("%s", str2); printf("%s##%s", str1, str2); /* input 1234 56 --> output 123##4 input 12 345 --> output 12##345 input 12 34567789835374 --> *** stack smashing detected ***: terminated \ [1] 25177 IOT instruction (core dumped) ./wk input 123456 --> output 123##456
程序参数
main函数原型:int main(int argc, char const *argv[])
-
int argc
- 含义: 表示命令行参数的数量(argument count)。
- 值:
- 包括程序名在内的参数总数。
- 至少为1(即使没有其他参数,程序名也会作为第一个参数)。
-
char const *argv[]
- 含义: 表示命令行参数的数组(argument vector)。
- 值:
argv[0]
: 通常是程序的名称(包括路径,取决于操作系统)。argv[1]
到argv[argc-1]
: 由用户提供的其他命令行参数。
- 类型:
const char* argv[]
是一个字符串指针数组,表示每个参数是一个以空字符\0
结尾的字符串。
- 用法:
- 可以通过索引访问每个参数。
- 如果需要将字符串参数转换为数字,可以使用函数如
atoi
或strtol
。
示例
#include <stdio.h>
int main(int argc, char const *argv[]) {
printf("Number of arguments: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}
运行
假设编译生成的可执行文件为 program
:
输出:
程序链接:
二维数组¶
定义和引用
- 先行数后列数
-
在内存中的存放
按行 —— 列顺序存放:先0行,再1行……每行按列顺序存放
从上到下:00,01,02,10,11,12,20,21,22……
初始化
-
分行赋值
-
按顺序每行一个括号
-
部分赋值
- 括号在:内部也可像一维的一样只赋前面几个的值,空括号代表全0
- 没or少括号:代表全0
- 顺序赋值 全写出 or 部分写出,
- 如果完整赋值,可以省略行长度
-
矩阵的术语
- 主对角线:
if(i == j)
- 上三角:
if(i <= j)
- 下三角:
if(i >= j)
- 副对角:
if(i + j == n - 1)
- 遍历上三角:
/*j的循环体:*/ j = i; j < n; j++
指针¶
概念与定义¶
基本定义
理解:有一个整型变量i,p这个变量的值是变量i地址的变量,称p为指针变量
易错:其中,q只是普通int变量;要想定义多个指针变量得分别指定规则¶
-
输出:得用%p,不能转换成整数再%x(16进制),因为这俩值一不一样取决于编译器、64or32位架构
-
取址符右边只能是变量;
&(++i)
,&(i + j)
均不合法
指针运算¶
q - p
:同类型指针相减,表示差的元素个数
(int)p - (int)q
:表示差的字节数
p + 1
/ p-1
:指向下一个存储单元 / 指向上一个存储单元
其他都非法:指针相加、相乘和相除,指针加or减浮点数
可以++,--,+=,-=:注意对应的表达式值&变量值
Info
++i(表达式值+1) 大于 *,i++(表达式值不变) 等于 * 但从右向左结合:意味着俩都先于*,那么*其实是对表达式的值取*
例如
应用
*p++ : 取出原本这个位置的值再把p+1
指针比较 <, <=, >, >=, =, != 比较地址大小 数组中元素地址 线性递增
应用¶
指针与函数¶
数组作参数
函数定义:
-
常用:指针 例如
*arr
orarr[]
-
其他
// 1. 不指定大小,单独传递数组名(等价于指针) void func(int arr[]); // 2. 带有形参大小的语义化声明(仅作提示,与1等价) void func(int arr[length]); // 3. 传递数组和大小(常用方式) void func(int arr[], int size); // 4. 使用指针的方式(与1等价) void func(int *arr); // 5. 传递指针和大小(常用方式,等价于3) void func(int *arr, int size); // 6. 使用通用指针(适合泛型处理) void func(void *arr) { int *intArr = (int *)arr; // 需要显式转换 } /* 1. 参数 void *arr 的含义 void * 是一种通用指针类型,表示它可以指向任意类型的数据(int、float、char 等)。 它不能直接用于解引用或进行算术运算,因为编译器不知道它指向的具体数据类型。 2. 显式类型转换 (int *)arr int *intArr = (int *)arr; 将 void * 指针显式转换为 int * 指针。 这样,编译器就知道 intArr 指向的是一个 int 类型数据,允许后续进行解引用和算术运算。 */ // 7. 多维数组,必须指定列数(编译器需要推导数组布局) void func(int arr[][N]); // 适用于静态二维数组 void func(int arr[M][N]); // 如果固定行数,也可显式指定 // 8. 多维数组同时传递行数(灵活处理,但需要列数固定) void func(int arr[][N], int rows); // 9. 动态分配的二维数组(需传递指针数组) void func(int **arr, int rows, int cols); // 10. const修饰符,保护数组内容(适合只读操作) void func(const int arr[], int size); void func(const int *arr, int size); // 11. restrict关键字(优化提示,避免指针别名) void func(int *restrict arr, int size); // 12. 使用stddef.h的size_t定义大小(推荐规范写法) #include <stddef.h> void func(int arr[], size_t size); void func(int *arr, size_t size); // 13. 使用指针加偏移处理子数组 void func(int *arr, int startIndex, int length); // 14. 同时传递数组和数据类型信息(处理泛型或多类型场景) void func(void *arr, size_t elementSize, size_t length);
函数调用:
- 常用:数组名
arr
-
其他
// 1. 直接传递数组名 int arr[10] = {0}; func(arr); // 对应 void func(int arr[]); 或 void func(int *arr); // 2. 带有大小参数 int arr[10] = {0}; func(arr, 10); // 对应 void func(int arr[], int size); 或 void func(int *arr, int size); // 3. 传递多维数组 int arr[3][4] = {{0}}; func(arr); // 对应 void func(int arr[][4]); 或 void func(int arr[M][4]); func(arr, 3); // 对应 void func(int arr[][4], int rows); // 4. 动态分配的一维数组 int *arr = malloc(10 * sizeof(int)); func(arr); // 对应 void func(int *arr); func(arr, 10); // 对应 void func(int *arr, int size); // 5. 动态分配的二维数组(指针数组) int **arr = malloc(3 * sizeof(int *)); for (int i = 0; i < 3; i++) arr[i] = malloc(4 * sizeof(int)); func(arr, 3, 4); // 对应 void func(int **arr, int rows, int cols); // 6. 传递通用指针 void *arr = malloc(10 * sizeof(int)); func(arr); // 对应 void func(void *arr); // 7. 传递子数组(指针偏移) int arr[10] = {0}; func(arr + 5, 5); // 对应 void func(int *arr, int size); 或子数组操作 // 8. 常量数组传递 const int arr[10] = {0}; func(arr, 10); // 对应 void func(const int arr[], int size); // 9. 使用 restrict 修饰的数组 int arr[10] = {0}; func(arr, 10); // 对应 void func(int *restrict arr, int size); // 10. 传递多类型数据 double darr[10] = {0}; func((void *)darr, sizeof(double), 10); // 对应 void func(void *arr, size_t elementSize, size_t length); // 11. 多维数组的动态分配模拟 int *arr = malloc(3 * 4 * sizeof(int)); func(arr, 3, 4); // 对应 void func(int *arr, int rows, int cols); // 12. 直接传递字符数组(字符串) char str[] = "hello"; func(str); // 对应 void func(char arr[]); 或 void func(char *arr);
经典交换
不理解就记住只有第二个能成功
试图理解
函数:仍然是参数的传递
变量作参数:将那个变量的值给到函数的形参,而函数结束后,形参消失,原来的变量仍然是原来的值
指针做参数:将那个变量的值给到函数的形参,这里,值是地址值,通过地址,可以在函数内部访问外面那个值
函数多返回值¶
原理
通过传入指针变量,更改对应地址上的值,实现“多返回值”。
形式
函数定义:参数例如 int* p
函数调用:参数例如 &num
例子:
#include<stdio.h>
void minmax(int arr[], int *min, int *max)
/*
传入参数有讲究:要在定义的函数中对main函数输入/定义的数组进行处理,就必须得传入它,这是函数参数传递的基本内容;
传入指针变量,因为定义的函数内部要对main函数中的min,max变量进行处理,原理同上面;
**核心:要在函数内部对主函数的变量进行操作,则必须得把主函数中的那个变量or其地址传入函数**
*/
{
int i, j;
*min = arr[0];
*max = arr[0];
for(i = 0; i < 5; i++){
if(arr[i] >= *max) *max = arr[i];
if(arr[i] <= *min) *min = arr[i];
}
printf("min = %d\n", *min);
printf("max = %d", *max);
}
int main()
{
int min; int max;
int arr[5] = {1,4,7,5,0};
minmax(arr, &min, &max);
return 0;
}
int a[]
int *a
都行 其他场景
函数返回状态:return返回运算的状态,指针返回运算结果
数组与指针¶
牢记几句话
“数组名是指向数组首元素的指针”
“同类型指针直接加减是加一个sizeof,实现移位的功能”
几组等价表示¶
那么,arr , &arr[0] , &*arr , p , &p[0] , &*p
/*注意:
不包含&arr: 他是整个数组的地址;
他的类型:int (*)[10],即一个指向包含 10 个 int 元素的数组的指针。
&arr + 1 :加一个数组的大小
arr + 1 :加一个元素的大小
*/
arr[i] , *(arr + i) , p[i] , *(p + i)
arr++
int n, i;
scanf("%d", &n);
int* arr = (int* )malloc(n * sizeof(int));
for(i = 0; i < n; i++) scanf("%d", &arr[i]);
for (i = 0; i < n; i++) printf("%d#", arr[i]);
return 0;
用法¶
遍历:p++
法一:
法二:其他¶
- 数组变量是const类型指针:常量指针 即:
int b[]
<=>int* const b
故数组变量不能直接赋值, 即:
const与指针¶
-
指针可以是const(指针不可修改:
const
在*
后面):这个指针变量的值(地址)不能变了,不能再指向其他变量 -
所指的是const(通过指针不可修改:
const
在*
前面):表示不能通过这个指针去修改那个变量,但是这个指针和那个变量都不是const都可以修改.用处:大的结构用const结构的指针 -
const数组
const int a\[] = {1,2,3,4,5,6}
: 这里的const表示每个元素都是const int用处:防止函数对数组修改:
int func(const int arr[], int len)
动态分配内存¶
C99中的代替方法:可变长度数组 C89中:malloc函数:#include<stdlib.h>
语法
int* a = malloc(n * sizeof(int))
malloc() & free()
函数原型:
void* malloc(size_t size);
void free(void *ptr);
free() and malloc() is 绑定使用的
参数:内存大小;
返回值:void* :一个指针,指向一块内存,单位为字节
之后需要强制类型转换 (int*)malloc(n * sizeof(int))
之后将malloc产生的那个指针当数组来用即可
之后要free(a) //a 是那个指针
必须得是malloc申请来的内存才能被free,其他不行。 好习惯:定义指针先赋值0,最后在free(0)
合理设计程序结构以找到合适地方进行free
如果没有内存了:返回0 orNULL
示例
int n, i;
scanf("%d", &n);
int* arr = (int* )malloc(n * sizeof(int));
for(i = 0; i < n; i++) scanf("%d", &arr[i]);
for (i = 0; i < n; i++) printf("%d#", arr[i]);
free(arr);
return 0;
数组与指针¶
指针数组¶
函数指针¶
char **a
: a是一个指针,指向另一个指针,那个指针指向一个字符串
char *a[]
:
杂项¶
- NULL与0地址
- 指针一定要现初始化!
- 指针类型转换
- 原理:换一种视角去看那些内存空间
- 普通指针
- 事实上,指针的大小都是一样的,可以进行强制类型转换
- 理解:用内存格格来看:原来这堆格格代表A类型的数据,强制类型转换后代表B类型的数据,按照数据存储的规则进行“翻译”即可 例子:
不同类型指针的相互赋值
在C语言中,int
类型和 char
类型的指针可以相互赋值,因为它们通常具有相同的大小(在大多数平台上,char
是1字节,int
是4字节,但指针的大小是固定的,通常是4字节或8字节,取决于系统架构)。然而,这种赋值通常是不安全的,因为 int
和 char
指针指向的数据大小不同,解引用这些指针可能会导致未定义行为。
以下是一些示例和说明:
示例1:将 int*
赋值给 char*
int i = 10;
int* intPtr = &i;
char* charPtr = (char*)intPtr; // 将int*转换为char*
// 打印charPtr指向的值
printf("%d\n", *charPtr); // 打印i的最低字节
int
类型的指针转换为 char
类型的指针,并打印出指向的值。这里打印的是 int
值的最低字节,因为 char
类型是1字节的。 示例2:将 char*
赋值给 int*
char chars[4] = {'a', 'b', 'c', 'd'};
char* charPtr = chars;
int* intPtr = (int*)charPtr; // 将char*转换为int*
// 打印intPtr指向的值
printf("%c\n", *intPtr); // 打印chars数组的前4个字节作为一个整数
char
类型的指针转换为 int
类型的指针,并打印出指向的值。这里打印的是 char
数组的前4个字节作为一个整数的ASCII值。 注意事项 虽然这些赋值在技术上是可能的,但它们可能会导致未定义行为,特别是当你尝试解引用这些指针并访问它们指向的数据时。这是因为 int
和 char
类型的数据在内存中的表示和对齐方式可能不同。
- 对齐问题:许多系统要求
int
类型的数据必须在4字节边界上对齐,而char
类型的数据没有这样的要求。将char*
赋值给int*
可能会导致对齐问题,从而导致程序崩溃或数据损坏。 - 数据解释:将
int*
赋值给char*
或反之,可能会导致数据解释错误,因为int
和char
类型的数据在内存中的布局不同。
因此,除非完全清楚这样做的后果,否则应避免将 int
和 char
类型的指针相互赋值。在实际编程中,最好使用相同类型的指针来操作相同类型的数据。
编译预处理和宏¶
编译预处理¶
"#":编译预处理指令,这不是C语言,其他语言都有!所以后面不加分号! 例子:#include<stdio.h>
,#define PI 3.14
宏¶
#define PI 3.14
:定义一个宏(是一个符号),PI为名称,3.14是值
范例程序见下
规范¶
- 语法:没有等号,没有分号(因为不是一条C语句,其他语言都是用#编译预处理)
- 值:可以是任何东西,可以空格、标点等:名字再空格后面所有东西
- 其中可以有注释,该注释不会被替换进去
-
没有值的宏:
-
位置: 普通:源代码文件的顶部,或者放在头文件中
宏的所有定义位置
在C或C++中,定义宏(Macro)通常有两种方式:
-
预处理器指令: - 宏定义通常写在源代码文件的顶部,或者放在头文件中。它们使用预处理器指令
- 这种方式定义的宏在编译器进行预处理阶段之前就会被处理。#define
来定义。 - 例如,在源文件或头文件中定义一个宏: -
命令行参数: - 你也可以在编译时通过命令行参数定义宏。这在编译器(如GCC)的命令行参数中使用
- 这种方式定义的宏会在预处理阶段被识别和替换。-D
选项来实现。 - 例如,在GCC中定义一个宏: -
配置文件: - 在某些项目中,宏定义可能被放在一个专门的配置文件中,然后通过包含该文件来使用这些宏定义。
-
Makefile: - 在使用Makefile构建项目时,可以在Makefile中定义宏,并通过
-D
选项传递给编译器。 -
构建系统: - 在使用现代构建系统(如CMake或Bazel)时,宏定义可以在构建脚本中设置,并在编译时传递给编译器。
宏定义的位置取决于你的项目结构和个人偏好。通常,如果宏是项目中多个文件共享的,最好将它们定义在头文件中。如果宏是特定于单个文件的,可以直接在该文件中定义。如果宏的值需要在不同的构建之间改变,那么在命令行或构建系统中定义宏可能更灵活。
当然,让我们更详细地探讨在C/C++项目中定义宏的第2、4和5点:
- 命令行参数
在编译时,你可以通过编译器的命令行参数来定义宏。这通常通过-D
选项来实现,后面跟着宏的名称和可选的值。这种方法特别适用于:
- 编译时配置:根据不同的编译选项启用或禁用特定的代码段。
- 环境变量:在不同的开发环境或部署环境中使用不同的宏值。
示例:
在这个例子中,DEBUG
和MAX_THREADS
被定义为宏,DEBUG
没有指定值(通常用来启用调试代码),而MAX_THREADS
被赋予了值4
。
- Makefile
在Makefile中定义宏可以在构建过程中提供灵活性,特别是当你需要根据不同的目标或平台调整编译选项时。你可以在Makefile中设置宏,然后在编译命令中使用这些宏。
示例:
在这个Makefile中,CFLAGS
变量被用来添加编译器标志,包括定义宏USE_FEATURE
和VERSION
。当你运行make
命令时,这些宏会被传递给GCC编译器。
- 构建系统
现代构建系统如CMake提供了强大的宏和变量管理功能,允许你在构建过程中定义和使用宏。这些宏可以用于控制编译选项、源文件包含、依赖关系等。
示例:
# CMakeLists.txt
# 定义一个宏
add_definitions(-DENABLE_DEBUG)
# 设置一个变量,可以作为宏的值
set(MY_VERSION "1.2.3")
# 将变量转换为宏
add_definitions(-DVERSION=${MY_VERSION})
# 指定C++标准
set(CMAKE_CXX_STANDARD 11)
# 构建目标
add_executable(my_program my_program.cpp)
在这个CMake配置文件中,我们使用add_definitions
来定义宏和添加编译器标志。set
命令用于设置变量,这些变量可以被用作宏的值。add_executable
定义了一个可执行文件目标,它将使用前面定义的宏和设置。
使用构建系统定义宏的好处包括:
- 跨平台支持:构建系统通常提供了跨平台的构建配置。
- 依赖管理:自动处理源文件和库的依赖关系。
- 可重用性:构建脚本可以在多个项目中重用,只需少量修改。
这些方法提供了在不同层面上控制宏定义的灵活性,使得项目构建更加模块化和可配置。
- 注释:依然作为C语言注释
范例程序:
#include<stdio.h>
#define PI 3.14
#define PI2 2 * PI //不能写成2PI,因为不是合法的标识符 //一个宏中包含另一个宏
#define FORMAT "%f\n" //宏的值可以是任何东西
#define PRT printf("%f\n", PI); \
printf(FORMAT, PI2 * 3) //上面那一行"\"后面不能有任何东西空格、注释啥也不行 FORMAT文本替换,多行,第一行得有分号,因为被替换处需要分号
int main()
{
PRT;
return 0;
}
/*
也可以这样:
#define PRT printf("%f\n", PI); \
printf(FORMAT, PI2 * 3);
int main()
{
PRT
return 0;
}
注意分号
*/
-
原理: 特别简单的原始的文本替换:编译之前,编译预处理程序(cpp)把文件中所有宏的名字换成值
shell查看gcc编译预处理过程中的文件
.c,.i, .o,.s 分别是什么
这些文件扩展名代表了C/C++编程和编译过程中的不同阶段和类型的文件:
-
.c: - 这是C语言源代码文件的扩展名。它包含了用C语言编写的程序代码。例如,
main.c
就是一个C语言源文件。 -
.i: - 这是预处理后的源代码文件的扩展名。当你使用编译器的
-E
选项时,它会生成一个包含了预处理指令(如宏展开、条件编译指令、头文件包含等)后的文件。这个文件通常用于调试预处理阶段。 -
.o: - 这是目标文件(Object file)的扩展名。目标文件是编译器编译源代码后生成的中间文件,包含了源代码对应的机器码,但还没有进行链接。目标文件可以被链接器用来生成最终的可执行文件。
-
.s: - 这是汇编代码文件的扩展名。当你使用编译器的
-S
选项时,它会生成一个包含了源代码对应的汇编语言代码的文件。这个文件可以被汇编器进一步转换成目标文件。 -
.a: - 这是静态库文件的扩展名。静态库是一组目标文件的集合,它们被打包在一起,可以在编译时被链接到程序中。静态库通常用于共享代码或资源,而不需要在运行时动态加载。
-
.so: - 这是共享库文件(在Linux系统中)的扩展名。共享库是一种特殊的库,它们在运行时被动态加载和链接到程序中。这允许多个程序共享同一份库代码,节省内存并减少磁盘空间。
-
.exe: - 这是可执行文件的扩展名,在Windows系统中使用。可执行文件是编译后的程序,可以直接在操作系统中运行。
-
a.out: - 这是一个传统的可执行文件的名称,在Unix和类Unix系统中使用。在早期的Unix系统中,编译器默认生成的可执行文件被命名为
a.out
。尽管现代编译器允许你指定可执行文件的名称,但a.out
仍然被用作默认名称,尤其是在某些特定的编译环境或教学示例中。
这些文件类型和扩展名是C/C++编程和编译过程中的基本组成部分,了解它们有助于更好地理解程序的构建和运行过程。
预定义宏: 即C帮我定义好了,我直接使用
__LINE__
, __FILE__
, __DATE__
, __TIME__
, __STDC__
示例:
用法¶
带参数的宏¶
语法
注意:
- 定义语句括号内不需要参数类型
- 可以多个参数
- 可以嵌套组合使用
调用,与C函数调用相同。
易错点:“未理解原始文本替换”
所以:一切都要右括号:整个值要有;参数出现的每个地方都要有。
示例:
杂项:- 非常常见
- 用
#
##
两个运算符实现复杂功能,例如产生函数 - 部分宏会被
inline
函数代替 - 西方程序员比中国人爱用
- 其他编译预处理指令:
- 条件编译
- error
可变数组¶
功能:可增大、可获取大小、可访问
设计思想:
- 自定义类型一般不定义指针
- free 的对象是Array结构里面的数组
链表¶
可变数组的缺陷:内存受限场景下,反复重新申请大内存会有内存不够的情况
方法:申请block大小的内存,再次申请一个,将他俩链起来(告诉编译器下一块内存在哪里)
结点包含:数据、指向下一个的指针
语法:
在 Python 中,链表可以用类和对象来实现,因为 Python 本身没有像 C++ 或 Java 那样的内置链表数据结构。链表有两种主要类型:单向链表和双向链表。以下是关于它们的实现和操作的详细说明。
单向链表实现
定义单向链表:
class Node:
def __init__(self, data):
self.data = data # 存储节点数据
self.next = None # 指向下一个节点
class LinkedList:
def __init__(self):
self.head = None # 初始化头节点为空
# 向链表末尾添加节点
def append(self, data):
new_node = Node(data)
if not self.head: # 如果链表为空
self.head = new_node
return
current = self.head
while current.next: # 找到最后一个节点
current = current.next
current.next = new_node
# 打印链表
def print_list(self):
current = self.head
while current:
print(current.data, end=" -> ")
current = current.next
print("None")
# 删除值等于 data 的节点
def delete(self, data):
current = self.head
# 如果要删除的是头节点
if current and current.data == data:
self.head = current.next
current = None
return
# 找到要删除的节点
prev = None
while current and current.data != data:
prev = current
current = current.next
if not current: # 未找到节点
print("Node not found")
return
prev.next = current.next
current = None
使用示例:
# 创建链表
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
# 打印链表
ll.print_list() # 输出: 1 -> 2 -> 3 -> None
# 删除节点
ll.delete(2)
ll.print_list() # 输出: 1 -> 3 -> None
双向链表实现
定义双向链表:
class Node:
def __init__(self, data):
self.data = data
self.next = None # 指向下一个节点
self.prev = None # 指向前一个节点
class DoublyLinkedList:
def __init__(self):
self.head = None # 初始化头节点为空
# 在链表末尾添加节点
def append(self, data):
new_node = Node(data)
if not self.head: # 如果链表为空
self.head = new_node
return
current = self.head
while current.next: # 找到最后一个节点
current = current.next
current.next = new_node
new_node.prev = current
# 从链表头部打印链表
def print_list(self):
current = self.head
while current:
print(current.data, end=" <-> ")
current = current.next
print("None")
# 从链表尾部打印链表(反向)
def print_reverse(self):
current = self.head
while current and current.next:
current = current.next
while current:
print(current.data, end=" <-> ")
current = current.prev
print("None")
# 删除值等于 data 的节点
def delete(self, data):
current = self.head
# 如果要删除的是头节点
if current and current.data == data:
self.head = current.next
if self.head:
self.head.prev = None
current = None
return
# 找到要删除的节点
while current and current.data != data:
current = current.next
if not current: # 未找到节点
print("Node not found")
return
if current.next:
current.next.prev = current.prev
if current.prev:
current.prev.next = current.next
current = None
使用示例:
# 创建双向链表
dll = DoublyLinkedList()
dll.append(1)
dll.append(2)
dll.append(3)
# 打印链表
dll.print_list() # 输出: 1 <-> 2 <-> 3 <-> None
# 反向打印链表
dll.print_reverse() # 输出: 3 <-> 2 <-> 1 <-> None
# 删除节点
dll.delete(2)
dll.print_list() # 输出: 1 <-> 3 <-> None
链表操作总结
- 插入操作: 可以在头部、尾部或中间插入节点。
- 删除操作: 需要更新相邻节点的指针。
- 遍历操作: 从头节点开始,依次访问每个节点。
链表的实现非常灵活,适合存储动态数据结构,适用于需要频繁插入和删除操作的场景。
在 C 中,链表可以用结构体和指针来实现。以下是单向链表和双向链表的实现和常用操作的详细说明。
单向链表
定义单向链表节点
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构
struct Node {
int data; // 节点数据
struct Node* next; // 指向下一个节点的指针
};
// 创建新节点
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
单向链表操作
- 插入节点到链表末尾
void append(struct Node** head, int data) {
struct Node* newNode = createNode(data);
if (*head == NULL) { // 如果链表为空
*head = newNode;
return;
}
struct Node* temp = *head;
while (temp->next != NULL) { // 遍历到链表末尾
temp = temp->next;
}
temp->next = newNode;
}
append函数超详解
函数原型
参数说明:
plist
:指向链表结构的指针,用于表示要操作的链表。 -List
是一个结构体,包含指向链表头节点的指针head
。 - 通过传入List*
,函数能够直接操作链表的内容(比如添加节点)。num
:要添加到链表的新节点的值。
函数实现
void append(List* plist, int num)
{
// 1. 创建一个新的节点并初始化
Node* p = (Node*)malloc(sizeof(Node)); // 为新节点分配内存
p->value = num; // 设置新节点的值
p->next = NULL; // 新节点的 next 指针初始化为 NULL
// 2. 找到链表的最后一个节点
Node* last = plist->head; // 从链表头开始遍历
if (last) {
// 如果链表不为空,找到最后一个节点
while (last->next) {
last = last->next; // 循环向下,直到最后一个节点
}
// 3. 将新节点连接到最后一个节点
last->next = p;
} else {
// 如果链表为空,将新节点作为头节点
plist->head = p;
}
}
详细分步解析
1. 创建新节点
- 动态内存分配:
- 使用
malloc
分配一段内存,用于存储新节点。 - 返回值是
void*
,需要强制类型转换为Node*
。 - 初始化新节点:
p->value = num
:将新节点的value
字段设置为传入的值num
。p->next = NULL
:将新节点的next
指针设置为NULL
,表示它暂时是链表的最后一个节点。
2. 查找链表的最后一个节点
- 指针
last
的作用: last
是一个临时指针,用于遍历链表,找到当前链表的最后一个节点。- 初始时,
last
指向链表的头节点(plist->head
)。 - 判断链表是否为空:
- 如果
plist->head
为NULL
,说明链表为空,直接跳到else
部分。 - 如果
plist->head
不为NULL
,说明链表中至少有一个节点,进入while
循环。 while
循环:last->next
表示当前节点的下一节点是否存在。- 循环条件
last->next
为真时,将指针last
移动到下一个节点。
3. 连接新节点
- 当找到链表的最后一个节点时,将其
next
指针指向新创建的节点p
,这样p
成为新的最后一个节点。
4. 处理空链表
- 如果链表为空(
plist->head == NULL
),新节点p
直接成为链表的头节点。
函数工作原理举例
假设链表初始状态为空,依次调用 append
函数,插入 3 个值:10
、20
、30
。
初始状态
- 链表为空:
plist->head = NULL
第一步:添加 10
- 创建新节点
p
,值为10
,p->next = NULL
。 - 因为链表为空(
plist->head == NULL
),将plist->head
指向p
。 - 链表状态:10 -> NULL
第二步:添加 20
- 创建新节点
p
,值为20
,p->next = NULL
。 - 遍历链表,找到最后一个节点(
10
)。 - 将最后一个节点(
10
)的next
指针指向新节点p
。 - 链表状态:10 -> 20 -> NULL
第三步:添加 30
- 创建新节点
p
,值为30
,p->next = NULL
。 - 遍历链表,找到最后一个节点(
20
)。 - 将最后一个节点(
20
)的next
指针指向新节点p
。 - 链表状态:10 -> 20 -> 30 -> NULL
总结
append
函数的核心功能是向链表末尾添加一个新节点。主要步骤:
- 创建新节点,动态分配内存并初始化。
- 判断链表是否为空: - 如果为空,将新节点设为头节点。 - 如果不为空,遍历链表找到最后一个节点,并将新节点连接到最后。
- 打印链表
void printList(struct Node* head) {
struct Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
- 删除节点
void deleteNode(struct Node** head, int key) {
struct Node* temp = *head;
struct Node* prev = NULL;
// 如果头节点需要删除
if (temp != NULL && temp->data == key) {
*head = temp->next;
free(temp);
return;
}
// 查找要删除的节点
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}
// 如果未找到节点
if (temp == NULL) {
printf("Node with data %d not found.\n", key);
return;
}
prev->next = temp->next; // 删除节点
free(temp);
}
单向链表完整示例
int main() {
struct Node* head = NULL;
// 插入节点
append(&head, 10);
append(&head, 20);
append(&head, 30);
// 打印链表
printf("Linked List: ");
printList(head);
// 删除节点
deleteNode(&head, 20);
printf("After Deletion: ");
printList(head);
return 0;
}
双向链表
定义双向链表节点
#include <stdio.h>
#include <stdlib.h>
// 定义双向链表节点结构
struct Node {
int data; // 节点数据
struct Node* next; // 指向下一个节点
struct Node* prev; // 指向前一个节点
};
// 创建新节点
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
双向链表操作
- 插入节点到链表末尾
void append(struct Node** head, int data) {
struct Node* newNode = createNode(data);
if (*head == NULL) { // 如果链表为空
*head = newNode;
return;
}
struct Node* temp = *head;
while (temp->next != NULL) { // 遍历到链表末尾
temp = temp->next;
}
temp->next = newNode;
newNode->prev = temp;
}
- 从头部打印链表
void printList(struct Node* head) {
struct Node* temp = head;
while (temp != NULL) {
printf("%d <-> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
- 从尾部打印链表
void printReverse(struct Node* head) {
struct Node* temp = head;
if (temp == NULL) return;
// 遍历到链表末尾
while (temp->next != NULL) {
temp = temp->next;
}
// 从尾部向头部打印
while (temp != NULL) {
printf("%d <-> ", temp->data);
temp = temp->prev;
}
printf("NULL\n");
}
- 删除节点
void deleteNode(struct Node** head, int key) {
struct Node* temp = *head;
// 找到要删除的节点
while (temp != NULL && temp->data != key) {
temp = temp->next;
}
if (temp == NULL) { // 未找到节点
printf("Node with data %d not found.\n", key);
return;
}
if (temp->prev != NULL) {
temp->prev->next = temp->next;
} else { // 删除头节点
*head = temp->next;
}
if (temp->next != NULL) {
temp->next->prev = temp->prev;
}
free(temp);
}
双向链表完整示例
int main() {
struct Node* head = NULL;
// 插入节点
append(&head, 10);
append(&head, 20);
append(&head, 30);
// 正序打印链表
printf("Doubly Linked List: ");
printList(head);
// 倒序打印链表
printf("Reverse List: ");
printReverse(head);
// 删除节点
deleteNode(&head, 20);
printf("After Deletion: ");
printList(head);
return 0;
}
总结
- 单向链表适合基本操作,结构简单。
- 双向链表可以在两个方向上遍历,适合需要频繁前后移动的操作。
- 操作中要特别注意指针的正确操作,避免内存泄漏或段错误 (
segmentation fault
)。
在 C++ 中,可以使用类和指针来实现链表结构。以下是单向链表和双向链表的实现及常用操作的详细说明。
单向链表
定义单向链表节点
#include <iostream>
using namespace std;
// 定义单向链表节点
class Node {
public:
int data; // 节点数据
Node* next; // 指向下一个节点的指针
Node(int val) : data(val), next(nullptr) {} // 构造函数
};
定义单向链表类
class LinkedList {
private:
Node* head; // 指向头节点的指针
public:
LinkedList() : head(nullptr) {} // 构造函数
// 添加节点到链表末尾
void append(int data) {
Node* newNode = new Node(data);
if (head == nullptr) {
head = newNode;
return;
}
Node* temp = head;
while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = newNode;
}
// 打印链表
void printList() {
Node* temp = head;
while (temp != nullptr) {
cout << temp->data << " -> ";
temp = temp->next;
}
cout << "NULL" << endl;
}
// 删除指定值的节点
void deleteNode(int key) {
Node* temp = head;
Node* prev = nullptr;
// 删除头节点
if (temp != nullptr && temp->data == key) {
head = temp->next;
delete temp;
return;
}
// 查找要删除的节点
while (temp != nullptr && temp->data != key) {
prev = temp;
temp = temp->next;
}
// 未找到节点
if (temp == nullptr) {
cout << "Node with value " << key << " not found." << endl;
return;
}
// 删除节点
prev->next = temp->next;
delete temp;
}
// 析构函数:释放所有节点
~LinkedList() {
Node* temp;
while (head != nullptr) {
temp = head;
head = head->next;
delete temp;
}
}
};
单向链表完整示例
int main() {
LinkedList list;
// 添加节点
list.append(10);
list.append(20);
list.append(30);
// 打印链表
cout << "Linked List: ";
list.printList();
// 删除节点
list.deleteNode(20);
cout << "After Deletion: ";
list.printList();
return 0;
}
双向链表
定义双向链表节点
class Node {
public:
int data; // 节点数据
Node* next; // 指向下一个节点
Node* prev; // 指向前一个节点
Node(int val) : data(val), next(nullptr), prev(nullptr) {} // 构造函数
};
定义双向链表类
class DoublyLinkedList {
private:
Node* head; // 指向头节点
public:
DoublyLinkedList() : head(nullptr) {} // 构造函数
// 添加节点到链表末尾
void append(int data) {
Node* newNode = new Node(data);
if (head == nullptr) {
head = newNode;
return;
}
Node* temp = head;
while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = newNode;
newNode->prev = temp;
}
// 从头部打印链表
void printList() {
Node* temp = head;
while (temp != nullptr) {
cout << temp->data << " <-> ";
temp = temp->next;
}
cout << "NULL" << endl;
}
// 从尾部打印链表(反向)
void printReverse() {
Node* temp = head;
if (temp == nullptr) return;
// 找到尾节点
while (temp->next != nullptr) {
temp = temp->next;
}
// 从尾节点向头打印
while (temp != nullptr) {
cout << temp->data << " <-> ";
temp = temp->prev;
}
cout << "NULL" << endl;
}
// 删除指定值的节点
void deleteNode(int key) {
Node* temp = head;
// 找到要删除的节点
while (temp != nullptr && temp->data != key) {
temp = temp->next;
}
// 未找到节点
if (temp == nullptr) {
cout << "Node with value " << key << " not found." << endl;
return;
}
// 更新前后节点的指针
if (temp->prev != nullptr) {
temp->prev->next = temp->next;
} else { // 删除头节点
head = temp->next;
}
if (temp->next != nullptr) {
temp->next->prev = temp->prev;
}
delete temp;
}
// 析构函数:释放所有节点
~DoublyLinkedList() {
Node* temp;
while (head != nullptr) {
temp = head;
head = head->next;
delete temp;
}
}
};
双向链表完整示例
int main() {
DoublyLinkedList list;
// 添加节点
list.append(10);
list.append(20);
list.append(30);
// 正序打印链表
cout << "Doubly Linked List: ";
list.printList();
// 反序打印链表
cout << "Reverse List: ";
list.printReverse();
// 删除节点
list.deleteNode(20);
cout << "After Deletion: ";
list.printList();
return 0;
}
总结
单向链表优点
- 结构简单,占用内存少。
- 适合只需要从头到尾遍历的场景。
双向链表优点
- 支持从头部和尾部两个方向遍历,操作更加灵活。
- 适合需要频繁插入、删除以及双向遍历的场景。
链表的实现中要特别注意内存管理,防止内存泄漏。在 C++ 中,可以使用智能指针(如 std::unique_ptr
或 std::shared_ptr
)来简化内存管理工作。
大程序结构¶
将不同功能(函数)放到很多个.c文件中
文件结构与内容:
main.c,其中 #include"func.h"
func.h,其中写func函数的原型声明
- 作用:“合同”,里面是func的原型
func.c,其中 #include"func.h"
,下面写函数的body
头文件¶
示例:
// hello.h
#ifndef HELLO_H
#define HELLO_H
#include <stdio.h>
extern void greet(const char *name);
#endif // HELLO_H
// hello.c
#include "hello.h"
void greet(const char *name)
{
printf("Hello, %s!\n", name);
}
// main.c
#include "hello.h"
int main(){
greet("各位大佬!");
}
知识
把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原刑
#include
:编译预处理指令,和宏一样,在编译之前就处理了
它把include后面那个文件的全部文本内容 原封不动地插入到include语句所在的地方,所以也不是一定要在.c文件的最前面#include
注意: 在定义和使用这个函数的地方都要 include"func.h"
;一般情况:任何.c都有同名的.h,把所有对外公开的函数原型和全局变量的声明都放进去。
补充:不对外公开:加 static
函数前面加它代表只有他在的这个.c文件(编译单元)可以用它,其他不行;全局变量前面加它代表他只是这个.c文件(编译单元)中可以使用的全局变量。
语法:
-
""
or<>
-
""
先在当前目录下找这个文件,找不到再去别的目录下找,一般用于自己写的 -
<>
不会在当前目录下找,一般用于系统的标准库头文件(在/usr/include目录下,另外有c++目录里面是cpp的头文件)
命令行可以
more stdio.h
一点点看,code stdio.h
的效果和ctrl + click
相同hhh -
-
标准头文件结构
理解:include不是在引入库,只是文本替换
声明¶
对于在一个.c文件(不是main.c,假设是func.c)定义的全局变量,要想在main.c访问它,需要在对应的func.h声明(声明变量);
语法:
func.h: extern int VARIBLENAME
func.c:int VARIBLENAME = 12
(在全局变量的位置,最外面)
文件¶
格式化输入输出¶
printf : %[flags][width][.prec][hlL]type
部分 | 说明 | 示例代码 | 输出 |
---|---|---|---|
% | 格式说明符的起始标志。 | printf("Value: %d\n", 42); | Value: 42 |
[flags] | 可选标志,用于修改输出格式: | ||
- - | 左对齐输出(默认右对齐)。 | printf("\|%-10d\|\n", 42); | \|42 \| |
- + | 输出符号(正数带+ ,负数带- )。 | printf("%+d\n", 42); printf("%+d\n", -42); | +42 -42 |
- 空格 | 正数前加空格,负数前加- 。 | printf("\|% d\|\n", 42); printf("\|% d\|\n", -42); | \| 42\| \|-42\| |
- 0 | 用0 填充宽度(在数字前有效)。 | printf("%05d\n", 42); | 00042 |
- # | 启用格式依赖的功能(例如八进制/十六进制前缀)。 | printf("%#x\n", 42); printf("%#o\n", 42); | 0x2a 052 |
[width] | 最小输出宽度(整数)。输出不足时用空格填充,超过时无影响。 | printf("\|%10d\|\n", 42); printf("\|%3d\|\n", 12345); | \| 42\| \|12345\| |
- * | 下一个参数是字符占位数(那个参数替换* ) | printf("%0*d\n", 7, 42); | 0000042 |
[.prec] | 精度控制: | ||
- . | 指定浮点数的小数位数或字符串最大字符数。 | printf("%.2f\n", 3.14159); printf("%.3s\n", "Hello"); | 3.14 Hel |
- 整数部分 | 对整数无影响(不推荐使用)。 | printf("%.5d\n", 42); | 00042 |
- .* | 下一个参数是小数点后的位数 | printf("%*lf\n", 5, 0.5); | 0.50000 |
[hIL] | 长度修饰符:控制输入数据的大小。 | ||
- h | 短整数类型(short )。 | short s = 42; printf("%hd\n", s); | 42 |
- l | 长整数类型(long )。 | long l = 123456789; printf("%ld\n", l); | 123456789 |
- ll | 长长整数类型(long long )。 | long long ll = 123456789012345; printf("%lld\n", ll); | 123456789012345 |
- L | 长浮点类型(long double )。 | long double ld = 3.141592653589; printf("%Lf\n", ld); | 3.141593 |
type | 数据类型,定义如何解释数据并输出: | ||
- d /i | 带符号整数(十进制)。 | printf("%d\n", 42); printf("%d\n", -42); | 42 -42 |
- u | 无符号整数(十进制)。 | printf("%u\n", 42); | 42 |
- f | 浮点数(小数形式)。 | printf("%.2f\n", 3.14159); | 3.14 |
- x /X | 无符号整数(十六进制)。 | printf("%x\n", 42); printf("%X\n", 42); | 2a 2A |
- o | 无符号整数(八进制)。 | printf("%o\n", 42); | 52 |
- c | 单个字符。 | `printf("%c\n", 'A'); | |
- s | 字符串。 | printf("%s\n", "Hello"); | Hello |
- p | 指针地址。 | printf("%p\n", (void*)&main); | 类似于 0x7ffc... |
- % | 输出百分号本身。 | printf("100%% Complete\n"); | 100% Complete |
- e | 科学计数法表示的浮点数(小写)。 | printf("%e\n", 12345.6789); | 1.234568e+04 |
- E | 科学计数法表示的浮点数(大写)。 | printf("%E\n", 12345.6789); | 1.234568E+04 |
- n | 将到目前为止读入/写出的字符数存储到指定的变量中。 | int n; printf("Hello%n\n", &n); printf("Chars: %d\n", n); | Hello 和 Chars: 5 |
- a | 十六进制表示的浮点数(小写),符合 C99 标准。 | printf("%a\n", 123.45); | 0x1.edp+06 |
- A | 十六进制表示的浮点数(大写),符合 C99 标准。 | printf("%A\n", 123.45); | 0X1.EDP+06 |
- G | 根据值自动选择%E 或%f 格式(大写)。 | printf("%G\n", 12345.6789); | 12345.7 |
- g | 根据值自动选择%e 或%f 格式(小写)。 | printf("%g\n", 12345.6789); | 12345.7 |
printf scanf的返回值
- scanf:读入的字符数
- printf:输出的字符数
大型程序中,应该判断每次调用scanf和printf的返回值,从而了解程序运行中是否存在问题。
基本概念¶
在C语言中,文件操作是一种将程序数据持久化到存储设备(如硬盘)中的方式,可以读取和写入数据到文件中。文件操作的核心思想是通过文件指针来访问文件,文件指针是一个连接程序和文件的桥梁。
文件与流¶
-
文件(File)
文件是存储在存储设备上的数据集合,可以是文本文件(如.txt
)或二进制文件(如.bin
)。C语言通过文件操作函数访问文件内容。 -
流(Stream)
流是文件与程序之间数据传输的抽象。通过流,数据可以从程序写入文件,或从文件读取到程序。
文件指针¶
- 类型为
FILE*
的指针,定义在stdio.h
中,用于标识程序与文件之间的连接。
使用:
- 使用
fopen
打开文件并获取指针。 - 操作文件(读/写/定位)。
- 使用
fclose
关闭文件并释放资源。
文件指针会随着在文件中写入内容而移动。
fprintf
、fwrite
等写操作:文件指针会移动到写入数据的末尾。fseek
和ftell
:可以用于手动调整和检查文件指针的位置。
示例代码¶
以下代码演示了文件指针如何随着写入内容而移动:
#include <stdio.h>
int main() {
FILE* fp = fopen("test.txt", "w+"); // 读写模式
if (fp == NULL) {
perror("Failed to open file");
return 1;
}
// 初始文件指针位置
printf("Initial position: %ld\n", ftell(fp)); // 输出 0
// 写入内容
fprintf(fp, "Hello, ");
printf("After first write: %ld\n", ftell(fp)); // 输出 7("Hello, " 有 7 个字符)
// 写入更多内容
fprintf(fp, "world!");
printf("After second write: %ld\n", ftell(fp)); // 输出 13("Hello, world!" 有 13 个字符)
// 关闭文件
fclose(fp);
return 0;
}
输出示例:
文件指针移动规则
-
写入操作后: - 文件指针移动到写入内容的末尾。 - 例如,写入
"Hello"
后,文件指针移动到5
处(字符数为5
)。 -
读操作后: - 文件指针移动到读取内容的末尾。
-
手动移动指针: - 可以通过
fseek
或rewind
函数调整指针位置。例如:
常用函数¶
1. 文件的打开与关闭¶
文件打开:fopen
- 函数原型:
- 功能:打开一个文件并返回一个指向该文件的指针。
- 参数:
filename
:要打开的文件名。mode
:文件打开模式。常用模式有: 在 C 语言中,文件操作模式也可以结合二进制模式进行操作。二进制模式通过在文件模式字符串中添加一个字符'b'
实现,具体格式如"rb"
,"wb"
, 等等。以下是补充二进制相关内容后的完整表格:
模式 | 作用 | 文件存在 | 文件不存在 |
---|---|---|---|
"r" | 打开只读 | 成功打开 | 返回 NULL |
"w" | 打开只写,文件存在则清空原文件内容 | 成功打开 | 创建新文件 |
"a" | 打开追加模式,文件指针位于末尾 | 成功打开 | 创建新文件 |
"r+" | 以读写模式打开文件,不清空文件内容 | 成功打开 | 返回 NULL |
"w+" | 打开读写,文件存在则清空原文件内容 | 成功打开 | 创建新文件 |
"a+" | 以读写模式打开文件,文件指针位于末尾 | 成功打开 | 创建新文件 |
"rb" | 打开二进制文件只读 | 成功打开 | 返回 NULL |
"wb" | 打开二进制文件只写,文件存在则清空原文件内容 | 成功打开 | 创建新文件 |
"ab" | 打开二进制文件追加模式,文件指针位于末尾 | 成功打开 | 创建新文件 |
"r+b" | 打开二进制文件读写,不清空文件内容 | 成功打开 | 返回 NULL |
"w+b" | 打开二进制文件读写,文件存在则清空原文件内容 | 成功打开 | 创建新文件 |
"a+b" | 打开二进制文件读写,文件指针位于末尾 | 成功打开 | 创建新文件 |
"wx" | 只创建新文件写入模式,文件已存在则返回 NULL | 返回 NULL | 创建新文件 |
"wbx" | 只创建新二进制文件写入模式,文件已存在则返回 NULL | 返回 NULL | 创建新文件 |
- 返回值:成功时返回文件指针,失败时返回
NULL
。
文件关闭:fclose
- 函数原型:
- 功能:关闭文件,释放相关资源。
- 参数:
stream
:文件指针。
- 返回值:成功时返回
0
,失败时返回EOF
。
2. 文件读取操作¶
文本文件读取¶
fgetc
- 函数原型:
- 功能:从文件中读取一个字符。
- 参数:
stream
:文件指针。
- 返回值:返回读取的字符,如果到达文件末尾,返回
EOF
。
fgets
- 函数原型:
- 功能:从文件中读取一行字符(包括空格)并存入
str
中,最多读取n-1
个字符。 - 参数:
str
:存储读取内容的缓冲区。n
:缓冲区的大小。stream
:文件指针。
- 返回值:返回
str
,如果读取到文件末尾返回NULL
。
二进制文件读取¶
fread
- 函数原型:
- 功能:从文件中读取二进制数据,存入内存中。
- 参数:
ptr
:数据存储缓冲区。size
:每个元素的大小(字节)。count
:要读取的元素个数。stream
:文件指针。
- 返回值:成功读取的元素个数。
3. 文件写入操作¶
文本文件写入¶
fputc
- 函数原型:
- 功能:向文件写入一个字符。
- 参数:
c
:要写入的字符。stream
:文件指针。
- 返回值:成功返回写入的字符,失败返回
EOF
。
fputs
- 函数原型:
- 功能:向文件写入一个字符串(不包含
\0
结束符)。 - 参数:
str
:要写入的字符串。stream
:文件指针。
- 返回值:成功返回非负值,失败返回
EOF
。
二进制文件写入¶
fwrite
- 函数原型:
- 功能:向文件写入二进制数据。
- 参数:
ptr
:数据源(内存中的数据)。size
:每个元素的大小(字节)。count
:要写入的元素个数。stream
:文件指针。
- 返回值:成功写入的元素个数。
4. 文件定位¶
fseek
- 函数原型:
- 功能:将文件指针移动到指定位置。
- 参数:
stream
:文件指针。offset
:偏移量,单位为字节。可以是正值、负值或零。whence
:偏移基准位置。常用值有:SEEK_SET
:文件开头。SEEK_CUR
:当前位置。SEEK_END
:文件末尾。
- 返回值:成功时返回
0
,失败时返回非零值。
ftell
- 函数原型:
- 功能:返回当前文件指针的偏移量(从文件开头开始计)。
- 参数:
stream
:文件指针。
- 返回值:返回当前文件指针的位置(以字节为单位),失败时返回
-1L
。
rewind
- 函数原型:
- 功能:将文件指针重置到文件开头。
- 参数:
stream
:文件指针。
- 返回值:无返回值。
5. 错误处理¶
feof
- 函数原型:
- 功能:判断是否到达文件末尾。
- 参数:
stream
:文件指针。
- 返回值:非零值表示文件结束,返回
0
表示未到达文件末尾。
ferror
- 函数原型:
- 功能:检查文件是否发生错误。
- 参数:
stream
:文件指针。
- 返回值:非零值表示发生错误,返回
0
表示没有错误。
6. 文件清空与创建¶
freopen
- 函数原型:
- 功能:关闭当前文件并重新打开文件。
- 参数:
filename
:文件名。mode
:打开模式(如"r"
,"w"
,"a"
等)。stream
:当前打开的文件指针。
- 返回值:成功返回新的文件指针,失败返回
NULL
。
7. 文件缓冲与同步¶
setbuf
- 函数原型:
- 功能:为文件流设置缓冲区。
- 参数:
stream
:文件指针。buffer
:缓冲区指针。若为NULL
,则禁用缓冲。
- 返回值:无返回值。
setvbuf
- 函数原型:
- 功能:为文件流设置缓冲区,支持不同的缓冲模式。
- 参数:
stream
:文件指针。buffer
:缓冲区指针。mode
:缓冲模式,通常为_IOFBF
(全缓冲)、_IONBF
(无缓冲)或_IOLBF
(行缓冲)。size
:缓冲区大小。
- 返回值:成功返回
0
,失败返回非零值。
fflush
- 函数原型:
- 功能:强制刷新文件缓冲区,将缓冲区中的内容写入文件。
- 参数:
stream
:文件指针。如果为NULL
,则刷新所有打开的输出流。
- 返回值:成功返回
0
,失败返回EOF
。
8. 临时文件处理¶
tmpfile
- 函数原型:
NULL
。 缓冲区¶
在 C 语言中,文件缓冲区(Buffer)是用来临时存储数据的一块内存区域,用于优化文件的输入和输出操作。缓冲区的引入可以减少实际文件操作的次数,从而提高程序的效率。
输入内容怎样到达输出?
用户输入 → 输入缓冲区 → 程序处理 → 输出缓冲区 → 输出设备
- 用户输入
当程序需要从用户获取输入时(例如通过键盘),数据首先由用户输入到操作系统的输入设备(如键盘缓冲区)。操作系统会将这些输入数据暂存,等待程序读取。
- 标准输入(stdin)
C语言通过标准输入流(stdin
)读取用户输入。stdin
是一个指向标准输入设备的文件指针,通常与键盘输入关联。常用的输入函数包括:
scanf()
:读取格式化输入。getchar()
:读取单个字符。fgets()
:读取一行字符串。
- 输入缓冲区
- 当用户输入数据并按下回车键时,数据会被送入输入缓冲区。 - 输入缓冲区是内存中的一块区域,用于临时存储用户输入的数据。 - 程序从输入缓冲区中读取数据,而不是直接从键盘读取。
- 程序处理
程序通过标准库函数(如scanf()
或getchar()
)从输入缓冲区中读取数据,并将其存储到程序的变量或内存中。程序可以对数据进行处理、计算或转换。
-
输出缓冲区
当程序需要输出数据时(例如通过printf()
),数据首先被写入输出缓冲区。输出缓冲区是内存中的一块区域,用于临时存储待输出的数据。 -
输出缓冲区的类型
- 行缓冲:当遇到换行符(\n
)或缓冲区满时,数据会被刷新到输出设备(如屏幕)。 - 全缓冲:当缓冲区满时,数据才会被刷新(通常用于文件输出)。 - 无缓冲:数据立即输出,不经过缓冲区(通常用于标准错误输出stderr
)。
- 标准输出(stdout)
C语言通过标准输出流(stdout
)将数据输出到标准输出设备(通常是屏幕)。常用的输出函数包括:
printf()
:格式化输出。putchar()
:输出单个字符。puts()
:输出字符串。
- 输出缓冲区的刷新
- 当输出缓冲区满、遇到换行符(\n
)或调用fflush(stdout)
时,缓冲区中的数据会被刷新到输出设备。 - 如果程序正常结束,所有未刷新的缓冲区数据也会被自动刷新。
- 操作系统和硬件设备
- 操作系统负责管理输入输出设备(如键盘、屏幕等)。 - 当数据从输出缓冲区刷新时,操作系统会将数据传递给硬件设备(如屏幕),最终显示给用户。
缓冲区的基本概念
- 缓冲的作用:
- 文件操作(如读写)通常涉及磁盘 I/O,这是一种相对慢的操作。
- 为了减少磁盘 I/O 的频率,C 标准库在内存中分配了一块缓冲区。
- 程序在读取或写入文件时,先将数据存储到缓冲区,达到一定量后再进行磁盘 I/O 操作。
- 缓冲区的分类:
- 全缓冲(Fully Buffered):当缓冲区填满时才进行实际的 I/O 操作。适用于文件。
- 行缓冲(Line Buffered):每次遇到换行符或缓冲区满时,进行一次 I/O 操作。适用于标准输入输出(如
stdin
、stdout
)。 - 无缓冲(Unbuffered):不使用缓冲区,每次 I/O 操作都直接访问设备或文件。适用于
stderr
。
缓冲区在文件中的应用
- 文件操作函数如
fopen()
、fwrite()
、fread()
、fprintf()
、fgets()
等都会使用缓冲区。 - 在写入时:
- 数据先写入缓冲区。
- 当缓冲区满、手动刷新缓冲区(
fflush()
)、或文件关闭时,数据被写入磁盘。 - 在读取时:
- 文件数据被读取到缓冲区,程序再从缓冲区中获取数据。
常用函数与缓冲区相关操作
fflush()
fflush()
用于清空缓冲区,将缓冲区中的数据立即写入文件或设备。 - 参数: -stream
:文件流指针,表示需要刷新的文件。如果传递NULL
,则刷新所有输出流。 - 典型应用: - 在程序中强制写入文件。 - 确保日志或调试信息及时输出。
示例:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
fprintf(file, "Hello, world!");
fflush(file); // 立即将缓冲区内容写入文件
fclose(file);
return 0;
}
setvbuf()
setvbuf()
用于设置文件的缓冲区模式及大小。 - 参数:stream
:文件流指针。buffer
:自定义缓冲区(传递NULL
表示使用默认缓冲区)。mode
:缓冲区模式,可选值:_IOFBF
:全缓冲模式。_IOLBF
:行缓冲模式。_IONBF
:无缓冲模式。
size
:缓冲区大小(以字节为单位)。- 返回值:
- 成功返回 0,失败返回非零值。
示例:设置自定义缓冲区
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
char buffer[1024]; // 自定义缓冲区
setvbuf(file, buffer, _IOFBF, sizeof(buffer)); // 设置全缓冲模式
fprintf(file, "Buffered data.");
fclose(file); // 文件关闭时,缓冲区中的数据会写入文件
return 0;
}
setbuf()
setbuf()
是setvbuf()
的简单版本,用于设置文件流为全缓冲或无缓冲。 - 参数:stream
:文件流指针。buffer
:缓冲区指针。如果为NULL
,文件流设置为无缓冲模式。
示例:设置无缓冲模式
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
setbuf(file, NULL); // 设置文件流为无缓冲模式
fprintf(file, "This will be written immediately.");
fclose(file);
return 0;
}
缓冲区的默认行为
- 标准流的默认缓冲模式:
stdout
:行缓冲模式。stdin
:行缓冲模式。stderr
:无缓冲模式。
- 文件流的默认缓冲模式:
- 通常是全缓冲模式,缓冲区大小一般由操作系统决定(通常为 4KB 或 8KB)。
缓冲区可能引发的问题
- 未及时刷新缓冲区:
- 如果程序异常退出,缓冲区中的数据可能未被写入文件,导致数据丢失。
- 并发操作冲突:
- 多线程或多进程同时操作同一文件时,缓冲区可能引发数据竞争,需要特别注意同步操作。
总结
- 缓冲区的作用:提高文件 I/O 的效率,通过减少磁盘读写次数来优化性能。
- 关键操作:
- 使用
fflush()
手动刷新缓冲区。 - 使用
setvbuf()
或setbuf()
自定义缓冲区行为。
- 使用
- 文件默认缓冲模式:文件通常是全缓冲,标准流根据类型可能是行缓冲或无缓冲。
文件的输入输出¶
方法一:shell的重定向
>
将输出定向值后面的文件<
从后面的文件中读取输入
方法二:FILE
int main()
{
FILE* fp = fopen("try.c", "r");
if(fp){
fscanf(fp, "%d", &num);
printf("%d\n", num);
fclose(fp);
}
else{
printf("文件打开失败");
}
}
fopen
: FILE *fopen(const char *__restrict__ __filename, const char *__restrict__ __modes)
fclose
: int fclose(FILE *__stream)
- Close STREAM.
对文本文件的读和写:
fscanf
:int fscanf(FILE *__restrict__ __stream, const char *__restrict__ __format, ...)
- Read formatted input from STREAM.
- 只是在
scanf
前面加一个FILE*的指针,其他都一样。
fprintf
:int fprintf(FILE *__restrict__ __stream, const char *__restrict__ __format, ...)
- Write formatted output to STREAM.
对二进制文件的读和写
-
fread
:size_t fread(void *__restrict__ __ptr, size_t __size, size_t __n, FILE *__restrict__ __stream)
- Read chunks of generic data from STREAM.
-
fwrite
:size_t fwrite(const void *__restrict__ __ptr, size_t __size, size_t __n, FILE *__restrict__ __s)
- Write chunks of generic data to STREAM.
-
FILE指针是最后一个参数
-
返回值是成功读写的字节数
-
对二进制文本的读写一般是通过对一个结构变量的操作进行的,因此,
__size
代表一个结构的大小;__n
代表读写几个结构变量
在文件中定位¶
ftell
: long ftell(FILE *__stream)
- Return the current position of STREAM.
fseek
: int fseek(FILE *__stream, long __off, int __whence)
- Seek to a certain position on STREAM. - 用来调整文件指针的位置。它适用于文本文件和二进制文件。通过 fseek,可以在文件中快速定位到某个特定的位置,以便后续读取或写入。 - 第三个参数:基准量:从哪里开始的第一步确定初始位置 - SEEK_SET
:从头开始 - SEEK_CUR
:从当前位置开始 - SEEK_END
:从尾开始(倒过来) - 第二个参数:偏移量:在第三个参数的基础上移动几个字节作为开始的seek的地方
未解决可移植性的问题:用文本
处理数据的方式:数据库 / 第三方库 (用C底层的文件/数据操作方式的少了)
MISC¶
标签与goto¶
label(标签) 是一种标识符,用于标记一个特定的代码位置,通常与 goto
语句配合使用。标签是定义在函数内部的局部标识符,作用范围仅限于所在的函数。
标签的定义语法
label_name
是标签的名字,必须是一个合法的标识符。- 冒号
:
表示这是一个标签。
标签的作用:标签通常与 goto
语句一起使用,实现程序流程的无条件跳转。
示例
#include <stdio.h>
int main() {
int x = 1;
start: // 标签 start
printf("x = %d\n", x);
x++;
if (x <= 5) {
goto start; // 跳转到标签 start
}
return 0;
}
运行结果:
在上面的代码中,goto start
语句使程序跳转回 start
标签所在的位置,形成了一个循环结构。
标签的特性
- 局部性: 标签只能在定义它的函数内使用。
- 唯一性: 在一个函数内,标签名必须唯一。
- 与
goto
配合: 通常通过goto
来跳转到标签,但仅仅定义标签并不会改变程序的执行顺序。
使用场景
虽然标签和 goto
提供了无条件跳转功能,但它们的使用会影响程序的可读性,容易导致“spaghetti code(意大利面条代码)”,因此应尽量避免使用。
标签和 goto
通常在以下特殊场景中有用:
-
错误处理: 当函数中出现复杂的嵌套逻辑时,可以使用标签和
goto
实现统一的错误处理。 -
跳出多重嵌套: 标签可以直接跳出多层嵌套,而不需要复杂的条件判断。
注意事项 1. 尽量避免滥用 goto
和标签,尤其是可以通过结构化控制语句(如 for
、while
、break
、continue
等)实现的逻辑,不要用 goto
。 2. 标签和 goto
的过度使用会使代码难以阅读、难以维护,因此应谨慎使用。
替代方案 大多数情况下,可以通过函数调用、循环、条件语句(如 if
)和异常处理机制代替标签和 goto
。