读书笔记(三十) 《C++ Primer》#4

已发布在微信公众号上,点击跳转

背景:

我为什么要重学C++?第一是巩固核心,软件编程有三大核心,语言、操作系统、框架设计,语言首当其冲,核心能力大都看不见摸不着只有你自己知道的东西。第二是将分散的知识串联起来,这样就能产生新的知识和新的创意。第三是当我们在巩固时创造出了新的知识,那么这些新的知识和旧的知识将同时变成智慧融合到身体中。

本系列基于《C++ Primer》学习,由于重点放在“重学”,我略过了滚瓜烂熟的部分,挑出以前常忽略的部分,以及记忆没有那么深刻的部分,特别是那些重要的但没有上心的部分。

正文

类相关定义

注意,this指针是常量指针,因为我们不允许改变this指针中保存的地址。

当我们定义类相关的非成员函数时,即如果函数在概念上属于类但不定义在类中,则它一般应与类声明在同一个头文件中。 这种方式下,当用户使用接口的任何部分都只需要引用一个文件,举例:

//testClass.h

//类声明
class testClass {
  //test class
  public:
    int testVar;
}

//类相关的非成员函数
int readFrom_testClass(const testClass & item);

默认合成的构造函数

编译器创建的默认构造函数称为,合成的默认构造函数(synthesized default constructor),它的规则如下:

  1. 如果存在类内的初始值,用它来初始化成员。
  2. 否则,默认初始化该成员。

合成的默认构造函数只适合非常简单的类,其他某些类不能依赖于合成默认构造函数,原因有三:

  1. 编译器只有在发现类不包含任何构造函数的情况下才替我们生成一个默认的构造函数。
  2. 类中含有内置类型或者复合类型成员的类应该在类的内部初始化这些成员,否则使用默认构造函数可能得到未定义值。
  3. 类中含有其他类型成员且成员类型没有默认构造函数,则默认构造函数无法初始化这个成员。

当然最好是我们自己定义每个类的构造函数而不是让编译器使用默认构造函数,如果你不能使用类内初始化值,则所有构造函数都应该显式地初始化每个内置类型的成员。

Class和Struct的区别

与C#、Java不同,C++中Class和Struct几乎一样,只是关键字不同和使用习惯不同而已。 唯一的微小区别就是,使用Class的默认访问模式为private,而struct的默认访问模式是public。

友元

友元允许其他类或函数访问自己的非公有成员,这个大家都知道就不多说了。 需要注意的是友元关系不存在传递性,也就是说子类的友元并不能理所当然的访问父类。 而且想要所有重载函数成为友元关系,必须为每个重载函数做一次友元声明。

不过在编译时,友元的声明仅仅指定了访问的权限,它只是标记编译的语法规则好让编译器在语法检查时能知道友元标记的存在,实际上编译器并没有做任何内存上和寄存器上的修改。

构造函数初始值列表

class testClass
{
  public:
      testClass(int ii);
  private:
      int i;
      const int ci;
      int &ri;
}

testClass::testClass(intii):
    i(ii),ci(ii),ri(i)
{
  //do something
}

我们在类中常使用以上这种构造函数来初始化成员,其实这就相当于成员的初始化顺序,即先初始化i,然后ci,然后ri。

隐式的类构造转换

在对象构造时,也会出现隐式的构造情况,例如:

class Test_data
{
   Test_data(const string str);
}

string test_str = "test";
Vector<Test_data> item;
item.push(test_str);

如果构造函数只接受一个实参,则实际上它同时也定义了转换为此类类型的隐式转换机制,我们把这种构造函数称为,转换构造函数(converting constructor)。

上面例子中传入的string对象从而引发了Test_data的转换构造函数,构造出对象后再传入,其实这些隐式转换和构造都是在编译时离线计算的,根本不会放到运行时去发生。

如果你不想让隐式构造发生,可以通过在构造函数前加 explicit 声明来阻止隐式转换,如下:

class Test_data
{
   explicit Test_data(const string str);
}

string test_str = "test";
Vector<Test_data> item;
item.push_back(test_str); // 编译错误

item.push_back(Test_data(test_str)); // 编译正确

声明了explicit的构造函数,只能以直接初始化的形式使用,编译器将不会再进行隐式的自动转换这些构造函数。

IO库

这里有些零散的知识需要注意下。

IO库定义了很多标记,这些标记可以帮助我们访问和操作流,例如由于流可能处于错误状态,我们在操作之前先去检查它是否处于良好状态。 其中badbit标记相对用的比较多,它表示系统级错误,例如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法再使用了。

IO流有缓冲机制,这可以提升IO的性能,当刷新时才将数据写入到文件中。

导致缓冲刷新的几种情况为:

  1. 程序正常结束,main函数return操作。
  2. 缓冲区满时,需要刷新缓冲。
  3. 使用操作符endl来显式刷新缓冲区。
  4. 使用操作符unitbuf设置流的内部状态,清空缓冲区。
  5. 一个输出流被关联到另一个流时,输出流会被刷新。

注意,如果程序崩溃了,输出流的缓冲区是不会被刷新的,这也是为什么崩溃时部分数据没有被写入文件的原因。

顺序容器

顺序容器指,vector,deque,list,array,string,它们都可以用迭代器访问元素。 如果我们不需要写访问元素,则应该使用cbegin和cend来获取常量迭代指针。

顺序容器中有一个方法比较有趣assign(array除外),它可以用参数所指定的元素(拷贝形式)替换容器中的所有元素。 seq.assign(b,e); // 将seq替换为迭代器b到e范围内的元素 seq.assign(il); // 将seq替换为容器列表il中的元素 seq.assign(il); // 将seq替换为n个t值的元素

注意,当我们用一个对象来初始化容器,或者将一个对象插入到容器中时,实际上放入到容器中的是对象的拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。

另外我们也要注意向容器中添加元素或删除元素的操作,可能会使得指向容器的指针、引用、迭代器失效,进而导致运行时错误。

泛型算法

标准库并没有给每个容器添加大量功能,而是提供了一组算法,这些算法大多是通用算法,可以独立于任何特定的容器。

归纳总结下来,泛型算法有4个特点:

  1. 大多是通用算法,可以独立于任何特定的容器
  2. 虽然独立于任何容器,但有些算法依赖于元素类型(例如累加算法依赖于带有加法运算的对象类型)。
  3. 不会直接操作容器,而是运行于迭代器之上,执行的是迭代器的操作。
  4. 算法可能改变容器中保存的值,或移动容器中的元素,但永远不会直接添加或者删除元素。

lambda函数

当定义一个lambda函数时,编译器生成一个与lambda对应的新(未命名的)类类型。

也就是说当向一个函数传递一个lambda函数时,编译器同时定义了一个新类型和该类型的一个对象,传递的参数就是此编译器生成的类类型的未命名对象,而不是传入的对象。

void func1()
{
  int v1 = 41;
  auto f = [v1] {return v1;}; // 值捕获
  v1 = 0;
  auto j = f(); // j 为 41

  auto f2 = [&v1] {return v1;} // 引用捕获
  v1 = 2;
  auto j = f2(); // j 为 2
}


transform(v1.begin(), vi.end(), vi.begin(), [](int i) -> int { if (i < 0) return -i; else return i; }); // lambda函数指定了int返回值和int形参

注意,如上代码中,如果lambda形参为引用则需要我们注意实参的生命周期,因为在lambda执行时,如果该对象已经被销毁则可能引起运行时错误。

除了使用lambda创建快速创建函数外,也可以使用标准库里bind函数来生成函数,例如:

// 格式: auto newCallable = bind(callable, arg_list);

void func1(int a, int b)
{
  return a + b;
}

int aa = 1;
int bb = 2;
auto newFunc1 = bind(func1, aa, bb); // 生成一个新的可调用对象来适配算法
auto wc = find_if(words.begin(), words.end(), newFunc1); // bind生成了一个适配泛型算法的函数

bind可以看做是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

已发布在微信公众号上,点击跳转

· 读书笔记, 前端技术

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    读书笔记(三十) 《C++ Primer》#4

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解

    QQ交流群: 777859752 (高级程序书友会)