12. 函数

函数声明

函数声明是告诉编译器函数的存在及其特性的过程。它包括函数的名称、返回值类型(如果有的话)以及参数的数量和类型。函数声明使得在程序的其他部分可以调用该函数,即使函数的定义在声明之后。

在函数声明中包含参数名称有助于理解函数的含义,但这些名称在声明中对编译器来说是可选的,除非声明同时也是函数的定义。

当函数声明的返回类型为 void ​时,表示该函数不返回任何值。

对于类成员函数,类名也是函数类型的一部分。例如:

char& String::operator[](int); // 类型:char& String::(int)

函数声明的组成要件

函数声明不仅包括函数的名称、参数列表和返回类型,还可以包含多种限定符和修饰符。以下是函数声明中可以包含的元素:

基本要素

  • 函数名称:必须提供,用于标识函数。
  • 参数列表:必须提供,可以为空,定义了函数调用时所需的参数。
  • 返回类型:必须提供,可以是 void​,也可以是前置或后置形式(使用 auto​)。

修饰符和限定符

  • inline​:表示一种愿望,通过内联函数体实现函数调用,以减少函数调用的开销。

    inline int add(int a, int b) { return a + b; }
    
  • constexpr​:表示当给定常量表达式作为实参时,应该可以在编译时对函数求值。

    constexpr int multiply(int a, int b) { return a * b; }
    
  • noexcept​:表示该函数不允许抛出异常。

    int safeDivide(int a, int b) noexcept { return a / b; }
    
  • 链接说明:例如 static​,用于控制函数的链接方式(在当前的翻译单元中有效)。

    static int staticFunction() { return 42; }
    
  • [[noreturn]]​:表示该函数不会用常规的调用/返回机制返回结果。

    #include <iostream>
    #include <stdexcept>
    
    // 定义一个不返回值的函数,抛出异常
    [[noreturn]] void terminate() {
        throw std::runtime_error("Terminating");
    }
    
    int main() {
        try {
            std::cout << "Calling terminate() function..." << std::endl;
            terminate(); // 调用终止函数,会抛出异常
        }
        catch (const std::runtime_error& e) {
            std::cerr << "Caught exception: " << e.what() << std::endl;
        }
    
        std::cout << "Program continues after handling the exception." << std::endl;
    
        return 0;
    }
    

成员函数限定符

对于类成员函数,还可以有以下限定符:

  • virtual​:表示该函数可以被派生类重写。

    class Base {
        virtual void show() { std::cout << "Base"; }
    };
    
  • override​:表示该函数必须重写基类中的一个虚函数。

    class Derived : public Base {
        void show() override { std::cout << "Derived"; }
    };
    
  • final​:表示该函数不能被派生类重写。

    class FinalClass {
        virtual void show() final { std::cout << "FinalClass"; }
    };
    
  • static​:表示该函数不与某一特定的对象关联。

    class StaticExample {
        static void staticMethod() { std::cout << "Static Method"; }
    };
    
  • const​:表示该函数不能修改其对象的内容。

    class ConstExample {
        void constMethod() const { std::cout << "Const Method"; }
    };
    

以下是一个包含多个修饰符和限定符的复杂函数声明示例:

struct S {
    [[noreturn]] virtual inline auto f(const unsigned long int* const) -> void const noexcept;
};

这个函数声明了一个不会返回的虚函数,它内联、不修改对象内容、不接受异常,并且返回类型为 void​。

函数定义

如果一个函数被调用,它必须在某处被定义,且只能定义一次。函数定义是特殊的函数声明,它提供了函数体的具体实现。

函数定义的基本规则

  • 函数声明与定义的一致性:函数的定义和所有声明必须对应同一类型。

  • 忽略顶层 const:为了与 C 语言兼容,C++ 会自动忽略参数类型的顶层 const​。

    void f(int); // 类型是void(int)
    void f(const int); // 实际上也是void(int)
    
    
    void f(int x) { /* 允许在此处修改x */ }
    // 或者
    void f(const int x) { /* 不允许在此处修改x */ }
    

    不论哪种情况,函数内部对参数的修改都不会影响调用者的实参。

  • 参数名称:函数的参数名称不属于函数类型的一部分,不同的声明中参数名称可以不同。

    int& max(int& a, int& b, int& c); // 返回一个引用,对应a、b、c中较大的一个
    int& max(int& x1, int& x2, int& x3) {
        return (x1 > x2) ? ((x1 > x3) ? x1 : x3) : ((x2 > x3) ? x2 : x3);
    }
    
  • 未使用的参数:在函数定义中未使用的参数可以不命名,这有助于简化代码并提升代码的可扩展性(预留参数,避免未来接口发生变化)。

    void search(table* t, const char* key, const char*) {
        // 未用到第3个参数
    }
    

返回值

除了构造函数和类型转换函数外,每个函数声明都包含函数的返回类型。函数的返回类型可以位于函数名称之前(前置返回类型),也可以位于参数列表之后(后置返回类型)。

  • 前置返回类型:

    string to_string(int a);
    
  • 后置返回类型(使用 auto ​关键字表示函数返回类型放在参数列表之后,类型由 -> ​引导):

    auto to_string(int a) -> string;
    

    后置返回类型在函数模板中特别有用,因为返回类型可能依赖于模板参数。

    template<class T, class U>
    auto product(const std::vector<T>& x, const std::vector<U>& y) -> decltype(x*y);
    

返回值的规则

  • 如果函数不返回任何值,其返回类型是 void​。
  • void ​函数必须返回一个值(main ​函数是个例外)。
  • void ​函数不允许返回值。

如果函数不会通过 return ​或跳转到函数末尾来返回(例如:抛出未被捕获的异常、在 noexcept ​函数中抛出异常导致程序终止,或者直接或间接请求无返回值的系统函数(如 exit()​)),可以将其标记为 [[noreturn]]​。

inline 函数

inline ​函数是一种由编译器在编译时期尝试展开的函数,旨在减少函数调用的开销,提高程序的执行效率。例如:

inline int fac(int n) {
    return (n < 2) ? 1 : n * fac(n - 1);
}

在这个例子中,fac ​函数是一个递归函数,用于计算阶乘。inline ​关键字请求编译器在每次调用 fac ​时直接插入函数体的代码,而不是生成函数调用的代码。

内联函数可能面临多种复杂情况,包括递归调用、单个内联递归函数以及与输入无关的函数。编译器的智能程度不同,可能导致不同的内联结果。有些编译器可能会直接计算出 fac(6) ​的值为 720,而其他编译器可能生成 6 * fac(5) ​的代码,或者完全不进行内联。

如果希望在编译时求值,最好将函数声明为 constexpr​,并确保所有相关函数也都是 constexpr​。这样可以更可靠地在编译时计算表达式的值。

为了确保在不同编译环境下都能成功内联,内联函数的定义(而不仅仅是声明)应该位于作用域内。inline ​限定符不会影响函数的语义,内联函数仍然拥有一个唯一的地址,包括其中的 static ​变量。

如果内联函数的定义出现在多个编译单元中(通常是因为定义在头文件中),则这些定义必须保持一致。这意味着在不同的编译单元中,函数的定义必须完全相同。

constexpr ​函数

通过将函数指定为 constexpr​,我们告诉编译器,如果给定了常量表达式作为实参,则希望该函数能被用在常量表达式中。

constexpr 函数的定义和使用:

constexpr int fac(int n) {
    return (n > 1) ? n * fac(n - 1) : 1;
}
constexpr int f9 = fac(9); // 必须在编译时求值

在这个例子中,fac ​函数是一个递归函数,用于计算阶乘。由于它被声明为 constexpr​,其结果可以在编译时计算。

constexpr 出现在对象定义中时,它意味着“在编译时对初始化器求值”:

void f(int n) {
    int f5 = fac(5);
    int fn = fac(n);
    constexpr int f6 = fac(6);
    constexpr int fnn = fac(n); // 错误:无法确保在编译时求值(n是变量)
    char a[fac(4)]; // OK:数组的尺寸必须是常量,而fac()恰好是constexpr
    char a2[fac(n)]; // 错误:数组的尺寸必须是常量,而n是一个变量
}

具体关于 constexpr​ 使用的限制,参考 cppreference 的 constexpr specifier

constexpr ​函数允许递归和条件表达式,这意味着如果确实希望某个函数是 constexpr​,就一定能做到。但这也意味着必须严格遵循 constexpr ​函数的使用习惯,将其用于相对简单的任务(否则,编译时间会变得很长)。

和内联函数一样,constexpr ​函数也遵循 ODR(一次定义法则)。因此,在多个编译单元中的定义必须保持一致。

constexpr ​函数必须不产生任何副作用,这意味着不能向非局部对象写入内容。然而,只要不写入非局部对象,constexpr ​函数就可以被使用。

constexpr ​函数可以接受引用实参,但不能通过这些引用写入内容。尽管如此,const ​引用参数仍然有用。

[[noreturn]] ​函数属性

在 C++ 中,属性是一种可以置于语法实体前的修饰符,用于描述该实体的性质。这些性质通常依赖于具体的实现。C++ 标准中包含两个标准属性:[[noreturn]] ​和 [[carries_dependency]]​。

[[noreturn]] ​属性用于声明一个函数不会返回任何结果。将 [[noreturn]] ​放在函数声明的开始位置,表示该函数不会通过常规的返回语句返回。

使用 [[noreturn]] ​属性的好处:

  • 理解程序:明确指出函数不会返回,有助于开发者理解代码的流程。
  • 代码生成:编译器可以利用这一信息优化代码,因为它知道调用这个函数后不需要处理返回值。

如果一个被声明为 [[noreturn]] ​的函数实际上返回了值,那么这种行为是未定义的。这意味着程序可能会出现不可预测的错误或崩溃。

局部变量

在 C++ 中,定义在函数内部的名字称为局部名字(local name)。这些变量在函数执行到它们的定义处时被初始化。局部变量的生命周期仅限于函数调用的上下文。

  • static ​局部变量

    • 除非声明为 static​,否则每次函数调用都会为非 static ​局部变量创建一份新的拷贝。​
  • static ​局部变量

    • 如果局部变量被声明为 static​,则在函数的所有调用中都将使用唯一的一份静态分配的对象。这个对象在线程第一次到达它的定义处时被初始化。

    • static ​局部变量可以在函数的多次调用间维护一份公共信息,而不需要使用全局变量。这有助于避免全局变量可能带来的问题,如被其他不相关的函数访问或干扰。

    • 通常情况下,static 局部变量的初始化不会导致数据竞争,因为 C++ 实现必须确保局部 static ​变量的初始化能被正确执行,通常使用无锁机制(如 call_once​)。

    • 递归地初始化一个局部 static ​变量将产生未定义的结果。例如:

      int fn(int n) {
          static int n1 = n;
          static int n2 = fn(n - 1) + 1;
          return n;
      }
      

      这个例子中,n1 ​和 n2 ​的初始化依赖于函数的递归调用,这会导致未定义的行为。

参数传递

当程序调用一个函数时(使用后缀 ()​,称为调用运算符 call operator 或者应用运算符 application operator),我们为该函数的形参(formal arguments,即,parameters)申请内存空间并用实参(actual argument)初始化对应的形参参数传递的语义与初始化的语义一致(严格地说是拷贝初始化) 。编译器负责检查实参的类型是否与对应的形参类型吻合,并在必要的时候执行标准类型转换或者用户自定义的类型转换除非形参是引用,其余情况下参数传递都是值传递。

参数传递方式的选择:

  1. 对于小对象,使用值传递。
  2. 对于不需要修改的大对象,使用 const ​引用传递。
  3. 如果需要返回计算结果,最好使用 return ​而非通过参数修改对象。
  4. 使用右值引用实现移动和转发。
  5. 如果找不到合适的对象,传递指针(用 nullptr ​表示“没有对象”)。
  6. 除非万不得已,否则不要使用引用传递。

数组参数传递

当数组作为函数参数传递时,实际上传入的是指向数组首元素的指针。这意味着数组参数的传递不是通过值传递,而是通过指针传递。

void odd(int* p);
void odd(int a[]);
void odd(int buf[1020]);

对于被调函数来说,数组的尺寸是不可见的。为了避免问题,可以采取以下方法:

  • 传递数组大小:额外传递一个参数表示数组的大小。

    void compute(int* vec_ptr, int vec_size); // 一种可行的办法
    
  • 使用容器:传递容器(如 vector​、array ​或 map​)的引用。

  • 数组引用:将参数类型声明为数组的引用。

    元素个数是数组引用类型的一部分,因此数组引用的灵活性不如指针或容器。

    void f(int(&r)[4]);
    void go() {
        int a1[] = { 1, 2, 3, 4 };
        int a2[] = { 1, 2 }; // 错误:元素个数不匹配
        f(a1); // OK
    	f(a2); // 错误:数组大小不匹配
    }
    

列表参数

由花括号 {} ​限定的列表可以作为函数参数传递,这些参数可以匹配以下几种形参:

  • 能用列表中的值初始化的类型(最关键,可覆盖所有情况,下面可以看作对他的解释)
  • std::initializer_list<T> ​类型,其中列表中的值能隐式地转换成 T
  • T ​类型数组的引用,其中列表中的值能隐式地转换成 T
#include <initializer_list>
#include <string>

template<class T>
void f1(std::initializer_list<T>);

struct S {
    int a;
    std::string s;
};

void f2(S);

template<class T, int N>
void f3(T(&&r)[N]); // 注意,这里要是右值引用

void f4(int);

void go() {
    f1({ 1, 2, 3, 4 }); // T是int,std::initializer_list的大小是4
    f2({ 1, "MKS" });    // 调用f2(S{1, "MKS"})
    f3({ 1, 2, 3, 4 });  // T是int,N是4
    f4({ 1 });           // 调用f4(int{1});
}

如果存在二义性,具有 std::initializer_list ​参数的函数会被优先考虑。

#include <initializer_list>
#include <string>

template<class T>
void f(std::initializer_list<T>);

template<class T, int N>
void f(T(&r)[N]);

void f(int);

void g() {
    f({ 1, 2, 3, 4 }); // T是int,std::initializer_list的大小是4
    f({ 1 });          // T是int,std::initializer_list的大小是1
}

上述准则只适用于 std::initializer_list<T> ​参数。对于 std::initializer_list<T>& ​或者其他碰巧也叫 initializer_list ​的类型(在其他作用域中),C++ 并没有制定特殊的规则。

数量未定的参数

有时需要设计接受任意数量和类型的参数的函数。有三种主要方式可以实现这样的接口:

  • 可变模板

    • 允许以类型安全的方式处理任意类型、任意数量的参数。需要编写模板元程序来解释参数列表并执行操作。
  • std::initializer_list

    • 允许以类型安全的方式处理某种类型的、任意数量的参数。这在处理元素类型相同的参数列表时非常有用。
  • 省略号(C 风格的变参函数)

    • 使用省略号 ... ​表示函数可以接受任意数量的参数。这不是类型安全的,并且难以用于复杂的用户自定义类型,但它允许通过 <cstdarg> ​中的宏处理任意类型的参数。

省略号参数的例子:

#include <cstdarg>
#include <iostream>

void error(int severity, ...);

int main() {
    error(0, "An", "error", "occurred", nullptr);
    return 0;
}

void error(int severity, ...) {
    va_list ap;
    va_start(ap, severity);
    for (char* p = va_arg(ap, char*); p != nullptr; p = va_arg(ap, char*)) {
        std::cerr << p << " ";
    }
    va_end(ap);
    std::cerr << "\n";
    if (severity) std::exit(severity);
}
  • 首先,定义 va_list ​并调用 va_start() ​初始化它。宏命令 va_start ​接受 va_list ​的名字和最后一个正式参数的名字作为它的参数。
  • 宏命令 va_arg() ​用于按顺序提取未命名的参数。每次调用它时,程序员必须提供一个类型;va_arg() ​假定该类型的一个实参被传入了函数,但它通常无法确保这一点。
  • 如果在函数中使用了 va_start()​,则在该函数返回前必须先调用 va_end()​。这么做的原因是 va_start() ​可能会修改栈的内容,从而造成函数无法正常返回;但是 va_end() ​可以撤销所有此类修改。

使用 std::initializer_list​ 的例子

#include <iostream>
#include <string>
#include <initializer_list>

void error(int severity, std::initializer_list<std::string> err) {
    for (const auto& s : err) {
        std::cerr << s << " ";
    }
    std::cerr << "\n";
    if (severity) exit(severity);
}

int main(int argc, char* argv[]) {
    error(0, { "An", "error", "occurred" });
    return 0;
}

在这个例子中,error​ 函数接受一个整数和一个字符串列表。调用时必须使用列表记法。

此外,也可以使用 std::vector​ :

#include <iostream>
#include <vector>
#include <string>

void error(int severity, const std::vector<std::string>& err) {
    for (const auto& s : err) {
        std::cerr << s << " ";
    }
    std::cerr << "\n";
    if (severity) exit(severity);
}

std::vector<std::string> arguments(int argc, char* argv[]) {
    std::vector<std::string> res;
    for (int i = 0; i < argc; ++i) {
        res.push_back(argv[i]);
    }
    return res;
}

int main(int argc, char* argv[]) {
    auto args = arguments(argc, argv);
    error(argc < 2 ? 0 : 1, args);
    return 0;
}

处理数量未定的参数时,优先考虑使用可变模板或 std::initializer_list​。当这些机制不适用时,才考虑使用省略号参数。省略号参数虽然灵活,但牺牲了类型安全,并且需要手动处理参数,这增加了代码的复杂性和出错的可能性。

默认参数

函数的默认参数允许我们为函数参数提供预设值,这样在调用函数时可以省略一些参数。这增加了函数的灵活性,并减少了代码的重复性。默认参数在函数声明时进行类型检查,并在函数调用时求值。

注意事项:

  1. 避免使用可变值作为默认参数:因为默认参数的值在函数声明时确定,如果使用可变的值作为默认参数,可能会导致对上下文的微妙依赖,从而引发错误。

  2. 只能为参数列表中靠后的参数提供默认值:

    int f(int, int = 0, char* = nullptr); // 正确
    int g(int = 0, int = 0, char*);       // 错误
    int h(int = 0, int, char* = nullptr); // 错误
    

    注意,*​ 和 =​ 之间的空格必不可少(*=​ 是赋值运算符)

  3. 在同一作用域中,不能有重复或不一致的默认参数。

    void f(int x = 7); // 正确
    void f(int = 7);   // 错误:不允许重复默认参数
    void f(int = 8);   // 错误:默认参数不一致
    

重载函数

函数重载允许我们为执行相同概念任务但在不同类型的对象上操作的函数使用相同的名称。这种技术使得代码更加简洁和直观,尤其是对于那些具有约定俗成名称的函数,如 sqrt​、print ​或 open​。

自动重载解析

当调用一个重载函数时,编译器会根据提供的实参类型来决定使用哪个函数。这个过程称为自动重载解析。解析的顺序如下:

  1. 精确匹配:无需类型转换或仅需简单类型转换即可匹配。
  2. 提升后匹配:执行整数提升和 float ​到 double ​的转换。
  3. 标准类型转换后匹配:如 int ​到 double​,double ​到 long double ​等。
  4. 用户自定义类型转换后匹配:如 double ​到 complex<double>​。
  5. 使用省略号 ... ​进行匹配。

重载与返回类型

在重载解析过程中,函数的返回类型不被考虑。这意味着重载函数的区分仅基于参数列表,而不是返回值。这样做的目的是为了确保运算符或函数调用的解析独立于上下文。

重载与作用域

重载发生在一组重载函数的成员内部,即重载函数应该位于同一个作用域内。不同非名字空间作用域中的函数不会重载。

基类和派生类提供不同的作用域,因此默认情况下基类函数和派生类函数不会发生重载。例如:

#include <iostream>
struct Base {
    void f(int) {
		std::cout << "Base::f(int)" << std::endl;
    }
};

struct Derived : Base {
    void f(double) {
		std::cout << "Derived::f(double)" << std::endl;
    }
};

void g(Derived& d) {
    d.f(1); // 调用 Derived::f(double)
	d.Base::f(1); // 调用 Base::f(int)
}

int main() {
	Derived d;
	g(d);
}

在这个例子中,尽管 f(int) ​在基类中定义,但由于 Derived ​类中存在 f(double)​,且 d ​是 Derived ​类型的引用,因此调用 Derived::f(double)​。

如果我们希望实现跨类作用域或名字空间作用域的重载,可以使用 using ​声明或 using ​指示来引入所需的函数。

#include <iostream>
struct Base {
	void f(int) {
		std::cout << "Base::f(int)" << std::endl;
	}
};

struct Derived : Base {
	void f(double) {
		std::cout << "Derived::f(double)" << std::endl;
	}
	using Base::f; // 使用 using 声明,使得 Base 的 f(int) 对 Derived 可见(名字空间中叫做using 指示,也是类似)
};

void g(Derived& d) {
	d.f(1); // 调用 Base::f(int)
}

int main() {
	Derived d;
	g(d);
}

多参数解析

当一个函数被重载,并且调用时提供了多个实参,重载解析规则将分别应用于每个参数。如果某个函数对一个参数是最佳匹配,并且在其他参数上也是最佳匹配或至少不比其他函数差,则该函数就是最终的最佳匹配函数。如果无法找到这样的函数,则调用将因二义性而被拒绝。

手动重载解析

当一组重载函数的版本过少或过多时,可能会导致二义性。程序员可以通过增加函数版本使用显式类型转换来解决二义性问题。

前置与后置条件

前置条件(precondition)和后置条件(postcondition)是确保函数正确性的重要概念。它们分别定义了函数调用前和返回后必须满足的条件。

  • 前置条件:函数执行前预期的参数条件,这些条件必须被满足,以便函数能正确执行。
  • 后置条件:函数执行后必须满足的条件,通常涉及函数的返回值和可能修改的状态。

处理策略

  1. 确保每个输入都对应一个有效的处理结果:在这种情况下,不需要添加前置条件,因为函数设计时已经考虑了所有可能的输入。
  2. 假定前置条件满足:依赖于函数的调用者不犯错误,这是一种信任调用者的做法。
  3. 检查前置条件是否满足,如果不满足则抛出异常:这是一种常见的做法,可以在检测到错误条件时立即通知调用者。
  4. 检查前置条件是否满足,如果不满足则终止程序:这是一种更极端的处理方式,通常用于严重错误。

函数指针

与(数据)对象类似,由函数体生成的代码也置于某块内存区域中,因此它也有自己的地址。

程序员只能对函数做两种操作:调用它或者获取它的地址。通过获取函数地址得到的指针能被用来调用该函数。

解引用函数指针时可以用 *​,也可以不用;同样,获取函数地址时可以用 &​,也可以不用:

int add(int a, int b) {
	return a + b;
}

int main() {
	int (*func_ptr)(int, int) = &add;  // 这里使用了 &
	int result = (*func_ptr)(1, 2);    // 这里使用了 *
}

或者:

int add(int a, int b) {
	return a + b;
}

int main() {
	int (*func_ptr)(int, int) = add;   // 这里没有使用 &
	int result = func_ptr(1, 2);       // 这里没有使用 *
}

函数指针的参数类型声明与函数本身类似,且在进行指针赋值操作时,要求完整的函数类型必须精确匹配。这意味着函数指针的返回类型和参数类型都必须与被赋值的函数完全一致。

C++ 允许将一个函数指针转换成其他类型的函数指针,但之后必须将结果指针转换回原来的类型,否则可能会出现不可预料的情况。

using P1 = int(*)(int*);
using P2 = void(*)(void);

void f(P1 pf) {
    P2 pf2 = reinterpret_cast<P2>(pf); // 可能发生严重错误
    pf2(); // 调用pf2()时没有提供任何参数
    P1 pf1 = reinterpret_cast<P1>(pf2); // 把pf2“转换回来”
    int x = 7;
    int y = pf1(&x); // OK
}

在 C 语言中,由于缺乏函数对象或 lambda 表达式等机制,函数指针常被用作算法参数化的一种方式。

当声明一个指向 noexcept ​函数的指针时,应该在指针声明中包含 noexcept ​关键字(也可以不包含,但会丢失信息),以确保指针类型反映了函数的异常安全属性。

void f(int) noexcept; // 声明一个不会抛出异常的函数
void g(int);         // 声明一个可能抛出异常的函数

// 错误声明:丢失了noexcept信息
void (*p1)(int) = f;

// 正确声明:保留了noexcept信息
void (*p2)(int) noexcept = f;

// 错误声明:尝试将可能抛出异常的函数赋值给指向noexcept函数的指针
void (*p3)(int) noexcept = g; // 错误:我们并不知道g不会抛出异常

链接说明(如 extern "C"​)和 noexcept ​关键字不能出现在类型别名中。

// 错误:别名中出现了链接说明
using Pc = extern "C" void(int);

// 错误:别名中出现了noexcept
using Pn = void(int) noexcept;

宏在 C 语言中非常重要,但在 C++ 中的作用较小。宏的使用应当谨慎,因为它们会在编译器处理之前重排程序文本,导致调试器、交叉引用和性能评估工具难以有效工作。如果必须使用宏,应遵循大写字母命名约定,并仔细阅读 C++ 预处理程序的参考手册。

宏的定义和使用

宏可以通过 #define ​指令定义,可以无参数或有参数。有参数的宏在展开时会替换参数占位符。

无参数宏的例子:

#define NAME rest of line
named = NAME; // 展开成 named = rest of line;

有参数宏的例子:

#define MAC(x, y) argument1: x argument2: y
expanded = MAC(foo bar, yuk yuk); // 展开成 expanded = argument1: foo bar argument2: yuk yuk;

宏不允许重载,也不能处理递归调用。它们与 C++ 的类型系统和作用域规则无关,只操作字符串。

宏可以使用 ## ​运算符拼接字符串,也可以包含字符串化的参数。

#define NAME2(a, b) a##b
int NAME2(hack, cah)(); // 展开成 int hackcah();

可以使用 #undef ​指令来取消宏定义。

#undef X

宏也可以是可变参数的,使用省略号 ... ​来表示。

#include <iostream>
#define err_print(...) fprintf(stderr, "error: %s%d\n", __VA_ARGS__)
int main() {
	err_print("The answer", 54); // 输出: error: The answer 54
}

其中,省略号(…)的意思是 _VA_ARGS_ ​把实际传入的参数当成一个字符串。

条件编译

宏的一个不可替代的用途是条件编译。条件编译允许根据是否定义了特定的标识符(IDENTIFIER)来包含或排除代码段。

#ifdef arg_two
int f(int a, int b);
#else
int f(int a);
#endif

#ifndef arg_two
int g(int a);
#else
int g(int a, int b);
#endif

选择用于条件编译的宏名称时必须谨慎,确保它们不会与现有的标识符冲突。

预定义宏

编译器预定义了一系列宏,这些宏提供了关于编译环境和代码位置的信息。这些预定义宏可以在程序中使用,以获取编译时的特定信息。

以下是一些常见的预定义宏:

  • __cplusplus​:在 C++ 编译器中定义,C 语言编译器中没有。C++11 程序中值为 201103L​,其他标准中值较小。
  • __DATE__​:编译日期,格式为“yyyy:mm:dd”。
  • __TIME__​:编译时间,格式为“hh:mm:ss”。
  • __FILE__​:当前源文件的名字。
  • __LINE__​:当前源文件的代码行数。
  • __FUNC__​:当前函数的名字,是一个 C 风格字符串。
  • __STDC_HOSTED__​:如果实现是宿主式的,则为 1;否则为 0。

以下是一些根据具体条件定义的宏:

  • __STDC__​:在 C 语言编译器中有定义,C++ 编译器中没有。
  • __STDC_MB_MIGHT_NEQ_WC__​:如果 wchar_t 的编码体系中基本字符集的成员的值与它作为普通字符字面值常量的值可能不同,则为 1。
  • __STDCPP_STRICT_POINTER_SAFETY__​:如果实现有严格的指针安全机制,则为 1;否则是未定义的。
  • __STDCPP_THREADS__​:如果程序可以有多个执行线程,则为 1;否则是未定义的。

例子:

#include <iostream>

int main() {
    std::cout << "__FUNC__(): in file " << __FILE__ << " on line " << __LINE__ << " at " << __TIME__ << std::endl;
    return 0;
}

编译指令

在 C++ 中,#pragma​ 是一种预处理指示,用于提供编译器特定的指令,这些指令通常不是标准 C++ 的一部分#pragma​ 指令允许程序员指示编译器执行某些非标准的、特定于实现的操作。

#pragma​ 的例子:

#pragma once  // 确保头文件只被包含一次

这个指令告诉编译器,该文件是一个头文件,并且应该只被包含一次,类似于包含守卫,但这是编译器特定的。

如果可能,尽量避免使用 #pragma​。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇