C语言复习第0章 基础语法
目录
- 一、概述及前置知识
- 1.1 什么是集成开发环境
- 1.2 main函数
- 1.3 单位
- 1.4 注释
- 1.5 函数简介
- 1.6 C语言中真和假的概念
- 1.7 C语言的内存分区
- 1.8 EOF-文件结束标志
- 1.9 头文件一般放什么内容
- 二、数据类型
- 2.1 八大类型的大小
- 2.2 默认浮点数为double类型
- 2.3 占位符(????????)
- 2.4 如何表示八/十进制
- 2.5 C语言中 表达式有两个属性
- 2.6 C语言标准没有规定char类型的符号
- 2.7 C99引入布尔类型
- 三、变量与常量
- 3.1 变量的命名规则
- 3.2 变量最好要初始化
- 3.3 变量的分类
- 3.4 变量的作用域
- 概念
- 局部变量
- 全局变量
- 3.5 变量的生命周期
- 3.6 常量
- 字面常量(注意字符串字面常量)
- const 修饰的==常变量==
- #define 定义的标识符常量
- 枚举常量
- 3.7 关于const
- 字符串常量 都用const修饰更规范
- const修饰指针变量(分左右)
- 擅用const的一个实际意义
- 3.8 变量的声明和定义
- 3.9 局部/全局/静态变量的默认值
- 3.10 变量的初始化和赋值
- 3.11 函数是没有生命周期的概念的
- 四、字符串
- 4.1 概念
- 4.2 两种字符数组
- 4.3 %s打印字符串(数组名/指针变量/起始地址作为参数)
- 4.4 strlen()求字符串长度
- 4.5 ==char *p = "hello"== 和 ==字符数组==的区别
- 4.6 sizeof和strlen()
- 4.7 ' ' 与 " "(存在问题)
- 4.8 直接printf("hello")(存在问题)
- 五、转义字符与ASCII码表
- 5.1 转义字符是什么
- 5.2 常见的转义字符
- 5.3 三字母词(了解即可)
- 5.4 打印c:\code\test.c(\\+任意字母是什么效果)
- 5.5 \ddd与\xdd 0开头与0x开头
- 5.6 ASCII码表
- 5.7 %c打印字符的时候 要注意第二个参数的大小不能太大
- 5.8 理解0 '0' '\0'
- 六、printf与scanf
- 6.1 scanf的返回值及如何多组输入
- 6.2 scanf的格式要注意
- 6.3 scanf和printf占位符的区别
- 6.4 printf打印怎么对齐
- 6.5 printf格式一定要匹配 避免不必要的问题
- 七、数组简介
- 7.1 数组的创建和初始化
- 7.2 数组不完全初始化的默认值
- 7.3 C99标准支持变长数组
- 7.4 如何访问数组元素
- 7.5 数组名本身就是地址 %s打印的时候不需要再&
- 八、常见操作符
- 8.1 %与/
- 8.2 !逻辑反操作-单目操作符
- 8.3 强制类型转换-单目操作符
- 8.4 逻辑操作符&& ||
- 8.5 易错:C语言中 不能使用连等判断
- 8.6 条件操作符
- 8.7 sizeof-单目操作符
- 8.8 ++ - - 单目操作符
- 8.9 易错:==是判断 =是赋值
- 8.10 下标引用操作符[ ]
- 九、常见关键字
- 9.1 C语言有哪些关键字
- 9.2 auto
- 9.3 register寄存器
- 9.4 typedf类型重命名
- 9.5 static
- 链接属性
- 修饰局部变量
- 修饰全局变量
- 修饰函数
- 9.6 #define==不是关键字== 他是预处理指令
- 9.7 宏和函数的区别
一、概述及前置知识
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
- 但是局部变量 不初始化 放的是随机值
1.8 EOF-文件结束标志
- EOF本质是-1
- 成功读取到2个整数 scanf就返回2
- 正常情况输入Ctrl+Z scanf就会读取失败 但是VS有一个bug 要连按三次
- 这样的话 第一个100成功读取 第二个失败 返回1
1.9 头文件一般放什么内容
二、数据类型
2.1 八大类型的大小
- sizeof 是一个操作符 而不是函数 计算结果的单位是字节
- C语言标准规定:sizeof(long)≥sizeof(int) 即可 当前编译器取的就是等于
- 为什么需要丰富的类型:为了描述实际问题 本身就需要整数 小数 字符…
- 为什么同一种类型又分好几类:如果描述年龄 short就足够了 适当的类型给适当的东西用 更加节省空间
2.2 默认浮点数为double类型
- 浮点数无法精确存储 详见后面的IEE754的规则
2.3 占位符(???)
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解引用被修改
- 总结来说 const修饰指针变量 修饰的就是const右边的那一坨东西
- 要么是:指针本身不可以修改(不可以指向别人)
- 要么是:不可以通过指针(解引用)修改指针所指向的内容
- 这种情况就是
p彻底被限制了 p不能改 *p也改不了
- 所以 当我们希望一个变量比如说num 不能被修改 首先num本身要用const修饰 当把num交给一个指针的时候 最好把const放在*的左边 这样也无法通过指针来修改num
擅用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”)(存在问题)
五、转义字符与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 %c打印字符的时候 要注意第二个参数的大小不能太大
- ASCII码表能表示128个字符(0~127)
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占位符的区别
- %lld - long long
- 对于浮点数来说 scanf分%lf和%f 而printf都是%f
6.4 printf打印怎么对齐
- %2d 右对齐 不足2位 左边边补空格
- 左对齐换成负数就行了
- 左-右+
- %-2d 左对齐 右边补空格
6.5 printf格式一定要匹配 避免不必要的问题
-
这种奇怪的bug 完全是可以避免的
-
格式一定要匹配 避免未定义行为!!!
七、数组简介
7.1 数组的创建和初始化
- 也可以不指定大小 但必须显式初始化 会根据后面的内容分配大小
7.2 数组不完全初始化的默认值
- 注意:char类型数组不完全初始化默认值是 ‘\0’ 而不是’0’
7.3 C99标准支持变长数组
- 定义时:arr[ x ] 一般来说 x 肯定是常量
- 访问时:arr[ x ] x 才可以是变量
- 如果编译器支持C99 x就可以写成变量
- 但是如果定义了变长数组 就绝对不可以初始化(创建的同时赋值 就是初始化)
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 宏和函数的区别
- 函数的参数有类型 宏没有
- 函数有返回类型 宏没有
- 函数{ }里是函数体 宏的宏体直接定义在后面
- 函数处理复杂逻辑 宏处理简单逻辑