本文为《C和指针》的阅读笔记,主要针对知识盲点进行分析与记录。
一章
形参中被声明为
const, 表示函数将不会修改调用者所传递的参数。无返回值的函数称为过程(procedure)。
在C语言中,数组参数是以引用(reference)形式传递的,也就是传址调用,而常量和标量则是按值(value)传递的。在函数中对标量参数的任何修改都会在函数返回时丢失,因此,被调用函数无法修改调用函数以传值形式传递给它的参数
c语言中不存在“string”数据类型,但在整个语言中存在一项约定:字符串就是一串以NUL字符结尾的字符。NUL 是作为字符串终止符,它本身并不被看成字符串的一部分。
注:NUL是ASCII 字符集中’\0’ 字符的名字,它的字节模式为全0。 NULL 指一个其值为0的指针。他们都是整型,其值也相同,所以他们可以互换使用。字符NULL在头文件
stdio.h中有定义,而另一方面并不存在预定义的NUL,所以使用字符常量’\0’ 替代它而不能直接使用NUL。字符串常量 “Hello” 在内存中占据6个字符空间,分别是H、e、l、l、o和NUL。
printf()函数的第一个参数是一个格式字符串,其包含格式指定符(格式代码)和一些普通字符,普通字符将按照原样打印出来,但每个格式指定符将使后续参数的值按照它所指定的格式打印。C语言形参中声明数组可以不必是给出其长度,不论调用函数的程序传递给它的数组参数长度是多少,这个函数将照收不误。如:
read_coluimn_numbers(int columns[], int max)。scanf()函数的格式代码与printf()颇为相似但却不完全相同,%d, %ld, %f, %lf, %c这5个参数用于读取标量值,所有变量参数的前面必须加上 “&” 符号。使用所有格式代码(除了%c之外)时,输入值之前的空白(空格、制表符、换行符等)都会被跳过,值后面的空白表示该值得结束。因此,使用 %s 格式码输入字符串时,中间不能包含空白。绝大多数C编译器不会对数组下标的有效性进行检查,因此在将输入字符存入数组时需要对数组下标进行有效性检查,使其不超出数组范围。如果不进行检查而读取的值超过数组长度,那么这些值将会存储在紧随数组之后的内存位置,这样便会破坏原先存储在这个位置的数据。
–20.11.12–
二章
- 一个C语言程序的源代码保存在一个或多个源文件中,但一个函数只能完整地除锈安在同一个源文件中。
- 每个源文件都被分别编译,产生对应的目标文件,然后目标文件被链接在一起,形成可执行程序。
- 程序必须载入到内存中才能执行。在宿主式环境中,这个任务由操作系统完成。在自由是环境中,程序常常永久存储于ROM 中。
- 在绝大多数机器里,程序在运行时使用堆栈存储函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行执行过程中将一直保留他们的值。
- 注释不允许嵌套,注释将被预处理器去除。
- 标识符由字母、数字和下划线组成,但不能以数字开头,在标识符中,大小写字母是不一样的。
- 关键字由系统保留,不能作为标识符使用。
- 在使用字符集缺少的某些字符时,可以使用三字母代替,但字符串常量中的字符可能被错误地解释为三字母词(以??开头),可以使用转义序列打印无法打印的字符。
三章
变量的三个属性:作用域、链接属性和存储属性。
C语言中有4种基本的数据类型—-整型、浮点型、指针和聚合类型(数组、结构等),所有其他类型都是从这4中基本类型的某种组合派生而来。
长整型至少应该和整型一样长,而整型至少应该和短整型一样长,long double至少和double一样长,而double至少和float一样长。
所有的浮点型至少能够容纳从10^-37 到 10^37之间的任何值。
浮点数字面值在缺省情况下是double类型,它后跟L或l表示long double,跟一个F或f表示一个float值。
字面值(literal,又翻译为常量)这个术语是字面值常量的缩写——这是一种实体,指定了自身的值,ANSI C允许命名常量(named constant, 声明为const的变量)的创建,它与普通变量极为类似。区别在于,当它被初始化后值便无法修改。
因为无法通过程序指定一个字面值作为变量存放在内存中的地址,所以C语言中没有指针常量这一概念定义(NULL指针除外,它可以用0值表示)
NUL字符作为字符串的终止符,其是不可打印的。空字符串结尾也有NUL字符作为终止符。
ANSI编译器允许把一个字符串常量存储于一个地方即使它在程序中出现多次。因此在ANSI中,不允许修改字符串常量,或者提供编译时选项,让你自行选择是否允许修改字符串常量。在实践中,请尽量避免修改字符串常量,如果需要修改,可将它存储于数组中。
当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些存储字符串常量的地址,而不是这些字符本身。因此,不能讲一个字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。
对于字符串常量的赋值、复制等操作可以通过C函数库的函数实现,包括对字符串进行复制、连接、比较以及计算字符串长度和在字符串中查找特定的字符的函数。
int* b, c, d中,只有b是指向整型的指针,而c和d都只是普通的整型。
在声明指针变量并同时进行赋值时容易产生误解,char *message = “Hello world!”初始值是字符串常量中第1个字符的地址赋值给message本身,而不是表达式 *message。其等效于
1
2char *message;
message = "Hello world!";typedef char *ptr_to_char;这个声明把标识符ptr_to_char作为指向字符指针类型的新名字,ptr_to_char a;则声明了a是一个指向字符串的指针。使用typedef而不用#define来创建新的类型名,是因为后者无法正确地处理指针类型,如:1
2#define d_ptr_to_char char*
d_ptr_to_char a, b;正确地声明了a,但是b却被声明为一个字符。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef更合适。
–20.11.13–
常量的值是不能被修改的,只有初始化时才能进行一次赋值。
指针变量和它所指向的实体都可能成为常量,例如:
1
int *pi;
pi是一个普通的指向整型的指针。而变量
1
int const *pci;
则是一个指向 整型常量 的 指针,你可以修改指针的值(指针指向的地址值),但不能修改它所指向的值(指针指向的地址上存储的内容)。相比之下:
1
int * const pci;
则声明pci为一个指向 整型 的 常量指针,无法修改它的值,但却可以修改它所指向的地址上的整型的值。
1
int const * const cpci;
最后,在cpci这个例子里, 无论是指针本身还是它所指向的地址的值都是常量,是无法修改的。
#define是指令是另一种常见创建名字常量的机制,#define只要在允许使用字面值常量的地方都可以使用,如声明数长度,而const变量只能用于允许使用变量的地方。使用名字常量可以给数值其符号名,否则数值只能写成字面值的形式,名字常量可以提高程序的维护性。
编译器可以确认4种不同类型的作用域:文件作用域、函数组用于、代码块作用域和原型作用域。
位于一堆花括号之间的所有语句称为一个代码块(block scope)。当代码块处于嵌套状态时,声明于内层代码块的标识符的作用域到达该代码块的尾部便告终止。然鹅,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将隐藏外层的标识符——外层的那个标识符无法在内层代码块中通过名字访问。
在非嵌套的情况下,声明于每个代码的变量无法被另一个代码块访问,因为它们的作用域并无重叠之处,且这两个代码块中的变量能够共享一个内存地址,因为任意时刻,两个非嵌套代码块最多只有一个处于活动状态。
任何在所有代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从它们的声明之处知道它所在的源文件结尾处都是可以访问的。
原型作用域(prototype scope)值适用于在函数原型中声明的参数名。
函数作用域(function scope)只适用于语句标签,语句标签用于goto语句。一个函数中的所有语句标签必须唯一。
链接属性共有3种——external(外部)、internal(内部)和none(无)。没有链接属性的标识符被当做单独的个体,该标识符的多个声明被当做独立不同的实体。internal链接属性的标识符在同一个源文件内的所有声明都指向同一个实体,但不同源文件的多个声明则分属不同的实体。最后external链接属性的标识符不论声明多少次、位于几个源文件,都表示一个实体。
只有具备文件作用域的标识符才能拥有external或internal的链接属性,其他作用域的标识符都是none属性。默认缺省情况下,具备文件作用域的标识符拥有external属性,即该标识符可以跨文件访问,也就是说,在一个源文件定义的全局变量,可以在另一个源文件直接访问,而一般会在需要使用该变量的源文件的声明中添加extern关键字来使读者更容易理解。
使用static关键字可以使得原先拥有external属性的标识符变为internal属性,但要注意:static关键字只有在作用于external属性的标识符(代码块之外的变量声明)才能修改其链接属性,但标识符的存储类型和作用域不收影响。对其他作用域的标识符是另一种功能。当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量。但变量的链接属性和作用域不受影响。这种方式声明的变量在程序执行前创建,在程序执行时一直存在,而不是每次代码块开始执行前后创建和销毁。
文件作用域的标识符经static关键字修饰后变成内部变量,其不能跨文件访问,只在该文件中可见(static关键字的隐藏属性)。另外加extern也没用,因为链接属性只能修改一次。
extern关键字没有像static关键字使用作用域的限制,如下方代码
extern int k声明为k指定external链接属性,这样一来函数就可以访问在其他原文件声明的外部变量了,链接属性只能修改一次1
2
3
4
5static int i;
int func() {
extern int i;
extern int k;
}如上例子中,函数内的extern关键字并不会修改static属性。
有3个地方可以存储变量:普通内存、运行时堆栈、硬件寄存器。
变量的缺省存储类型取决于其声明位置。凡是在代码块之外声明的变量总是存储于静态内存中,也就不属于堆栈的内存,这类变量为静态变量,静态变量在程序运行之前创建,在程序整个执行期间始终存在,若无重新赋值,在程序结束前会维持不变。
在代码块内部声明的变量缺省的存储类型是自动的,只有程序执行到声明自动变量的代码块时其才被创建,离开该代码块时便自动销毁。再次执行同一代码块时便重新创建,与上一次无关联。
代码块内声明的变量加上static关键字可使其从自动变为静态存储类型,使其生命周期摆脱代码块执行周期限制而在程序的整个执行周期内都存在。
修改变量的存储类型并不表示修改该变量的作用域,它仍然只能在该代码块内部按名字访问。
函数形参不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。
关键字register用于自动变量的声明,提示其存储于硬件寄存器中,这类变量称为寄存器变量。通常寄存器变量比存储于内存的变量访问起来效率更高。但编译器并全部识别register关键字,甚至忽略一部分,这由编译器决定。
有些计算机中,把频繁执行间接访问操作的指针存放于寄存器可大大提高程序效率。
函数形参可以声明为寄存器变量,编译器会在函数的起始位置把这些值从堆栈复制到寄存器中,但完全有可能这个优化措施节省的时间和空间开销还抵不上复制几个值所用的开销。
寄存器变量的创建和销毁时间与自动变量相同,但它需要一些额外的工作。在一个使用寄存器变量的函数返回之前,这些寄存器先前存储的值必须恢复,确保调用者的寄存器未被破坏。许多机器使用堆栈来完成这个任务,在函数开始时把所有寄存器的内容都保存在堆栈中,当函数返回时,这些值在复制回寄存器中。
机器并不向你提供寄存器变量的地址,因为许多机器的硬件实现并未给寄存器指定地址,由于寄存器值的保存和恢复,某个特定的寄存器在不同的时刻所保存的值不一定相同。
自动变量和静态变量的初始化存在一个重要的差别。子啊静态变量的初始化中,可以把可执行文件想要初始化的值放在程序执行时变量将会使用的位置。当可执行文件载入内存时,这个已经保存了正确初始值的位置将幅值给那个变量。
完成静态变量初始化任务并不需要额外的时间和指令,变量将会得到正确地值,如果未显式地指定初始值则默认初始化为0。
自动变量初始化需要更多的开销,因为当程序链接时还无法判断自动变量的存储位置。因此局部变量在函数的每次调用可能占据不同的位置,自动变量没有缺省初始值。
自动变量的初始化较之赋值语句效率并无提高,除了声明const变量外,在声明变量的同时进行初始化和声明后赋值只有风格只差,并无效率之别。
隐式的赋值语句使自动变量在程序执行到它们所声明的函数(或代码块)时,每次都将重新初始化。这个行为与静态变量大不相同,后者只是在程序开始执行前初始化一次。
由于自动变量的初始化在程序运行时执行,所以任何表达式都可作为其初始化的值,而静态变量没有这一优点。
–20.11.13–
- 作用域、存储类型示例
1 | int a = 5; |
以上例子程序阐明了作用域和存储类型。属于文件作用域的声明在缺省情况下为external链接属性,所以第1行的a的链接属性为external。如果b的定义在其他地方,第2行的extern关键字在技术上井非必需,但在风格上却是加上这个关键字为好。第3行的static关键字修改了c的缺省链接属性,把他改成了internal。声明了变量a和b(具有external链接属性)的其他源文件在使用这两个变量时实际所访问的声明于此处的这两个变量。但是,变量c只能由这个源文件访问,因为它具有internal链接属性。
变量a、b、c的存储类型为静态,表示它们并不是存储于堆栈中。因此,这些变量在程序执行之前创建,并一直保持它们的值,直到程序结束。当程序开始执行时,变量a将初始化为5。
这些变量的作用域一直延伸到这个源文件结束为止,但第7行和第13行声明的局部变量a和b在那部分程序中将隐藏同名的静态变量。因此这3个变量的作用域为:
a 第1至12行,第17至29行
b 第2至6行,第25至29行
c 第3至29行
第4行声明了两个标识符。d的作用域从第4行直到文件结束。函数d的定义对于这个源文件中以后想要调用它的函数而言起到函数原型的作用。作为函数名,d在缺省情况下具有 external 链接属性,所以其他源文件只要在文件上存在d的原型,就可以调用d。如果我们将函数声明为 static,就可以把它的链接属性从 external改为 internal,但这样做将使其他源文件不能访问这个函数。对于 函数而言,存储类型并不是问题,因为代码总是存储于静态内存中。
参数e不具有链接属性,所以我们只能从函数内部通过名字访问它。它具有自动存储类型,所以它在函数被调用时被创建,当函数返回时消失。由于与局部变量冲突,它的作用域限于第6至11行,第17至19行以及第23至24行。
第6至8行声明局部变量,所以它们的作用域到函数结束为止。它们不具有链接属性,所以它们不能在函数的外部通过名字访问(这是它们称为局部变量的原因)。f 的存储类型是自动,当函数每次被调用时,它通过显式赋值被初始化为15。b的存储类型是寄存器类型,所以它的初始值是垃圾。g的存储类型是静态,所以它在程序的整个执行过程中一直存在。当程序开始执行时,它被初始化为20。当函数每次被调用时,它并不会被重新初始化。
第9行的声明并不需要。这个代码块位于第1行声明的作用域之内。 第12和13行为代码块声明局部变量。它们都具有自动存储类型,不具有链接属性,它们的作用域延伸至第16行。这些变量和先前声明的a和e不同,而且由于名字冲突,在这个代码块中,以 前声明的同名变量是不能被访问的。
第14行使全局变量h在这个代码块内可以被访问。它具有external链接属性,存储于静态内存中。这是唯一一个必须使用extern关键字的声明,如果没有它,h将变成另一个局部变量。
第19行和20行用于创建局部变量(自动、无链接属性、作用域限于本代码块)。这个e和参数e是不同的变量,它和第12行声明的e也不相同。在这个代码块中,从第11行到第18行并无嵌套 ,所以编译器可以使用相同的内存来存储两个代码块中不同的变量e。如果你想让这两个代码块中的e表示同一个变量,那么你就不应该把它声明为局部变量。
最后,第25行声明了函数i,它具有静态链接属性。静态链接属性可以防止它被这个源文件之外的任何函数调用。事实上,其他的源文件也可能声明它自己的函数i,它与这个源文件的i是不同的函数。i的作用域从它声明的位置直到这个源文件结束。函数d不可以调用函数i,因为在d之前不存在i的原型。
具有external链接属性的实体在其他语言的术语里成为全局(global)实体。
只要变量并非声明于代码块内部,它在缺省情况下的链接属性即为external。如果一个变量声明于代码块内部,在它前面添加extern关键字将使它所引用的是全局变量而非局部变量。
具有external链接属性的实体总是具有静态存储类型。
作用域、链接属性和存储类型总结
| 变量类型 | 声明的位置 | 是否存储于堆栈 | 作用域 | 如果声明为static |
|---|---|---|---|---|
| 全局 | 所有代码块之外 | 否 | 从声明到文件尾 | 不允许从其他源文件访问 |
| 局部 | 代码块起始处 | 是 | 整个代码块 | 变量不存储于堆栈中,它的值在程序整个执行期一直保持 |
| 形式参数 | 函数头部 | 是 | 整个函数 | 不允许 |
一个无符号变量可不可以比相同长度的有符号变量容纳更多的值?
否,任何给定的n个位的值2^n个不同的组合。一个有符号值和无符号值仅有的区别在于它的一半值是如何解释的。在一个有符号值中,它们是负值。在一个无符号值中,它们是一个更大的正值。即容纳的值的范围是一样的,只是值的区间不同。
假如int和float类型都是32位长,你觉得那种类型能容纳的值更多一些?
在绝大多数浮点系统中,零通常有多种表示形式,而且通过使用不规范的小数形式,其他值也具有不同的表示形式,因此 float 能够表示的不同值的数量比 int 少。
某个函数包含了一个自动变量,这个函数子啊同一行中被调用了两次,该变量第二次调用起始时的值和第一次调用即将结束时的值有无可能相同?
有可能相同,但不能指望它,它是不可靠的,在有些架构的机器上,一个硬件中断将把机器的状态信息压倒堆栈上,它们将破坏这些变量。
–20.11.16–
四章
- if else 语句中else语句从属于最靠近它的不完整的 if 语句
五章
右移位操作存在一个左移位操作不存在的问题:从左移入新位时,(1)逻辑移位,左移入的位用0填充;(2)算术移位,新位由符号位决定,符号位为1则均为1,0则移入的位均为0,以保持原数正负形式不变。只有操作数是负值时算术左移和逻辑左移不同。标准中,右移位情况下,所有无符号值都是逻辑移位,而有符号值移位类型取决于编译器。因此有符号数的右移位操作是不可移植的。
左右移位操作符的操作数都必须是整数类型。
移位操作数是负数时
a << -5,该表达式实际可能执行左移27位,这类移位行为在标准中未定义,所以应该避免这种类型的移位,这类移位操作不可移植。位操作实例
1
2置1操作:value = value | 1 << bit_number; 或者value |= 1 << bit_number;
置0操作:value = value & ~(1 << bit_number); 或者value &= ~(1 << bit_number);对指定位进行测试,如果该位为1,则下列表达式结果为非零值:
1
value & 1 << bit_number
赋值表达式的值就是左操作数的新值,它可以作为其他赋值操作符的操作数,
1
r = s + ( t = u - v ) / 3;
这条语句把表达式
u-v的值赋值给t,然后把t的值除以3,再把除法结果与s相加,其结果赋值给r。尽管这种写法合法,但为了方便阅读维护,建议拆开写。将整型值赋值给字符型变量时,整型值将被截短后进行赋值,
1
2char ch;
while( ( ch = getchar() ) != EOF )上面代码中,
getchar()函数返回值是整型,而在定义ch时定义成字符型,将导致getchar返回值被截短,而EOF是整型,所以截短后的返回值存入ch并提升为整型并与EOF进行比较。当这段错误代码在有符号字符集的机器上运行时,如果取了一个值为\377的字节时,循环会终止,因为这个值截短再提升之后与EOF相等。而当这段代码在使用无符号字符集的机器上运行时,这个循环将永远不会终止!尽量使用复合赋值符,
1
a[ 2 * (y - 6*f(x)) ] += 1
相当于
1
a[ 2 * (y - 6*f(x)) ] = a[ 2 * (y - 6*f(x)) ] + 1
但+=操作符的左操作数a[ 2 * (y - 6*f(x)) ]只求值一次,效率更高,出错率更低。
sizeof不是函数而是一个操作符,它可以判断操作数(表达式、变量)的长度,以字节为单位表示,1
sizeof(int) sizeof x
第一个表达式返回整型变量的字节数,其结果取决于机器环境。第二个表达式返回变量x所占的字节数,当操作数为数组时返回该数组的长度,以字节为单位。判断表达式的长度并不需要对表达式进行求值,所以
1
sizeof(a = b + 1)
并没有向a赋任何值。
++或--操作符要求操作数必须是左值,前缀和后缀形式的增值操作符都复制一份变量值的拷贝,前缀操作符在进行复制之前增加变量的值,而后缀操作符在进行复制之后在增加变量的值。这些操作符的结果不是被他们所修改的变量,而是变量值的拷贝,因此:1
++a = 10
++a的结果是a值的拷贝,并不是变量本身,因此无法向一个值进行赋值。关系操作符(>、>=、<、<=、!=、==)产生的结果都是一个整型值,而不是布尔值,c语言没布尔值。
逻辑操作符( && 和 || )的短路求值(short-circuited evaluation):&&和||操作符总是先对左操作数进行求值,如果满足短路条件(&&中左操作数为假或||左操作数为真),则不再对右操作数进行求值。
逗号操作符将两个或多个表达式分隔开来,这些表达式自左向右逐个求值,整个逗号表达式的值就是最后那个表达式的值。逗号操作符妙用:
1
2
3
4
5
6a = get_value();
count_value( a );
while( a > 0 ) {
a = get_value( 0 );
count_value( a );
}可以改写成
1
2while( a = get_value( 0 ), count_value( a ), a > 0 ) {
}或者使用内嵌赋值:
1
2while( count_value(a = get_value(0) ), a > 0 ) {
}arrar[ 下标 ]与*( array + ( 下标 ) )是等价的。如果s是个结构变量,那么
s.a就访问s中名叫a的成员。用指向结构的指针访问结构成员时,可以用->访问其成员,而不是用.操作符。c语言没有布尔类型,所以用整型替代,零是假,任何非零值都为真。
c语言中常通过
#define FALSE 0和#define TRUE 1来把整型值当做布尔值使用,因此某一布尔值判断是否为真时不要通过比较而是直接用if(a)或if(!a)来进行判断。整型值与布尔值不要混用,如果该整型变量在程序中作为布尔值使用,则该变量始终只有TRUE(1)或FALSE(0)两个值,而不要有其他2、3等整型数值。如果变量是整型值而不是布尔值,在对其进行测试时应该显式地进行测试
if( value != 0 )而不用简写if(value)因为简写暗示该变量本质上是布尔型的。左值可以出现在赋值符的左边,它标识计算机内存中的一个位置。右值表示一个值,只能出现在赋值符的右边。每个左值同时也可以是个右值,但右值不能是左值。
字面值常量都不是左值,变量可以作为左值而表达式也可以作为左值:
1
a[ b + 10 ] = 0;
下标引用实际上是一个操作符,所以表达式的左边实际上是个表达式,但它因为表示了一个特定的位置而合法存在。
c语言中有4个操作符( ? : 、&&、||、, )可以对整个表达式的求值顺序施加控制,它们或者保证某个子表达式能够在另一个子表达式的所有求值过程完成之前进行求值,或者可能使得某个表达式被完全跳过而不再求值。
表达式的求值顺序并非完全有操作符优先级决定,如下面的语句是非常危险的:
1
c + --c
操作符的优先级规则要求自减运算在加法运算之前进行,但没法确定加法操作数左边的c是自减之前还是之后的值,不同的编译器会有不同的情况。
操作符的优先级决定了相邻的操作数哪个先被执行,如果优先级相等,那么将由它们的结合性决定,但这些不能完全决定表达式的求值顺序。编译器只要不违反优先级和结合性原则,便可以自由决定求值顺序,这种情况是不可移植的,要极力避免的。
1
f() + g() + h();
上面尽管左边的加法运算必须在右边的加法运算之前执行,但对于各个函数的调用顺序并没有规则加以限制,如果这些函数执行同一个I/O任务或修改同一个全局变量,那么顺序将会导致结果有区别,所以最好使用临时变量让函数调用单独运行。
条件操作符在运行时较if语句是更快还是更慢?
if方案看上去更臃肿一些,因为它具有两条存储到i的指令,但是它们之间只有一条指令会执行,所以条件操作符和if语句二者速度并无区别。
–20.11.17–
六章
硬件并不提供变量名字与内存位置之间的关联,它是由编译器实现的,硬件仍然通过地址访问内存位置。
变量包含了一序列内容为0或1的位,它们可以被解释成整数,也可以被解释成浮点数,这取决于它们被使用的方式。不能简单的通过检查一个值的位来判断它的类型,必须观察这个值的使用方式。
通过一个指针访问它所指向的地址的过程称为间接访问(indirection)或解引用指针(dereferencing the pointer)。这个用于执行间接访问的操作符是单目操作符*。
声明一个变量指针并不会自动分配任何内存。在对指针执行间接访问前,指针必须进行初始化:或者使它指向现有的内存,或者给它分配动态内存。对未初始化的指针变量执行间接访问操作是非法的,而且这种错误常常难以检测。
1
2int *a;
*a = 12;在unix系统上,这个错误被称为“段违例( segmentation violation )”或“内存错误(memory fault)”,它提示程序试图访问一个未分配给程序的内存位置。在windows中,对未初始化或者非法指针进行间接的访问操作是一般保护性异常( General Portection Exception )的根源之一。
NULL 指针是一个特殊的指针变量,它表示不指向任何东西。要使一个指针变量为NULL,你可以给它赋一个零值,为测试一个指针变量是否为NULL,你可以将它与0值进行比较。
对一个NULL指针进行解引用是非法的。其后果因编译器而异,两个常见的后果是返回内存位置0的值以及终止程序。
除了NULL指针外,没有其他内建的记法来表示指针常量,因为程序员通常无法预测编译器会把变量放在内存中的什么位置。
指针是变量,因此也可以作为左值。
假设a存储于位置100,那么
*100 - 25看上去是把25赋值给a,但这是错的,这是非法的,因为字面值100的类型是整型,而间接访问操作只能作用于指针类型的表达式,如果确实想把25存储于位置100,那么必须使用强制类型转换:1
*(int *)100 = 25;
把100从整型转换为指向整型的指针。
指针变量与其他变量一样,占据内存中某个特定的位置,所以用&操作符取得它的地址是合法的,而register变量不能用&。
下面
1
2char ch = 'a';
char *cp = &ch;表达式 右值 能否为左值 &ch 变量ch的地址 非法 cp cp的值 可 &cp 指向字符的指针cp的地址 非法 *cp cp所指地址存储的值: ‘a’ 可 *cp +1 cp所指地址存储的值的拷贝加上1: ‘b’ 非法 *(cp + 1) cp后面内存位置上的值 可 ++cp 增值后的指针的一份拷贝: 拷贝的cp的值为原来cp的值+1 非法 cp++ 返回cp的值的一份拷贝然后在增加cp的值,整个表达式的值是cp原值的拷贝 非法 *++cp ch后面内存地址的值 可 *cp++ ch的值: *cp *cp = ?, cp+1 ++*cp ++作用于所有右侧操作数,结果是cp指向的位置的值+1 非法 (*cp)++ ch增值前的原先值 非法 ++*++cp 访问cp+1后指向的值并+1作为输出 非法 ++*cp++ 访问cp指向的值+1作为输出,最后cp+1 非法 注:++操作符优先级高于*操作符。但
*cp++中表达式结果看上去先执行间接访问操作。事实上,这里涉及3个步骤:(1) ++操作符产生cp的一份拷贝,(2)++操作符增加cp的值, (3)cp的拷贝执行间接访问操作并作为表达式的输出。