Learning Cpp

| 0 | 总字数 2.5k | 期望阅读时间 10 min
  1. 1. Basic
  2. 2. Class
  3. 3. Generic
  4. 4. Memory
  5. 5. Class, again
  6. 6. Template

From C++ Primer (5th edition)

From OI to Engineering

Basic

  • 引用 == 别名。
  • 引用的初值必须是一个同类型对象
  • 顶层 const:指针本身是常量,int *const a = &i;
  • 底层 const:指针指向的内容是常量,const int *p2 = &c1;

按照经验来说,复合类型的判断通过打括号来进行。如 (int*) (const a = &i); 最靠近其的符号即其具体内容,”a 本身是个常量“。后者最靠近其的为*,说明其是个指针。

const expression 的定义为,需要在编译时就能计算得到结果的表达式。一旦你认定一个值为常量表达式,使用 constexpre 申明即可(这样是不是能够编译时计算圆周率)

在 type alias 中,可以使用 typedef 来创建类型别名。比如 typedef double base, *p;,在新方法中,可以使用 using db = double;

除了 auto,还有 decltype 能用于进行类型推断。decltype(f()) 甚至可以推断函数返回值的类型。decltype((var)) 的结果永远是引用

int b = 1, _b = 2;
int *c = &b;
decltype((c)) d = c;
cout << *d << endl;
d = &_b;
cout << *c << endl;

输出是 1 2。若要直接写指针引用,则

int *(&d) = c;

这种时候为了理解,还是打括号。(int*) (&d) = c;

再叠加一层,声明顶层 const 附带的指针引用呢,算了算了(((

头文件中不应该包含 using,以免引发冲突。

返回数组指针的函数,int *func(int i)[10];,或者写作 auto func(int i) -> int(*)[10];

预处理功能中,assert(expr) 能够对表达式求值,若表达式非零则继续,否则停止。预处理器无需使用 std::assert(expr)

使用 NDEBUG

#ifndef NDEBUG
        std::cerr << __func__ << std::endl;
#endif

若要脱离调试阶段,只需 #define NDEBUG 即可,即 not debug.

类似的预处理器定义的名字有

  • __FILE__ 文件名
  • __LINE__ 存储当前行号
  • __TIME__ 存放文件编译时间
  • __DATE__ 存放编译日期

Class

函数指针,int (*pt)(const int &, const int &);,而将函数名作为一个值使用的时候,该函数名就被当成指针。比如,pt = apb,写成 pt = &apb 也行。

const 成员函数,即常量成员函数。具体的写法为,valtype func(int, int) const { ... },主要用于限制 this 指针的行为。类内部可以忽略 this,但是为了内容清晰,之后的写法还是加上 this->... 比较好。decltype(this) 的结果是 ClassType *const,即顶层 const,而顶层 const 是允许修改其指向对象的值的。添加 const 以后,this 的类型变为了 const ClassType *const,即同时满足顶层 const 与底层 const。本身不能被修改,也不能修改其成员函数。

友元函数,声明位于类最前或最后,意在为非成员函数提供访问 private 的途径。不光如此,还可以为某一个类的成员提供访问权限。但是啊,声明顺序的问题就出现了。假设我们由 A 给 B 提供友元,即 B 要操作 A 的 private 成员。顺序必须为

  1. 声明 A,但是不定义 A 的成员函数(因为这时候还没有进行友元声明)
  2. 在 B 的内容中给 A 添加友元
  3. 定义 A 中的成员函数

而友元函数真正出现在类的定义域时,是友元函数被定义的时候。所以才建议将类的成员函数分开写。

默认构造函数在 C++ 11 中的写法为 ClassName() = default;

通过添加 mutable,可以设置可变数据成员,即该对象可以被 const 对象修改。

一个类中,若存在常量成员变量或引用的成员变量时,需要在类的内部添加构造函数并且通过构造函数初始值列表为这些成员提供初始值。

需要注意,构造函数的初始值最好按照声明变量的顺序进行。否则可能会出现未定义的情况。

委托构造函数:在多种构造函数的情况下,委托构造函数能够复用之前构造函数的内容以进行构造。

构造函数偶尔会出现隐式转换,若要抑制隐式转换,使用 explicit

#include <iostream>
#include <memory>

using std::make_shared;
using std::shared_ptr;
using std::string;

class Tmp {
public:
    Tmp() = default; $qwq$
    Tmp(const char *&st) : s(st) { }
    void print() const {
        std::cout << this->s << std::endl;
    }

private:
    string s;
};

int main() {
    Tmp a = "qwq";
    a.print();
}

若将 line 11 加上 explict,该编译不通过。因为 line 21 出现了隐式类型转换。

类的静态成员,即与类本身直接相关的成员。静态成员可以是 private 或者 public 的,只需要加上前缀 static 即可。既可以用作用域运算符来访问,也可以从类的对象引用或者指针来访问。

Generic

泛型算法依赖于迭代器,而迭代器通常不会修改容器本身,于是我们可以知道,泛型算法不会进行容器的操作。

比如这份代码

void eraseDuplicated(std::vector<std::string> &words) {
    std::sort(words.begin(), words.end());
    auto end_unique = std::unique(words.begin(), words.end());
    words.erase(end_unique, words.end());
}

有些时候,会遇到需要重载 sort 行为的情况,标准库接受一元谓词和二元谓词。stable_sort 表示,若无法判断大小,则保留原有的顺序。

lambda 表达式可以通过
$$
[\text{capture list}](\text{parameter list}) \rightarrow \text{return type} { \ \text{function body} \ }
$$
定义。可以忽略 parameter list 和 return type,但是必须保留 capture list 和 function body

int x = 3;
// Capture the parameters
auto f2 = [x](const int &i) { return (i > x); };
for (int i = 1; i <= 5; ++i)
    cout << f2(i) << " ";
cout << endl;

这么说,lambda 表达式是不是可以实现作用域的穿透。(从上向下的穿透)

而捕获分为几种,值捕获,引用捕获,等。

来个骚操作,假如,我是说假如哦,如果我想让 sort 函数输出排序的具体过程。

std::ostream& os = cout;
std::vector<int> lst{ 5, 4, 3, 2, 1 };
std::sort(lst.begin(), lst.end(), [&os](const int &a, const int &b) {
        os << "Comparing:" << a << " " << b << endl;
        return a <= b;
});

若要让编译器进行自动的捕获,parameter list 写成 &= 即可。前者是引用捕获,后者是值捕获。

若要让编译器进行返回值类型推断,必须使用尾置返回类型。

标准库的 bind 函数,可以 auto f = bind([](const int &a, const int &b) { return a + b; }, 1, 2),则 f() 得到 3。类似于提供了参数默认值吧。感觉这操作有种人体改造的感觉。

之前实现的捕获,也可以用 bind 来复现,不过没多大意思了。顺带,_1_2 可以被当作占位符使用。这玩意儿居然是 std::placeholders::_n,忽然觉得有点神奇。

ref 函数可以返回一个引用,而 cref 函数可以返回一个常量引用。

Memory

我每次在 C++ 中看到 Memory 都会觉得莫名浪漫

#include <memory>

智能指针 shared_ptr<T> sp 中,sp.get() 表示返回被管理的原指针,不要轻易使用。

如果混用 shared_ptr 和普通指针,会导致引用计数的实际值与期望值不符合。

理论上来说,unique_ptr 是不能被拷贝的。但是拷贝并不只有 = 的拷贝等等,从一个函数返回时,unique_ptr 是能够被拷贝的,因为原对象立马就会被销毁。

指向数组的 unique_ptr 不支持成员访问运算符(?)

而若要使用 shared_ptr 来管理数组,需要提供删除器,即 [] (int *p) { delete [] p; } 若不提供删除器,则会出现只 delete 某一个元素的问题。

Class, again

合成拷贝构造函数,编译器为我们定义的通过拷贝进行构造的函数,会拷贝除了 static 成员之外的所有成员到正在创建的对象中。

这种操作让我感到了一丝不合缝的优雅,虽然对成员的类型进行不同方式的定义或许有必要,为了拷贝构造单独规范每种类型的拷贝方式也颇伤大雅了吧?如果将拷贝构造提升到一种更高的逻辑层次或许可以解释这种不和谐。或许是我的理解有些问题,就当我碎碎念了。

拷贝初始化和直接初始化的差距已经体现了出来,直接初始化采用普通的函数匹配。而(先不忙)

若拷贝构造函数初始化为 ClassName::ClassName(ClassName c); 而非 ClassName::ClassName(const ClassName &c); 会出现怎样的问题?

在拷贝时(调用拷贝构造函数),被拷贝对象被拷贝进 c 中,这个过程需要调用拷贝构造函数,而拷贝构造函数又会拷贝构造函数,如此往复。若使用常量引用,第一次的拷贝构造就会被停下来。

析构函数,指在类中的

~ClassName();

表示如何移除类所占用的空间。

虚函数,virtual,用于在派生类的类中重新定义这个函数而存在。在派生的类里,需要加上 override 来表明这是一个派生的函数。

派生类的定义需要添加 public/protected/private 三个访问说明符之一。class ClassName : public ClassName2 { ... },这个说明符表示基类中的成员是否对派生类用户可见。

Template

具体来说,模板函数写成这样

template<typename T>
T max(const T &a, const T &b) {
    return a > b ? a : b;
}

...

cout << max(1, 2) << endl;
cout << max(1.0, 2.0) << endl;

这样写就能应付不同的数据类型。

再看一个

template<typename T, typename U>
T max(const T &a, const U &b) {
    return a > b ? a : b;
}

...

cout << max(1, 2.0) << endl;

其实 typenameclass 是基本一样的,都能完成对类型的匹配。

但是,若这个类型没有同时定义 >< 呢,