Featured image of post OIer 速通编入门

OIer 速通编入门

这是一篇学习笔记

C 动态内存

malloc (字节数) // 返回 void* 类型,可以隐式与别的指针类型转换
calloc (数量, 单个元素字节数) // 全置 0
realloc (指针, 新的字节数) // 指针可以是先前申请的或 NULL,如果是申请的,相当于 free,malloc 并复制。
free (指针) // 释放

糖:int * p = calloc(10, sizeof * p);

对了,关于 C++ 的 new 有两点提一下,一个时 new int [1] 必须用 delete [] 因为 new 数组时会额外申请空间记录数组大小。另一个是 placement new

多文件编译

ODR

  1. In any translation unit, a template, type, function, or object can have no more than one definition. Some of these can have any number of declarations. A definition provides an instance.
  2. In the entire program, an object or non-inline function cannot have more than one definition; if an object or function is used, it must have exactly one definition. You can declare an object or function that is never used, in which case you don’t have to provide a definition. In no event can there be more than one definition.
  3. Some things, like types, templates, and extern inline functions, can be defined in more than one translation unit. For a given entity, each definition must have the same sequence of tokens. Non-extern objects and functions in different translation units are different entities, even if their names and types are the same.

头文件 ifndef

多文件编译的原理为:将 .h 中的内容加到每个 .cpp 中(preprocess),然后编译这些 .cpp,然后把他们链接起来。

.h 里的 #ifndef 那些东西是为了避免同一个 .cpp include 多次同一个 .h。例如

// a.cpp
#include "h1.h"
#include "h2.h"

// h1.h
#include "h.h"

// h2.h
#include "h.h"

// h.h
const int PI = 3.14;
class node {
	int a, b, c;
}

常量、类定义等内容不能在一个翻译单元中定义多次。

注意,这个技巧不能避免多个文件之间的重复定义,例如在 .h 里加了 int a; 之类的照样会造成 redefinition。

链接性

链接性分为外部、内部、无。外部链接性意味着作用域可以在多个 .cpp,前提是在定义所在的文件以外的文件中,要写 extern(并且不初始化)。内部链接性意味着作用域只在当前文件内,无链接性就是在代码块内。

全局变量、函数都是默认外部的,这就意味着它们不能在多个文件里或头文件里定义(但是函数可以多次声明,甚至在同一个文件里连续写两次 int f(int) ; 都没问题)。如果要变成内部就在开头加 static

头文件里可以定义什么

#define 肯定没什么问题。

const 比较特殊,它是内部链接的,所以可以写在头文件里。如果要强制它变成外部的,就加 extern

structclass 可以,但是成员函数不能在头文件里放在外面。类不能重复定义,但是可以在链接是出现重复,但是两个重复的不能不同,否则会出一些奇怪的情况。例如:

// a.cpp
#include "h.h"
struct node {
    int a, b, c;
}

// b.cpp
#include "h.h"
struct node {
    int a, b, d;
}

// h.h
struct node ;

这样不会 CE,但是会出现一些诡异的问题,比如一个文件里得按另一个的名字来。

类定义不能在外部链接,也就是说不能在另一个文件里写 extern node x; 来定义一个 node(在另一个文件里定义的类)的实例。

CMake

cmake 目录 指令会对目录中的文件(含 CMakeLists.txt、源文件)生成 build system。这一步还没有编译,可以理解为生成了以 Makefile 为代表的一系列文件。

如果要进一步指定目录,-B 可指定输出目录,-S 可指定输入目录。如果没有这个目录,直接 cmake 目录 会报错,但 cmake -B 目录 则会自动创建一个。

如果 cmake 报错

  Running

   'nmake' '-?'

  failed with:

   系统找不到指定的文件。

就在 cmake 后面加 -G "MinGW Makefiles"。或者直接改环境变量:setx CMAKE_GENERATOR "MinGW Makefiles" /m(需要管理员模式运行 Powershell,或者直接图形界面改也行)。

注意一旦 cmake 失败一次,就需要删掉所有相关文件再重新 cmake,不然会傻傻地仍然报错,或提示已经 build 过。

然后正式编译,使用 make [-C 目录](不写 -C 只能 make 当前目录)或 cmake --build [目录]

以后改代码,只需重新 make 就行。

一般习惯把 build system 的部分放到子文件夹里。汇总:

mkdir build 
cd build
cmake ..
make
./对应名字

参考:

https://wshibin.github.io/misc/cmake/

https://cmake.org/cmake/help/latest/guide/tutorial/index.html

https://www.bilibili.com/video/BV14h41187FZ/

vcpkg

vs 或 wsl,请。

基本结构、重载运算符、后置 const、构造析构略。

C struct

struct node {
    int a, b, c;
} x1;
struct node x2;
struct node x3 = {1, 2, 3};
typedef struct _ {
	int a, b, c;
} node;
node x1, x2;

这里 _ 可省,直接写成匿名结构体。

静态成员变量和函数

顾名思义,加 static,就是与类关联而不是与某个对象关联。注意 C++11 之前,类里是不能 const 的,只能 static const,或者 enum

成员函数

成员函数如果传同类参,可以访问对方的 private 成员。

类内定义的成员函数是自动内联的。

成员函数可以类外定义,必须再类内先声明,定义时要加 ::

class A {
    int a, b;
    int f() ;
}
int A :: f() {
    return a + b;
}

众所周知,在成员函数头后加 const 可以保证成员不被改变。但是它还能用于重载,拓宽了可使用情景:

struct node {
	int a;
	void f(int x) { cerr << "non-const\n"; a = x; }
	void f(int x) const { cerr << "const\n"; }
} ;
int main() {
	node t; t. f(1);
	const node s {1}; s. f(1);
}

这段代码会输出 non-const\nconst\n

友元

友元函数是一种非成员函数,其声明必须在类内,定义可以在类内或类外,但在类内时不能直接使用成员变量。其关键作用是能在类外访问私有成员。显然这种东西不可能是私有的。

class A {
    int a, b;
    friend ostream & operator << (ostream &, const A &) ;
} ;
ostream & operator << (ostream &out, const A & x) {
    out << x. a << '/' << x. b << endl;
    return out;
}

注意声明时参列里 & 不能少。

友元类是在类定义中写 friend class 另一个类的名字,然后另一个类就可以访问这个类的私有成员了。

class A {
    int a, b;
    friend class B;
} ;
class B {
    int sa, sb;
    void add(const A &x) {
        sa += x. a, sb += x. b;
    }
} ;

如果只希望 B 的某个函数可以访问 A 的私有成员(以限制 B 不在别的地方瞎搞),可以用友元成员函数:

class A ;
class B {
    int sa, sb;
	public: // 必须要有!
        void add(const A &) ;
} ;
class A {
	int a, b;
    friend void B :: add(const A &) ;
} ;
void B :: add(const A & x) {
    sa += x. a, sb += x. b;
}

我们来仔细考虑一下:

  1. A 定义 → B 定义:A 里根本写不了 B
  2. B 定义 → A 定义:B 里根本写不了 A
  3. A 声明 → B 定义 → A 定义:B 定义里要用 A 的成员,但 A 没定义。
  4. B 声明 → A 定义 → B 定义:A 要写 friend void B :: ...B 是得定义完的。

所以最后只能 ABAB

唐。

类继承

思想:两个类有共同特征,不想重复写这部分。

class 派生类名 : [public|protected|private] 基类名 { 定义派生类独有的内容 } ;

如果要继承多个,就用 , 分隔。注意 class C : public A, B 相当于 class C : public A, private B

基类中 private 的无法在派生类中访问,如果想访问但不希望被外面访问,就用 protected

继承方式\基类内权限 public protected private
public public protected inaccessible
protected protected protected inaccessible
private private private inaccessible

构造函数:推荐在派生类初始化列表中调用基类的构造函数,而不是直接赋值基类的元素。如果不写,则调用基类的默认构造函数。注意,唯一不行的是在初始化列表中初始化基类的元素,因为构造函数是先调用基类的构造函数,如果这样写就重复了。

如果不写构造函数,也可以用初始化列表的方式(C++ 17),注意列表对应顺序是先基类后派生类。

派生类无法使用基类的重载运算符和友元函数。

多态

派生类可以重载基类的变量和函数,如果要使用基类中的,就用 ::

多态可以展开讲的最大的点就是虚方法。考虑:

class A {
    public:
    	int a;
    	void print() { cout << "Value = " << a << endl; }
} ;
class B : public A {
    public:
    	int sum;
    	void rec() { sum += a; }
    	void print() { cout << "Sum = " << sum << endl; }
} b1, b2;
class C : public A {
    public:
    	int prod = 1;
    	void rec() { prod *= a; }
    	void print() { cout << "Product = " << prod << endl; }
} c1, c2;
int main() {
    // do something to b1, b2, c1, c2.
    A * List[4] {& b1, & b2, & c1, & c2}; // 指针和引用可以从派生类隐式转换到基类
    for (int i=0; i<4; i++)
        List[i] -> print();
}

显然他会输出四行 Value = ...,但是我们希望它能输出两行 Sum,两行 Product,又懒得一个个 . print()。那怎么办呢?

我们在基类的函数开头加一个 virtual(必须),然后在派生类的函数大括号前加 override(可选)。在多重继承时,会自动找到最深的重载。

如果强制一个类的派生类不能重载,就加 final

问题:如何使继承链中途不再虚?

问题:如果真的基类对象调用纯虚函数会怎样?

抽象基类是具有纯虚函数的类,纯虚函数是不给出定义的虚函数:

virtual void f() = 0;

这种情况下,不能建立该类的对象,且必须在(要有对象的)派生类中给出纯虚函数的定义。

析构函数是一类在类继承中往往必须要是虚函数的函数。考虑以下代码:

class A {
    public: // 析构函数必须是公有的或保护的,不然程序结束时都调用不了,直接 CE
        ~ A () { cout << 'a'; }
} ;
class B : public A {
    public:
        ~ B () { cout << 'b'; }
} b;

程序结束时会调用 b 的析构函数,先执行 ~B(),再执行 ~A(),输出 ba。这没啥问题。(注意:动态分配内存的对象在程序结束时不会调用析构函数)

但是如果 main 里有这样的代码:

A * p = new B;
delete p;

那就会导致只有基类的析构函数被调用。这在派生类里没有单独的动态内存时是没问题的(有静态变量是没问题的,也会跟着删掉),但是如果:

class B : public A {
    public:
        int * p;
        B () { p = new int [10] {}; }
        ~ B () { delete [] p; }
} ;

那就会造成内存泄露。所以如果存在基类指针指向派生类对象的情况,析构函数就必须要是虚的。

虚函数的多态的原理,是动态联编,大概就是每个对象都记录了一个函数指针表,用于查找它应该调用哪一级的重载的函数。因为类指针指向的对象类型可能取决于输入之类的,所以不得不边运行边确定,导致很慢。

虚继承

复杂的继承会出问题,比如:

class A { public: int a; } ;
class B : public A { public: int b; } ;
class C : public A { public: int c; } ;
class D : public B, public C { public: int d; } ;

一个 D 对象,没法直接访问 .a,必须指定是 .B::a 还是 .C::a,函数同理。

如果希望合并两个 a,需要把 BC 的继承方式改成虚继承 virtual public A,与此同时当 D 的构造函数递归调用 BC 的构造函数时,将会忽略对 A 的构造函数的递归调用,需要单独调用 A

但是重载的函数还是没法合并,所以最好 D 自己写一个,当然可以直接调用 B::C:: 的,这个的重复就无法避免了。

模板

最基础的略。

double res = max(2.5, 3); 不行,double res = max <double> (2.5, 3); 可以。

显式实例化:template 返回类型 函数名 <具体化类型> (参数列表) ; 表示生成一个对应的实例。

显式具体化:template <> 返回类型 函数名 (<具体化类型>) (参数列表) { ... } 其中第二个 <> 是可选的。和实例化不同的是,它相当于一种“内容不同的重载”,必须有函数定义,且出现在第一次调用之前(否则与隐式实例化冲突)。例如

template <> char * max <char *> (char * a, char * b) {
    return strcmp(a, b) > 0 ? a : b;
}

模板类:开头加一样的东西就行。类外定义函数: template <typename T> 返回类型 类名 <T> :: 函数头 { ... }

然后我们考虑一个例子:希望写一个 max,它能接受一个 int 和一个 double(之类的),并返回 double(选择级别高的类型)。如果这样:

template <typename T, typename U> (T? U?) max (T a, U b) {
    return a > b ? a : b;
}
int main() {
    cout << max(3.5, 3) << ' ' << max(3, 3.5);
}

这个就会有问题。可以用 C++14 的特性,自动推导返回类型:

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

STL 查漏补缺

遍历一些 STL 对象时,建议用 const_iterator,对应 . cbegin(). cend()。作用是无法修改内容。

++ itit ++ 快,考虑 it ++ 具体该怎么实现:

iterator operator ++ (int) {
    iterator tmp (* this);
    // do increment
    return tmp;
}

STL 提供了一个一般的 find,它是暴力,不要将它用于已经有自己的 find 的类。

移动语义和右值引用

左值是有地址的,可以赋值的变量。

右值的例子包括字面量、四则运算的结果、函数的非引用返回值等。

能否引用 非常量左值引用 常量左值引用 非常量右值引用 常量右值引用
非常量左值 × ×
常量左值 × × ×
右值 ×

目前不知道常量右值是什么,也不知道常量右值引用有啥用。

右值引用实际上是开了空间的。因此可以这样:int && p = 1; p = 2;

可以用 move(C++ 11)将左值转成右值。

目前看起来右值引用没啥用。在复杂的类的应用中,考虑提高这段代码的效率:

class Data {
	public:
		Data (int _size) : size (_size) { data = new int [size]; }
		Data (const Data & t) { size = t. size, data = new int [size], memcpy (data, t. data, size * sizeof (int)); }
		Data & operator = (const Data & t) {
			if (& t == this) return * this;
			delete [] data, size = t. size, data = new int [size], memcpy (data, t. data, size * sizeof (int));
			return * this;
		}
		~ Data () { delete [] data; }
	private:
		int size, * data;
} ;

注意必须重载复制构造函数和赋值,否则会导致多对一。

如果将一个对象复制给另一个对象,且这个对象不再使用(例如,这个对象是函数的返回值),那么暴力赋值就可以优化——我们希望直接把赋值者的内容转移给被赋值者,但是为了避免多对一析构爆炸,得把赋值者清空(delete nullptr 不会发生任何事)。这就必须要求我们能修改等号右边的东西,这就是右值引用在移动语义中的作用。我们可以新增一个“移动构造函数”:

Data (const Data && t) { size = t. size, data = t. data, t. size = 0, t. data = nullptr; }

移动赋值同理。如果要将 a 赋值给 ba 不再用了,写 b = move(a)

智能指针

我们现在希望偷懒不写 delete,于是类似上面,我们会写个指针类来自动 delete,但是与上面不同的是,指针类并不负责申请内存,也不负责复制内容,只负责自动释放空间。这就导致如果两个指针指向同一片内存就会爆。

头文件 memory。C++98 有 auto_ptr,略。C++11 有 unique_ptrshared_ptrunique_ptr 禁止复制(构造和赋值),只能 move 之后再赋;shared_ptr 会维护一个计数器,等最后一个相关智能指针对象析构时才 delete

用法 1:unique/shared_ptr <int> p (new int [100]);

不推荐用法 2:int * p = new int [100]; unique/shared_ptr <int> q (p); 会导致多对一。

用法 3:auto p (make_unique/shared <int> (100));

类型转换

C++ 的类型转换:static_cast <转换成的类型> (原变量)。在编译时会检查转换是否合法。

C 中,这样的代码是可以的(逆天):

const int a = 1;
int & b = (int &) a;
b = 2;

C++ 中,不能这样 static_cast <int &> 但是可以 const_cast <int &>。只有特殊情况需要修改时才用。

C 中,这样的代码是可以的:

struct node {
    int a, b;
} ;
int main() {
    int * p = new int [2];
    p[0] = 1, p[1] = 2;
    node * q = (node *) p;
}

C++ 中,不能这样 static_cast <node *> 但是可以 reinterpret_cast <node *>。总的来说,感觉就是避免你无意识地瞎转类型,但是你硬要转也是可以的,效果和 C 的一样,就是“棒转”,不做任何事情。

有且只有 dynamic_cast 只能用于指针和引用。dynamic_cast 可以把基类转换到派生类(static_cast 也可以做)。如果被转换者类型不对(包括把本身就指向基类对象的指针转成派生类),会返回 nullptrdynamic_cast 转换的必须是多态类型,即至少声明或继承(可以在本身 override)了一个虚函数的类。

异常

  1. 抛出异常 throw 信息
  2. try 开始代码块,里面可以用 throw
  3. 捕获异常 catch (信息类型 [变量名字]) { ... } 接在 try 的代码块后,可以接多个处理方式。catch (...) 通用。

可以跨函数处理(再抛出)。跨的函数有返回值的赋值也会跳过。

为了区分不同(大类的)异常情况,可以建立各种异常类。值得注意的是,如果抛出的类型是派生类,而捕获的类型中既有基类又有派生类,且前者写在前面,那就会被前者捕获。

STL 中的东西的异常都是 std :: exception 的派生类,可以调用 . what() 知道它是哪种。

优化原理

  1. 分支预测
  2. 循环展开
  3. 并行处理、SIMD
  4. 循环顺序

标准属性

发现有个 project 里有些 [[...]] 这种东西,加在声明或定义的类型名前面。这是起到修改编译器对一些特殊情况的反应的。包括:

  • [[nodiscard]] 加在函数声明前,如果该函数的返回值未被使用会报警告,可以通过 [[nodiscard("...")]] 加警告信息。
  • [[deprecated]] 表示对应东西可能被弃用,不鼓励使用(使用会警告),同上。
  • [[noreturn]] 表示对应函数不会正常返回(exitthrow 之类),提示编译器优化。
  • [[likely]][[unlikely]] 加在分支语句后,表示更可能进入哪个分支,提示编译器优化。

可变参数

https://zhuanlan.zhihu.com/p/694317432

例题

有个题感觉还是很 educational 的。如何实现一个高维数组,每维的长度都是 $n$,但维数 $d$ 不定?可以用模板类:

template <typename T, int n, int d> class Array {
    Array <T, n, d - 1> a[n];
    ...
} ;

但这里的问题是 $d=0$ 的时候爆了。可以用部分具体化:

template <typename T, int n> class Array <T, n, 0> {
    T a;
    ...
}
Licensed under CC BY-NC-SA 4.0

评论

使用 Hugo 构建
主题 StackJimmy 设计