(一)基础问题
1.关于return 0
在main函数中,在执行的最后设置一个return 0;
语句,当主函数结束时,得到的函数值为0,当执行main函数中出现错误或者异常时,函数值为一个非0的整数。这个函数值时返回给调用main函数的操作系统。程序员可以利用操作系统指令检查main函数的返回值,从而判断main函数是否正常执行,并根据次决定以后的操作,如果在程序中不写这个语句,有的C编译系统会在目标程序中自动加上这一个语句,为了程序规范和可移植性,希望读者写的程序一律将main函数指定为int型,并在main函数最后加一个return 0;
语句。
2.标准输入的思考题
int ch = 0;
while ((ch = getchar()) != EOF && ch != '\n')
;
效果相同的代码
int ch = 0;
ch = getchar();
while (ch != EOF && ch != '\n')
ch = getchar();
经验丰富的C程序员在阅读和编写这类语句不会产生困难。
这段代码的含义是什么?
剔除当前输入行最后的剩余字符
首先,getchar
函数从标准输入读取一个字符并返回它的值,如果输入中不存在任何字符就会返回EOF
,用于提示文件末尾。从getchar
返回的值赋给ch,然后把它与EOF
进行比较,赋值表达式两端加上括号用于确保赋值运算符先于比较操作进行,如果ch=EOF
,整个表达式为假,循环将终止。否则,再把赋值后的ch与换行符进行比较,如果两者相等循环也终止。
为什么ch被声明为整型?
getchar原型
int ch = getchar();
错误代码
char ch;
while ((ch = getchar()) != EOF)
;
规定EOF
是一个int
型的负数常量,通常编译器把它值定义为-1,如果把getchar
的返回值赋给char
型的变量,32位转8位会发生截断,被截断的值被提升为整型与EOF
比较,有可能截断后的值与EOF
相同从而导致循环中断,把ch声明为整形可以防止输入读取的字符意外地被解释为EOF
。所以选择int
类型作为函数返回值的接受类型。当这段存在错误的代码在使用有符号字符集的机器上运行时,如果读取一个值为\377的字节(八进制377代表的字符,二进制255)时,循环将会终止,因为这个值截断再提升之后与EOF
相等。这段代码再使用无符号字符集的机器上运行时,这个循环将永远不会终止
3.字符\ddd \xddd
\ddd
ddd
表示1-3个八进制数,这个转义字符代表相应八进制数值所代表的字符
\xddd
\xddd
表示1-3个十六进制数,这个转义字符代表相应十六进制所代表的字符
注意,如果其结果值的大小超过了字符范围-128-127,其结果就是未定义的。
\40的值是多少 '\40' = 32
\100的值是多少 '\100' = 64
\x40的值是多少 '\x40' = 64
\x100的值是多少 占据12位,无法存储在一个字符内,计算结果因编译器而异
\0123的值是多少 vs2019和DEV-C++上的结果是2611,《C和指针》描述这是两个字符'\012'和'3'
int main()
{
char ch1 = '\0123';
printf("%d, %d\n", ch1, '\0123');
}
会得到 51, 2611
32位的(int)2611截断成8位的(char)51
4.常变量与符号常量有什么不同?
#define PI 3.1415926 //定义符号常量
const float pi=3.1415926 //定义常变量
符号常量PI和常变量pi都代表3.1415926,在程序中都能使用,但二者性质不同:
定义符号常量用#define指令,它是预编译指令,它只是用符号常量代表一串字符串,在预编译时进行字符替换,在预编译后,符号常量就不存在了(全部被替换成3.1415926),对符号常量的名字时不分配存储单元,是不占内存空间的。而常变量是要占用存储单元,有变量值,只是性质为只读变量不允许改变。符号常量和常变量都不能出现赋值号的左边。
5.自定义编写函数strncpy
将字符串src的n个字符赋给字符串dst
如果src的长度小于n,全部复制后,继续添加NUL字符
否则将src的前n个字符复制给dst
void copy_n(char dst[], char src[], int n)
{
int dst_index, src_index = 0;
for (dst_index = 0; i < n; ++i)
{
dst[dst_index] = src[src_index];
if (src[srt_index] != '\0')
{
srt_index++;
}
}
}
(二)数据类型
在C语言中,仅有4中基本的数据类型
整型、浮点型、指针、聚合类型(如数组和数据结构)。
只有整型(包括字符型)数据可以加signed或者unsigned修饰符,实型数据不能加。
对于无符号整型数据输出用%u格式输出,%u表示用无符号十进制的格式输出。
正数的补码、反码、原码相同。
负数的反码是原码按位取反(符号位不变),补码是在反码加一。
字符是以整数形式(字符的ASCII代码)存放在内存单元中的。
同一个字母,用小写表示的字符的ASCII代码比用大写的ASCII代码大32。
C标准中并未规定是按signed char 处理还是按unsigned char 处理,由各编译系统字节决定。
C语言允许一种特殊形式的字符常量,就是以字符\开头的字符序列。
\' 一个单撇号'
\" 一个双撇号"
\? 一个问号?
\\ 一个反斜杠
\a 警告
\b 退格
\f 换页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\o、\oo、\ooo其中o代表一个八进制数字 与该八进制码对应的ASCII字符
\xh[h...]其中代表一个十六进制数字 与该十六进制码对应的ASCII字符
C本质上是一种自由形式的语言,这很容易诱使你把星号写在靠近类型的一侧
int* a;
int *a;
第一种声明与第二个声明具有相同的意思,而且看上去更为清楚,a被声明为类型为int*
的指针,但这并不是一个好的技巧,在以下的用法当中便会使用不当。
int* a, b, c;
只有a的类型int*
,b,c的类型都是int
int *a, *b, *c;
只有这样声明a,b,c的类型才是int*
。
2.1变量的属性
2.1.1作用域
2.1.1.1代码作用域
位于一对花括号之间的所有语句称为一个代码块。代码块的作用域:从标识声明的位置开始到该对应的代码块结束的位置。
对于嵌套代码块,如果标识同名,采取内部代码块屏蔽外部代码块
{
int a = 3;
{
int a = 2;
printf("%d", a);//内部的a屏蔽外部的a
}
}
输出3
对于非嵌套代码块,每个代码块无法访问另一个代码块的数据和方法,同名变量可以共享一个内存地址,这种共享不会带来任何危害,因为在任意时刻,两个非嵌套的代码块最多只有一个处于活动状态。
2.1.1.2文件作用域
任何在所有代码块之外的声明的标识符都具有文件作用域。作用域:从标识符的声明之处到所有的源文件结尾处都可以访问。
2.1.1.3函数作用域
只适用语句标签,语句标签用于goto语句。
2.1.1.4原型作用域
只适用于在函数原型中声明的参数名。
2.1.2链接属性
2.1.2.1external外部
标识符无论被声明多少次,位于多个源文件都表示同一个实体。
2.1.2.2internal内部
标识符在一个源文件内所有声明都是指同一个实体,但位于不同源文件的多个声明分属于不同的实体。
2.1.2.3none无
标识符总是被当作单独的个体。多处声明被当作独立不同的实体。
函数名和全局变量的缺省链接属性都是external也就是外部extern
。
- static+全局变量 变成源文件私有的全局变量,但标识符的存储类型和作用域不受影响。
- static+函数名 变成源文件私有的函数,防止被其他文件使用,但标识符的存储类型和作用域不受影响。
static只对缺省链接属性为external的声明才有改变链接属性的效果。
2.1.3存储类型
变量的存储类型是指存储变量值的内存类型,变量的存储类型决定变量何时创建、何时销毁以及它的值将保持多久。有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。
2.1.3.1auto自动
在代码块内部声明的变量的缺省类型是自动的,也就是说它存储于堆栈中,称为自动变量。
代码块局部变量在缺省的情况下为auto基于两个原因:
- 这些变量在需要才分配存储,这样可以减少内存的总需求量
- 在堆栈上为他们分配存储,可以有效实现递归。
在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行离开该代码块时,自动变量便被自动销毁。对于代码块内部声明的变量(自动变量),如果给它加上关键字static,可以使它的存储类型从auto变成static,但标识符的链接属性和作用域不受影响。它仍是只能在代码块内部按照名字访问。
2.1.3.2static静态
凡是在任何代码块之外声明的变量总是存储于静态内存中,不属于堆栈的内存,成为静态变量。静态变量在程序运行之前创建,在程序的整个执行期间都存在,始终保持着原先的值,除非给它赋一个不同的值或者程序结束。
2.1.3.3register寄存器
请求编译器尽可能地将变量存在CPU内部寄存器中,而不是通过内存寻址访问以提高效率。
注意点:
- register变量必须是能被CPU所接受的类型。
- register变量可能不存在于内存中,所以不能用取地址符来获取register变量的地址。
(三)运算符
%取余运算符要求参加运算的运算对象(即操作数)为整数,结果也是整数。
除%以外的运算符的操作数可以是任何算术类型。
自增运算符(++)和自减运算符(–)只能用于变量,而不能用于常量表达式。
在指针变量的使用中是使指针指向下一个地址。
优先级与结合性是运算符的重点学习内容。
逗号运算符是指在C语言中,多个表达式能用逗号分开,其中逗号分开的表达式分别计算,但整个表达式是最后一个表达式的值。
3.1复合运算符
a += expression;//把expression加到a
a = a + (expression);
功能相同,唯一的不同之处是+=操作符的左操做数只求值一次。
看一个复杂的例子
a[2 * (y - 6*f(x))] = a[2 * (y - 6*f(x))] + 1;
a[2 * (y - 6*f(x))] += 1;
在第一种形式,由于不知道函数f是否具有副作用,下表必须计算两次,一次在赋值号的左边,一次在赋值号的右边。而+=只用计算一次,效率更高。
3.2位运算
计算二进制中1的个数:
/*
循环检查数的最低位是否为1
*/
int count_one_bits(unsigned value)
{
int ones;
for (ones = 0; value != 0; value >>= 1)
{
if (value % 2 != 0)
{
ones++;
}
}
return ones;
}
int count_one_bits(unsigned value)
{
int ones;
for (ones = 0; value != 0, value >>= 1)
{
if ((value & 1) != 0)
{
ones++;
}
}
return ones;
}
3.3sizeof操作符
判断它的操作数的类型长度,以字节位单位表示,操作数既可以是个表达式,也可以是两边加上括号的类型名。
sizeof (int) sizeof x
判断表达式的长度不需要对表达式进行求值,所以sizeof(a = b + 1)
并没有对a进行赋值。
sizeof详解
3.4自增运算符
3.5运算符优先级以及结合性
参考运算符优先级
(四)指针
4.1指针作为参数
不能通过执行调用函数来改变实参指针变量的值(地址),但可以改变实参指针变量所指向元素的值。使用指针变量作为函数参数,在函数执行过程中指针变量指向的变量值发生变化,函数调用结束后这些变量值的变化仍然保留下来。通过指针变量作为函数参数,使变量值发生改变。
4.2指针与运算符的操作
基类型 *p = &基类型变量;
p++;p指向的是原地址加上一个基类型单位字节的地址。
*p++;
计算字符串长度的函数
size_t strlen(char *string)
{
int length = 0;
while (*string++ != '\0')
length++;
return length;
}
由于*和++运算符优先级相同,结合性是右结合性,自右向左
即*p++=*(p++);
先使用p的值,再自增。
4.3指针数组
int *array[10];//数组的每一个元素都是指向int类型的指针。
指针数组的每一个元素都是指向基类型的指针。指针数组常常用于字符串数组。
每一个指针元素都指向一个字符串常量.
char *words[] = {
"hello world",
"C",
"C++",
NULL//这里我们通常在末尾增加一个NULL指针,使在搜索这个表时可以检查表的结束,而无需知道表的长度
};
判断参数是否与一个关键字列表中的任何单词匹配,找到并返回索引值,否则返回-1
int lookup_keyword(char const * const desired_word, char const *keyword_table[],int const size)
{
char const **kwp;
for (kwp = keyword_table; kwp < keyword_table + size; ++kwp)
//如果指针数组优化末尾增加了NULL,那么不需要知道表的大小
//for (kwp = keyword_table; *kwp != NULL; ++kwp)
{
if (strcmp(desired_word, *kwp) == 0)
{
return kwp - keyword_table;
}
}
return -1;
}
4.4指针变量安全性问题
p = NULL;
指针变量可以有空值,即指针变量不指向任何变量,NULL
是一个符号常量,代表整数0,它使p指向地址为0的单元,系统保证该单元不存放有效数据。p的值为NULL
与未对p赋值是两个不同的概念。前者是有值的,不指向任何变量,后者虽然未对p赋值但并不等于p无值,只是它的值是一个无法预料的值,p可能指向一个事先未指定的单元。这种情况是危险的。对NULL
指针变量进行解引用是非法的,对指针进行解引用操作之前,必须确保指针非NULL
。
(五)数组
神秘和离题的例子
int array[10];
2[array] = 2;//这是什么意思
然而它是合法的,把它转换成对等的间接访问表达式,你会发现它的有效性:
*(2 + (array))
==
*(array + 2) = a[2]
也就是说那个看上去古怪的表达式与array[2]等同。
之所以可行,缘于C实现下标的方法,array[2]
与2[array]
对于编译器来说并无差别。
5.1一维数组
5.1.1一维数组名作为函数参数
实际上,C编译都是将形参数组名作为指针变量来处理的。一维数组名作为函数参数,实行“址传递”的过程,将实参数组首地址传递给形参指针变量,使形参数组和实参数组共用一段内存单元。
一维数组名代表首元素的地址,并不代表数组首地址。
5.1.2数组与指针
通过指针来使用数组的优势是:每次使用数组元素不必重新计算地址,p++这种操作是比较快的提高执行效率。
int a[10];// a = &a[0]
int *p1 = a + 1;
int *p2 = &a + 1;
p1指向的是元素a[1]的地址。p2指向的是元素a[10]的地址。p2是下一个这个单位字节的数组的首地址,a[10]并不存在,在这里仅是形象化表达。
5.1.2.1指针与下标
下标绝不会比指针更有效率,但指针有时效率更高
在汇编层次判断效率。
5.2数组初始化
5.3字符串的初始化
观察下列语句
char message[] = "hello";
char *message2 = "hello";
前者初始化字符数组的元素,后者将指针变量指向字符串常量的地址。
5.4二维数组
请问下列声明是合法的吗?
int vector[10], *vp = vector;
int matrix[3][10], *mp = matrix;
第一条语句是合法的, 第二条语句是非法的。
一维数组名,代表数组首元素地址 ,这里的vector
等于&vector[0]
,vp
的类型是int*
类型,vector
和vp
具有相同的类型:指向整形的指针,这里初始化正确。
二维数组名,同样代表数组首元素地址,只不过是它的首元素matrix[0]
地址是&matrix[0]
,而mp
的类型是int*
类型,matrix
的类型是int (*)[10]
类型,matrix
是一个指向整型数组的指针。
二维数组地址
二维数组名是指向行的,一维数组名是指向列的。
表达式 | 地址 |
---|---|
a | 0行首地址 |
a[0] | 0行0列元素地址 |
a[0][0] | 0行0列元素地址 |
&a[0] | 0行首地址 |
*a | 0行0列首地址 |
(六)字符串
6.1字符串基础
6.2字符串的概念
C语言没有字符串类型,字符串出现的形式有两种:
- 字符数组
- 字符串常量
字符串:一串零个或者多个字符组成,以NUL
字节为字符串的结束符。
6.21字符串的长度
字符串长度为所包含的字符个数,长度并不包括NUL
字节。
库函数strlen的原型如下:
size_t strlen(char const *string);
#include <stddef.h>
基础版本
size_t strlen(char const *string)
{
int length;
for (length = 0; *string != '\0'; )
{
length++;
}
return length;
}
注意strlen返回一个类型为size_t的值,这个类型在头文件stddef.h,是一个无符号整形,在表达式中使用会产生不可以预料的结果。
if (strlen(x) >= strlen(y)) ...
if (strlen(x) - strlen(y) >= 0)...
看起来一样,事实上是不相等的,第1条语句会按照你预预想的工作,但第2条语句的结果永远为真。strlen的结果是一个无符号数,所以>=的左边仍是一个无符号数,所以>=0恒成立。
使用一个函数,不仅函数的功能、函数参数个数以及类型、返回值重要,其实返回值的类型更为重要。
6.3字符串常量
字符串常量出现于表达式中,它的值是指针常量,编译器会把这些指定字符的一份拷贝存储在某个位置,并存储指向第一个字符的指针。字符串常量的值就是指向第一个字符的指针。
"xyz" + 1 指向第二个字符y的指针
*"xyz" 字符x
"xyz"[2] 字符z,下标实际上就是指针的使用 等于*("xyz" + 2)
(七)文件
所谓文件一般指存储在外部介质上数据的集合。
FILE *fp;
通过文件指针变量能找到与它关联的文件。打开与关闭文件:实际上,所谓打开是指为文件建立相应的信息区(用来存放有关文件的信息)和文件的缓冲区(用来暂时存放输入输出的数据)。所谓关闭是指撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,就不能进行对文件的读写。如果不关闭文件将会丢失数据,因为在向文件写数据时,是先将数据输出到文件缓冲区,待缓冲区充满后才正式输出给文件。如果当数据未充满缓冲区而程序结束运行,就很有可能使缓冲区的数据丢失,要用fclose函数关闭文件,先将缓冲区的数据输出到磁盘文件中,然后才撤销文件信息区。
函数原型:FILE * fopen(const char * path, const char * mode);
字符串 | 说明 |
---|---|
r | 以只读方式打开文件,该文件必须存在。 |
r+ | 以读/写方式打开文件,该文件必须存在。 |
rb+ | 以读/写方式打开一个二进制文件,只允许读/写数据。 |
rt+ | 以读/写方式打开一个文本文件,允许读和写。 |
w | 打开只写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。 |
w+ | 打开可读/写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。 |
a | 以附加的方式打开只写文件。若文件不存在,则会创建该文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF 符保留)。 |
a+ | 以附加方式打开可读/写的文件。若文件不存在,则会创建该文件,如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF符不保留)。 |
wb | 以只写方式打开或新建一个二进制文件,只允许写数据。 |
wb+ | 以读/写方式打开或新建一个二进制文件,允许读和写。 |
wt+ | 以读/写方式打开或新建一个文本文件,允许读和写。 |
at+ | 以读/写方式打开一个文本文件,允许读或在文本末追加数据。 |
ab+ | 以读/写方式打开一个二进制文件,允许读或在文件末追加数据。 |
函数原型:int fclose( FILE *fp );
返回值:如果流成功关闭,fclose 返回 0,否则返回EOF(-1)。
(八)代码优化
8.1注释
注释以/*
开始,以*/
结束。在C程序中,凡是可以插入空白的地方都可以插入注释,然而注释不能嵌套使用。通常有时会注释掉一段代码而不删除,如果这段代码内部原先有注释存在,再在首位加上注释符号会出现问题。要从逻辑上删除一段C代码,更好的方法是使用**#if编译指令**,如下:
#if 0
statements
#endif
8.2用typedef声明新类型名
1.简单地用一个新的类型名代替原有的类型名
typedef int Count; //指定Count代表int
Count i,j; //用Count定义变量i和j,相当于int i,j;
2.命名一个简单的类型名代替复杂的类型表示方式
命名一个新的类型名代表结构体类型:
typedef struct
{
int month;
int day;
int year;
}Date;
Date birthday; //定义结构体类型变量birthday
Date *p; //定义结构体指针变量
命名一个新的类型名代表数组类型:
typedef int Num[100]; //声明Num为整形数组类型名
Num a; //定义a为整形数组名,它有100个元素
命名一个新的类型名代表指针类型
typedef char *String; //声明String为字符指针类型
String p,s[10]; //定义p为字符指针变量,s为字符指针数组
命名一个新的类型名代表指向函数的指针类型
typedef int (*Pointer)(); //声明Pointer为指向函数的指针类型,该函数返回整形值
Pointer p1,p2; //p1,p2为指向函数的指针变量
习惯上,常把用typedef声明的类型名的第1个字母用大写表示,以便与系统提供的标准类型标识符相区别。
typedef和#define表面上有相似之处,但事实上#define是在预编译时处理的,它只能作简单的字符串替换,而typedef是在编译阶段处理的,实际上不是简单的字符串替换,而是采用如同定义变量的方式生成一个新的类型名去定义变量。
你应该使用typedef而不是#define来创建新的类型名,因为后者无法正确处理指针类型,例如:
#define d_ptr_to_char char*;
d_ptr_to_char a, b;
跟上面指针的声明一样容易出现问题,本想声明两个指针字符型的指针,但仅有a是char*
,而b是char
。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef
更为合适。
使用typedef名称有利于程序的通用与移植,有时候会依赖硬件特性,用typedef类型就便于移植。
例如,有的计算机系统int型数据占用两个字节,数值范围为-32768~32768,而另外一些机器则以4个字节存放一个整数,数值范围为±21亿。如果把一个C程序从一个以4字节存放整数的计算机系统移植到以2个字节存放整数的计算机系统中,按一般方法需要将定义变量中的每个int改成long,将“int a,b,c"改成"long a,b,c",如果程序中有多处用int定义变量,则多次改动。现在可以使用一个Integer来代替int
typedef int Integer;
在移植时候只需改动typedef定义
typedef long Integer;