🔗 《C语言趣味教程》👈 猛戳订阅!!!
—— 热门专栏《维生素C语言》的重制版 ——
- 💭 写在前面:这是一套 C 语言趣味教学专栏,目前正在火热连载中,欢迎猛戳订阅!本专栏保证篇篇精品,继续保持本人一贯的幽默式写作风格,当然,在有趣的同时也同样会保证文章的质量,旨在能够产出 "有趣的干货" !本系列教程不管是零基础还是有基础的读者都可以阅读,可以先看看目录! 标题前带星号 (*) 的部分不建议初学者阅读,因为内容难免会超出当前章节的知识点,面向的是对 C 语言有一定基础或已经学过一遍的读者,初学者可自行选择跳过带星号的标题内容,等到后期再回过头来学习。值得一提的是,本专栏 强烈建议使用网页端阅读! 享受极度舒适的排版!你也可以展开目录,看看有没有你感兴趣的部分!希望需要学 C 语言的朋友可以耐下心来读一读。最后,可以订阅一下专栏防止找不到。
" 有趣的写作风格,还有特制的表情包,而且还干货满满!太下饭了!"
—— 沃兹基硕德
【C语言趣味教程】(7) 存储类:auto 关键字 | register 关键字 | 存储期 | 自动存储期 | 动态存储期 | 线程存储期 | 动态分配存储期 | 静态变量
📜 本章目录:
Ⅰ. 存储类(Storage Class)
0x00 引入:什么是存储类?
0x01 auto 关键字
0x01 注意:auto 只能修饰局部变量!
* 0x02 拓展阅读:C++ 中改版后的 auto
0x03 static 关键字初探
* 0x04 register 关键字
0x05 extern 关键字
Ⅱ. 存储期(Storage Duration)
0x00 引入:存储器的概念
0x01 自动存储期
0x02 静态存储期
* 0x03 动态分配存储期
* 0x04 线程存储期
Ⅲ. 静态变量(Static)
0x00 static 关键字
0x01 局部静态变量
0x02 全局静态变量
0x03 静态变量的初始值默认为0
Ⅰ. 存储类(Storage Class)
0x00 引入:什么是存储类?
❓ 你没有听说过存储类的概念?
存储类 (Storage Class) 在 C 语言标准中用来 规定变量与函数的可访问性与生命周期。
"可访问性" 的概念就是我们上一章说的作用域范围,我们先关注以下 4 种存储类别:
auto
static
register
extern
简单来说,存储类别定义了变量和函数的存储位置、生命周期和作用域。
为什么要引出存储类的概念?
大多数教程似乎并不涉及 "存储类" 的说法概念,讲解 auto, static 等关键字的时候都是直接介绍,而不引出存储类 (即 Storage Class) 的概念。
正因如此,我们想把存储类的概念单独抽出来作为一个章节去讲解,介绍存储类的概念,介绍一些常见的存储类别 (auto, staitc, register...) 。而不是单独的介绍这些关键字,孤立疏远它们的联系。
当然了,如果你是 C 语言初学者,一开始就接触存储类的概念大有裨益,利于体系化学习。如果你掌握 C语言基础,但是似乎之前没有听说过这个概念,也不用担心。像常见的 auto, extern, register, static 等关键字就属于 "存储类",通过本章的学习,可以体系化地了解这些东西,把它们归类起来。
0x01 auto 关键字
auto 是 "自动" 的意思,代表 变量在函数开始时自动创建,在函数结束时被自动销毁。
即使用 auto 修饰的变量,是具有自动存储器的局部变量。
auto int a = 0; // 表示a是一个自动存储类型,会在函数结束后自动销毁。
遗憾的是,大家都懒得去用它,这是为什么呢?
因为定义在函数中的所有变量都是自带 auto 的,即局部变量都是自带 auto 的。
💭 举个例子:
auto int a = 10; (A)
int a = 10; (B)
(A) 和 (B) 是一样的效果,我们不需要手动去加,因为 auto 是所有局部变量的默认存储类。
当使用 auto 修饰后,表示 a 是一个自动存储类型,它会在函数结束以后自动销毁。
0x01 注意:auto 只能修饰局部变量!
值得注意的是,auto 只能在函数内使用!这就意味着 auto 只能修饰局部变量。
❌ 错误演示:auto 修饰全局变量
#include <stdio.h>
auto int a = 10; // 全局变量
int main(void)
{
auto int b = 20; // 局部变量
return 0;
}
🚩 运行结果:error E0149
此时必然会触发报错和警告:warning C4042: “a”: 有坏的存储类
* 0x02 拓展阅读:C++ 中改版后的 auto
温馨提示:以下内容涉及 C++,读者可按自身情况阅读。
C++ 标准委员会觉得这 auto 也太尴尬了,我们得给它来一波加强。为了缓解 auto 的尴尬,C++ 标准委员会把 auto 原来的功能给废弃了。并赋予了 auto 全新的含义!
📚 游戏更新补丁(bushi):auto 现在不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器。auto 声明的变量必须由编译器在编译时推导而得。
也就是说,它可以自动推导出数据的类型:
int a = 0;
auto c = a; // C++11给auto关键字赋予了新的意义:自动推导c的类型
你的右边是什么,它就会推导出相应的类型,任何类型都可以实现,包括但不限于:
auto ch = 'A';
auto e = 10.11;
auto pa = &a;
为了方便测试,我们来打印一下对象的类型看看:
#include<iostream>
using namespace std;
int main()
{
int a = 0;
auto c = a; // 自动推导c的类型
auto ch = 'A';
auto e = 10.11;
auto pa = &a;
// typeid - 打印对象的类型
cout << typeid(c).name() << endl; // i
cout << typeid(ch).name() << endl; // c
cout << typeid(e).name() << endl; // d
cout << typeid(pa).name() << endl; // Pi
return 0;
}
🚩 运行结果如下:
emmm... 确实
这时候可能有人会觉得,这一波操作好像也没啥意义啊,
直接写数据类型不香吗?int c = a,我们继续往下看~
💭 举个例子:auto 的使用场景
处理又臭又长的数据类型
💬 遇到这种场景,就能体会到 auto 的香了:
#include <iostream>
#include <map>
int main(void)
{
std::map<std::string, std::string> dict = {{"sort", "排序"}, {"insert", "插入"}};
std::map<std::string, std::string>::iterator it = dict.begin();
// 这个类型又臭又长,写起来太麻烦了。。。
auto it = dict.begin(); // 可以改成这样,爽!
// ↑ 根据右边的返回值去自动推导it的类型,写起来就方便多了
return 0;
}
像遇到这种又臭又长的类型,而且还要经常使用。
这时候使用 auto 帮你自动推到类型,就很爽了!
📌 注意事项:使用 auto 是必须要给值的!
int i = 0;
auto j; ❌
auto j = i; 必须给值!!
这就意味着,auto 是不能做参数的!
auto 不能作为函数的参数
void TestAuto(auto a); ❌
此处代码编译失败,auto 不能作为形参类型,因为编译器无法对 a 的类型进行推导!
auto 不能直接用来声明数组
auto b[3] = {4,5,6}; ❌
📌 为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。
auto 在实际中最常见的优势用法就是 C++11 提供的新式 for 循环,
还有 lambda 表达式等进行配合使用。我们可以继续往下看~
auto 与指针结合起来使用:
📚 改版后的 auto 非常聪明,它在推导的时候其实是非常灵活的:
int main(void)
{
int x = 10;
auto a = &x; // int*
auto* b = &x; // int*
auto& c = x; // int
return 0;
}
在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型!
否则编译器将会报错,因为编译器实际只对第一个类型进行推导,
然后用推导出来的类型定义其他变量。
auto a = 1, b = 2;
auto c = 3, d = 4.0; ❌ 该行代码会编译失败,因为c和d的初始化表达式类型不同
0x03 static 关键字初探
static 是全局变量的默认存储类,它指示编译器在程序的生命周期内保持局部变量的存在。
static int a;
而不会像 auto 那样,每次进入和离开作用域时都会进行创建和销毁。
因此,我们可以使用 static 修饰局部变量在 "延长" 局部变量的生命周期。
让它能在函数调用之间保持局部变量的值。
static 也可以作用于全局变量,当 static 修饰全局变量时会让作用于提升至声明它的文件内。
静态变量的初始化:静态变量只被初始化一次,及时函数调用多次该变量的值也不会重置。
* 0x04 register 关键字
寄存器变量通常被存储在内存中,如果运气不错,寄存器变量就可以被存储在 CPU 寄存器中。
寄存器变量的访问速度比普通变量快的多。
而 register 关键字就是用来建议编译器把局部变量或函数的形参放入 CPU 的寄存器中的。
放到寄存器可以提高访问速度,如果一个变量比较常用,我们可以考虑加上 register 修饰:
register int count;
值得注意的是,register 对编译器只是一个 "建议",具体情况得看硬件和各种限制是否通过。
准确来说这是对编译器的一个请求,而不是一个命令,请求需要同意,命令无需同意。
这也是为什么在开头我说 "如果运气不错",用了 register 能不能放进去是要看天时地利人和的。
"天时地利人和,缺一不可。"
被 register 修饰的变量,编译器将尽可能地将其存入 CPU 的寄存器中。
并且,既然要存到寄存器中,那么 变量的字节长度应小于等于寄存器的长度。
局部变量存储在 RAM 中,而 register 可以建议编译器将某变量存到 CPU 寄存器中。
因此,如果一个变量需要频繁访问,可以使用 register 声明一下,提高程序的运行速度。
📌 注意事项:不能对 register 使用取地址 &,因为它没有内存位置。
0x05 extern 关键字
extern 关键字用于定义在其他文件中声明的全局变量或函数。
extern 让全局变量可以被各个对象模块访问。
使用 extern 关键字时,表示变量已经在别处定义,不能再初始化。
Ⅱ. 存储期(Storage Duration)
0x00 引入:存储器的概念
存储期描述了通过这些标识访问对象的生命周期,简单来说就是变量在内存中的 "存活期"。
C 语言中有 4 种存储期,分别是:
我们下面将逐个介绍它们,对于初学者来说只需做一个简单的了解即可。
0x01 自动存储期
我们一般在函数中创建的变量 (没被 static 定义的变量),都具有 "自动存储期"。
int main(void)
{
int a; // 具有自动存储期
}
具有自动存储期的变量,仅存在于当前代码块内(大括号)。
如果不给自动存储期的变量初始化,会自动初始化一个不确定的随机值。
0x02 静态存储期
函数中用 static 关键字定义出来的变量,或在函数外声明定义的对象都具有 "静态存储期"。
int A; // 具有静态存储期
int main(void)
{
static int b; // 具有静态存储期
}
具有静态存储期的变量,从程序开始结束该变量会一直存在,寿 (生命周期) 与天 (程序) 齐。
同时具备自动初始化为 0 的特性,即不给这个变量初始化,变量的初始值默认为 0。
* 0x03 动态分配存储期
C 语言中的动态存储期是指在程序运行时分配和释放内存的过程。
这种存储期允许程序在运行时根据需要来管理内存,以适应不同的数据结构和问题需求。
动态存储期主要通过 malloc
和 free 实现。
(该部分知识点将在动态内存分配章节补充讲解)
* 0x04 线程存储期
线程存储期 (Thread-Local Storage),它的生命周期是创建它的线程的整个执行过程。
比如并发中具有线程存储期的对象,在该线程开始执行时创建,在线程结束时销毁。
这意味着每个线程都拥有其自己的一组变量副本,这些副本在不同的线程中具有不同的值。
举个例子,我们使用线程存储期来存储线程特定的数据。
💬 代码演示:线程 ID 计数器
#include <stdio.h>
#include <pthread.h>
// 定义线程存储期变量
__thread int threadSpecificValue = 0;
// 线程执行的函数
void* threadFunction(void* arg) {
// 每个线程可以独立地修改 threadSpecificValue 的值
threadSpecificValue = threadSpecificValue + 1;
printf("Thread ID: %ld, \
threadSpecificValue: %d\n", \
pthread_self(), \
threadSpecificValue);
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 创建两个线程
pthread_create(&thread1, NULL, threadFunction, NULL);
pthread_create(&thread2, NULL, threadFunction, NULL);
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
💡 解读:我们定义了线程存储期变量 threadSpecificValue
,每个线程都有其自己的副本。可以看到在 threadFunction
中,每个线程独立地对 threadSpecificValue
进行递增操作,并打印出线程的 ID 以及变量的值。因为每个线程都有自己的变量副本,所以每个线程的输出是独立的。(这里用的 __thread
是 GCC 编译器的扩展,用于声明线程存储期变量)
使用存储类说明符声明标识符的对象
_Thread_local
(自C11开始)具有线程存储持续时间。它的生命周期是创建它的线程的整个执行过程,并且在线程启动时初始化它的存储值。每个线程都有一个独特的对象,并且在表达式中使用声明的名称是指与评估表达式的线程相关联的对象。尝试从与对象关联的线程以外的线程间接访问具有线程存储持续时间的对象的结果是实现定义的。
这边再多提一下,C++ 11 引入了 thread_local 关键字,用于声明线程局部变量。线程局部变量在每个线程中都有独立的实例,因此在不同线程中访问这些变量时不会相互干扰。
#include <stdio.h>
#include <threads.h>
thread_local int tls_var = 0;
int main() {
thrd_t thread;
thrd_create(&thread, another_thread, NULL);
tls_var = 42;
printf("Main thread TLS: %d\n", tls_var);
thrd_join(thread, NULL);
return 0;
}
int another_thread(void *arg) {
tls_var = 99;
printf("Another thread TLS: %d\n", tls_var);
return 0;
}
还可以使用 POSIX 线程局部存储函数,在 POSIX 线程库中,可以使用这些函数来创建和操作线程局部存储:
pthread_key_create()
pthread_setspecific()
pthread_getspecific()
线程局部存储允许每个线程维护一份独立的数据副本,这对于需要在线程之间保持独立状态的情况非常有用,例如线程特定的配置信息、日志句柄等操作。
Ⅲ. 静态变量(Static)
0x00 static 关键字
我们可以用 static 关键字来修饰一个变量为静态变量,修饰方法如下:
static 数据类型 变量名;
如果我们想让一个变量为静态变量,我们就在其数据类型前加一个 static 关键字就行了。
静态变量可以重复赋值,默认初始化的值为 0。
静态变量会被分配在静态存储区,静态变量在数据段中,函数退出后变量值不变。
上一章中我们学习了全局变量和局部变量,静态变量也是分全局和局部的。
分别是 静态局部变量 和 静态全局变量,下面我们将逐个介绍。
0x01 局部静态变量
静态局部变量的作用域和局部变量一致,在自己所处的代码块内有效。
但是生命周期被 "提升" 成全局的了,生命周期与全局变量一致,在整个程序运行期间有效。
0x02 全局静态变量
静态全局变量的作用域在定义它的源文件内。
它的生命周期和全局变量一致,在整个程序运行期间有效。
举个例子,如果我们在一个源文件中定义了静态全局变量,那么该变量只能在该源文件内使用。
0x03 静态变量的初始值默认为0
静态变量的初始值都是 0,静态局部变量和静态全局变量的初始值都是 0。
💬 代码演示:静态变量的初始值为 0
#include <stdio.h>
static A;
int main(void)
{
static a;
printf("静态局部变量 a = %d\n", a);
printf("静态局部变量 A = %d\n", a);
return 0;
}
🚩 运行结果如下:
📌 [ 笔者 ] 王亦优 | 雷向明
📃 [ 更新 ] 2023.8.27
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
本人也很想知道这些错误,恳望读者批评指正!
📜 参考文献: - C++reference[EB/OL]. []. http://www.cplusplus.com/reference/. - Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . - 百度百科[EB/OL]. []. https://baike.baidu.com/. - 维基百科[EB/OL]. []. https://zh.wikipedia.org/wiki/Wikipedia - R. Neapolitan, Foundations of Algorithms (5th ed.), Jones & Bartlett, 2015. - B. 比特科技. C/C++[EB/OL]. 2021[2021.8.31] - 林锐博士. 《高质量C/C++编程指南》[M]. 1.0. 电子工业, 2001.7.24. - 陈正冲. 《C语言深度解剖》[M]. 第三版. 北京航空航天大学出版社, 2019. - 侯捷. 《STL源码剖析》[M]. 华中科技大学出版社, 2002. - T. Cormen《算法导论》(第三版),麻省理工学院出版社,2009年。 - T. Roughgarden, Algorithms Illuminated, Part 1~3, Soundlikeyourself Publishing, 2018. - J. Kleinberg&E. Tardos, Algorithm Design, Addison Wesley, 2005. - R. Sedgewick&K. Wayne,《算法》(第四版),Addison-Wesley,2011 - S. Dasgupta,《算法》,McGraw-Hill教育出版社,2006。 - S. Baase&A. Van Gelder, Computer Algorithms: 设计与分析简介》,Addison Wesley,2000。 - E. Horowitz,《C语言中的数据结构基础》,计算机科学出版社,1993 - S. Skiena, The Algorithm Design Manual (2nd ed.), Springer, 2008. - A. Aho, J. Hopcroft, and J. Ullman, Design and Analysis of Algorithms, Addison-Wesley, 1974. - M. Weiss, Data Structure and Algorithm Analysis in C (2nd ed.), Pearson, 1997. - A. Levitin, Introduction to the Design and Analysis of Algorithms, Addison Wesley, 2003. - A. Aho, J. - E. Horowitz, S. Sahni and S. Rajasekaran, Computer Algorithms/C++, Computer Science Press, 1997. - R. Sedgewick, Algorithms in C: 第1-4部分(第三版),Addison-Wesley,1998 - R. Sedgewick,《C语言中的算法》。第5部分(第3版),Addison-Wesley,2002 |