
[C 数据类型和变量方面](#C 数据类型和变量方面)
[char 类型的注意事项](#char 类型的注意事项)
[scanf 函数的注意事项](#scanf 函数的注意事项)
[if 语句的注意事项](#if 语句的注意事项)
[switch 语句的注意事项](#switch 语句的注意事项)
[if 和 while 的对比](#if 和 while 的对比)
[while 循环和for 循环中的continue的对比](#while 循环和for 循环中的continue的对比)
[return 语句的注意事项](#return 语句的注意事项)
[const 修饰指针变量](#const 修饰指针变量)
[sizeof 与strlen 的区别](#sizeof 与strlen 的区别)
以下是关于C的概念梳理、知识要点、易混淆概念区别的总结
C常见概念方面
简单错误梳理
第⼀次写代码,⼀些常见的错误总结:
- main 被写成了mian
- main后边的()漏掉了
- 代码中不能使用中文符号,比如括号和分号
- ⼀条语句结束后,有分号
以下是一个正确的main函数
cpp
#include <iostream>
int main(){
return 0;
}
C 数据类型和变量方面
注意事项梳理
char 类型的注意事项
C 语⾔规定 char 类型默认是否带有正负号,由当前系统决定。
这就是说, char 不等同于 signed char ,它有可能是 signed char ,也有可能是 unsigned char 。
这⼀点与 int 不同, int 就是等同于 signed int 。
cpp
signed char c; // 范围为 -128 到 127
unsigned char c; // 范围为0 到 255
scanf 函数的注意事项
scanf函数原型如下图所示:
cpp
scanf("%d", &i);
变量前面必须加上 & 运算符(指针变量除外 ),因为 scanf() 传递的不是值,而是地址,即将变量 i 的地址指向用户输⼊的值。 如果这里的变量是指针变量(比如字符串变量),那就不⽤加 & 运算符。
占位符列举的注意事项
printf() 的占位符有许多种类,与 C 语⾔的数据类型相对应。其中 %G 等同于 %g ,唯⼀的区别是指数部分的 E 为⼤写。
分支循环语句方面
注意事项梳理
if 语句的注意事项
只要带上适当的大括号 ,代码的逻辑就会更加的清晰,所以⼤家以后在写代码的时候要注意括号的使用,让代码的可读性更⾼。
cpp
#include <iostream>
int main(){
if(表达式1){
//逻辑处理
}
else if(表达式2){
//逻辑处理
}
else{
//逻辑处理
}
return 0;
}
关系操作符的注意事项
相等运算符 == 与 赋值运算符 = 是两个不⼀样的运算符,不要混淆。有时候,可能会不小心写
出下⾯的代码,它可以运行,但很容易出现意料之外的结果。
cpp
if(a = b) {}
if(a == b) {}
以上的两句代码是不同的!!一个是赋值运算符,一个是比较大小的相等运算符。
switch 语句的注意事项
- case 和后边的数字 之间必须有空格
- 每⼀个 case 语句中的代码执行完成后,需要加上****break ,才能跳出这个 switch 语句。
cpp
switch (number){
case 0:
//逻辑处理
break;
case 1:
//逻辑处理
break;
default:
//逻辑处理
break;
}
区别
if 和 while 的对比
cpp
if(表达式)
语句;
while(表达式)
语句;//如果循环体想包含更多的语句,可以加上⼤括号
while 语句可以实现循环效果,if 语句不可以。
while 循环和for 循环中的continue的对比
while循环
while循环中的 continue 可以帮助我们跳过某⼀次循环 continue 后边的代码, 直接到循环的判断部分 ,进行下⼀次循环的判断,如果循环的调整是在 continue 后边的话,可能会造成 死循 环。


for循环
for 循环中的 continue 的作⽤是跳过本次循环中 continue 后的代码, 直接去到循环的调
整部分 。未来当某个条件发⽣的时候,本次循环⽆需再执行后续某些操作的时候,就可以使⽤
continue 来实现。


数组方面
注意事项梳理
数组传参的注意事项
- 函数的形式参数要和函数的实参个数匹配
- 函数的实参是数组,形参也是可以写成数组形式的(一般不这么做,这么做传参涉及开空间,效率低下,一般传参使用指针解决)
- 形参如果是⼀维数组,数组大小可以省略不写
- 形参如果是⼆维数组,行可以省略,但是列不能省略
- 数组传参,形参是不会创建新的数组的
- 形参操作的数组和实参的数组是同⼀个数组
cpp
void set_arr(int arr[], int sz);
void set_arr(int* arr, int sz);//推荐使用
函数方面
注意事项梳理
return 语句的注意事项
- return 后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
- return 后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是 void 的情况。
- return 返回的值和函数返回类型不⼀致,系统会自动将返回的值隐式转换为函数的返回类型。
- return 语句执行后,函数就彻底返回,后边的代码不再执行。
- 如果函数中存在 if 等分⽀的语句,则要保证每种情况下都有 return 返回,否则会出现编译错误。
static的注意事项
-
如果未来⼀个变量出了函数作用域后,我们还想保留值,等下次进⼊函数继续使用,就可以使用 static 修饰。
-
如果⼀个全局变量,只想在所在的源⽂件内部使用,不想被其他⽂件发现,就可以使用 static 修饰。
-
如果⼀个函数只想在所在的源文件内部使⽤,不想被其他源⽂件使⽤,就可以使⽤ static 修 饰。

程序调试方面
内存布局
计算机的内存是如何布局的?

cpp
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}


栈区 :栈区内存的使⽤习惯是从高地址向低地址使⽤的,所以变量i的地址是较大的。arr 数组的地址整体是小于 i 的地址。
数组在内存中的存放是:随着下标 的增⻓,地址是由低到高变化的。
如果是上面的内存布局,那么 随着数组 下标的增⻓,往后越界就有可能覆盖 到i,这样就可能造成死循环的。
栈区的默认的使用习惯是先使用高地址,再使用低地址的空间,但是这个具体还是要编译器的
实现,比如:在VS上切换到X64,这个使⽤的顺序就是相反的,在Release版本的程序中,这个使用的顺序也是相反的,但Relase版本的程序不支持调试,这个顺序也就没有意义了。
常见错误归类
编译时错误
编译型错误⼀般都是语法错误,这类错误⼀般看错误信息就能找到⼀些蛛丝⻢迹的,双击错误信息也 能初步的跳转到代码错误的地方或者附近。编译错误,随着语⾔的熟练掌握,会越来越少,也容易解决。

链接时错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。
⼀般是因为 标识符名不存在 、 拼写错误 、头⽂件没包含 、引⽤的库不存在


运⾏时错误
运⾏时错误,是千变万化的,需要借助调试,逐步定位问题,调试解决的是运行时问题。
函数递归方面
注意事项梳理
函数递归的注意事项
一句话:能用迭代的地方尽量使用迭代 ,但该用递归的地方,还得使用递归。但使用递归时要注意:递归函数调⽤的过程中涉及⼀些运⾏时的开销。
在C语⾔中每⼀次函数调用,都要需要为本次函数调用在栈区申请⼀块内存空间来保存函数调用间的各种局部变量的值,这块空间被称为运行时堆栈 ,或者函数栈帧。
函数不返回,函数对应的栈帧空间就⼀直占用,所以如果函数调⽤中存在递归调用的话,每⼀次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间 ,也可能引起栈溢出(stack overflow)的问题。
操作符方面
两个问题
数据在内存中为什么是以补码的形式存放的?
在计算机系统中,数值⼀律⽤补码来表示和存储。
原因在于:使⽤补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算 过程是相同的,不需要额外的硬件电路。
整型提升的意义是什么?
C语⾔中整型算术运算总是至少以缺省整型类型的精度来进⾏的。
为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型 ,这种转换称为整型提升。
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度⼀ 般就是int的字节长度,同时也是CPU的通⽤寄存器的长度。
因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准长度。
通⽤CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送⼊CPU去执行运算。
一个总结
cpp
int ret = (++i) + (++i) + (++i);
即使有了操作符的优先级 和结合性 ,我们写出的表达式依然有可能不能通过操作符的属性确定唯⼀的计算路径,那这个表达式(比如:上述的代码)就是存在潜在风险的,建议不要写出特别负责的表达式。
指针方面
指针的本质
内存单元的编号 == 地址 == 指针

指针变量的大小
- 32位平台下地址是32个bit位,指针变量大小是4个字节
- 64位平台下地址是64个bit位,指针变量大小是8个字节
- 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
指针变量类型的意义
指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。
⽐如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。
指针+=整数
指针的类型决定了指针向前或者向后走⼀步有多大(距离)。
void*指针
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。
但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引用的运算。
void*指针的作用

一般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。
const 修饰指针变量
const修饰指针变量的时候:
- 如果const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。(底层const)
- 如果const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。(顶层const)
导致野指针的情况
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放
规避野指针的方法
指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL。NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
cpp
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
小心指针越界访问
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。
因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使用指针之前可以判断指针是否为NULL。
避免返回局部变量的地址
不要返回局部变量的地址。
传值调用与传址调用的区别
传值调用传参,实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。这时如果你想修改实参值,就得是是使用传址调用。
传址调用 ,可以让函数和主调函数之间建立 真正的联系,在函数内部可以修改主调函数中的变量。所以未来函数中如果只是 需要主调函数中的变量值 来实现计算,就可以采⽤传值调用 。如果函数内部要修改 主调函数中的变量的值,就需要传址调用。
数组名的意义
- sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表示整个数组,计算的是整个数组的大大小,单位是字节
- &数组名,这⾥的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
数组传参
一维数组传参的本质是 传递数组首元素的地址。
因此, ⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
cpp
void test(int arr[]);//参数写成数组形式,本质上还是指针
void test(int* arr);//参数写成指针形式
二维数组传参的本质也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。
因此,⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
cpp
void test(int p[3][5], int r, int c);//数组形式
void test(int (*p)[5], int r, int c);//指针形式
函数指针变量
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
数组指针与函数指针写法的区别
数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
cpp
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
函数指针void(*)(int) 类型重命名为 pf_t ,就可以这样写:
cpp
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
什么是回调函数
回调函数就是⼀个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,⽽是在特定的事件或条件发生时由另外的⼀方调用的,⽤于对该事件或条件进行响应。
sizeof 与strlen 的区别
sizeof 是用来计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据。
strlen 是C语言库函数,功能是求字符串长度。
函数原型:
统计的是从 strlen 函数的参数 str 中这个地址开始向后,' \0'之前字符串中字符的个数。
strlen 函数会⼀直向后找' \0'字符,直到找到为止,所以可能存在越界查找。
|-------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| sizeof | strlen |
| 1、sizeof 是操作符 2、计算的是操作数所占内存的大小,单位是字节 3、不关注内存中存放的是什么数据 | 1、strlen是库函数使用需要包含头文件string.h / cstring 2、strlen是求字符串长度的,统计的是'\0'之前字符的个数 3、关注内存中是否有'\0',如果没有'\0',就会持续往后找,可能会越界 |
C的知识体系太大了,体系梳理先到这,希望大家有所收获!!
