C/C++教程

C++ 基础(十)程序文件和预处理指令

本文主要是介绍C++ 基础(十)程序文件和预处理指令,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. 理解转换单元

  • 头文件包含函数声明和类型定义,源文件创建额外的函数定义。
  • 源文件所包含的头文件的内容称为一个转换单元。
  • 编译器独立处理程序中每个转换单元来生成对象文件。
  • 单一定义原则:
    • 单一定义原则ODR是一个重要概念,程序中定义的每个实体类型对应一个规则。
    • ODR思想:有助于更好理解如何多个文件中组织程序的代码。
      • 转换单元中,变量,函数,类类型,枚举类型,模板都只能定义一次。不同代码块中,定义的变量可以由相同的名称。
      • 整个程序内部,大部分函数和变量必须定义一次,且只能被定义一次。内联函数和变量定义需要在调用它们的每个转换单元出现一次,但它们的定义必须相同。
      • 类和枚举类型通常出现在多个转换单元中,几个转换单元允许分别包含定义,定义必须相同。
      • 函数模板,使用之前未见的模板实参实例化的单元中,包含模板定义。
  • 文件和链接
    • 转换单元中的名称在编译/链接过程中处理方式由链接属性确定。
    • 链接属性指定了由一个名称表示的实体可以在程序的什么中使用。
    • 每个名称,要么由链接属性,要么没有链接属性。某个名称由于在声明该名称的作用域外部访问程序时,有链接属性,否则没有链接属性。
  • 确定名称的链接属性
    • 内部链接属性:该名称表示的实体可以在同一个转换单元内任何地方访问。
    • 外部链接属性:除了在定义它的转换单元中访问外,还可以在另一个转换单元访问。
    • 没有链接属性:没有链接属性,该名称只能应用于改名称的作用域中访问。
  • 外部函数:
    • 函数没有在调用它的转换单元中定义,编译器会把这个调用标记为外部的,让链接程序处理。
    • #include 一句指令,也会在转换单元中引入大量的代码。
  • 外部变量:
    • 在源文件中使用外部定义的变量,方法是在外部的cpp文件中定义变量,在使用时extern导入。
      • int power_range {3};  // Range.cpp                   extern int power_range;  // main.cpp
      • extern声明中不能指定初始值,它是名称的声明,不是变量的定义。
      • 函数定义前面也能加上extern,但这种做法是可选的。
    • 具有外部链接属性的const变量
      • const关键字默认情况下具有内部链接属性,这使它不能在其它转换单元中使用。定义中使用extern可以重写这个属性。
      • 最好在头文件中定义常量。
  • 内部名称:
    • 函数默认会有外部链接属性,当一个函数充当源文件中的辅助函数,应该具有内部链接属性而不是外部链接属性。
    • 第一种方式是添加intern修饰符,或者为一个名称(变量名或者函数名)添加static关键字。
    • 最为推荐的方式是使用未命名的名称空间。

 

2. 预处理源代码

  • 所有预处理指令都由#号开头,以便与C++的语句区分开。
  • 比较少见的几个预处理指令:
    • #line          重新定义当前行号,也可以用来修改文件名。
    • #error        输出编译错误,停止编译。
    • #pragma    提供某计算机专用的特性,同时保证与C++整体兼容。

 

3. 定义预处理宏

  • #define  PI  3.1415926 
  • C++建议的做法:
    • inline const double pi {3.1415926};      // 常量
    • extern const double pi {3.1415926};    // 定义为具有外部的链接属性
    • C++不使用#define指令定义标识符,这么做有三个缺点:不支持类型检查,没有考虑作用域,标识符名称不能限定在名称空间中。
  • C++中#define的作用主要是管理头文件。
  • 定义类似于函数的宏:
    • 宏函数看起来很像函数,但不是函数,没有实参,没有返回类型。
    • 宏函数常见的缺陷:需要注意括号的使用;宏函数参数展开可能是函数,数值可能不是期望的值。
    • C++的解决方案是函数模板。
  • 预处理运算符:
    • #   字符串化运算符,将实参转为包含其值的字符串字面量,用双引号包围实参,并添加必要的转义字符。
    • ## 连接运算符,将两个标识符的值连接起来,类似于使用 + 将两个string对象的值连接起来。
  • 取消宏定义:#undef

 

4. 包含头文件

  • <> 和 " " 两种方式。
  • 方式重复包含:
    • #ifndef     #define     #endif
    • #pragma once

 

5. 名称空间

  • 使用名称空间避免命名冲突。
  • 没有定义名称空间的代码都在全局名称空间中,在多个源文件中,具有链接属性的名称都在全局名称空间中。
  • 定义名称空间:namespace myRegin {}
  • 名称空间不能包含 main()
  • 同名的名称空间会扩展名称空间的作用域。
  • 名称空间若放在头文件中,#include保护符保证了这些定义不会在同一转换单元出现一次以上。但是不会阻止被包含到多个独立的转换单元,需要把成员都声明内联。这样就允许出现在多个转换单元中,不违反ODR。没有#include保护符时,也能使用extern代替inline。
  • 应用using声明:using ns_name::identifier;
  • 函数和名称空间:把函数原型放到名称空间中,定义时需要使用名称空间进行限定。
  • 未命名的名称空间:
    • 只使用了namespace关键字,没用命名。编译器为其生成内部的名称,一个转换单元内只能有一个未命名空间,其他命名空间都是第一个的扩展。
    • 未命名空间不在全局名称空间,有内部链接属性,无外部链接属性。实际上是确保名称不在转换单元外部访问的一种方式,声明为static有相同的效果。
  • 嵌套的名称空间:
    • 使用时需要增加多个名称空间的名字进行限定。
  • 名称空间的别名:namespace AA = ABC::A::a;

 

6. 逻辑预处理指令

  • #ifdef     #endif
  • #if  (不包含强制类型转换的整数常量表达式)    #endif
  • 代码选择:
    • #if  #else  #endif
    • #if  #elif  #else #endif
  • 标准预处理宏:
    • __LINE__    __FILE__  __DATE__  __TIME__  __cplusplus__
  • 检查是否包含头文件:
    • C++17中引入了__has_include()宏
    • #if __has_include(<header>)   #elif __has_include("Someheader.h")   #else  #endif

 

7. 调试方法

  • 集成调试器
    • C++编译器提供了内置的调试工具。
    • 功能一般有:跟踪程序流,设置断点,设置观察窗口,检查程序元素。
  • 预处理指令
    • 使用#ifdef SWITCH  #endif   设置代码开关。
  • 随机数
    • 先调用srand()传递一个无符号的整数种子值,调用rand()得到随机数序列。
    • ctime中的time()返回秒数,作为随机数的一个参数。
  • 使用assert()宏:(动态断言,运行时起作用)
    • assert(expression);表达式是false,程序终止,调用std::abort(),显示标准错误流,cerr中的信息。
    • 关闭assert()宏:程序开头定义#define  NDEBUG   需要放在包含assert头文件中的#include之前才会有效。

 

8. 静态断言

  • static_assert(constant_expression);
  • static_assert(constant_expression, error_message) 
    • 参数一:编译期间可以转为bool的语句,false时,编译失败,输出参数二的message。
    • 可以做参数一的语句:字面量,字面量初始化的const变量,宏,sizeof运算符,模板实参等。
  • 常见用途是模板定义中验证模板参数:
    • static_assert(std::is_arithmetic_v<T>);//判断T是否为算术类型。
这篇关于C++ 基础(十)程序文件和预处理指令的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!