1. 上机naive问题
- 输出的格式一定要与题目要求一致:
- 不要多空格、不要少空格
- 不要多换行、不要少换行
scanf后面的变量要加&:
int a; float b; char c; scanf("%d%f%c", &a, &b, &c);
窗口一闪而过,在main函数最后加getchar();
#include <stdio.h> int main() { /*your code here...*/ getchar(); return 0; }
学会调试会让你事半功倍。
- 程序正常运行但没反应?陷入死循环了,在for或while语句里下个断点看看什么情况。
- 答案是对的,但提交却“答案错误”?再试试几组比较特殊的测试用例。
- 四舍五入用+0.5后取整:int a = 0.6; int x = int(a + 0.5);得x等于1。
- to be continued…
2. 常见问题
2.1 ‘\0’,‘0’和0;
首先回忆一下转义字符的概念,形如’\XX’是一个转义字符,表示ASCII码为XX(八进制数)的字符。我们来测验一下,我们知道字符’A’的ASCII码为十进制65,也就是八进制101,我们定义 char c = ‘\101’; printf(“%c\n”, c);看看是不是输出了’A’?我们再看看将c输出成整型会发生什么,printf(“%d\n”,c);输出结果是65,也即是’A’的ASCII码。
实际上,字符型变量在内存中存储的是ASCII码,当一个字符需要转换成整型(例如需要与整型进行比较,c==65)时,字符转换成其ASCII码。
‘0’表示字符’0’,ASCII码值为十进制48。当’\0’与’0’进行比较时,相当于0与48比较,当然不等。
2.2. 按enter键得到什么?
对C/C++程序员来说,简单来说,就是得到了’\r’,而windows系统又会将’\r’转换成’\n’。
参考我之前写的一篇博客:
详解二进制文件和文本文件的区别暨回车与换行详解
2.3. 二进制文件和文本文件读写区别
二者唯一的区别就是对换行符的处理上:文本文件用’\r\n’表示换行,而二进制文件用’\n’表示换行。在Linux系统中是不存在二进制文件和文本文件之分的。
参考我之前写的一篇博客:
详解二进制文件和文本文件的区别暨回车与换行详解
3. 指针相关
3.1 指针的含义
指针本质上是个地址(32位,4字节)。用于指向内存中的一块空间。而指针的类型用于定义这块空间的大小。
为什么用指针(地址)?这就像你在YouTube上发现了一个好看的视频,你是选择把链接发给你的朋友呢,还是把视频下载下来,然后发给朋友?也就是说,使用指针让数据的传递和访问变得非常快速和简单。
指针为什么要类型?实际上指针的值是一块内存空间的首地址,当访问指针指向的变量时(p操作),会根据变量类型的大小size,访问size大小的空间。举例说明:int *p = &a; 假如p的值为0x0000ac01;(刚才已经说了地址有32位),由于p指向int,而一个int是4字节,那么当你进行解引用操作*p时,会从0x0000ac01开始,往后找4个字节也就是将0x0000ac01, 0x0000ac02, 0x0000ac03, 0x0000ac04这四个空间的内容看作是一个int。注意:一个字节分配一个地址。*
3.2 常量指针和指针常量
注:2017-1-19之前的版本是错的,正好弄反了。老司机阴沟翻船。
3.2.1 通过英文名称理解
这二者容易混淆的主要原因在于很难从定义形式分辨出到底是常量指针(constant pointer),还是指针常量(pointer to a constant)。
根据修饰符号的位置关系,有三种定义形式:
char * const p1 = &ch1; /*(1)第一种形式:指针常量 */
char const *p2 = &ch2; /*(2)第二种形式:常量指针*/
const char *p2 = &ch2; /*(3)第三种形式:常量指针*/
Bjarne在他的The C++ Programming Language里面给出过一个助记的方法:
把一个声明从右向左读。char * const cp; ( * 读成 pointer to )
cp is a const pointer to charconst char * p;
p is a pointer to const char;
应付C语言考试,只要强记住这三种形式即可:
3.2.2
- 常量指针(constant pointer)是指指针本身不能改变:
char ch1 = 'a';
char ch2 = 'b';
char * const p1 = &ch1; /* (1)第一种形式 */
*p1 = 'x'; /*正确*/
p1 = &ch2; /*错误*/
- 指针常量(pointer to a constant)是指指针指向的内容不能改变
char ch1 = 'a';
char ch2 = 'b';
char const *p2 = &ch2; /*(2)第二种形式*/
/*const char *p2 = &ch2;*/ /*(3)第三种形式*/
p2 = &ch1; /*正确*/
*p2 = 'x'; /*错误*/
3.3.3 高级技巧
当定义二级三级甚至多级指针常量/常量指针,用上面的方法就会很复杂。我自己观察到一个规律:
const修饰符和变量名之间有几个*,那么在用这么多*解引用时,就不能改变这个值。
例如,
char * const p1 = &ch1; /* (1)第一种形式 */ const和变量名p1之间有0个*,那么p1 = &ch2;这样就是错的。
再如,
char const *p2 = &ch2; /*(2)第二种形式*/ const char *p2 = &ch2; /*(3)第三种形式*/ const和变量名p2之间有一个*,那么*p='b';这样就是错的,
再举一个复杂一点的例子,
char ch1 = 'a'; char ch2 = 'b'; char *p1 = &ch1; char *p2 = &ch2; const char **pp = &p1; 因为const到变量名pp之间有2个*,那么**pp = 'b';这样就是错的。而, *pp = &ch2;或 pp = &p2;这样都是对的。
更高级的例子,
char ch1 = 'a'; char ch2 = 'b'; char *p1 = &ch1; char *p2 = &ch2; char * const *pp = &p1; 因为const到变量名pp之间有1个*,那么*pp = &ch2;这样就是错的。而, **pp = ch2;或 pp = &p2;这样都是对的。
掌握了这个规律,再看谷歌的这道面试题是不是很简单呢。
[题目]
const char *p="hello";
foo(&p);//函数foo(const char **pp)下面说法正确的是[]
A.函数foo()不能改变p指向的字符串内容
B.函数foo()不能使指针p指向malloc生成的地址
C.函数foo()可以使p指向新的字符串常量
D.函数foo()可以把p赋值为 NULL.
解答:
foo函数的形参类型是const char **pp; 利用上面所说的规律,只有**pp = ‘w’;这样访问是不行的。其它都可以,因此答案是ACD。
实际上,即使foo函数声明成这样,A也是错的:foo(char **pp);
原因是,p指向的字符串”hello”是存在放常量区的,不能改变。所以我们用字符串字面值给字符指针赋值的时候,一般都将字符指针定义成指针常量:
const char *p = "hello";
*p = 'w'; /*编译错误!*/
char *q = "hello";
*q = 'w'; /*编译正确,运行时错误!*/
3.3 数组名跟指针的关系
在大多数情况下数组名相当于指向数组第一个元素的常量指针。
- 因为数组名是指针常量,所以不能对数组名进行赋值,例如下面的语句是错的:
char str[10] = "hello";
str = "world"; /*错误!数组名不能赋值*/
- 因为数组名是指向第一个元素的指针,因此,如下语句等价:
char str[10] = "hello";
*(str + i) 与 str[i]等价
3.4 二维数组名和二级指针
二维数组名不是二级指针! 参考我之前写的一篇博客:
C/C++二维数组名和二级指针
3.5 字符数组的初始化
定义的时候赋值,叫做初始化。
- 以字符串初始化:注意字符串的大小(size)不能大于数组的大小
char str1[10] = "hello"; /*未初始化的部分全部给0(整数0,不是字符'0')*/
char str2[10] = "helloworld"; /*错误!字符串的大小为11(包括字符串结尾标志'\0')*/
- 以大括号初始化
char str3[10] = {'h', 'e', 'l' , 'l', 'o' }; /*未初始化部分全部给0*/
注意,这样也是允许的:
char str4[10] = {}; /*这样str4初始化为全0*/
全局定义的数组,系统会自动初始化,局部定义的数组系统不会自动初始化。
char str5[10]; /*全局定义数组,系统自动初始化为全0*/
int main()
{
char str6[10]; /*是局部定义数组,没有初始化,内容不确定,一般为“烫烫烫烫烫烫”*/
}
- -
3.6 to be continued…
- 指针可以相互赋值,但跟strcpy的作用不同
- strcpy的两个参数都要指向一块空间,
- 指针数组和数组指针
4. 典型习题解答
4.1 字符数组初始化
解答:
答案:D。
A、C都错在,数组名不能赋值,改写成如下形式就是正确的:
char s[8] = {"Beijing"};
或
char s[8] = "Beijing";
B,不应该有大括号。
另外,用字符串初始化字符数组,应保证数组有足够的空间存储字符串的大小(字符串的长度+1),例如,下面的代码就是错的:
char str[10] = “helloworld”;/str有10个空间,而”helloworld”的大小为11,即,还要存储字符串结尾标志’\0’/
4.2 逻辑非运算符作用于字符串
指针用于条件判断时,指针为NULL条件为假,指针不为NULL,条件为真。
char *p = NULL;
if(p)
{
/*如果p不等于NULL,进入这里*/
}
if(!p)
{
/*如果!p不能与NULL,也就是p等于NULL进入*/
}
解答:
答案:true
- 字符串字面值”2010-01-27”相当于const char *,即字符指针,不为NULL。
- 因此!”2010-01-27”返回false.
- 因此!!”2010-01-27”返回true.
4.3 左移右移
这种题一定要将数字写成补码进行操作,然后再转换成原码
4.3.1
解答:
4.3.2
解答:
4.4 赋值运算符返回赋值后的值
解答:
4.5 条件运算符和自增自减
解答:
4.6 自增运算符++优先级高于取内容操作符*
解答:
答案:5
注:++的优先级高于*,因此,*p++和(*p)++是不一样的:
- *p++等价于*(p++),也就是说,先做p++,再对p++的返回值取内容。又,后置的++先返回值,再做++,那么*(p++)的作用就是:去指针p指向的内容,再对p自增。
- 对于(*p)++,显然,先做*p,即去指针p指向的内容,再对这个内容自增,注意,与上面不同,不是对p自增。
4.7 for语句
解答:
答案:B
4.8 逻辑与或的短路求值
解答:
答案:C,A
4.9 while条件恒真
解答:
16)答案:B
- printf返回输出的字符个数,因此printf(“*”);返回1,即恒真。
- 而while(‘0’),因为’0’会返回字符’0’的ASCII码48,即,while(‘0’)
while(48)等价,也是恒真。
4.10 printf返回输出字符个数
解答:
答案:4
printf返回输出的宽度,也就是输出的字符个数。上面的语句,输出一个字符*,以三个字符的宽度输出12,因此输出 的字符个数为1+3 = 4. i也就等于4
4.11 先做关系判断再做赋值
解答:
- 答案:2
- 这题的考点是逻辑与操作符的短路求值。
- 先求&&左边的,m=a>b; 因为a>b返回false,因此m赋值为0。而赋值运算符返回的是所赋的值,即,(m=a>b)这个表达式返回false,不管&&右边是什么整个表达式都为false了,因此&&右边不再做,这就是&&的短路求值。
- 另外,关系操作符的优先级大于赋值,因此先做关系判断,再做赋值。
4.12 二维数组名是指向一维数组的指针
解答:
- 答案:A。
- 字符串即char * 类型,那么只要看下面的是否为char *类型即可。
- A)strp是个二维字符数组名,我们知道数组名相当于指向第一个元素的指针,那么strp指向strp[0],而strp[0]的类型是个含有10个元素的char数组。因此strp不是一个char *;
- B)C)都是对的。
- D )A)已经说了strp指向strp[0],因此*strp跟strp[0]是一样的,也是个char *。
- 有人可能会认为把str看出二级指针char**不是更简单,是的,把str看出char**是很容解出这题,但实际上二维数组名并不是一个二级指针,详见我之前写的一篇博客:
C/C++二维数组名和二级指针
另外,这里的strp虽然是一维数组名,但因为数组里的每一个元素是char *,因此strp相当于一个二级指针char **.
4.13 只有指针可以用->,指针访问成员必须用->
解答:
9. 答案:D
- A,数组名不能赋值;
- B,.优先级高于*,因此先做.,而p是个指针,应该用->
- C,(*p)不是个指针,不能用->
- D,(p).name,数组名是指向第一个元素的指针,所以(*p).name相当于(*p).name[0]
4.14 ->和*混合访问
解答:
答案:31
- p指向x[0]
- ++p,指向x[1]
- ->优先级高于*,先做->得到x[1].b,即数组x1
- 再解引用,得到31
4.15 字符串末尾必须有’\0’,有’\0’的字符数组就能看成是字符串
解答:
答案:C
- A,以字符串初始化字符数组,用保证字符数组有足够空间存放字符串末尾的’\0’,显然A不够
- B,字符串结尾必须要有’\0’。要是这样就对了,char s[6] = {‘A’, ‘B’, ‘C’, ‘D’, ‘E’},这样还有一个空间自动初始化为0(数字0,等价于’\0’)。
- D,s是个“野指针”,没有指向实际的空间,因此不能用scanf输入。
4.16 连续的关系运算符
解答:
答案:true
- 这题考的是关系操作符的优先级:先做<,再做==,而且是从左到右:
- 所以上述表达式等价于:(‘b’< a)==(97<’1’)。
这里’b’的ASCII码是98,’1’的ASCII码是49,因此等价于false == false, 返回 true。
4.17 int型占用的空间不一定是4字节
解答:
答案:D
- 不同的编译器不一样。
- 实际上C语言规定int型的长度跟机器字长一样,所以,你的机器如果是32位机器,那么int型就是4字节。像上个世纪的大多数机器都是16位的,所以那时候的int就是2字节。
4.18 按位取反的技巧
问:16按位取反(~16)为什么是-17?
解答:
技巧:对任何整数a, a+~a = -1;
证明:~a是a的按位取反,因此a+~a的每一位都是1,而补码为全1的数是-1。
2017-6-29 10:31:20更新
4.x
to be continued…
5. 不负责任考点预测
5.1 理论
- 运算符优先级
- 前置++和后置++
- 取反、左移、右移
- 不同数据类型参与运算时的自动转换
- 逻辑与或(&& ||)的短路求值,见4.8
- ‘0’跟0不一样,见2.1
- 整除和取整:int a = 1/2; int b = 0.5; /*a,b都等于0*/
- to be continued…
5.2 上机
- 学会调试:大多数同学问我的问题是“助教我错在哪儿”,而一旦我指出错误所在,他很快能改过来。调试的意义就是让你自己能找到错误。
- 机试的难度就是平时的作业的难度