文章目录
- C++中的智能指针
- 普通指针的问题
- 智能指针的概念
- 智能指针的使用
- 1. `std::unique_ptr`(独占所有权)
- 2. `std::shared_ptr`(共享所有权)
- 智能指针的内存泄漏
- 循环引用示例
- 3. `std::weak_ptr`(弱引用,解决循环引用)
- 总结
- `#define` 和 `const` 定义常量的选择
- 1. `#define` 是简单的文本替换
- 2. `const` 有类型检查
- 3. 调试支持
- 4. 作用域管理
- 5. 节省内存
- `typedef` 和 `#define` 的区别
- 1. 类型安全
- 2. 指针类型区别
- 3. 调试和可读性
- 总结
- **`#define` vs `const`**
- **`#define` vs `typedef`**
- 头文件中定义静态变量的问题
- 1. 每个包含该头文件的源文件都会创建一个独立的静态变量
- 2. 可能导致调试困难
- 3. 头文件的正确做法
- 推荐:在头文件中使用 `extern` 声明,而在 `.c` 文件中定义
- 4. `static` 变量的正确使用场景
- 结论
C++中的智能指针
普通指针的问题
在C++传统的手动内存管理中,动态分配的对象需要手动释放,否则会产生内存泄露。
智能指针的概念
智能指针(Smart Pointer)是一个类模板,主要用于管理动态分配的堆内存,避免手动管理带来的内存泄露、二次释放等问题。智能指针通过RAII(Resource Acquisition Is Initialization) 机制,在对象生命周期结束时自动释放资源,从而提高程序的安全性和可维护性。
C++标准库提供了三种主要的智能指针:
std::unique_ptr
(独占所有权)std::shared_ptr
(共享所有权)std::weak_ptr
(弱引用)
智能指针的使用
智能指针通过自动管理资源来解决普通指针的管理问题。
1. std::unique_ptr
(独占所有权)
unique_ptr
只能有一个指针拥有某块堆内存,不能被复制,只能被移动。- 适用于独占资源,如文件句柄、互斥锁等。
#include <iostream>
#include <memory> // 需要包含头文件void func() {std::unique_ptr<int> ptr = std::make_unique<int>(10);std::cout << *ptr << std::endl; // 输出 10
} // ptr 离开作用域,自动释放内存int main() {func();return 0;
}
特点:
- 不能被复制:
std::unique_ptr<int> p2 = p1; // 错误
- 只能转移所有权:
std::unique_ptr<int> p2 = std::move(p1);
2. std::shared_ptr
(共享所有权)
shared_ptr
允许多个shared_ptr
实例共享同一块堆内存。- 通过引用计数(Reference Count)管理资源,只有最后一个
shared_ptr
被销毁时,资源才会被释放。
#include <iostream>
#include <memory>void func() {std::shared_ptr<int> p1 = std::make_shared<int>(20);std::shared_ptr<int> p2 = p1; // p2 也共享这块内存std::cout << *p1 << ", " << *p2 << std::endl; // 输出 20, 20
} // p1, p2 离开作用域,引用计数变为 0,自动释放内存int main() {func();return 0;
}
特点:
- 适用于多个对象共享资源的场景,如缓存、线程池等。
- 通过
use_count()
方法可以查询当前引用计数。
智能指针的内存泄漏
虽然 shared_ptr
自动管理资源,但如果两个 shared_ptr
互相持有对方,会导致循环引用(Cyclic Reference),导致资源无法释放。
循环引用示例
#include <iostream>
#include <memory>class B; // 先声明class A {
public:std::shared_ptr<B> ptrB;~A() { std::cout << "A 被销毁\n"; }
};class B {
public:std::shared_ptr<A> ptrA;~B() { std::cout << "B 被销毁\n"; }
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->ptrB = b;b->ptrA = a;// 离开 main() 作用域时,a 和 b 的引用计数都不会变为 0,导致内存泄露return 0;
}
问题:
a
和b
互相持有shared_ptr
,导致引用计数始终不为 0,资源不会释放。- 解决方案:使用
std::weak_ptr
破坏循环引用。
3. std::weak_ptr
(弱引用,解决循环引用)
weak_ptr
不会增加引用计数,它只是一个观察者,不会影响 shared_ptr
管理的对象生命周期。
#include <iostream>
#include <memory>class B; // 先声明class A {
public:std::weak_ptr<B> ptrB; // 改为 weak_ptr~A() { std::cout << "A 被销毁\n"; }
};class B {
public:std::weak_ptr<A> ptrA; // 改为 weak_ptr~B() { std::cout << "B 被销毁\n"; }
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->ptrB = b;b->ptrA = a;// 现在没有循环引用,资源会被正常释放return 0;
}
为什么 weak_ptr
解决了问题?
weak_ptr
不增加shared_ptr
的引用计数,因此不会影响对象的生命周期。weak_ptr
只是一种观察shared_ptr
是否已经失效,可以通过lock()
方法获取有效shared_ptr
。
总结
智能指针类型 | 主要特点 | 适用场景 |
---|---|---|
unique_ptr | 独占所有权,不可复制 | 独占资源,如文件、互斥锁 |
shared_ptr | 共享所有权,引用计数管理 | 适用于多个对象共享资源 |
weak_ptr | 不增加引用计数,防止循环引用 | 适用于 shared_ptr 互相引用的情况 |
智能指针是现代 C++ 的重要工具,能有效减少手动管理内存带来的问题,提高程序的稳定性和安全性。在实际开发中,合理选择合适的智能指针能帮助编写高效、安全、可维护的代码。
#define
和 const
定义常量的选择
两者都可以用于定义常量,但 const
通常是更好的选择,原因如下:
1. #define
是简单的文本替换
- 预处理器会直接将
#define
定义的内容替换到代码中,不进行类型检查。 - 这可能导致难以发现的错误,例如:
#define PI 3.1415926 float radius = 2; float area = PI * radius * radius; // 预处理器替换成 3.1415926 * radius * radius
- 但是如果写成
#define PI 3,1415926
(错误的逗号),编译器不会报错,而是会产生奇怪的行为。
2. const
有类型检查
const
变量具有类型,编译器会进行类型检查:const double PI = 3.1415926;
- 这样可以避免
#define
的文本替换错误,提高代码的可读性和安全性。
3. 调试支持
#define
定义的常量在编译时被替换,调试器无法查看它们的值。const
变量存储在内存中,可以在调试时查看其值。
4. 作用域管理
#define
没有作用域的概念,定义后在整个代码中都可用,容易引起冲突。const
变量可以限制在特定的作用域,减少潜在错误。
5. 节省内存
#define
定义的值会在代码中多次重复,而const
变量只存储一次,提高内存利用率。- 例如:
#define MAX_SIZE 100 const int MAX_SIZE = 100;
- 在
#define
版本中,每次使用MAX_SIZE
,编译器都会插入100
,而const
版本只会存储一份100
,程序会引用该变量。
typedef
和 #define
的区别
两者都可以创建别名,但 typedef
更推荐用于定义数据类型的别名,原因如下:
1. 类型安全
#define
只是简单的文本替换,不做类型检查:#define INT_PTR int * INT_PTR a, b; // 实际等价于 `int *a, b;`,b 只是 int 变量
- 而
typedef
具有类型安全:typedef int* IntPtr; IntPtr a, b; // `a` 和 `b` 都是 `int*`
2. 指针类型区别
typedef
的含义更加清晰:typedef char* String; String s1, s2; // s1 和 s2 都是 `char*`
- 但是
#define
:#define String char* String s1, s2; // 实际等价于 `char* s1, s2;`,其中 s2 是 `char` 而不是指针
3. 调试和可读性
typedef
允许更好的调试支持,因为它是一个真正的类型定义,而#define
只是替换文本。
总结
- 定义常量时,优先使用
const
而不是#define
。 - 定义类型别名时,使用
typedef
而不是#define
,尤其是在指针和复杂结构时。
#define
vs const
对比项 | #define | const |
---|---|---|
基本原理 | 预处理器宏替换,不分配存储空间 | 变量存储在内存中,有类型信息 |
类型检查 | 无类型,不进行类型检查 | 有类型,编译器进行类型检查 |
作用域 | 全局作用域,无作用域概念 | 受作用域限制(局部或全局) |
存储位置 | 代码段,不占用存储空间 | 数据段,可能存储在栈、全局区或常量区 |
调试支持 | 不能在调试器中查看值 | 可在调试器中查看 |
编译方式 | 预处理阶段替换,无法优化 | 编译时优化 |
代码可读性 | 低,容易引入难以发现的错误 | 高,更安全,避免潜在错误 |
示例 | #define PI 3.1415926 | const double PI = 3.1415926; |
推荐使用场景 | 适用于条件编译、函数宏 | 适用于定义常量,推荐使用 |
#define
vs typedef
对比项 | #define | typedef |
---|---|---|
基本原理 | 预处理器宏替换,字符串替换 | 类型定义,创建类型别名 |
类型检查 | 无类型检查,可能引入错误 | 受编译器检查,类型安全 |
使用方式 | 只替换文本,可能导致意外错误 | 定义新的类型,更加清晰 |
调试支持 | 无法调试,替换后看不到原始定义 | 受调试器支持,可查看变量类型 |
指针定义区别 | #define INT_PTR int* 定义的 INT_PTR p1, p2; 只让 p1 成为指针,而 p2 仍然是 int | typedef int* IntPtr; 让 IntPtr p1, p2; 都是 int* |
结构体定义 | 需要 #define 结构体名,并手动写 struct 关键字 | typedef struct 可直接使用别名 |
示例 | #define INT_PTR int* | typedef int* IntPtr; |
推荐使用场景 | 仅适用于简单文本替换 | 适用于创建类型别名,推荐使用 |
头文件中定义静态变量的问题
在头文件中定义静态变量(static
变量)通常是不推荐的,主要有以下几个原因:
1. 每个包含该头文件的源文件都会创建一个独立的静态变量
static
变量的作用域限定在当前编译单元(即当前.c
文件),如果在头文件中定义static
变量,那么每个包含该头文件的.c
文件都会有各自独立的static
变量副本,而不是共享同一个变量。- 这不仅导致 资源浪费(因为每个
.c
文件都有自己的一份拷贝),还可能导致逻辑错误(多个文件中存在相同名称但不同的变量)。
示例(错误示范):
// file.h
static int count = 0; // 头文件中定义静态变量(错误示范)
// file1.c
#include "file.h"
void func1() {count++; // file1.c 有自己独立的 count 变量
}
// file2.c
#include "file.h"
void func2() {count++; // file2.c 也有自己独立的 count 变量
}
问题:
file1.c
和 file2.c
各自有自己的 count
变量,它们不会共享同一个 count
,这可能不是我们想要的行为。
2. 可能导致调试困难
由于 static
变量的作用域仅限于当前编译单元,多个 .c
文件中可能会存在多个同名但不同的变量,这会使得代码的可维护性变差,调试时容易造成混淆。
3. 头文件的正确做法
推荐:在头文件中使用 extern
声明,而在 .c
文件中定义
如果变量需要在多个 .c
文件中共享,推荐使用 extern
关键字在头文件中声明,而在某个 .c
文件中定义变量。
示例(正确做法):
// file.h(仅声明,不定义)
#ifndef FILE_H
#define FILE_Hextern int count; // 使用 extern 声明全局变量#endif // FILE_H
// file.c(定义变量)
#include "file.h"int count = 0; // 仅在一个 .c 文件中定义void increment() {count++;
}
// main.c(使用变量)
#include "file.h"
#include <stdio.h>int main() {printf("count = %d\n", count);return 0;
}
这种做法可以确保 count
变量在整个程序中只有一个实例,避免了 static
变量在多个文件中重复定义的问题。
4. static
变量的正确使用场景
尽管不推荐在头文件中定义 static
变量,但它在 .c 文件内部 还是有实际用途的:
- 限制变量作用域:避免变量在其他文件中被错误访问和修改。
- 防止命名冲突:局部
static
变量不会污染全局命名空间。
示例(正确使用 static
):
// file.c
#include <stdio.h>static int count = 0; // 仅 file.c 内部可见void increment() {count++;printf("count = %d\n", count);
}
// main.c
#include "file.h"int main() {increment(); // 访问 file.c 内部的静态变量return 0;
}
这种 static
变量不会被其他 .c
文件访问,因此是 static
的正确用法。
结论
方案 | 是否推荐 | 原因 |
---|---|---|
在头文件中定义 static 变量 | ❌ 不推荐 | 每个包含头文件的 .c 文件都会有独立的变量,浪费资源且容易出错 |
在头文件中使用 extern 声明,全局变量放在 .c 文件 | ✅ 推荐 | 确保变量在多个 .c 文件中共享,并且只有一个实例 |
在 .c 文件中定义 static 变量 | ✅ 推荐 | 限制变量作用域,避免污染全局命名空间 |
最佳实践:
- 如果变量需要在多个
.c
文件中共享 → 用extern
声明,在.c
文件中定义。 - 如果变量仅在当前
.c
文件中使用 → 用static
限制作用域,防止其他文件访问。 - 避免在头文件中定义
static
变量,否则会导致多个.c
文件各自拥有独立的变量实例,增加内存占用,且可能产生意想不到的错误。