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
- 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.
- 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.
- 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
。
struct
和 class
可以,但是成员函数不能在头文件里放在外面。类不能重复定义,但是可以在链接是出现重复,但是两个重复的不能不同,否则会出一些奇怪的情况。例如:
// 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;
}
我们来仔细考虑一下:
A
定义 →B
定义:A
里根本写不了B
。B
定义 →A
定义:B
里根本写不了A
。A
声明 →B
定义 →A
定义:B
定义里要用A
的成员,但A
没定义。B
声明 →A
定义 →B
定义:A
要写friend void B :: ...
,B
是得定义完的。
所以最后只能 A
→ B
→ A
→ B
。
唐。
类继承
思想:两个类有共同特征,不想重复写这部分。
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
,需要把 B
和 C
的继承方式改成虚继承 virtual public A
,与此同时当 D
的构造函数递归调用 B
和 C
的构造函数时,将会忽略对 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()
。作用是无法修改内容。
++ it
比 it ++
快,考虑 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
赋值给 b
且 a
不再用了,写 b = move(a)
。
智能指针
我们现在希望偷懒不写 delete
,于是类似上面,我们会写个指针类来自动 delete
,但是与上面不同的是,指针类并不负责申请内存,也不负责复制内容,只负责自动释放空间。这就导致如果两个指针指向同一片内存就会爆。
头文件 memory
。C++98 有 auto_ptr
,略。C++11 有 unique_ptr
和 shared_ptr
。unique_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
也可以做)。如果被转换者类型不对(包括把本身就指向基类对象的指针转成派生类),会返回 nullptr
。dynamic_cast
转换的必须是多态类型,即至少声明或继承(可以在本身 override
)了一个虚函数的类。
异常
- 抛出异常
throw 信息
。 try
开始代码块,里面可以用throw
。- 捕获异常
catch (信息类型 [变量名字]) { ... }
接在try
的代码块后,可以接多个处理方式。catch (...)
通用。
可以跨函数处理(再抛出)。跨的函数有返回值的赋值也会跳过。
为了区分不同(大类的)异常情况,可以建立各种异常类。值得注意的是,如果抛出的类型是派生类,而捕获的类型中既有基类又有派生类,且前者写在前面,那就会被前者捕获。
STL 中的东西的异常都是 std :: exception
的派生类,可以调用 . what()
知道它是哪种。
优化原理
- 分支预测
- 循环展开
- 并行处理、SIMD
- 循环顺序
标准属性
发现有个 project 里有些 [[...]]
这种东西,加在声明或定义的类型名前面。这是起到修改编译器对一些特殊情况的反应的。包括:
[[nodiscard]]
加在函数声明前,如果该函数的返回值未被使用会报警告,可以通过[[nodiscard("...")]]
加警告信息。[[deprecated]]
表示对应东西可能被弃用,不鼓励使用(使用会警告),同上。[[noreturn]]
表示对应函数不会正常返回(exit
或throw
之类),提示编译器优化。[[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;
...
}