宏、可调用对象
可调用对象
如果一个对象可以使用调用运算符(),()里面可以放参数,这个对象就是可调用对象。 注意:可调用对象的概念新手只要记住就可以了,后面会反复用到,这个概念很重要。
预编译
C标准不仅描述c语言,还描述如何执行c域处理器,c标准库有哪些函数以及详述这些函数的工作原理。C域处理器在程序执行之前查看程序,故称之为预处理器。根据程序中的预处理器操作,预处理器把符号缩写替换成其表示的内容。预处理器可以包含程序所需的其他文件。基本上他的工作是把一些文本转换成另外一些文本。
宏
#define 指令用于创建宏,本质上就是给一个符号名称(宏名)赋予一个特定的文本(替换列表)。预处理器会在编译之前将程序中所有出现的宏名替换为它的替换列表。
定义常量
#define MAX_SIZE 1000不过在Cpp中, 这汇总定义常量的模式经常被const常量所替代
const int MAX_SIZE = 1000;#define LIMIT 20
const int LIM = 50;
static int detal[LIMIT]; //有效
static int data2[LIM] //无效
const int LIM2 = 2 * LIMIT; //有效
const int LIM3 = 2 * LIM; //无效在 C 中,非自动存储期数组的大小应该是整型常量表达式,这意味着表示数组的大小必须是整型常量的组合。枚举常量和 sizeof 表达式不包括 const 声明的值。(这也是 C++ 和 C 的区别之一,在 C++ 中可以把 const 值作为常量表达式的一部分)。
函数式宏
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
int num = 5;
int square_num = SQUARE(num + 2); // 注意括号的使用,避免运算优先级问题
int max_val = MAX(10, 20);
// 预处理器会将代码中的 SQUARE(num + 2) 替换为 ((num + 2) * (num + 2))
// MAX(10, 20) 替换为 ((10) > (20) ? (10) : (20))
return 0;
}使用宏函数定义这样的运算表达式, 在cpp中可以被内联函数取代, 而且不容易出错, 更易调试。
inline int mul(int a, int b) {return a * b;}条件编译
#define LARGE_ARRAY#ifdef LARGE_ARRAY
#define MAXSIZE 60
#else
#define MAX_SIZE 20
#endif#ifdef LARGE_ARRAY
#undef MAX_SIZE
#define MAX_SIZE 100
#else
#undef MAX_SIZE
#define MAX_SIZE 15
#endif这里的 #undef 用来取消原来的定义
#if SYS == 1
#include "A.h"
#elif SYS == 2
#include "B.h"
#elif SYS == 3
#include "C.h"
#endif#和##
#的作用
#会将#a替换成以字符串表示的参数a
#define PRINT(a) std::cout<<#a<<" = " << (a)<<"";float a = 3.0;
PRINT(a*2+3);>>a*2+3 = 9##的作用
##是连接符 也就是将2个表达式拼接在一起
例如类Example使用宏MEMBER写了2条语句
#define MEMBER(Type, member) \
public:\
const Type& get##member() const {return m_##member;}\
void set##member(const Type& m) {m_##member = ml}\
private:\
Type m_##member;
class Example {
MEMBER(int, Age)
MEMBER(std::string, Name)
};预处理器将他的宏展开后 他的展开形式就是这样的
class Example {
public:
const int& getAge() const {return m_Age;}
void setAge(const int& m) {m_Age = m;}
private:
int m_Age;
public:
const std::string& geName() const {return m_Name;}
void setAge(const std::string& m) {m_Name = m;}
private:
std::string m_Name;
};对于上面这2个例子来说 除了增加程序的阅读理解和调试难度 并没有太多好处 不过当知道了这些用法后 读到一些奇怪的宏时 就不会被吓到了
预定义的宏
C 预处理器在编译开始时会自动定义一些有用的宏,这些宏可以提供关于当前编译环境和代码位置的信息。这些宏可以帮助我们编写更灵活、更易于调试和更具可移植性的代码。
常见的预定义宏及其用法:
__FILE__: 表示当前源文件的名称,是一个字符串字面量。
#include <stdio.h>
int main() {
printf("当前文件名: %s\n", __FILE__);
return 0;
}用途: 常用于错误消息或日志记录,方便追踪错误发生的文件。
__LINE__: 表示当前代码在源文件中的行号,是一个十进制整数常量。
#include <stdio.h>
int main() {
printf("当前行号: %d\n", __LINE__);
// ...
printf("又一个行号: %d\n", __LINE__);
return 0;
}用途: 同样常用于错误消息或日志记录,方便定位错误发生的具体代码行。
__DATE__: 表示编译发生的日期,是一个字符串字面量,格式为 "Mmm dd YYYY" (例如: "Apr 05 2025")。
#include <stdio.h>
int main() {
printf("编译日期: %s\n", __DATE__);
return 0;
}用途: 可以用于在程序中记录编译版本信息。
__TIME__: 表示编译发生的时间,是一个字符串字面量,格式为 "hh:mm:ss" (例如: "06:34:44")。
#include <stdio.h>
int main() {
printf("编译时间: %s\n", __TIME__);
return 0;
}用途: 类似于 __DATE__,用于记录编译时间。
__STDC__: 如果编译器符合 ISO C 标准,则该宏被定义为整数常量 1。否则,该宏可能未定义或定义为其他值。
#include <stdio.h>
int main() {
#ifdef __STDC__
printf("编译器符合 ISO C 标准。\n");
#else
printf("编译器不完全符合 ISO C 标准。\n");
#endif
return 0;
}用途: 用于判断编译器是否遵循 C 标准。
__STDC_VERSION__: 表示当前编译器支持的 C 标准版本。它是一个长整型常量,格式为 yyyymmL。例如:
- 对于 C99 标准,其值为
199901L。 - 对于 C11 标准,其值为
201112L。 - 对于 C17 标准,其值为
201710L。 - 对于 C23 标准,其值为
202311L。
#include <stdio.h>
int main() {
printf("当前 C 标准版本: %ld\n", __STDC_VERSION__);
#if __STDC_VERSION__ < 199901L
printf("警告:编译器可能不支持 C99 标准。\n");
#elif __STDC_VERSION__ < 201112L
printf("当前使用的是 C99 标准。\n");
#elif __STDC_VERSION__ < 201710L
printf("当前使用的是 C11 标准。\n");
#elif __STDC_VERSION__ < 202311L
printf("当前使用的是 C17 标准。\n");
#else
printf("当前使用的是 C23 或更高版本的标准。\n");
#endif
return 0;
}#if __STDC_VERSION__ != 201112L
#error Not C11
#endif如果编译器支持的 C 标准版本不是 C11 (201112L),预处理器会发出一个错误消息 "Not C11",导致编译失败。这可以用来确保代码在特定的 C 标准下编译。
__func__ (或 __FUNCTION__ 或 __PRETTY_FUNCTION__): 表示当前函数的名字,是一个字符串字面量。不同的编译器可能使用不同的宏名,但 __func__ 是 C99 标准引入的。
#include <stdio.h>
void myFunction() {
printf("当前函数名: %s\n", __func__);
}
int main() {
myFunction();
return 0;
}用途: 常用于日志记录,方便追踪代码执行到哪个函数。
#line 指令
#line 指令用于重置预处理器在编译过程中跟踪的当前行号和(可选地)文件名。这主要影响编译器在发出警告或错误消息时报告的位置,以及 __LINE__ 和 __FILE__ 宏的值。
#line 1000: 将当前行号重置为1000。之后出现的代码行将被视为从第 1000 行开始计数。文件名保持不变。#line 10 "cool.c": 将当前行号重置为10,并将当前文件名重置为"cool.c"。之后出现的代码行将被视为来自文件"cool.c"的第 10 行开始计数。
用途:
- 代码生成: 当您的 C 代码是由另一个程序自动生成时,可以使用
#line指令来保持原始代码的行号信息,方便调试。 - 逻辑分组: 有时,您可能希望将一段代码在错误消息中标记为来自某个特定的逻辑块或虚拟文件。
可变参数的宏参数
在 C 和 C++ 中,预处理器允许定义可以接受可变数量参数的宏,这被称为可变参数宏 (Variadic Macros)。这使得我们可以创建类似于 printf 这样的函数,但又拥有宏的特性。
语法:
定义一个可变参数宏的语法是在宏的参数列表中使用省略号 ...。在宏的替换列表中,可以使用 __VA_ARGS__ 来表示这些可变数量的参数。
例子:
让我们创建一个简单的日志输出宏:
#include <stdio.h>
#define LOG(format, ...) printf("[LOG] " format, __VA_ARGS__)
int main() {
int value = 42;
char message[] = "Important information";
LOG("The value is %d\\n", value);
LOG("The message is: %s, and the value is %d\\n", message, value);
LOG("Just a simple message.\\n");
return 0;
}解释:
#define LOG(format, ...): 定义了一个名为LOG的宏。它接受一个名为format的固定参数,以及一个用...表示的可变参数列表。printf("[LOG] " format, __VA_ARGS__): 在宏的替换列表中,VA_ARGS 会被替换为调用LOG宏时传递给...的所有参数。这些参数会原封不动地传递给printf函数。
运行结果:
[LOG] The value is 42
[LOG] The message is: Important information, and the value is 42
[LOG] Just a simple message.内联函数
普通方法在调用时需要给方法分配栈空间以供方法执行, 压栈等操作会影响成员运行效率, 于是C++提供了内联方法将方法体放到需要调用方法的地方, 用空间换效率。
函数的调用过程包括建立调用传递参数,跳转到函数代码并返回使用宏使代码内联可以避免这样的开销。
创建内联函数的定义有多种方法,标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。
关键字inline必须与方法定义放在一起才能使方法成为内联, 仅仅将inline放在方法声明前不起任何作用。
#include <stdio.h>
inline int square(int num) {
return num * num;
}
int main() {
for (int i = 1; i <= 5; ++i) {
int sq = square(i); // square 函数可能会被内联展开
printf("The square of %d is %d\n", i, sq);
}
return 0;
}内联函数和普通函数的选择:时间和空间的选择
内联函数的目标是生成内联代码,即将函数体直接插入到调用该函数的地方。如果程序中多次调用内联函数,那么在程序的多个位置都会插入该函数的代码。
如果调用普通函数多次,程序中只有一份函数语句的副本,因此在代码大小上可能更节省空间。然而,另一方面,程序的控制必须跳转至函数内部,然后再返回到调用处,这显然比执行内联代码花费更多的时间。
内联函数的优点是避免了函数调用的开销,从而可能提高程序的执行速度,尤其对于小型、频繁调用的函数。
总结:
使用inline关键字(内联函数)就是一种提高效率, 但加大编译后文件大小的方式。
内联函数是一种可以减少函数调用开销的技术。通过将函数体直接嵌入到调用处来提高程序的执行效率。
选择是否使用内联函数需要在代码的执行速度和最终程序的大小之间进行权衡。 现在随着硬件性能的提高, inline关键字用的越来越少了。
inline关键字只是一个建议, 开发者建议编译器将成员方法当做内联方法, 一般适合搞内联的情况编译器都会采纳建议。
可调用对象
可调用对象 (Callable Objects) 是指任何可以像函数一样被"调用"的东西。这意味着你可以使用调用运算符 () 来执行它们,并可能传递一些参数给它们。
总结: C++的可调用对象主要就函数 仿函数 lambda表达式,当然,这三个也可以衍生出很多写法。 最常见的就是函数指针,函数指针的本质就是利用指针调用函数,本质还是函数。
函数指针要细分也可以分为指向类成员函数的指针,指向普通函数的指针。
函数
函数自然可以调用()运算符,是最典型的可调用对象。
void test(int i) {
std::cout<< i << std::endl;
std::cout<< "hello world"<< std::endl;
}
using pf_type = void(*)(int);
void myFunc(pf_typf pf, int i) {
pf(i);
}
int main() {
myFunc(test, 200);
}仿函数
具有operator()函数的类对象(知道有这么个东西就可以了,具体实现过程结构、联合、枚举、类会讲),此时类对象可以当做函数使用,因此称为仿函数。
class Test{
public:
void operator()(int i) {
std::cout<< i << std::endl;
std::cout<< "void operator()(int i)" <<std::endl;
}
};
int main() {
Test t;
t(20); //t明明是个类对象 却可以像函数一样使用
return 0;
}lambda表达式
就是匿名函数,普通的函数在使用前需要找个地方将这个函数定义,于是C++提供了lambda表达式,需要函数时直接在需要的地方写一个lambda表达式,省去了定义函数的过程,增加开发效率。
注意:lambda表达式很重要,现代C++程序中,lambda表达式是大量使用的。
lambda表达式的格式:最少是[] {},完整的格式为[] () ->ret {}。
代码演示: lambda各个组件介绍
- []代表捕获列表:表示lambda表达式可以访问前文的哪些变量。
(1) []表示不捕获任何变量。 (2) [=]:表示按值捕获所有变量。 (3) [&]:表示按照引用捕获所有变量。 (4) [=, &i]:表示变量i用引用传递,除i的所有变量用值传递。 (5) [&, i]:表示变量i用值传递,除i的所有变量用引用传递。 (6) [i]:表示以值传递的形式捕获i (7) [&i]:表示以引用传递的方式捕获i
- ()代表lambda表达式的参数,函数有参数,lambda自然也有。
int main() {
int i = 10;
[i] (int elem) {
std::cout<< i << std::endl; //10
std::cout<< elem <<std::endl; //200
std::cout<< "hello world"<<std::endl; //hello world
}(200);
return 0;
}- ->ret表示指定lambda的返回值,如果不指定,lambda表达式也会推断出一个返回值的。
- {}就是函数体了,和普通函数的函数体功能完全相同。
- lambda最常用的就是作为参数被调用
#include <functional>
using func_type = std::function<void(int)>;
void myFunc(func_type func, int i) {
func(i);
}
int main() {
int i = 10;
myFunc([i](int elem) {
std::cout<< elem <<std::endl;
std::cout<< i <<std::endl;
std::cout<< "lambda" <<std::endl;
},200);
return 0;
}lambda后面会广泛使用,现在只要理解基础就可以了。
函数指针
对于一个函数来说,它是由一段代码组成。这段代码也是存储在内存的代码区中,因此函数也有地址。
指向函数的指针中储存着函数代码的的起始处地址,这种指针就是函数指针。其实函数的名称就代表了函数的地址。
函数指针的声明和定义与普通变量指针的声明略有不同。
他的声明方式是这样的
返回类型 (*函数指针名称)(参数列表);double multiply(double a, double b) {
return a * b;
}
int main() {
double (*ptr)(double, double) = multiply;
double result1 = ptr(4.2, 1.5);
double result2 = (*ptr)(4, 1.5);
// double wrong1 = *ptr(4.2, 1.5);
// double wrong2 = *(ptr(4.2, 1.5))
// 以上是wrong1 和 wrong2是等价的, 相当于调用了ptr所指向的函数并对函数的返回值解引用
std::cout << result1 <<" "<< result2 << std::endl;
}当我们定一个函数指针时, 定义的是这个指针所指向函数的接口形式。因此凡是符合这样接口的函数都可以使用这样的指针来指向
double multiply(double a, double b) {
return a * b;
}
double add(double a, double b) {
return a + b;
}
int main() {
// 第一次获得multiply函数的地址 加了取地址运算符
double (*ptr)(double, double) = multiply;
std::cout << ptr(4, 1.5) << std::endl; //6
// 第二次获得函数add的地址 没有用取地址符
ptr = &add;
std::cout << ptr(4, 1.5) << std::endl; //5.5
// 以上2次使用的方式都是等价的 没有区别
}void ToUpper(char *);
void ToLower(char *);
int round(double);
int main() {
void (*pf) (char *);
pf = ToUpper; //有效 ToUpper 是该类型函数的地址
pf = ToLowwer; //有效 ToLowwer 是该类型函数的地址
// pf = round; //无效 round 与指针类型
// pf = ToLowwer() //无效 ToLower()不是地址的同时 void也不能作为返回值
}以上这些定义方式看上去比较繁琐 所以可以使用 typedef 给函数指针定义一个别名
typedef double (*MyFuncTypePtr)(double, double);
typedef double (MyFuncType)(double, double);
int main() {
MyFuncTypePtr ptr1 = add;
std::cout << ptr1(1, 2) << std::endl;
MyFuncType* ptr2 = add;
std::cout << ptr2(2, 3) << std::endl;
auto funcPtr = add;
std::cout << funcPtr(3, 3) << std::endl;
}值得注意的是, 当我们要定义一个函数指针的数组时, 则无法使用auto
typedef double (*MyFuncTypePtr)(double, double);
int main() {
// auto ptrs[2] = {add, multiply}; // 不合法
MyFuncTypePtr ptrs[2] = {add, multiply};
std::cout << ptrs[0](4.2, 1.5) << std::endl;
}函数指针的应用场景包括用作参数, 或者是回调函数
void log(std::string msg) {
std::cout<<"[Log] "<<msg<<std::endl;
}
void show(void (*fp)(std::string), std::string str) {
(*fp)(str);
fp(str);
}
int main() {
std::string mis = "hello world";
auto pf = log;
show(log, mis);
show(pf, mis);
}以下代码中使用标准库的排序函数对浮点数向量进行排序,模板函数的第三个参数可以是用函数指针或者函数对象。
bool compare(float a, float b) {
return a < b;
}
int main() {
std::vector<float> nums = {0.1, 3, 0.0, -3.4, 7};
sort(nums.begin(), nums.end(), compare);
for(const auto& num : num) std::cout<<num<<" ";
}我们这里将自定义的函数compare的地址传入。这样排序函数就可以使用传入的比较函数的指针来判断两个元素的大小。
函数指针早在c语言中就广泛使用了,不过在cpp中可以有更多替代。
例如我们在前面的课程中讲过,可以通过重载一个类的函数运算符来实现函数对象。使用函数对象来替代函数指针更加灵活方便。
struct Compare{
bool operator()(float a, float b) {
return a < b;
}
};
int main() {
std::vector<float> nums = {0.1, 3, 0.0, -3.4, 7};
sort(nums.begin(), nums.end(), Compare());
for(const auto& num : num) std::cout<<num<<" ";
}类成员函数指针
函数指针不仅可以用于普通函数,还可以用于类的成员函数。类成员函数的指针定义方法与普通函数的指针相似。定义方式如下:
返回类型 (类名::*函数指针名称)(参数列表);class DemoClass {
public:
double add(double a, double b) {return a + b;}
double multiply(double a, double b) {return a * b;}
}
int main() {
double (DemoClass::*ptrMemberFunc)(double, double);
ptrMemberFunc = DemoClass::add;
DemoClass obj;
std::cout<< (obj.*ptrMemberFunc)(0.5, 2.1) <<std::endl;
DemoClass pObj;
ptrMemberFunc = DemoClass::multiply;
std::cout<< (pObj->*ptrMemberFunc)(0.5, 2.1) <<std::endl;
}对于有虚函数的多态类, 使用成员函数指针仍然具有多态性。
class BaseClass {
public:
virtual void print() {std::cout<<"Base Class";}
};
class SubClass : public BaseClass {
public:
void print() override {std::cout<<"Sub Class";}
};
int main() {
void (BaseClass::*pMemFunc)() = BaseClass::print;
BaseClass* pObj = new SubClass();
(pObj->*pMemFunc)(); //输出Sub Class
}std::function
在STL标准库中提供了一些函数包装的模板, 他们可以对函数或者可调用的对象进行包装,方便在其他的函数中调用。
std::function是一个通用的多态函数封装器。他将一个可调用的对象进行封装, 方便在后续的代码中调用。
template<typename _Signature>
class function;但是在标准库中只做了声明, 并没有定义。我们要真正使用的是这个定义。
template<class R, class... Args>
class function<R(Args...)>;这是个类模板的部分特化。只是这个特化模板的定义形式比较特殊。
它的参数是函数类型, 而小括号里面是参数。将其拆开来就是两个参数, 函数返回类型R和函数参数类型
可以看到Args的定义与普通的类型参数略有不同。Args 前面用的是class ...
这说明它是一个可变模板。参数代表模板可以接受任意多个参数。
要实例化这样一个模板并定义一个函数的包装器对象, 可以使用这样的方式
function<R(Args...)> fnname = target;double add(double a, double b) {return a + b;}
int main() {
std::function<double(double, double)> func = add;
std::cout << func(1.1, 2,3) << std::endl;
}我们还可以使用 std::function 封装类的成员函数和成员变量。
struct Linear {
Linear(float k, float b):k_(k), b_(b) {}
float f(float x) {return k_ * x + b_;}
float k_;
float b_;
};
int main() {
std::function<float(Linear&, float)> memberFunction = &Linear::f;
Linear linear(1.2, 2.3);
std::cout << memberFunction(linear, 5);
std::function<float(Linear&)> k = &Linear::k_;
std::cout << k(linear) << std::endl;
}类型擦除
std::function 实现了一种叫做类型擦除的模式
类型擦除简单的说, 就是可以通过单个通用接口来使用各种具体类型。
float add(float a, float b) {return a + b;}
struct Substract {
float operator()(float a, float b) {return a - b;}
};
int main() {
std::map<char, std::function<double(double, double)>> calculator{
{'+', add},
{'-', Substract()},
{'*', [](double a, double b)->double {a * b;}}
};
std::cout << calculator['+'](12.0, 13) << std::endl;
std::cout << calculator['-'](12.0, 13) << std::endl;
std::cout << calculator['*'](12.0, 13) << std::endl;
}通过 std::function 可以把各种完全不同的类型, 按照同一接口(函数签名)统一封装成一个类型来使用。
至于它所封装的对象是什么, 具体类型则不必再关注了。这就是类型擦除模式带来的好处。
std::mem_fn
对于封装类的成员, 可以直接使用 std::mem_fn() 函数更加方便。
template<class M, class T>
/* unspecified */ mem_fn( M T::* pm ) noexcept;std::mem_fn() 的参数是指向类成员的指针。返回值是一个可调用的包装器。
struct Foo {
float w;
float calculate(float a, float b) {return w * a + w * b;}
Foo& operator+=(float a) {
w += a;
return *this;
}
void print() {
std::cout<<"w = "<<w<<std::endl;
}
};
int main() {
Foo f{1.0};
auto memfn = std::mem_fn(&Foo::calculate);
float res = memfn(f, 2.0, 3.0);
std::cout<<"res = " << res <<std::endl;
auto op_add_assign = std::mem_fn(&Foo::operator+=);
auto f2 = op_add_assign(f, 2.0);
f2.prnit();
}std::bind
std::bind 是个函数模板。它用来生成一个函数调用的转发包装器, 也就是一个函数对象。
template<class F, class... Args>
bind(F&& f, Args&&.. args);调用这个包装器就相当于, 调用它所包装的函数或者对象f并使用 args 作为函数的参数。
int sum(int a, int b, int c) {
std::cout << "a = "<<a<<", b = "<<b<<", c = "<<c<<std::endl;
return a + b + c;
}
int main() {
auto f = std::bind(sum, 1, 2, 3);
std::cout << f() << std::endl;
}std::placeholders
可以使用std::placeholders中的占位符, 也就是_1,_2来代替被绑定的实际参数。
这些参数可以在调用函数对象时, 再传入实际的参数
using namespace std::placeholders;
int sum(int a, int b, int c) {
std::cout << "a = "<<a<<", b = "<<b<<", c = "<<c<<std::endl;
return a + b + c;
}
int main() {
auto f = std::bind(sum, 1, _1, 3);
std::cout << f(5) << std::endl; // 9
//a = 1, b = 5, c = 3
}当需要将变量绑定作为参数时, 需要注意一下
using namespace std::placeholders;
int sum(int a, int b, int c) {
std::cout << "a = "<<a<<", b = "<<b<<", c = "<<c<<std::endl;
return a + b + c;
}
int main() {
int n = 6;
auto f = std::bind(sum, 1, 2, n);
n = 11;
std::cout << f(5) << std::endl; // 9
//a = 1, b = 2, c = 6
}可以看到, 第三个参数仍然是n的初始值6, 没有随着n的变化而变化, 这是因为n是作为值传入的。
std::cref
如果想要将变量n作为引用传入, 则可以使用std::cref()函数。它用于返回一个变量的引用封装器。
using namespace std::placeholders;
int sum(int a, int b, int c) {
std::cout << "a = "<<a<<", b = "<<b<<", c = "<<c<<std::endl;
return a + b + c;
}
int main() {
int n = 6;
auto f = std::bind(sum, 1, 2, std::cref(n));
n = 11;
std::cout << f(5) << std::endl; // 14
//a = 1, b = 2, c = 11
}