《C++Primer》第十五章:面向对象程序设计

  1. 面向对象程序设计的核心思想是数据抽象、继承和动态绑定。使用数据抽象,我们将类的接口和实现分离;使用继承,可以对相似的关系进行建模;使用动态绑定,可以在一定程度上忽略相似类的区别,并以统一的方式使用它们的对象,即在运行时选择执行函数的版本。
  2. 先简单的记住一个原则,作为继承关系中的根节点的基类通常都会定义一个虚析构函数,即使该函数不执行任何代码。
  3. 需要动态绑定的函数被写成虚函数,虚函数不是一定要被重写的。任何构造函数之外的非静态函数都可以是虚函数。成员函数如果没有被声明为虚函数,则其解析过程发生在编译时而非运行时。在派生类中覆盖虚函数时,可以再次使用virtual关键字,但这不是必须的,因为一旦某个函数被声明为虚函数,则在所有派生类中他都是虚函数。
  4. 派生类可以访问公有成员而不能访问私有成员,可以使用保护成员给派生类开放访问权限。
  5. 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,对于每个静态成员来说,都只存在唯一的实例。

Read More

《C++Primer》第十四章:重载运算和类型转换

  1. 逻辑与、逻辑或、逗号运算符的运算对象求值顺序不能保留,&&和||符号的短路属性也会消失,因此不建议重载他们,这会使得重载后的对象和我们平常熟悉的计算规则不同。另外取地址也不建议被重载。
  2. 重载输出运算符尽量不要控制格式,比如不应该打印换行符。
  3. 输入输出运算符一定是非成员函数,如果是成员函数那么第一个参数会默认是对象自身,然而输入输出运算符要求第一个参数是流对象的引用。由于需要读取对象成员,因此也应该是友元,除非只打印公有成员。
  4. 通常情况下把算术和关系运算符定义成非成员函数以允许对左侧或右侧的对象进行转换,另外不需要改变他们所以形参都是常量引用。
  5. 定义算术运算符后通常也要定义对应的符合赋值符,最好的方法就是使用符合赋值来定义算术运算符。同理,相等运算符和不等运算符也应该一起实现,具体的工作写在其中一个里面就可以。
  6. 定义关系比较运算符比如大于小于时,要小心和等于运算符的兼容性,比如a<b时,一定要a!=b,当a!=b时,大于小于比较一定要有一个为真。
  7. 下标运算的返回值通常是元素的引用,这使得下标的返回值的改变可以体现在原对象上,即可以成为左值。另外下边运算通常有常量和非常量两种版本的重载,这使得常量对象使用下表操作时不会改变自身。
  8. 如果类定义了调用运算符,则该类的对象被称为函数对象,函数对象常常作为泛型算法的实参。而我们普遍的做法是在泛型算法中传入一个lambda表达式,实际上编译器将lambda表达式翻译为一个未命名类的未命名对象,该对象有重载的调用运算符。该调用运算符是const的,除非加上mutable修饰lambda表达式。
  9. 没有捕获列表的lambda表达式形成的匿名类中没有数据成员,只需要一个重载的调用运算符就可以。通过引用捕获变量时也是这样。但是如果有值捕获的捕获列表,实际上该类就可以理解为有私有的数据成员,并且有对应的构造函数。但注意这只是一种理解方式,实际上lambda表达式产生的类不含有默认构造函数、赋值运算符以及默认析构函数。
  10. 标准库定义了一系列函数对象,在functional头文件中,通过模板的方式实现。(类似于python中的operator模块,里面也有一大堆add/mul之类的可调用对象。)比如我们希望按照字典序排序字符串,可以在sort的第三个参数中传入greater<string>。甚至这里面的函数对象可以对指针操作,比如less<string*>可以比较指针的内存地址,而如果是我们手动比较两个无关指针,将会产生未定义的行为。

《C++Primer》第十三章:拷贝控制

  1. 对象的拷贝、移动、赋值、销毁通过五种特殊的成员函数来控制。包括:拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数、析构函数。
  2. 当使用花括号列表的时候,会调用拷贝初始化,使用标准库容器的insert或者push成员的时候会调用拷贝初始化,非引用的参数传递与返回值需要拷贝初始化。需要注意的是,emplace创建的元素是直接初始化的。
  3. 拷贝构造函数的参数必须是引用类型,否则拷贝构造函数自身在传参的时候又要调用一次拷贝构造函数,就会一直调用下去。
  4. 有时候对一些对象使用=进行赋值但是没有重载相应的=运算符时也没有问题,这通常是由于进行了隐式类型转换并调用了相应的构造函数。使用explicit修饰构造函数阻止这种隐式调用。(explicit关键字只能出现在类内。)
  5. 上面提到的这种隐式类型转换,编译器只会自动执行一次类型转换步骤,而不能重复进行多次(P264)。
  6. 编译器可能自动的跳过拷贝构造函数,直接调用普通构造函数创建对象。
  7. 重载运算符本质上是函数,赋值运算符本质就是名为operator=的函数。合成拷贝运算符会将右侧每一个非static成员赋予左侧的相应成员。
  8. 析构函数执行与构造函数相反的操作,释放资源,销毁非static数据成员。不接收参数,因此没有重载。
  9. 隐式销毁一个内置指针类型的成员不会delete它所指向的对象,但是如果使用智能指针,由于是智能指针是类对象,那么在析构截断智能指针成员会被自动销毁。
  10. 合成析构函数默认为空。
  11. 五三原则:三个基本拷贝相关方法(拷贝构造函数、拷贝赋值运算符、析构函数)和两个新标准中的方法(移动构造函数、移动赋值运算符)。我们不需要定义所有这些操作,但是我们应该把这些操作看成一个整体。
  12. 如果需要自定义析构函数,那么往往也需要自定义拷贝构造函数(自定义析构函数往往由于指针,那么默认的拷贝构造函数会导致两个对象的数据成员指针指向一个地方)。
  13. 如果需要自定义拷贝操作,那么往往也需要自定义赋值运算符。反之亦然。
  14. =default使得这些操作为合成的版本,使用=delete使得上述这些操作被声明但是不能执行。
  15. 当编写赋值运算符的时候,要考虑到自己赋值给自己的这种特殊情况,通常赋值运算符会涉及到构造和析构各自的一部分工作,使用一个临时变量链接这两部分会更加鲁棒而清晰。
  16. 还应该重载swap方法,并声明为友元函数,很多基于交换的算法需要调用该方法。通常默认的swap已经足够了,会对于内置的各个数据成员调用swap,但是对于动态分配资源的类,定义swap是一种重要的优化性能的手段。
  17. 可以在赋值运算符中使用swap,这是一个小trick。此时的赋值运算符是异常安全的且可以处理自赋值的情况。
  18. 关于移动构造函数:如果使用allocator<string>.construct(p, 'asd'),这个是调用拷贝构造函数来构造p指向位置的字符串对象,而如果allocator<string>.construct(p, std::move(s)),则会使用标准库的移动构造函数,将s移动到p所在位置,此时内存不会被拷贝,而是接管内存的所有权。

《C++Primer》第十二章:动态内存

  1. 到目前为止,所有的对象都有严格定义的生命周期。全局对象在程序启动时分配,在程序销毁时结束。局部自动对象,进入其所在的程序块时,离开时销毁。局部static对象在第一次使用前分配,程序结束时销毁。而动态对象就需要被显式释放的时候才会被销毁。
  2. 静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。这两种内存中的对象由编译器自动创建和销毁。除了这两种内存,每个程序还拥有一个内存池,这部分内存就是堆,用于存储动态分配的对象,即程序运行时分配的对象。
  3. shared_ptr:当拷贝一个shared_ptr时计数器都会递增,比如拷贝构造、作为函数参数、作为函数返回值时,其引用计数会增加。当给shared_ptr赋予新值或是被销毁(例如离开作用域),计数器会递减。通常使用make_shared来创建一个共享指针,该函数需要一个类型模板,且传入的参数一定要与类型的构造函数匹配。
  4. 单独使用一个智能指针我们可能不会忘记释放内存,容器出错的是复杂的情况。比如当用一个vector保存智能指针的时候,然后用一个指针不会被用到,那么就要erase掉他们。
  5. 使用动态内存的常见原因:(1)程序不知道需要使用多少对象。(2)程序不知道对象的准确类型。(3)程序需要在多个对象之间共享数据。

Read More

LeetCode刷题笔记——Greedy

55. Jump Game

没有get到思路,一开始使用动态规划的思路解决,这就是O(n^2)的时间复杂度,从后往前走,判断每一个格子是否可以到达末尾。
然后看了最快的思路的解法:

1
2
3
4
5
6
7
8
9
10
11
public class Solution {
public boolean canJump(int[] nums) {
int lastPos = nums.length - 1;
for (int i = nums.length - 1; i >= 0; i--) {
if (i + nums[i] >= lastPos) {
lastPos = i;
}
}
return lastPos == 0;
}
}

这个是O(n)的解法。lastpos表示上一次可以到达末尾的goodpoint,如果自己能jump到这个最近的goodpoint,那么自己也是goodpoint了。然后结束之后只需要判断最近的goodpoint是不是0就可以。

Read More

本站总访问量