C++学习笔记——宏与动态内存
C++宏与动态内存学习笔记
引言:宏是什么?
宏(Macro)是C/C++预处理器提供的一种文本替换机制。在程序编译之前,预处理器会根据宏定义将代码中的宏名称替换为对应的文本。宏在某些场景下非常有用,但也需要谨慎使用,因为它只是简单的文本替换,可能导致一些意想不到的问题。
1. 宏中的特殊符号
宏定义中可以使用一些特殊的符号,它们赋予了宏更强大的功能。
1.1 字符串化操作符(#
)
- 作用:将宏参数转换为一个字符串字面量。
- 用法:在宏定义中,将
#
放在参数名前面。 - 注意事项:
- 只能用于带参数的宏定义中,且必须置于宏定义体中的参数名前。
#
操作符会忽略参数前后的空格。- 如果参数中间有多个连续的空格,它们会被压缩成一个单一的空格。
示例与辨析:exp("hello")
vs exp1(hello)
假设有以下宏定义:
1 |
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 |
|
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. do{...}while(0)
在宏定义中的妙用
do{...}while(0)
结构在C/C++宏定义中非常常见,它能解决宏的一些潜在问题,使宏更健壮。
2.1 避免语义曲解(if-else
陷阱和分号问题)
- 问题:如果宏包含多条语句,且没有用
{}
包裹,在if-else
等控制流中使用时可能导致逻辑错误。即使使用{}
包裹,宏调用后多余的分号也可能导致语法问题(虽然某些情况下编译器会忽略)。 - 解决方案:
do{...}while(0)
将多条语句封装成一个单一的语句块,确保宏在任何上下文中的行为都符合预期,并且可以安全地在宏调用后加上分号。
示例:
1 | // 错误示例: |
2.2 替代 goto
进行资源清理
- 问题:在函数中,为了在返回前进行资源清理(如释放内存),有时会使用
goto
语句跳转到清理代码块。但这不符合结构化编程原则。 - 解决方案:
do{...}while(0)
结合break
语句可以实现类似goto
的跳转效果,但保持了代码的结构化和可读性。
示例:
1 | int ff() { |
2.3 避免空宏警告
- 问题:定义一个空的宏(如
#define EMPTY_MACRO
)在某些编译器下可能会产生警告。 - 解决方案:使用
do{}while(0)
来定义空宏,可以避免这些警告。1
2.4 定义单一的函数块来完成复杂的操作
- 作用:在函数内部,
do{...}while(0)
可以创建一个独立的局部作用域。您可以在其中定义临时变量,而不用担心与函数其他部分的变量名冲突,这有助于组织复杂的逻辑。
示例:
1 | int fc() { |
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 | int *p = (int *)malloc(sizeof(int)); |
希望这份学习笔记能帮助您更好地理解C++中的宏和动态内存管理!