一、字符串:初始化、常量与赋值陷阱
1. 用字符串字面量初始化字符数组时,必须给'\0'留位置
char str[]="good!";数组长度由编译器自动定为6(g,o,o,d,!,\0)。- 若显式指定大小,例如
char str[5]="good!";,5个字节只能放下字符,'\0'无处安放,这不是合法的初始化(尽管某些编译器可能不报错,但程序行为不定)。 - 换成字符列表
char str[5]={'g','o','o','d',0};则完全正确,最后一个0就是'\0',刚好占满5个字节。 - 扩展:
'\0'是字符串结束标志,若缺失会导致printf或strlen等函数访问越界。用字符串字面量初始化字符数组时,建议不写大小或预留足够空间。
2. 字符串常量与字符常量不能混淆
" "、"0"、"12'12"都是字符串常量(双引号包裹),占用内存时会自动添加'\0'。'abc'不是字符串!C语言中单引号只用来括单个字符,像'a'、'\x10'。'abc'是多字符常量,其值由实现定义,绝不是字符串。书写字符串常量务必用双引号。
3. 字符数组名不是可修改的左值
- 定义
char a[10];后,用a="abc";试图赋值是错误的。数组名是一个常量指针,指向首元素地址,不能作为赋值语句的左值。 - 正确方式:
strcpy(a,"abc");,或逐个字符赋值(a[0]='a';等),或先a[0]=0;再用strcat拼接。 - 扩展:初始化与赋值不一样——初始化是在定义的同时赋初值,允许
char a[10]="abc";,但后续再出现a="xyz";就是赋值,非法。
二、数组:大小匹配、部分初始化与参数传递
1. 初始化列表元素个数不能超过数组大小
int y[5]={0,1,3,5,7,9};给了6个初值却只有5个位置,编译报错(超出边界)。- 数组大小可以省略,由初始值个数决定:
int y[]={0,1,3,5,7,9};则正确,数组长度为6。
2. 部分初始化时,未指定的元素自动置0
int a[10]={6,7,8,9,10};只给了前5个值,系统会把a[0]~a[4]依次赋为6,7,8,9,10,a[5]~a[9]补0。- 扩展:这个规则对全局数组永远成立;局部数组若不初始化则内容为随机值,若用
={0}可全置零。
3. 数组名作为函数实参,传递的是首地址
- 调用函数时写
func(arr),实际上是把数组第0个元素的地址传进去,函数内对形参数组的修改会直接影响原数组。 - 函数原型中
int arr[]等价于int *arr,它们都只是指针,所以sizeof(arr)在函数内部只能得到指针大小,而非数组长度。 - 扩展:要想在函数内知道数组长度,必须额外传递长度参数,如
func(a, 3)。
三、指针:定义、使用与那些容易混淆的声明
1. 指针是用来存放地址的变量,不是存放某个变量的值
int *p = &i;意味着p存的是i的地址,*p才是i的值。- 常考描述:“指针是一种变量,用来存放某个变量的值”——这是错误的,应存放“地址值”。
2. *与&的用法
- 已有
int i=3,*p; p=&i;,则printf("%d",*p);输出3;*p是间接访问运算符,等价于i。 printf("%d",p);输出的是地址值;&p是指针变量自身的地址,与i无关。- 注意:
*i是错误的,因为i是整型变量,不能解引用。
3. 函数返回指针 vs 函数指针,声明差别巨大
int *f();声明了一个返回值为整型指针的函数,f是函数名,调用后返回int*。int (*f)();则是函数指针,f是一个指针,指向返回值为int的函数。- 这种题目通常拿
int *f()让你判断f是什么,答案应选“一个返回值为指针型的函数名”,而不是函数指针。
4. 数组元素的多种引用方式
- 已知
int a[10]; int *p = a;,以下写法均正确引用数组元素:a[i]、p[i](下标法)*(a+i)、*(p+i)(指针位移)*(&a[i])(先取元素地址再解引用)
- 错误写法:
*(*(a+i))。因为*(a+i)已经是元素值(如a[0]),再解引用就是把整数当指针用,非法。多层解引用必须对应指针的指针。 - 扩展:
a[p-a]也是合法的,因为p-a是两指针的差,结果为整数偏移,再取下标,结果和*(p)或p[0]动态相关,这里p初值为a,所以p-a=0。
四、运算符:优先级与自增的左值要求
1. 运算符优先级顺序(由高到低)
- 算术运算符(
+ - * / %) > 关系运算符(> < == !=) > 逻辑运算符(&& || !) > 赋值运算符(= += -=等)。 - 所以表达式计算时,先算乘除加减,再比较大小,再逻辑运算,最后赋值。
- 记忆口诀:算法关逻赋(算关罗夫),或者自己发挥。
2. 自增/自减运算符要求操作数是左值
++(i+1);错误,因为(i+1)是个临时值(右值),不能放在自增运算符后面。j++;正确,j是变量;n=(i=2,++i);正确,逗号表达式返回最后一个值,且++i作用于变量i。- 扩展:
++i与i++的区别,以及它们可以和取值符号组成*p++这样复杂的表达式,需注意结合方向。
五、函数:值传递、不可嵌套定义与数组参数
1. 值传递:实参与形参各自拥有独立存储空间
- 调用函数时,实参的值被复制给形参,函数内部对形参的修改不影响实参。
- 即使是传递地址(如数组名),被复制的是地址值本身,形参和实参是同一地址的两个独立副本指针,但地址指向同一片内存,所以可以通过指针修改原数据。这就是“传地址仍为值传递”的本质。
2. 函数定义不可嵌套,但调用可以
- C语言不允许在函数内部定义另一个函数,所有函数定义都是平行、全局的。
- 函数调用可以嵌套(递归或调用其他函数)。
- 选择语句、循环语句的嵌套当然合法。
3. 带数组参数的函数要注意边界
- 如函数
f(int b[], int n)内循环for(i=0; i<=n; i++) r=r*b[i];,实际乘了从b[0]到b[n]共n+1个元素。 - 调用
f(a, 3),若a为{2,3,4,5,6,7,8,9},则计算2*3*4*5 = 120。边界值直接决定结果。
六、结构体与枚举:初始化与默认值规则
1. 结构体数组初始化及成员运算
- 定义
struct cmplx { int x; int y; } cnum[2] = {1,3,2,7};初始化按顺序匹配:cnum[0].x=1, cnum[0].y=3, cnum[1].x=2, cnum[1].y=7。 - 之后表达式
cnum[0].y / cnum[0].x * cnum[1].x即3/1*2,整除得3,乘2得6。 - 注意:除法/乘法同级左结合,
3/1得3,3*2=6,不会先算1*2。
2. 结构体数组成员字符串访问
struct person { char name[9]; int age; } class[4] = {...}中,class[2].name[0]可取出"Mary"的首字母'M'。- 要访问字符串中某一字符,“结构体数组名[下标].成员名[下标]”即可。
3. 枚举类型定义与元素值
- 定义枚举时,成员名不需引号,值可指定,未指定则自动递增。
- 如
enum a {sum=9, mon=-1, tue};,tue未赋值,其值为前一项mon的值加1,即 -1+1=0。 - 不能用字符串定义枚举成员,如
enum a {"sum","mon","tue"};是错误的。
七、关键字辨识
- C语言关键字全是小写:
default、int、char、float、if、while、return等。 - 常见干扰项:
VAR(Pascal)、integer(Fortran/Pascal)、cher(拼错)均非C关键字。 - 特别注意:
const、enum、struct、typedef、unsigned等有些不常用但也必记。
八、思维扩展与复习建议
1. 字符串的本质
不要将字符数组与字符串字面量混为一谈,搞清楚'\0'的存放位置。多练习strcpy、strlen与数组大小的关系。
2. 指针与数组的同一性a[i]与*(a+i)无条件等价,数组名就是常量指针,但指针变量可以自增。理解int *p=a;后p[i]、&p[i]、*(p+i)的相互转换。
3. 运算符优先级的题海战术
优先级不必死记所有运算符顺序,但必须记住大段落(算术>移位>关系>逻辑>赋值),考试中常出判断结果和括号去留的题目。
4. 函数传参机制
所有参数传递都是“值传递”,形参是实参的副本。想修改变量本身,需要传地址,并在函数内用*p修改。
5. 结构体和枚举
它们是构造复杂数据的基础,初始化细节、成员访问、默认值的规则很有规律,掌握后拿分轻松。