跳转至

第1章 从基础开始——预备知识和复习

1.1.8 C是只能使用标量的语言

Page 15

对于标量(scalar)这个词,大家可能有些陌生。

简单地说,标量就是指char、int、double和枚举型等数值类型,以及指针。相对地,像数组、结构体和共用体这样的将多个标量进行组合的类型,我们称之为聚合类型(aggregate)。

早期的C语言一度只能使用标量

经常听到初学者有以下的提问:

if (str == "abc")

这样的代码为什么不能执行预期的动作呢?确实已经将“abc”放到了str中,条件表达式的值却不为真。这是为什么?

对于这样的疑问,通常给出的答案是“这个表达式不是在比较字符串的内容,它只是在比较指针”,其实还可以给出另外一个答案:

字符串其实就是char类型的数组,也就是说它不是标量,当然在C里面不能用==进行比较了。

来讨论一下这个问题,对于(str == "abc")这个表达式,如果你正在使用CLion,你应该会得到一个warning: Result of comparison against a string literal is unspecified (use strcmp instead)。左侧是char *(指向了char数组的第一个元素),右侧也是char *(指向了字符串字面量的第一个元素),也就是书中所说的这是在比较指针。

另外,我还看见有大一同学写(str == "")这样的表达式来判断字符串是否为空,这样的比较正如上文所说,自然不能成功。如果真的需要比较,应当使用strcmp()函数。如果是为了判断字符串是否为空,可以这样:

1
if (strcmp(str, "")) { }
或者对初始化后的char型数组:
1
if (strlen(str) == 0) { }
一本正经地强调要初始化...因为不初始化用这个是很危险的,然后大部分同学用的Dev-C++也不会警告变量未初始化,也就很经常没写好初始化(其实就算有警告我也敢打赌好多同学还是强行忽略),然后喜欢说“本地过了就是过了,提示WA说明OJ有问题”这样不负责任的话。讲真只要把后台数据拿出来试一下,99.9999%的锅都是学生的锅。(摊手.jpg)

好吧跑题跑得有点远,回来继续看字符串...又或者,直接使用C++的STL:

1
2
std::string str;
if (str == "") { }
当你使用string容器时,确实可以直接使用==符号来判断是否相等。这个时候可能有人要骂我了,先别急...
1
2
std::string str;
if (!str.empty()) { }
只需要调用empty()方法就可以判断str是否为空了。

1.2.1 恶名昭著的指针究竟是什么

Page 16

关于“指针”一词,在K&R中有下面这样的说明(第5章“指针和数组”的开头部分):

指针是一种保存变量地址的变量,在C中频繁地使用。

其实在表达上,这样的说明是有很大问题的。总会让人感觉,一旦提起指针,就要把它当作变量的意思。实际上并非总是如此。

此外,在C语言标准中最初出现“指针”一词的部分,有这样一段话:

指针类型(pointer type)可由函数类型、对象类型或不完全的类型派生,派生指针类型的类型称为引用类型。指针类型描述一个对象,该类对象的值提供对该引用类型实体的引用。由引用类型 T 派生的指针类型有时称为“(指向)T的指针”。从引用类型构造指针类型的过程称为“指针类型的派生”。这些构造派生类型的方法可以递归地应用。

这段话的内容也许会让你一头雾水。那就让我们先关注第一句话吧,那里出现了“指针类型”一词。

提到“类型”,立刻会让人想起“int类型”、“double类型”等。同样,在C语言中也存在“指针类型”这样的类型。

“指针类型”其实不是单独存在的,它是由其他类型派生而成的。以上对标准内容的引用中也提到“由引用类型T派生的指针类型有时称为‘(指向) T的指针’”。

也就是说,实际上存在的类型是“指向int的指针类型”、“指向double的指针类型”。

因为“指针类型”是类型,所以它和int类型、double类型一样,也存在“指针类型变量”和“指针类型的值”。糟糕的是,“指针类型”、“指针类型变量”和“指针类型的值”经常被简单地统称为“指针”,所以非常容易造成歧义,这一点需要提高警惕。

要点

先有“指针类型”。

因为有了“指针类型”,所以有了“指针类型的变量”和“指针类型的值”。

比如,在C中,使用int类型表示整数。因为int是“类型”,所以存在用于保存int型的变量,当然也存在int型的值。

指针类型同样如此,既存在指针类型的变量,也存在指针类型的值。

因此,几乎所有的处理程序中,所谓的“指针类型的值”,实际是指内存的地址

也不知道摘这么长一段出来有没有人看...主要是挺经常要跟大二的学生解释这个问题:“指针类型”是由其它类型派生而成的,而非指向任何不同对象的指针都是同一种类型(好吧这后半句我也解释不清楚,谁让我根本不知道各位同学是怎么被教坏的呢)。

还有个更要命的问题就是...学校的习题基本上是让你拿几个指针处理各种数组和字符串,也不讲讲指针和数组之间的区别,最后我看到的都是一堆风格奇差的代码和各种类型不匹配...(摊手.jpg)

后续慢慢来谈上面的这些问题,特别是语法糖之类的部分。

1.2.2 和指针的第一次亲密接触

Page 19

前面曾经提到,因为存在“指针类型”,所以存在“指针类型的变量”和“指针类型的值”。这里输出的“地址”,是指“指针类型的值”。

另外,以上的例子在使用printf()输出指针的值时,使用了参数%p。很多人都使用过%x 这样的参数。遗憾的是,这种使用方式是错误的。关于这点的解释,请参照1.2.3节。

补充 关于int main(void)

Page 22

在C语言标准中,关于main()函数的使用只有如下两种方式:

int main(int argc, char *argv[])

或者

int main(void)

尽管如此,还是可以在一些入门书籍中遇到

void main(void)

这样的写法,这是错误的。确实,就算是这么写,很多程序也能动起来。但是在有些环境下,编译器可能会报告一些警告信息。

main 函数返回一个 int 类型的值,因此在处理的最后必须有 return (现在的很多编译器都会提示没有return的警告)。

本书所有例程的 main 函数的末尾都写了 return 0;。

返回0表示通知运行环境程序“正常结束”。

补充 NULL、0和'\0'

Page 28

经常有一种错误的程序写法:使用NULL来结束字符串。

/*

通常,C的字符串使用'\0'结尾,可是因为strnpy()函数在src的长度大于len的情况下没有使用'\0'来结束,所以一板一眼地写了一个整理成C的字符串形式的函数(企图)

*/

void my_strncpy(char *dest, char *src, int len) {

strncpy(dest, src, len);

dest[len] = NULL; ←使用NULL来结束字符串!!

}

上面的代码,尽管在某些运行环境下能跑起来,但无论怎样它就是错误的。因为字符串是使用“空字符”来结束的,而不是用空指针来结束。

在C语言标准中,空字符的定义为“所有的位为0的字节称为空字符(null character)”(5.2.1)。也就是说,空字符是值为0的字符。

空字符在表现上通常使用'\0'。因为'\0'是常量,所以实际上它等同于0。也许有些吓到你了,'\0'呀'a'呀什么的,它们的数据类型其实并不是char,而是int(注)。

注:如果是C++,就不是这个结论了。

https://stackoverflow.com/questions/433895/why-are-c-character-literals-ints-instead-of-chars

1.2.6 实践——swap函数

Page 31

调用C的函数,参数传递往往是传值,这种方式传递的是参数的副本。

Page 33

说个题外话,如果仅仅是需要交换整型变量的值,完全不使用临时变量也是可以的。比如使用下面的宏定义:

#define SWAP(a, b) (a += b, b = a - b, a -= b)

在这种方式(还可以使用异或运算符)下,在颠倒使用同一个变量时,这个程序是不能正常运行的。比如你写了SWAP(a[i], a[j]),并且恰巧i == j,那我只能恭喜你中招了。当然,如果你能担保这种情况永远不可能出现,使用这个宏也未尝不可。

补充 形参和实参

Page 33

几乎所有的 C语言的入门书籍中,都会讲解“形参”和“实参”的概念。但是它们还是经常被轻易混淆。

实参是调用函数时的参数。

func(5); ←这里的5是实参。

形参是接受实参的一方。

void func(int hoge) ←这里的hoge是形参 { }

1.3.3 下标运算符[]和数组是没有关系的

Page 39

在前一小节的“改写版”例程中,像下面这样将指针指向数组的初始元素。

p = &array[0];

其实也可以写成下面这样:

p = array;

对于这种写法,很多C语言的入门书籍是这样说明的:

在 C中,如果在数组名后不加[],单独地只写数组名,那么此名称就表示“指向数组初始元素的指针”。

在这里,我可以负责地告诉你,上面的说明是错误的*。

* 如果考虑给人留点面子,其实我应该这么说:”不能说这个说明总是对的。“可是考虑一下听到这个说明的人如何解释它,就感觉还不如痛痛快快地指出来”这个说明完全是错误的“。

又惊着你了吧?

在 C的世界里,事到如今你再去否定“数组名后不加[],就代表指向初始元素的指针”这个“强大的”误解显得有点无奈。对于这种已经深入人心的观点,你突然放言它其实是个误解,可能很多人无法接受。下面让我们依法来证明。

将&array[0]改写成array,“改写版”的程序甚至可以写成下面这样:

p = array; ←只是改写了这里,可是……

for (i = 0; i < 5; i++) {

printf("%d\n", *(p + i));

}

另外,程序中*(p + i)也可以写成 p[i]。

p = array;

for (i = 0; i < 5; i++) {

printf("%d\n", p[i]);

}

也就是说,

*(p + i)

p[i]

是同样的意思。可以认为后面的写法是前面的简便写法。

在这个例子中,最初通过 p = array;完成了向 p 的赋值,但之后 p 一直没有发生更改。所以,早知如此,何必当初偏要多声明一个p,还不如一开始就写成array呢。

for (i = 0; i < 5; i++) {

printf("%d\n", array[i]);

}

呀,好像又回去了呢。

结论就是,

p[i]

这种写法只不过是

*(p + i)

这种写法的简便写法,除此之外,它毫无意义。array[i]和p[i]有什么不一样吗?array[i]也可以像 p[i]一样,将 array 解读成“指向数组的初始元素的指针”。

也就是说,存在

int array[5];

这样的声明的时候,“一旦后面不追加[],只写array”并不代表要使array具有指向数组第1 个元素的指针的含义,无论加不加[],在表达式中,数组都可以被解读成指针。

顺便说一下,对于这个规则来说,有三个小的例外,我们会在第3章作详细说明。

你可以认为这是一个哗众取宠的异端邪说,但至少在语法上,数组下标运算符[]和数组无关。

这里也是C的数组下标从0开始的理由之一。

要点

【非常重要!!】

表达式中,数组可以解读成“指向它的初始元素的指针”。尽管有三个小例外,但是这和在后面加不加[]没有关系。

要点

p[i]是*(p + i)的简便写法。

下标运算符[]原本只有这种用法,它和数组无关。

需要强调的是,认为[]和数组没有关系,这里的[]是指在表达式中出现的下标运算符[]。

声明中的[],还是表达数组的意思。也就是说,声明中的[]和表达式中的[]意义完全不同。表达式中的*和声明中的*的意义也是完全不同的。这些现象使得 C语言的声明在理解上变得更加扑朔迷离……对此,第3章将会进行详细的说明。

此外,如果将 a + b 改写成 b + a,表达式的意义没有发生改变,所以你可以将*(p + i)写成*(i + p)。其次,因为 p[i]是*(p + i)的简便写法,实际上它也可以写成i[p]。

引用数组元素的时候,通常我们使用array[5]这样的写法。其实,就算你写成 5[array],还是可以正确地引用到你想要的元素。可是,这种写法实在太另类了,它不能给我们带来任何好处。

要点

p[i]可以写成i[p]。

要点

【比上面这个要点更重要的要点】

但是别写成那样。

详细得不需要我再废话什么了...

补充 语法糖

Page 42

p[i]是*(p+i)的简单写法,实际上,至少对于编译器来说,[]这样的运算符完全可以不存在。

可是,对于人类来说,*(p + i)这种写法在解读上比较困难,写起来也麻烦(键入量大)。因此,C语言引入了[]运算符。

就像这样,这些仅仅是为了让人类容易理解而引入的功能,的确可以让我们感受到编程语言的甜蜜味道(容易着手),有时我们称这些功能为语法糖(syntax sugar或者syntactic sugar)。

语法糖

语法盐

评论