<<现代C++实战30讲>>打卡学习笔记—基础篇

说明

基础篇9讲主要学习内存管理、容器、迭代器、异常、C++11/14/17的新语法。
每日打卡更新。

打卡Day1:堆、栈、RAII:C++里该如何管理资源?

  • 内存管理:
    C++的堆和栈与JAVA中意思是一样的,不同的是,C++中没有垃圾回收这个概念,而是使用RAII管理内存(参考第一部分析构函数部分代码);
    栈中的内存,在函数调用结束后,自动释放,不需要手动释放,而且由于栈的LIFO特性,保证了释放内存空间的连续性;
    堆内存分配需要使用new关键字,分配内存,并且使用delete关键字手动释放内存。
  • 和JAVA语言不同的是,C++的默认都是值定义,只有指针或者引用对象才可以new对象(new的对象才会在堆上面分配内存),不new的对象也会默认调用构造函数,但是内存并不是分配在堆上面,而是分配在栈上面。

打卡Day2:自己动手,实现C++的智能指针

  • 一些基础概念学习:
    运算符重载:C++中的运算符都可以看做是一个函数,这样就类似于Java中的函数重载。
    拷贝构造函数:通过使用另一个同类型的对象来创建一个新对象。
    拷贝赋值:重载拷贝赋值运算符。
    移动构造函数:构造新的对象后,原有对象资源清空。
    移动赋值: 重载赋值运算符。
    现代C++中尽量不要使用裸指针(原始指针),最好使用标椎库中智能指针(shared_ptr和unique_ptr)。
    根据第一讲的RAII思想,实现shared_ptr和unique_ptr。
    shared_ptr的对象使用拷贝操作;
    unique_ptr的对象使用移动操作;
    引用计数(共享计数):shared_ptr和unique_ptr的最大区别,就是share_ptr多个智能指针同时拥有一个对象,需要一个共享计数,用来标记,是否需要删除这个对象。
    第一讲和第二讲的代码如下:
#include 
using namespace std;

// template 类似于Java中的泛型
template 
class smart_ptr {
 private:
  T *ptr_;

 public:
  // explicit 构造函数关键字;
  // ptr_(ptr)表示初始化构造函数的成员变量,这个地方由于不熟悉语法,一开始没有理解。
  // C++11起 空指针用nullprt表示,古代C++用0或者NULL
  explicit smart_ptr(T *ptr = nullptr) : ptr_(ptr) {
    cout << "调用默认构造函数,分配内存;" << endl;
  }
  //析构函数(delete 对象时,才会调用)
  ~smart_ptr() {
    cout << "调用析构函数,释放内存;" << endl;
    delete ptr_;
  }
  //拷贝构造函数(通过使用另一个同类型的对象来创建一个新对象。)
  smart_ptr(smart_ptr &other) {
    cout << "调用拷贝构造函数;\n";
    ptr_ = other.rlease();
  }
  //拷贝赋值(覆盖原有的对象)
  smart_ptr &operator=(smart_ptr &other) {
    cout << "调用拷贝赋值,赋值对象;\n";
    smart_ptr(other).swap(*this);
    return *this;
  }

  //移动构造函数
  smart_ptr(const smart_ptr &&other) { cout << "调用移动构造函数\n"; }

  smart_ptr &operator=(smart_ptr other) {
    other.swap(*this);
    return *this;
  }

  void swap(smart_ptr &rhs) {
    using std::swap;
    swap(ptr_, rhs.ptr_);
  }

  //将传入引用对象赋值为空,并把该对象赋值给新创建的对象
  T *rlease() {
    T *ptr = ptr_;
    ptr_ = nullptr;
    return ptr;
  }

  // const
  // 表示该方法外部不能修改,和java不同,在C++中,const可以用来声明成员函数
  T *get() const {
    cout << "对象地址:" << this << ";get==>ptr value:" << *ptr_ << endl;
    return ptr_;
  }
  // C++中的重载运算符,类似于重载现有的运算符函数。
  T *operator->() const { return ptr_; }
  T &operator*() const { return *ptr_; }
};
//共享计数
class share_count {
 public:
  share_count() : count_(1){};
  void add_count() { ++count_; }
  long reduce_count() { return --count_; }
  long get_count() const { return count_; }

 private:
  long count_;
};

// 程序的主函数
int main() {
  string value = "share_ptr";
  string *ptr = &value;
  smart_ptr ptr1{
      ptr};  // C++11起可以用大括号初始化一个对象,调用默认构造函数
  // smart_ptr ptr2;
  ptr1.get();
  // delete ptr1;
  smart_ptr ptr3{ptr1};  //调用拷贝构造函数
  ptr3.get();
  smart_ptr ptr4;
  //ptr4=ptr1; //调用拷贝赋值,编译器会报错;
  ptr4 = std::move(ptr1);  //调用移动构造函数(C++11起)
  ptr4.get();
  // 设置长度
  return 0;
}

验证输出结果:

调用默认构造函数,分配内存;
对象地址:0x7ffffffedf08;get==>ptr value:share_ptr
调用拷贝构造函数;
对象地址:0x7ffffffedf10;get==>ptr value:share_ptr
调用默认构造函数,分配内存;
调用移动构造函数
调用析构函数,释放内存;
对象地址:0x7ffffffedf18;get==>ptr value:share_ptr
调用析构函数,释放内存;

打卡Day3:右值和移动究竟解决了什么问题?

继续理解一些基础概念

  • 左值和右值的概念好理解,例如++x是右值,是一个临时变量;一个特殊的概念是亡值,是右符号标记的有值,联系到上一讲的移动构造函数调用就是std::move(ptr1)亡值。
    拷贝构造函数和移动构造函数中的参数,一个是左值引用(T&),一个是右值引用(T&&)。
// lvalues_and_rvalues2.cpp
int main()
{
    int i, j, *p;
    //  i 是 lvalue , 7 是prvalue.
    i = 7;
    // 错误使用: `j * 4` 是一个 prvalue.
    7 = i; // C2106
    j * 4 = 7; // C2106

    // *p 是 lvalue.
    *p = i;

    //  ((i < 3) ? i : j) 是左值
    ((i < 3) ? i : j) = 7;
    
    // 错误使用: ci is 不能修改的左值
    const int ci = 7;
    ci = 9; // C3892
}
  • 与Java不同的是,C++中的原生类型、枚举、结构、联合、类都是值类型,只有指针和引用是引用类型。
  • C++移动的真正含义:
class A {
  B b_;
  C c_;
};

从实际内存布局的角度,很多语言——如 Java 和 Python——会在 A 对象里放 B 和 C 的指针(虽然这些语言里本身没有指针的概念)。而 C++ 则会直接把 B 和 C 对象放在 A 的内存空间里。这种行为既是优点也是缺点。说它是优点,是因为它保证了内存访问的局域性,而局域性在现代处理器架构上是绝对具有性能优势的。说它是缺点,是因为复制对象的开销大大增加:在 Java 类语言里复制的是指针,在 C++ 里是完整的对象。这就是为什么 C++ 需要移动语义这一优化,而 Java 类语言里则根本不需要这个概念。

上面的一段直接引用。

  • 返回一个本地对象意味着这个对象会被拷贝,虽然编译器会做返回值优化,因此,不要返回本地变量引用。

打卡Day4:容器汇编 I:比较简单的若干容器

  • 这一讲都是简单的顺序数据结构
    vector 类似于java中的Arraylist;
    deque 类似于java中的ArrayDeque;
    list(双链表)类似于java中的LinkedList;
    forward_list是C++11中提供的单链表。
    queue和stack没有begin和end成员函数,所以不支持迭代访问。
    在C++中有个容器适配器概念,意思是queue和stack支持定义基础容器类型,默认的基础容器类型是deque。
  • noexcept保证一个移动构造函数不抛出异常。
    如果vector的移动构造函数不能确保不抛出异常,vector 通常会使用拷贝构造函数。这种情况下对于某些自定义类型的拷贝就是灾难。
    代码如下:
#include 
#include 

using namespace std;

class obj1 {
 private:
  /* data */
 public:
  obj1() { cout << "obj1 构造函数\n"; };
  ~obj1() { cout << "obj1 析构函数\n"; }
  obj1(const obj1&) { cout << "obj1 拷贝构造函数\n"; }
  obj1(obj1&&) { cout << "obj1 移动构造函数\n"; }
};

class obj2 {
 private:
  /* data */
 public:
  obj2() { cout << "obj2 构造函数\n"; };
  obj2(const obj2&) { cout << "obj2 拷贝构造函数\n"; }
  // noexcept 保证提供一个不抛异常的移动构造函数
  obj2(obj2&&) noexcept { cout << "obj2 移动构造函数\n"; }
};

int main() {
  vector v1;
  //使用reserve 分配连续内存空间
  v1.reserve(2);
  //在尾部新构造一个元素
  v1.emplace_back();
  v1.emplace_back();
  //构造第三个对象时,内存不足,分配一个新的内存,并构造第三个对象,
  //同时调用拷贝构造函数,复制第一个和第二个对象
  v1.emplace_back();
  vector v2;
  v2.reserve(2);
  v2.emplace_back();
  v2.emplace_back();
  // 构造第三个对象时,内存不足,需要分配一个新的内存,构造第三个对象,
  //同时调用移动构造函数(因为该函数保证了抛出异常),复制第一个和第二个对象
  v2.emplace_back();
}

输出:

#第一次调用v1.emplace_back();
obj1 构造函数
#第二次调用v1.emplace_back();
obj1 构造函数
#第三次次调用v1.emplace_back();
obj1 构造函数
obj1 拷贝构造函数
obj1 拷贝构造函数
#完成后释放旧vector中的两个对象内存
obj1 析构函数
obj1 析构函数
#第一次调用v2.emplace_back();
obj2 构造函数
#第二次调用v2.emplace_back();
obj2 构造函数
#第三次调用v2.emplace_back();
obj2 构造函数
obj2 移动构造函数
obj2 移动构造函数
#完成后释放新vector中的三个对象内存
obj1 析构函数
obj1 析构函数
obj1 析构函数

打卡Day5:容器汇编 II:需要函数对象的容器

熟悉两个概念:

  • 函数对象,我的理解是类或者结构重新定义了重载运算符"()",则该类或者结构声明的对象就是函数对象
//less struct
template 
struct less
  : binary_function {
 //重载运算符“()”
  bool operator()(const T& x,
                  const T& y) const
  {
    return x < y;
  }
};
// hash struct
template  struct hash;
template <>
struct hash
  : public unary_function {
  size_t operator()(int v) const
    noexcept
  {
    //static_cast类型转换函数,size_t是无符号整数类型
    return static_cast(v);
  }
};

这一讲的容器都会带上这两个函数对象

  • priority_queue这种数据结构和stack类似,但是默认带有less函数对象,这样栈顶存储的就是最大数。
  • C++中的关联容器( set、map、multiset和 multimap)与Java不同,不是按插入数据的顺序,会默认按key排序。为此,从C++11起定义了无序关联容器(unordered_set,unordered_map,unordered_multiset,unordered_multimap);
  • C++ 中的set不允许存在重复的元素,要存储重复的元素,需要使用multiset和 multimap
  • 使用数组的话,不建议用C的原始数组,
    较大数组可以使用vector,较小的数组可以使用array(C++ 11)
    由于容器的输出不方便,测试的时候推荐使用 xeus-cling 在线可视化工具。
    xeus-cling online

打卡Day6:异常,用还是不用,这是个问题

  • C++标准库里面已经使用了异常,所有肯定会用,最关键的是当异常发生时,要保障不会发生内存泄露,所以要理解RAII,在JAVA中会用GC机制释放内存。
  • Google不建议使用异常,有两方面原因,一是历史原因,早期他们的编译器对异常处理不好,所有他们产生了一大堆异常不安全的代码;另一个原因是追求更高的性能(打开和关闭异常时,会产生一些二进制文件大小)
  • 第一讲有个“栈展开”,其实就是在函数执行异常之前,调用函数之前所有局部变量的析构函数,释放所有资源。

打卡Day7:迭代器和好用的新for循环

  • 迭代器是为了解耦数据结构和算法。先泛型数据容器,然后泛型数据容器的迭代器,最后泛型算法就好写了。本质上是一种将容器的访问高度抽象,Java语言编译器自动处理了这些,它底层的思想也是类似C++的指针。
  • 作者列了一些常用迭代器:
    Input迭代器,需要重载,->,==,!=运算符;
    Output迭代器,需要重载
    ,->,==,!=运算符;;
    Forward迭代器:具有input和out迭代器的所有功能;
    Bidirectional迭代器,具有Forward迭代器所有功能;
    Random Access迭代器;
    下面是Input迭代器的实现:
#include 
#include 
#include    // std::istream
#include   // std::input_iterator_tag
#include 

using namespace std;

class istream_line_reader {
 public:
  class iterator {
   public:
    typedef ptrdiff_t difference_type;
    //迭代器指向的对象的值类型
    typedef std::string value_type;
    //迭代器指向的对象的指针类型
    typedef const value_type* pointer;
    //迭代器指向的对象的引用类型
    typedef const value_type& reference;
    //输入迭代器
    typedef std::input_iterator_tag iterator_category;

    iterator() noexcept : stream_(nullptr) {}
    explicit iterator(istream& is) : stream_(&is) { ++*this; }

    reference operator*() const noexcept { return line_; }
    pointer operator->() const noexcept { return &line_; }
  
    iterator& operator++() {
      getline(*stream_, line_);
      if (!*stream_) {
        stream_ = nullptr;
      }
      return *this;
    }
    iterator operator++(int) {
      iterator temp(*this);
      ++*this;
      return temp;
    }
  //迭代器相等比较
    bool operator==(const iterator& rhs) const noexcept {
      return stream_ == rhs.stream_;
    }
  //迭代器不等比较
    bool operator!=(const iterator& rhs) const noexcept {
      return !operator==(rhs);
    }

   private:
    istream* stream_;
    string line_;
  };

  istream_line_reader() noexcept : stream_(nullptr) {}
  explicit istream_line_reader(istream& is) noexcept : stream_(&is) {}
  iterator begin() { return iterator(*stream_); }
  iterator end() const noexcept { return iterator(); }

 private:
  istream* stream_;
};
int main() {
  //读取文件
  ifstream ifs{"test.txt"};
  for (const string& line : istream_line_reader(ifs)) {
    //示例循环体中仅进行简单输出
    cout << line << endl;
  }
}

打卡Day8:易用性改进 I:自动类型推断和初始化

  • 类型推断,Java10 应该也支持了类型推断var;
    auto:编译器会根据表达式的类型推断变量类型,不需要手动声明。
  • 函数模板参数推导,其实auto的实现规则类似模版参数推导。根据实际调用的实参类型,推导形参类型。
    值类型参数推导;
    引用类型参数推导;
//定义一个模版函数
template
T max(T a, T b)
{
    // if b < a then yield a else yield b
    return b < a ? a : b;
}
//调用
int const c = 42;
max(i, c); // OK :实际调用mar(int,int)
max(c, c); // OK: 实际调用mar(int,int)
int arr[4];
max(&i, arr); // OK: 实际调用mar(int*,int*)
max(4, 7.2);    //ERROR: T 有可能是 int 或 double
std::string s;
foo("hello", s); //ERROR: T 有可能是char const[6] 或者std::string
  • 类模板推导
#include 
using namespace std;
int main() {
//推导,C++17之前的写法 auto pr = make_pair(1, 42);
//这里还用到C++11的大括号初始化写法。
pair pr{1, 2}; 
}

打卡Day9:易用性改进 II:字面量、静态断言和成员函数说明符

  • 字面量:,在Java中,会有一些标准的字面量,例如3.14f表示浮点数。这个字面量是原生支持的,在传统C++(C++11之前)中,也是如此,但是从C++11起开始支持自定义字面量。
#include 
struct length {
  double value;
  //单位
  enum unit {
    metre,
    kilometre,
    millimetre,
    centimetre,
    inch,
    foot,
    yard,
    mile,
  };
  static constexpr double factors[] = {1.0,    1000.0, 1e-3,   1e-2,
                                       0.0254, 0.3048, 0.9144, 1609.344};
  explicit length(double v, unit u = metre) { value = v * factors[u]; }
};

//重载字面量运算符(""),自定义字面量_m(注意自定义的必须以_开头)
length operator"" _m(long double v) { return length(v, length::metre); }
//自定义字面量_cm
length operator"" _cm(long double v) { return length(v, length::centimetre); }

length operator+(length lhs, length rhs) {
  return length(lhs.value + rhs.value);
}

int main() {
  length lhs = 1.0_cm;  //调用运算符 "" _m(1.0)
  length rhs = 2.0_m;   //调用运算符 "" _cm(2.0)
  length sum = lhs + rhs;
  std::cout << sum.value << std::endl;
}
  • default 和 delete 成员函数
    第二讲说过,当定义一个类类型(class,struct)时,开发者不声明构造函数,析构函数,拷贝函数,移动函数,拷贝赋值运算函数,移动赋值运算函数,编译器会自动声明这些函数。
    当类中存在用户声明的构造函数时,用户仍可以关键词 default 强制编译器自动生成原本隐式声明的默认构造函数,析构函数,拷贝函数,移动函数,拷贝赋值运算函数,移动赋值运算函数。
#include   //std::cout

struct A {
  int x;
  A(int x = 1) : x(x) {
    std::cout << "调用A构造函数" << std::endl;
  }  // 用户定义默认构造函数
};

struct B : A {
  // 隐式定义 B::B(),调用 A::A()
};

struct C {
  A a;
  // 隐式定义 C::C(),调用 A::A()
};

struct D : A {
  D(int y) : A(y) {}
  // 不会声明 D::D(),因为存在另一构造函数
};

struct E : A {
  E(int y) : A(y) {}
  E() = default;  // 显式预置,调用 A::A()
};

struct F {
  int& ref;     // 引用成员
  const int c;  // const 成员
                // F::F() 被隐式定义为弃置
};

int main() {
  A a;
  B b;
  C c;
  // D d; // 编译错误
  E e;
  //  F f; // 编译错误
}
  • override 和 final 关键字
    这个两个关键字和Java中意义是一样的。
class A {
public:
  virtual void foo();
  virtual void bar();
  void foobar();
};

class B : public A {
public:
  void foo() override; // OK
  void bar() override final; // OK
  //void foobar() override;
  // 非虚函数不能 override
};

class C final : public B {
public:
  void foo() override; // OK
  //void bar() override;
  // final 函数不可 override
};

class D : public C {
  // 错误:final 类不可派生
  …
};

你可能感兴趣的:(<<现代C++实战30讲>>打卡学习笔记—基础篇)