第2章 C 语言基本概念
2.1. 字符串字面量(String Literal)
字符串字面量是用一对双引号包围的一系列字符。
2.2. 把换行符加入到字符串中
错误方式
printf("hello World\n");
正确方式
printf("hello " "World");
根据 C 语言标准,当两条或者更多条字符串字面量相连时(仅用空白字符分割),编译器必须把它们合并成单独一条字符串。这条规则允许把字符串分割放在两行或更多行中。
2.3. 编译器是完全移走注释还是用空格替换掉注释?
根据标准 C,编译器必须用一个空格符替换每条注释语句。a/**/b = 0
相当于 a b = 0
。
2.4. 在注释中嵌套一个新的注释是否合法?
在标准 C 中是不合法的,如果需要注释掉一段包含注释的代码,可以使用如下方法:
#if 0 printf("hello World\n"); #endif
这种方式经常称为「条件屏蔽」。
2.5. C 程序中使用 //
作为注释的开头,如下所示,是否合法?
// This is a comment.
在标准 C 中是不合法的,//
注释是 C++ 的方式,有一些 C 编译器也支持,但是也有一部分编译器不支持,为了保持程序的可移植性,因此应该尽量避免使用 //
。
第3章 格式化的输入/输出
3.1. 转换说明(Conversion specification)
转换说明以 %
开头,转化说明用来表示填充位置的占位符。
3.2. printf 中转换说明的格式
转化说明的通用格式为:
%m.pX 或者 %-m.pX
对于这个格式的解释如下:
m 和 p 都是整形常量,而 X 是字母。
m 和 p 都是可选项。
如果省略 m,小数点要保留,例如
%.2f
。如果省略 p,小数点也要一起省略,例如
%10f
。
m 表示的是最小字段宽度(minimum field width),指定了要显示的最小字符数量。
printf("%4d\n",1); printf("%4d\n",11); printf("%4d\n",111); printf("%4d\n",1111); printf("---------------\n"); printf("%4d\n",12345); printf("---------------\n"); printf("%-4d\n",1); printf("%-4d\n",11); printf("%-4d\n",111); printf("%-4d\n",1111);
如果要打印的数值比 m 个字符少,那么值在字段内是右对齐的(换句话说,在数值前面放置额外的空格)。
如果要打印的数值比 m 个字符多,那么字段宽度会自动扩展为需要的尺寸,而不会丢失数字。
在 m 前放上一个负号,会发生左对齐。
举例如下:
p 表示的是精度(precision),p 的含义依赖于转换说明符(conversion specifier) X 的值,X 表明需要对数值进行哪种转换。常见的转换说明符有:
printf("%.3d\n",1); printf("%.5d\n",1); printf("%.6d\n",1); printf("--------------------\n"); printf("%e\n",12.1); printf("%.2e\n",12.1); printf("%.0e\n",12.1); printf("--------------------\n"); printf("%f\n",12.1); printf("%.2f\n",12.1); printf("%.0f\n",12.1); printf("--------------------\n"); printf("%g\n",12.120000); printf("%g\n",12.12345678); printf("%.2g\n",12.1); printf("%.0g\n",12.1);
d —— 表示十进制的整数。当 x 为 d 时,p 表示可以显示的数字的最少个数(如果需要,就在数前加上额外的零),如果忽略掉 p,则默认它的值为 1。
e —— 表示指数(科学计数法)形式的浮点数。当 x 为 e 时,p 表示小数点后应该出现的数字的个数(默认为 6),如果 p 为 0,则不显示小数点。
f —— 表示「定点十进制」形式的浮点数,没有指数。p 的含义与在说明符 e 时一样。
g —— 将 double 值转化为 f 形式或者 e 形式,形式的选择根据数的大小决定。仅当数值的指数部分小于
-4,或者指数部分大于或等于精度时,会选择 e 形式显示。当 x 为 g 时,p 表示可以显示的有效数字的最大数量(默认为 6)。与 f 不同,g 的转换将不显示尾随零。举例如下:
3.3. %i 和 %d 有什么区别?
在 printf 中,两者没有区别。在 scanf 中,%d 只能和十进制匹配,而 %i 可以匹配八进制、十进制或者十六进制。如果用户意外将 0 放在数字的开头处,那么用 %i 代替 %d 可能有意外的结果。由于这是一个陷阱,所以坚持使用 %d。
3.4. printf 如何显示字符 %?
printf 格式串中,两个连续的 % 将显示一个 %,如下所示:
printf("%%");
第4章 表达式
4.1. 运算符 /
和 %
注意的问题
运算符
/
通过丢掉分数部分的方法截取结果。因此,1/2
的结果是 0 而不是 0.5。运算符
%
要求整数操作数,如果两个操作数中有一个不是整数,无法编译通过。当运算符
/
和%
用于负的操作数时,其结果与实现有关。-9/7
的结果既可以是 -1 也可以是 -2。-9%7
的结果既可以是 2 也可以是 -2。
4.2. 由实现定义(implementation-defined)
由实现定义是一个术语,出现频率很高。
C 语言故意漏掉了语言未定义部分,并认为这部分会由「实现」来具体定义。
所谓实现,是指软件在特定平台上编译、链接和执行。
C 语言为了达到高效率,需要与硬件行为匹配。当 -9 除以 7 时,一些机器产生的结果可能是 -1,而另一些机器的结果可能是 -2。C 标准简单的反映了这一现实。
最好避免编写与实现行为相关的程序。
4.3. 赋值运算符「=」
许多语言中,赋值是语句,但是 C 语言中,赋值是运算符,换句话说,赋值操作产生结果。
赋值表达式
v = e
产生的结果就是赋值运算后 v 的值。运算符
=
是右结合的,i = j = k = 0
相当与(i = (j = (k = 0)))
。由于结果发生了类型转换,串联赋值运算的最终结果不是期望的结果,如下所示:
int i; float f; f= i =33.6; printf("i=%d,f=%f",i,f);
4.4. 左值
左值(lvalue)表示储存在计算机内存中的对象,而不是常量或计算结果。
变量是左值,诸如 10 或者 2*i 这样的表达式不是左值。
赋值运算符要求它左边的操作数必须是左值,以下表达式是不合法的,编译不通过:
12 = i;
i + j = 0;
-i = j;
4.5. 子表达式的求值顺序
C 语言没有定义子表达式的求值顺序(除了含有逻辑与运算符及逻辑或运算符、条件运算符以及逗号运算符的子表达式)。因此,在表达式 (a + b) * (c - d)
中,无法确定子表达式 (a + b)
是否在子表达式 (c - d)
之前求值。
这样的规定隐含着陷阱,如下所示:
a = 5; c = (b = a + 2) - (a = 1);
如果先计算
b = a + 2
,则 b = 7,c = 6。如果先计算
a = 1
,则 b = 3,c = 2。
为了避免此问题,最好不要编写依赖子表达式计算顺序的程序,一个好的建议是:不在字表达式中使用赋值运算符,如下所示:
a = 5; b = a + 2; a = 1; c = b - a;
4.6. v += e
一定等价与 v = v + e
么?
不一定,如果 v 有副作用,则两者不想等。
计算
v += e
只是求一次 v 的值,而计算v = v + e
需要求两次 v 的值。任何副作用都能导致两次求 v 的值不同。如下所示:a [i++] += 2; // i 自增一次a [i++] = a [i++] + 2; // i 自增两次
4.7. ++ 和 -- 是否可以处理 float 型变量?
可以,自增和自减可以用于所有数值类型,但是很少使用它们处理 float 类型变量。如下所示:
float f = 1.3; printf("%f",++f);
4.8. 表达式的副作用(side effect)
表达式有两种功能,每个表达式都产生一个值(value),同时可能包含副作用(side effect)。副作用是指改变了某些变量的值。如下所示:
20 // 这个表达式的值是 20,它没有副作用,因为它没有改变任何变量的值。 x=5 // 这个表达式的值是 5,它有一个副作用,因为它改变了变量 x 的值。 x=y++ // 这个表达示有两个副作用,因为改变了两个变量的值。 x=x++ // 这个表达式也有两个副作用,因为变量 x 的值发生了两次改变。
4.9. 顺序点(sequence point)
表达式求值规则的核心在于顺序点。
顺序点的意思是在一系列步骤中的一个「结算」的点,C 语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。
C 标准规定代码执行过程中的某些时刻是 Sequence Point,当到达一个 Sequence Point 时,在此之前的 Side Effect 必须全部作用完毕,在此之后的 Side Effect 必须一个都没发。至于两个 Sequence Point 之间的多个 Side Effect 哪个先发生哪个后发生则没有规定,编译器可以任意选择各 Side Effect 的作用顺序。
C 语言中常见顺序点的位置有:
分号
;
未重载的逗号运算符的左操作数赋值之后,即
;
处。未重载的
||
运算符的左操作数赋值之后,即||
处。未重载的
&&
运算符的左操作数赋值之后,即&&
处。三元运算符
? :
的左操作数赋值之后,即?
处。在函数所有参数赋值之后但在函数第一条语句执行之前。
在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前。
在每一个完整的变量声明处有一个顺序点,例如
int i, j;
中逗号和分号处分别有一个顺序点。for 循环控制条件中的两个分号处各有一个顺序点。
第5章 选择语句
5.1. 表达式 i < j < k
是否合法?
此表达式是合法的,相当于 (i < j) < k
,首先比较 i 是否小于 k,然后用比较产生的结果 1 或 0 来和 k 比较。
5.2. 如果 i 是 int 型,f 是 float 型,则条件表达式 i > 0 ? i : f
是哪一种类型的值?
当 int 和 float 混合在一个表达式中时,表达式类型为 float 类型。如果 i > 0 为真,那么变量 i 转换为 float 型后的值就是表达式的值。
第7章 基本类型
7.1. 读 / 写整数
读写无符号数时,使用 u、o、x 代替 d。
u:表示十进制。
o : 表示八进制。
x:表示十六进制。
读写短整型时,在 d、u、o、x 前面加上 h。
读写长整型时,在 d、u、o、x 前面加上 l。
7.2. 转义字符(numeric escape)
在 C 语言中有三种转义字符,它们是:一般转义字符、八进制转义字符和十六进制转义字符。
一般转义字符:这种转义字符,虽然在形式上由两个字符组成,但只代表一个字符。常用的有:
\a
\n
\t
\v
\b
\r
\f
\\
\’
\"
八进制转义字符:
它是由反斜杠
\
和随后的 1~3 个八进制数字构成的字符序列。例如,\60
、\101
、\141
分别表示字符0
、A
和a
。因为字符0
、A
和a
的 ASCII 码的八进制值分别为 60、101 和 141。字符集中的所有字符都可以用八进制转义字符表示。如果你愿意,可以在八进制数字前面加上一个 0 来表示八进制转移字符。
十六进制转义字符:
它是由反斜杠
/
和字母 x(或 X)及随后的 1~2 个十六进制数字构成的字符序列。例如,\x30
、\x41
、\X61
分别表示字符0
、A
和a
。因为字符0
、A
和a
的 ASCII 码的十六进制值分别为
0x30、0x41 和 0x61。可见,字符集中的所有字符都可以用十六进制转义字符表示。
由上可知,使用八进制转义字符和十六进制转义字符,不仅可以表示控制字符,而且也可以表示可显示字符。但由于不同的计算机系统上采用的字符集可能不同,因此,为了能使所编写的程序可以方便地移植到其他的计算机系统上运行,程序中应少用这种形式的转义字符。
7.3. 读字符的两种惯用法
while (getchar() != '\n') /* skips rest of line */ ;
while ((ch = getchar()) == ' ') /* skips blanks */ ;
7.4. sizeof 运算符
sizeof 运算符返回的是无符号整数,所以最安全的办法是把其结果转化为 unsigned long 类型,然后用 %lu 显示。
printf("Size of int: %lu\n", (unsigned long)sizeof(int));
7.5. 为什么使用 %lf 读取 double 的值,而用 %f 进行显示?
一方面,函数 scanf 和 printf 有可变长度的参数列表,当调用带有可变长度参数列表的函数时,编译器会安排 float 自动转换为 double,其结果是 printf 无法分辨 float 和 double。所以在 printf 中 %f 既可以表示 float 又可以表示 double。
另一方面,scanf 是通过指针指向变量的。%f 告诉 scanf 函数在所传地址上存储一个 float 类型的值,而 %lf 告诉 scanf 函数在所传地址上存储一个 double 类型的值。这里两者的区别很重要,如果给出了错误的转换,那么 scanf 可能存储错误的字节数量。
第11章 指针
11.1 指针总是和地址一样么?
通常是,但不总是。在一些计算机上,指针可能是偏移量,而不完全是地址。
char near *p; /*定义一个字符型“近”指针*/char far *p; /*定义一个字符型“远”指针*/char huge *p; /*定义一个字符型“巨”指针*/
近指针、远指针、巨指针是段寻址的 16bit 处理器的产物(如果处理器是 16 位的,但是不采用段寻址的话,也不存在近指针、远指针、巨指针的概念),当前普通 PC 所使用的 32bit 处理器(80386 以上)一般运行在保护模式下的,指针都是 32 位的,可平滑地址,已经不分远、近指针了。但是在嵌入式系统领域下,8086 的处理器仍然有比较广泛的市场,如 AMD 公司的 AM186ED、AM186ER 等处理器,开发这些系统的程序时,我们还是有必要弄清楚指针的寻址范围。
近指针
近指针是只能访问本段、只包含本段偏移的、位宽为16位的指针。
远指针
远指针是能访问非本段、包含段偏移和段地址的、位宽为32位的指针。
巨指针
和远指针一样,巨指针也是 32 位的指针,指针也表示为 16 位段:16 位偏移,也可以寻址任何地址。它和远指针的区别在于进行了规格化处理。远指针没有规格化,可能存在两个远指针实际指向同一个物理地址,但是它们的段地址和偏移地址不一样,如 23B0:0004 和 23A1:00F4 都指向同一个物理地址 23B04!巨指针通过特定的例程保证:每次操作完成后其偏移量均小于 10h,即只有最低 4 位有数值,其余数值都被进位到段地址上去了,这样就可以避免 Far 指针在 64K 边界时出乎意料的回绕的行为。
11.2. const int * p、int * const p、const int * const p
const int * p
保护 p 指向的对象。
int * const p
保护 p 本身。
const int * const p
同时保护 p 和它指向的对象。
第12章 指针和数组
12.1. * 运算符和 ++ 运算符的组合
表达式 | 含义 |
---|---|
*p++ 或 *(p++) | 自增前表达式的值是 *p,然后自增 p |
(*p)++ | 自增前表达式的值是 *p,然后自增 *p |
*++p 或 *(++p) | 先自增 p,自增后表达式的值是 *p |
++*p 或 ++(*p) | 先自增 *p,自增后表达式的值是 *p |
12.2. i[a] 和 a[i] 是一样的?
是的。
对于编译器而言,i[a] 等同与 *(i+a),a[i] 等同与 *(a+i),所以两者相同。
12.3. *a 和 a[]
在变量声明中,指针和数组是截然不同的两种类型。
在形式参数的声明中,两者是一样的,在实践中,*a 比 a[] 更通用,建议使用 *a。
第13章 字符串
13.1. 字符串字面量的赋值
char *p; p = "abc";
这个赋值操作不是复制 "abc" 中的字符,而是使 p 指向字符串的第一个字符。
13.2. 如何存储字符串字面量
从本质上讲,C 语言将字符串字面量作为字符数组来处理,为长度为 n 的字符串字面量分配 n+1 的内存空间,最后一个空间用来存储空字符
\0
。既然字符串字面量作为数组来储存,那么编译器会将他看作 char* 类型的指针。
13.3. 对指针添加下标
char ch ch = "abc"[1];
ch 的新值将是 b。
如下,将 0 - 15 的数转化成等价的十六进制:
char digit_to_hex_char (int digit){ return "0123456789ABCDEF"[digit]; }
13.4. 允许改变字符串字面量中的字符
char *p = "abc"; *p = 'b'; /* string literal is now "bbc" */
不推荐这么做,这么做的结果是未定义的,对于一些编译器可能会导致程序异常。
针对 "abc" 来说,会在 stack 分配 sizeof(char *) 字节的空间给指针 p,然后将 p 的值修改为 "abc" 的地址,而这段地址一般位于只读数据段中。
在现代操作系统中,可以将一段内存空间设置为读写数据、只读数据等等多种属性,一般编译器会将 "abc" 字面量放到像 ".rodata" 这样的只读数据段中,修改只读段会触发 CPU 的保护机制 (#GP) 从而导致操作系统将程序干掉。
13.5. 字符数组和字符指针
char ch[] = "hello world";char *ch = "hello world";
两者区别如下:
在声明为数组时,就像任意元素一样,可以修改存储在 ch 中的字符。在声明为指针时,ch 指向字符串字面量,而修改字符串字面量会导致程序异常。
在声明为数组时,ch 是数组名。在声明为指针时,ch 是变量,这个变量可以在程序执行期间指向其他字符串。
13.6. printf 和 puts 函数写字符串
转换说明 %s 允许 printf 写字符串,printf 会逐个写字符串的字符,直到遇到空字符串为止(如果空字符串丢失,则会越过字符串末尾继续写,直到在内存某个地方找到空字符串为止)。
char p[] = "abc";printf("p=%s\n",p);
转换说明
%m.ps
和%-m.ps
显示字符串m 表示在大小为 m 的域内显示字符串,对于超过 m 个字符的字符串,显示完整字符串;对于少于 m 个字符的字符串,在域内右对齐。为了强制左对齐,在 m 前加一个负号。
p 代表要显示的字符串的前 p 个字符。
%m.ps
表示字符串的前 p 个字符在大小为 m 的域内显示。
puts 的使用方式如下,str 就是需要显示的字符串。在写完字符串后,puts 总会添加一个额外的换行符。
puts(str);
13.7. scanf 和 gets 函数读字符串
转换说明 %s 允许 scanf 函数读入字符串,如下所示。不需要在 str 前加运算符 &,因为 str 是数组名,编译器会自动把它当作指针来处理。
scanf("%s", str);
调用时,scanf 会跳过空白字符,然后读入字符,并且把读入的字符存储到 str 中,直到遇到空白字符为止。scanf 始终会在字符串末尾存储一个空字符
\0
。用 scanf 函数读入字符串永远不会包括空白字符。因此,scanf 通常不会读入一整行输入。
gets 函数可以读入一整行输入。类似 scanf,gets 函数把读到的字符存储到数组中,然后存储一个空字符。
gets(str);
两者区别:
gets 函数不会在开始读字符之前跳过空白字符(scanf 函数会跳过)。
gets 函数会持续读入直到找到换行符为止(scanf 会在任意空白符处停止)。
gets 会忽略换行符,不会把它存储到数组里,而是用空字符代替换行符。
scanf 和 gets 函数都无法检测何时填满数据,会有数组越界的可能。
使用 %ns 代替 %s 可以使 scanf 更安全,n 代表可以存储的最大字符数量。
由于 gets 和 puts 比 scanf 和 printf 简单,因此通常运行也更快。
13.8. 自定义逐个字符读字符串函数
在开始存储之前,不跳过空白字符。
在第一个换行符处停止读取(不存储换行符)。
忽略额外的字符。
#include <stdio.h>#include <stdlib.h>int read_line(char[] ,int);int main(void){ char str[10]; int n = 10; read_line(str, n); printf("--- end ---"); return 0; }int read_line(char ch[], int n){ char tmp_str; int i = 0; while((tmp_str = getchar()) != '\n') { if(i<n) { ch[i++] = tmp_str; } } ch[i] = '\0'; printf("message is:%s\n", ch); return i; }
13.9. 字符串处理函数
C 语言字符串库 string.h 的几个常见函数:
strcpy 函数(字符串复制)
char* strcpy (char* s1, const char* s2);
把字符串 s2 赋值给 s1 直到(并且包括)s2 中遇到的一个空字符为止。
返回 s1。
如果 s2 长度大于 s1,那么会越过 s1 数组的边界继续复制,直到遇到空字符为止,会覆盖未知内存,结果无法预测。
strcat 函数(字符串拼接)
char* strcat (char* s1, const char* s2);
把字符串 s2 的内容追加到 s1 的末尾
返回 s1。
如果 s1 长度不够 s2 的追加,导致 s2 覆盖 s1 数组末尾后面的内存,结果是不可预测的。
strcmp 函数(字符串比较)
int strcmp (const char* s1, const char* s2)
abc
小于bcd
,abc
小于abd
,abc
小于abcd
。比较两个字符串时,strcmp 会查看表示字符的数字码。以 ASCII 字符集为例:
所有大写字母(65 ~ 90)都小于小写字母(97 ~ 122)。
数字(48 ~ 57)小于字母。
空格符(32)小于所有打印字符。
比较 s1 和 s2,根据 s1 是否小于、等于、大于 s2,会返回小于、等于、大于 0 的值。
strcmp 利用字典顺序进行字符串比较,比较规则如下:
strlen 函数(求字符串长度)
size_t strlen (const char* s1)
返回 s1 中第一个空字符串前的字符个数,但不包括第一个空字符串。
当数组作为函数实际参数时,strlen 不会测量数组的长度,而是返回数组中的字符串长度。
13.10. 字符串惯用法
strlen 的简单实现
size_t strlen(const char * str) { const char *cp = str; while (*cp++ ) ; return (cp - str - 1 ); }
strcat 的简单实现
char* strcat ( char * dst , const char * src ){ char * cp = dst; while( *cp ) cp++; /* find end of dst */ while( *cp++ = *src++ ) ; /* Copy src to end of dst */ return( dst ); /* return dst */}
13.11. 存储字符串数组的两种方式
二维字符数组
char planets[][8] = {"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"};
这种方式浪费空间,如下所示:
字符串指针数组
char *planets[] = {"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"};
推荐这种方式,如下所示:
13.12. read_line 检测读入字符是否失败
增加对 EOF 的判断。
int read_line(char ch[], int n){ char tmp_str; int i = 0; while((tmp_str = getchar()) != '\n' && ch != EOF) { if(i<n) { ch[i++] = tmp_str; } } ch[i] = '\0'; printf("message is:%s\n", ch); return i; }
作者:gfson
链接:https://www.jianshu.com/p/3e6ced8bad88
共同学习,写下你的评论
评论加载中...
作者其他优质文章