ptz 发布的文章
c++ 程序设计 -- 现代方法 阅读笔记
第一章
1.1
-
g++ 编译参数
-fsanitize=address
用于运行时,程序对内存不恰当访问,会暴露出相应内容,是一种内存检查机制。使用 makefile 的时候,直接增加到cppflag
里面,使用 cmakelist 的时候,增加到target_link_libraries
里面。 参考: https://stackoverflow.com/questions/50163828/c-addresssanitizer-with-cmakelists-txt-results-in-asan-errors#:~:text=When%20compiling%20code%20to%20be%20run%20with%20one,do%20this%20by%20calling%20target_link_libraries%3A%20target_link_libraries%20%28MyTarget%20-fsanitize%3Daddress%29
https://www.cnblogs.com/justin-y-lin/p/11314059.html - 命令行运行程序结束后,可以通过在命令行输入
echo $?
命令来获取 main 函数的返回值。在 main 函数中exit(0)
和return 0
等价,其他函数内,exit
立即终止程序。
1.2
- OOA(Object-Oriented Analysis) 建立面向对象模型,主要考虑系统做什么,不关心系统如何实现
- OOD(Object-Oriented Design) 以OOA模型为基础,重新定义或补充一些类,或修改原有类。目标是产生一个可实现的OOD模型。和OOA模型相比,OOD模型抽象层次低,包含了与具体实现相关的细节,但是建模原则和方法是相同的。
- OOP(Object-Oriented Programming) 是对OOD成果进行具体的实现 。把OOD模型转换为选定语言的具体代码实现。
第二章
2.1
- bool 类型不属于 int 类型,不要直接和 int 类型进行运算
- enum 标识名就是类型名,可以直接使用,例如:
enum SIDE {LEFT, RIGHT}; SIDE s = LEFT;
enum 不同于 int,不能直接和 int 已经常量进行混合运算。 还可以限制枚举常量的作用域,例如:
enum class SIDE {LEFT, RIGHT}; enum class STATUS {WRONG, RIGHT}; SIDE a = SIDE::RIGHT; STATUS B = STATUS:RIGHT;
2.2
- 空指针不可以用
NULL
来赋值,只能用nullptr
来赋值。 - 对释放后悬空的指针写入,属于释放后使用 use-after-free,可以使用 sanitizer 来清楚的显示具体情况。
- 左值引用定义时必须被初始化,也称为别名绑定。左值引用符号是
&
- 左值引用通常不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。
- 不存在
void &
引用,存在void *
指针; 不能创建引用数组int &arr[10]
错误,int * arr[10]
正确; 引用不存在因为类型不同造成的内存重解释问题。 - 右值主要是两类:纯右值和将亡值。右值引用符号是
&&
- 将亡值在使用过程中可能产生效率问题。比如函数返回值,首先从栈中的局部对象复制到一个临时右值对象中,局部对象失效;然后从这个临时对象中复制给接收对象,临时对象失效;在这个过程中,存在二次复制,影响执行效率。
- 为了提高效率,右值可以通过
std::move()
,std::forward()
等来转换。可以参考: http://www.ramlife.org/2021/06/03/346.html
2.3
decltype
decltype
用来进行类型推导
int i = 0;
decltype(i) j; // int
decltype((i)) ri = i; // const int &
auto
auto
用于类型自动推导,修饰的对象必须被初始化
auto d = 0.1; // double
2.4
new, delete 是 cpp 的特有运算符,malloc, free 是 c 的库函数
double *p, *q, *t;
p = new double;
q = new double(1.0);
t = new double[10]; // 分配了 10 个 double 的连续单元,也就是 80字节的数组
delete p;
delete [] t;
delete 有下面的注意点:
- delete 不会把指针设置为 nullptr
- cpp 释放一个 nullptr 的指针不会有安全问题
int * p = new int[10];
delete p;
if (p == nullptr) ... // false
double * q = nullptr;
delete q; // safe
类型转换
a = static_cast<int>(b)
int & e = const_cast<int &>(c)
, 用于把 const/volatile(指针或引用)数据转换为非 const/volatile 数据dynamic_cast<类型>(表达式)
用于把基类指针/引用 转换为派生类指针 / 引用double * q = reinterpret_cast<double *>(p)
, 用于把一个指针转换其他类型的指针,属于内存的重新解释。
无论哪一种转换,都要申请临时单元,把要转换的值复制到临时单元里面,转换发生在临时单元,对原始数据不影响。
typeid 运算符用于获取对象类型
#include <typeinfo>
int a, b;
typeid(a) == typeid(b); // true
typeid(a) == typeid(int); // true
std::cout << typeid(a).name(); // i
lambda 表达式
lambda 表达式是轻量级匿名函数,归类为 元表达式 primary expression
,属于闭包 closure
类型,语法形式:
[捕获列表](参数列表) -> 返回值类型 复合语句
,返回类型写在参数列表后,属于 拖尾返回类型 trainling-return-type
捕获形式:
形式 | 详细 |
---|---|
[] | 不捕获 |
[=] | 值方式捕获所有 |
[&] | 引用方式捕获所有 |
[x, y] | 值方式捕获 x, y |
[&x, &y] | 引用方式捕获 x, y |
[=, &x] | 引用捕获 x,值捕获其他 |
[&, x] | 值捕获 x, 引用捕获其他 |
如果复合语句有 return e
这样的语句,那么返回的类型就是 e 的类型,否则就是 void 类型。
int a = 1, b = 2;
auto square = [] (int x) { return x * x; };
auto add = [a, b] (int t) { return a + b + t; };
auto mul = [] (auto a, auto b) { return a * b; };
cout << square(add(3)) << endl;
cout << mul(1, 2.3) << endl;
2.5
for 循环新增形式
int a[] = {1, 2, 3};
for (auto val : a) cout << val << ' ';
2.6 异常
- 函数内处理异常, try{}catch(){},如果异常严重,就跳出程序,如果不严重,可以恢复,那么可以在循环里面使用异常。
- 函数级别异常,可以捕获函数中抛出的异常,如下:
void f() try
{
throw int(0);
}
catch (int)
{
cout << "int exception catured" << endl;
}
- 可以在函数头增加说明,明确指定函数不会抛出异常,如下:
void g() noexcept;
void h() noexcept(true);
2.7 函数
函数类型
int f(int i){...}
这个函数的类型是 int (int)
函数返回值
正常情况下函数返回值是一个右值对象。
int add(int a, int b)
返回是一个右值char * strcopy(const char * src)
返回的也是一个右值,但是这个值是地址,是指针,通过这个指针可以间接修改指向的对象,所有当这个返回值和*
结合的时候,就是一个左值对象。可以认为如果函数返回值是指针,就是返回了一个左值对象(需要结合*
)
*(strcopy("abc")) = 'a'; // ok, 和 * 结合,实际是这个右值指向的对象,这个指向的对象本身是一个左值。
strcopy("abc") = nullptr; // error, 返回值是右值
int& getVar(int * p) { return * p;}
返回的是一个引用,与返回指针相比,返回引用是一个真正的左值对象。这个引用是输入的这个指针指向的对象,这个返回是一个左值对象
auto add (int a, int b) -> int { return a + b; }
注意返回类型必须是 auto- 如果返回类型是 auto,那么函数内部多个 return 返回的类型必须一致。
- 结构化绑定可以让函数返回多个值,也可以初始化多个值,如下:
struct X {int a, double b};
X f() {return {1, 2.3}; }
int main()
{
auto [a, b] = f()
double arr[] = {1.2, 3.4, 5.6};
auto [c, d, e] = arr;
}
- 函数可以返回 lambda 表达式。作为返回值类型。
auto f() { return [](int a) -> int {return a * a; }; }
int main()
{
auto square = f();
cout << square(2) << ", " << f()(3) << endl;
return 0;
}
constexpr
表明函数的返回值是字面量,在编译期就可以确定。可以用来说明变量和对象,效果等效于 const 说明。但是不能用于类型,函数参数的说明。常量参数一般使用 const 说明。
constexpr int f() {return 5;}
constexpr int g(int a) {return a + 3;}
int main()
{
int x = 5;
int a[f()] = {1, 2, 3};
int b[g(x)];
}
- 函数重载,参数类型和个数不一样就可以重载。但是有以下几点注意:
- 只有返回值类型不同,不能重载。
- 同一个作用域中函数不能原型一致重载,如果要重载,必须在参数列表上有所不同。
int f() { return 0;}
int f(int i) { return i;}
// int f(int i = 0) { return i;} // error, 默认参数不能和无参版本区分。
int f(int& i) { return i; }
int main()
{
int a = 2;
// cout << f(a) << endl; // error, 无法区分调用的是 f(int) 还是 f(int&)
}
- 回调函数,如果返回值是 bool,那么这类回调被称为谓词。参数是回调函数时,参数类型可以是函数指针也可以是函数,比如说
void f(void (*callback)())
和void f(void callback())
这两个都可以。但是函数返回值只能是函数指针类型,不能是函数类型。
2.8 复杂类型声明简化
void g(void (*p)())
这个p,首先 p 是和*
结合的,所以首先是指针,然后把(*p)
去掉,就是void()
, 这个是指针 p 的基类型,可以看出是一个函数类型,所以 p 是一个指向函数的指针,类型是void(*)()
void (*(h()))()
这个h, 更加复杂,首先 h 是和()
结合的,所以,h 首先是个函数,并且h()
这个里面没有参数,所以是个无参函数,然后去掉(h())
,就是void (*)()
是一个函数指针,这个函数指针就是 h 函数的返回值,所以 h 就是一个返回类型是函数指针的函数。- 可以使用 typedef 来简化
typedef void (*FUN_PTR)(); typedef void FUN(); void g(FUN_PTR p) {...} void g(FUN p) {...} FUN_PTR h() { return ...} // FUN h() {return ...} //error
-
using 比 typedef 更加易懂
using FUN_PTR = void (*)(); using FUN = void ();
- 使用拖尾类型也能够简化
auto h() -> void (*)() {return ...} auto h() -> FUN_PTR {return ...} g(f); h()(); // h() 返回的是指向函数的指针,再用一个() 来实现对这个指针的调用
2.9 命名空间
- 不建议使用
using namespace std;
建议使用using std::cout;
这样,用到什么就申明什么。 - 命名空间可以嵌套,内部隐藏外部的同名标识符。
- 如果命名空间前面是空着的,表明是全局的,比如说
::malloc(...)
这个就是 c 里面的malloc
函数。 参考: https://blog.csdn.net/wk_bjut_edu_cn/article/details/79620622
类
3.1
struct node {...}
在 new 的时候,不需要 struct,只要struct node * p = new node{...}
这样结构化赋值定义即可。- 多使用
using node_ptr = struct node *;
这样的写法,因为struct node * head, tail
这样定义变量的时候,tail 不是指针。需要这样node_ptr head, tail
就可以了。
3.2
- 类的定义里面可以包含指向该类的指针或者是引用,但是不能包含该类的对象。
- 类的成员变量是在 ram 里面,并且一个对象是一份。成员函数是公用的,只有一份。
- 为了类的封装性,外部不适合直接获取链表的内部数据,从而进行遍历。所以建议遍历做到类的内部,做成 public ,由外部调用。具体遍历时执行什么操作,可以把成回调函数做成参数的形式:
auto linked_list::traverse(callback * af) -> void
{
for (auto p = head; p != nullptr; p = p->next)
af(p->data);
}
int main()
{
list.traverse([] (value_t& v) {cout << v << " ";});
}
- 类的静态成员变量必须在类的外部进行初始化,缺省时初始化为0。静态成员变量的存储是独立于类的,所以在所有类对象被创建之前,就已经存在了。如果一定要在类内部进行初始化,那么这个只能是静态常量成员,
static const int a = 0;
。 - 类的静态成员函数想要访问非静态成员变量,必须显式的提供一个对象指针或是引用作为参数。
- struct 默认公用,可以使用 private 或 protected,建议只有数据成员的简单对象使用 struct,包含数据和行为的对象使用 class
- union 不能使用 private, protected。建议在 c 里面使用 union,不要用 c++ 来使用。
- 聚集:弱关系,组成的各个部件虽然独立工作无实际意义,但是确实可以不依赖于整体而独立存在。体现了整体与部件的拥有关系(has-a)。类似于电脑和硬盘这样的关系。
- 组合:强关系,组成的各个部件不能独立存在。体现了整体和部件的包含关系(contains-a)。类似于企业和部门的关系。
3.3
- 没有参数的构造函数一般称为默认构造函数
- 类的构造函数是
public
。可以被explicit
修饰,这样隐式的类型转换将被阻止。 - 默认参数只能放在函数形参列表的最后。
X() : a(9), ra(a) {}
这样的是构造函数初始化列表。初始化列表比构造函数的函数体先执行。如果类包含常量,独立引用等特殊成员,他们的初始化只能使用初始化列表,或者在定义类的同时完成初始化。- 包围类的构造函数中需要显式的调用嵌入类对象的构造函数。
- 花括号初始化列表构造函数,
initializer_list
是初始化列表类型的名字:
linked_list::linked_list(const std::initializer_list<value_t>& l)
{
for (auto e: l) push_back(e);
}
void main()
{
linked_list l{1, 2, 3, 4, 5};
}
- 构造函数委托可以将其他版本构造函数的初始化部分交给某个特定版本去完成,包括执行初始化列表和函数体,然后才执行自己的代码,从而减少代码量和错误概率。
linked_list::linked_list() : head(nullptr), tail(nullptr), _size(0) {}
linked_list::linked_list(size_t len, value_t * a) : linked_list() {...}
linked_list::linked_list(const std::initializer_list<value_t>& l) : linked_list() {...}
- 以前初始化可以分为直接初始化和复制初始化,主要区别是
=
// 直接初始化
const char *p("string");
linked_list l(5);
// 复制初始化
int a = 0;
linked_list k = {1, 2, 3, 4};
- 现在可以使用统一的初始化语法,列表初始化,使用花括号。
int a = {1};
int b{2};
int *p = new int[]{1, 2, 4};
X n;
n = {7, 8};
- 类的析构函数可以被显示调用,
l.~linked_list()
, 一旦调用,那么类对象就会失效。 - 析构函数可以是虚或者纯虚的,并且也应该是虚的。
- 当没有显式的给出构造函数时,编译器会生成隐式默认构造函数。一旦显示的给出构造函数,编译器就不会给出默认构造函数。
- 可以使用
default
来显式默认构造函数。使用delete
来显式删除函数,用于防止复制等场合。可以参考: https://blog.csdn.net/weixin_38339025/article/details/89161324
X() noexcept = default;
~X() noexcept = default;
A(const A&) = delete;
A& operator=(const A&) = delete;
A(double) = delete;
3.5
- 面向对象开发,进行系统分析,首先问 why, who, how, when, where, what.
类的高级特性
4.1 案例 - 链表类的复制问题
-
复杂类型的复制操作对于简单类型组合的聚集类型是没问题的。
聚集 aggregates:
- 没有用户提供的说明为显式货继承的构造函数
- 没有私有的或保护的非静态私有数据成员
- 没有虚函数,并且没有虚继承,私有继承或保护继承的基类
- 链表的复制操作
linked_list l1{1, 2, 3};
linked_list l2{l1};
- 链表的赋值操作
linked_list l1{1, 2, 3};
linked_list l2;
l2 = l1;
- 复制操作,编译器使用的是复制构造函数,如果是编译器合成的,那么就是合成复制构造函数。
4.2 复制控制
- 复制操作分为 复制 和 赋值 两种
- 复制
linked_list l2{l1}; // 直接初始化
linked_list l3 = l1; // 复制初始化
void f(linked_list l);
linked_list x;
f(x); // 形参 l 是实参 x 的复制副本
linked_list g() {return linked_list({1, 2, 3}); }
auto y = g(); // 对象 y 是 g() 返回的临时对象 (右值对象) 的副本。
- 复制构造函数: 只要构造函数的第一个参数是本类型的引用。
linked_list::linked_list(const linked_list& l) : head(l.head)
{
...
}
- 赋值
struct X {int i, j;} a = {1, 2}; // 初始化
X b;
b = a; // 赋值
- 赋值运算符
=
是一个函数,可以被重载linked_list& operator=(const linked_list& l);
- 复制发生在对象初始化,右操作对象存在,左操作对象正在被创建。赋值时,左右两个对象都已存在,所以在赋值前,需要先释放左操作对象的原来资源。
linked_list& linked_list::operator=(const linked_list& l)
{
clear(); // 释放资源
head = l.head;
return *this; // 返回对象本身
}
-
默认的复制和赋值操作,采用的都是浅复制,如果对象由额外资源的情况下,析构的时候,还是会出问题。这种情况下,需要深复制,也就是显式的使用循环等语句,把额外的资源复制一份出来。
- 转移可以省掉一部分复制操作
linked_list(linked_list&& l);
linked_list::linked_list(linked_list&& l) : head(l.head)
{
l.head = nullptr; // 把前者资源置空,c++ delete 空指针是安全的。
}
void main()
{
linked_list l1{1, 2, 3};
auto l2 = std::move(l1}; // 把 l1 伪造为一个转移对象,否则编译器只会调用 l2 的常规复制构造函数。
}
- 赋值运算符也可以转移,直接使用
swap
来交换资源。
linked_list& operator=(linked_list&& l);
linked_list& linked_list::operator=(linked_list&& l)
{
std::swap(head, l.head);
return *this;
}
- 有时期望每个类只有一个唯一的实例,那么就可以使用
delete
来禁止复制
linked_list(const linked_list&) = delete;
linked_list& operator=(const linked_list& l) = delete;
4.3 指向类成员的指针
- 定义使用
指针 = &类名::成员名;
- 使用
对象.*指针
,对象->*指针
class Y
{
int a;
int b;
}
Y o{1, 2};
int Y::* ptr = &Y::b;
++(o.*ptr);
4.4 友元
- 友元是为了提高访问效率,把内部封装的数据暴露给友元直接使用。
- 友元包括 全局友元函数,成员函数友元,友元类。
class A
{
friend int set(A&, int); // 全局友元函数
friend B::set(A&, int); // 成员函数友元
friend class C; // 友元类
}
- 友元适合于运算符重载和泛型编程。
- 嵌入类的友元不能访问外部的包围类,除非显式的在包围类中也申明了相应的友元。
4.5 类 const 和 mutable 成员
- 类的常数据成员必须在构造函数的初始化列表中完成,或者在定义时初始化完成。
- 常成员函数需要 const 修饰,如果申明和定义分开写,那么必须完全一致
size_t size() const; // 常成员函数,表明没有修改行为
const linked_list l{1, 2, 3};
std::cout << l.size(); // l 的 this 指针从 linked_list * const this, 变成了 const linked_list * const this.
- 有些时候需要修改常量对象的属性,那么这个属性必须申明为
mutable
class A
{
mutable int x;
}
const A o;
o.y = 2;
4.6 类中的类型名
- 在包围类外使用嵌套类的名字,或者定义嵌套类的成员,那么必须
encircle::nested n;
这样去做。 - 嵌套类形成了一个局部作用域,包围类的成员在这个作用域时不可见的。嵌套类的作用域对于包围类来说也是封闭的。
- 嵌套类可以被申明为包围类的友元,这样就可以直接访问包围类的所有成员了。
class encircle
{
int i;
friend class nested;
class nested
{
void g(encircle& e) {e.i = 0;}
}
}
- 类中的枚举,在类中使用的时候,可以不需要加限定,在包围类外使用需要加名字限定
class encircle
{
public:
enum STATUS { WRONG, RIGHT };
};
encircle::STATUS S = encircle::RIGHT;
- 类中的 typedef 和 using 定义的别名,也被限制在包围类的作用域中,在包围类外使用也需要名字限定。
- 如果模板之类的特殊场合,编译器无法确定 B 是否时包围类 A 中定义的一个类型名时,可以在 B 前面增加
typename
,用于说明修饰的名字时类型名,而不是成员名。typename A::B o;
参考: https://zhuanlan.zhihu.com/p/335777990
5 运算符重载
5.2 运算符函数重载
- 和普通函数一样,区分运算符重载版本的唯一依据时参数列表
- 运算符重载要考虑运算符的原始语义,不要产生歧义
- 重载形式考虑为类的成员还是友元。比如加法,结果时产生一个新值,所以函数适合以友元的形式重载。
- 重载形式考虑函数的参数,正常情况下,加法的左右操作数不会被改变,所以类型使用 const 修饰。
- 重载形式考虑函数返回值类型。正常情况下,加法产生新值,所以返回的是值类型,而非指针和引用类型
- 如果重载形式为友元,那么会解释为
::operator+(c1, c2)
,如果重载形式为成员函数,那么解释为c1.operator+(c2)
。正常情况下两个都可以使用,但是,如果是1 + c
这样的表达式,友元形式会把1
进行隐式转换为临时的复数类型,然后正确的进行计算,成员函数形式就会出错,转换机制不起作用。 - 友元形式级联会解释为
::operator+(::operator+(a, b), c)
得到正确的结果 -
重载运算符规则
::
,?:
,.
,.*
这些运算符不能被重载- 重载运算符不能改变优先级,不能改变结合性,不能改变操作数的数目。
- 重载运算符必须和用户自定义类对象一起使用,参数至少由一个是类对象,否则会改变内建类型的运算符性质。
- 用于类对象的运算符一般必须重载。
- 重载运算符功能应当类似于该运算符作用于标准类型数据时的功能,或者含义显而易见,否则应该采用函数调用方法。
- 重载运算符函数不能时类的静态成员
- 重载运算符函数的参数和返回值建议
=
,()
,[]
,->
,type-casting
这些运算符必须为成员函数,单目运算符和复合赋值运算符建议为成员函数,其他双目运算符建议为友元函数。- 重载的单目运算符,成员函数应该没有参数,运算作用在 lhs 上,函数返回 lhs 的左值引用。
- 重载的双目运算符,有两个参数,并且都是常量,至少有一个是将该运算符函数作为友元对待。如果参数不是指针和引用,那么会引起类的复制构造函数调用,因此,最好为类显式定义一个复制构造函数。函数返回值对象(非引用/非指针)同样会引起复制构造函数的调用。
- 对于关系和逻辑运算符,应该产生一个 bool 类型的结果。
- 作为成员重载的运算只是读取对象属性而不会改变他们时,建议将该函数设为常成员。
5.3 常用运算符的重载
=
赋值运算符必须重载为类的成员,因此该函数参数只有一个,并且改变了左操作数,结果就存放在其中。赋值可以级联,结合性是从右到左,还要注意是否完成深复制。+=
这样的复合赋值运算符,完成加法和赋值,运算结果存储在左操作数中,并且右操作数不会改变,是常量引用,还可以级联。++
作为前缀时,作为成员重载,函数没有参数,返回操作数的左值引用。complex& operator++() {++real; ++imag; return *this;}
++
作为后缀时,必须有一个整型参数,没有实际用途,可以只有类型,不需要名字,返回的时值类型。complex operator++(int) {complex t = *this; ++real; ++image; return t;}
+
注意原始语义是运算结果是一个新值,在重载实现中,先赋值左操作数到一个临时对象中,再添加元素到该临时对象,并返回这个对象的值。==
对于浮点类型数相比较是否相等,一般只能比较两个的差值小于一个接近 0 的数值,比如fabs(a.real - b.real) < 1E-7
- 只重载必要的运算符,比如只重载
<
,然后只使用<
,不使用>
>>
,<<
用于流操作时,特性是:重载为类的友元,有两个参数,第一个是流对象的引用,第二个是右操作数,返回值是参数流对象的引用,以便于级联。
5.4 类型转换
- 标量转类,称为装箱(boxing)。注意,如果相应的构造函数使用 explicit 修饰,那么某些语句会被编译器拒绝。
complex(double r = 0.0, double i = 0.0);
complex a = 5.6; // 隐式(implicit)转换
a = 7.8; // 隐式(implicit)转换
explicit complex(double r = 0.0, double i = 0.0);
complex a = complex{5.6};
z = complex{7.8};
- 类转标量,称为拆箱(unboxing),必须明确这样操作的意义。
比如复数转换标量,可以表示为取模,那么必须重载类型转换运算符
- 并且这个函数必须是类的成员函数,并且没有参数
- 而且不能为这个函数指定返回值类型,但是函数体需要 return 一个和函数名相同类型的实例。
operator double() {return sqrt(real * real + image * image);}
complex a{1.2, 2.3};
double b = double(a)
- 类转换类,比如复数除了直角坐标还有指数表示方法
class complexa; // forwarding
class complex
{
operator class complexa();
};
class complexa
{
operator complex() { return complex{r * cos(a), r * sin(a)}; }
}
complex::operator complexa()
{
return complexa{sqrt(real * real + image * image), atan2(image, real)};
}
5.5 重载特殊运算符
- 重载
[]
,特点:双目运算符,数组名是左操作数,下标是有操作数,a[i] 是个左值对象。重载特点:重载为类的成员,下标是唯一参数,返回元素的左值引用,必须是类的非静态成员。并且再重载函数中,还可以对下标进行判断,是否需要抛出异常。
value_t& operator[](size_t index);
- 重载
[]
也可以给字典使用。但是字典直接对 字符串类型的 key 进行对比比较耗时,所以需要使用hash
把字符串转换为整数,就可以对比整数了。
struct pair
{
std::string key;
value_t value;
};
class dictionary
{
struct_node
{
pair data;
size_t hash;
_node * next;
}
}
void dictionary::push_back(pair d)
{
std::hash<std::string> hash_fn;
node_ptr p = new _node{d, hash_fn(d.key), nullptr};
...
}
value_t& dictionary::operator[](std::string key)
{
std::hash<std::string> hash_fn;
for (auto p = head; p != nullptr; p = p->next)
if (hash_fn(key) == p->hash) return p->data.value;
throw std::string("key '" + key + "' doesn't exist.");
}
- 重载
*
,->
运算符。指针类应该与另一种类相关联,后者为关联类。重载*
能提取对象中的相关数据成员,提取出来的是个左值。重载->
要返回指向关联类对象的原生指针,通过该指针可以访问关联类的共有成员。两种运算符必须重载为类的非静态成员。
class foo
{
int a = 0;
void print() {std::cout << a << std::endl;}
friend class foo_ptr;
}
class foo_ptr
{
foo * p;
foo_ptr(foo * q) : p(q) {}
int& operator*() {return p->a;}
foo* operator->() {return p;}
}
foo x;
foo_ptr p = &x;
std::cout << *p << std::endl;
*p = 3;
p->print();
- 重载
()
运算符,上面size_t hash = hash_fn("one");
这个就是典型的重载了()
运算符,因为 hash_fn 是一个结构体(也是类)的对象,冒充了一次函数调用。这种对象称为函数对象(function object)
。
double& operator()(char part) {return part == 'r' ? real : imag;}
complex c{1.2, 2.3};
++c('r');
c('i')--;
- 可调用对象分为3种,函数,lambda表达式,函数对象。他们可以通过标准库 functional 来完成统一。但是注意,他们的类型必须一致,比如说,下面这样都是
int()
这样才能通用,写作function<int ()> fn
#include <functional>
int f() {return 0;}
class foo
{
int x = 1;
int operator()() {return x;}
}
void g(std::function<int ()> fn) { std::cout << fn() << std::endl;}
int a = 2;
foo x;
g(f); // 函数做参数
g([=]()->int {return a;}); // lambda 做参数
g(x); // 函数对象做参数
6. 继承
6.1 家族关系
- is-a 关系特点: 上层分类全部特性将自动传递给下层分类而无须显式说明,下层分类会逐层增加上层分类所没有的特性,越向上,越抽象,越向下越具体。
- 后代对祖先全部接受的行为就是继承。
6.2 继承和派生
- 如果类 a 和类 b 形成了 is-a 的关系,那么 b 就是一种 a,a 和 b 形成了继承关系。
- 继承时,public 继承除了基类中的 private 之外都不变,private 继承把基类中的所有都变为 private,protected 把除了 private 的都变为 protected.
- 可以通过
using
申明恢复成员原有的访问属性,这就是访问申明。但是只可以恢复,不能提升。而且恢复的这个在基类中不能处于不同的段中,比如有些重载的在 public 里面,有些在 private 里面。
class B
{
public:
void f();
};
class D: private B
{
public:
using B::f; // f 恢复成 public
}
- 在构造派生类对象之前,必须先构建基类子对象,可以在派生类的构造函数的初始化列表中引起基类构造函数的调用来实现。如果基类有一个默认构造函数,那么在派生类的构造函数初始化列表中,可以不显式的显出基类的初始化部分,编译器会自动调用基类的默认构造函数。
class parallelogram : public quadrangle
{
parallelogram(size_t w = 5, size_t h = 3) : quadrangle(n), width(w), height(h) {}
}
- 在派生类的成员函数中使用直接基类或祖先类的成员,语法是
祖先类名::祖先类成员
- 如果派生类的构造函数与基类的功能完全相同,都只是初始化二者共同用于的基类成员,那么派生类中可以不用显式定义自己的构造函数,而是使用 using 申明直接引入基类的构造函数。这就称为继承的构造函数。不仅可以继承基类的所有构造函数,甚至包括重载的赋值运算符。 注意: 析构函数不能被继承,如果是虚的,可以被派生类的覆盖。
class B
{
char name;
B(char n) : name(n) {}
B(int, char n) : name(n) {}
B& operator=(const B&) {...}
};
class D : public B
{
using B::B; // 构造函数继承
};
- 所有后代和基类共享静态成员的唯一实例
- 派生类中可以重定义基类中同名的成员。如果在派生类中访问基类的重定义版本,可以两种方法:
double rectangle::area() const { return parallelogram::area();}
rectangle r(10, 6);
std::cout << r.parallelogram::area();
6.3 赋值兼容原则
- 派生类可以直接赋值给基类,基类不可以直接赋值给派生类
quadrangle q;
parallelogram p;
q = p;
- 派生类对象可以直接赋值给基类的引用,不会引起相应的转换
parallelogram p;
quadrangle & q = p;
- 如果基类对象要赋值给派生类引用,那么需要
dynamic_cast<>()
,并且不一定能成功。 - 指针也是一样,派生类可以赋值给基类,基类不可以赋值给派生类。
- 派生类赋值给基类,称为 up-casting, 不需要使用任何强制类型转换,多态就是利用这个机制。反过来就是 down-casting, 需要
dynamic_cast
6.4 多继承
- 多继承的派生类中,构造函数应该显式的调用所有基类的构造函数。
- 某个基类有默认构造函数,或者构造函数的所有参数都是可以默认的,那么在初始化列表中可以省略这个基类的构造函数调用,这个省略的直接基类子对象将用自己的默认构造函数去构造。
- 多继承如果产生了 grid 这样的继承路线,那么将格顶端公共基类的所有直接派生类说明为虚的,这样就是虚基类,这种继承就是虚继承。
class parallelogram : public quadrangle {...};
class rectangle : virtual public parallelogram {...};
class diamod : virtual public parallelogram {...};
class square : public rectangle, public diamod {...};
- 多继承会导致很多函数需要在派生类中重新定义来覆盖掉基类的同名版本。
- 不建议使用多继承。
6.5 继承的前提: 正确的分类
- is-a 和 has-a 一定要考虑情况。
- 高校教师,教授,讲师,助教 这几个的关系看起来是 is-a,从教师派生出其他几个。但实际上,教师是职业,其他三个是职称,教师通过 has-a 来包含职称,职称通过 is-a 派生出 教授,讲师,助教。 这样的划分才是科学的。教师可以通过努力,来提升他的职称。
7. 多态
7.1
- 用派生类的方法覆盖(override)基类的同原型方法,这就是多态技术。
- 使用基类的指针,运行时实际需要派生类的方法,这就是多态。
7.2 多态概念
- 多态指的是一个接口,多种实现。分为静态和动态两种,都是通过函数重载来实现。
- 静态多态是早期匹配,在编译时完成。包括普通的重载函数和派生类中的重定义函数。
- 动态多态是晚期匹配,主要靠虚函数机制来实现。
7.3 虚函数:实现多态的关键
- 派生类方法必须覆盖 override 而非简单重载 overload 基类的同原型方法。覆盖操作通过虚函数 virtual function 机制来实现。
- 如果派生类需要覆盖基类的同原型成员,那么基类需要申明该成员是虚函数
virtual double quadrangle::area() const {return -1.0;}
- 申明了虚函数的类,或者祖先包含了虚函数的类称为多态类 polymorphic class
-
虚函数有以下特点
- 虚特性必须赋给类的非静态成员函数
- 不能把友元说明为虚函数,但虚函数可以是另一个类的友元
- 虚特性可以被继承,如果派生类原型一致的继承了基类的某个虚函数,即使在派生类中没有用 virtual 显式的说明是虚的,也会被编译器认为是虚的。
- 不是每一代派生类都需要显式覆盖祖先类的虚函数,如果实现相同,就不需要。如果一定要覆盖,可以直接调用祖先的虚函数。
double rectangle::area() const
{
return parallelogram::area();
}
-
有基类 X,申明了虚函数 f(),想要 f() 得到多态效果
- 派生类中有覆盖的 f()
- 定义 X 的指针或引用 pr
- 定义派生类对象 o,并用 pr 指向他
- 通过类似 pr->f() 或 pr.f() 的方法来访问虚函数,而不是通过 o.f() 的方式。
-
一旦基类中的某函数申明为虚,那么无论后代是否有 virtual 修饰,原型相同的函数都是虚的。但是如果派生类中重载了一个同名都是原型不同的函数,那么在这代派生类中,虚函数特性会丢失,但是在下一代中,虚函数特性还是会遗传下去。
- 显式覆盖使用
override
,显式表明最终版本使用final
class X
{
virtual void f() {}
virtual void g() {}
};
class Y : class X
{
virtual void f() final {}
virtual void g() override {}
}
class Z final : class X
{
virtual void f() override {} // error, 上一代已经是最终版本了
virtual void g() override {}
}
class W : class Z {}; // error, 上一代已经是最终版本了
-
覆盖需要原型一致,包含函数名,参数列表,返回值。但是返回值有下面差异,也可以认为是覆盖,这种称为协变 covariant 覆盖
- 返回二者都是自己类型的指针,或左值引用,或右值引用
- 二者都是同一种类 T,或者 D::f 返回的类是 T 的一个无二义,可访问的祖先类
- 二者(指针 / 引用)都含有 c-v 修饰符(const, volatile, const volatile),并且 D::f 返回类型的 c-v 修饰符等于或少于 B::f 的。c-v 修饰符最好不要和 using 定义的别名一起用,容易报错。
alignas(8)
指的是 8 字节对齐,也可以alignas(double)
- 多态类都有一个虚指针 virtual pointer / vptr,指向了一个虚表 vtable,非多态调用的时候,直接使用函数入口地址。多态调用的时候,需要先获得虚指针,然后再这个指针指向的虚表中,获取虚函数的入口地址,然后再调用。
- 多态类的析构函数也应该是虚的。或者说所有使用基类指针指向派生类的,只要有这种行为,析构函数做成虚的更合适。
7.4 纯虚函数和抽象类
- 纯虚函数 pure virtual 在基类中只有申明,没有定义,语法
virtual 函数名(参数列表) = 0;
virtual double area() const = 0;
- 纯虚函数要求派生类都必须定义自己的版本来实现覆盖。
-
有纯虚函数的类就是抽象类 abstract class. 有以下几个特点:
- 抽象类只能作为其他类的基类
- 派生类如果还有未实现的纯虚函数,那么这个派生类还是抽象类
- 不能创建抽象类的对象
- 可以申明和使用抽象类的指针和引用
- 抽象类不能用作函数的参数类型和返回类型,但抽象类的指针和引用可以
- 抽象类不能作为显式转换的类型
- 即使在申明了纯虚函数后,给出了这个函数的实现,这个类依然是抽象类。
- 抽象类因为没有自己的实现,所以不能创建相应的对象,所以不能作为值的形式存在,才有了上面这些特点
- 所有函数成员都是纯虚函数,并且没有数据称为的类称为接口 interface.
8. 模板
8.1
- 宏定义本质是一种无类型机制。
- c++ 模板分为3种,变量模板,函数模板,类模板。
8.2 变量模板
- 变量模板语法
template <typename T>
T 变量名 [ = 初始化表达式];
- 变量模板有普通情况,还有针对特定情况的特化情况。
template <typename T>
const T pi = static_cast<T>(3.1415);
using cstring = const char *;
template <>
cstring pi<cstring> = "3.1415";
std::cout << pi<double> << std::endl;
std::cout << pi<int> << std::endl;
std::cout << pi<cstring> << std::endl;
8.3 函数模板
- 函数模板实例化
template <typename T>
bool lt(T a, T b) { retuan a < b; }
int a{1}, b{2};
lt(a, b);
- 函数模板也可以给出非类型参数,非类型参数只能是整数类型(整型,字符型,bool型)和枚举类型其中之一。
template <typename T, T threshold>
bool lt2(T a) { return a < threshold; }
int a{100};
lt2<int, 1000>(a);
- 函数模板的所有参数都可以取默认值
template <typename T = int, T threshold = 10>
bool lt3(T a) { return a < threshold; }
int a{1}, b{2};
lt3(a);
lt3<int>(b);
lt3<size_t, 2>(b);
- 泛型 lambda 是一种特殊的函数模板,泛型 lambda,不需要使用 template.
auto lt3 = [](auto a, auto b)->bool { return a < b; };
// a, b 类型一致
auto lt3 = [](auto a, decltype(a)b)->bool { return a < b; };
- 多个类型也可以作为模板
template <typename T, typename U>
bool lt(T a, U b) { return a < b; }
-
调用模板和非模板按照如下约定
-
- 寻找一个参数完全匹配的非模板函数
-
- 否则,寻找一个函数模板,实例化一个匹配的模板函数。
-
- 否则,试试函数的重载方法,通过类型转换产生参数匹配
-
- 针对字符串类型可以进行模板特化,注意特化函数模板参数在形式上必须与普通模板一致,如普通模板要求左值引用,则特化模板也要是左值引用。
using cstring = char *;
templat <>
bool lt(cstring a, cstring b) { return strcmp(a, b) < 0; }
- 特化时,还可以对函数模板的部分特化 partial specialization,也称为 偏特化。
template <typename T, typename U> void f(T, U) {}
template <> void f(int, char) {} // 完全特化
template <typename T> void f(T, int) {} // 偏特化
- 在函数模板种调用另外一个函数,参数类型可能会改变,如果想要不变,需要完美转发,形参需要
&&
,函数体中需要std::forward
.
void f(int&) {...}
void f(int&&) {...}
template <typename T>
void wrapper1(T a) {f(a);} // 普通转发
template <typename T>
void wrapper2(T&& a) {f(std::forward<T>(a)); } // 完美转发。
- 折叠表达式语法类似
template<typename ... Types> void f(Types ... args);
...
在左边的时左折叠,在右边是右折叠,只有...
和args
的称为一元,还有其他数值的称为二元,二元里面只能有一边是args
,另外一边必须是确定的。- 左折叠
(... op e)
展开后(((e1 op e2) op e3) ... op en)
- 右折叠
(e op ...)
展开后(e1 op ... (en-2 op (en-1 op en)))
template<typename ... Args>
auto sum_unaryLeft(Args ... args) { return (... + args); }
template<typename ... Args>
auto sum_binaryLeft(Args ... args) { return (0 + ... + args); }
template<typename ... Args>
auto sub_unaryLeft(Args ... args) { return (... - args); }
template<typename ... Args>
auto sub_unaryRight(Args ... args) { return (args - ...); }
std::cout << sum_unaryLeft(1, 2, 3) << std::endl; // 6
std::cout << sum_binaryLeft(1, 2, 3) << std::endl; // 6
std::cout << sum_unaryLeft() << std::endl; // error, 参数包为空
std::cout << sub_unaryLeft(3, 2, 1) << std::endl; // 0
std::cout << sub_unaryRight(3, 2, 1) << std::endl; // 2
8.4 类模板
- 类模板包含成员函数(所有成员函数都是函数模板),成员类(内部类使用包围类的类型参数),成员模板(类内部的类或者成员函数被冠以 template 关键字)。
template <typename value_t>
class linked_list
{
using value_type = value_t;
// 成员模板
template <typename callback_t>
void traverse(callback_t af)
{
for (auto p = head; p != nullptr; p = p->next)
af(std::forward<value_type>(p->data)); // 完美转发
}
}
auto af = [](auto&& v) {std::cout << v << ' ';};
linked_list<int> l1{1, 2, 3};
l1.traverse(af);
- 函数模板可以隐式实例化,类模板必须显式实例化
linked_list<int> l1{1, 2, 3};
- 类模板可以有非类型参数,用来定制类模板的特性,比如数组长度
template <typename value_t, size_t maxLen>
class array { value_t arr[maxLen]; ...};
- 类模板各种参数可以是默认的
template <typename T = int>
class linked_list {...};
linked_list<> l1; // 使用默认类型参数 int
linked_list<double> l2;
- 类模板可以特化
template <>
class linked_list<float> {...};
linked_list<float> l3;
- 类模板可以偏特化
template <typename T>
struct A<T*> {T v;};
using intptr = int*;
A<intptr> a; // a 的成员 v 类型是 int
- 类模板的友元分为 普通友元,普通模板友元,特化模板友元
template <typename T> void TPrint(const X<T>&) {}
template <typename T>
class X
{
friend void print();
// 普通模板友元
template <typename P> friend void TPrint(const X<P>&);
// 特化模板友元
friend void TPrint<float>(const X<float>&);
};
- 实例化继承模板
template <typename T> struct A{};
class B : public A<int> {};
- 继承模板
template <typename T> struct A{};
template <typename T> class B : public A<T> {};
- 变长参数模板
template <typename ... types> class A{};
A<> a;
A<int> b;
A<int, float> c;
struct A { A() {std::cout << "A" << std::endl;} };
struct B { B() {std::cout << "B" << std::endl;} };
struct C { C() {std::cout << "C" << std::endl;} };
template <typename ... bases>
struct D : public bases...
{
D() : bases()...
{ std::cout << "D has " << sizeof...(bases) << std::endl;} // sizeof...() 表示变长参数包的个数
};
int main()
{
D<A> o1;
D<A, B> o1;
D<A, B, C> o1;
}
python [::]
python 中经常能够看到的是 [a : b], 这个表示的是取一个区间中的数据。 [a : b : c] 这种形式比较少见,其实第三个参数指的是取值时候的步进量,默认是 1。
参考: https://blog.csdn.net/gaofengyan/article/details/90697743
image mean
image mean 在深度学习中指的就是平均值。 对多个图像进行训练后,在同一个位置,同一个通道上的平均值。
图像数据减去均值之后,能够让图像数据在 0 两边比较均匀。
正常情况下, mean 是在 rgb 三个通道上分别求平均值,比如说 [0.5, 0.5, 0.5] 就是三个通道上的分别平均值,还有一个标准差 std,也是三个通道分别求值,比如说 [0.5, 0.5, 0.5]。
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
这个执行的计算是: image =(图像 - 平均值)/ std
参考:
https://blog.csdn.net/weixin_37251044/article/details/81157344
https://www.cnblogs.com/volcao/p/9089716.html
https://www.zhihu.com/question/330534169
https://www.cnblogs.com/booturbo/p/12688105.html
https://blog.csdn.net/KaelCui/article/details/106175313
https://discuss.pytorch.org/t/understanding-transform-normalize/21730
numpy resize 和 reshape 区别
在 numpy 中,resize 是用来改变数组的元素个数,而 reshap 不改变元素个数,只是改变维度,比如说从一维变成二维。
import numpy as np
X=np.array([[1,2,3,4],
[5,6,7,8],
[9,10,11,12]])
X_new=np.resize(X,(3,3)) # do not change the original X
print("X:\n",X) #original X
print("X_new:\n",X_new) # new X
>>
X:
[[ 1 2 3 4]
[ 5 6 7 8]
[ 9 10 11 12]]
X_new:
[[1 2 3]
[4 5 6]
[7 8 9]]
import numpy as np
X=np.array([1,2,3,4,5,6,7,8])
X_2=X.reshape((2,4)) #retuen a 2*4 2-dim array
X_3=X.reshape((2,2,2)) # retuen a 2*2*2 3-dim array
print("X:\n",X)
print("X_2:\n",X_2)
print("X_3:\n",X_3)
>>
X:
[1 2 3 4 5 6 7 8]
X_2:
[[1 2 3 4]
[5 6 7 8]]
X_3:
[[[1 2]
[3 4]]
[[5 6]
[7 8]]]
参考:
https://blog.csdn.net/qq_24193303/article/details/80965274
https://blog.csdn.net/DocStorm/article/details/58593682
python %matplotlib inline
在 python 代码中看到 %matplotlib inline
,这个感觉比较奇怪。经过在网上搜索之后,才知道是 用于 Jupyter notebook 的,使用 Jupyter 时,当输入plt.plot(x,y_1)后,不必再输入 plt.show(),图像将自动显示出来。
参考:
https://blog.csdn.net/Code_Mart/article/details/82385293
https://www.jianshu.com/p/2dda5bb8ce7d
目标检测
mobilenet 介绍
目标检测
目标检测算法使用已知的对象类别集合来识别和定位图像中的对象的所有实例。该算法接受图像作为输入并输出对象所属的类别,以及它属于该类别的置信度分数,该算法还使用矩形边界框预测对象的位置和比例。
目标检测的目的是“识别对象并给出其在图中的确切位置”,可以分为三部分:
- 识别某个对象
- 给出对象在图中的位置
- 识别图中所有的目标及其位置
tengine 介绍和环境搭建
介绍
Tengine概述:
Tengine是OPEN AI LAB(开放智能)推出的边缘AI推理框架,致力于应用场景下多厂家多种类的边缘智能芯片与多样的训练框架、算法模型之间的相互兼容适配,同时提升算法在芯片上的运行性能,将从云端完成训练后的算法高效迁移到异构的边缘智能芯片上执行,缩短AI应用开发与部署周期,助力加速AI产业化落地。
Tengine重点关注嵌入式设备上的边缘AI计算推理,为大量应用和设备提供高性能AI推理的技术支持。一方面可以通过异构计算技术同时调用CPU、GPU、DSP、NPU等不同计算单元来完成AI网络计算,另一方面,它支持TensorFlow、Caffe、MXNet、PyTorch、MegEngine、DarkNet、ONNX、ncnn等业内主流框架,是国际上为数不多的通过ONNX官方认证的战略合作伙伴之一。
动手学深度学习
win 安装
- Miniconda 安装好以后,把安装路径
Miniconda3
和Miniconda3\Scripts
添加到 path 里面。 - 从开始菜单里面开始 conda,然后
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
添加阿里的源,清华的源安装好像有点问题。 - cd 命令打开到代码目录
conda env create -f environment.yml
安装相应的环境软件conda activate gluon
开启 gluon 环境,conda deactivate
退出环境。jupyter notebook
启动 jupyter,浏览器也会自动打开。如果不能打开,就在浏览器中输入http://localhost:8888
- 安装 gpu 版本. 首先卸载 cpu 版本的 mxnet
pip uninstall mxnet
, 然后安装 cudaconda install cudatoolkit=10.0
, 修改environment.yml
把mxnet
修改为mxnet-cu100
,然后执行conda env update -f environment.yml
或者直接pip install mxnet-cu100==1.5.0
这样就安装好了。
深度学习基础
线性回归
误差公式其中常数1/2使对平方项求导后的常数系数为1,这样在形式上稍微简单一些。
参考
https://stackoverflow.com/questions/44597662/conda-command-is-not-recognized-on-windows-10
https://discuss.gluon.ai/t/topic/12816/4
https://zhuanlan.zhihu.com/p/109939711
https://discuss.gluon.ai/t/topic/13576
活体检测
原理
活体检测
随着人脸识别、人脸解锁等技术在金融、门禁、移动设备等日常生活中的广泛应用,人脸防伪/活体检测(Face Anti-Spoofing)技术在近年来得到了越来越多的关注。一个可以正常工作的人脸识别系统,除了实现“认人”还包括应用于人脸识别身份认证系统中至关重要的一项技术————活体检测。
活体检测就是判断捕捉到的人脸是真实人脸,还是伪造的人脸攻击(如:彩色纸张打印人脸图,电子设备屏幕中的人脸数字图像 以及 面具 等)。本质是分类问题,可看成二分类(真 or 假);也可看成多分类(真人,纸张攻击,屏幕攻击,面具攻击)