江哥带你玩转C语言 | 15- 修饰符和预处理指令

Posted 极客江南

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了江哥带你玩转C语言 | 15- 修饰符和预处理指令相关的知识,希望对你有一定的参考价值。

全局变量和局部变量

  • 变量作用域基本概念
    • 变量作用域:变量的可用范围
    • 按照作用域的不同,变量可以分为:局部变量和全局变量
  • 局部变量
    • 定义在函数内部的变量以及函数的形参, 我们称为局部变量
    • 作用域:从定义的那一行开始, 直到遇到}结束或者遇到return为止
    • 生命周期: 从程序运行到定义哪一行开始分配存储空间到程序离开该变量所在的作用域
    • 存储位置: 局部变量会存储在内存的栈区中
    • 特点:
      • 相同作用域内不可以定义同名变量
      • 不同作用范围可以定义同名变量,内部作用域的变量会覆盖外部作用域的变量
  • 全局变量
    • 定义在函数外面的变量称为全局变量
    • 作用域范围:从定义哪行开始直到文件结尾
    • 生命周期:程序一启动就会分配存储空间,直到程序结束
    • 存储位置:静态存储区
    • 特点: 多个同名的全局变量指向同一块存储空间

auto和register关键字

  • auto关键字(忘记)
    • 只能修饰局部变量, 局部变量如果没有其它修饰符, 默认就是auto的
    • 特点: 随用随开, 用完即销
auto int num; // 等价于 int num;
  • register关键字(忘记)
    • 只能修饰局部变量, 原则上将内存中变量提升到CPU寄存器中存储, 这样访问速度会更快
    • 但是由于CPU寄存器数量相当有限, 通常不同平台和编译器在优化阶段会自动转换为auto
register int num; 

static关键字

  • 对局部变量的作用
    • 延长局部变量的生命周期,从程序启动到程序退出,但是它并没有改变变量的作用域
    • 定义变量的代码在整个程序运行期间仅仅会执行一次
#include <stdio.h>
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 <stdio.h>
#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 <stdio.h>
#include "ds.h"

int num; // 不和A文件中的num共享
int main()
{
    num = 666;
    test(); // test中输出0
    return 0;
}

extern关键字

  • 对局部变量的作用
    • extern不能用于局部变量
    • extern代表声明一个变量, 而不是定义一个变量, 变量只有定义才会开辟存储空间
    • 所以如果是局部变量, 虽然提前声明有某个局部变量, 但是局部变量只有执行到才会分配存储空间
#include <stdio.h>

int main()
{
    extern int num;
    num = 998; // 使用时并没有存储空间可用, 所以声明了也没用
    int num; // 这里才会开辟
    printf("num = %i\\n", num);
    return 0;
}
  • 对全局变量的作用
    • 声明一个全局变量, 代表告诉编译器我在其它地方定义了这个变量, 你可以放心使用
#include <stdio.h>

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 <stdio.h>
    int main(){
        printf("hello lnj\\n");
        return 0;
    }
    
    • 2.执行预处理编译
    • 执行预处理编译后生成的文件
    • 打开预处理编译后生成的文件
      • 处理源文件中预处理相关的指令
      • 处理源文件中多余注释等

    • 3.执行汇编编译
    • 执行汇编编译后生成的文件
    • 打开汇编编译后生成的文件

    • 4.执行二进制编译
    • 执行二进制编译后生成的文件
    • 打开二进制编译后生成的文件

    • 5.执行链接操作
      • 将依赖的一些C语言函数库和我们编译好的二进制合并为一个文件
    • 执行链接操作后生成的文件

    • 6.运行链接后生成的文件

计算机是运算过程分析

  • 1.编写一个简单的加法运算
  • 2.调试编写好的代码, 查看对应的汇编文件
  • 结论:
    • 1.通过地址线找到对应地址的存储单元
    • 2.通过控制线发送内存读取指令
    • 3.通过数据线将内存中的值传输到CPU寄存器中
    • 4.在CPU中完成计算操作
    • 5.通过地址线找到对应地址的存储单元
    • 6.通过控制线发送内存写入指令
    • 7.通过数据线将计算结果传输到内存中

预处理指令

预处理指令的概念

  • C语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释(比如之前使用的#include文件包含指令),产生一个新的源程序(这个过程称为编译预处理),之后再进行通常的编译
  • 为了区分预处理指令和一般的C语句,所有预处理指令都以符号“#”开头,并且结尾不用分号
  • 预处理指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文件尾。习惯上我们尽可能将预处理指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文件
  • C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

宏定义

  • 被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。
  • 宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。在C语言中,“宏”分为有参数和无参数两种。
    ##不带参数的宏定义
  • 格式:#define 标识符 字符串
    • 其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
#include <stdio.h>

  // 源程序中所有的宏名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;
}
  • 注意点:
    1. 宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误
  • 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;
 }
    1. 宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用#undef命令
#define PI 3.14
int main ()
 {
    printf("%f", PI);
    return 0;
}
#undef PI
void test()
{
    printf("%f", PI); // 不能使用
}
    1. 定义一个宏时可以引用已经定义的宏名
#define R  3.0
#define PI 3.14
#define L  2*PI*R
#define S  PI*R*R
    1. 可用宏定义表示数据类型,使书写方便
#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 <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;
 }
  • 3)计算结果最好也用括号括起来
#include <stdio.h>
// 下面定义一个宏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 <stdio.h>
// 计算结果用括号括起来
#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 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;
}

const关键字

  • const是一个类型修饰符
    • 使用const修饰变量则可以让变量的值不能改变
      ##const有什么主要的作用?
  • (1)可以定义const常量,具有不可变性
const int Max=100;
int Array[Max];
  • (2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
 void f(const int i) { .........}
+ 编译器就会知道i是一个常量,不允许修改;
  • (3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。 同宏定义一样,可以做到不变则已,一变都变!如(1)中,如果想修改Max的内容,只需要:const int Max=you want;即可!

  • (4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。 还是上面的例子,如果在 函数体内修改了i,编译器就会报错;

void f(const int i) { i=10;//error! }
  • (5) 可以节省空间,避免不必要的内存分配。
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ...... double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存! const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存 中有若干个拷贝。
  • (6) 􏰀高了效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表 中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

如何使用const?

  • (1)修饰一般常量一般常量是指简单类型的常量。这种常量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后
int const x=2;const int x=2;
  • (当然,我们可以偷梁换柱进行更新: 通过强制类型转换,将地址赋给变量,再作修改即可以改变const常量值。)
    // const对于基本数据类型, 无论写在左边还是右边, 变量中的值不能改变
    const int a = 5;
    // a = 666; // 直接修改会报错
    // 偷梁换柱, 利用指针指向变量
    int *p;
    p = &a;
    // 利用指针间接修改变量中的值
    *p = 10;
    printf("%d\\n", a); 
    printf("%d\\n", *p);
  • (2)修饰常数组(值不能够再改变了)定义或说明一个常数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5};
const int a[5]={1, 2, 3, 4, 5};
const int a[5]={1, 2, 3, 4, 5};
a[1] = 55; // 错误
  • (3)修饰函数的常参数const修饰符也可以修饰函数的传递参数,格式如下:void Fun(const int Var); 告诉编译器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 标识指向和值都不能改。

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是江哥持续更新的动力。

以上是关于江哥带你玩转C语言 | 15- 修饰符和预处理指令的主要内容,如果未能解决你的问题,请参考以下文章

江哥带你玩转C语言 | 17-文件操作

江哥带你玩转C语言 | 17-文件操作

江哥带你玩转C语言 | 08 - C语言函数

江哥带你玩转C语言 01 - C语言开发环境配置

江哥带你玩转C语言 | 11- C语言排序算法

江哥带你玩转C语言 | 10- C语言数组