小伙伴们大家好啊,丸丸答应你们的《C和指针》系列已经开始更新了,今天是第一弹,就是本书的前三章,后续会持续更新,相比《C陷阱与缺陷》,这本书同样也有自己的独到之处,相信对专精C/C++得人会有比较大的帮助,当然,这本书也是C/C++方向从业人员得必备书籍,丸丸会把此书的独到之处帮大家进行总结出来,同时呢,丸丸也会把一些触类旁通的知识也相应的列举总结出来,同时上一些代码帮助大家进行理解,在后续的更新中呢,丸丸也会把一些自己的理解加上,当然,也会尽可能地保持书的原汁原味,有得内容确实是直接将其赋值黏贴上了,哈哈,至于原因嘛,因为那些内容太硬核了,实在无法精简了,大家尽情食用吧!希望大家尽早观看呀,因为后续这个专栏有一定概率会被设为收费专栏(当然,概率比较小),不过还是希望大家尽早观看,因为内容真的很棒呀(自己夸自己不过分吧,嘿嘿)!同时,丸丸感谢大家一路以来得支持,丸丸会持续进行输出C/C++方面的书籍,帮大家进行总结,希望大家一定要多多支持丸丸呀,你们的支持是丸丸更新最大的动力!❤️❤️❤️
gets函数从标准输入读取一行文本并把它存储于作为参数传递给它的数组中。==一行输入由一串字符组成,以一个换行符结尾。==gets函数会丢弃换行符,并在该行的末尾存储一个NUL字节(一个NUL字符是指字节模式为全0的字节,类似’\0’这样的字符常量)。然后,gets函数返回一个非NULL值,表示该行已经被成功读取。当gets函数被调用但事实上不存在输入行时,它就返回NULL值,表示它到达了输入的末尾(文件尾)。
int ch = 0;
while((ch = getchar())!=EOF)
{
···
}
把ch声明为整型可以防止从输入读取的字符意外的被解释为EOF。但同时,这也意味着接收字符的ch必须足够大。
问:下面的语句存在什么问题?
while(gets(input)!=NULL)
{
···
}
答:当一个数组作为函数的参数进行传递时,函数无法知道它的长度。因此,gets函数没有办法防止一个非常长的输入行,从而导致input数组溢出。fgets函数要求数组的长度作为参数传递给它,因此不存在这个问题。
如果我们在编译程序的命令行中加入了要求进行优化的选项,优化器就会对目标代码进一步进行处理,使它效率更高。优化过程需要额外的时间,所以在程序调试完毕并准别生成正式产品之前一般不进行这个过程。
注意:当编译的源文件超过一个时,目标文件便不会被删除。这就允许你对程序进行修改后,只对那些进行过改动的源文件进行重新编译。如下面命令所示:
cc main.c sort.c lookup.c//第一次编译
cc main.o sort.c lookup.o//第二次编译,只有sort.c文件发生了修改
程序的执行过程也经历几个阶段。
首先,程序必须载入到内存中。在宿主环境中(也就是操作系统的环境),这个任务由操作系统完成。那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到初始值。在独立环境中,程序的载入必须由手工安排,也可能是通过把可执行代码置入只读内存(ROM)来完成。
宿主环境和独立环境的区分
宿主环境:有操作系统,程序的加载、执行和终止通常要受操作系统的控制和调度,并允许使用操作系统提供的各种功能和组件,比如文件系统。举例Windows连同运行它的硬件就构成了宿主式环境。
独立环境:没有操作系统,程序能独立自主地执行。典型的例子包括仪器仪表的固件、控制器等嵌入式领域里的设备。
然后,程序的执行便开始。在宿主环境中,通常一个小型的启动程序于程序链接在一起。它负责处理一系列日常事务,如收集命名行参数以便程序能够访问它们。接着,便调用main函数。
现在,开始执行程序代码。
三字母词
三字母词原型 | 三字母词实际含义 |
---|---|
??( | [ |
??) | ] |
??! | | |
??< | { |
??> | } |
??’ | ^ |
??= | # |
??/ | \ |
??- | ~ |
两个问号开头再尾随一个字符的形式一般不会出现在其它表达形式中,所以把三字母词用这种形式来表示,这样就不致引起误解。
当然,现在在比较新的C语言编译器中已经取消了三字母词,一般只会在比较老的编译器中看到。
问:\40的值是多少?\100、\x100、\0123、\x0123的值又分别是多少?
答:假定系统使用的是ASCII字符集,则存在下面的相等关系。
\40 = 32 = 空格字符(八进制)
\100 = 64 = ‘@’
\x40 = 64
\x100 占据12位(尽管前3位为0),在绝大多数机器上,这个值过于庞大,无法存储一个字符内,所以它的结果因编译器而异。
\0123由两个字符组成:‘\012’和’3’,其结果因编译器而异。
\x0123过于庞大,无法存储于一个字符内,其结果值因编译器而异。
问:假定你有一个C程序,它由几个单独的文件组成,而这几个文件由分别包含了其它文件。如下所示:
文件 | 包含文件 |
---|---|
main.c | stdio.h、table.h |
list.c | list.h |
symbol.c | symbol.h |
table.c | table.h |
table.h | symbol.h、list.h |
如果对list.c做了修改,应该用什么命令进行重新编译?如果是对list.h或者table.h做了修改,又应该分别使用什么命令?
答:当一个头文件被修改时,所有包含它的文件都必须重新编译。这也是为什么我们重复执行同一份代码时速度为什么快的原因,因为没有修改的代码无需重新编译,编译本身也是需要花费一定时间的。
如果这个文件被修改 | 这些文件必须重新编译 |
---|---|
list.c | list.c |
list.h | list.c,table.c,main.c |
table.h | table.c,main.c |
长整型至少应该和整型一样长,而整型至少应该和短整型一样长。
缺省的char要么是signed char,要么是unsigned char,这取决于编译器。这个事实意味着不同机器上的char可能拥有不同范围的值。
八进制和十六进制字面值可能的类型是int、unsigned int、long或unsigned long。在缺省情况下,字面值的类型就是上述类型中最短但足以容纳整个值得类型。
另外还有字符常量。它们的类型总是int。你不能在它们的后面添加unsigned或long后缀。
字符常量就是一个用单引号包围起来得单个字符(或字符转义序列或三字母词)。
注意:现在较新的编译器都无法使用三字母为字符常量进行赋值了。
如果一个多字节字符常量的前面有一个L,那么它就是宽字符常量。如:
L'X'
L'e^'
当运行环境支持一种宽字符集时,就有可能使用它们。
如果一个值被当作字符使用,那么把这个值表示为字符常量可以使这个值的意思更为清晰。例如,下面三种形式表达的意义是一样的:
value = value - 48; value = value - \60; value = value - '0';
但是第三行的含义更为清晰,它用于表示把一个字符转换为二进制值。更为重要的是,不管所采用的是何种字符集,使用字符常量所产生的总是正确的值,所以它能提高程序的可移植性。
枚举类型就是指它的值为符号常量而不是字面值的类型。
enum color
{
RED,
GREEN,
BLUE
};
enum color color1 = RED;
color1 = 100;//给枚举类型的变量赋字面值
int a = RED;//给普通变量赋枚举常量值
注意:符号名RED被当作整型常量处理,声明为枚举类型的变量实际上是整数类型。这个事实意味着可以给上述enum color
类型的变量赋诸如100这样的字面值,也可以把RED赋给任何整型变量。但是,要避免以这种方式使用枚举,因为把枚举变量同整数无差别地混在一起使用,会削弱它们值的含义。
ANSI标准规定long double
至少和double
一样长,而double
至少和float
一样长。标准同时规定了一个最小范围:浮点类型至少能够容纳10-37~1037之间的任何值。
字符串常量
注意:由于NUL字节是用于终结字符串的,所以在字符串内部不能有NUL字节。不过,在一般情况下,这个限制并不会造成问题。之所以选择NUL作为字符串内部的终止符,是因为它不是一个可打印的字符。
C数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围之内。这种不加检查的行为也有好处也有坏处。好处是不需要浪费时间对有些已知是正确的数组下标进行检查。坏处是这样做将无法检查出无效的下标引用。一个良好的经验法则是:
如果下标是从那些已知是正确的值计算得来,那么就无需检查它的值。如果一个用作下标的值是根据某些方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测,确保它们位于有效的范围之内。
C在本质上是一种自由形式的语言,这很容易诱使你把星号写在靠近类型的一侧,如下所示:
int* a;
这个星号与前面一个声明具有相同的意思,而且看上去更为清楚,a被声明为类型为int*的指针。但是,这不是一个好技巧,原因如下:
int* b, c, d;
人们很自然的以为这条语句把所有3个变量声明为指向整型的指针,但事实上并非如此。星号实际上是表达式*b的一部分,只对这个标识符有用。b是一个指针,但其余两个变量只是普通的整型。要声明3个指针,正确的语句如下:
int *b, *c, *d;
int a[10];
int c;
b[10];
d;
f(x)
{
return x+1;
}
第三行和第四行是在ANSI C中是非法的。第三行缺少类型名,但对于K&R编译器而言,它已经拥有足够的信息判断出这条语句是一个声明。令人惊奇的是,有些K&R编译器还能正确的把第4行也按照声明进行处理。函数f缺少返回类型,于是编译器就默认它返回整型。参数x也没有类型名,同样被默认为整型。
注意:不建议使用隐式声明!
位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域,表示它们可以被这个代码块中的所有语句访问。但是一旦跨出代码块,代码块中的变量就无法被访问例如:
#include
int main()
{
{
int a = 10;
}
printf("%d",a);
}
上述代码就会无法运行,提示a未定义,因为a的代码块中的变量,它的作用域只在代码块中,而printf函数在代码块外使用a变量,所以肯定无法运行的。
注意:如果在函数体内部声明了名字与形参相同的局部变量,它们就将隐藏形参。ANSI C把形参的作用域设定为函数最外层的那个作用域(也就是整个函数体)。这样,声明于函数最外层作用域的局部变量无法和形参同名,因为它们的作用域相同。
举例表示:
#include
int fun(int a)
{
int a = 5;
}
int main()
{
int a = 10;
fun(a);
return 0;
}
这样就无法正常运行,出现形参a重定义的现象,但是如果像下面这样写,就不会有问题:
#include
void fun(int a)
{
{
int a = 5;
printf("%d", a);
}
}
int main()
{
int a = 10;
f(a);
return 0;
}
任何在所有代码之外声明的标识符都具有文件作用域,它表示这些标识符从它们的声明之处直到它所在的源文件 结尾处都是可以访问的。
原型作用域只适合在函数原型中声明的参数名。
函数作用域只适用于语句标签,语句标签用于goto语句。基本上,函数作用域可以简化为一条规则——一个函数中的所有语句标签都必须唯一。
标识符的链接属性决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关,但这两个属性并不相同。
链接属性一共有三种——external(外部)、internal(内部)和none(无)。没有链接属性的标识符总是被当作单独的个体,也就是或该标识符的多个声明被当作不同的独立实体。属于internal链接属性的标识符在同一个源文件内的所有声明中都指向同一个实体,但位于不同源文件中的多个声明则分属不同的实体。最后,属于external链接属性的标识符不论声明多少次,位于几个源文件都表示同一个实体。
注意extern的使用:
extern int c;//此处只是告诉编译器c在其它源文件中已经存在,并引入当前源文件,此句不能当作定义进行使用,在编译文件时并没有c这个全局变量 extern int c = 10;//定义并声明变量c,并表明c的属性是外部链接属性,此时 可以在其它源文件中引用并使用
再次区分下面的代码:
int c;
此时是定义了一个全局变量c并且有默认的初始化值0,可以正常使用。
下面是一个例子:
typedef char *a;
int b;
int c(int d)
{
int e;
int f(int g);
}
注意:函数可以嵌套使用,但是不能嵌套定义!
在缺省情况下,标识符b、c和f的链接属性为external,其余标识符的链接属性则为none。因此,如果另一个源文件也包含了标识符b的类似声明并调用函数c,它们实际上访问的是这个源文件所定义的实体。f的链接属性之所以是external,是因为它是个函数名,它们实际上访问的是源文件所定义的实体。f的链接属性之所以是external,是因为它是个函数名。在这个源文件中调用函数f,它实际上将链接到其它源文件所定义的函数,甚至这个函数的定义可能出现在某个函数库中。
注意:static只对缺省链接属性为external的声明才有改变链接属性的效果,换句话说,只能改变全局变量和函数的链接属性:即将外部链接属性变为内部链接属性。
当extern关键字用于源文件中一个标识符的第一次声明时,它指定该标识符具有external链接属性。但是,如果它用于该标识符的第2次或以后的声明时,它并不会改变由第一次声明所指定的链接属性。
例如,下面代码中的声明2并不修改由声明1所指定的变量i的链接属性:
举例表示
test2.c源文件
int c = 5;
test.c源文件
#include
static int c = 10; int main() { extern int c;//并没有改变变量c的内部链接属性,所以此时的输出结果仍然是10 printf("%d",c);//10 return 0; }
此时再来看下面的代码
test2.c文件
int c = 5;
test.c文件
#include
int main() { int c = 10; extern int c; printf("%d",c); return 0; } 此处就会出现重定义的现象,为什么呢?因为extern只能对全局变量进行修饰,将全局变量的外部链接属性变为内部链接属性,而不能对局部变量进行修饰,改变局部变量的链接属性,此处在将test2.c中的源文件引入后,有两个c变量,且它们的定义域是一样的,所以会出现重定义的现象。
==注意:使用了extern之后就不能再使用初始化操作了,==注意看以下代码:
test2.c
int c = 5;
test.c
#include
int main() { extern int c = 10;//此处程序无法进行编译,因为出现了变量c重定义的现象 printf("%d ",c); return 0; } 注意:
extern int c;
是声明,声明c是个外部变量,在其它源文件中已经定义。如果去掉前面的extern就是一个未赋初值的变量的定义了。
extern int c = 10
此处是个变量的定义,而且赋了初值。
凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态变量。
对于这类变量,无法为它们指定其它存储类型。
==静态变量在程序运行之前创建,在程序的整个执行期间始终存在。==它始终保持原先的值,除非给它赋一个不同的值或者程序结束。
函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持 递归。
在静态变量的初始化中,我们可以把可执行程序想要初始化的值放在程序执行时变量将会使用的位置。当可执行程序载入到内存时,这个已经保存了正确初始值的位置将赋值给那个变量。完成这个任务不需要额外的时间,也不需要额外的指令,变量将会得到正确的值。如果不显式的指定其初始值,静态变量将初始化为0。
自动变量的初始化需要更多的开销,因为当程序链接时还无法判断自动变量的存储位置。事实上,函数的局部变量在函数的每次调用中都可能占据不同的位置。基于这个理由,自动变量没有缺省的初始值,而显式的初始化将在段代码块的起始处插入一条隐式的赋值语句。
这个技巧造成四种后果:
自动变量的初始化较之赋值语句效率并无提高,除了声明尾const的变量之外,在声明变量的同时进行初始化和先声明后赋值只有风格之差,并无效率之别。
这条隐式的赋值语句使自动变量在程序执行到它们所声明的函数(或代码块)时,每次都将重新初始化。这个行为与静态变量大不相同,后者只是在程序开始执行前初始化一次。
第三个后果则是个优点,由于初始化在运行时执行,因此可以用任何表达式作为初始化值,例如:
int fun(int a) { int b = a + 3; }
最后一个后果是:除非对自动变量进行显式的初始化,否则当自动变量创建时,他们的值总是垃圾。
如果一个变量声明于代码块内部,在它前面添加extern关键字将使它所引用的是全局变量而非局部变量。
具有extern链接属性的实体总是具有静态存储类型。全局变量在程序开始执行前创建,并在程序整个执行过程中始终存在。从属于函数的局部变量在函数开始执行时创建,在函数执行完毕后销毁,但用于执行函数的机器指令在程序的声明周期内一直存在。
局部变量只能在内部使用,不能被其它函数通过名字引用。它在缺省情况下的存储类型为自动,这是基于两个原因:
- 当这些变量需要时才为它们分配存储,这样可以减少内存的总需求量
- 在堆栈上为它们分配存储可以有效的实现递归。
作用域、链接属性和存储类型的总结见表
变量类型 | 声明的位置 | 是否存在于堆栈 | 作用域 | 如果声明为static |
---|---|---|---|---|
全局 | 所有代码之外 | 否 | 从声明处到文件尾 | 不允许从其它源文件访问 |
局部 | 代码块起始处 | 是 | 整个代码块 | 变量不存储于堆栈中,它的值在程序整个执行期一直保持 |
形式参数 | 函数头部 | 是 | 整个函数 | 不允许 |
问:下列代码会打印出什么内容?
enum Liquid
{
OUNCE = 1,
CUP = 8,
PINT = 16,
QUART = 32,
GALLON = 128
};
enum Liquid jar;
···
jar = QUART;
printf("%s\n",jar);
jar = jar + PINT;
printf("%s\n",jar);
答:==变量jar是一个枚举类型,但它的值实际上是个整数。但是,printf格式代码%s用于打印字符串而不是整数。==结果,我们无法判断它的输出会是什么样子的。如果格式代码是%d,那么输出将会是:
32
48
问:一个无符号变量是否可以比相同长度的有符号变量所能容纳更大的值?
答:否。任何给定的n个位的值只有2n个不同的组合。一个有符号值和无符号值仅有的区别在于它的一半值是如何解释的。在一个有符号值中,它们是负值。在一个无符号值中,它们是一个更大的正值。
问:假定一个函数包含了一个自动变量,这个函数在同一行中被调用了两次。试问,在函数第二次调用开始时,该变量的值和函数第一次调用即将结束时的值有无可能相同?
答:是的,这是有可能的,但不应该指望它。而且,即使不存在其它的函数调用,它们的值也有可能不同。在有些架构的机器上,一个硬件中断将把机器的状态信息压到堆栈上,它们将破坏掉这些变量。