C++宏与动态内存学习笔记

引言:宏是什么?

宏(Macro)是C/C++预处理器提供的一种文本替换机制。在程序编译之前,预处理器会根据宏定义将代码中的宏名称替换为对应的文本。宏在某些场景下非常有用,但也需要谨慎使用,因为它只是简单的文本替换,可能导致一些意想不到的问题。

1. 宏中的特殊符号

宏定义中可以使用一些特殊的符号,它们赋予了宏更强大的功能。

1.1 字符串化操作符(#

  • 作用:将宏参数转换为一个字符串字面量。
  • 用法:在宏定义中,将#放在参数名前面。
  • 注意事项
    • 只能用于带参数的宏定义中,且必须置于宏定义体中的参数名前。
    • #操作符会忽略参数前后的空格。
    • 如果参数中间有多个连续的空格,它们会被压缩成一个单一的空格。

示例与辨析:exp("hello") vs exp1(hello)

假设有以下宏定义:

1
2
#define exp(s) printf("test s is:%s\n",s)
#define exp1(s) printf("test s is:%s\n",#s)
  • exp("hello");

    • 这里,我们直接传入了一个字符串字面量"hello"。宏展开后是 printf("test s is:%s\n","hello");
    • 输出test s is:hello
  • exp1(hello);

    • 这里,我们传入的是一个标识符hello。由于exp1宏中使用了#s,预处理器会将hello这个标识符字符串化"hello"。宏展开后是 printf("test s is:%s\n","hello");
    • 输出test s is:hello

关键点exp宏期望你传入一个已经用双引号括起来的字符串。如果你传入exp(hello);(没有双引号),hello会被视为一个未定义的变量名,导致编译错误。而exp1宏则利用#操作符,无论你传入的是什么标识符,它都会帮你转换成字符串。

1.2 符号连接操作符(##

  • 作用:将宏定义的多个形参或形参与固定文本连接起来,形成一个新的标识符。
  • 用法:将##放在需要连接的部分之间。
  • 注意事项
    • ##前后的空格可有可无,不影响连接结果。
    • 连接后的标识符必须是实际存在的变量名、函数名或编译器已知的宏定义。
    • 如果##后的参数本身也是一个宏,##会阻止这个宏的展开(即先连接,再考虑展开)。

示例与辨析:#define vs const char*

假设有以下代码:

1
2
3
4
5
6
7
#define expA(s) printf("前缀加上后的字符串为:%s\n",gc_##s)
#define gc_hello1 "I am gc_hello1" // 宏定义
int main() {
const char * gc_hello = "I am gc_hello"; // 变量定义
expA(hello);
expA(hello1); // 注意这里传入的是hello1,而不是"hello1"
}
  • expA(hello);

    • 宏展开为 printf("前缀加上后的字符串为:%s\n",gc_hello);
    • gc_hello是一个const char*类型的变量,指向字符串"I am gc_hello"
    • 输出前缀加上后的字符串为:I am gc_hello
  • expA(hello1);

    • 宏展开为 printf("前缀加上后的字符串为:%s\n",gc_hello1);
    • gc_hello1是一个宏,它在预处理阶段会被替换为字符串字面量"I am gc_hello1"
    • 输出前缀加上后的字符串为:I am gc_hello1

关键点

  • #define 定义的是一个,它在预处理阶段进行简单的文本替换,不占用运行时内存,也没有类型信息。
  • const char * 定义的是一个变量,它在编译阶段被处理,并在运行时占用内存(指针本身占用内存,字符串字面量也存储在程序的只读数据段)。它们是两种完全不同的概念。

1.3 续行操作符(\

  • 作用:当宏定义不能在一行内写完时,用于指示宏定义在下一行继续。
  • 用法:在需要换行的位置,紧接着行尾放置一个反斜杠\
  • 注意事项\后面不能有任何字符,包括空格,否则会导致编译错误。\前面可以有空格。

示例

1
2
#define MAX(a,b) ((a)>(b) ? (a) \
:(b))

这个宏会正确展开,计算两个数的最大值。

2. do{...}while(0) 在宏定义中的妙用

do{...}while(0) 结构在C/C++宏定义中非常常见,它能解决宏的一些潜在问题,使宏更健壮。

2.1 避免语义曲解(if-else 陷阱和分号问题)

  • 问题:如果宏包含多条语句,且没有用{}包裹,在if-else等控制流中使用时可能导致逻辑错误。即使使用{}包裹,宏调用后多余的分号也可能导致语法问题(虽然某些情况下编译器会忽略)。
  • 解决方案do{...}while(0) 将多条语句封装成一个单一的语句块,确保宏在任何上下文中的行为都符合预期,并且可以安全地在宏调用后加上分号。

示例

1
2
3
4
5
6
7
8
9
// 错误示例:
#define fun() f1();f2();
if(a>0)
fun(); // 展开为 if(a>0) f1();f2(); 导致f2()总是执行

// 改进示例:
#define fun_safe() do{f1();f2();}while(0)
if(a>0)
fun_safe(); // 展开为 if(a>0) do{f1();f2();}while(0); 行为正确

2.2 替代 goto 进行资源清理

  • 问题:在函数中,为了在返回前进行资源清理(如释放内存),有时会使用goto语句跳转到清理代码块。但这不符合结构化编程原则。
  • 解决方案do{...}while(0) 结合 break 语句可以实现类似 goto 的跳转效果,但保持了代码的结构化和可读性。

示例

1
2
3
4
5
6
7
8
9
10
11
12
int ff() {
int *p = (int *)malloc(sizeof(int));
// ...
do {
if (some_error_condition)
break; // 遇到错误,跳出do-while块,执行后续清理
// dosomething
} while(0);
// 清理工作,例如:
free(p); // 释放内存
return 0;
}

2.3 避免空宏警告

  • 问题:定义一个空的宏(如#define EMPTY_MACRO)在某些编译器下可能会产生警告。
  • 解决方案:使用 do{}while(0) 来定义空宏,可以避免这些警告。
    1
    #define EMPTY_MACRO do{}while(0)

2.4 定义单一的函数块来完成复杂的操作

  • 作用:在函数内部,do{...}while(0) 可以创建一个独立的局部作用域。您可以在其中定义临时变量,而不用担心与函数其他部分的变量名冲突,这有助于组织复杂的逻辑。

示例

1
2
3
4
5
6
7
8
9
10
int fc() {
int k1 = 10;
cout << k1 << endl; // 输出 10
do {
int k1 = 100; // 这是一个新的局部变量 k1,只在do-while块内有效
cout << k1 << endl; // 输出 100
} while(0);
cout << k1 << endl; // 再次输出外部的 k1,值为 10
return 0;
}

3. 动态内存分配与指针基础

在C/C++中,动态内存分配是管理程序运行时内存的关键技术。

3.1 malloc 的基本使用

  • 代码示例int *p = (int *)malloc(sizeof(int));

  • **malloc(sizeof(int))**:

    • sizeof(int):这是一个运算符,用于计算 int 类型在当前系统上占用的字节数(例如,4字节)。
    • malloc():这是一个C标准库函数,用于在堆(Heap)上动态地分配指定大小(以字节为单位)的内存。如果分配成功,它会返回这块内存的起始地址;如果失败,则返回 NULL
    • 所以,这部分代码的作用是:在堆上开辟一块大小等于一个 int 类型变量所需的内存空间。
  • (int *) 强制类型转换

    • malloc 函数返回的是 void* 类型的指针,表示它指向一块通用内存,不带任何类型信息。
    • C++ 中,不允许 void* 隐式转换为其他类型的指针(为了类型安全)。因此,需要使用 (int *) 进行显式类型转换,告诉编译器您希望将这个通用指针视为一个指向 int 类型的指针。
    • 在C语言中,这个强制类型转换通常可以省略,但为了代码的兼容性和明确性,加上它也是一个好习惯。
  • **int *p = ...;**:

    • 这声明了一个名为 p 的指针变量,它的类型是 int*(指向 int 类型的指针)。
    • 它将 malloc 返回的、并经过类型转换后的内存地址赋值给 p。所以,p 存储着这块新分配内存的起始地址。

3.2 指针解引用 (*p = 10;)

  • 代码示例*p = 10;
  • *p:这里的 *解引用操作符。它表示访问指针 p 所指向的内存位置。换句话说,*p 代表的是 p 指向的那个内存位置上存储的“值”。
  • *p = 10; 的含义:将整数值 10 存储到指针 p 所指向的内存地址中。这完成了对动态分配内存的赋值操作。

3.3 动态内存分配的用途(为什么需要它?)

动态内存分配是编程中不可或缺的,主要有以下几个原因:

  • 运行时确定内存大小:程序在编译时无法确定所需内存大小,例如处理用户输入的不确定数量的数据。
  • 处理可变大小的数据结构:构建链表、树、图、动态数组等数据结构时,需要根据需要动态创建和销毁节点。
  • 避免栈溢出:栈空间有限,分配大块内存(如大数组)在堆上可以避免栈溢出问题。
  • 数据生命周期控制:堆上分配的内存直到显式释放才销毁,允许数据在创建它的函数结束后仍然存在。
  • 函数间共享数据:通过传递指针,不同函数可以高效地共享和操作同一块内存数据,避免大量拷贝。

3.4 别忘了 free()

使用 malloc 分配的内存,在不再需要时,必须使用 free() 函数来显式释放。否则会导致内存泄漏(Memory Leak),即程序占用的内存越来越多,最终可能耗尽系统资源。

示例

1
2
3
4
5
6
7
8
9
int *p = (int *)malloc(sizeof(int));
if (p == NULL) { // 检查是否分配成功
// 处理内存分配失败的情况
return 1;
}
*p = 10;
// ... 使用p指向的内存 ...
free(p); // 释放p指向的内存
p = NULL; // 良好的编程习惯:将已释放的指针置为NULL,避免野指针

希望这份学习笔记能帮助您更好地理解C++中的宏和动态内存管理!