C语言核心知识点详解

C语言核心知识点详解

简介

C语言是现代编程语言的基石,无论是操作系统内核、嵌入式开发还是高性能计算,C语言都扮演着不可替代的角色。本文从数据类型、指针、内存模型、预处理、存储类别等多个维度,系统梳理C语言的核心知识点,帮助开发者在实际工程中写出更高效、更安全的代码。

一、数据类型与内存表示

1.1 整数在计算机中的存储

计算机中所有整数都以**补码(Two's Complement)**的形式存储:

  • 正数:原码、反码、补码相同
  • 负数:符号位为1,其余位取反后加1
c 复制代码
// 以8位为例
// -1 的表示:
// 原码:10000001
// 反码:11111110
// 补码:11111111 = 255(无符号视角)

1.2 有符号与无符号的转换

当一个表达式中同时存在有符号和无符号数参与运算时,有符号数会被自动转换为无符号数。负数在转换后会变成一个非常大的正数,这是很多隐蔽Bug的来源。

c 复制代码
unsigned int a = 10;
int b = -5;
if (b < a) {
    // 可能不会按预期执行!
    // b 被转换为无符号数后变为一个很大的正数
}

关键规则:只要有无符号数参与运算,所有操作数都会被转换为无符号数。

1.3 数据溢出

c 复制代码
unsigned char a = 256;  // a = 0,只取低8位
char b = 255;           // b = -1,溢出后的值取决于类型解释

1.4 sizeof 运算符

sizeof 是一个运算符而非函数,它在编译期就确定结果,返回无符号整数(size_t)。

c 复制代码
char str[] = "Hello";
sizeof(str);   // 6,包含 '\0'
strlen(str);   // 5,不包含 '\0'

char *p = "Hello";
sizeof(p);     // 4(32位) 或 8(64位),指针大小
strlen(p);     // 5

图片占位符:sizeof与strlen对数组和指针的区别示意图

二、指针深入理解

2.1 指针的本质

指针就是一个变量,其值为另一个变量的地址。不同类型的指针在内存中占用的空间相同(32位系统4字节,64位系统8字节),唯一的区别在于指针运算时的偏移量不同。

c 复制代码
int *p = (int*)1;
p + 1;          // 偏移 sizeof(int) = 4,结果为 5

char *q = (char*)1;
q + 1;          // 偏移 sizeof(char) = 1,结果为 2

double *r = (double*)1;
r + 1;          // 偏移 sizeof(double) = 8,结果为 9

2.2 指针的引用

c 复制代码
int a = 10;
int *ptr = NULL;
int *&r = ptr;   // r 是指针ptr的引用
r = &a;          // 等价于 ptr = &a

2.3 数组指针与指针数组

c 复制代码
int (*p)[5];     // 数组指针:p指向含5个int元素的数组
                 // p+1 移动 5*sizeof(int)=20 个字节
int a[5];
p = &a;          // 取数组名的地址

int *q[4];       // 指针数组:含4个元素的数组,每个元素是int指针

记忆方法 :看变量名先和谁结合。p 先和 * 结合说明是指针,先和 [] 结合说明是数组。

2.4 void 指针

void* 指针可以接受任何类型的地址,但不能直接参与运算,因为编译器不知道它指向的数据大小。使用前必须强制转换为确定类型。

c 复制代码
void *p = malloc(100);
((int*)p)[0] = 12345;          // 当作 int 数组使用
((char*)p)[0] = 'A';           // 当作 char 数组使用
((int**)p)[0] = &some_int;     // 当作 int 指针数组使用

2.5 指针相减

两个同类型指针相减的结果是它们之间相隔的元素个数,而不是地址差值。

c 复制代码
int arr[10];
int *p = &arr[0];
int *q = &arr[5];
printf("%ld", q - p);    // 输出 5,不是 20

注意:不同类型的指针相减是没有意义的,编译器也不允许。

2.6 字符串与字符数组

c 复制代码
char *GetStr1(void) {
    char *p = "Hello World";
    return p;
    // 正确:字符串常量存储在 .rodata 段,函数结束不释放
}

char *GetStr2(void) {
    char p[] = "Hello World";
    return p;
    // 危险!p 是栈上的局部数组,函数返回后内存被释放
}

核心区别:字符串常量是不可修改但生命周期贯穿整个程序;字符数组是可修改的局部变量,存储在栈上,函数结束即释放。

2.7 不能返回局部变量的地址

这是C语言编程中的铁律。局部变量分配在栈上,函数返回后栈帧被回收,返回的地址指向的内存内容是未定义的。

三、const 关键字

const 是限定修饰符,将变量放在只读数据段,对其进行写操作是非法的。

3.1 const 与指针

c 复制代码
const int *p = &a;          // 底层 const:指向的值不能改变,指针本身可以改变
int const *p = &a;          // 同上,const int* 和 int const* 等价
int *const p = &a;          // 顶层 const:指针本身不能改变,指向的值可以改变
const int *const p = &a;    // 两者都不能改变

阅读技巧 :从右往左读,const 修饰它左边最近的符号。

3.2 const 与字符串

c 复制代码
char *p = "Hello World";
// 可以通过 p[0] 语法访问,但不能通过 *(p+1)='s' 来修改
// 实际上等价于 const char *p = "Hello World"

3.3 赋值兼容性

c 复制代码
const char *ptr = (char*)"hello";   // 合法:增加限定
char *ptr2 = (const char*)"hello";  // 警告:丢弃 const 限定

规则:等号左边变量的限定符应当包含右边变量的所有限定符。

四、volatile 关键字

volatile 告诉编译器该变量可能被意想不到地改变,禁止编译器对该变量进行优化,确保每次都从内存中重新读取值。

4.1 典型使用场景

c 复制代码
// 场景1:单片机/硬件寄存器访问
volatile int *XBYTE = (volatile int*)0x1234;
XBYTE[2] = 0x55;
XBYTE[2] = 0x56;
XBYTE[2] = 0x57;
XBYTE[2] = 0x58;
// 不加 volatile,编译器可能只执行最后一条赋值

4.2 const 与 volatile 可以同时使用

c 复制代码
const volatile int status = 0;
// const:程序代码不能修改
// volatile:可能被外部硬件/中断修改,每次都要重新读取
// 典型场景:只读硬件状态寄存器

五、数组与函数参数

5.1 一维数组作为函数参数

c 复制代码
int a[4];
// 以下三种形式等价:
void fun(int *a);
void fun(int a[4]);
void fun(int a[]);
// 编译器总是将其解析为指向首元素的指针

5.2 二维数组作为函数参数

c 复制代码
int a[4][3];
// 以下三种形式等价:
void fun(int (*a)[3]);
void fun(int a[][3]);
void fun(int a[4][3]);

核心原理:C语言中数组作为函数实参时,编译器总是将其解析为指向数组首元素地址的指针。

六、内存对齐

6.1 什么是内存对齐

对于32位系统,默认采用4字节对齐;64位系统采用8字节对齐。内存对齐是一种空间换时间的策略,便于CPU高效访问数据。

c 复制代码
struct val1 {
    char ch;    // 1 字节
    short s;    // 2 字节
    int i;      // 4 字节
};
// sizeof = 8,成员按大小排列,浪费较少

struct val2 {
    char ch;    // 1 字节 + 3 字节填充
    int a;      // 4 字节
    short s;    // 2 字节 + 2 字节填充
};
// sizeof = 12,排列不佳导致更多填充

图片占位符:两种结构体内存布局对比图

6.2 优化建议

  • 按成员大小从大到小排列可以减少填充字节
  • 在嵌入式等空间紧张的场景中,可以使用 #pragma pack(1) 取消对齐

七、位运算详解

位运算在系统编程中极为重要,它的核心目的是节省存储空间,将最小的存储单元由字节变为位。

7.1 掩码操作

c 复制代码
// 将最后一位清零(可移植写法)
int b = ~1;        // 在所有平台上通用
// 不可移植写法:0xFFFE(只在16位机器上正确)

7.2 异或运算(使用最广泛的位运算)

c 复制代码
// 规则:相同为0,不同为1
// 1. 与1异或取反
// 2. 与0异或保持不变
// 3. 交换两个值,不需要中间变量
a = a ^ b;
b = a ^ b;
a = a ^ b;

7.3 移位操作注意事项

c 复制代码
int a = 31;
int x = 0xFFFFFFFF;

x >> 31;   // 结果为 1(无符号右移,高位补0)
x >> a;    // 结果为 -1(a是有符号数,编译器进行算术右移,高位补1)

// 移位次数不能超过数据类型的位数,否则是未定义行为

7.4 循环右移

c 复制代码
unsigned int rotate_right(unsigned int a, int n) {
    unsigned int b = a << (32 - n);
    unsigned int c = a >> n;
    c = c & ~(~0U << n);
    return c | b;
}

7.5 位运算代替乘除法

c 复制代码
x << 1;    // 等价于 x * 2
x >> 1;    // 等价于 x / 2(正数)
x << n;    // 等价于 x * (2^n)
12 * 5;    // 等价于 (12 << 2) + 12

性能对比:除法指令约50个机器周期,位运算和加法运算仅1-2个周期。

八、变量作用域与存储类别

8.1 局部变量与全局变量

c 复制代码
// 全局变量:定义在函数外,存储在静态存储区
// 未初始化时,编译器自动初始化为0(数值型)或NULL(指针型)

// 局部变量:定义在函数内,存储在栈帧上
// 未初始化时,值为该内存位置之前的残留值(垃圾值)

8.2 存储类别关键字

关键字 作用
auto 默认存储类别,编译器自动辨别
register 建议编译器将变量存入寄存器(如循环变量)
extern 声明外部定义的全局变量
static 限制作用域或延长局部变量生命周期

8.3 static 关键字的三大用途

1. 静态局部变量:延长生命周期至程序结束,但作用域不变

c 复制代码
void counter(void) {
    static int count = 0;  // 只初始化一次
    count++;
    printf("%d\n", count);
}

2. 静态全局变量/函数:限制作用域为当前文件

c 复制代码
// module.c
static Node head;  // 只能在 module.c 中访问

// 对外提供接口
extern int insert(int dat);
extern void print(void);
extern void destroy(void);

3. 模块封装:通过 static + 外部接口函数实现信息隐藏

8.4 访问同名全局变量

c 复制代码
int a = 10;  // 全局变量
void func(void) {
    int a = 5;     // 局部变量
    printf("%d %d", a, ::a);  // 输出 5 10(C++中)
}

九、预处理机制

9.1 预处理的三个工作

  1. 过滤注释
  2. 包含文件(#include
  3. 展开宏(#define

9.2 条件编译(调试开关)

c 复制代码
#define DEBUG 1

#ifdef DEBUG
    printf("调试信息:x = %d\n", x);
#else
    // 正式发布代码
#endif

9.3 模块化设计原则

  • 含有类似功能或操作同一对象的函数应放在同一个 .c 文件中
  • 需要被其他模块引用的函数定义为外部函数(不加 static
  • 仅在本模块中使用的函数和变量应定义为 static,防止命名冲突

十、函数调用与优化

10.1 函数调用的开销

函数调用需要以下步骤,每一步都涉及内存访问:

  1. 将参数压入堆栈
  2. 保存寄存器的值
  3. 保存返回地址
  4. 跳转到函数入口

10.2 函数优化策略

1. 减少函数调用次数

c 复制代码
// 优化前
result = fun(1) + fun(1) + fun(1);
// 优化后(如果 fun 无副作用)
result = 3 * fun(1);

2. 使用局部变量替代频繁访问的全局变量

编译器会将使用频率高的局部变量存储在寄存器中,而全局变量每次都要访问内存。

3. 循环优化

c 复制代码
// 优化前:每次循环都调用 strlen
for (i = 0; i < strlen(buf); i++) { ... }

// 优化后:只计算一次
const int len = strlen(buf);
for (i = 0; i < len; i++) { ... }

4. 表达式优化

c 复制代码
x = x + 1;    // 访问两次内存
x += 1;       // 访问一次内存

5. if-else 分支优化

将概率最高的条件放在最前面判断,可以减少平均判断次数。

6. switch 与 if-else

switch 维护一张跳转表,是空间换时间 的典型例子。当分支较多且为离散值时,优先使用 switch

10.3 inline 关键字(C99)

inline 建议编译器将函数代码直接展开到调用位置,避免函数调用的开销。

c 复制代码
inline int add(int a, int b) {
    return a + b;
}

注意事项

  • 不是所有 inline 函数都会被展开,由编译器决定
  • 只有代码较短时才会展开,过多使用反而会导致代码膨胀
  • inline 不一定展开,但 #define 宏一定会展开

10.4 restrict 关键字(C99)

restrict 告诉编译器指针指向的内存区域不会重叠,允许编译器进行更激进的优化。

c 复制代码
// 无 restrict:编译器不能确定指针是否重叠,只能串行执行
void sum(int *out, int *arr1, int *arr2, int num);

// 有 restrict:编译器确信指针不重叠,可以并行优化
void sum(restrict int *out, restrict int *arr1, restrict int *arr2, int num);

十一、大端与小端

c 复制代码
// 小端模式(Little Endian):高地址存放高位,低地址存放低位
// Intel x86 架构采用小端模式

// 大端模式(Big Endian):高地址存放低位,低地址存放高位
// 网络字节序采用大端模式

// 判断方法:
int a = 1;
if (*(char*)&a == 1) {
    printf("小端模式\n");
}

十二、链接与符号解析

12.1 声明与定义

  • 声明:告诉编译器某个符号存在
  • 定义:给符号分配内存空间

12.2 符号解析的三个规则

  1. 不允许有多个强符号定义 :两个文件中不能都有 int a = 4;
  2. 一个强符号和一个弱符号:选择强符号作为定义
  3. 两个弱符号:先扫描到的作为定义

总结

C语言的核心知识体系涵盖了从数据表示到内存管理、从指针操作到编译优化的方方面面。理解这些底层原理不仅有助于写出高质量的C代码,也是学习C++、操作系统和嵌入式开发的坚实基础。

在实际开发中,尤其需要注意以下几点:

  • 深入理解指针和内存模型,避免内存泄漏和非法访问
  • 善用 conststatic 实现封装和信息隐藏
  • 在性能敏感场景中,合理使用位运算和编译优化技巧
  • 重视代码的模块化设计,遵循高内聚、低耦合原则

原始笔记来源: frasight/上课笔记.c

相关推荐
Bluetooth7302 小时前
c语言(选择与循环)程序与算法
c语言
努力努力再努力wz2 小时前
【Qt 入门系列】从应用场景到开发环境:建立对 Qt 的第一层认知
c语言·开发语言·数据库·c++·b树·qt·缓存
无限进步_2 小时前
【C++】红黑树完全解析:从概念到插入与平衡维护
java·c语言·开发语言·数据结构·c++·后端·算法
50万马克的面包3 小时前
C语言数据在内存中的存储(后续会持续优化)
c语言
无限进步_3 小时前
简单聊聊 C++ 中的 unordered_map 和 unordered_set
c语言·开发语言·数据结构·c++·windows·哈希算法·散列表
枫叶丹44 小时前
【HarmonyOS 6.0】Data Augmentation Kit 智慧化数据检索 C 接口解析:向量化、知识检索与知识问答
c语言·开发语言·华为·harmonyos
TANGLONG2224 小时前
【C++】STL基础必备:深入解析vector容器的实现(含源码)
c语言·开发语言·数据结构·c++·笔记·算法·stl
50万马克的面包4 小时前
C语言第3讲:分支和循环
c语言·开发语言·笔记·算法
孬甭_4 小时前
预处理详解
c语言·开发语言