本文最后更新于 105 天前,其中的信息可能已经过时,如有错误请发送邮件到 shikeAB@outlook.com。
【转载】C 语言入门到精通,这一篇就够了(13 万字笔记)5
枚举
-
什么是枚举类型?
- 在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等。如果把这些量说明为整型,字符型或其它类型 显然是不妥当的。
- C 语言提供了一种称为 “枚举” 的类型。在 “枚举” 类型的定义中列举出所有可能的取值,被说明为该 “枚举” 类型的变量取值不能超过定义的范围。
- 该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。
-
枚举类型的定义
- 格式:
enum 枚举名 { 枚举元素1, 枚举元素2, …… };
-
- 示例:
// 表示一年四季 enum Season { Spring, Summer, Autumn, Winter };
- 枚举变量
- 先定义枚举类型,再定义枚举变量
enum Season { Spring, Summer, Autumn, Winter }; enum Season s;
-
- 定义枚举类型的同时定义枚举变量
enum Season { Spring, Summer, Autumn, Winter } s;
- 省略枚举名称,直接定义枚举变量
enum { Spring, Summer, Autumn, Winter } s;
- 枚举类型变量的赋值和使用
enum Season { Spring, Summer, Autumn, Winter } s; s = Spring; // 等价于 s = 0; s = 3; // 等价于 s = winter; printf("%d", s);
- 枚举使用的注意
- C 语言编译器会将枚举元素 (spring、summer 等) 作为整型常量处理,称为枚举常量。
- 枚举元素的值取决于定义时各枚举元素排列的先后顺序。默认情况下,第一个枚举元素的值为 0,第二个为 1,依次顺序加 1。
- 也可以在定义枚举类型时改变枚举元素的值
enum Season { Spring, Summer, Autumn, Winter }; // 也就是说spring的值为0,summer的值为1,autumn的值为2,winter的值为3
enum Season { Spring = 9, Summer, Autumn, Winter }; // 也就是说spring的值为9,summer的值为10,autumn的值为11,winter的值为12
全局变量和局部变量
- 变量作用域基本概念
- 变量作用域:变量的可用范围
- 按照作用域的不同,变量可以分为:局部变量和全局变量
- 局部变量
- 定义在函数内部的变量以及函数的形参,我们称为局部变量
- 作用域:从定义的那一行开始,直到遇到} 结束或者遇到 return 为止
- 生命周期:从程序运行到定义哪一行开始分配存储空间到程序离开该变量所在的作用域
- 存储位置:局部变量会存储在内存的栈区中
- 特点:
- 相同作用域内不可以定义同名变量
- 不同作用范围可以定义同名变量,内部作用域的变量会覆盖外部作用域的变量
- 全局变量
- 定义在函数外面的变量称为全局变量
- 作用域范围:从定义哪行开始直到文件结尾
- 生命周期:程序一启动就会分配存储空间,直到程序结束
- 存储位置:静态存储区
- 特点:多个同名的全局变量指向同一块存储空间
auto 和 register 关键字
- auto 关键字 (忘记)
- 只能修饰局部变量,局部变量如果没有其它修饰符,默认就是 auto 的
- 特点:随用随开,用完即销
auto int num; // 等价于 int num;
- register 关键字 (忘记)
- 只能修饰局部变量,原则上将内存中变量提升到 CPU 寄存器中存储,这样访问速度会更快
- 但是由于 CPU 寄存器数量相当有限,通常不同平台和编译器在优化阶段会自动转换为 auto
register int num;
static 关键字
- 对局部变量的作用
- 延长局部变量的生命周期,从程序启动到程序退出,但是它并没有改变变量的作用域
- 定义变量的代码在整个程序运行期间仅仅会执行一次
#include void test(); int main() { test(); test(); test(); return 0; } void test(){ static int num = 0; // 局部变量 num++; // 如果不加static输出 1 1 1 // 如果添加static输出 1 2 3 printf("num = %i\n", num); }
- 对全局变量的作用
- 全局变量分类:
- 内部变量:只能在本文件中访问的变量
- 外部变量:可以在其他文件中访问的变量,默认所有全局变量都是外部变量
- 默认情况下多个同名的全局变量共享一块空间,这样会导致全局变量污染问题
- 如果想让某个全局变量只在某个文件中使用,并且不和其他文件中同名全局变量共享同一块存储空间,那么就可以使用 static
// A文件中的代码 int num; // 和B文件中的num共享 void test(){ printf("ds.c中的 num = %i\n", num); }
// B文件中的代码 #include #include "ds.h" int num; // 和A文件中的num共享 int main() { num = 666; test(); // test中输出666 return 0; }
// A文件中的代码 static int num; // 不和B文件中的num共享 void test(){ printf("ds.c中的 num = %i\n", num); }
// B文件中的代码 #include #include "ds.h" int num; // 不和A文件中的num共享 int main() { num = 666; test(); // test中输出0 return 0; }
extern 关键字
- 对局部变量的作用
- extern 不能用于局部变量
- extern 代表声明一个变量,而不是定义一个变量,变量只有定义才会开辟存储空间
- 所以如果是局部变量,虽然提前声明有某个局部变量,但是局部变量只有执行到才会分配存储空间
#include int main() { extern int num; num = 998; // 使用时并没有存储空间可用, 所以声明了也没用 int num; // 这里才会开辟 printf("num = %i\n", num); return 0; }
- 对全局变量的作用
- 声明一个全局变量,代表告诉编译器我在其它地方定义了这个变量,你可以放心使用
#include int main() { extern int num; // 声明我们有名称叫做num变量 num = 998; // 使用时已经有对应的存储空间 printf("num = %i\n", num); return 0; } int num; // 全局变量, 程序启动就会分配存储空间
static 与 extern 对函数的作用
-
内部函数:只能在本文件中访问的函数
-
外部函数:可以在本文件中以及其他的文件中访问的函数
-
默认情况下所有的函数都是外部函数
-
static 作用
- 声明一个内部函数
static int sum(int num1,int num2);
- 定义一个内部函数
static int sum(int num1,int num2) { return num1 + num2; }
- extern 作用
- 声明一个外部函数
extern int sum(int num1,int num2);
-
- 定义一个外部函数
extern int sum(int num1,int num2) { return num1 + num2; }
- 注意点:
- 由于默认情况下所有的函数都是外部函数,所以 extern 一般会省略
- 如果只有函数声明添加了 static 与 extern, 而定义中没有添加 static 与 extern, 那么无效
Qt Creator 编译过程做了什么?
- 当我们按下运行按钮的时,其实 Qt Creator 编译器做了 5 件事情
- 对源文件进行预处理,生成预处理文件
- 对预处理文件进行编译,生成汇编文件
- 对汇编文件进行编译,生成二进制文件
- 对二进制文件进行链接,生成可执行文件
- 运行可执行文件
- Qt Creator 编译过程验证
- 1. 编写代码,保存源文件:
#include int main(){ printf("hello lnj\n"); return 0; } - 2. 执行预处理编译
- 执行预处理编译后生成的文件
- 打开预处理编译后生成的文件
- 处理源文件中预处理相关的指令
- 处理源文件中多余注释等
- 3. 执行汇编编译
- 执行汇编编译后生成的文件
- 打开汇编编译后生成的文件
- 4. 执行二进制编译
- 执行二进制编译后生成的文件
- 打开二进制编译后生成的文件
- 5. 执行链接操作
- 将依赖的一些 C 语言函数库和我们编译好的二进制合并为一个文件
- 将依赖的一些 C 语言函数库和我们编译好的二进制合并为一个文件
- 执行链接操作后生成的文件
- 6. 运行链接后生成的文件
计算机是运算过程分析
- 1. 编写一个简单的加法运算
- 2. 调试编写好的代码,查看对应的汇编文件
- 结论:
- 1. 通过地址线找到对应地址的存储单元
- 2. 通过控制线发送内存读取指令
- 3. 通过数据线将内存中的值传输到 CPU 寄存器中
- 4. 在 CPU 中完成计算操作
- 5. 通过地址线找到对应地址的存储单元
- 6. 通过控制线发送内存写入指令
- 7. 通过数据线将计算结果传输到内存中
预处理指令
预处理指令的概念
- C 语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释 (比如之前使用的 #include 文件包含指令),产生一个新的源程序 (这个过程称为编译预处理), 之后再进行通常的编译
- 为了区分预处理指令和一般的 C 语句,所有预处理指令都以符号 “#” 开头,并且结尾不用分号
- 预处理指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文件尾。习惯上我们尽可能将预处理指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文件
- C 语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
宏定义
- 被定义为 “宏” 的标识符称为 “宏名”。在编译预处理时,对程序中所有出现的 “宏名”, 都用宏定义中的字符串去代换,这称为 “宏代换” 或 “宏展开”。
- 宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。在 C 语言中,“宏” 分为有参数和无参数两种。
## 不带参数的宏定义 - 格式:
#define 标识符 字符串
- 其中的 “#” 表示这是一条预处理命令。凡是以 “#” 开头的均为预处理命令。“define” 为宏定义命令。“标识符” 为所定义的宏名。“字符串” 可以是常数、表达式、格式串等。
#include // 源程序中所有的宏名PI在编译预处理的时候都会被3.14所代替 #define PI 3.14 // 根据圆的半径计radius算周长 float girth(float radius) { return 2 * PI *radius; } int main () { float g = girth(2); printf("周长为:%f", g); return 0; }
- 注意点:
- 宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误
- 2) 对程序中用双引号扩起来的字符串内的字符,不进行宏的替换操作
#define R 10 int main () { char *s = "Radio"; // 在第1行定义了一个叫R的宏,但是第4行中"Radio"里面的'R'并不会被替换成10 return 0; }
- 3) 在编译预处理用字符串替换宏名时,不作语法检查,只是简单的字符串替换。只有在编译的时候才对已经展开宏名的源程序进行语法检查
#define I 100 int main () { int i[3] = I; return 0; }
-
- 宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用 #undef 命令
#define PI 3.14 int main () { printf("%f", PI); return 0; } #undef PI void test() { printf("%f", PI); // 不能使用 }
-
- 定义一个宏时可以引用已经定义的宏名
#define R 3.0 #define PI 3.14 #define L 2*PI*R #define S PI*R*R
-
- 可用宏定义表示数据类型,使书写方便
#define String char * int main(int argc, const char * argv[]) { String str = "This is a string!"; return 0; }
带参数的宏定义
- C 语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参
- 格式:
#define 宏名(形参表) 字符串
// 第1行中定义了一个带有2个参数的宏average, #define average(a, b) (a+b)/2 int main () { // 第4行其实会被替换成:int a = (10 + 4)/2;, int a = average(10, 4); // 输出结果为:7是不是感觉这个宏有点像函数呢? printf("平均值:%d", a); return 0; }
- 注意点:
- 1) 宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串.
#define average (a, b) (a+b)/2 int main () { int a = average(10, 4); return 0; } 注意第1行的宏定义,宏名average跟(a, b)之间是有空格的,于是,第5行就变成了这样: int a = (a, b) (a+b)/2(10, 4); 这个肯定是编译不通过的
- 2) 带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。
#include // 下面定义一个宏D(a),作用是返回a的2倍数值: #define D(a) 2*a // 如果定义宏的时候不用小括号括住参数 int main () { // 将被替换成int b = 2*3+4;,输出结果10,如果定义宏的时候用小括号括住参数,把上面的第3行改成:#define D(a) 2*(a),注意右边的a是有括号的,第7行将被替换成int b = 2*(3+4);,输出结果14 int b = D(3+4); printf("%d", b); return 0; }
- 3) 计算结果最好也用括号括起来
#include // 下面定义一个宏P(a),作用是返回a的平方 #define Pow(a) (a) * (a) // 如果不用小括号括住计算结果 int main(int argc, const char * argv[]) { // 代码被替换为:int b = (10) * (10) / (2) * (2); // 简化之后:int b = 10 * (10 / 2) * 2;,最后变量b为:100 int b = Pow(10) / Pow(2); printf("%d", b); return 0; }
#include // 计算结果用括号括起来 #define Pow(a) ( (a) * (a) ) int main(int argc, const char * argv[]) { // 代码被替换为:int b = ( (10) * (10) ) / ( (2) * (2) ); // 简化之后:int b = (10 * 10) / (2 *2);,最后输出结果:25 int b = Pow(10) / Pow(2); printf("%d", b); return 0; }
条件编译
- 在很多情况下,我们希望程序的其中一部分代码只有在满足一定条件时才进行编译,否则不参与编译 (只有参与编译的代码最终才能被执行),这就是条件编译。
- 为什么要使用条件编译
- 1) 按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。有利于程序的移植和调试。
- 2) 条件编译当然也可以用条件语句来实现。 但是用条件语句将会对整个源程序进行编译,生成 的目标代码程序很长,而采用条件编译,则根据条件只编译其中的程序段 1 或程序段 2, 生成的目 标程序较短。
##if-#else 条件编译指令
- 第一种格式:
- 它的功能是,如常量表达式的值为真 (非 0), 则将 code1 编译到程序中,否则对 code2 编译到程序中。
- 注意:
- 是将代码编译进可执行程序,而不是执行代码
- 条件编译后面的条件表达式中不能识别变量,它里面只能识别常量和宏定义
#if 常量表达式 ..code1... #else ..code2... #endif
#define SCORE 67 #if SCORE > 90 printf("优秀\n"); #else printf("不及格\n"); #endif
- 第二种格式:
#if 条件1 ...code1... #elif 条件2 ...code2... #else ...code3... #endif
#define SCORE 67 #if SCORE > 90 printf("优秀\n"); #elif SCORE > 60 printf("良好\n"); #else printf("不及格\n"); #endif
typedef 关键字
- C 语言不仅供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取 “别名”。
- 格式:
typedef 原类型名 新类型名;
- 其中原类型名中含有定义部分,新类型名一般用大写表示,以便于区别。
- 有时也可用宏定义来代替 typedef 的功能,但是宏定义是由预处理完成的,而 typedef 则是在编译 时完成的,后者更为灵活方便。
##typedef 使用
- 基本数据类型
typedef int INTEGER INTEGER a; // 等价于 int a;
- 也可以在别名的基础上再起一个别名
typedef int Integer; typedef Integer MyInteger;
-
用 typedef 定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为 明确,因而增强了可读性。
-
数组类型
typedef char NAME[20]; // 表示NAME是字符数组类型,数组长度为20。然后可用NAME 说明变量, NAME a; // 等价于 char a[20];
- 结构体类型
- 第一种形式:
struct Person{ int age; char *name; }; typedef struct Person PersonType;
+ 第二种形式:
typedef struct Person{ int age; char *name; } PersonType;
+ 第三种形式:
typedef struct { int age; char *name; } PersonType;
- 枚举
- 第一种形式:
enum Sex{ SexMan, SexWoman, SexOther }; typedef enum Sex SexType;
+ 第二种形式:
typedef enum Sex{ SexMan, SexWoman, SexOther } SexType;
+ 第三种形式:
typedef enum{ SexMan, SexWoman, SexOther } SexType;
- 指针
- typedef 与指向结构体的指针
// 定义一个结构体并起别名 typedef struct { float x; float y; } Point; // 起别名 typedef Point *PP;
- typedef 与指向函数的指针
// 定义一个sum函数,计算a跟b的和 int sum(int a, int b) { int c = a + b; printf("%d + %d = %d", a, b, c); return c; } typedef int (*MySum)(int, int); // 定义一个指向sum函数的指针变量p MySum p = sum;
宏定义与函数以及 typedef 区别
- 与函数的区别
- 从整个使用过程可以发现,带参数的宏定义,在源程序中出现的形式与函数很像。但是两者是有本质区别的:
- 1> 宏定义不涉及存储空间的分配、参数类型匹配、参数传递、返回值问题
- 2> 函数调用在程序运行时执行,而宏替换只在编译预处理阶段进行。所以带参数的宏比函数具有更高的执行效率
- 从整个使用过程可以发现,带参数的宏定义,在源程序中出现的形式与函数很像。但是两者是有本质区别的:
- typedef 和 #define 的区别
- 用宏定义表示数据类型和用 typedef 定义数据说明符的区别。
- 宏定义只是简单的字符串替换,是在预处理完成的
- typedef 是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能
- 用宏定义表示数据类型和用 typedef 定义数据说明符的区别。
typedef char *String; int main(int argc, const char * argv[]) { String str = "This is a string!"; return 0; } #define String char * int main(int argc, const char * argv[]) { String str = "This is a string!"; return 0; }
typedef char *String1; // 给char *起了个别名String1 #define String2 char * // 定义了宏String2 int main(int argc, const char * argv[]) { /* 只有str1、str2、str3才是指向char类型的指针变量 由于String1就是char *,所以上面的两行代码等于: char *str1; char *str2; */ String1 str1, str2; /* 宏定义只是简单替换, 所以相当于 char *str3, str4; *号只对最近的一个有效, 所以相当于 char *str3; char str4; */ String2 str3, str4; return 0; }
来自陕西