- struct、union、enum
- 编程范式
- 机器/汇编/高级语言
- 进制表示
- 值类别与运算符优先级
- 格式化输出与 ASCII
- 强制类型转换
- typdef
- printf("%*c") 的用法
- C++
std::string与sizeof - 循环与退出
- 构造数据类型
- 面向对象/面向过程
- 关于数组大小和 '\0' 终止符
- 二维数组与行指针
- EOF
- switch
- 整体概念:编译与链接
- inline 作用机制
- 存储类别与内存区域
- 函数调用的开销
- 整形实型
- 页表
- 变量 vs 实例
- 结构体对齐与 sizeof
- HLD / HAL / HLE 概览
- 文件存储:字符文件 与 二进制文件
什么是命令式编程,什么是函数式编程?
命令式编程时大部分指令都已写好,我可以去按部就班的编译。有函数,那也只是不同参数的传递。template<\class>看似是传递,但你后续typeT怎样的都已经写好了。
而函数式编程是另一个层面,函数是第一等公民。
比如我有一个鼠标操控电脑,其实 alt+tab 切换 app 后,鼠标对应的操作完全不同,可以是笔可以是准心。对于左键右键都是不一样的。这个时候其实就是把整个鼠标函数作为主体在各个 app 中去传递,app 怎么用完全就不管了。
注:C++ 模板应写作 template<class T>(原文中的 template<\class\> 为笔误)。
(1) 机器语言
计算机的指令系统称为机器语言,所有的计算机都只能直接执行用机器语言编写的程序。机器语言与计算机的硬件密切相关,机器语言中的计算机指令用二进制形式的代码表示,由若干位 1 和 0 组成。通常,一条计算机指令只能指示计算机完成一个最基本的操作。例如,将某个地址中的内容读入某个寄存器,某寄存器的内容加上另一寄存器的内容,将某寄存器的内容存入某地址等。
(2) 汇编语言
由于计算机的机器语言很难被人理解和阅读,因此人们用类似英语单词缩写的符号指令代替机器语言的二进制代码指令。汇编语言就是用有助于记忆的符号表示计算机机器指令的程序设计语言。例如,取数指令“LD GR0,X”表示从对应变量 X 的内存中取数到寄存器 GR0。加指令“ADD GR0,GR1”将寄存器 GR1 中的内容与寄存器 GR0 中的内容相加,并把结果存于寄存器 GR0 中。存数指令“ST GR0,X”将寄存器 GR0 中的内容存入与变量 X 对应的内存中。
用汇编语言编写的程序要在计算机上执行,应先将用汇编语言编写的程序(称为源程序)转换成机器语言程序,完成这个转换功能的程序称为“汇编器”或“汇编程序”。
(引自《C语言程序设计(第三版)》)
(3) 高级语言
高级语言主要由语句构成,有一定书写规则,程序员用语句表达要计算机完成的操作。与汇编语言相比,高级语言有统一的语法,独立于具体机器,便于人们编码、阅读和理解。
用高级语言编写的源程序要在计算机上执行,也要先将源程序转换成机器语言程序。把用高级语言编写的源程序转换成机器语言程序的翻译程序称为“编译器”或“编译程序”。高级语言是一种既能方便地描述客观对象,又能借助于编译器为计算机所接受、理解和执行的人工语言。例如,用于科学计算的 FORTRAN 语言,早期非常普及的 BASIC 语言,第一个用严格的文法描述的 ALGOL60 语言,便于编写结构化程序的 Pascal 语言以及本书讲述的 C 语言都是高级语言。
问题 6:什么是面向过程语言?
高级语言又可分为面向过程语言和面向问题语言两类。面向过程语言虽然可以独立于计算机编写程序,但用这类语言编写程序时,程序要非常详细地告诉计算机如何做,程序需要详细描述解题的过程和细节。C 语言就是一种面向过程的语言。例如,在某个职工数据文件中查找工号为 22650 的职工,面向过程语言需要详细描述查找过程。以下算法是适应面向过程语言的一个查找过程的描述。
- 打开职工文件。
- 当文件未结束时重复执行以下工作: a. 读取文件的当前记录。 b. 如果当前记录中的工号是 22650,则结束步骤 2。
- 关闭职工文件。
- 如果找到,则返回找到的职工信息,否则返回找不到的标志信息。
其中,步骤 2 是一个循环控制结构,控制记录一个一个地读取和比较。
问题 7:什么是面向问题语言?
面向问题语言通常是指在特定应用领域中使用的高级语言。人们使用面向问题语言时,不要详细给出问题的求解算法和求解过程,只须指出问题做什么、数据输入和输出形式,就能得到所需的计算结果。实际上,计算机是根据预先的规定,执行先前准备好的程序,回答问题的结果。面向问题语言又称非过程化语言或陈述性语言,如报表语言、SQL(Structured Query Language)等。SQL 是数据库查询语言,在数据库管理系统的支持下,用 SQL 提出的查询或操纵要求,就能由数据库管理系统完成。使用面向问题语言解题时,只要告诉计算机做什么,不必告诉计算机如何做,能方便用户使用和提高程序的开发速度。但实现面向问题语言的系统从最一般的意义下实现问题如何做,通常实现的效率较低。另外,面向问题语言要求问题已有确定的求解方法,目前其应用范围还比较狭窄。
如果用面向问题语言 SQL 描述问题 6 中所述的查找要求,只需要用一条能表达以下含义的简单命令:从职工数据文件选取信息,条件是工号等于 22650。数据文件打开、用循环控制结构描述记录逐个地读取和比较,以及数据文件使用结束的关闭等细节都不再详述。
问题 8:什么是面向对象语言?
为克服面向过程语言过分强调求解过程的细节、程序不易复用等缺点,推出了面向对象程序设计方法和面向对象程序设计语言。面向对象语言引入了对象、消息、类、继承、封装、抽象、多态性等概念和机制。用面向对象语言进行程序设计时,以问题域中的对象为基础,将具有类似性质的对象抽象成类,并利用继承机制,仅对差异进行程序设计。对于大型程序,面向对象语言能提高程序的开发效率、提高程序的可靠性及可维护性等。
注:面向对象与“解释型”并无必然关系;如 C++/Java(JIT 亦可编译)等均可编译执行。
| 进制 | 前缀 | 示例 | 对应的十进制值 |
|---|---|---|---|
| 二进制 | 0b 或 0B | 0b1010 | 10 |
| 八进制 | 0 | 012 | 10 |
| 十进制 | 无 | 10 | 10 |
| 十六进制 | 0x 或 0X | 0xA | 10 |
左值:特定的有地址的变量
右值:右值通常是“临时”的、没有名字的、即将被销毁的值,我们不能对它取地址。
- 后缀递增与后缀递减属于第 2 组优先级,从左到右关联。
- 前缀递增与前缀递减属于第 3 组优先级,从右到左关联。
* / %属于第 5 组优先级;+ -属于第 6 组优先级,皆为从左到右关联。
示例:
a*a++ // 先执行(a++)语句返回 4,但将 a 左值递增至 5;再执行 *,得到 5*4=20
c=++a+a // 先执行(++a)语句,a 左值递增至 6,并返回 6,再执行 +,得到 6+6=12
注:在 C/C++ 中,a*a++、++a + a 等表达式存在未序列化的读写,属于未定义行为(结果不可靠)。请避免编写此类代码。
printf("%c对应的ASCII码为%d\n", ch, ch);// %d 对应数字,%c 输出对应字符;%c、%d 类似于占位符,提前说明这里如何输出并从后找。
float n, m;
int _n, _m;
_n = (int)n; _m = (int)m;显式类型转化 (int)(x)。
隐式类型转化
- 整数提升(integer promotion)会把小整型提升为 int 或 unsigned int。
- 不同精度的浮点/整数参与运算时,低精度向高精度转换。
对隐式类型转换规则进一步说明:
- 运算时,int类型最低,long double类型最高。
- short型和char型的运算分量必须先转换为int型才能运算。
- 两个相同类型的数据(除short型和char型)可以直接运算,不需要类型转换。 当两个不同类型的数据运算时,由系统自动转换。例如,一个int型数据与一个 double型数 据运算,先要将int型数据转换为double型,才能与另一个double型数据运算,运算结果也是int
int
计算机的进制主要包括:二进制(binary)、八进制(octal)、十进制(decimal)和十六进制(hexadecimal)。
printf(" 十进制 %d 对应的八进制数是:%o\n", x, x);
printf(" 十进制 %d 对应的十六进制数是:%x\n", x, x);%+5d右对齐输出 5 个字符宽度,不足前补空格(当 5 改成 2 时,仍然会输出正常的 168)。%-5d左对齐输出 5 个字符宽度,不足后补空格。%+6.2f输出 6 个字符宽度(其中 2 位小数),注意正负号占宽度,当 6 改为 1 后也会正常输出整数部分。%E输出科学计数法。
注:
scanf("%*d");注:%*d 表示读取但丢弃该整数,不应再传入参数(原文中 scanf("%*d", &k) 为错误用法)。
int a = 168;
float b = 123.456;
float x = 35.567, y;
%5d 右对齐输出 5 个字符宽度,不足前补空格(当 5 改成 2 时,仍然会输出正常的 168)
%-5d 左对齐输出 5 个字符宽度,不足后补空格
%+6.2f 输出 6 个字符宽度(其中 2 位小数)
%E 输出科学计数法
#define是预处理指令,在编译前的预处理阶段进行简单文本替换。typedef是类型别名声明,在编译阶段为类型起别名,不做文本替换。
#define的替换发生在预处理阶段,即编译器还未分析语法时,直接用文本替换。 (所以宏名不占用运行时间)typedef的作用发生在编译阶段,编译器已知类型信息。
#define只是简单的字符串替换,不检查类型,容易出错。typedef只用于类型,能被编译器识别和检查。
#define INT_PTR int*
typedef int ARRAY[100];> 英文思路:#define is a preprocessor macro for text replacement, typedef is a type alias at compile time. #define is error-prone for complex types, typedef is safer and type-aware.
---
## struct、union、enum
### 1. struct(结构体)
- 用于将不同类型的数据组合成一个整体。
- 每个成员有独立的内存空间,大小为所有成员之和(含对齐)。
```c
struct Point {
int x;
int y;
};
- 所有成员共用同一块内存,大小为最大成员的大小。
- 任何时刻只能存储一个成员的值。
union Data {
int i;
float f;
char c;
};- 用于定义一组具名整型常量,默认从 0 开始递增。
enum Color {
RED,
GREEN,
BLUE
};- struct:组合多种类型,各成员独立。
- union:节省空间,多成员共用一块内存。
- enum:定义一组相关常量,增强代码可读性。
英文思路:struct combines different types, union shares memory for different types, enum defines named integer constants.
printf(" a = %5d\n", a);
printf(" a = %-5d\n", a);
printf(" b = %6.2f\n", b); // %.2f 形式控制小数精度
printf(" b = %E\n", b);
y = (int)(x*100 + 0.5) / 100.0;
printf(" y = %f\n", y);int x; double y;
x = y = 3.5;最终 x=3, y=3.5(自动截断转换)。
if (语句1 && 语句2)// 如果语句 1 为假,那么语句 2 不会执行。
逗号运算:
int y = 7;
float z = 4;
x = (y = y + 6, y / z);
// 结果 x = 3(按最后的子表达式结果赋值)运算过程中:
char -> intfloat -> double
运算对象不同类时,short
#include <stdio.h>
int main() {
int i; char c; long k; float f; double x;
// i=2,c='B',k=123456,f=5.8,x=3.4;
/*
input:
i=2 c=B
k=123456
f=5.8 x=3.4
*/
scanf("i=%d c=%c", &i, &c);
getchar();
scanf("k=%ld", &k);
getchar();
scanf("f=%f x=%lf", &f, &x);
printf("i=%d c=%c k=%ld f=%f x=%lf", i, c, k, f, x);
return 0;
}scanf 为格式化输入,会按照双引号内的格式去匹配输入的字符串。它不在意
"i=%d c=%c"间的空格数量,但很在意其他字符匹配,以及两个 scanf 间是否有空格。从本质上讲,我们从键盘输入的数据并没有直接交给 scanf(),而是放入了缓冲区中,直到我们按下回车键(输入换行符),scanf() 才到缓冲区中读取数据。如果缓冲区中的数据符合 scanf() 的要求,那么就读取结束;如果不符合要求,那么就继续等待用户输入,或者干脆读取失败。所以这里要用 getchar() 读掉换行符/空格。
#include <stdio.h>
int main() {
int a = 8, b = 9; float x = 127.895, y = -123.456; char c = 'B'; long n = 12345678L; unsigned u = 65535u;
printf("%f,%f\n", x, y); // 输出 float x y
printf("%-12f,%-12f\n", x, y); // 左对齐 共宽 12
// 由于二进制小数精度问题,所以有误差
// 127.894997 ,-123.456001
printf("%8.3f,%8.3f,%.3f,%.3f,%4f,%5f\n", x, y, x, y, x, y); // 负号占用一格
// 127.895,-123.456,127.895,-123.456,127.894997,-123.456001
printf("%s,%6.3s,%-10.5s\n", "c language", "c language", "c language"); // %s 输出字符串,%6.3s 表示总宽 6,取前 3 个字符
// c language, c l, c lan
return 0;
}基本的,正整数对应右对齐,负整数对应左对齐,数字大小为输出宽度。值得注意的是,小数都是二进制小数表示,所以除非明确精度或者恰好比较好表示,则都会出现“≠”的情况。另外,字符串
.x类似于“取小数”,即取前 x 个字符。
自顶向下,逐步细化;模块化编程;结构化编码。
- 不能互相耦合。
exit app 级的退出。
C++ 数据和它的属性打包了,想怎么写怎么写。C 语言:数据属性/变量要在一开头就全定义完毕(早期?)
// 必须定义在作用域开头:在 C89/C90 标准中,变量必须在代码块(如函数、循环、条件语句)的开头定义。例如:
void example() {
int a = 10; // 正确:定义在函数开头
printf("%d", a);
// 错误:不能在语句后定义新变量
printf("Hello");
int b = 20; // 编译错误(C89/C90)
}
// 历史原因:早期编译器设计简单,变量定义需集中处理内存分配。这种限制也源于 C 语言的“声明在前”传统(如函数原型需在调用前声明)。
注:自 C99 起,C 已允许“声明与语句交错”(可在块内任意处定义变量)。
switch 接口加速?执行列表,列表要跳?
switch (expression) {
case 常量表达式1: [break];
case 常量表达式2: [break];
[default: ;]
}// switch_statement1.cpp
#include <stdio.h>
int main() {
const char *buffer = "Any character stream";
int uppercase_A, lowercase_a, other;
char c;
uppercase_A = lowercase_a = other = 0;
while ( (c = *buffer++) ) { // Walks buffer until NULL
switch ( c ) {
case 'A':
uppercase_A++; // 不 break,跳转,会继续执行下一个 case,包括 default
break;
case 'a':
lowercase_a++;
break;
default:
other++;
}
}
printf("\nUppercase A: %d\nLowercase a: %d\nTotal: %d\n",
uppercase_A, lowercase_a, (uppercase_A + lowercase_a + other));
}注:编译器可能将密集的 switch 分支优化为“跳转表”,从而加速分派。printf_s 为 MSVC 扩展,Linux 上可改用 printf。
简短回答:switch 属于选择(分支/判断)语句,但编译器会把它翻译成不同实现(if/else 链、二叉查找、或跳表),性能取决于 case 的分布和编译器优化,不一定“很慢”。
要点:
switch 是选择语句(判断哪一分支执行)。 编译器实现可能有三种常见策略: if/else 链(稀疏 case,线性比较,O(n)); 二分/查找树(中等稀疏,O(log n)); 跳表(jump table,密集连续的整型 case,O(1) 通过索引跳转)。 跳表最快(常数分派),但会增大代码/数据段且需密集索引范围;稀疏 case 则不会用跳表。 真实性能还受分支预测、缓存、编译器优化
这是 C/C++ 中一个特殊的格式化输出方式。printf("%*c", width, character) 中的 %*c 是一个格式说明符,它允许你通过参数动态指定字段宽度。
说明:
%*c:*表示字段宽度由参数指定,c表示输出一个字符。- 第一个参数是宽度值(整数),第二个参数是要输出的字符。
示例代码:
#include <stdio.h>
int main() {
int j;
scanf("%d", &j);
for (int i = 0; i < 40; ++i) printf("%d", i);
printf("\n");
// 输出字符 'A',并指定字段宽度为 10
printf("%*c\n", 10, 'A');
// 输出字符 'B',并指定字段宽度为 5
printf("%*c\n", 5, 'B');
// 输出字符 'X',并指定字段宽度为 1(实际上就是普通输出)
printf("%*c\n", 1, 'X');
printf("j=%5d\n", j);
/*
%*c 格式说明符:
%c 是输出单个字符的格式说明符
* 表示字段宽度由参数指定
所以 %*c 的含义是:输出一个字符,但该字符占据的宽度由参数指定
所以之后会增加一个参数
*/
printf("%*c", j, ' ');
printf("end");
return 0;
}让我来解释一下你提出的两个问题:
sizeof(s) 返回的是 std::string 对象本身的大小,而不是字符串内容的长度。在大多数现代系统上,std::string 对象内部通常包含以下成员:
- 指向实际字符串数据的指针(在 64 位系统上通常占 8 字节)
- 字符串长度信息
- 字符串容量信息
所以 sizeof(s) 返回的是这个 string 对象结构体的大小,通常是 8 字节(在 64 位系统上)或 4 字节(在 32 位系统上),与实际存储的字符串内容长度无关。
如果你想要获取字符串的实际长度,应该使用 s.length() 或 s.size() 方法:
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
cout << "sizeof(s) = " << sizeof(s) << endl; // string 对象的大小
cout << "s.length() = " << s.length() << endl; // 字符串内容的实际长度
return 0;
}注:std::string 的对象大小完全“依实现而定”。在常见的 GCC/libstdc++ 64 位环境下通常为 24 字节或更多,并非固定为 8 字节。
在格式化字符串 "%0.2f" 中:
0表示用 0 来填充字段宽度(如果不指定宽度则无效果)。.2表示小数点后保留 2 位小数。f表示这是一个浮点数。
所以 %0.2f 的意思是:输出一个浮点数,保留 2 位小数,不足的位数用 0 填充。
但要注意,在你的例子中,格式是 "%0.2f",但没有指定总宽度,所以 0 实际上没有效果。如果你想要看到用 0 填充的效果,需要指定总宽度,比如 "%08.2f":
#include <bits/stdc++.h>
using namespace std;
int main() {
float f = 12.3f;
printf("默认格式: %.2f\n", f); // 输出: 12.30
printf("0填充格式: %08.2f\n", f); // 输出: 00012.30
printf("空格填充格式: %8.2f\n", f); // 输出: 12.30 (前面有空格)
return 0;
}在你的代码中,%0.2f 实际上等同于 %.2f,都是保留两位小数的意思。
- for 顶测试
- do while 底测试
exit(0) exit(EXIT_SUCCESS)
数组,指针,结构,联合。
构造(或复合)数据类型是由更基础的标量类型或其他构造类型组合而成的类型,用于表示有结构的复合数据。常见的构造类型包括:
- 数组(array):同类型元素的有序集合,大小通常在编译期确定,内存连续。
- 指针(pointer):保存对象地址,可用于间接访问或表示动态/线性集合的首元素。
- 结构体(struct):把不同类型的成员组合成一个整体,成员按对齐规则布局在内存中。
- 共用体(union):各成员共享同一内存区域,大小为最大成员,适合按不同视角解释同一内存。
初始化与零初始化提示:
- 聚合(数组、结构体、共用体等)可用花括号
{...}进行初始化,编译器按元素顺序填充。未提供的尾部元素会被置 0;空的花括号{}表示对该子聚合不提供初值(等价于将该子聚合的所有成员置 0)。
简短示例:
struct Point { int x, y; };
int arr[3] = {1, 2, 3};
int mat[2][2] = {{1,2},{3,4}}; // 二维数组的嵌套大括号
struct Point p = { .x = 1, .y = 2 };
int b[3][2] = {{1,2},{}, {3,4}}; // 中间的 {} 会使 b[1] 全为 0何时使用:构造类型用于需要表达复合数据(记录、矩阵、缓冲区、对象等)时,选择数组/结构/共用体/指针取决于数据布局、访问模式与内存管理需求。
面向对象是解释性的,可以临时定义变量分配内存。
面向过程,整个函数是一个对象,必须要在开头把变量定义以分配内存。
注:是否“解释”与是否“面向对象”无关;此外,现代 C(C99 起)允许在块内任意位置定义变量。
常量存储空间永远不会被更改。
int i = 1;
printf("%d %d", ++i, --i);是什么东西,执行顺序?看编译器。
注:上述写法对同一标量在一个表达式内多次读写,属于未定义行为;请不要依赖其顺序或结果。
* 与 & 究竟是什么。
你问到两个重要问题:
为什么数组大小没有填?
这是 C/C++ 的特性:当你用字符串字面量初始化字符数组时,编译器会自动计算所需的数组大小,这样做既方便又安全,避免了手动计算可能出现的错误。
最后一个元素会是 '\0' 吗?
是的,当你用双引号括起来的字符串字面量初始化字符数组时,编译器会自动在末尾添加 '\0' 终止符。所以 char str[] = "hello world"; 实际存储的是 {'h','e','l','l','o',' ','w','o','r','l','d','\\0'}。这使得该数组可以被所有期望以 '\0' 结尾的 C 风格字符串函数正确处理,如 strlen(), strcpy() 等。
示例:int (*q)[3] = b;
结论:
- C 的二维数组在内存里是连续的“行优先”存储,但类型信息(每行列数)仍然重要,影响指针运算与索引。
关于 q 和访问方式:
- 正确写法:
int b[N][3]; int (*q)[3] = b;q指向整行,q+i == &b[i],访问用q[i][j](等价b[i][j])。
- “
(*q) = b”不成立:*q的类型是int[3](数组),数组是不可赋值对象;应写q = b。 - 线性访问也可:
int *p = &b[0][0];然后用p[i*3 + j];但p是int*,不能写p[i][j]。
为什么不能省略列数:
- 编译器做
q+1需要知道跳过一整行的大小(3*sizeof(int))。若省略[3]形成不完整类型,就无法进行行级指针运算与q[i][j]。
函数参数的正确姿势:
- 固定列:
void f(int a[][3]) { /* ... */ } // 等价于 void f(int (*a)[3])- 运行期列数(VLA):
void g(int n, int m, int a[n][m]) { /* ... */ } // a 等价于 int (*a)[m]小例:
- 行指针:
int (*q)[3] = b; printf("%d\n", q[1][2]); // b[1][2] - 线性:
int *p = &b[0][0]; printf("%d\n", p[1*3 + 2]); // b[1][2]
当“指向一维数组的指针” p 指向二维数组 a 的首行后,数组元素与地址的等价写法如下(以 short a[][4] 为例):
前提定义:
short a[3][4]; // 3 行 4 列
short (*p)[4] = a; // 行指针:p 指向“含 4 个 short 的一行”元素访问的等价式:
a[i][j]等价于p[i][j]等价于*(*(p + i) + j)等价于*(p[i] + j)
元素地址的等价式:
&a[i][j]等价于&p[i][j]等价于*(p + i) + j等价于p[i] + j
圆括号为何必需:
short (*p)[4]的括号必不可少。若写成short *p[4],语义就变成“指针数组”:p是一个有 4 个元素的数组,每个元素是short*(指向short的指针),完全不同于“指向一行(含 4 列)的指针”。
对比示例:
short a[2][4] = { {1,2,3,4}, {5,6,7,8} };
short (*row)[4] = a; // 行指针:row+i 跳过一整行(4*2字节)
short *col = &a[0][0]; // 线性指针:按单个 short 前进
// 访问 a[1][2]
printf("%d\n", a[1][2]);
printf("%d\n", row[1][2]);
printf("%d\n", *(*(row+1)+2));
printf("%d\n", *(row[1] + 2));
printf("%d\n", *(col + 1*4 + 2));
// 地址等价
printf("%p\n", (void*)&a[1][2]);
printf("%p\n", (void*)&row[1][2]);
printf("%p\n", (void*)(*(row+1) + 2));
printf("%p\n", (void*)(row[1] + 2));要点总结:
T (*p)[M]表示“指向含 M 个T的一维数组”的指针(即一整行)。T *q[M]表示“有 M 个元素的数组,每个元素是指向T的指针”(指针数组)。- 用行指针时,
p+i会跳过一整行的字节数;而线性指针&a[0][0]每次仅按一个元素前进。
EOF endofile
是个字符。
while (getchar() != EOF) {
// 读一个字符,如果读到文件末尾,则返回 EOF
}ctrl+z 结束。
注:EOF 代表“end of file”,在 C 中是一个整型常量宏(通常为 -1),不是字符。终端中触发 EOF:Linux/Unix 为 Ctrl+D,Windows 为 Ctrl+Z。
转向语句是编程语言中直接改变程序执行顺序的结构化指令,主要通过选择结构和循环结构实现流程控制。在 C 等现代编程语言中,if-else 语句通过条件表达式实现程序分支跳转,switch-case 语句配合 break 命令完成多分支转向。循环结构中的 continue 与 break 语句可分别实现循环体内部跳转和循环终止。
转向语句非常的慢。比较符本质还是运算,顺序运行也很快。
注:分支是否“慢”取决于分支预测与局部性,不能一概而论。
线程互相独立,沟通起来很困难。进程。
C/C++ 的构建通常分两步:
- 编译:每个源文件 → 目标文件(.o)
- 链接:把所有目标文件(+ 库)→ 可执行文件(或共享库)
流程:预处理 → 编译成汇编 → 汇编成目标文件。
产物:可重定位的目标文件(.o),包含机器码、符号表、重定位信息。
作用范围:只检查单个翻译单元内的语法/语义(不知道其他文件是否提供缺失函数)。
常用命令:
工作:合并目标文件,解析符号引用(谁定义了 foo、谁需要 foo),执行重定位,生成可执行文件或 .so/.dll。
输入:.o 文件 + 库(.a 静态库,.so 共享库),注意库的搜索顺序与链接次序。
常用命令:
注:链接器决定二进制的静态布局(节、符号、相对地址等);最终装载到内存的实际基址由操作系统/加载器决定,启用 PIE/ASLR 时每次运行都可能不同。
/* add.c */ int add(int a,int b){ return a+b; }
/* add.h */ int add(int a,int b);
/* main.c */ #include "add.h"
int main(){ return add(2,3); }编译期内联:编译器把函数体直接“展开”到调用点,消除函数调用/返回的固定开销(入栈/出栈、保存/恢复寄存器、参数传递)。
跨边界优化:展开后,编译器能在更大的基本块上继续做优化(常量传播、死代码消除、公共子表达式、循环不变代码外提、向量化、分支预测改写),这通常比纯粹省“call”更值钱。
链接期内联(LTO):开启 LTO 时,链接器阶段会用编译器的中间表示做“全程序优化”,可在不同源文件间继续内联与跨模块优化(去死代码、去重复、跨 TU 常量传播、去虚拟化等)。
两阶段提速点:
- 编译阶段(每个源文件内)
- 消除调用开销:去掉 prologue/epilogue、参数栈传递。
- 曝露上下文:能把小函数与调用点合并,启用更激进的局部优化。
- 受限于“翻译单元”:默认只能对同一源文件看见的定义做内联。
- 链接阶段(全程序,需 LTO)
- 跨文件内联:把多个 .o 的 IR 合并后继续内联,突破单 TU 限制。
- 全局裁剪:移除未用函数、合并重复模板/内联体,整体瘦身或重排,提升 i-cache 局部性。
- 更深入的跨模块优化:内联后触发进一步的全程序常量折叠、分支消除。
语义与关键字:
- C++ 的 inline:语义上允许“在多个翻译单元提供相同定义”(ODR 规则),方便把小函数放到头文件;是否真的内联由优化器决定,不保证。
- C 的 inline(C99)更复杂:头文件里推荐用
static inline定义小函数,避免外部链接冲突;extern inline/裸inline各编译器行为有差异,按规范谨慎使用。 - 宏 vs inline:宏是纯文本替换,无类型检查;inline 是函数,类型安全、可调试,优化器也更聪明。
何时更快:
- 很小、调用频繁、在热循环里的函数(如
min/max/访问器/位操作)收益明显。 - 内联能触发后续优化(如把参数是常量时整段代码折叠掉)。
- 与模板/泛型结合的短函数(STL 迭代器/算法)常被充分内联。
何时可能更慢:
- 代码膨胀:大量内联使二进制变大,指令缓存命中率下降,反而变慢。
- 链接/编译时间:开启大量内联/LTO 会增加构建时间与内存占用。
- 大函数内联:收益不及代价,优化器通常会拒绝(除非强制)。
实用建议:
- 写法:头文件里的小函数,C++ 用 inline;C 用 static inline。
- 不强制:除非明确知道要强制,否则别用
always_inline;让优化器决策。 - 编译参数:常规内联
-O2或-O3。跨文件内联(LTO):编译与链接都加-flto。 - 观察内联决策:
-Winline(对被标记 inline 却未内联给出提示),或用-fverbose-asm/objdump -d看汇编。 - 选择性控制:禁止/限制
-fno-inline、-fno-inline-functions-called-once;强制(慎用)__attribute__((always_inline))/[[gnu::always_inline]](需与-O配合)。
一句话总结:
“inline”加速的核心不是“省一次 call”这么简单,而是“把调用点与被调函数融合,让优化器在更大范围上做更强的优化”;常规内联发生在编译期,同一源文件内;配合 LTO 则可在链接期跨文件继续内联与全程序优化。合理使用(小而热的函数 + 合理优化级别 + LTO)通常带来可观收益,但要警惕代码膨胀与构建时间上升。需要的话我可以把你的 VS Code 构建任务加上 -O3 -flto 版本,做一次对比构建和体感测试。
| 变量类型 | 存储类型 | 可见性 | 存在性 | 未赋初值 |
|---|---|---|---|---|
| 局部变量 | auto | 定义的范围内 | 离开定义范围,值消失 | 值不确定 |
| 局部变量 | register | 定义的范围内 | 离开定义范围,值消失 | 值不确定 |
| 局部变量 | static | 定义的范围内 | 离开定义范围,值消失 | 值0 |
| 局部变量 | 形参 | 定义的函数内 | 离开定义函数,值消失 | 值不确定 |
| 全局变量 | static | 本文件内 | 程序运行期间,值有效 | 值为0 |
| 全局变量 | extern | 本文件或其他文件 | 程序运行期间,值有效 | 值为0 |
register:动作极快,但是找不到地址。
Got it. Here’s a concise, practical rundown of C/C++ 的 register 特性和“能用几个”的问题。
是什么:
语义:对编译器的“建议”把变量放入 CPU 寄存器以加速访问(减少内存读写)。现代现实:几乎所有现代编译器都会忽略这个建议,由寄存器分配器根据优化策略自动决定;写与不写通常无差别。
限制与规则:
- 作用域/存储期:与 auto 一样是块作用域、自动存储期(进入块创建,离开块销毁)。
- 取地址:不能对 register 变量取地址(
&x非法),因为寄存器没有内存地址这一抽象。 - 位置:不能用于文件作用域(全局变量),只能用于块内或函数形参。
- 组合:不能与 static 同时使用。
- 数组/大对象:即使写成
register int a[100];,编译器也不会把整个数组放进寄存器(寄存器太小),最多对标量临时值做寄存器分配。
能用几个 register?
语言层面没有固定上限,真正的上限由“硬件可用寄存器数 + 编译器的寄存器分配算法 + 活跃生命周期冲突”决定。历史上某些老编译器可能只“尊重”前几个 register 声明;现代编译器通常直接忽略关键字,由优化器全局决策。结论:写再多 register 也不会让你“超额”占用寄存器;能用多少由编译器和硬件决定,而不是由关键字数量决定。
与 C++/标准演进:
- C:register 仍存在,但基本无实际优化意义(C23 标准已删除,GCC/Clang 多为兼容保留)。
- C++:自 C++11 起废弃,C++17 开始移除其语义(多数编译器视为“无效果”的关键字或给出弃用警告)。
实践建议(更有效的提速方式):
- 打开优化选项:
-O2或-O3(GCC/Clang),这是决定寄存器使用的关键。 - 让编译器更易优化:合理拆分热循环、小函数 inline、
restrict(C)指针、消除别名、避免不必要的易变访问等。 - 对性能敏感片段,用基准测试 + 查看汇编(
objdump -d或-S -fverbose-asm)验证寄存器分配是否达标。
- 调用/返回指令:执行 call/ret 或等价序列;涉及返回地址入栈/出栈、分支预测(返回栈预测器)。预测正确时开销很小,预测失败代价明显。 // 以上为函数调用的主要开销点汇总。
回调(callback)是把函数作为“可传递的值”使用的编程模式,在 C 中通常用函数指针实现。事件驱动则是把系统划分为“产生事件的一方”和“处理事件的一方”,通过注册回调来在事件发生时调用对应的处理器。
基本要点:
- 类型别名:用
typedef给回调定义清晰签名,便于维护。
typedef void (*event_cb_t)(void *ctx, int event, void *event_data);
struct event_handler { int event_type; event_cb_t cb; void *ctx; };
/* 简单注册/分发骨架 */
void register_handler(struct event_handler *arr, int *n, int type, event_cb_t cb, void *ctx);
void dispatch_event(struct event_handler *arr, int n, int type, void *data) {
for (int i = 0; i < n; ++i) {
if (arr[i].event_type == type && arr[i].cb) {
arr[i].cb(arr[i].ctx, type, data);
}
}
}实践与安全检查清单:
- 指针有效性:在调用回调前务必保证
cb与ctx的有效性,避免 use-after-free。 - 调用约定:文档化回调的线程语义(在哪个线程/事件循环调用),让注册者能正确同步。
- 最小职责:回调应做快速、确定性的工作;若需耗时操作,回调应把任务投递到工作队列/线程池。
- 错误处理:回调中不要 longjmp 或抛出未捕获异常(C++ 中注意跨语言边界)。
- 所有权明确:谁负责释放
event_data/ctx要有明确约定。
分派策略对比:
- 函数指针表(jump table):对整型事件 ID 可以用数组索引直接跳转,速度快但需连续 ID 范围。
- 链表/数组遍历:灵活,支持多处理器订阅同一事件,但每次分发需线性扫描。
- switch/case:编译器可优化为跳转表,适合静态、稠密的分支集合。
C vs C++:
- C++ 可使用
std::function、lambda 和成员函数绑定,语义更丰富但有额外开销;模版与 inline 能在零开销的情形下实现回调样式。
并发注意:
- 若多线程注册/注销回调,请用互斥/原子结构或采用复制-更新(copy-on-write)策略确保分发期间结构稳定。
- 可用无锁队列或事件环(event loop)把跨线程调用解耦:生产者仅 enqueue,消费者在单线程事件循环中 dequeue 并调用回调。
小结:回调与事件驱动是构建解耦系统的重要手段。关键在于清晰的边界、明确的所有权与线程语义、以及尽量把复杂/耗时工作放到异步任务中处理。
事件驱动(Event-driven)的核心思想是:
程序不主动“推着走”,而是等事件来触发对应的处理函数。
而 函数指针(Function Pointer) 或 回调函数(Callback) 的作用,就是告诉系统:
“事件发生时,要调用哪一个函数来处理。”
所以: 事件驱动 = 事件 + 回调函数(函数指针) 当一个事件发生 → 程序就用函数指针指向的处理函数来响应。
🧲 为什么要用函数指针?
因为事件是不确定的,而你想让它发生不同事情。
例如:
鼠标单击 → 调用 onClick()
键盘按下 → 调用 onKey()
网络收包 → 调用 onReceive()
你不能写死,只能让系统保存一个“指向函数的指针”,等事件发生时再调用。
这就是:
void (*callback)(int);
如果 callback = NULL,那就是:
“事件发生了,但没有函数可以处理。”
int compute(int x,int y, int (*p)(int ,int))
{ /*函数指针p作为compute函数的参数*/
int n;
n=(*p)(x,y);/*通过函数指针变量p调用函数*/
return n;
}
在C/C++中,当你写 int a[][3] = {1,2,3,4}; 时,编译器会按照以下规则进行初始化:
- 自动分组: 编译器会根据第二维的大小(这里是3)自动将初始化列表中的元素分组
- 等价形式:
{1,2,3,4}会被自动分组为{{1,2,3}, {4}} - 自动补零: 第二行只有1个元素,剩余的2个元素会被自动初始化为0
所以最终数组的实际内容是:
a[0][0] = 1, a[0][1] = 2, a[0][2] = 3
a[1][0] = 4, a[1][1] = 0, a[1][2] = 0
这两种写法完全等价:
int a[][3] = {1,2,3,4};(隐式分组)int a[][3] = {{1,2,3}, {4}};(显式分组)
注意: 第二维必须指定大小(这里是3),否则编译器无法知道如何分组。第一维可以省略,编译器会根据初始化元素的数量自动推导(这里是2行)。
- 整型常量: 十进制/八进制/十六进制整数字面量,允许后缀
u/U、l/L、ll/LL(顺序可变,如0xFFuL)。八进制以0开头,仅允许[0-7];十六进制以0x/0X开头,允许[0-9A-Fa-f]。 - 实型常量: 小数点或指数形式(
e/E);指数部分是十进制整数,e/E后的指数不可省,指数可带符号(如1e-3)。可带后缀f/F(float)、l/L(long double)。 - 一元负号:
-不是常量的一部分,是一元运算符;-1.0解析为一元负号作用于常量1.0。 - 十六进制浮点(C99): 形如
0x1.8p+1,指数引导符为p/P(不是e/E);没有p的0x12.5不是合法浮点常量。 - 常见错误: 八进制中出现
8/9;0x后没有数字;E/e后缺少指数;在十六进制整数中写小数点。
E-4; //(1) 非常量:标识符 E、运算符 -、整型常量 4(整体不是一个常量)
A423; //(2) 非常量:标识符 A423
-1E-31; //(3) 实型常量:一元负号 + 浮点常量 1E-31
// 指数规则:±N e/E ±A,A 必须为十进制整数,N 不可省
0xABCL; //(4) 整型常量:十六进制整数 + L(long)后缀
.32E31; //(5) 实型常量:小数点在前 + 指数
087; //(6) 非法:八进制不允许数字 8(以 0 开头即按八进制解析)
0xL; //(7) 非法:0x 后必须有至少一个十六进制数字,L 只能作后缀
003; //(8) 整型常量:八进制(值为十进制 3)
0x12.5; //(9) 非法:十六进制浮点在 C99 需用 p/P 指数(如 0x1.2p+3),此处无 p
077; //(10) 整型常量:八进制(值为十进制 63)
11E; //(11) 非法:E 后缺少指数
056L; //(12) 整型常量:八进制 + long 后缀(值为十进制 46)
0.; //(13) 实型常量:等价于 0.0
.0; //(14) 实型常量:等价于 0.0
- 补充提示:
- 十进制整数字面量不能带前导
0来表示十进制,否则会按八进制解析(如09非法)。 - C 中允许
.5、5.、5e0等多种浮点写法;C99 起支持十六进制浮点0x...p±A。
- 十进制整数字面量不能带前导
int(2)→unsigned int→long(4)→unsigned long→float(4)→double(8)→long double
↑
short(2)、char(1)默认升型
现代操作系统为每个进程提供独立的“虚拟地址空间”。源代码里的指针数值(如 p = malloc(16); 返回的地址)是“虚拟地址”,并不是物理内存的真实硬件地址。CPU 访问内存时,硬件的 MMU(Memory Management Unit)会按照“页表”把虚拟地址翻译成物理地址,并同时进行权限检查(读/写/执行)。
- 常规页:4KB(x86_64 常见),ARM 等平台类似;
- 大页 / HugePages:2MB 或 1GB(减少页表层级查找与 TLB 缺失);
- 页是最小的映射/换入换出/权限管理单位(而不是字节)。
虚拟地址被分段索引页表:
- PML4(顶级,P4)
- PDPT(PUD,P3)
- Page Directory(PMD,P2)
- Page Table(PTE,P1)→ 得到物理页帧 + 权限位(Present、R/W、User、NX 等)
这样做的目的:不必为整个地址空间准备一个巨大的单级表,未使用区域可以不分配中间结构(节省内存)。
- 为加速虚拟→物理地址翻译,CPU 保留一个硬件缓存(TLB);
- 每次内存访问先查 TLB;命中则直接得到物理地址及权限;不命中触发页表遍历;
- TLB 容量有限,频繁跨大量不同页的访问会导致 TLB miss(性能下降)。
当访问的虚拟页“不在内存”或权限不满足:
- CPU 产生缺页异常(陷入内核);
- 内核判断:是否合法(在进程 VMA 范围内)?是否读/写权限满足?
- 若需要磁盘数据(匿名页或文件映射):分配物理页帧 → 读入/置零;
- 更新页表条目(Present=1,权限位设置);
- 重新执行被中断指令;
- 若非法访问:向进程发送 SIGSEGV(段错误)。
- R/W/X:读/写/执行;现代系统常启用不可执行栈(NX bit),阻止栈执行代码;
- 用户态与内核态隔离:用户态页表条目的“User”位确保不能直接访问内核空间;
- ASLR(地址空间布局随机化):运行时随机化栈、堆、共享库、可执行装载基址,增加攻击难度。
fork()后父子进程共享相同物理页(只读),写入时触发 COW:分配新页并复制原内容;- 多进程共享只读代码段(文本段)与共享库页,节省物理内存;
mmap可映射文件,多个进程共享同一页;写入脏页后可回写或标记私有。
普通 C/C++ 程序不能直接取得物理地址(除非通过内核接口/驱动)。指针是纯虚拟地址;内核维护页帧与引用计数,防止进程相互干扰。
malloc 管理“虚拟地址空间中的一块区域”,向内核申请更多内存通常通过:
brk/sbrk:扩展/收缩进程 heap 顶;mmap:为较大块或特殊对齐直接映射匿名页或文件; 分配器再在获得的虚拟区间里切割小块、维护元数据(空闲链表、大小分类、对齐)。
减少页表层级查找与 TLB 项占用(一个大页覆盖更大虚拟范围)。适合内存访问模式连续且大规模的场景(数据库、科学计算)。缺点:分配不够灵活、可能增加内存碎片。
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
int main(){
void *p = malloc(4096); // 申请一页
strcpy((char*)p, "Hello Page\n");
printf("malloc addr=%p\n", p);
// 以只读方式保护该页(写会段错误)
mprotect((void*)((uintptr_t)p & ~(4095ULL)), 4096, PROT_READ);
printf("/proc/self/maps:\n");
fflush(stdout);
system("grep -m1 heap /proc/`pidof a.out 2>/dev/null || echo $$`/maps || head -n5 /proc/$$/maps");
return 0;
}(说明:mprotect 改变页的权限;/proc/<pid>/maps 展示虚拟地址区间、权限与映射来源。)
- “指针直接指向物理内存”:实际是虚拟→物理的页表映射;
- “页表就是一个巨大的数组”:现代是多级/稀疏结构,只为用到的区域分配中间层;
- “可以随意访问相邻进程内存”:权限与隔离机制阻止;越界访问只在本进程地址空间内(仍可能段错误)。
页表提供:地址隔离、安全权限、交换/懒加载、共享与复制优化。指针读写本身看似“直接访问”,底层却有 MMU + TLB + 多级页表的协同;理解这些有助于解释:为什么越界会段错误、为什么大数组随机访问慢、为什么启用大页可能提升性能。
*(a+i)//等价于a[i];
*p=&a[2]
p[-1];//等于a[1]
*(p+1);//等价于a[3];
for(p=a;p<a+10;p++)
char *s;
s="I LOVE U;//正确
char str[12];
str="FDU"//错误
定义与直观理解
- 变量(variable):具名的存储单元,绑定“名字 + 类型 + 存储位置”,其值可变。局部变量、全局变量、成员变量都属于变量。
- 实例(instance):某类型在运行时创建出的具体对象。更常用于面向对象语境(C++ 类的对象是类的实例)。在 C 中虽少用“实例”一词,但每次声明出的对象(结构体、数组、动态分配的块)都可视作该类型的一个实例。
C 与 C++ 的区别
- C:
- 不以“类”为中心,常用“对象/变量”术语;“实例”不常见。
- 声明一个 struct/数组变量即得到该类型的一个对象(实例)。
- 无构造/析构语义;初始化靠语法(聚合初始化、赋值、memset 等)。
- C++:
- 类的对象是“类的实例”;有构造、析构、拷贝/移动、虚函数与多态。
- 实例可以匿名(临时对象),不一定是变量;变量是具名对象或引用。
- RAII:实例的生命周期与资源管理绑定。
相通之处
- 都“占内存且有类型”,生命周期由作用域/存储期决定。
- 更通用概念是“对象(object)”:可能具名(变量)或匿名(临时值)。
- 指针/引用可指向对象;大小与对齐由类型决定。
易混点与澄清
- 变量强调“名字与存储的绑定”;实例强调“类型的具体对象”(可能匿名)。
- 成员变量在每个实例内各占一份;静态成员不属于实例而是共享。
示例对比
C(struct 的对象/变量)
#include <stdio.h>
struct Point { int x, y; };
int main() {
struct Point p = {1, 2}; // p 是变量,也是 Point 的一个对象(实例)
struct Point *q = &p; // 指针指向该对象
printf("%d,%d\n", q->x, q->y);
return 0;
}C++(类的实例与临时对象)
#include <iostream>
#include <string>
struct User {
std::string name;
explicit User(std::string n) : name(std::move(n)) {}
};
int main() {
User u("Alice"); // 具名变量,也是实例
User("Bob"); // 匿名临时实例(表达式结束后销毁)
const User &ref = User("Eve"); // 引用绑定临时,延长生命周期
std::cout << u.name << " / " << ref.name << "\n";
}一句话总结
- 变量是“名字 + 类型 + 存储”的绑定;实例是“类型的一个具体对象”(可具名也可匿名)。在 C++ 中常说“类实例”;在 C 中更常说“对象/变量”,本质相同。
核心结论(以 VC6/常见 32 位对齐规则为例):
- 编译器会对结构体做“对齐填充”,成员按各自对齐要求放置;结构体总大小通常按最大成员对齐的倍数对齐。
- 对齐能提高访问速度(减少跨字节/跨字词的读取),因此默认启用。
示例结构与直观计算:
struct student {
int num; // 4 bytes,对齐到 4
char name[20]; // 20 bytes,对齐到 1(但后续成员的对齐会引入填充)
char sex; // 1 byte,后面可能补 3 字节填充以满足下一个 int 的 4 字节对齐
int score[3]; // 12 bytes,需要 4 字节对齐
} s1;按常见规则的内存布局(32 位,最大对齐为 4):
num放在偏移 0..3(4 字节,对齐良好)。name紧随其后,占 20 字节(偏移 4..23)。sex在偏移 24(1 字节)。- 为保证下一个
int(score[0])4 字节对齐,编译器会在偏移 25..27 插入 3 字节填充。 score[3]占 12 字节,放在偏移 28..39。- 结构体总大小为 40 字节,且本身也满足 4 字节对齐。
因此:sizeof(s1) 为 40。若定义 struct student s2[2];,则数组占 40*2=80 字节;3 个则为 120 字节。
为何不是“4+20+1+12=37”?
- 因为对齐填充的存在:
sex后面插入了 3 字节,使后续int成员按 4 字节边界对齐。
不同平台/编译器可能差异:
- 最大对齐单位可能为 8(例如包含
double时),结构体总大小会按最大对齐的倍数对齐(常见:有double时以 8 为准)。 #pragma pack(n)或编译选项可修改对齐粒度(例如pack(1)减小填充),但可能降低性能并导致与 ABI 不兼容。
实践建议:
- 用
sizeof获取真实大小,不要手算忽略填充。 - 如需稳定的二进制布局(序列化/网络协议/硬件寄存器映射),应:
- 显式使用定长类型(如
uint32_t、uint8_t)。 - 控制对齐(
#pragma pack或逐字节拼装),并避免跨平台不一致。 - 通过
static_assert(sizeof(T) == 期望值, "...");(C++)或构建检查验证。
- 显式使用定长类型(如
示例:打印结构体大小与成员偏移(GCC/Clang 可用):
#include <stdio.h>
#include <stddef.h> // offsetof
struct student {
int num;
char name[20];
char sex;
int score[3];
};
int main(void) {
printf("sizeof(student) = %zu\n", sizeof(struct student));
printf("offset(num) = %zu\n", offsetof(struct student, num));
printf("offset(name) = %zu\n", offsetof(struct student, name));
printf("offset(sex) = %zu\n", offsetof(struct student, sex));
printf("offset(score) = %zu\n", offsetof(struct student, score));
return 0;
}补充:
- VC6 等老环境对齐行为与现代编译器大体一致,但请注意不同 ABI 的细节差异。
- 对齐的目的正是你备注的“加快取数速度”:让 2 字节类型位于可被 2 整除的地址;4 字节类型位于可被 4 整除的地址;以此类推。
void *malloc(size);
void *:指向任何类型的数据,在使用时,要进行强制类型转换
size: 一个无符号整数
Node *p;
p=(Node*)malloc(sizeof(Node));
- 标准声明是:
void *malloc(size_t size);
- 也就是说,
malloc是一个返回类型为void *的函数:void *:泛型指针,可以被转换成Node*、int*等任意对象指针。- 如果分配失败,
malloc返回NULL(一个空指针,而不是“合法地址”)。
- 在 C 里,你甚至可以不写强制类型转换:
Node *p = malloc(sizeof(Node)); // 在 C 语言中是 OK 的
可以粗略地记:“malloc 返回一块新内存的起始地址(指针)”,但要记得检查 p == NULL,以及用完后 free(p);。
int **a 的含义:
a是一个“指向int*的指针”,类型可以读作“pointer to int*”;- 换一种说法:
a指向的是一块“指针数组”(每个元素都是int*),这些int*再分别指向若干行int数据。
常见场景:需要一个“指向若干行 int 数组的指针数组”,例如动态二维数组 int **a:
int **a;
int rows, cols;
scanf("%d%d", &rows, &cols);
/* 1) 先为指针数组分配 rows 个元素 */
a = malloc(rows * sizeof(int*));
if (a == NULL) {
/* 申请失败,直接返回或退出 */
return 1;
}
/* 2) 再为每一行分配 cols 个 int */
for (int i = 0; i < rows; i++) {
a[i] = malloc(cols * sizeof(int));
if (a[i] == NULL) {
/* 这里应当释放前面已分配的行,然后返回 */
return 1;
}
}
/* 3) 使用 a[i][j] 像普通二维数组一样访问 */
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
a[i][j] = i + j;
}
}
/* 4) 用完要按“与分配相反的顺序” free */
for (int i = 0; i < rows; i++) {
free(a[i]);
}
free(a);注意:int **a 自身只是一块“指针数组”的起始地址,真正的二维内存是每一行单独 malloc 出来的;a 的类型是“指向 int* 的指针”,也就是 int **。
-
HLD(High-Level Design / 高层设计):
- 指软件系统的架构层面设计,注重模块划分、接口、数据流、组件交互与高层约束,而不是具体实现细节。
- 产物通常包含模块图、接口说明、时序图、数据模型与部署方案,便于团队协作与详细设计(LLD)。
-
HAL(Hardware Abstraction Layer / 硬件抽象层):
- 把底层硬件差异屏蔽为统一接口,让上层内核、驱动或应用通过一致的 API 与硬件交互。
- 在 Windows/嵌入式系统中常见(例如 Windows 的 hal.dll),负责中断、时钟、DMA、IO 访问等与平台相关的低级功能。
- 作用:提高可移植性、简化驱动编写、把平台相关代码隔离在一层。
-
HLE(High-Level Emulation / 高层模拟):
- 常见于模拟器领域,使用高层语义或行为等价的实现来替代精确模拟硬件的底层时序或寄存器级行为。
- 优点:实现更简单、运行更快;缺点:可能不适用于需要 cycle-accurate(时序精确)或对底层行为敏感的软件。
-
字符文件(文本文件):以可读字符(例如 ASCII/UTF-8)存储数据,数值通过文本表示(例如 "1268"),每个字符通常占 1 字节。优点是可读、便于调试与跨平台;缺点是占用空间大且读写时需要解析/格式化。
-
二进制文件:直接按内存中的原始字节写入文件(例如
int16_t占 2 字节就写入 2 字节)。优点是节省空间、读写速度快、适合批量操作;缺点是可移植性问题(类型宽度、对齐与字节序)。
- 例:
int16_t x = 1268;在内存中以 2 字节表示(十六进制 0x04F4)。写入二进制文件时就是写入这两个字节:- 小端(x86 常见):
F4 04 - 大端:
04 F4
- 小端(x86 常见):
- 文本形式则把数字转换为字符序列 "1268"(ASCII 0x31 0x32 0x36 0x38),占 4 字节。
因此,二进制按数据类型的内部表示占用字节数(固定宽度类型节省空间),而文本按字符数占用字节数(随数字长度而变)。
- “用于批量” 指一次性读写大量同类型数据(例如数组、结构体记录、图像像素等),常用函数是
fwrite/fread。示例:
// 批量写入 int16_t 数组(一次写入多个元素)
#include <stdio.h>
#include <stdint.h>
int main(void) {
int16_t arr[4] = {126, 1268, -1, 300};
FILE *f = fopen("data.bin","wb");
fwrite(arr, sizeof(arr[0]), 4, f); // 一次写入 4 个元素的二进制块
fclose(f);
return 0;
}-
优点:
- 写入/读取是内存到磁盘的块拷贝,CPU 开销低;
- 节省磁盘空间(按类型宽度存储);
- 可通过
fseek直接定位到第 N 条记录,便于随机访问; - 适合程序间或设备间高效传输大量数据(批量 I/O)。
-
风险与注意事项:
- 字节序(Endianness):不同平台解释字节顺序可能不同,跨平台读写需约定字节序或做转换(例如 network byte order 或手动字节反转)。
- 类型宽度与对齐:应使用固定宽度类型(
int16_t/int32_t)并注意结构体填充(可手动序列化或使用打包规则)。 - 可读性与安全:二进制不可直接阅读,且若格式不明确会导致错误解析;建议在文件头写入元数据(版本、字节序、记录大小)。
写好二进制/文本文件后,可用 hexdump 或 xxd 查看字节表示:
# 编译并运行示例程序(假设生成 data.bin 或 out.txt)
gcc write_bin.c -o write_bin && ./write_bin
gcc write_txt.c -o write_txt && ./write_txt
hexdump -C data.bin
hexdump -C out.txt你会看到 data.bin 中为二进制字节(例如 f4 04),而 out.txt 为 ASCII 字节(例如 31 32 36 38 对应字符 "1268")。
- 若仅在同一平台/程序内部读写大量数据:使用二进制和
fread/fwrite,并使用固定宽度类型。 - 若需跨平台或供人查看:使用文本格式(CSV/JSON)或使用标准化二进制序列化(protobuf/MessagePack/FlatBuffers),或在二进制文件头写入元数据(版本、字节序)。
(以上为补充:HLD/HAL/HLE 概念与字符/二进制文件的整理说明)
下面总结常见的 fopen 模式及其行为(文本/二进制、读/写/追加、是否创建或截断):
-
基本规则:
fopen(path, mode)返回FILE *,失败返回NULL,务必检查返回值。"b"(binary)在 POSIX/Unix 上通常无效但向后兼容;在 Windows 上会关闭文本模式下的换行转换(即不把"\n"转换为"\r\n"或反向转换)。"+"表示可读写(update mode),即既可读又可写。"b"与"+"可以任意順序组合(例如"rb+"、"r+b"等均可)。
-
常见模式说明:
-
"r":只读。文件必须存在;文件指针置于文件开头(不可写)。 -
"w":只写。若文件存在则截断为长度 0(会丢失原有内容);若不存在则创建新文件;文件指针置于开头(写入覆盖)。 -
"a":追加写。若文件不存在则创建;写操作总是追加到文件末尾(文件指针趋向于末尾,写入不会覆盖已有内容),读操作通常不可用(取决于实现)。 -
"rb"/"wb"/"ab":分别是r/w/a的二进制版本;在 Windows 上差别在于不做 CRLF 转换;在 Unix 上与无b等效。 -
"r+":可读可写,文件必须存在;文件指针初始在开头;写操作不会自动截断(除非显式调用freopen/ftruncate),读写位置由fseek/ftell控制;写入会覆盖当前位置的字节。 -
"w+":可读可写。若文件存在则截断为 0 长度,若不存在则创建;适用于“重建并同时读写”的场景。 -
"a+":可读可写并以追加为默认写入位置。若文件不存在则创建;可以读取文件中已有内容,但每次写操作通常会移到文件末尾追加(若需在中间写入,需使用fseek,并注意不同平台/实现对a+的语义差异)。 -
"rb+"/"r+b"、"wb+"/"w+b"、"ab+"/"a+b":分别是r+/w+/a+的二进制版本。
-
-
细节与实践建议:
- 总是检查
fopen返回的FILE*是否为NULL,并在失败时用perror/strerror(errno)报错诊断。例:FILE *f = fopen("data.bin","rb"); if (!f) { perror("fopen"); return 1; }
- 当使用
"w"或"w+"时要小心数据丢失(会截断),若要避免覆盖已有文件可先用access/stat检查或使用扩展模式"x"(POSIX/ISO C11"x"为独占创建,如果文件已存在则失败,例如"wx"、"w+x")。 - 在
"a"/"a+"模式下,写入通常被强制追加;若需要在文件中间更新,应使用r+/w+并配合fseek。注意并发场景下追加与读写的语义可能受系统调用原子性的影响。 - 二进制模式(含
b)在跨平台读写二进制数据时很重要(Windows 上区别明显);同时对二进制数据的读写应使用fread/fwrite而不是fgets/fprintf。 - 操作结束后务必
fclose(f),并在需要时fflush在文件关闭前强制写入缓冲区。
- 总是检查
#include <stdio.h>
#include <stdint.h>
int main(void) {
int16_t x = 1268;
FILE *f = fopen("data.bin","wb");
if (!f) { perror("fopen"); return 1; }
if (fwrite(&x, sizeof x, 1, f) != 1) perror("fwrite");
fclose(f);
return 0;
}以上為 fopen 常用模式的匯總與實踐建議;如需我把示例文件寫入倉庫並運行 hexdump 演示,請告知。
原型(简化):
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);-
共同点:
- 都在堆上申请一块连续内存,返回
void*,失败返回NULL。 - 都需要配对
free(ptr);释放。
- 都在堆上申请一块连续内存,返回
-
区别:
malloc(size):只保证拿到一块大小为size字节的内存,内容是未初始化的“垃圾值”。calloc(nmemb, size):申请nmemb * size字节,并且把这块内存全部置为 0(按字节清零)。
示例对比:
int *p1 = malloc(10 * sizeof(int)); // p1[0..9] 是未定义值
int *p2 = calloc(10, sizeof(int)); // p2[0..9] 保证一开始全是 0
if (p1 == NULL || p2 == NULL) {
/* 处理分配失败 */
}
/* 用完 */
free(p1);
free(p2);经验小结:
- 如果后面无论如何都会把数组完全写一遍(比如立刻用循环给所有元素赋值),用
malloc即可。 - 如果你一开始就需要“全 0”状态(例如清空数组、布尔标记数组、统计计数数组),用
calloc写起来更方便、也更不容易漏掉初始化。
原型(简化):
void *realloc(void *ptr, size_t new_size);作用:在堆上“调整一块已分配内存的大小”。
ptr:原来由malloc/calloc/realloc得到的指针;也可以是NULL(此时效果等价于malloc(new_size))。new_size:新的字节数。
典型用法:
int *p = malloc(10 * sizeof(int));
if (p == NULL) return 1;
/* 需要扩容为 20 个 int */
int *q = realloc(p, 20 * sizeof(int));
if (q == NULL) {
/* 失败时,原来的 p 仍然有效,需要继续使用或 free(p);不要直接丢失 */
free(p);
return 1;
}
p = q; /* 只在 realloc 成功后再修改 p */要点:
realloc可能:- 在原地成功扩容(返回同一个地址);
- 或者在新位置申请一块更大的内存,把旧内容拷贝过去,再 free 掉旧块,返回新地址。
- 因此:
- 不能写成
p = realloc(p, ...)后再检查p == NULL,否则失败时会丢失原指针,造成内存泄漏; - 正确写法是先用一个临时指针接住结果(如上例中的
q),只在成功时再赋回给p。
- 不能写成
realloc(ptr, 0)在很多实现中等价于free(ptr),但语义上更推荐直接写free(ptr)来释放。
一块内存被 free/delete 之后,可以这样理解:
-
从逻辑上变成“无主”,但物理字节还在
- 分配器(运行库/操作系统)把这块区域标记为“空闲,可以以后再分配给别人”。
- 之前指向它的指针变量还在,但此时这些指针指向的是一块你已经不再拥有使用权的区域。
- 这种仍然保存旧地址、但指向已释放内存的指针,叫作“悬空指针”(dangling pointer)。
-
指针中的“地址数值”还在,但不能再用来解引用
- 指针变量本身不会被
free,它只是一个普通变量;free(p)只释放p所指向的那块堆内存。 - 如果继续用这类指针做
*p、p[i]、p->field等操作,就是未定义行为:- 有时看起来还能读到“旧值”;
- 有时那块内存已经重新分配给别的对象并被改写;
- 也可能直接导致程序崩溃。
- 指针变量本身不会被
-
“内存上没有存值”这句话不严格
free并不会保证把那块内存清零,字节内容往往还是上一次写进去的东西,只是从语言层面你不能再依赖这些内容。- 更准确的说法是:这块内存的内容“对你来说不再有定义的意义”,就像你已经搬出去了,屋子里可能残留东西,但房子已经交还房东。
实践建议:
free之后,立刻把指针设为NULL,可以避免很多悬空指针误用:free(p); p = NULL; /* 之后 if(p != NULL) 再用 */
- 所有对动态内存的访问,都应该发生在“成功分配且尚未释放”这一段生命周期中。