分类 Code 下的文章
[转] 函数参数是右值引用类型,能够接受什么样的参数输入
转自: https://blog.csdn.net/veghlreywg/article/details/88556681
假设我们有一个函数
class Data
{
};
void func(Data && data)
{
}
那么func能接收什么样的参数输入
情形一
Data data;
func(data); //[Error] cannot bind 'Data' lvalue to 'Data&&'
data是个左值,不能绑定到右值上
情形二
Data data;
Data & d = data;
func(d); //[Error] cannot bind 'Data' lvalue to 'Data&&'
d同样是个左值
情形三
都说const 引用和 右值引用有相似之处,尝试传递const 引用
Data data;
const Data & d = data;
func(d); // [Error] invalid initialization of reference of type
// 'Data&&' from expression of type 'const Data'
情形四
传递匿名对象,匿名对象
func(Data()); // OK
情形五
标准做法
Data data;
func(std::move(data));//OK
情形六
move一个左值引用
Data data;
Data & p = data;
func(std::move(p)); //OK
情形七
直接声明一个右值引用,来做参数传递
Data p;
Data && p1 = std::move(p);
func(p1); // [Error] cannot bind 'Data' lvalue to 'Data&&'
同样的错误,说明p1 还是左值
我们可以通过这个方式验证一下
void func(Data && data)
{
}
void func_1(Data && data)
{
func(data);//[Error] cannot bind 'Data' lvalue to 'Data&&'
}
Data data;
func_1(std::move(data));
那么明明是个右值为什么,就变成左值了呢,这个时候就轮到 我们的std::forward出场了
Data p;
Data && p1 = std::move(p);
func(std::forward<Data>(p1)); // OK
void func_1(Data && data)
{
func(std::forward<Data>(data));
}
//这就是完美转发的意义所在
情形八
把一个右值参数传递给const 引用类型
void func(const Data & data)
{
}
void func_1(Data && data)
{
func(data);//OK
}
Data data;
func_1(std::move(data));
makefile 编译报错,警告
warn
"defined but not used"
gcc 编译有"defined but not used" 警告,那么在 CPPFLAGS
这边进行修改:
WARNFLAGS = -Wall -Wno-unused-function
CPPFLAGS =-std=c++11 $(WARNFLAGS) ...
参考: https://blog.csdn.net/qq_28584889/article/details/97764810
https://cloud.tencent.com/developer/ask/119165
error: this ‘if’ clause does not guard... [-Werror=misleading-indentation], misleadingly indented as if it were guarded by” an if?
这个是 if 代码下面的缩进有些问题,只要检查缩进就行。有可能是多缩进了,也有可能是空格和 tab 混合使用。
参考: https://stackoverflow.com/questions/50318900/why-is-gcc-warning-me-this-line-is-misleadingly-indented-as-if-it-were-guarded
https://blog.csdn.net/lun55423/article/details/115530114
pyqt 简介
pyqt 介绍
qt 介绍
Qt 是一个著名的 C++ 库——并不只是一个 GUI 库。使用 Qt,在一定程序上你获得的是一个“一站式”的服务:不再需要研究 STL,不再需要 C++ 的string,因为Qt有它自己的 QString 等等。Qt 本身包含的模块也日益丰富, 一直有新模块和第三方模块加入进来。
QT中的信号与槽(Signal & Slot)机制是 Qt 编程的基础,也是 Qt 的一大创新,使得在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。
Qt的优点在于:
- 跨平台实现
- 广泛的库
- 友好的API,友好的库实现代码
- 提供偏向生产力的 Qt widgets 和 偏向交互的 Qt Quick,以及使用 html 的 Qt webengine
基于 Qt 开发的软件: KDE, WPS、YY语音、Skype、豆瓣电台、虾米音乐、淘宝助理、千牛、暴雪的战网客户端、VirtualBox、Opera、咪咕音乐、Google地图、Adobe Photoshop Album 等
嵌入式 Linux 上很多带界面软件也是基于 Qt 来编写的。
pyqt 介绍
PyQt实现了一个Python模块集。它有超过300类,将近6000个函数和方法。它是一个多平台的工具包,可以运行在所有主要操作系统上,包括UNIX,Windows和Mac。
为可用的类有很多,他们被分成几个模块。
- QtCore模块包含核心的非GUI功能。该模块用于时间、文件和目录、各种数据类型、流、网址、MIME类型、线程或进程。
- QtGui模块包含图形组件和相关的类,例如按钮、窗体、状态栏、工具栏、滚动条、位图、颜色、字体等。
- QtNetwork模块包含了网络编程的类,这些类允许编写TCP/IP和UDP的客户端和服务器,他们使网络编程更简单,更轻便。
- QtXml包含使用XML文件的类,这个模块提供了SAX和DOM API的实现。
- QtSvg模块提供显示的SVG文件的类。可缩放矢量图形(SVG)是一种用于描述二维图形和图形应用程序的XML语言。
- QtOpenGL模块使用OpenGL库渲染3D和2D图形,该模块能够无缝集成Qt的GUI库和OpenGL库。
- QtSql模块提供用于数据库的类。
基于 qt 的 python 界面: pyqt, pyside 基于 qt 的界面:Tkinter, WxPython
[转] 左值与右值,左值引用与右值引用(C++11)
阅读目录
- 关于左值和右值的定义
- 转移语义以及转移构造函数和转移复制运算符
右值引用是解决语义支持提出的
https://blog.csdn.net/xiaolewennofollow/article/details/52559306
这篇文章要介绍的内容和标题一致,关于C++ 11中的这几个特性网上介绍的文章很多,看了一些之后想把几个比较关键的点总结记录一下,文章比较长。给出了很多代码示例,都是编译运行测试过的,希望能用这些帮助理解C++ 11中这些比较重要的特性。
关于左值和右值的定义
左值和右值在C中就存在,不过存在感不高,在C++尤其是C++11中这两个概念比较重要,左值就是有名字的变量(对象),可以被赋值,可以在多条语句中使用,而右值呢,就是临时变量(对象),没有名字,只能在一条语句中出现,不能被赋值。
在 C++11 之前,右值是不能被引用的,最大限度就是用常量引用绑定一个右值,如 :
const int& i = 3;
在这种情况下,右值不能被修改的。但是实际上右值是可以被修改的,如 :
T().set().get();
T 是一个类,set 是一个函数为 T 中的一个变量赋值,get 用来取出这个变量的值。在这句中,T() 生成一个临时对象,就是右值,set() 修改了变量的值,也就修改了这个右值。 既然右值可以被修改,那么就可以实现右值引用。右值引用能够方便地解决实际工程中的问题,实现非常有吸引力的解决方案。
右值引用
左值的声明符号为”&”, 为了和左值区分,右值的声明符号为”&&”。
给出一个实例程序如下
#include <iostream>
void process_value(int& i)
{
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i)
{
std::cout << "RValue processed: " << i << std::endl;
}
int main()
{
int a = 0;
process_value(a);
process_value(1);
}
结果如下
wxl@dev:~$ g++ -std=c++11 test.cpp
wxl@dev:~$ ./a.out
LValue processed: 0
RValue processed: 1
Process_value 函数被重载,分别接受左值和右值。由输出结果可以看出,临时对象是作为右值处理的。
下面涉及到一个问题: x的类型是右值引用,指向一个右值,但x本身是左值还是右值呢?C++11对此做出了区分:
Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.
对上面的程序稍作修改就可以印证这个说法
#include <iostream>
void process_value(int& i)
{
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i)
{
std::cout << "RValue processed: " << std::endl;
}
int main()
{
int a = 0;
process_value(a);
int&& x = 3;
process_value(x);
}
wxl@dev:~$ g++ -std=c++11 test.cpp
wxl@dev:~$ ./a.out
LValue processed: 0
LValue processed: 3
x 是一个右值引用,指向一个右值3,但是由于x是有名字的,所以x在这里被视为一个左值,所以在函数重载的时候选择为第一个函数。
右值引用的意义
直观意义:为临时变量续命,也就是为右值续命,因为右值在表达式结束后就消亡了,如果想继续使用右值,那就会动用昂贵的拷贝构造函数。(关于这部分,推荐一本书《深入理解C++11》) 右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。 转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。 通过转移语义,临时对象中的资源能够转移其它的对象里。 在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。 普通的函数和操作符也可以利用右值引用操作符实现转移语义。
转移语义以及转移构造函数和转移复制运算符
以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MyString() {
_data = NULL;
_len = 0;
}
MyString(const char* p) {
_len = strlen (p);
_init_data(p);
}
MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
return *this;
}
virtual ~MyString() {
if (_data) free(_data);
}
};
int main() {
MyString a;
a = MyString("Hello");
std::vector<MyString> vec;
vec.push_back(MyString("World"));
}
Copy Assignment is called! source: Hello
Copy Constructor is called! source: World
这个 string 类已经基本满足我们演示的需要。在 main 函数中,实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。
我们先定义转移构造函数。
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str._data << std::endl;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
有下面几点需要对照代码注意:
- 参数(右值)的符号必须是右值引用符号,即“&&”。
- 参数(右值)不可以是常量,因为我们需要修改右值。
- 参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
现在我们定义转移赋值操作符。
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str._data << std::endl;
if (this != &str) {
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
return *this;
}
这里需要注意的问题和转移构造函数是一样的。 增加了转移构造函数和转移复制操作符后,我们的程序运行结果为 :
由此看出,编译器区分了左值和右值,对右值调用了转移构造函数和转移赋值操作符。节省了资源,提高了程序运行的效率。 有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。
关于std::move()和std::forward 再次推荐一本书:《effective modern C++》 英文版的,这里有篇关于其中item25的翻译不错
请看这里 https://blog.csdn.net/coolmeme/article/details/44459999
但是这几点总结的不错
-
std::move执行一个无条件的转化到右值。它本身并不移动任何东西;
-
std::forward把其参数转换为右值,仅仅在那个参数被绑定到一个右值时;
- std::move和std::forward在运行时(runtime)都不做任何事。
/*
* MS.cpp
*
* Created on: 2020年10月28日
* Author: xor
*/
#include <iostream>
#include <string.h>
#include <utility>
class MS {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MS() {
_data = NULL;
_len = 0;
}
MS(const char *str)
{
if(str)
{
_len = strlen(str)+1;
_data = new char[strlen(str)+1];
memcpy(_data, str, _len);
}
else
{
_data = new char[1];
_data[0] = '\0';
}
}
MS(MS &&ms)
{
_data = ms._data;
_len = ms._len;
ms._data = NULL;
ms._len = 0;
std::cout << _data << " lc " << _len << std::endl;
}
MS &operator=(MS &&ms);
};
MS &MS::operator =(MS &&ms)
{
if(this != &ms)
{
_data = ms._data;
_len = ms._len;
ms._data = NULL;
ms._len = 0;
}
std::cout << _data << " lop " << _len << std::endl;
return *this;
}
int main()
{
MS &&ms = MS("hik");
MS ms1(std::forward<MS>(ms));
return 0;
}
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
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
python onnxruntime 安装问题
安装
pip3 install onnxruntime --user
会报错找不到对应的版本。
python3 -m pip install --upgrade pip
更新 pip,结果报错 Permission denied: '/usr/bin/pip'。
python3 -m pip install --upgrade pip --user
成功更新 pip。
pip3 install onnxruntime --user
安装 onnxruntime.
使用
python3
import onnxruntime
报错 Illegal instruction (core dumped)。
gdb python3
r
import onnxruntime
报错
Program received signal SIGILL, Illegal instruction.
0x0000007fa8ca5f54 in gotoblas_dynamic_init ()
from /home/openailab/.local/lib/python3.6/site-packages/numpy/core/../../numpy.libs/libopenblasp-r0-32ff4d91.3.13.so
解决方法, export OPENBLAS_CORETYPE=ARMV8
然后再使用 python3 就可以了。问题就是 openblasp 在 cpu 检测的地方除了问题,所以明确告诉它 cpu 类型就可以了。
参考:
https://github.com/opencv/opencv-python/issues/485
https://github.com/numpy/numpy/issues/18131