第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, "")) { } |
1 | if (strlen(str) == 0) { } |
好吧跑题跑得有点远,回来继续看字符串...又或者,直接使用C++的STL:
1 2 | std::string str; if (str == "") { } |
1 2 | std::string str; if (!str.empty()) { } |
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)。