我院新闻
<<c和指针>>温故及问题研讨(第三章)
发布时间:2024-01-18

第三章-数据


1. 前言

这篇文章向大家分享<>第三章的内容–数据,这篇文章在专栏:书籍分享中,有兴趣阅读更多关于<>的朋友可以来浏览一下.和之前一样,我分享的内容是我认为容易被忽略的点,和C语言中更接近于底层的一些逻辑.有些内容我在C语言的学习中已经分享过了的,我会给出之前的博客链接并且跳过这一段.



2. 基本数据类型

2.1 整型家族

我们在之前的分享中已经大致介绍过了整型家族c语言分享,这里我再做一点补充:首先是我们的范围问题,我们给出所有整型家族变量的范围:


我们在阅读一些文献的时候,时常会遇见一个返回类型: size_t ,比如我们在cplusplus上搜索strlen函数时:

这里的 size_t 等同于 unsigned int .



2.2 字面值常量

这里我们引出字面值常量的概念,因为后面又很多定义都会涉及到这个 “字面值常量”.
字面值这个术语是字面值常量的缩写——这是一种实体,指定了自身的值,并且不允许发生改变。这个特点非常重要,因为ANSI C允许命名常量(named constant,声明为const的变量)的创建,它与普通变量极为类似。
区别在于,当它被初始化以后,它的值便不能改变



3. 基本声明

3.1 数组的声明以及引用

在我们声明一个数组values之后

int values[20]={0};

数组的下标总是从0开始,最后一个元素的下标是元素的数目减1。我们没有办法修改这个属性,但如果你一定要让某个数组的下标从10开始,那也并不困难,只要在实际引用时把下标值减去10即可。

C数组另一个值得关注的地方是: 编译器并不检查程序对数组下标的引用是否在数组的合法范围之内。 这种不加检查的行为有好处也有坏处。好处是不需要浪费时间对有些已知是正确的数组下标进行检查。坏处是这样做将使无效的下标引用无法被检测出来。 所以我们可以养成一个良好的习惯:在使用数组下标前进行检查,确保它们位于有效范围之内



3.2 指针的声明注意事项

当我们这样定义指针变量时:

int* b,c,d;

人们很自然地以为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此。我们被它的形式愚弄了。 星号实际上是表达式 * b的一部分,只对这个标识符有用。 这里只有b是一个指针,但其余两个变量只是普通的整型。要声明三个指针,正确的语句如下

int* b,*c,*d;


3.3 隐式声明

C语言中有几种声明,它的类型名可以省略。例如,==函数如果不显式地声明返回值的类型,它就默认返回整型。当你使用旧风格声明函数的形式参数时,如果省略了参数的类型,编译器就会默认它们为整型。==比如这个地方:

f(x)
{
   return x+1; 
}

函数f缺少返回类型,于是编译器就默认它返回整型。参数x也没有类型名,同样被默认为整型。

提醒:依赖隐式声明可不是一个好主意。隐式声明总会在读者的头脑中留下疑问:是有意遗漏类型名呢?还是不小心忘记写了?显式声明就能够清楚地表达你的意图



4. 常量

我们之前提过用const修饰的变量会变成常量,当我们这里涉及到指针的时候就容易混淆起来, 因为有两样东西都有可能成为常量–指针变量和和它所指向的实体,下面是几个声明的例子 :

int* pi;//这里pi是普通的指针变量
int const *pi;//这里pi是一个指向整型常量的指针.你可以修改指针的值,但是不能修改它所指向的值
int* const pi;//这里pi是一个指向整型的常量指针.此时指针是常量,它的值无法修改,但是我可以修改它指向的整型值
int const *const pi;//最后这里无论是指针本身还是它指向的值都是常量都不能修改.

这四种是完全不一样的定义方式,看到这儿你可能会有一点迷糊,下面我们用一段代码来演示一下:

int x=1,y=2,z=3;
int const *a=&x;
 a=&y;//这种写法是允许的
 *a=2;//这种写法是不允许的
int *const b=&y;
 b=&x;//这种写法是不允许的
 *b=1;//这种写法是允许的
 int const *const c=&z;//这种写法都不能变


5. 作用域

当变量在程序的某个部分被声明时,它只有在程序的一定区域才能被访问。这个区域由标识符的作用域(scope)决定。标识符的作用域就是程序中该标识符可以被使用的区域。 例如,函数的局部变量的作用域局限于该函数的函数体 这个规则意味着两点。首先,其他函数都无法通过这些变量的名字访问它们,因为这些变量在它们的作用域之外便不再有效。其次,只要分属不同的作用域,你可以给不同的变量起同一个名字。

编译器可以确认4种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。 我们下面给出这些作用域的定义,后面我们会用到



5.1 代码块作用域

我们经常听说一个词:代码块,代码块.那么代码块到底是什么?这里给出它的定义,后面会经常看见它:

位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域(block scope),表示它们可以被这个代码块中的所有语句访问。
这里包括我们的分支与循环语句的{},函数的{},甚至于main函数的{},以及你自己写出来的{}.

在K&R C中,函数形参的作用域开始于形参的声明处,位于函数体之外。如果在函数体内部声明了名字与形参相同的局部变量,它们就将隐藏形参。

但是ANSI C扼止了这种错误的可能性,它把形参的作用域设定为函数最外层的那个作用域(也就是整个函数体)。这样,声明于函数最外层作用域的局部变量无法和形参同名,因为它们的作用域相同。 也就是说我们的函数的形参名可以和实参名相同

如果你想深入了解K&R C和ANSL C,请跳转K&R C与ANSI C的区别



5.2 文件作用域

任何在所有代码块之外声明的标识符都具有文件作用域(file scope), 它表示 这些标识符从它们的声明之处直到它所在的源文件结尾处都是可以访问的。 在文件中定义的函数名也具有文件作用域,因为函数名本身并不属于任何代码块。我应该指出,在头文件中编写并通过#include指令包含到其他文件中的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。 ,也就是说我们包含的头文件的作用域是在我们自己写的文件当中结束.



5.3 原型作用域

原型作用域(prototype scope)只适用于在函数原型中声明的参数名。 在原型中(与函数的定义不同),参数的名字并非必需。但是,如果出现参数名,你可以随你所愿给它们取任何名字,它们不必与函数定义中的形参名匹配,也不必与函数实际调用时所传递的实参匹配。原型作用域防止这些参数名与程序其他部分的名字冲突。事实上,唯一可能出现的冲突就是在同一个原型中不止一次地使用同一个名字。



6. 链接属性

链接属性一共有3种——external(外部)、internal(内部)和none(无)。没有链接属性的标识符(none)总是被当作单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。 属于internal链接属性的标识符在同一个源文件内的所有声明中都指同一个实体,但位于不同源文件的多个声明则分属不同的实体 最后,属于external链接属性的标识符不论声明多少次、位于几个源文件都表示同一个实体。 ,我们用一个图来进一步说明:


6.1 链接属性分类以及作用范围

图3.2的程序骨架通过展示名字声明的所有不同方式,描述了链接属性。在缺省情况下,标识符b、c和f的链接属性为external,其余标识符的链接属性则为none因此,如果另一个源文件也包含了标识符b的类似声明并调用函数c,它们实际上访问的是这个源文件所定义的实体。 f的链接属性之所以是external是因为它是个函数名。在这个源文件中调用函数f,它实际上将链接到其他源文件所定义的函数,甚至这个函数的定义可能出现在某个函数库。


6.2 关键字:extern和static

再一个:关键字 extern 和 static 用于在声明中修改标识符的链接属性。 如果某个声明在正常情况下具有external链接属性,在它前面加上static关键字可以使它的链接属性变为internal。 例如,如果第2个声明像下面这样书写:

6.21 static

static int b;

那么变量b就将为这个源文件所私有.在其他源文件中,如果也链接到一个叫做b的变量,那么它所引用的是宁外iyig不同的变量.类似的你也可以把函数声明为static,如下:

static int c(int d)

这样可以防止它被其他源文件调用

值得注意的是static只对缺省链接属性为external的声明才有改变链接属性的效果。例如,尽管你可以在声明5前面加上static关键字,但它的效果完全不一样,因为e的缺省链接属性并不是external。

6.22 extern

extern关键字的规则更为复杂。一般而言,它为一个标识符指定external链接属性,这样就可以访问在其他任何位置定义的这个实体。请考虑图3.3的例子。 声明3为k指定external链接属性。这样一来,函数就可以访问在其他源文件声明的外部变量了。也就是可以访问其他源文件中定义的k

当extern关键字用于源文件中一个标识符的第1次声明时,它指定该标识符具有external链接属性。但是,如果它用于该标识符的第2次或以后的声明时,它并不会更改由第1次声明所指定的链接属性。 例如,图3.3中的声明4并不修改由声明1所指定的变量i的链接属性。


6.3 static关键字的总结

当用于不同的上下文环境时,static关键字具有不同的意思。确实很不幸,因为这总是给C程序员新手带来混淆。本节对static关键字作了总结,应该能够帮助你搞清这个问题。

  1. 当它用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。

  1. 当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。


7. 总结

  1. 在声明指针变量时不要采用容易误解的写法
  2. 不要误解指针声明中初始化的含义
  3. 不要依赖隐式声明
  4. 用const声明其值不会改变的变量
  5. 不要在嵌套的代码块之间使用相同的变量名
  6. 除了实体的具体定义位置外,在它的其他声明位置都要使用extern关键字
[返回上级]