当前位置: 首页 > news >正文

C PRIMER PLUS——第9节:动态内存分配、存储类别、链接和内存管理

目录

1.动态内存分配

1.1 malloc 函数

1.2 calloc 函数

1.3 realloc 函数

1.4 free 函数

1.5常见错误

1.6综合例题

 2.C语言的内存结构

3.存储类别

3.1作用域(Scope)

3.2链接(Linkage)

3.3存储期(Storage Duration)

3.4自动变量(Automatic Variables)

3.5寄存器变量(Register Variables)

3.6块作用域的静态变量

3.7外部链接的静态变量

3.8内部链接的静态变量

3.9多文件(Multiple Files)

3.10存储类别说明符(Storage Class Specifiers)

3.11存储类别和函数

3.12存储类别的选择

4.随机数函数和静态变量

4.1随机数函数

①含义

②作用

③注意事项

④例题

4.2静态变量

①含义

②作用

③注意事项

④例题

4.3综合例题

5.掷骰子

①含义

②作用

③例题

例 1:单次掷骰子

例 2:多次掷骰子并统计结果

例 3:简单的掷骰子游戏

④注意事项

6.分配内存

6.1变长数组(VLA)

①作用:

②例题:

6.2存储类别

①类别:

②作用:

③例题:

6.3动态内存分配与存储类别的关系

7.ANSI C类型限定符

7.1 const类型限定符

①含义与作用

②注意事项

③例题

7.2 volatile类型限定符

①含义与作用

②注意事项

③例题

7.3 restrict类型限定符

①含义与作用:

②注意事项

③例题

7.4 _Atomic类型限定符(C11)

①含义与作用

②注意事项

③例题

7.5 旧关键字的新位置

7.6 总结


1.动态内存分配

1.1 malloc 函数

①含义malloc 即 "memory allocation"(内存分配)的缩写。
②作用:此函数会在堆区分配指定大小的连续内存空间,不过不会对分配的内存进行初始化。
③函数原型void* malloc(size_t size);
④返回值:若分配成功,会返回一个指向分配内存起始地址的 void* 指针;若分配失败,则返回 NULL
⑤注意事项与细节

  • 要自行计算所需内存的字节数,例如 int* arr = (int*)malloc(10 * sizeof(int));

  • 返回的指针类型为 void*,在使用时需要将其转换为合适的类型。

  • 若请求的内存大小为 0,其行为取决于具体的编译器实现,可能返回 NULL,也可能返回一个不能被解引用的有效指针。

  • 分配的内存中存储的是未初始化的数据,也就是垃圾值。

⑥示例

int* ptr = (int*)malloc(5 * sizeof(int));
if (ptr == NULL) {printf("内存分配失败\n");exit(1);
}
// 使用ptr...
free(ptr); // 释放内存

1.2 calloc 函数

含义calloc 是 "contiguous allocation"(连续分配)的缩写。
作用:该函数会分配指定数量、指定大小的连续内存空间,并且会将分配的内存全部初始化为 0。
函数原型void* calloc(size_t num, size_t size);
返回值:分配成功时返回指向内存起始地址的 void* 指针,失败则返回 NULL
注意事项与细节

  • 分配的总字节数是 num * size,要是这两个参数的乘积超出了 size_t 类型的表示范围,就会引发溢出问题。

  • 由于会进行内存初始化操作,所以 calloc 的执行速度比 malloc 要慢。

  • 适合用于分配数组或者结构体,因为这些数据结构通常需要初始化为 0。

示例

int* arr = (int*)calloc(5, sizeof(int)); // 分配5个int大小的内存并初始化为0
if (arr != NULL) {// arr[0]到arr[4]都被初始化为0
}
free(arr);

1.3 realloc 函数

含义realloc 即 "reallocate"(重新分配)的缩写。
作用:该函数用于调整之前分配的内存块的大小,可以扩大或者缩小。
函数原型void* realloc(void* ptr, size_t new_size);
返回值

  • 若调整成功,返回指向新内存块的指针,新内存块可能和原来的地址相同,也可能不同。

  • 若调整失败,返回 NULL,此时原来的内存块不会被释放。

  • 若 ptr 为 NULL,其作用等同于 malloc(new_size)

  • 若 new_size 为 0,其行为取决于具体的编译器实现,可能返回 NULL 并释放原内存块,也可能有其他行为。

注意事项与细节

  • 若新的内存块地址发生了改变,realloc 会将原内存块中的内容复制到新内存块中,但只复制较小内存块大小范围内的数据。

  • 扩大内存时,如果原内存块后面的空间不足,realloc 会重新分配一块更大的连续内存,并复制原有数据,然后释放原内存块。

  • 不要使用原指针访问经过 realloc 调整后的内存块,要使用 realloc 返回的新指针。

示例

int* arr = (int*)malloc(3 * sizeof(int));
// 使用arr...
arr = (int*)realloc(arr, 5 * sizeof(int)); // 扩大为5个int的大小
if (arr == NULL) {printf("内存重新分配失败\n");exit(1);
}
// 使用扩大后的arr...
free(arr);

1.4 free 函数

含义free 意思是 "释放"。
作用:该函数用于释放之前通过 malloccalloc 或 realloc 分配的内存,使其能被系统再次使用。
函数原型void free(void* ptr);
返回值:无返回值(void)。
注意事项与细节

  • 只能释放动态分配的内存,释放静态分配的内存(如栈上的变量)会导致未定义行为。

  • 释放内存后,原指针会变成野指针,再次使用它会引发错误,所以释放后通常要将指针置为 NULL

  • 多次释放同一块内存会导致程序崩溃,所以必须确保每块内存只被释放一次。

  • free(NULL) 不会产生任何效果,这一点可以用于简化代码。

示例

int* ptr = (int*)malloc(100);
free(ptr);
ptr = NULL; // 防止成为野指针

1.5常见错误

  1. 内存泄漏:分配了内存却没有释放。

  2. 野指针:释放内存后仍然使用原指针。

  3. 双重释放:多次释放同一块内存。

  4. 内存越界:访问超出分配内存范围的数据。

  5. 未检查返回值:没有检查 malloccallocrealloc 是否返回 NULL,在内存不足时可能导致程序崩溃。


1.6综合例题

题目:编写一个程序,先动态分配一个能存储 5 个整数的数组,将其初始化为 1 到 5,然后将数组大小调整为 10 个整数,把新增加的元素初始化为 6 到 10,最后打印数组并释放内存。

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main() {int* arr = (int*)malloc(5 * sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;}// 初始化前5个元素for (int i = 0; i < 5; i++) {arr[i] = i + 1;}// 扩大数组arr = (int*)realloc(arr, 10 * sizeof(int));if (arr == NULL) {printf("内存重新分配失败\n");return 1;}// 初始化新增的元素for (int i = 5; i < 10; i++) {arr[i] = i + 1;}// 打印数组for (int i = 0; i < 10; i++) {printf("%d ", arr[i]);}printf("\n");// 释放内存free(arr);arr = NULL;return 0;
}

输出

1 2 3 4 5 6 7 8 9 10

关键点

  • 合理使用 malloc 和 realloc 来动态调整内存大小。

  • 每次内存分配后都检查是否分配成功。

  • 正确释放内存并避免野指针。


 2.C语言的内存结构


3.存储类别

3.1作用域(Scope)

作用域描述了程序中可以访问一个标识符的区域。C 语言中有四种作用域:

  • 文件作用域(File Scope):定义在所有函数外部的标识符具有文件作用域,从定义处到文件结束都可以访问。

  • 函数作用域(Function Scope):仅用于 goto 语句的标签,其作用域是整个函数内部。

  • 块作用域(Block Scope):在块(由花括号 {} 包围的代码区域)中定义的标识符具有块作用域,从定义处到块结束有效。

  • 函数原型作用域(Function Prototype Scope):函数原型中的参数名具有函数原型作用域,仅在原型声明中有效。


3.2链接(Linkage)

链接决定了标识符在不同文件之间的可见性。C 语言中有三种链接:

  • 外部链接(External Linkage):具有外部链接的标识符可以在多个文件中使用。函数和在文件作用域中定义且没有使用 static 关键字的变量具有外部链接。

  • 内部链接(Internal Linkage):具有内部链接的标识符只能在定义它的文件中使用。在文件作用域中使用 static 关键字定义的变量和函数具有内部链接。

  • 无链接(No Linkage):局部变量、函数参数和使用 register 关键字声明的变量具有无链接,它们只能在定义它们的块内访问。


3.3存储期(Storage Duration)

存储期决定了变量占用内存的时间。C 语言中有四种存储期:

  • 静态存储期(Static Storage Duration):具有静态存储期的变量在程序执行期间一直存在。文件作用域的变量(无论是否使用 static)都具有静态存储期。

  • 线程存储期(Thread Storage Duration):使用_Thread_local 关键字声明的变量具有线程存储期,每个线程都有自己的副本。

  • 自动存储期(Automatic Storage Duration):块作用域中未使用 static 或 extern 声明的变量具有自动存储期,它们在块执行时创建,块结束时销毁。

  • 动态存储期(Dynamic Storage Duration):通过 malloc ()、calloc () 或 realloc () 动态分配的内存具有动态存储期,直到使用 free () 释放或程序结束。


3.4自动变量(Automatic Variables)

自动变量是在块作用域中声明的变量,默认情况下具有自动存储期和无链接。它们在块执行时创建,块结束时销毁。可以使用 auto 关键字显式声明,但通常省略。

void func() {int x = 10;  // 自动变量,等同于 auto int x = 10;// ...
}  // x被销毁

3.5寄存器变量(Register Variables)

寄存器变量是使用 register 关键字声明的自动变量,建议编译器将其存储在 CPU 寄存器中,以提高访问速度。但编译器可以忽略这个建议。

void func() {register int count = 0;  // 寄存器变量// ...
}

注意事项:

  • 不能获取寄存器变量的地址(&count 是非法的)。

  • 寄存器变量的存储位置由编译器决定,可能不在内存中。


3.6块作用域的静态变量

在块作用域中使用 static 关键字声明的变量具有静态存储期,但作用域仍然是块内部。它们只在第一次执行到声明处时初始化,之后保持其值。

void func() {static int count = 0;  // 静态变量,只初始化一次count++;printf("%d\n", count);
}
int main() {func();  // 输出1func();  // 输出2return 0;
}

3.7外部链接的静态变量

在文件作用域中声明且没有使用 static 关键字的变量具有外部链接,可以在其他文件中使用。需要使用 extern 关键字在其他文件中声明。

// file1.c
int global_var = 10;  // 外部链接的静态变量
// file2.c
extern int global_var;  // 声明外部变量
void func() {printf("%d\n", global_var);  // 使用file1.c中的global_var
}

3.8内部链接的静态变量

在文件作用域中使用 static 关键字声明的变量具有内部链接,只能在定义它的文件中使用

// file1.c
static int file_private = 20;  // 内部链接的静态变量
void func() {printf("%d\n", file_private);  // 合法
}
// file2.c
extern int file_private;  // 声明,但无法使用,因为file_private是内部链接
void another_func() {printf("%d\n", file_private);  // 错误:无法访问
}

3.9多文件(Multiple Files)

在 C 语言中,大型程序通常分为多个源文件。使用 extern 关键字可以在一个文件中声明另一个文件中定义的外部变量或函数。

// file1.c
int global_var = 10;
void func() { /* ... */ }
// file2.c
extern int global_var;  // 声明外部变量
extern void func();     // 声明外部函数
int main() {global_var = 20;  // 使用file1.c中的global_varfunc();           // 调用file1.c中的funcreturn 0;
}

3.10存储类别说明符(Storage Class Specifiers)

C 语言提供了五个存储类别说明符:

  • auto:声明自动变量(通常省略)。

  • register:声明寄存器变量

  • static:声明静态变量或内部链接的文件作用域变量 / 函数

  • extern:声明外部变量或函数

  • _Thread_local:声明线程局部存储的变量(C11 新增)


3.11存储类别和函数

函数默认具有外部链接,可以在其他文件中使用。使用 static 关键字可以将函数的链接设置为内部,使其只能在定义它的文件中使用。

// file1.c
static void helper() { /* ... */ }  // 内部链接的函数
void public_func() { /* ... */ }    // 外部链接的函数
// file2.c
extern void public_func();  // 可以声明并使用
extern void helper();       // 声明合法,但调用会导致链接错误

3.12存储类别的选择

选择合适的存储类别时,应考虑以下因素:

  • 作用域和可见性:根据变量或函数需要被访问的范围选择作用域和链接。

  • 生命周期:根据变量需要存在的时间选择存储期。

  • 线程安全:在多线程环境中,考虑使用_Thread_local 或适当的同步机制。

  • 性能:对于频繁访问的变量,考虑使用 register 声明(但现代编译器通常会自动优化)。

例题

1.分析以下代码的输出:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void func() {int a = 0;static int b = 0;a++;b++;printf("a = %d, b = %d\n", a, b);
}
int main() {func();  // 输出:a = 1, b = 1func();  // 输出:a = 1, b = 2return 0;
}

2.设计一个多文件程序,包含一个外部变量和内部变量,并演示它们的使用。

#define  _CRT_SECURE_NO_WARNINGS
// file1.c
#include <stdio.h>
int global = 10;        // 外部链接的全局变量
static int private = 20; // 内部链接的全局变量
void print_vars() {printf("Global: %d, Private: %d\n", global, private);
}
// file2.c
#include <stdio.h>
extern int global;  // 声明外部变量
int main() {global = 30;    // 修改外部变量// private = 40; // 错误:无法访问privateprint_vars();   // 输出:Global: 30, Private: 20return 0;
}

4.随机数函数和静态变量

4.1随机数函数

①含义

在 C 语言中,随机数函数主要有srand()rand()

  • rand()函数的功能是生成一个范围在 0 到RAND_MAX之间的伪随机整数。这里的RAND_MAX是一个常量,其值至少为 32767。

  • srand()函数的作用是为随机数生成器设置种子。如果不调用srand(),那么系统会默认使用 1 作为种子。

②作用

随机数函数常用于模拟、游戏、密码学等场景,通过生成随机数来增加程序的不确定性。

③注意事项

  1. 若使用相同的种子调用srand(),那么rand()生成的随机数序列也会相同。

  2. 通常会用time(NULL)作为种子,因为它能根据当前时间生成不同的种子值。不过要注意,在程序运行期间,time(NULL)每秒才会更新一次,所以短时间内多次运行程序可能会得到相同的随机数序列。

  3. 如果想要生成特定范围内的随机数,需要进行适当的转换。

④例题

下面的代码展示了随机数函数的基本用法,生成 10 个 1 到 100 之间的随机数:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {srand(time(NULL));  // 设置随机数种子for (int i = 0; i < 10; i++) {int num = rand() % 100 + 1;  // 生成1到100之间的随机数printf("%d ", num);}return 0;
}

4.2静态变量

①含义

在 C 语言中,静态变量是使用static关键字声明的变量。静态变量有以下两种情况:

  1. 局部静态变量:在函数内部声明的静态变量,它会在程序的整个运行期间都存在,并且只会被初始化一次。

  2. 全局静态变量:在函数外部声明的静态变量,其作用域仅限于声明它的文件

②作用

  1. 局部静态变量能够在函数调用之间保持其值,可用于实现计数器、缓存等功能。

  2. 全局静态变量可以防止不同文件之间的命名冲突。

③注意事项

  1. 静态变量如果没有显式初始化,会被自动初始化为 0。

  2. 局部静态变量的初始化语句只会在第一次调用函数时执行。

  3. 全局静态变量会限制变量的作用域,使其无法被其他文件访问。

④例题

下面的代码展示了局部静态变量的基本用法,统计函数被调用的次数:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void count_calls() {static int count = 0;  // 局部静态变量,初始值为0count++;printf("函数已被调用 %d 次\n", count);
}
int main() {count_calls();  // 输出:函数已被调用 1 次count_calls();  // 输出:函数已被调用 2 次count_calls();  // 输出:函数已被调用 3 次return 0;
}

4.3综合例题

下面的代码结合了随机数函数和静态变量,实现了一个猜数字的小游戏:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int generate_number() {static int initialized = 0;if (!initialized) {srand(time(NULL));  // 只初始化一次随机数种子initialized = 1;}return rand() % 100 + 1;
}
int main() {int secret = generate_number();int guess;int attempts = 0;printf("猜一个1到100之间的数字:\n");do {scanf("%d", &guess);attempts++;if (guess > secret) {printf("太大了!再试一次:\n");} else if (guess < secret) {printf("太小了!再试一次:\n");} else {printf("恭喜你,猜对了!你用了 %d 次尝试。\n", attempts);}} while (guess != secret);return 0;
}

总结

  1. 随机数函数可以为程序带来不确定性,不过要合理设置种子,以避免生成相同的随机数序列。

  2. 静态变量可以在函数调用之间保持状态,适合用于实现需要记忆功能的程序。

  3. 使用这两个特性时,要留意初始化和作用域的问题,防止出现意外的结果。


5.掷骰子

①含义

掷骰子的模拟主要依靠 C 语言标准库中的随机数生成函数,其核心是生成 1 到 6 之间的随机整数,以此对应骰子的六个面。下面是相关的关键函数:

  • rand():能够返回一个范围在 0 到 RAND_MAX(至少为 32767)之间的伪随机整数。

  • srand(seed):该函数用于设置随机数生成器的种子值。要是使用相同的种子值,那么每次生成的随机数序列都是一样的。

  • time(NULL):会返回当前的时间戳,常被用作srand()的种子,从而让随机数更具随机性。

②作用

掷骰子在编程学习以及实际应用中都有着重要的作用:

  1. 教学意义:是初学者学习随机数生成、条件判断和循环结构的典型例子。

  2. 游戏开发:在各种游戏里,像棋盘游戏、角色扮演游戏等,都需要模拟掷骰子来推动游戏进程。

  3. 概率模拟:可以用来验证概率理论,例如大数定律。

  4. 随机决策:在程序中需要随机选择时,掷骰子模拟就能派上用场。

③例题

下面通过几个具体的例子来加深对掷骰子模拟的理解:

例 1:单次掷骰子
#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {// 初始化随机数种子srand(time(NULL));// 生成1-6之间的随机数int dice = rand() % 6 + 1;printf("你掷出了: %d\n", dice);return 0;
}
例 2:多次掷骰子并统计结果
#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {int n = 1000;  // 掷骰子的次数int counts[7] = {0};  // 用于统计每个点数出现的次数,索引0不使用srand(time(NULL));// 掷骰子n次并统计结果for (int i = 0; i < n; i++) {int dice = rand() % 6 + 1;counts[dice]++;}// 输出统计结果printf("掷%d次骰子的统计结果:\n", n);for (int i = 1; i <= 6; i++) {printf("点数%d: %d次 (%.2f%%)\n", i, counts[i], (double)counts[i] / n * 100);}return 0;
}
例 3:简单的掷骰子游戏

下面是一个简单的掷骰子游戏,玩家和电脑轮流掷骰子,点数大的获胜:

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {char name[50];int player_score, computer_score;printf("欢迎参加掷骰子游戏!请输入你的名字:");scanf("%s", name);srand(time(NULL));// 玩家掷骰子printf("\n%s,轮到你掷骰子了!按Enter键掷骰子...", name);getchar();  // 消耗掉scanf留下的换行符getchar();  // 等待用户按Enter键player_score = rand() % 6 + 1;printf("你掷出了: %d\n", player_score);// 电脑掷骰子printf("\n电脑正在掷骰子...\n");// 延时效果for (int i = 0; i < 3; i++) {printf(".");fflush(stdout);  // 立即输出for (int j = 0; j < 100000000; j++);  // 简单延时}printf("\n");computer_score = rand() % 6 + 1;printf("电脑掷出了: %d\n", computer_score);// 判断胜负if (player_score > computer_score) {printf("\n恭喜,%s!你赢了!\n", name);} else if (player_score < computer_score) {printf("\n很遗憾,%s,你输了。\n", name);} else {printf("\n平局!\n");}return 0;
}

④注意事项

  1. 随机数的局限性rand()生成的是伪随机数,在实际应用中,如果对随机性要求较高,可以考虑使用更高级的随机数生成器。

  2. 种子的设置:在程序运行过程中,只需要调用一次srand()函数,通常在程序开始时调用即可。如果多次调用并且使用的是相近的时间作为种子,可能会导致生成的随机数序列相近。

  3. 取模运算的偏差:使用rand() % 6这种方式生成随机数,当 RAND_MAX 不是 6 的整数倍时,可能会使某些数字出现的概率稍高一些。不过在大多数简单应用场景下,这种偏差可以忽略不计。


6.分配内存

6.1变长数组(VLA)

变长数组是 C99 标准引入的特性,允许在声明数组时使用变量来指定数组的大小。数组的大小在运行时确定,但一旦确定就不能改变。

①作用

  • 简化代码,无需手动管理内存。

  • 避免固定大小数组的浪费。

  • 适用于需要根据输入确定数组大小的场景。

②例题

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main() {int n;printf("请输入数组的大小:");scanf("%d", &n);// 声明变长数组int arr[n];   // 初始化数组for (int i = 0; i < n; i++) {arr[i] = i + 1;}    // 打印数组printf("数组元素:");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");   return 0;
}

6.2存储类别

C 语言中的存储类别定义了变量的作用域、生命周期和链接属性

①类别:

  • auto:默认存储类别,局部变量

  • static静态变量,生命周期为整个程序运行期间。

  • extern:声明外部变量,用于跨文件访问

  • register建议编译器将变量存储在寄存器中。

②作用

  • 控制变量的可见性和生命周期。

  • 优化内存使用和访问速度。

③例题

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 外部变量声明
extern int global;
// 静态全局变量
static int static_global = 10;
// 函数内部的静态变量
void test_static() {static int count = 0;count++;printf("静态变量 count = %d\n", count);
}
int main() {// 自动变量auto int a = 5;// 寄存器变量register int b = 10; printf("自动变量 a = %d\n", a);printf("寄存器变量 b = %d\n", b);// 调用函数测试静态变量test_static();test_static(); return 0;
}

6.3动态内存分配与存储类别的关系

动态内存分配与存储类别是两个不同的概念,但它们之间有一些联系:

  • 动态分配的内存存储在堆中,与存储类别无关。

  • 静态变量和全局变量存储在数据段中,自动变量存储在栈中。

  • 动态内存的生命周期由程序员控制,而存储类别的生命周期由语言规则控制。

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 全局变量
int *global_ptr;
void allocate_memory() {// 动态分配内存并赋值给全局指针global_ptr = (int *)malloc(sizeof(int));if (global_ptr == NULL) {printf("内存分配失败!\n");exit(1);}*global_ptr = 100;
}
int main() {// 调用函数分配内存allocate_memory();// 访问全局指针printf("全局指针指向的值:%d\n", *global_ptr);    // 释放内存free(global_ptr);   return 0;
}

7.ANSI C类型限定符

7.1 const类型限定符

①含义与作用


const 用于声明不可修改的变量,即常量。编译器会阻止对 const 变量的直接修改,增强程序的安全性和可读性。

②注意事项

  • const 变量必须在声明时初始化(除非是函数参数)。

  • 指针可以是 const(指向常量的指针)或本身是常量(常量指针),需注意区分:

const int* p;      // 指向常量的指针(数据不可变)
int* const p;      // 常量指针(地址不可变)
const int* const p; // 指向常量的常量指针(均不可变)
  • const 不能防止通过非 const 指针间接修改常量(需编译器支持)。

③例题

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {const int MAX = 100;// MAX = 200;  // 错误:无法修改const变量   int arr[5] = {1, 2, 3, 4, 5};const int* ptr = arr;// *ptr = 10;  // 错误:不能通过指针修改const数据ptr++;         // 允许:指针本身不是const  int* const cptr = arr;*cptr = 10;    // 允许:数据不是const// cptr++;    // 错误:不能修改const指针return 0;
}

7.2 volatile类型限定符

①含义与作用


volatile 告诉编译器变量的值可能以不可预测的方式被改变(如硬件自动更新、多线程共享),从而禁止编译器对该变量进行优化(如缓存到寄存器)。

②注意事项

  • 常用于:

    • 硬件寄存器访问(如嵌入式系统)。

    • 多线程环境中的共享变量。

    • 中断服务程序(ISR)中的全局变量。

  • volatile 不保证线程安全,需结合同步机制(如互斥锁)。

③例题

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 假设这是一个硬件状态寄存器
volatile int status_reg;
// 中断服务程序
void ISR(void) {status_reg = 1;  // 硬件可能随时修改此值
}
int main() {while (status_reg == 0) {// 循环等待,编译器不会优化此检查// 若没有volatile,编译器可能假设status_reg不变而陷入死循环}printf("Status changed!\n");return 0;
}

7.3 restrict类型限定符

①含义与作用:


restrict 用于指针,表示该指针是访问其所指向对象的唯一方式。编译器可据此进行激进的优化(如消除冗余内存访问)。

②注意事项

  • 仅用于指针类型。

  • 程序员必须确保:若指针被声明为 restrict,则在其作用域内,对象不会通过其他指针被访问。

  • 违反此约定会导致未定义行为。

③例题

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void add(int* restrict a, int* restrict b, int* restrict result) {*result = *a + *b;  // 编译器可优化内存访问
}
int main() {int x = 5, y = 10, sum;add(&x, &y, &sum);printf("Sum: %d\n", sum);  // 输出15// 错误示例(违反restrict约定)// int val = 100;// add(&val, &val, &val);  // 未定义行为return 0;
}

7.4 _Atomic类型限定符(C11)

①含义与作用


_Atomic 用于声明原子类型,确保对变量的操作是不可中断的,避免多线程环境中的数据竞争。

②注意事项

  • 需包含 <stdatomic.h>

  • 支持原子操作(如 atomic_fetch_add)。

  • 替代传统的锁机制,提供更高效的线程同步。

③例题

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>
atomic_int counter = 0;
int increment(void* arg) {for (int i = 0; i < 100000; i++) {atomic_fetch_add(&counter, 1);  // 原子递增}return 0;
}
int main() {thrd_t t1, t2;thrd_create(&t1, increment, NULL);thrd_create(&t2, increment, NULL);thrd_join(t1, NULL);thrd_join(t2, NULL);   printf("Counter: %d\n", counter);  // 输出200000(无竞争)return 0;
}

7.5 旧关键字的新位置

C11 允许在结构体 / 联合体成员声明中使用 _Atomic 作为类型说明符的一部分,例如:

struct {_Atomic int value;  // 原子整型成员
} atomic_struct;

这等价于:

struct {atomic_int value;
} atomic_struct;

7.6 总结

相关文章:

  • 17前端项目----支付弹框
  • Three.js + React 实战系列 - 页脚区域 Footer 组件 ✨
  • vector--OJ1
  • Windows系统更新一键禁用:WindowsUpdateBlocker轻量级工具推荐
  • Typescript 源码核心流程
  • LeetCode 热题 100 101. 对称二叉树
  • 79.评论日记
  • UOJ 164【清华集训2015】V Solution
  • 元数据分类
  • 并发笔记-给数据上锁(二)
  • 怎样选择成长股 读书笔记(一)
  • Linux epoll 详解:概念、使用、数据结构、流程及应用
  • 力扣-二叉树-101 对称二叉树
  • 常见的 DCGM 设备级别指标及其含义
  • 一个网球新手的学习心得
  • 【C语言文件操作详解】fopen 函数全解析 —— 模式参数、使用技巧与重定向的区别
  • 运动员技术等级分为国际级运动健将
  • C——猜数字游戏
  • RuoYi-v4.7.8 jar/war部署
  • n8n中订阅MQTT数据
  • 生态环境保护督察工作条例对督察对象和内容作了哪些规定?有关负责人答问
  • 历史地理学者成一农重回母校北京大学,担任历史系教授
  • 郎朗也来了,在辰山植物园“轻松听古典”
  • 2025年4月份CPI环比由降转涨,核心CPI涨幅稳定
  • 巴基斯坦称成功拦截印度导弹,空军所有资产安全
  • 四川资阳市原市长王善平被双开,“笃信风水,大搞迷信活动”