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

说明

提高篇11讲主要学习泛型编程、面向对象编程、元模版编程、函数式编程、并发。
每日打卡更新。

打卡Day10:到底应不应该返回对象?

  • C++11 起返回对象,既可以保证性能,有能提升可读性。如果能理解RAII原理,我觉得返回对象应该是比较好的一种方式。
    但需要注意返回一个本地变量时,不要返回引用和指针,这样等于把这个局部的引用暴露给函数调用栈了,会被随意更改。
  • 有个半正则概念,意思是返回的这个对象的类,应该有移动,拷贝和默认构造函数。原因是返回的对象,会默认调用移动函数,如果没有移动函数,会调用拷贝函数,生成一个新的对象,返回给外部调用。代码如下:
#include 
#include 
using namespace std;
// Can copy and move
class A {
 public:
  A() { cout << "Create A\n"; }
  ~A() { cout << "Destroy A\n"; }
  A(const A&) { cout << "Copy A\n"; }
  A(A&&) { cout << "Move A\n"; }
};
A getA_unnamed() {
  A a;
  return a;
}
A getA_duang()
{
  A a1;
  A a2;
  if (rand() > 42) {
    return a1;
  } else {
    return a2;
  }
}

int main() {
  // setA();
  //返回值优化
  //auto a = getA_unnamed();
  auto a = getA_duang();
}

输出结果

Create A
Create A
#返回a1时,调用移动函数
Move A
Destroy A
Destroy A
Destroy A

打卡Day11:Unicode:进入多文字支持的世界

  • 字符乱码是最常见的现象,我们来梳理一下字符编码的历史:
    1.ASCII编码:最早计算机的设计是用8bit作为一个字节,由于计算机是有美帝人民发明的,他们把自己常用的127个字符编码(大小英文字母和数字等)到计算机,就是ASCII编码。
    2.ISO/IEC 8859 系列:对于欧洲人民来说,他们不仅仅使用英语,还有法语,德语等语音,发现一些字符无法编码,所以欧洲国家在原来127位的基础上(8个字节最多256个字符),补充了128个字符,这些编码就是ISO 646 系列、ISO/IEC 8859 系列。
    3.GBK:对于亚洲人民来说,尤其中日韩国家,汉字动辄上万,256个字符远远不够,所以我国使用2个字节编码(最多编码65535字符)收录了 6763 个常用汉字和 682 个其他符号。这就是GB2312 ,后面有扩展了繁体字符等,成为GBK标准,为了接轨美帝,同时必须兼容了ASCII编码,它用单字节表示 ASCII 字符而用双字节表示 GB2312 中的字符。
    4.Unicode:
    4.1 Unicode规范:上面这么多字符编码标准,如果出现多语言文本,那几本就乱套了, 因此Unicode应运而生,Unicode只是个标准,在1.0规范中,当时的人们觉得用2个字节实现肯定够了,所以就定下了16bit的编码(微软是最早采纳Unicode标准的),这就是UTF-16编码。随着计算机的发展,发现2个字节还不够,而且用UTF-16编码的时候,单字节也要用16bit,浪费了空间,所以诞生了UTF-8(1 到 4 字节的变长编码),这样灵活多了,完全可以兼容 ASCII编码,英语字母就用单字节,汉字就用3-4个字节。随着Unicode标椎的不断编码。目前 一共可以编码 1,114,112 个字符。
字符 ASCII Unicode UTF-8
A 01000001 00000000 01000001 01000001
无法编码 01001110 00101101 11100100 10111000 10101101

4.2 BOM的作用:Unicode实现的标准太多,为了区分,通常在Unicode 文本文件头加一个字符 U+FEFF,这个字符就是 BOM(byte order mark)字符的约定,根据这个字符区分是UTF-32 编码,还是UTF-16等。

  • 主流操作系统上的编码:
    Unix(包括 Linux 和 macOS 在内),已经全面转向UTF-8;
    Windows系统比较特殊,英文 Windows 上是 Windows-1252,简体中文 Windows 上是 GBK,用 wchar_t 表示 UTF-16,所以经常会有编码转换,为什么会这样?因为Windows历史包袱太重,一直在做向后兼容。

打卡Day12:编译期多态:泛型编程和模板入门

如果有Java基础,这一章节是比较好理解的。

  • 面向对象是Java语言的一大特点,同样C++语言也一样。做个比对,在JAVA中的多态有集中实现方式:1.是重写父类的方法;2.是重载方法;3.面向接口编程;C++完全可以实现上述多态思想(作者吧这种多态形式叫“动态”多态,其实就是在执行时蔡)
  • 接着上面的说,还有一种多态形式,就是利用模版实现(我个人了理解为Java中的泛型),作者叫"静态"多态。
#include 
using namespace std;
//定义函数模版
template 
E my_gcd(E a, E b) {
  while (b != E(0)) {
    E r = a % b;
    a = b;
    b = r;
  }
  return a;
}
int main() {
  //如果使用cl_I类型(开源库定义的cl_I
  //高精度整数类型,不支持求余运算),就会报错 因此可以重载my_gcd方法
  cout << my_gcd<>(3, 4) << endl;
}
  • C++中的特化,类似与Java中的重载,只不过Java中只有函数重载,对于类的重载就叫特化;
  • 下面是作者对C++中的多态理解,其实和我第一条讲的Java中的多态类似。

我前面描述了面向对象的“动态”多态,也描述了 C++ 里基于泛型编程的“静态”多态。需要看到的是,两者解决的实际上是不太一样的问题。“动态”多态解决的是运行时的行为变化——就如我前面提到的,选择了一个形状之后,再选择在某个地方绘制这个形状——这个是无法在编译时确定的。“静态”多态或者“泛型”——解决的是很不同的问题,让适用于不同类型的“同构”算法可以用同一套代码来实现,实际上强调的是对代码的复用。C++ 里提供了很多标准算法,都一样只作出了基本的约定,然后对任何满足约定的类型都可以工作

打卡Day13:编译期能做些什么?一个完整的计算世界

  • 模版元编程:C++是一门静态语言,需要编译成可执行文件,代码在运行期执行,模版元编程是一门黑科技,让代码在编译的时候可以在执行,这个技术C++竟然默认支持(据说是C++设计是的漏洞),想想Java也可以通过插桩的方式,在编译器修改或者生成代码,但是语言自身肯定是不支持的。
  • 上面说的模版元编程虽然C++语言默认支持,但是要符合一些条件,只能操作元数据(C++编译器在编译期可以操作的数据)和元函数(实际上表现为C++的一个类、模板类或模板函数)。元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等。
int factorial(int n) 
{
    //控制语句元函数不能使用
    if (n == 0)
       return 1;
    return n * factorial(n - 1);
}

void foo()
{ 
    //运行时执行
    int x = factorial(4); // == (4 * 3 * 2 * 1 * 1) == 24
    int y = factorial(0); // == 0! == 1
}

模版元编程

#include 
template 
struct Factorial {
  //枚举是元编程可以使用的数据
  enum { value = N * Factorial::value };
};
template <>
struct Factorial<0> {
  enum { value = 1 };
};
// Factorial<4>::value == 24
// Factorial<0>::value == 1
void foo() {
  //编译时执行
  int x = Factorial<4>::value;  // == 24
  std::cout << x << std::endl;
  int y = Factorial<0>::value;  // == 1
  std::cout << y << std::endl;
}
int main() { foo(); }

打卡Day14 SFINAE:不是错误的替换失败是怎么回事?

这一讲的内容比较晦涩,后面还要在看几遍

  • 函数模板的重载决议:我理解这个概念是指有函数重载(特化)的情况下,根据实参类型去匹配形参,匹配失败的话,不会报错,继续匹配下一个函数,如果最后没有找到的话,编译器会报错;
#include 
struct Test {
  typedef int foo;
};
template 
void f(typename T::foo) {
  puts("1");
}
template 
void f(T) {
  puts("2");
}

int main() {
  f(10);  //两个重载函数f(Test::foo) 和 f(Test),实际匹配到f(Test::foo)
  f(10);  //两个重载函数f(int::foo) 和 f(int),实际匹配f(int)
}
  • SFINAE 模板技巧:
    enable_if decltype 返回值 void_t 标签分发
    这几个概念在实际场景中在理解;

下面是一段其他同学的学习留言:

我自己在写数据序列化为json文本的时候,就遇到了这样头疼的问题:如何根据类型,去调用对应的函数。
如果是简单的int,bool,float,直接特化就好了。
如果是自定义的结构体呢?我的做法就是判断自定义结构体中是否有serializable和deserializable函数,就用到了文中最开始的方法判断。
然而那会儿我写得还是太简单粗暴,在代码中用的是if去判断,对于不支持的类型,直接报错,并不能做到忽略。

打卡Day15 constexpr:一个常态的世界

  • constexpr 和 const区别
    基本相同,不同的点constexpr是一个编译期常量, const是一个
    运行时常量。
    如果是编译期常量,可以改造第13讲的阶乘函数,实现编译期计算。
constexpr int factorial(int n)
{
  if (n == 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}
int main()
{
  constexpr int n = factorial(10);
  printf("%d\n", n);
}

打卡Day16 函数对象和lambda:进入函数式编程

这一讲的内容和Java语言中的内容类似;

  • 函数对象:定义了函数调用运算符"()"的类对象,参考下面代码。
  • 函数指针:传递给一个函数实参,被称为函数指针,Java8以后也有这样的
  • Lambda 表达式:以一对中括号开始的匿名函数;每个 lambda 表达式都有一个全局唯一的类型,要精确捕捉 lambda 表达式到一个变量中,只能通过 auto 声明的方式
#include   // std::array
#include 
#include   // std::cout/endl
#include    // std::accumulate

using namespace std;
struct adder {
  adder(int n) : n_(n) { std::cout << n << std::endl; }
  //定义函数调用运算符
  int operator()(int x) const {
    std::cout << x << std::endl;
    return x + n_;
  }

 private:
  int n_;
};

int add_2(int x) {
  std::cout << x << std::endl;
  return x + 2;
};

template 
auto test1(T fn) {
  return fn(2);
}

int main() {
  //定义了函数调用运算符的类对象称为函数对象
  auto add = adder(1);
  add(2);
  //传递给一个函数的实参称为函数指针或者引用
  test1(add_2);
  // Lambda 表达式(以一对中括号开始)
  auto add_3 = [](int x) {
    std::cout << x << std::endl;
    return x + 2;
  };
  add_3(3);
  //泛型Lambda 表达式
  array ary{1, 2, 3, 4, 5};
  auto s = accumulate(ary.begin(), ary.end(), 0,
                      [](auto x, auto y) { return x + y; });
  cout << s << endl;
}

打卡Day17 函数式编程:一种越来越流行的编程范式

个人对于函数式编程接触的比较多,Java8的Stream Api,RxJava的操作算子,还有搞大数据的时候,接触到的Hadoop MapReduce和Spark的各种操作算子都是经典的函数式编程。举个经典例子统计一个文本中的
例如Map将一个对象映射为另一个对象,reduce将映射后归为一个整体。参考代码

  • 其实我觉得函数式编程是更高层次的抽象,实现数学意思上的纯函数,这样函数式编程最大的特点是无状态(想想Java中多线程中状态变化引起的各种问题,当然Java8也支持函数式编程,目前大家对这种编程思想使用的还是太少),这样特别适合并发编程。因此Hadoop作为最早的分布式和并行计算平台, 最先采用了这种思想。这里提一下并行和并发的区别,并发关心的是多个线程或者多个进程同时去操作一个内存或者CPU,而并行是多个CPU同时进行工作,例如现在的CPU基本都是8核,更需要充分利用起来。
  • C++中的高阶函数或者操作算子
    transform类似于Map
    accumulate类似于Reduce

打卡Day18 应用可变模板和tuple的编译期技巧

  • 可变参数模版,意思很明确可以定义任意数量和类型的实参。如下
#include 
void tprintf(const char* format) // 基础函数
{
    std::cout << format;
}

//第一个实参和剩余的实参分离开来,然后对于剩余的实参递归
template
void tprintf(const char* format, T value, Targs... Fargs) // 递归变参函数
{
    for ( ; *format != '\0'; format++ ) {
        if ( *format == '%' ) {
           std::cout << value;
           tprintf(format+1, Fargs...); // 递归调用
           return;
        }
        std::cout << *format;
    }
} 
int main()
{
    tprintf("% world% %\n","Hello",'!',123);
    return 0;
}
  • tuple 一个特殊的容器,这个容器中可以存储不同类型的元素,这个功能好强,Java中没有这中类型的容器。
    定义:tuple 的成员数量由尖括号里写的类型数量决定。
    读写数据:可以使用 get 函数对 tuple 的内容进行读和写。
#include 
#include 
#include 
#include 
 
std::tuple get_student(int id)
{
    if (id == 0) return std::make_tuple(3.8, 'A', "Lisa Simpson");
    if (id == 1) return std::make_tuple(2.9, 'C', "Milhouse Van Houten");
    if (id == 2) return std::make_tuple(1.7, 'D', "Ralph Wiggum");
    throw std::invalid_argument("id");
}
 
int main()
{
    //初始化tuple对象
    auto student0 = get_student(0);
    //通过get读取数据
    std::cout << "ID: 0, "
              << "GPA: " << std::get<0>(student0) << ", "
              << "grade: " << std::get<1>(student0) << ", "
              << "name: " << std::get<2>(student0) << '\n';

}

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