《C和指针》---- 第二弹(当前可食用字数:5000)----读书笔记硬核分享系列

x33g5p2x  于2022-05-05 转载在 其他  
字(6.0k)|赞(0)|评价(0)|浏览(182)

前言

大家好呀,今天丸丸给大家带来的是《C和指针》系列第二弹——读书笔记硬核分享系列,希望大家能够多多支持呀!都是干货,感兴趣的话大家就收藏一下吧!大家的支持是我更新这一系列最大的动力!

第四章 语句

4.2 表达式语句

C语言中不存在专门的“赋值语句”,在C语言中,赋值是一种操作,就像加法和减法一样,所以赋值就在表达式内进行。

只要在表达式后面加上一个分号,就可以把表达式转变为语句。所以,下面这两个表达式

x = y + 3;
ch = getchar();

实际上是表达式语句,而不是赋值语句。所以,下面的操作也是合法的:

y + 3;

getchar();

当这些语句在执行时,表达式被求值。但是它们的结果并不保存于任何地方,因为它们并未使用赋值操作符。

4.3 代码块

代码块可以用于要求出现语句的地方。它允许你在语法要求中只出现一条语句的地方使用多条语句。代码块还允许让数据的声明非常靠近使用它的地方。
注意:代码块是否具有返回值要分具体的情况,比如代码块中只有一个表达式,或者它的最终的值是一个数字的话,那么他就具有返回值,可以将其作为右值赋值给变量,如果里面是一个带有分号的语句,那么这个代码块就不具有右值的性质。

注意:下面的这种情况是可行的:

C int n = 10; int a = {n};

下面的程序是可以正常运行的,因为{n}也是具有右值的性质,虽然这个值是在运行的时候确定的。

4.4 if语句

if语句的语法结构如下所示:

if(expression)
    statement;
else
    statement;

括号是if语句的一部分,而不是表达式的一部分,因此它是必须出现的,即使是那些极为简单的表达式也是如此。

4.8 switch语句

注意:switch后面的括号中的语句会实际执行,如果执行了自增操作、自减操作或者赋值操作就会对相应的变量产生副作用并发生改变。

注意:在switch语句中,continue语句没有任何的效果,如果强行使用,编译器会报错。

4.9 goto语句

注意:包含这些标签的goto语句可以出现在同一个函数的任何位置,即goto语句不能跨函数。

问与答

  1. 问:当编写if语句时,如果在then子句中没有语句,但在else子句中有语句,该如何编写?还能改用其它形式来达到同样的目的吗?

答:

if(condition)
    ;
else
{
    statements
}

可以对条件进行修改,省略空的then语句。它们的效果是一样的:

if(!(condition))
{
	statements
}

第五章 操作符和表达式

5.1 操作符

5.1.2 移位操作符

警告:标准规定无符号值执行的所有移位操作都是逻辑移位,但对于有符号值,到底是采用逻辑移位还是算术移位则取决于编译器。因此,一个程序如果使用了有符号数的移位操作,它就是不可移植的。

警告:在下面的语句中,认为a和x被赋予相同的值的说法是不正确的:

a = x = y + 3;

如果x是一个字符性变量,那么y+3的值就会被截去一段,以便容纳于字符类型的变量中。那么a所赋得值就是这个被截短后得值。

在下面得这个常见得错误中,这种截短正是问题的根源所在:

char ch;
···
while((ch = getchar())!=EOF) ···

EOF需要的位数闭字符型值所能提供的位数要多,这也是getchar返回 一个整型值而不是字符值的原因。然后,把getchar得返回值首先存储于ch中将导致它被截短。然后这个被截短后得值被提升为整型并与EOF进行比较。当这段存在错误得代码在使用有符号字符集的机器上运行时,如果读取了一个值为\377的字节,循环将会终止,因为这个值截短再提升之后与EOF相等。当这段代码在是以哦那个无符号字符集的机器上运行时,这个循环将永远都不会终止!

警告:a = a+5和a+=5之间在现代编译器上并没有太大的区别,而且现代的编译器为这两种表达式产生优化代码并无多大问题,但请考虑下面两条语句,如果函数f没有副作用,它们是相同的。

a[2*(y - 6*f(x))] = a[2*(y - 6*f(x))] + 1;
a[2*(y - 6*f(x))] += 1;

在第一种形式中,用于选择增值位置的表达式必须书写两次,一次在赋值号的左边,另一次在赋值号的右边。由于编译器无从知道函数h是否具有副作用,所以它必须两次计算下标表达式的值。第二种形式效率更高,因为下标只计算一次。

5.1.5 单目操作符

sizeof操作数既可以是个表达式(常常是单个变量),也可以是两边加上括号的类型名。这里有个例子:

sizeof(int);
sizeof x;//x是一个变量

下面的写法也是合法的:

sizeof(x);

这是因为括号在表达式中总是合法的。

在判断表达式的长度时,并不需要对表达式进行求值,所以sizeof(a = b+1)并没有向a赋任何值。

5.1.9 逗号表达式

a = get_value();
count_value(a);
while(a > 0)
{
···
a = get_value();
count_value(a);
}

上面的代码我们可以写成其它更为简洁的形式:

while(a = get_value(),count_value(a),a>0)
{
···
}

也可以使用内嵌的赋值形式,如下所示:

while(count_value(a = get_value()),a > 0)
{
···
}

现在,循环中用于获得下一个值得语句只需要出现一次。逗号操作符使源程序更易于维护。如果用于获得下一个值得方法在将来需要改变,那么代码中只有一个地方需要修改。

这里还有一个技巧:

while(x<10)
	b+= x,
	x+= 1;

在这个例子中,逗号操作符把两条赋值语句整合成一条语句,从而避免了在它们得两端加上花括号。不过,这并不是一个好做法,因为逗号和分号得区别过于细微,人们很难注意到第一个赋值后面是一个逗号而不是分号。

5.3 左值和右值

左值就是那些可以出现在赋值符号左边得东西。右值就是那些可以出现在赋值符号右边得东西。

下面是一个例子:

a = b + 25;

a是一个左值,因为它标识了一个可以存储结果得值得地点;b+25是一个右值,因为它指定了一个值。

它们可以互换吗?

b + 25 = a;

原先用作左值得a此时也可以当作右值,因为每个位置都包含一个值。然而,b+25不能作为左值,因为它并未标识一个特定得位置。因此,这条赋值语句是非法的。

注意:当计算机计算b+25时,它的结果必然保存于机器的某个地方,也许是寄存器中,也许是内存中的栈上。但是,程序员并没有办法去预测该结果会存储在什么地方,也无法保证这个表达式的值下次还会存储于同一个地方。其结果是,这个表达式的不是一个左值。基于同样的理由,字面值也都不是左值。

听上去似乎变量是可以作为左值而表达式不能作为左值,但这个推断并不准确。在下面的赋值语句中,左值便是一个表达式:

int a[30];
···
a[ b + 10 ] = 0;

**下标引用实际上是一个操作符,所以表达式的左边实际上是个表达式,但它却是一个合法的左值,因为它标识了一个特定的位置,我们以后可以在程序中引用它。**这里还有一个例子:

int a,*pi;
···
pi = &a;
*pi = 20;

看第二条赋值语句,它左边的那个值显然是一个表达式,但它确实一个合法的左值。为什么?指针pi的值是内存中某个特定位置的地址,*操作符使机器指向那个位置。当它作为左值使用时,这个表达式指向需要进行修改的位置;当它作为右值使用时,它就提取当前存储于这个位置的值。

有些操作符,如间接访问和下标引用,它们的结果是个左值。其余操作符的结果则是个右值。

5.4 表达式求值

5.4.2 算术转换

下面这个代码段包含了一个潜在的问题:

int a = 5000;
int b = 25;
long c = a*b;

即表达式a*b的是以整型进行计算,在32位整数的机器上,这段代码运行起来毫无问题,但在16位整数的机器上,这个乘法运算会产生溢出,这样c就会被初始化位错误的值。

解决方案是在执行乘法运算之前把其中一个(或两个)操作数转换为长整型:

long c = (long)a*b;

第六章 指针

6.1 内存和地址

名字与内存位置之间的关联并不是硬件来提供的,而是由编译器为我们实现的。所有这些变量给了我们一种更为方便的方法记住地址——硬件仍然通过地址访问内存位置。

6.2 值和类型

不能简单的通过检查一个值的位来判定它的类型。值得类型并非值本身所固有得一种特性,而是取决于它的使用方式。

6.11 指针表达式

下面介绍一些有关指针表达式作为左值和右值时分别代表什么

char ch = 'a';
char *cp = &ch;
表达式左值右值
ch合法左值。变量ch这个内存的地址,表示的是这块地址空间‘a’
&ch非法左值。(当表达式&ch进行求值时,它的结果应该存储于计算机中的什么地方我们并不知道,即这个表达式并未表示任何机器内存的特定位置,所以它不是一个合法的左值)变量ch的地址
cp合法左值。变量cp所处的内存位置,表示的是这块地址空间ch的地址
&cp非法左值。(原因与&ch类似,不过这次所取得是指针变量的地址)cp的地址
*cp+1非法左值。(这个表达式的最终结果的存储位置并未清晰定义,所以它不是一个合法的左值)‘b’
*(cp+1)合法左值。cp+1表示的是变量ch所处的内存空间的下一块字符型空间的地址,对其解引用表示的就是这块空间cp+1表示的是变量ch所处内存空间的下一块字符型空间的地址,对其解引用就是表示这块空间中存储的值
++cp非法左值。表达式的结果是增值后的指针的一份拷贝,因为前缀++先增加它的操作数的值再返回这个结果。这份副本的存储位置并未清晰定义,所以它不是一个合法的左值。ch的地址(cp的值)+1(加1是因为ch是字符型)
cp++非法左值。后缀++同样增加cp的值,但它先返回cp的值的一份拷贝然后再增加cp的值。这样,这个表达式的值就是cp原来的值的一份副本。ch的地址(cp的值)
*++cp合法左值。表示变量ch所处内存空间的下一块字符型空间(和*(cp+1)是等效的)变量ch所处内存空间的下一块字符型空间中存储的值(和*(cp+1)是等效的)
*cp++ch的存储位置(三个步骤:操作符产生cp的一份副本;然后操作符增加cp的值;最后,在cp的副本上执行间接访问操作)ch的值
++*p非法cp所指向位置的值增加1,即’b’。(由于这两个操作符的结合性都是从右向左,因此首先执行的是间接访问操作,然后cp所指向的位置的值增加1,表达式的结果是这个增值后的值的一份副本)
(*cp)++非法ch的值
++*++cp非法首先对cp进行操作,然后取地址得到变量ch后面空间中存放的值,然后对这个值进行操作,最终得到的是变量ch后面空间中存放的值+1
*cp非法ch的值+1即’b’

6.13.1 算术运算

让指针指向数组最后一个元素后面的那个位置是合法的,但是对这个指针执行间接访问可能会失败。

下面是一个例子:

#define N_VALUES 5
float value[N_VALUES];
float *vp;
for(vp = &value[0]; vp < &values[N_VALUES]; )
    *vp++ = 0;

&values[N_VALUES]表示数组最后一个元素后面那个内存位置的地址。当vp到达这个值时,我们就知道到达了数组的末尾,故循环终止。

这个例子中的指针最后指向的是数组最后一个元素后面的那个内存位置。指针可以合法的获得这个值,但对它执行间接访问时,将可能意外的访问原先存储于这个位置的变量。程序员一般无法知道那个位置原先存储的是什么变量。因此,在这种情况下,一般不允许对指向这个位置的指针执行间接访问操作。

6.13.2 关系运算

同上面的代码。for语句用了一个关系测试来决定是否结束循环。这个测试是合法的,因为vp和指针常量都指向同一数组中的元素(事实上,这个指针常量所指向的是数组最后一个元素后买你的那个内存位置,虽然在最后一次比较时,vp也指向了这个位置,但由于此时未对vp执行间接访问操作,因此它是安全的)。使用!=操作符代替<操作符也是可行的,因为如果vp未到达它的最后一个值,这个表达式的结果总是假的。

现在考虑下面这个循环:

for(vp = &values[N_VALUES]; vp > &values[0]; )
{
    *--vp = 0;
}

它和前面那个循环所执行的任务相同,但数组元素将以相反的次序清除。我们让,vp执行数组最后个元素后面的内存位置,但在对它进行间接访问之前先执行自减操作。当vp指向数组第一个元素时,循环便终止,不过这发生在第一个数组元素被清除之后。

有些人会对上面的循环进行如下改进:

for(vp = &values[N_VALUES]; vp >= &values[0]; vp--)
{
    *vp = 0;
}

现在vp指向数组最后一个元素,它的自减操作放在for语句的调整部分进行。这个循环将存在一个问题:

警告:在数组的第一个元素被清除后,vp的值还将减去1,而接下来的一次比较运算是用于结束循环的,但这就是问题所在:比较表达式vp>=&values[0]的值是未定义的,因为vp移到了数组的边界之外。标准允许指向数组元素的指针指向数组的最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组第一个元素之前的那个内存位置的指针进行比较。

实际上,在绝大多数编译器中,这个循环将顺利完成任务。然而还是应该避免使用它,因为标准并不保证它可行。

6.14 总结

  • 指针运算只有作用于数组中时,其结果才是可以预测的。对任何并非指向数组元素的指针执行算术运算都是非法的(且常常很难被检测到)。如果一个指针减去一个整数后,运算结果产生的指针所指向的位置在数组的第一个元素之前,那么它也是非法的。加法运算符稍有不同,如果结果指针指向数组最后一个元素后面的那个内存位置,则仍然是合法的(但不能对这个指针执行间接访问操作),不过再往后就不合法了。
  • 如果两个指针都指向同一个数组中的元素,那么它们可以相减。但如果两个指针并不是指向同一个数组的元素,那么它们之间进行相减就是错误的。
  • 对两个不相关的指针执行关系运算,其结果是未定义的。

问与答

  1. 问:如果一个值得类型无法简单得通过观察它得位模式来判断,那么机器是如何知道它怎样对这个值进行操纵得呢?

答:机器无法判断。编译器根据值得声明类型后创建适当得指令,机器只是盲目得执行这些指令而已。

  1. 问:下面得代码有没有问题?如果有得话,问题在哪里?

答:有两个错误。对增值后得指针进行解引用,数组的第一个元素没有被清零。另外,若指针在越过数组的右边界以后仍然进行解引用,它将把其它内存地址的内容清零。

相关文章