day 21

C++11新特性

  1. 智能指针
  2. 右值引用和move语句
  3. auto关键字(根据初始化的值自动推导类型)
  4. lambda表达式
  5. for的范围遍历
  6. 类和结构体中初始化列表
  7. nullptr代替null
  8. 统一的初始化方式

    智能指针: 智能指针利用RAII思想将指针进行封装,使其在构造时分配内存,析构时释放内存,将动态分配的内存交给类对象管理,防止堆内存泄漏。常见的三种智能指针分别是unique_ptr ,shared_ptr,weak_ptr。使用时要包含memory头文件。
    unique_ptr(独占指针):这个指针独占指向对象的所有权,不允许多个指针指向同一个对象(不支持拷贝和复制),但可以通过转移所有权给另一个指针(通过.release()方法和move()语句即可)。
    使用示例:
std::unique_ptr<int> p1 = std::make_unique<int>(5);
 // std::unique_ptr p2 = p1; // 这会导致编译错误,因为unique_ptr不能被复制
 std::unique_ptr<int> p2 = std::move(p1);
 int* p3 = p2.release();
 std::cout<<*p3<<std::endl;

**shared_ptr:**这个指针允许多个shared_ptr指向同一个对象,内部不仅包含了指向动态分配的内存的指针,还包含一个引用计数块,用于维持一个引用计数(当前有多少shared_ptr指向这个对象),当引用计数==0时,自动释放内存。
使用示例:

  std::shared_ptr<int> p1 = std::make_shared<int>(5);
  std::shared_ptr<int> p2 = p1;
  std::cout << "引用计数: " << p1.use_count() << std::endl;
  // 此时引用计数为2
  {
    std::shared_ptr<int> p3 = p1;
    std::cout << "引用计数: " << p1.use_count() << std::endl;
    // 此时引用计数为3,因为p3也指向同一块内存
  }
  std::cout << "引用计数: " << p1.use_count() << std::endl;
  // 当p3超出作用域,引用计数变回2
  return 0;
  • 当创建类的新对象时,初始化指针并置为1;
  • 每当调用拷贝构造函数时,新对象的指针引用计数=传入对象的指针引用计数,并++;
  • 使用赋值运算符时,减少当前对象的计数,减为0则删除当前指针,不为0则让内部指针指向=右侧的对象的指针,引用计数在=右侧对象的基础+1;
  • 包含一个模板指针指向实际对象;
  • 一个引用计数,必须new分配的,不然多个指针指向不同的引用计数。
    weak_ptr: 引用计数可能导致引用成环问题,比如A对象的指针指向B,B对象的指针指向A,互相指向,引用计数不可能为0,永远不释放了。弱指针不参与引用计数,可以指向shared_ptr所指向的对象,也就是只引用,不计数。shared_ptr和弱指针同时指向一块内存,shared_ptr释放内存,不用考虑弱指针还在指向内存,所以弱指针使用前必须通过.lock()函数升级为shared_ptr检查内存是否为空。

右值引用和move语句
左值:有实际的内存地址的变量。
右值:无实际内存地址,临时量,语句结束后即消失。
左值可以作为右值参加运算,但是右值不可以作为左值。

int a = 5;
int b = a + 1;

左值引用:用一个&将另一个变量作为这个变量的别名,绑定后不能更换。
右值引用:用&&绑定一个右值,可以增加右值的生命周期,也只能绑定一个右值。
move函数:告诉编译器可以将一个左值看作右值引用,将左值的资源看作右值绑定在右值引用上,将资源的直接转移到新对象中而不是赋值(需要拷贝一份,浪费资源),move后的左值不可再使用。

lambda表达式
定义一个匿名函数,结构是[捕获列表](参数列表)->返回值类型 {函数体}

  • 捕获列表:表达式可以访问的外部变量,分为值捕获和引用捕获,值捕获在表达式内部只传入了值(类似函数的值传递),引用捕获,类似传入引用变量,表达式内修改也会影响外部。
  • 参数列表:调用lambda表达式时需要传入的参数。如auto lambda = [](int x, int y) -> int { return x + y; }; int result = lambda(3, 4);
    std::vector<int> numbers = {5, 2, 8, 1, 9};
    // 使用lambda表达式定义升序排序规则,传递给std::sort函数
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
        return a < b;
    });
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;

统一初始化方式
统一初始化主要使用花括号{}来初始化对象,这种方式可以用于初始化基本数据类型、数组、结构体、类对象以及容器等多种类型的对象。

//数组初始化
int arr[]{1, 2, 3, 4, 5};
//变量初始化
int a{5};
double b{3.14};
//类初始化
class MyClass {
public:
    int value;
    MyClass(int val) : value(val) {}
};
MyClass mc{42};
//序列式容器初始化
std::vector<int> v{1, 2, 3, 4, 5};
//关联式容器初始化
std::map<int, std::string> m{{1, "one"}, {2, "two"}};

c++的内存管理

内存构成分别是栈,堆,全局/静态数据区,常量区,代码段。

  • 栈:编译器管理分配和释放,速度较快,远远小于堆,高地址向低地址扩展。
  • 堆:手动分配和释放,速度较慢,低地址向高地址扩展。
  • 全局/静态数据区:存放全局变量和静态变量。全局变量和静态全局变量编译器编译时根据值类型为其分配空间,程序启动时就创建。
    局部的静态变量,是在运行到这条语句时创建,有初始化值就赋值,没有就默认值,第一次赋值时赋值,之后就不能再次赋值。
  • 常量区:存放常量的区域
  • 代码段:存放程序编译后生成的可执行的机器指令。

如何防止内存泄漏

  • new和delete,malloc和free要成对使用
  • 使用智能指针管理对象的内存

STL一些基本容器是如何实现的

vector:可以扩展的数组,底层包含了一个数组和三个迭代器,分别指向数组头部start,数组最后一个元素的下一个地址capacity,和指向当前最后一个元素的后面的size。
当size==capacity时,vector会找一个2倍于当前内存空间的地址将原数组的值拷贝(有移动构造调用对象的移动构造函数)过去,在释放原来那块内存。
支持随机访问和指定位置插入:通过首地址和偏移量,就可以快速找到访问的地址。先插入到尾部,再和前面的值交换到指定位置。
list:由多个节点组成,节点之间的连接靠节点本身的两个指向前后节点的指针。
不支持随机访问,只能顺序访问,但是插入和删除的速度快。
deque:双端队列,中控区是一个指针数组,存放指向各个相等大小的缓冲区的指针,在缓冲区(连续地址空间)内存放数据,访问deque的迭代器中含有指向中控区的指针和指向缓冲区元素的指针
索引/缓冲区个数就是位于第几个缓冲区,索引%缓冲区个数就是缓冲区内的偏移量。通过这种方式实现随机访问。插入操作时,迭代器找到插入的位置,然后将较少一边的元素后移。
map和set:底层用红黑树实现,有序的存放,按照key值从小到大存,查找和插入删除都是Ologn。存放的值是唯一的。multimap和multiset允许存放相同的key。
unordered_map和unordered_set:unordered_map和unordered_set的key值无序存储,底层使用哈希表存放,查找和插入删除是O1,不适合顺序遍历的场景。

你可能感兴趣的:(C++学习专栏,c++,stl)