c++(8·嵌套类、联合体、宏、智能指针)
嵌套类
1
2
3
4
5
6
7
8
class A
{
private:
class B
{
public:
};
}
展开
- 嵌套类:一个类的定义在另一个类中,定义嵌套类的类称为外围类
- 使用嵌套类类型,遵守成员访问修饰符,当用public修饰可以被外部使用,需要外围类::
- 访问成员:(相互访问的权限非常有限)
- 嵌套类访问外围类的成员 / 外围类想要使用嵌套类,需要通过创建对象、指针或者引用
- 嵌套类可以直接访问外围类的静态成员、类型名( typedef )、枚举值
- 嵌套类的私有成员不能被外围类访问,只能被自身成员和友元访问
- 一个好的嵌套类设计:嵌套类应该设成私有(只能被外围类和外围类的友元访问),成员和方法可以设为 public(可以被外围类访问)
- 作用:强调主从关系
union
1
2
3
4
5
6
7
8
9
union Data {
int i;
double d;
char str[20];
};
Data data;
data.i = 10;
data.d = 3.14;
std::strcpy(data.str, "Hello");
展开
联合体/共用体:可以存储不同数据类型的对象
默认成员访问修饰符:public
内存分配:不同于结构体的内存分配机制,union的内存容量取决于最大的成员,内存对齐要考虑所有的成员
共享性:允许在 同一块内存空间 存储不同的数据类型,所有成员共享内存,它所有成员的偏移量都是0,内存地址都相同
使用方式:同一时间只能使用其中一个成员:为某类型成员赋值,操作其他类型成员很容易出现问题,最好再次赋值
作用:节省内存
使用场景:互斥数据
变长参数列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename... Args>
void function(Args... args) {
}
print(args...);
sizeof...(args);
int sum(int count, ...) {
va_list args; // 声明参数列表
va_start(args, count); // 初始化参数列表
int total = 0;
for (int i = 0; i < count; i++) {
int num = va_arg(args, int); // 获取下一个int参数
total += num;
}
va_end(args); // 清理参数列表
return total;
}
int result1 = sum(3, 10, 20, 30);
展开
- 作用:允许函数接受不定数量的参数
- 可变参数只能处理POD类型:不能有自定义的特殊成员函数(只能有合成版本的),不能有 虚函数 和 虚继承,有相同的访问级别(成员访问修饰符),第一个成员必须是内置类型,如果有父类成员全部需要在同一个类中
宏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define DEBUG
#define INIT_ARRAY(arr, size) \
do { \
for (int i = 0; i < (size); i++) { \
(arr)[i] = 0; \
} \
} while(0)
#define PI 3.1415926
#ifdef _WIN32
#define PATH_SEPARATOR '\\' // Windows平台
#else
#define PATH_SEPARATOR '/' // Linux/macOS平台
#endif
#ifdef _WIN32
……
#else
……
#endif
#ifndef HEAD_H
#define HEAD_H
// 头文件内容
#endif
展开
- 接受:空值,字面值,标识符,类型,表达式
- 函数宏:带参 + 表达式
- 多行宏:每行结尾用 \ 作为连接
- 宏作用域:无论定义在哪里,都是从定义开始,到文件结束,也可以用#undef提前结束作用域
- 使用场景和作用:
- 无参宏,定义标识,用于和条件预处理指令配合使用,预处理时期的文本丢弃,使目标程序变小
- 函数宏
- 优点:无函数调用成本(inline),类型无关性(模板),表达式在编译期计算(constexpr)
- 缺点:无类型检查,无法断点
- 类型别名
- 文本替换,简化代码使用
- 平台适配,针对不同情况有不同替换方式,否则代码会比较麻烦
- 头文件保护,防止重复包含错误
智能指针
- T* name = new T;
- new创建的指针指向动态生命周期,需要通过delete才能释放内存
- 当内置指针在生命周期结束前忘记释放指向的动态内存,内存会一直存在,容易造成内存泄露
- RAII资源获取即初始化机制(将资源的生命周期与对象的生命周期绑定):如果我们将动态内存交由类对象托管,对象过期调用析构时释放动态内存
智能指针常用函数
- get获取托管的指针指向的地址
- 不要用get初始化/赋值给另一个智能指针,这种非典型的用法并不会增加引用计数,当它们都调用析构,将多次释放同一内存,程序崩溃
- 不要delete掉get获得的内存,这样智能指针托管的指针变为无效内存,调用析构时,将多次释放同一内存,程序崩溃
- release取消托管,托管指针置为nullptr,动态内存需要手动释放,
- 要记得手动释放,函数会返回指向原托管的内存的指针
- reset重置托管,
- 如果参数为空,取消托管, 托管指针置为nullptr,释放托管的内存
- 如果参数不为空,如果地址不一致,释放掉旧的内存,托管新的内存,如果此内存被其他指针托管,取消旧托管,然后再托管
auto_ptr(C++11中已弃用)
1
2
3
4
5
6
7
8
9
10
11
12
#include < memory >
auto_ptr<type> test(new type);
Test *tmp = test.get();
Test *tmp2 = test.release();
delete tmp2;
test.reset();
test.reset(new Test());
auto_ptr<int[]> array(new int[5]);//❌:不支持管理数组
展开
- auto_ptr
- auto_ptr<T> name(new T);
- 不能多个智能指针同时指向同一内存
- 智能指针生命周期结束释放内存
- 由于auto_ptr模板类重载了-> 和 * 因此可以像普通指针一样使用
- 支持拷贝构造,拷贝赋值,允许左值赋值,但内部使用了资源转移,隐式操作非常容易忘记谁的资源被转移(拷贝函数做了移动函数的功能)
- 已弃用因为:
- 赋值会转移资源的所有权,当容器T为auto_ptr<T>时,元素赋值后被转移的元素托管的指针指向nullptr,访问它会出现问题,因此它作为容器T时存在重大风险
- 不支持管理数组
unique_ptr
1
2
3
4
5
6
7
8
unique_ptr<string> t1;//创建空对象
unique_ptr<string> p1(new string("hello"));//类模板
p1 = p2; // ❌:禁止左值拷贝赋值
unique_ptr<string> p3(p2); // ❌:禁止左值拷贝构造
p1 = std::move(p2); //✅:
unique_ptr<string> p3(std::move(p1)); //✅:
unique_ptr<int[]> array(new int[5]);//✅:支持管理数组
t9 = nullptr;//释放对象
展开
- unique_ptr
- unique_ptr<T> name(new T);
- 不能多个指针同时指向同一内存
- 智能指针生命周期结束释放内存
- 它仅支持移动构造,移动赋值,不允许左值赋值,仅支持传递右值,显示操作不容易忘记谁的资源被转移(移动函数做了移动函数的功能)
- 当容器T为unique_ptr<T>时,由于不允许左值赋值,因此它作为容器T时是安全的
- 支持管理数组
- 使用陷阱,当一个指针托管内存被另一个指针托管,原指针无法访问
shared_ptr
1
2
3
4
5
6
7
shared_ptr<string> sp1;
shared_ptr<string> sp2(new string("hello"));
sp1 = sp2;
shared_ptr<string> sp3(sp1);
shared_ptr<string[]> sp5(new string[5] { 3, 4, 5, 6, 7 });
shared_ptr<int> up3 = make_shared<int>(2);
up1 = nullptr ;//释放对象
展开
- shared_ptr
- shared_ptr<T> name(new T);
- 允许多个指针同时指向同一内存
- 当构造、拷贝构造、拷贝赋值……引用计数++,被赋值、析构……时引用计数–,如果计数为0,释放内存
- 支持拷贝构造,拷贝赋值,移动构造,移动赋值
- use_count可以获得当前托管指针的托管内存的引用计数
- make_shared 初始化对象,分配内存效率更高
- 支持管理数组
循环引用问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
class A{ void Set(shared_ptr<B> _b) { this->b = _b; } private: shared_ptr<B> b; }; class B{ void Set(shared_ptr<A> _a) { this->a = _a; } private: shared_ptr<A> a; }; void fun(){ shared_ptr<A> a(new A());//引用计数1 shared_ptr<B> b(new B());//引用计数1 a->Set(b);//引用计数2 b->Set(a);//引用计数2 } int main(void) { fun(); //引用计数1 //引用计数1 return 0; }
展开
- 当有AB两类,A包含B类型的智能指针成员,B包含A类型的智能指针成员,且分别托管对方类类型智能指针,
- 此时A和B托管的动态内存引用计数都是2,当AB类类型智能指针作用域结束,调用析构将托管指针置为空,两个动态内存引用计数–,托管的内存引用计数都变为1,所以动态内存没有被释放
- 解决方法一(避免出现引用循环):使用单方面管理,A托管的动态内存引用计数为2,B托管的动态内存引用计数为1,析构将A托管的动态内存引用计数–,B托管的动态内存引用计数–为0,动态内存被释放,成员指针作用域结束,动态内存引用计数–置为0,动态内存都被释放
- 解决方法二(使用weak_ptr):
weak_ptr
1
2
3
4
5
6
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
weak_ptr<A> w(a); // 使用共享指针构造
weak_ptr<B> w1; // 使用共享指针构造
w1 = b; // 使用共享指针赋值
shared_ptr<B> b1= w1.lock();
展开
- 弱指针 设计目的是为了协助 shared_ptr 工作,
- 原理:相当于托管了shared_ptr(就像shared_ptr托管动态内存一样)
- 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,
- 没有重载*和->,不能像普通指针一样使用
- lock获得托管的shared_ptr 对象
- use_count可以获得托管的引用计数
- expired判断当前weak_ptr智能指针是否还有托管的对象,有则返回false,无则返回true
- 不会增加/减少引用计数
- 对于循环引用问题:让AB任意一个智能指针成员为weak_ptr,由于不会增加/减少引用计数,和解决方法一原理一致
本文由作者按照 CC BY 4.0 进行授权