前言

这节课是最后一节了,在最后一节课里,老师给我们介绍了如何存储多个字符串、一些字符串存储函数、数据存储类别的相关知识。至此,这个系列课程部分就到此结束了,后面还有几个专题,敬请期待。


字符串存储

二维数组的存储原理

说到字符串存储,我们知道C语言中用字符数组存储字符串,而在C++中还有一种string类可以单独定义字符串。那么我们有很多字符串的时候该怎么储存呢?我们要用到二维数组来存储,在此之前,我们先介绍一下二维数组的存储原理。

二维数组也许在大家的认知中是形如矩阵的:

或者是像二维坐标系一样,但实际上在计算机存储的时候不是大家所想的这样的。

数组在计算机里就是一串数列的存储方法,你可能以为rain[0][0]和rain[1][0]离得很近,但其实上它们隔得很远,它们之间正好隔了12个。

意思是,这个数组在计算机中存储是连续的,即:

rain[0][0]-rain[0][1]-rain[0][2]-...-rain[0][11]-rain[1][0]-...

这样的方式和我们默认的不太一样。


二维数组存储字符串

因此我们用二维数组存储字符串时,实际上是把几个字符串拼在了一起,用'\0'来隔开。

例如:

char fruit[3][7] = {"Apple", "Pear", "Orange");

我们定义了二维字符数组,第一维存储字符串的个数,第二维存储各个字符串。

这是一种非常常用的存储二维数组的方式,而且调用各个字符串的时候可以通过编号来调用,也挺方便的。

但是当我们要比较字符串的大小的时候,我们要更改两个字符串的位置,这就很麻烦了,那我们有什么改进方法呢?


指针存储字符串

交换两个物体本身可能操作的步骤比较麻烦,特别是字符串。那我们有什么办法可以简化这个过程呢,就是交换他们的地址。地址相当于编号,我们只是存储并交换了他们的地址这样的操作就大大简化了运算过程。

所以,我们可以用指针字符数组存储字符串:

char * fruit[3] = {"Apple", "Pear", "Orange");

我们相当于是用指针存储了这些字符串,我们排序的时候只需要交换他们的指针就可以了。

但是可能编译器会报错,因为一般这样存储的字符串是不会被修改的,我们一般写成:

const char * fruit[3] = {"Apple", "Pear", "Orange");

注意:此时[]表示的是个数,不是字符串的长度。因为其存储的是地址,而不是字符串。


两种存储方式的比较

这两种存储方式的声明不一样,第一种我们需要知道最长的字符串有多长,第二种就不用,按照需要来选择这两种存储方式。

对比图:


字符串转换函数

有些时候我们会把字符串转换成数字等类型来处理,也可能会把数字等其他类型转换成字符串,我们在<stdlib.h>中有一些函数可以帮我们做到这些功能。

double atof(const char *nptr);
将字符串转换成双精度浮点数
int atoi(const char *nptr);
将字符串转换成整形数
long atol(const char *nptr); 将字符串转换成长整型数
double strtod(const char *nptr, char **endptr);
将字符串转换成双精度浮点数
long int strtol(const char *nptr, char **endptr, int base); 将字符串转换成长整型数
unsigned long int strtoul(const char *nptr, char **endptr, int base); 将字符串转换成无符号长整型数
char *gcvt(double number, int ndigit, char *buf); 将浮点数转换成字符串,取四舍五入
itoa() 将整型值转换为字符串。
ltoa() 将长整型值转换为字符串。
ultoa() 将无符号长整型值转换为字符串。
gcvt() 将浮点型数转换为字符串,取四舍五入。

一般来说,最常用的就是:atoi()和itoa()了,还有挺多的大家需要读下定义表来使用这些函数。

字符串与整数的转换还可以直接加减ASCII码来做到,例如:

字符串转数字:例如字符"1"

char ch='1';
int a;
a=ch-'0';

实际上就是1的ASCII码值减去0的ASCII码值,把'1'转化成了1.

当然我们也可以逆过来:

char ch;
int a=1;
ch=(char)a;//强制类型转换

但是这时要用强制类型转换。


存储类别

这一部分专业性词语比较多,仅是解释了原理,如果仅为实践(非类计算机专业),这部分可以不看了。

我们知道我们定义的变量、常量等是存储在某一个位置,C语言提供了多种不同的模型或存储类别(storage class)在内存中储存数据。

被储存的每个值都占用一定的物理内存, C语言把这样的一块内存称为对象(object)。对象可以储存一个或多个值。一个对象可能并未储存实际的值, 但是它在储存适当的值时一定具有相应的大小。

我们定义变量,例如:

int a=1;

该声明创建了一个名为a的标识符(identifier)。标识符是一个名称, 在这种情况下, 标识符可以用来指定(designate)特定对象的内容,此时被指定为了对象1。

而其中:可以用存储期(storage duration)描述对象, 所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象, 可以用作用域(scope) 和链接(linkage)描述标识符, 标识符的作用域和链接表明了程序的哪些部分可以使用它。不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用, 甚至只在函数中的某部分使用。对象可存在于程序的执行期, 也可以仅存在于它所在函数的执行期。对于并发编程, 对象可以在特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存。


作用域

作用域(scope)描述程序中可访问标识符的区域。一个C 变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。

块作用域

块(block)是用一对花括号括起来的代码区域。例如, 整个函数体是一个块, 函数中的任意复合语句也是一个块。定义在块中的变量具有块作用域(block scope), 块作用域变量的可见范围是从定义处到包含该定义的块的末尾。

简单来说就是被两个花括号包括的部分是块作用域。在其中定义的变量只在花括号内有效。

在C99以后规定,for循环、while循环、do...while循环、if条件判断语句等所控制的代码,即使这些代码没有用花括号括起来, 也算是块的一部分。

例如:

for (int i = 0; i < 10; i++)

其中的i只在这个for循环中有效。


函数作用域

函数作用域(function scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中, 它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。

例如:

int a;
scanf("a");
LABLE: printf("hahaha\n");
if(a==1)
{
    a=1;
    goto LABLE;
}
else printf("haha\n");

 此时,goto的标签作用范围就是整个函数,故它可以往前面跳。


函数原型作用域

函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名)。函数原型作用域的范围是从形参定义处到原型声明结束。这意味着, 编译器在处理函数原型中的形参时只关心它的类型, 而形参名(如果有的话)通常无关紧要。

简单来说就是定义在函数声明时的变量只在函数内有效。

例如:
int mighty(int mouse, double large);

其中的mouse和large只在mighty这个函数里有效。


文件作用域

具有文件作用域(file scope)的变量定义在函数的外面,具有文件作用域的变量, 从它的定义处到该定义所在文件的末尾均可见。

简单来说就是全局变量的意思。


链接

C变量有3种链接属性: 外部链接、内部链接或无链接。具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。

外部链接变量可以在多文件程序中使用, 内部链接变量只能在一个翻译单元中使用。

外部链接和内部链接的区分:有无static标签。例如:

int giants = 5; //文件作用域, 外部链接
static int dodgers = 3; //文件作用域, 内部链接

如果你只编译一个文件,那其实也不必要专门写内部链接。内部链接在多文件时仅能被当前文件使用。


存储期

作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期: 静态存储期、线程存储期、自动存储期、动态分配存储期。

静态存储期

如果对象具有静态存储期(static storage duration), 那么它在程序的执行期间一直存在。文件作用域变量具有静态存储期。存储类型说明符(static)用于声明对象拥有静态存储期(static storage duration)。

static这个词是一个一词多义,对于文件作用域变量, 关键字static表明了其链接属性, 而非存储期。以static声明的文件作用域变量具有内部链接。但是无论是内部链接还是外部链接, 所有的文件作用域变量都具有静态存储期。

线程存储期

线程存储期用于并发程序设计, 程序执行可被分为多个线程。具有线程存储期的对象, 从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时, 每个线程都获得该变量的私有备份。

自动存储期

块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时, 为这些变量分配内存;当退出这个块时, 释放刚才为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。

简单来说就是用完即焚,再用再生。


存储类别

C使用作用域、链接和存储期为变量定义了多种存储方案。介绍完了上面那些,我们的主角:5种存储类别: 自动、寄存器、静态块作用域、静态外部链接、静态内部链接才登场。


自动变量

属于自动存储类别的变量具有自动存储期、块作用域且无链接。默认情况下, 声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚地表达你的意图(例如, 为了表明有意稷盖一个外部变量定义,或者强调不要把该变量改为其他存储类别), 可以显式使用关键字auto.

例如:

auto int plox;

关键字auto是存储类别说明符(storage-class specifier)。auto关键字在C++中的用法完全不同, 如果编写C/C++兼容的程序, 最好不要使用auto作为存储类别说明符。其实我们用得真的不多,没有必要多加一个auto。


寄存器变量

变量通常储存在计算机内存中。寄存器变量储存在CPU的寄存器中, 或者概括地说,储存在最快的可用内存中。

与普通变量相比, 访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中, 所以无法获取寄存器变量的地址。绝大多数方面, 寄存器变量和自动变量都一样。也就是说, 它们都是块作用域、无链接和自动存储期。使用存储类别说明符register 便可声明寄存器变量。

例如:

register int quick;

可声明为register的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来储存double类型的值。


静态无链接

在块中(提供块作用域和无链接)以存储类别说明符static(提供静态存储期)声明这种变量。具有块作用域,但它的存储期是静态的,也就是说,在该变量被创建后,程序停止运行才会释放该变量。

例如:

static int stay = 1;

但是,不能在函数的形参中使用static:
int wontwork(static int flu); //不允许


外部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class),属于该类别的变量称为外部变量(external variable) 。把变量的定义性声明(defining declaration)放在在所有函数的外面便创建了外部变量。

为了指出该函数使用了外部变量, 可以在函数中用关键字extern 再次声明。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。

例如:

extern char Coal;//如果Coal 被定义在另一个文件,则必须这样声明


内部链接的静态变置

该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类别说明符static 定义的变量具有这种存储类别。

static int svil = l;//静态变量, 内部链接
这种变量也称为外部静态变量(external static variable)。

内部链接的静态变量只能用于同一个文件中的函数。可以使用存储类别说明符extern, 在函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性。


五种存储类别的区别

自动:

默认的存储类型,最早接触,通常不用关键字,在需要与其他类型做区分时,可以加上auto关键词.在块作用域中声明,具有自动存储期,即程序进入该块时变量存在,退出该块时变量被销毁.

寄存器:

关键字为register,其性质与自动变量很相似,块作用域,自动存储期.但是因为它被存储在寄存器中而不是内存中,所以无法获取它的地址.同时要注意的是,声明寄存器变量只是一种请求,最终是否将该变量存在寄存器中则是由编译器来决定.但无论是否被存入寄存器,该变量都无法获取地址.

静态无链接:

在块内用关键字static声明变量,具有块作用域,但它的存储期是静态的,也就是说,在该变量被创建后,程序停止运行才会释放该变量.在循环中,但程序再一次运行到该声明时,会跳过去.自动初始化为0.

静态外部链接:

作为外部变量作用域自然是文件,静态存储期,感觉叫全局变量比较舒服,也比较熟悉,这种变量在所有块外面声明,可以在同一个程序不同翻译单元(很奇怪的名字,反正就是不同的文件)中作用.如果要在一个文件中使用另一个文件的全局变量,要用关键字extern.自动初始化为0.

静态内部链接:

这个与全局变量的区别在于声明时需要加上static关键字,同时无法被其他文件使用.自动初始化为0.


这里static的含义并不相同,静态无链接时意为存储期是静态的,而静态内部链接中意为不能在文件之间共享。

我们用一张图来概括它们在内存或寄存器中的位置:


 

这里的一切都有始有终,却能容纳所有的不期而遇和久别重逢。
最后更新于 2024-01-14