一 全局变量和局部变量
变量作用域:变量的可用范围
- 按照作用域的不同,变量可以分为:局部变量和全局变量
局部变量:
- 定义在函数内部的变量以及函数的形参称为局部变量
- 作用域:从定义哪一行开始直到与其所在的代码块结束(遇到return为止)
- 生命周期:从程序运行到定义哪一行开始分配存储空间到程序离开该变量所在的作用域
- 特点:
- 1、相同作用域内不可以定义同名变量
- 2、不同作用范围可以定义同名变量,内部作用域的变量会覆盖外部作用域的变量
注意:局部变量没有固定的初始化值,开发中千万不能使用未初始化的局部变量;
存储位置:局部变量存储在栈中,当作用域结束系统会自动释放栈中的的局部变量。
- 全局变量:
- 定义在函数外边的变量称为全局变量
- 作用域范围:从定义哪行开始直到文件结尾
- 生命周期:程序一启动就会分配存储空间,直到程序结束
- 存储位置:静态存储区
- 特点:
- 多个同名的全局变量指向同一块存储空间
如果存在和全局变量同名的局部变量,那么局部变量会覆盖全局变量
注意:全局变量如果没有进行初始化,系统默认会将全局变量初始化为0
二 内部全局变量和外部全局变量
全局变量分为2种:
- 外部全局变量,默认所有全局变量是外部全局变量;
可以被其他文件访问的全局变量称之为外部全局变量。 - 内部全局变量,给全局变量加上static关键词就是内部全局变量;
只能被当前文件访问的全局变量称之为内部全局变量。
- 外部全局变量,默认所有全局变量是外部全局变量;
外部局部变量特点:
可以定义同名的外部全局变量,多个同名的外部全局局部变量指向同一块存储空间;- 内部局部变量特点:
定义多个同名的内部全局变量,多个同名的内部全局变量如果不在同一个文件中, 指向不同的存储空间。
三 static和extern关键字
static和extern关键字-对变量的作用
static 与 extern对局部变量的作用
static对局部变量的作用
- 延长局部变量的生命周期,从程序启动到程序退出,但是它并没有改变变量的作用域
- 定义变量的代码在整个程序运行期间仅仅会执行一次
extern用在函数内部
- 不是定义局部变量,它用在函数内部是声明一个全局变量
static 全局变量的作用
全局变量分类:
- 内部变量:只能在本文件中访问的变量
- 外部变量:可以在其他文件中访问的变量,默认所有全局变量都是外部变量
static对全局变量的作用
- 声明一个内部变量
static int a;
- 定义一个内部变量
static int a = 10;
- 声明一个内部变量
由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
extern对全局变量的作用
extern作用
- 完整声明一个外部变量
extern int a;
- 完整定义一个外部变量
extern int a = 10;
- 完整声明一个外部变量
如果声明的时候没有写extern那系统会自动定义这个变量,并将其初始化为0
- 如果声明的时候写extern了,那系统不会自动定义这个变量。
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; }
四 预处理指令
C语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释(比如之前使用的#include文件包含指令),产生一个新的源程序(这个过程称为编译预处理),之后再进行通常的编译
为了区分预处理指令和一般的C语句,所有预处理指令都以符号“#”开头,并且结尾不用分号
预处理指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文件尾。习惯上我们尽可能将预处理指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文件
C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。
五 宏定义
宏的概念及定义方法
被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。
宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。在C语言中“宏”分为有参数和无参数两种。
不带参数的宏定义
- 格式 :
#define 标识符 字符串
- 其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
- 格式 :
注意点:
- 1) 宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误
- 2)对程序中用双引号扩起来的字符串内的字符,不进行宏的替换操作
- 3)在编译预处理用字符串替换宏名时,不作语法检查,只是简单的字符串替换。只有在编译的时候才对已经展开宏名的源程序进行语法检查
4) 宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用#undef命令
#define PI 3.14 int main () { printf("%f", PI); return 0; } #undef PI void test() { printf("%f", PI); // 不能使用 }
5) 定义一个宏时可以引用已经定义的宏名
- 6) 可用宏定义表示数据类型,使书写方便
#define String char * int main(int argc, const char * argv[]) { String str = "This is a string!"; return 0; }
带参数的宏定义
- C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
格式:
#define 宏名(形参表) 字符串 例: #define SUM(v1, v2) v1+v2
注意点:
- 1)宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串
2)带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。
#include <stdio.h> // 下面定义一个宏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; }
计算结果最好也用括号括起来
#include <stdio.h> // 下面定义一个宏P(a),作用是返回a的平方 #define Pow(a) (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; }
六 条件编译
一 条件编译基本概念
- 很多情况下,我们希望程序一部分代码只有在满足一定条件时才进行编译,否则不参与编译(只有参与编译的代码最终才能被执行),这就是条件编译。
条件编译和选择结构if的区别:
- 1.生命周期不同
- if 运行时
- #if 编译之前
- 2.#if需要一个明确的结束符号 #endif
为什么需要一个明确的结束符号?
如果省略掉#endif, 那么系统就不知道条件编译的范围, 那么会将满足条件之后的第二个条件之后的所有内容都清除
- 3.if会将所有的代码都编译到二进制中
- #if只会将满足条件的部分一直到下一个条件的部分编译到二进制中
- 1.生命周期不同
二 #if-#else 条件编译指令
第一种格式:
#if 常量表达式 ..code1... #else ..code2... #endif
- 它的功能是,如常量表达式的值为真(非0),则对code1 进行编译,否则对code2进行编译。因此可以使程序在不同条件下,完成不同的功能。
- 注意: 条件编译后面的条件表达式中不能识别变量,它里面只能识别常量和宏定义
第二种格式:
#if 条件1 ...code1... #elif 条件2 ...code2... #else ...code3... #endif
- 1>如果条件1成立,那么编译器就会把#if 与 #elif之间的code1代码编译进去(注意:是编译进去,不是执行,很平时用的if-else是不一样的)
- 2> 如果条件1不成立、条件2成立,那么编译器就会把#elif 与 #else之间的code2代码编译进去
- 3> 如果条件1、2都不成立,那么编译器就会把#else 与 #endif之间的code3编译进去
- 4> 注意:条件编译结束后,要在最后面加一个#endif,不然后果很严重
- 5> #if 和 #elif后面的条件一般是判断宏定义而不是判断变量,因为条件编译是在编译之前做的判断,宏定义也是编译之前定义的,而变量是在运行时才产生的、才有使用的意义。
三 #ifdef 条件编译指令
格式:
#ifdef 标识符 程序段1 #else 程序段2 #endif
它的功能是,如果标识符已被#define命令定义过则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式中的#else可以没有,即可以写为:
#ifdef 标识符 程序段 #endif
四 #ifndef 条件编译指令
格式:
#ifndef 标识符 程序段1 #else 程序段2 #endif
与第二种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define命令 定义过则对程序段1进行编译,否则对程序段2进行编译。这与第二种形式的功能正相反。
五 使用条件编译指令调试bug
- 应用:可变参数宏,更方便地打印调试信息
七 typedef关键字
- typedef基本概念
- C语言允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”。
typedef语法格式:
typedef 原类型名 新类型名;
- 其中原类型名中含有定义部分,新类型名一般用大写表示,以便于区别。
- 有时也可用宏定义来代替typedef的功能,但是宏定义是由预处理完成的,而typedef则是在编译时完成的,后者更为灵活方便。
typedef使用方法
基本数据类型
也可以在别名的基础上再起一个别名
typedef int Integer; typedef Integer MyInteger;
数组类型
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 char *String1; // 给char *起了个别名String1 #define String2 char * // 定义了宏String2 int main(int argc, const char * argv[]) { String1 str1, str2; /* 只有str1、str2才是指向char类型的指针变量 由于String1就是char *,所以上面的两行代码等于: char *str1; char *str2; */ String2 str3, str4; /* 宏定义只是简单替换, 所以相当于 char *str3, str4; *号只对最近的一个有效, 所以相当于 char *str3; char str4; */ return 0; }
九 const关键字
const是一个类型修饰符
- 使用const修饰变量则可以让变量的值不能改变
- 常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。
const主要的作用
- (1)可以定义const常量,具有不可变性。
- (2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
- (3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。
(4)可以保护被修饰的东西,防止意外的修改。如果在函数体内修改了i,编译器就会报错
void f(const int i) { i=10; //error! }
(5) 可以节省空间,避免不必要的内存分配。
- (6) 提高了效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表 中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作
如何使用const?
- (1)修饰一般常量一般常量是指简单类型的常量。这种常量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。
- (2)修饰常数组(值不能够再改变了)定义或说明一个常数组
- (3)修饰函数的常参数const修饰符也可以修饰函数的传递参数,格式如下:void Fun(const int Var)
(4)修饰函数的返回值: const修饰符也可以修饰函数的返回值,是返回值不可被改变
const int Fun1(); const MyClass Fun2();
(5)修饰常指针
- const int *A; //const修饰指针,A可变,A指向的值不能被修改
- int const *A; //const修饰指向的对象,A可变,A指向的对象不可变
- int *const A; //const修饰指针A, A不可变,A指向的对象可变
- const int *const A;//指针A和A指向的对象都不可变
技巧
先看“*”的位置 如果const 在 *的左侧 表示值不能修改,但是指向可以改。 如果const 在 *的右侧 表示指向不能改,但是值可以改 如果在“*”的两侧都有const 标识指向和值都不能改。 总结: (1) 如果const修饰在*p前,则不能改的是*p,p的值可以修改。 (2) 如果const是直接写在p前面,则p不能改,*p的值可以修改。