目录
一、概述及前置知识
1.1 什么是集成开发环境
IDE集成开发环境 编辑器 编译器 链接器 调试器
1.2 main函数
- 在一个工程(项目)下 可以有多个.c文件 但是只能有一个main函数
- C语言中必须有主函数 而且有且仅有一个主函数
1.3 单位
1.4 注释
- 在预处理阶段 被注释掉的代码就删掉了
- C语言的风格不能嵌套注释
1.5 函数简介
- ( )是个操作符 函数调用操作符
- 函数的特点就是简化代码 代码复用
1.6 C语言中真和假的概念
- 0=假
- 非0(!0)=真 所以-1也是真
- !假=真
1.7 C语言的内存分区
- 下图栈区上的函数参数指的是形参
- 静态区创建的变量(全局变量/静态变量)不初始化 默认是0
- 但是局部变量 不初始化 放的是随机值(而且直接用的话 VS直接报错说未定义)
1.8 EOF-文件结束标志
- EOF本质是-1
- 成功读取到2个整数 scanf就返回2
- 正常情况输入Ctrl+Z scanf就会读取失败 但是VS有一个bug 要连按三次
- 这样的话 第一个100成功读取 第二个失败 返回1
1.9 头文件一般放什么内容
- 而且一般include专门包含头文件的 不要去包含.c文件 这种牛角尖不要钻 没有意义
- 写代码还是要规范!! 不要自找麻烦!!
1.10 栈:从高用到低 数组:下标越大 地址越高
这个代码出现的错误 本身就依赖环境(VS2019 x86)
比如在VC6.0 i和arr之间就没有多余的空间
在gcc i和arr之间有一个整型空间
或者把x86改成x64 效果也会不一样
主要是学会调试和栈的使用方式
- 按照
函数栈帧
那一节的画法 下面画成高地址 比较好理解 因为高地址先被使用 先入栈- 栈先使用高地址 再使用低地址
- 数组下标越大 地址越高
- 那么在这个环境下 先创建arr数组 再创建i 虽然还是越界了 但避免了死循环
1.11 断言assert(assert.h)
下图的if有两个问题:
- 直接return 是逃避问题 没有明确指出问题 程序员难以排查
- 不管是release还是debug if判断都会存在(但是这种判断不应该存在于release版本才对)
assert断言指针会更有效
assert会告诉你准确的错误信息
assert主要可以帮助程序员debug 所以release版本中assert就会被优化掉
1.12 release和debug
- Debug通常称为调试版本 它包含调试信息 并且不作任何优化 便于程序员调试程序
- Release称为发布版本 它往往是进行了各种优化 使得程序在代码大小和运行速度上都是最优的 以便用户很好地使用
1.10的案例 其实在release版本下运行就不会死循环
i的地址居然比较低 所以arr[i]就算越界 也不会影响到i
因为数组下标越大 地址越高
1.13 编程常见的错误
编译型
直接看错误提示信息 解决问题 编译都编译不过去的
相对来说简单 (语法错误)
双击你的报错 会出现一个黑色箭头
链接型
看错误提示信息 一般是标识符名不存在或者拼写错误
假如忘记include头文件 也会报这个错误
这种LNK的就是
可惜报错信息没啥用 只能自己Ctrl+F找找看了
运行时
借助调试 逐步定位问题 最难搞
需要经验 需要实践 不要畏惧 冲冲冲
二、数据类型
2.1 八大类型的大小
- sizeof 是一个操作符 而不是函数 计算结果的单位是字节
- long long是C99标准加入的数据类型
- C语言标准规定:sizeof(long)≥sizeof(int) 即可 当前编译器取的就是等于
- 为什么需要丰富的类型:为了描述实际问题 本身就需要整数 小数 字符…
- 为什么同一种类型又分好几类:如果描述年龄 short就足够了 适当的类型给适当的东西用 更加节省空间
2.2 默认浮点数为double类型
- 浮点数无法精确存储 详见后面的IEE754的规则
2.3 占位符
-
更多细节见本文的6.3
-
sizeof的返回值其实是size_t
-
size_t在64位的环境下是64位的整型 建议用 %zd或者%zu来打印
-
size_t在32位的环境下是32位的整型 用 %d %u %zd %ud打印都没问题
-
%u是打印无符号数的 也就是打印unsigned int的数据类型(他打印的应该是32位的无符号整型)
-
反正sizeof就用%zd; unsigned int就用%u 要根据编译器的提示灵活运用
2.4 如何表示八/十进制
● 这个B的值 是10 8+2(八进制)
● 16进制就是int b = 0x55 (= 85 = 1010 1010)
2.5 C语言中 表达式有两个属性
2.6 C语言标准没有规定char类型的符号
2.7 C99引入布尔类型
- 其实本质还是0或者1
三、变量与常量
3.1 变量的命名规则
3.2 变量最好要初始化
- vs2019还是比较严格的 所以还是建议务必要初始化
3.3 变量的分类
- 主要分为局部变量和全局变量
- 区分的标准:看{} 在{}内部就是局部 在外部就是全局变量
- 全局变量不安全 谁都可以用 要尽可能的少用
- 局部和全局变量名冲突的时候 局部优先 下面代码的结果为1
3.4 变量的作用域
概念
局部变量
-
局部变量的作用域是变量所在的局部范围
-
局部变量如果先使用 后定义 需要先声明
-
这里VS的比较严格 直接报错而不是报警告(从前往后扫 没扫到g_a就使用了)
-
声明一下即可
-
使用变量: 先声明再使用; 什么时候使用,什么时候定义
-
如果定义就直接定义在前面的话 就不需要单独声明了(这样就更规范)
全局变量
-
全局变量的作用域是整个工程 (因为只要合理声明 全局变量在当前项目下都可以使用)
-
在另一个.c 文件里定义的全局变量 需要 extern 声明外部符号 才可以使用(跨文件使用全局变量 需要 extern)
3.5 变量的生命周期
- 局部变量a的生命周期:进入局部范围生命周期开始 出了局部范围生命周期结束
- 进入局部范围:申请内存 创建变量 开始
- 出了局部范围:消亡 归还内存给操作系统 a已经不可以再使用 不是真的销毁了
- 只要程序还活着(程序还没结束) 全局变量就可以使用
- 全局变量在整个main函数里都可以使用 而主函数的生命周期就是整个程序的生命周期
- 进入主函数 程序的生命周期开始 出了主函数 程序就结束了 所以可以说全局变量的生命周期 就是整个程序的生命周期
3.6 常量
字面常量(注意字符串字面常量)
- 字符串常量本身作为一个表达式 赋给变量的时候 就是把首字符的地址给变量
- int a = 3; char* p = “abc”;(这里p指向的字符串常量 是不可以被修改的)
const 修饰的常变量
-
仿佛让一个变量具有了常量的属性 但他 本质上还是一个变量 出了作用域也会销毁
-
就算我把下图的 n 换成 const修饰的 n 仍然报错 可以证明 n 还是个变量
-
只不过具有了常量不可修改的属性(语法层面上的常量 不能被修改)
-
当我希望一个变量不可以被修改–>用const修饰的常变量
#define 定义的标识符常量
- M确实是全局的 但是不能说他是全局变量 M就是一个#define标识符常量
枚举常量
- 这三个可能的取值 (R G B)就是枚举常量 表示Color所有的可能取值(也就是下图c可能的取值就是R G B)
- 所以BLUE就可以定义数组的大小
- 把 enum Color 看成 int 这样的一个类型 只不过是自定义类型
3.7 关于const
字符串常量 都用const修饰更规范
- 既然p指向的字符串字面常量是不可以被修改的 那直接显式的把指针p用const修饰 这样更好
- 不用const修饰 程序直接崩了 用const修饰 就会报一个编译时的错误 更规范 更安全
const修饰指针变量(分左右)
下图有一个漏洞 这里的常变量num 可以通过指针修改(绕过了const?)
const放在*左边的时候(const int *p = &num 或者 int const *p = &num)
const限制的是*p 即p所指向的内容 不可以再通过*p解引用被修改
注意 是不能通过*p解引用这种方式去修改num
假如指针p指向的是num本身没有被const修饰 还是可以把其它值赋给num的
指针变量p本身还是可以修改的
const在*右边 (int * const p)
const限制的是p 即指针变量p本身不可以被修改 不能再指向别人
但指针变量p指向的内容 还是可以通过*p解引用被修改
下图这种情况就是
p彻底被限制了 p不能改 *p也改不了
所以当我们希望一个变量比如说num 不能被修改:
首先num本身要用const修饰
其次当把num交给一个指针的时候 最好把const放在*的左边 这样也无法通过指针来修改num
总结来说 const修饰指针变量 修饰的就是const右边的那一坨东西
要么是:指针本身不可以修改(不可以指向别人)
要么是:不可以通过指针(解引用)修改指针所指向的内容
擅用const的一个实际意义
- 在模拟实现strcpy的时候 把src(源字符串)加一个const修饰 是最规范的 如果硬要改 编译都通不过 不会产生运行时错误 程序不会直接崩掉
- 假如下面传上来的是 char *p = "accc"的p 本身字符串常量就不能被修改 那就直接加个const 更加明确
- 或者说 不管传进来的是字符数组还是字符常量 肯定不希望被拷贝的字符串被修改 那就直接用
const char *src
修饰 而且是放在左边 const修饰*src src指向的内容不能被修改了 增加了代码的健壮性
- 或者说 不管传进来的是字符数组还是字符常量 肯定不希望被拷贝的字符串被修改 那就直接用
const能让一个运行时错误(错了只能自己慢慢debug) 变成一个编译时错误
3.8 变量的声明和定义
-
定义变量的时候 尽量要赋初始值(初始化) int a = 0;(不赋值 可能会给一个随机值 也可能编译都不通过 比如 VS2022)
-
声明:int a; 只需要告诉数据类型+变量名
-
定义本身也是一种特殊的声明 变量必须先声明(定义)再使用
-
经过测试发现:在VS2022里 全局变量定义的时候不赋初试值 会有默认值(int就是0) 不会报错 但是局部变量定义不赋初值的话 就直接报错了
-
如果定义在后面 使用在前面 就要声明一下
-
总而言之 先定义再使用 定义的时候记得初始化(代码更规范 可控)
-
对于
全局变量(不初始化有默认值)
来说 有如下规则(老师提了一下…我感觉有点抠字眼了)
3.9 局部/全局/静态变量的默认值
- 静态区创建的变量(全局变量or静态变量)不初始化 默认是0
- 但是局部变量 不初始化 放的是随机值 建议都初始化一下
3.10 变量的初始化和赋值
- 赋值要从右往左读 读成把20赋给a
3.11 函数是没有生命周期的概念的
- 生命周期是针对变量来说的
- 函数就是一坨代码 不管调不调用它都在 只不过存在一个能不能调用的问题 是没有生命周期的概念的
- 函数是一段代码 一段二进制的东西 如果函数没有被调用 他是不会向内存申请空间的 只有函数被调用 才会开辟函数栈帧
- static其实就是影响了函数能够调用的范围
四、字符串
4.1 概念
4.2 两种字符数组
- ‘\0’ 作为字符串的结束标志 不算作字符串的内容(strlen的时候不会算\0)
- { }定义什么就是什么;"XXX"自带一个\0
- 一个字符串也可以放到一个字符数组里去
4.3 %s打印字符串(数组名/指针变量/起始地址作为参数)
- %s打印字符串 从传给printf的指针变量(看做起始地址)开始 打印到 第一个’\0’ 就停止
- strcpy返回的是arr1的首地址 然后用ret接该首地址
- 对于{ }的方式 这样写就正常了
4.4 strlen()求字符串长度
- strlen()计算字符串长度 只计算 \0 之前的长度
- 因为\0不算字符串的内容 所以用strlen()求长度的时候 也不会算\0(遇到第一个\0就不再往后算)
- 所以len1的结果是个随机值 未知
4.5 char *p = “hello” 和 字符数组的区别
- char *p = “hello” 和 char arr[10]=“hello” 是不一样的概念
- p是一个指向字符串常量的指针 是不可以通过解引用p来修改p所指向的字符串常量的
*p是一个指针(建议用const修饰*p) 指向字符串常量;arr是一个字符数组
- 字符串字面常量存在只读内存中;字符数组存在栈区
4.6 sizeof和strlen()
- strlen( )是库函数 是计算字符串长度的 统计的是字符串中第一个\0之前出现的字符个数 他仅仅针对于字符串
- sizeof是一个单目操作符(只有一个操作数)
● sizeof(数据类型) 数据类型占内存大小
● sizeof(变量名) 变量所占内存大小 - 注意:sizeof(数组名)算的是整个数组的大小 此时的数组名表示整个数组 而不是首元素地址
4.7 ’ ’ 与 " "(存在问题)
- 字符一定要用’ '单引号引起来 而且里面只能有一个字符
- " "里可以引≥1个字符 “a” 这也是一个字符串 只不过该字符串只有一个字符
- 这个问题应该和char的范围有关系 忘记后面在哪 看到记得补上
4.8 直接printf(“hello”) 或者 printf(地址)
来看一下函数原型 这是没问题的
字符串常量如果作为一个表达式 他的值本来就是首字符地址
printf(“hello”)本来就相当于printf(&h)
也可以直接写成printf(str)
因为这里只需要打印一个字符串
常量字符串 放在表达式里面 放的其实是首字符的地址
注意:一模一样的字符串常量在只读区只需要存一份
4.9 sizeof和strlen()求字符串(数组元素个数)长度
1.
不能用strlen求int arr[]的长度 strlen是专门给字符串用的
int类型的数组只能用sizeof计算
2.求字符串的长度直接就用strlen()函数 下图用sizeof求是错的因为它无法考虑字符串结束标志
3.总结来说
strlen()函数时专门用来求字符串(有\0作为结束标志)的长度的 即有效字符个数
而sizeof单目操作符可以通过计算来求出各种类型数组的元素个数
五、转义字符与ASCII码表
5.1 转义字符是什么
- 转义字符 本质还是字符–>%c 也满足char的范围 也要用单引号’ '引起来
5.2 常见的转义字符
-
\?防止在书写连续多个?的时候 ?被解析成三字母词
-
\\防止\被解析成一个转移序列符号
-
\0也是一个转义字符 表示空字符(NULL)
-
退格符
5.3 三字母词(了解即可)
- 在支持三字母词的编译器下 ??) 被解析成 ]
- 为了避免这种情况 就可以用\? 让?就是一个? 而不是三字母词的一份子
5.4 打印c:\code\test.c(\+任意字母是什么效果)
- 我的推论是不管C语言有没有规定过某个转义字符 \总是会跟他最近的一个字符结合起来 所以想打印\ 无脑用\\就行
5.5 \ddd与\xdd 0开头与0x开头
- 注意了 \ddd和\xdd这俩本质都是字符 是char
- \ddd表示1到3个八进制数字 如:\130(字符X)
- \xdd表示2个十六进制数字 如:\x30(字符0)
- 八进制073 = 十进制59 对应到ASCII码表就是字符;
- 意思是\后面跟的1到3位0~7的数字 都看做是八进制数字 他们总体只算做一个字符
如果是字面上去表示8/16进制的时候 八进制是0开头:071 十六进制是0x开头:0x23
5.6 ASCII码表
?#$a....这些字符或者符号 比较特殊 而内存里存的都是二进制 怎么把这些符号存到内存里?--->给这些符号编号 再把编号对应的二进制存进去--->ASCII编码
- ‘a’—>97
- ‘A’—>65
- ‘0’—>48
- 小写a比大写A大了32 A+32=a
5.7 printf(“%d”,‘\777’); 为什么会报错(char的范围) 仍存在问题
-
首先 不管用%d还是%c 去打印转义字符’\777’都会报错 因为转义字符他本质上也是个字符 也就是char 范围要么是-128~127(有符号) 要么是0~255(无符号) \777这个字符对应十进制511 本身就太大了!!!
-
而ASCII码表能表示128个字符(0~127) 在这个范围用%c打印 是可以查表看看打印啥字符的
-
%c打印字符的时候 如果这个字符不在ASICC码表的范围 他也能打印出奇怪的东西 就不再深究了
-
\200=128 但对于char来说 128=127+1= -128 后续章节会详述
-
仍存在问题 写完数据的存储 再回头
5.8 理解0 ‘0’ ‘\0’
-
字符’a’—>97(ASCII 码值)
-
字符’0’—>48(ASCII 码值)
-
字符’\0’—>0(ASCII 码值)
-
只不过字符\0这个转义字符 他就表示一个空字符 所以在ASCII码码值为0的地方是空白
-
所以"abc" = {‘a’,‘b’,‘c’,‘\0’} = {97,98,97,0}
六、printf与scanf
6.1 scanf的返回值及如何多组输入
-
scanf是C语言的 scanf_s是VS特有的
-
scanf的返回值:实际读到数据的个数 如果读取数据失败 就返回 EOF
-
EOF(-1)—End Of File 文件结束标志(只不过在 scanf 这里函数里用到了)
-
如下图 就可以实现多组输入了
-
假设明确知道需要读取到几个数据 也可以用==num来判断
-
EOF的本质其实是-1 如果读取失败或者一个都没读到 就返回EOF
6.2 scanf的格式要注意
- scanf一定注意格式 要一模一样
- 要不然 明显就有bug了
6.3 scanf和printf的占位符
- long long - scanf和printf都是%lld
- 对于scanf float是%f double是%lf 不能混淆
- 在vs里 对对printf float肯定是%f double也可以写成%lf的
建议就统一float-%f double-%lf
6.4 printf打印怎么对齐
- %2d 右对齐 不足2位 左边边补空格
- 左对齐换成负数就行了
- 左-右+
- %-2d 左对齐 右边补空格
6.5 printf格式一定要匹配 避免不必要的问题
-
这种奇怪的bug 完全是可以避免的
-
格式一定要匹配 避免未定义行为!!!
6.6 scanf里不要有中文提示\n之类的
- 写昏了头 居然写出这种代码了 还没反应过来…
- scanf是严格根据" "里的格式来的 这么写 控制台输入也要是请输入:XX 很不合理 所以一般情况 scanf只有占位符+地址 然后去缓冲区读取数据
6.7 scanf遇到空格或者\n就不继续读了
- 详细介绍见本文的10.3
注意了:scanf拿字符串的时候 只拿到空格或者\n之前
遇到空格或者\n 就不拿了
6.8 scanf用%s读取字符串的时候 在恰当的时候会自己加上’\0’
注意我按回车键入之后的变化
自己加上了一个’\0’
我猜测这是scanf做的事
因为虽然"ff"自带\0 但是我键入ff回车的话 应该并没有自带\0
七、数组简介
7.1 数组的创建和初始化
- 也可以不指定大小 但必须显式初始化 会根据后面的内容分配大小
7.2 数组不完全初始化的默认值
- 注意:char类型数组不完全初始化默认值是 ‘\0’ 而不是’0’
7.3 C99标准支持变长数组
- 定义时:arr[ x ] 一般来说 x 肯定是常量
- 访问时:arr[ x ] x 才可以是变量
- 如果编译器支持C99 x就可以写成变量(下图是利用gcc测试变长数组)
- 但是如果定义了变长数组 就绝对不可以初始化(创建的同时赋值 就是初始化)
- 这里也有一个误区:变长数组不是说数组的大小能随时变化 "变"仅仅体现在可以用变量定义数组的元素个数 一旦那个变量的值确定了 数组的元素个数也就确定了
7.4 如何访问数组元素
- 下标默认从0开始
- 定义数组的时候 [ ]里必须是常量 表示数组的元素个数
- 但是访问数组元素的时候 [ ]里可以是变量 变量的值就是下标值
7.5 数组名本身就是地址 %s打印的时候不需要再&
八、常见操作符
8.1 %与/
-
%取模(取余) 两边必须都是整数
-
/号两端的操作数如果都是整数 执行的是整数除法 结果也是整数
-
/号两端如果至少有一个浮点数 就执行浮点数除法 结果也是浮点数
-
注意(float)强转的优先级
-
(float)(10/4)答案是2.0->10/4整体被强转
-
(float)10/4答案是2.5->10先被强转
8.2 !逻辑反操作-单目操作符
- 一般是在if里用 !真就是假
8.3 强制类型转换-单目操作符
- 强转( ) 也是一个单目操作符
- 强转不会发生四舍五入 直接舍去了小数部分
8.4 逻辑操作符&& ||
- &&有假则假
- || 有真就真
- 他们只针对真和假
8.5 易错:C语言中 不能使用连等判断
-
先判断18<= 2 假的 结果为 0
-
然后判断0<=36 成立 故打印青年 显然是错误的
-
正确逻辑
8.6 条件操作符
- 如果a>b
- 就执行a = a - 1 :后的表达式就不执行
- 并把 a = a - 1的结果 作为整个右边条件表达式的结果赋给m
- 如果1为真 就执行2但不执行3 且2的结果作为整个表达式的结果
- 如果1为假 就执行3但不执行2 且3的结果作为整个表达式的结果
8.7 sizeof-单目操作符
- sizeof是操作符 不是函数
8.8 ++ - - 单目操作符
- a++是一个表达式 把表达式的值赋给b 由于后置++先使用再++的特点 表达式的值是a原来的值 也就是100
- 最终a = 101 b = 100
8.9 易错:==是判断 =是赋值
- == >= 等等是关系操作符
- =是赋值操作符
8.10 下标引用操作符[ ]
- 第一个[ ]不是操作符 他就是定义数组的语法
- 第二个[ ]才是下标引用操作符
九、常见关键字
9.1 C语言有哪些关键字
- 关键字是不能作为变量名的
- C语言规定好了关键字 用户是不可能自己造关键字的
9.2 auto
- 进{ }的时候 创建变量 出去的时候 就销毁了 (不是真销毁 是还给操作系统)
- 即:局部变量:自动创建 自动销毁 所以又叫做自动变量(auto修饰的变量)
- 既然所有局部变量都是这样的 那么 auto 后来就被省略了
9.3 register寄存器
早期CPU处理的数据来自于内存,因为早期CPU计算/处理的速度不是非常快,内存的访问速度能跟得上CPU(配合的好)
但是后来随着发展,CPU的处理速度越来越快,存储设备的读写速度提升的却没有那么快,逐渐拉开差距,就采取下面这种方式,使得CPU的处理速度整体更高效
CPU每次都去寄存器拿数据 但是与此同时内存的数据都被载入到高速缓存 高速缓存的数据也在载入到寄存器
- 寄存器是集成到CPU上的 和内存没有关系 寄存器是一块独立的存储空间
- 使用这个关键字 只是建议把a放进寄存器 到底放不放 取决于编译器
- 假如a频繁大量的使用 把a放到寄存器 效率会高一点
- 有时候编译器根据实际情况 不写register 它也会载入寄存器 所以在当前比较聪明的编译器下 register的意义也不是很大了
9.4 typedf类型重命名
9.5 static
链接属性
- 内部链接属性:只能在当前源文件内部使用
- 外部链接属性:声明得当,可以跨文件使用
- 局部变量是没有链接属性的 不管是不是被static修饰 局部变量都只能在自己的局部范围使用 所以static改变的是局部变量的生命周期 而非作用域
- 全局变量和函数才谈论链接属性 没有static修饰 是具有外部链接属性的(自然也涵盖了内部链接属性) 一旦被static修饰 就只有内部链接属性了
修饰局部变量
- 注意:这俩a都还是局部变量 作用域是没有改变的!!! 相当于是改变了生命周期
- 作用域不会改变:static修饰的局部变量a 依然只能在test函数内部才能使用
- 生命周期变长了:static修饰的局部变量a 出了他的作用域 并没有销毁 直到程序结束才销毁
- 下图栈区说的函数参数一般指的是形参
不用static修饰的情况:
- 每次调用test( ) 进入这个函数 创建局部变量 出去就销毁 所以这十次 每次都是重新创建的a 然后a++ 然后打印a 然后出去并且销毁
- 这个a是在栈区的 是临时的 出了作用域就被释放了
用static修饰的情况:
- 这个a是在静态区的 静态区的变量在创建之后 直到程序结束才会释放 作用域不变 但是生命周期延长了
- 第一次调用test()的时候创建的a 并没有被销毁
- 调试的时候 直接跳到56行执行++了 而如果没有static 每次都会执行int a = 0 重新创建a
修饰全局变量
-
如果只想自己独享某个全局变量–>就用static修饰该全局变量
-
无static修饰的全局变量:全局变量本身具有外部链接属性 在A文件中定义的全局变量 在B文件中可以通过链接使用
-
static修饰的全局变量:只有内部链接属性 只能在该全局变量当前的源文件下使用
-
即使用extern声明了 也无法使用
-
即static关键字会把全局变量的外部链接属性变成内部链接属性–>使得全局变量作用域变小
修饰函数
-
函数和全局变量的情况很类似 函数本身也是具有外部链接属性的
-
如果只想自己独享某个函数–>就用static修饰该函数
理解一下为啥要用extern声明一下?
-
当编译test.c的时候( 如果没有extern int Add(int,int);) 会报警告:说Add未定义
-
因为C语言的编译器 都是对这种.c文件单独分开编译的 单独编译test.c的时候 如果直接用了Add 但是Add其实是定义在add.c的 你在test.c里又不声明一下说已经有了Add这个函数 肯定不规范 编译器肯定也不认识Add函数 想不报警告 就要加上extern外部声明
-
被static修饰之后 就变成了内部链接属性了 使得该函数只能在自己所在的源文件内部使用 在其他源文件无法使用
-
即使声明了该函数 也不能在其他文件使用
-
其实就是限制了函数的作用域 改变了函数能够被调用的范围 仿佛把那个函数隔离了
9.6 #define不是关键字 他是预处理指令
-
这东西不是关键字!!! 他是用来定义符号和宏的
-
他可以定义标识符常量 这个M不是全局变量 但是可以看作一个全局的"符号"
-
或者定义宏 宏会直接被它的宏体替换 仅仅替换 不做任何其他操作
-
符号和宏的区别就在于:宏是有参数的
-
一般宏都是处理比较简单的逻辑 复杂的不建议用宏
9.7 宏和函数的区别
- 函数的参数有类型 宏没有
- 函数有返回类型 宏没有
- 函数{ }里是函数体 宏的宏体直接定义在后面
- 函数处理复杂逻辑 宏处理简单逻辑
十、getchar和putchar
10.1 getchar的功能和返回值
- scanf 和 printf针对各种各样的类型的数据
- 但是getchar 和 putchar是专门针对字符(char)的
- getchar的返回值是:读取到的字符对应的ASCII码值 比如读取到A 就返回65
- getchar如果读取失败 也会返回EOF
10.2 putchar的功能和返回值
这就是二者最基本的使用
10.3 输入缓冲区的概念
平时用scanf或者getchar的时候 为什么光标在黑框框闪烁等你输入? **这是因为输入缓冲区里没有数据 所以只能等你再从键盘输入数据到输入缓冲区!**
下图要好好理解一下:其实我键盘输入A 然后按回车
是执行了两次循环的
循环开始之后 发现输入缓冲区没有东西 只能闪烁等待键盘输入
当我输入A(也就是按了A 和 回车)这个时候输入缓冲区里放的是A和\n
getchar先读取到A
返回值是65 != EOF(-1) 就进入循环体 执行putchar(‘A’)
然后又回到判断部分这一次getchar读取到了\n
也!=EOF 所以又进入了循环体 并putchar(‘\n’) 所以有换行的效果
然后第三次
来到判断部分 这才发现 输入缓冲区里已经没有东西了 再次闪烁等待键入
- 那下面的代码 理解起来就很容易了
10.4 getchar和scanf关于缓冲区的问题
键盘输入数据-->缓冲区 getchar和scanf 是从缓冲区里拿数据
下图的错因 就很显然了
刚开始执行 缓冲区没东西
所以scanf等待键盘输入东西进缓冲区 我输入123456 然后按回车
这个时候缓冲区有:123456\n
然后scanf就把123456这个字符串拿走了 并赋给password(因为是%s)
缓冲区还剩:\n
注意了:scanf拿字符串的时候 只拿到空格或者\n之前 遇到空格或者\n 就不拿了
继续执行getchar发现缓冲区有东西:\n 他就不会闪烁光标等待键盘输入 而是直接把\n拿走 并赋给ch
然后发现ch != Y故直接就打印:确认失败
在ch = getchar( )前面 再加一句getchar( ); 就可以解决问题
这个新的getchar( )把\n拿走了
然后执行到ch=getchar( )发现缓冲区没东西 就会闪烁光标等待输入
10.5 利用while清理缓冲区
进一步的 假如我输入的更逆天
不仅仅是字符串+回车
比如我输入 abc空格efg\n
那么scanf只读到abc(遇到空格或者\n都不读了) 后面一坨留在缓冲区了怎么清理?
方案是:用while清理(注意下图的循环 包括\n也清理了 因为是先清理 再判断的)
执行流程 假如输入abc efg\n:
scanf把abc拿走了 遇到空格就不继续读了
`
然后getchar拿走空格 返回对应的ACISS码值 !=\n 继续
拿走e 返回对应的ACISS码值 !=\n 继续
拿走f 返回对应的ACISS码值 !=\n 继续
拿走g 返回对应的ACISS码值 !=\n 继续
拿走\n(拿走了再判断的) 发现== ‘\n’ 循环结束
10.6 我的一个猜测-建议每次%c读取字符之前 都清理一下缓冲区
- 这个没什么意义 仅仅是我的上机得到的猜测 没必要纠结 随便看看
问题描述:
经过实践 我猜测
- 如果缓冲区里的东西不满足占位符比如%d的要求 他也会等待键入
- %d似乎把上次%s键入的\n也给拿走了 这也许是VS的优化
那为什么%c不把marry留下的\n也拿走?
因为%c要的就是字符啊
他一看缓冲区还有一个\n 就是字符 都不等待键入 直接拿走了
- 那也就是说 除了%c 其他的没必要过于操心
如果有%c读取 就要当心缓冲区是不是干净了 在读取%c字符之前 最好用while循环清理一下缓冲区的内容
int main()
{
char name[10] = "";
int age = 10;
double saly = 0.0;
char gender = ' ';
//读取
printf("姓名:\n");
scanf("%s", name);
printf("年龄:\n");
scanf("%d", &age);
printf("工资:\n");
scanf("%lf", &saly);
//此前 都很正常
//读取%c之前 就要当心缓冲区是不是干净了 建议用while清理一下
while (getchar() != '\n')
{
;
}
//清理过后 再读取%c 就不会出错
printf("性别:\n");
scanf("%c", &gender);
printf("%s %d %1f %c", name, age, saly, gender);
return 0;
}
十一、其他
说明
这是平时钻的一些牛角尖 我认为没有太大意义 不需要过度纠结
这种语句放在函数内部
- 给他加个大括号也不行 必须在函数内部 不然没机会执行 语法也是错的
- 这tm谁想出来的
变量才能赋值 表达式不能
- 常量和变量比较的时候 建议把常量放在左边 这样就可以防止把==写成=而没发现了
- 因为写if(10 = a) 就err了 然后就发现了 ==写成=了
- 10 = a //err
- a = 10 //ok