define – undef
#define
和 #undef
是 C++ 中的预处理指令,用于定义和取消定义宏。
- #define:
#define
用于定义宏,宏是一种简单的文本替换机制,在预处理阶段将宏名称替换为对应的文本内容。- 宏的一般格式为
#define 宏名称 替换文本
#define 宏名称(参数列表) 宏函数体
#define 宏名称
- #undef:
#undef
用于取消定义已经定义过的宏,使得该宏在后续代码中无效。- 格式为
#undef 宏名称
。
使用#define
定义的,会影响之后的所有代码,直到使用#undef
取消定义,同时使用#include
包含具有#define
定义的文件,也可以在其他文件中生效,并且在某个文件中使用#undef
取消定义不会影响其他文件。
如下例子是可以正常编译运行,但是如果将fun1和fun2的位置调换,那么会导致fun1无法使用PI。
#include <iostream>
using namespace std;
#define PI 3.1415926 // 定义全局宏
void fun1() { cout << PI << endl; }
void fun2() {
#undef PI
cout << "取消定义 PI" << endl;
}
int main() {
fun2();
fun1();
return 0;
}
预处理阶段对 #define
和 #undef
的处理是基于文本替换的,它只会简单地将宏名称替换为对应的文本内容,而不考虑语法的关系。
ifndef – endef
#ifndef
是 C++ 中的一个预处理指令,用于条件编译。它的全称是 “if not defined”,意思是如果某个宏未定义,则执行条件编译区块中的代码。
通常情况下,#ifndef
会与 #define
和 #endif
结合使用,这样设计的目的是为了确保头文件在编译时只被包含一次,避免了重复定义和编译错误。
#ifndef
的一般用法是:
#ifndef MACRO_NAME // 如果宏未定义,则执行 ifndef - endif 的代码
#define MACRO_NAME // 定义宏
#endif
具体解释如下:
– #ifndef MACRO_NAME
:检查宏 MACRO_NAME
是否已经定义。如果未定义,则执行 #ifndef
和 #endif
之间的代码,否则忽略之间的代码。
– #define MACRO_NAME
:则定义宏 MACRO_NAME
。
– #endif
:结束条件编译区块。
函数相关概念
函数指针
函数指针是一个变量,它存储了一个函数的地址。这意味着这个指针指向一个函数,可以通过这个指针调用函数。函数指针常用于回调函数和动态链接功能。
- 假设有一个函数原型如下:
int someFunction(int, double);
- 声明函数指针:基于函数的签名,你可以声明一个指向这种类型函数的指针。使用
(*指针名称)
来表示指针,并在其前面写上函数的返回类型,在后面写上参数列表类型。对于上面的函数,一个指向该类型函数的指针的声明如下:int (*functionPointer)(int, double);
在这里,
functionPointer
是一个指向返回int
且接受一个int
和一个double
参数的函数的指针。 -
使用函数指针:你可以将任何匹配该签名的函数地址赋给这个指针,并通过这个指针调用函数。例如:
int myFunction(int a, double b) { return (int)(a + b); } int main() { int (*functionPointer)(int, double); functionPointer = myFunction; // 将myFunction的地址赋给指针 // 方式二 int (*functionPointer)(int, double) = myFunction; int result = functionPointer(5, 2.3); // 通过指针调用函数 return 0; }
在上面的例子中,functionPointer
是一个函数指针,指向了myFunction
。通过functionPointer
,可以像调用普通函数一样调用myFunction
。这种方式在C语言中特别有用,例如在实现回调函数或者需要将函数作为参数传递给其他函数时。
返回值是函数指针的函数
int add(int x, int y) { return x + y; }
int subtract(int x, int y) { return x - y; }
// int (*)(char op) getOperation(int, int) 错误定义方式
// 编译器无法识别两个并行的 (char op) 和 (int, int) 定义方式,因此实际上的做法是下面的方法
int (*getOperation(char op))(int, int) {
if (op == '+') {
return add;
} else if (op == '-') {
return subtract;
}
return NULL;
}
int main() {
int (*operation)(int, int);
operation = getOperation('+');
printf("Addition: %d\n", operation(5, 3)); // 使用返回的函数指针进行加法操作
operation = getOperation('-');
printf("Subtraction: %d\n", operation(5, 3)); // 使用返回的函数指针进行减法操作
return 0;
}
函数指针作参数
void funPtr(int x){
cout << "funPtr : " << x << endl;
}
// 参数为一个返回值为void, 参数类型为int的函数指针
void function(void (*ptr)(int)){
ptr(10);
}
function(funPtr); // 调用
指针函数
函数的返回值是一个普通指针,如下
int *function(int x);
回调函数
回调函数是一种在特定事件或条件发生时由另一个函数自动调用的函数。回调通常通过函数指针实现,使得可以动态地指定哪个函数将被调用。回调二字体现在其中一个函数(回调函数)被传递到另一个函数(被调函数)中作为参数,然后在被调函数执行过程中的适当时机被调用。
具体有如下几点:
- 异步处理:在进行异步操作,如文件读写、网络请求等时,回调函数被用来处理这些操作完成后的结果。这样,程序可以在不阻塞当前线程的情况下继续执行其他任务,直到异步操作完成时自动调用回调函数来处理结果。
- 事件驱动编程:在图形用户界面(GUI)编程或网络编程中,回调函数广泛用于事件处理。当特定事件发生,如用户点击按钮、接收到网络数据等,相应的回调函数会被触发,允许程序对这些事件做出反应。
- 自定义操作:回调允许库或框架的使用者提供自定义逻辑。例如,在排序算法中,可以通过回调函数自定义比较逻辑,或在遍历容器时,通过回调函数处理每个元素。
- 解耦代码:回调函数有助于分离功能模块,使得程序更加模块化。调用者不需要知道回调函数的具体实现,只需要知道调用时机和方法,这有助于降低程序各部分之间的依赖性,增强代码的可维护性和可扩展性。
- 定时器和延迟执行:在需要延迟执行某些操作或定期执行的场景中,回调函数被用来指定当时间到达时应执行的代码。
例1:
#include <stdio.h>
// 创建回调函数别名
typedef void (*Callback)(int);
// 相当于 typedef void(*)(int) Callback; 表示Callback是一个参数为int,返回值为void的函数指针
// 只不过这种方式定义函数指针会出现错误
// 一个简单的回调函数实现
void myCallback(int x) {
printf("Callback called with value: %d\n", x);
}
// 一个接收回调函数作为参数的函数,
void doSomethingWithCallback(Callback callback) {
printf("Doing something in the function.\n");
callback(42); // 调用回调函数
}
// 不使用 typedef 的形参
void doSomethingWithCallback(void (*callback)(int)) {
printf("Doing something in the function.\n");
callback(42); // 调用回调函数
}
int main() {
// 调用函数并传递回调函数
doSomethingWithCallback(myCallback);
return 0;
}
例2:
下面以冒泡排序为例,实现自定义排序
bool compareGreater(int a, int b){ return a < b; }
bool compareLess(int a, int b) {return a > b; }
void bubbleSort(int *nums, int len, bool (*compare)(int, int)) {
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j<len - i - 1; j++) {
if (compare(nums[j], nums[j+1]))
swap(nums[j], nums[j+1]);
}
}
}
int main(){
int nums[10] = {1,54,6,4,8,6,12,47,8,21};
bubbleSort(nums, 10, compareGreater); // 通过回调函数实现自定义排序
return 0;
}
lambda表达式
lambda是C++ 11中新添的内容,实现原理为使用匿名类,lambda表达式也称为匿名函数对象,lambda表达式有如下优点:
- 声明式编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或者函数对象。以更直接的方式去写程序,好的可读性和可维护性。
- 简洁:不需要额外再写一个函数或者函数对象,避免了代码膨胀和功能分散,让开发者更加集中精力在手边的问题,同时也获取了更高的生产率。
- 在需要的时间和地点实现功能闭包,使程序更灵活。
C++中Lambda表达式(匿名函数)与传统的函数指针有什么本质区别? – 知乎 (zhihu.com)
先看下lambda的基本语法,如下:
[capture](parameters) specifiers exception attr -> return type { /*code; */ }
在上面定义中:
[capture]
代表捕获列表,括号内为外部变量的传递方式,包括值传递、引用传递等(parameters)
代表参数列表,其中括号内为形参,和普通函数的形参一样specifiers exception attr
代表附加说明符,一般为mutable
、noexcept
等->return type
代表lambda函数的返回类型如->int
、-> string
等。在大多数情况下不需要,因为编译器可以推导类型{}
内为函数主体,和普通函数一样
const auto l1 = []() { return 1; }; // 没有捕获任何内容
const auto l2 = [=]() { return x; }; // 按值捕获所有变量
const auto l3 = [&]() { return y; }; // 按引用捕获所有变量
const auto l4 = [x]() { return x; }; // 仅对x进行按值捕获
const auto l5 = [&y]() { return y; }; // 仅对y进行按引用捕获
const auto l6 = [x, &y]() { return x * y; }; // 对x按值捕获,对y按引用捕获
const auto l7 = [=, &x]() { return x + y; }; // 对x按引用捕获,其余的按值捕获
const auto l8 = [&, y]() { return x - y; }; // 对y按值捕获,其余的按引用捕获
const auto l9 = [this]() { } // 捕获this指针
const auto la = [*this]() { } // 按值捕获*this对象
const
作用
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
- 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量。
const 的指针与引用
- 指针
- 指向常量的指针(pointer to const)
- 自身是常量的指针(常量指针,const pointer)
- 引用
- 指向常量的引用(reference to const)
- 没有 const reference,因为引用只是对象的别名,引用不是对象,不能用 const 修饰
(为了方便记忆可以想成)被 const 修饰(在 const 后面)的值不可改变,如下文使用例子中的
p2
、p3
使用
// 类
class A
{
private:
const int a; // 常对象成员,可以使用初始化列表或者类内初始化
public:
// 构造函数
A() : a(0) { };
A(int x) : a(x) { }; // 初始化列表
// const可用于对重载函数的区分
int getValue(); // 普通成员函数
int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
// 对象
A b; // 普通对象,可以调用全部成员函数
const A a; // 常对象,只能调用常成员函数
const A *p = &a; // 指针变量,指向常对象
const A &q = a; // 指向常对象的引用
// 指针
char greeting[] = "Hello";
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
char* const p3 = greeting; // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
const char* const p4 = greeting; // 自身是常量的指针,指向字符数组常量
}
// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常量
void function4(const int& Var); // 引用参数在函数内为常量
// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();
扩展:
const int * ptr
等价于int const * ptr
,都是一个int指针,且不允许通过指针修改其值
宏定义 #define 和 const 常量
宏定义 #define | const 常量 |
---|---|
宏定义,相当于字符替换 | 常量声明 |
预处理器处理 | 编译器处理 |
无类型安全检查 | 有类型安全检查 |
不分配内存 | 要分配内存 |
存储在代码段 | 存储在数据段 |
可通过 #undef 取消 |
不可取消 |
static
作用
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为
static
。 - 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
详细介绍
静态成员变量
- 当我们在类的成员变量声明
static
关键字时,该成员为静态成员变量,静态成员变量在类的所有对象中是共享的,静态成员变量不依赖类的对象,可以直接通过类名访问,存储在全局存储区。 - 静态成员变量需要在类外初始化
typeName className::staticMember = number;
,静态成员变量的内存分配不是在对象创建时进行的,而是在程序启动时分配一次,并在整个程序运行期间存在。因此,它们必须在类外部、通常在全局或命名空间作用域中初始化。如果你试图在函数内部对它进行初始化,编译器会抛出错误。 - 注意:静态成员变量不能像const变量一样在构造函数中的初始化列表中进行初始化
静态成员函数
- 类中的成员函数也可以声明为静态,通过在成员函数前加上
static
关键字,静态成员函数和普通成员函数的区别在于,可以不通过对象调用该函数(使用对象调用静态函数也是可以的) -
静态成员通常情况下,只能访问静态成员变量和静态成员函数,它是类级别的,不依赖于类的对象来执行
-
如果需要使用静态成员函数去修改非静态成员变量的数据,那么需要传递参数,例如传递当前对象的一个引用
class MyClass { public: int nonStaticMember; MyClass(int value) : nonStaticMember(value) {} static void staticFunction(MyClass *instance) { if (instance != nullptr) { cout << instance->nonStaticMember << endl; } } }; int main() { MyClass obj(42); MyClass::staticFunction(&obj); return 0; }
静态局部变量
- 在函数体内部,局部变量加上
static
关键字,可以使该变量在函数调用结束后不被销毁,并保持其值直到下次函数调用(只会初始化一次),这意味着变量的声明周期贯穿整个程序运行。void fun(int n){ if(n >= 4) return; static int x; // 只会初始化一次,默认初始化为0,之后便不会在初始化 cout << "n: " << n << "\tx: " << (x++) << endl; fun(n + 1); } // 假设main中调用fun(0) /* n: 0 x: 0 n: 1 x: 1 n: 2 x: 2 n: 3 x: 3 */
- 这意味着变量的声明周期贯穿整个程序运行,但它的作用域仍然限制在声明他的函数内部。只有在整个程序结束后,
static
变量的内存才会被释放,它由操作系统管理,无法人为的释放。
静态全局变量
- 在全局变量前加上
static
关键字,可以将其作用域限制在声明它的文件内。这意味着这个静态全局变量只能在它被定义的源文件中被访问,其他源文件中不能访问。
控制名称的可见性
- 在函数或变量前使用
static
关键字可以限制它们的链接范围,这意味着这些函数或变量只能在定义他们的文件内部访问(在其他文件中,即使通过extern
也无法访问),对其他文件是不可见的,从而避免了命名冲突。注:即使使用
include
包含具有static
函数的文件,也无法访问这些static
函数或变量。
扩展:关于全局变量
全局变量默认的作用域是整个工程,它具有外部链接性,在另一个文件中可以使用
extern
全局变量的声明,就可以使用当前文件下的全局变量了,而静态变量的作用域是文件,即使使用extern
也无法访问。
this 指针
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给
this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。 - 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
this
指针被隐含地声明为:ClassName *const this
,这意味着不能给this
指针赋值;在ClassName
类的const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得this
的地址(不能&this
)。- 在以下场景中,经常需要显式引用
this
指针:- 为实现对象的链式引用;
- 为避免对同一对象进行赋值操作;
- 在实现一些数据结构时,如
list
。 - 当形式参数与成员变量同名时,可以用
this
指针来区分。 - 确保操作的是对象本身。
链式调用:也称为链式编程或方法链,是一种编程技术,允许在单个语句中顺序调用同一个对象上的多个方法。这种技术通过每个方法返回其所属对象的引用(或指针),来实现连续调用。
链式调用使代码更加紧凑和易读,常用于构建流畅的接口和进行流式编程;但也会导致可读性变差和不易维护。
class Car { public: Car& setColor(const std::string& color) { // 设置颜色 return *this; // 返回当前对象的引用 } Car& setWheels(int wheels) { // 设置轮子数量 return *this; // 返回当前对象的引用 } Car& start() { // 启动汽车 return *this; // 返回当前对象的引用 } }; int main() { Car car; car.setColor("red").setWheels(4).start(); // 链式调用 }
inline 内联函数
特征
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
使用
// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);
// 声明2(不加 inline)
int functionName(int first, int second,...);
// 定义
inline int functionName(int first, int second,...) {/****/};
// 类内定义,隐式内联
class A {
int doA() { return 0; } // 隐式内联
}
// 类外定义,需要显式内联
class A {
int doA();
}
inline int A::doA() { return 0; } // 需要显式内联
编译器对 inline 函数的处理步骤
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
优缺点
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数在运行时可调试,而宏定义不可以。
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
内联函数和普通函数的区别
- 内联函数:编译器会尝试将内联函数的代码直接展开到每个调用点。这意味着函数的机器码会直接插入到每个调用它的地方,而不是通过常规的函数调用机制。这可以减少函数调用的开销,并有可能优化相关的代码。由于函数代码被内联展开,理论上讲,内联函数本身在代码段中可能没有独立的地址或位置,除非也存在对该函数的直接调用。
- 非内联函数(普通函数):函数调用会通过常规的函数调用机制进行,这涉及到在调用栈上设置参数、跳转到函数代码的地址执行代码,然后返回。普通函数的代码在程序的代码段中有固定的位置。
虚函数可以是内联函数吗?
Are “inline virtual” member functions ever actually “inlined”?
- 虚函数(virtual)可以是内联函数(inline),内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
虚函数内联使用
#include <iostream>
using namespace std;
class Base
{
public:
inline virtual void who() {
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() { // 不写inline时隐式内联
cout << "I am Derived\n";
}
};
int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();
// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();
// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
ptr = nullptr;
system("pause");
return 0;
}