文章

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 进行授权
本页总访问量