g++-11
关于C++的编程环境配置,请参考博文《【C++学习笔记】C++编程环境配置》
关于C++使用的IDE配置,请参考博文《【C++】IDE Cookbook by Eric》
请参考博文《【C++】基础知识的学习笔记》
请参考博文《【C++】Philosophy》
.h
:C++头文件。
.cc
:CUDA语境下的CPU代码文件。
.cu
:CUDA语境下的GPU代码文件。
C++在引入头文件时,有下面两种方式:
1.
:仅适用于标准库文件的引入,例如
#include
2. file_name.h
:适用于自定义的头文件、以及兼容C方式的头文件,例如
#include
#include
#include "custom_api.h"
Note:此外,为了与C语言兼容,C++在将原有C语言头文件标准化后,头文件前会带有c
字母,如cstdio
、cstring
、ctime
、ctype
等等。于是,当我们要用C++标准化了的C语言头文件时,可以进行如下的转换:
#include --> #include <cstdio>
#include --> #include <cstdlib>
#include --> #include <cstring>
#ifndef & #define & #endif
防止声明冲突使用#ifndef & #define & #endif
可以防止重复声明,这是因为如果不使用文件标识符的话,多次引用相同头文件会重复声明某些相同的字段,而引起冲突;
# pragma once
:对"#ifndef & #define & #endif"的升级宏定义头文件通常包含以下内容:
将函数的定义放在头文件中并使用#pragma once
是可以减少重复编译的问题,但并不是一种推荐的做法:
使用小写单词;
第一个单词的字母全部小写,第二个单词的首字母大写;
跟变量名类似;
缩写单词全部大写,后如果跟一个单词的话,第一个字母仍然大写;
例如:TextView, JSONArray;
类型模板:
const decltype(auto)
引用模板:
const auto&
关于const
的作用,我感觉菜鸟教程的解释是很好理解的,这里引用一下:
C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
const
跟const int
是两种不同的类型示例代码:[Cpp_const_int]
constexpr
表示可以在编译时确定值的常量;
Note:constexpr
是一种对常量定义的声明,但并非是一种类型关键字,
可以看到在进行类型判断时,编译器认为constexpr并不是有效的类型关键字。
#define
和constexpr
定义常量有什么区别呢?#define
写起来会更加简单一些,不需要加上类型声明;而且constexpr
的主要应用并不是用来定义数值常量,这一点我们是从cppreference的示例中感受到的,这些示例中目前还没有使用constexpr
定义数值常量的例子;
关于常见的扩展数据类型,请参考博文《c++基础之uint8_t》
uint8_t
: unsigned char,无符号字符类型。
size_t
: 在C++用于描述大小的类型,常用于数组索引和循环计数。
025
在C++中表示“025八进制”
在 C++ 中,整数字面量以0
开头时,表示该数字使用八进制。
也就是说,025
在C++中表示八进制025,也就是十进制21。
auto
自动类型Note
在使用auto
自动声明变量类型时,会出现类型退化的现象,不过C++标准并没有规定具体的“退化规则”,所以这需要我们通过实验的方式进行验证。
decltype()
确定表达式类型std::is_same_v<decltype(var), type>
Note:
is_same_v()
是对is_same()函数的简化,也就是输出时不用加上::value来获得bool值结果了。
std::is_same_v
的写法简化成std::is_type_same(var)
是比较困难的呢?主要原因是,某些类型的变量之间本身可以相互赋值而无法有效区分,例如:Type a | Type &a | const Type a | const Type &a
。SO上面曾尝试给出一个解答,但是在类型const int&
上就验证失败了【测试 is_same】;
基础类型转换模板:
Note:这里使用{}
避免调用C风格转换。
Type conversion | support |
---|---|
double → int | ✔ |
赋值时不允许收缩转换:x = {y}
(替代赋值语句 x = y)
对于
x = {3}
或者constexpr int y = 3; x = {y}
,编译器会检查数值是否符合x的类型;Note
关于为什么引入右值引用,请参考视频教程《[C++] 为什么需要右值引用?》
Reference Collapsing 是C++中的一种类型转换规则。它是在 C++11 标准中引入的,用于解决引用嵌套类型的问题。当一个引用类型嵌套在另一个引用类型中时,“reference collapsing” 将自动应用,并使嵌套的引用类型转换为非引用类型。
例如,T& & 转换为 T&,T& && 转换为 T&,T&& & 转换为 T&,T&& && 转换为 T&&。
using MyType = type
示例:using MyInt = int;
书写形式 | 中文名称 |
---|---|
{} 列表 | 初始化列表(list initialization) |
Note:
C++标准 明确表示了braced-init-list
不是一种表达式,请参见 List-initialization - cppreference.com。
() | {}
进行初始化的习惯用法,请参考《C++中五花八门的初始化》std::move(v)
:临时拷贝#include
std::move(v)
可以生成一个匿名的临时拷贝的对象;
int a = 1;
std::vector<int&> vec_try; //错误,不能定义引用类型的vector
std::vector<int*> vec; //正确,可以定义指针类型的vector
vec.push_back(&a);
可以使用=
运算符实现类对象的正常析构 ⇒ [Cpp_unique_ptr]
模板:
auto p = std::make_shared<Type>(...);
因为 shared_ptr 最主要的作用是进行内存资源的自动释放,而类数据成员的内存资源是由类本身负责管理的,并不需要 shared_ptr 来进行管理,所以不推荐使用 shared_ptr 来标记类对象的数据成员;
Note:直接使用C风格指针是推荐的做法。
Weak_ptr可以解决循环引用的问题,其中,循环引用问题(也称为 Resource Loops 问题)是指:当两个对象中分别包含指向对方的 shared_ptr 时,会出现循环计数的现象,导致两个都无法由 shared_ptr 自动销毁,相应代码请参考[Cpp_resource_loops_with_shared_ptr]
Note:
这里,我们也找到了一种比较简单的解决方案(请参见[Cpp_release_shared_ptr]),就是在 shared_ptr 释放前调用reset()
消除引用计数,就可以解开两个对象成员之间的循环引用,从而进行正常的析构了。
在CLion中,可以使用快捷键ctrl+W来确定表达式的计算顺序,
示例:对于以下代码,我们想看?=
跟=
的执行顺序,可以看到此时光标位于a
的右侧,
此时使用ctrl+W,会选中a
变量
再次按下ctrl+W,则会选中a = 42
说明赋值表达式会先执行。
=
代码示例:[Cpp_structured_binding_demo]
int n = 3;
std::string s = "123";
auto tpl = std::make_tuple(n,s);
expr_a + expr_b
:C++标准不保证expr_a
和expr_b
在计算时的先后顺序。
++x | --x
Attribute | Prefix | Suffix |
---|---|---|
return | 操作数x | 原始值 |
返回值性质 | 前缀时返回左值 | 后缀时返回右值 |
++x
)的性能常常优于后缀运算(x++
)这是因为后缀运算可能会涉及到新对象的构造操作;
Auto数组引用:auto &a
在C++中,C-style数组是以 row-major 的方式存储的,即每一行的数据存储在连续的内存块中。
Note
C++标准并没有明确规定 C-style array 的layout,而对此 GCC Manual 也没有明确说明;不过按照指针地址的一般定义,数组的内存访问顺序是row-major的。
对于二维数组,第一个维度用Row表示,第二个维度用Column表示,(跟矩阵的表示相同)
Note
数组读取操作一般比写入操作快10倍以上。
stdArr.at(i)
在超过数组下标时,程序会直接报错,而不是返回未定义值或者程序崩溃;std::array
C++20: std::to_array
int s[3] = { 1, 5, 8 };
auto a = std::to_array(s);
By C++17:
关于在C++17语境中将C数组[]
转换为std::array
,请参考博文《在 C++ 中将 C 样式数组转换为 std::array 容器》
std::list
关于list
的初始化,请参考《在 C++ 中初始化一个 std::list》
std::vector
std::vector
可以同时支持索引访问和迭代访问;
std::vector
的元素是存储在堆内存上;
Note
vector
内部是使用array实现的,在进行中间插入时速度较慢,如果需要频繁地进行中间插入,可以考虑使用list
。
vec.reserve(n)
如果提前知道数组最大长度范围的话,可以使用vec.reserve()
预先申请内存空间;
vec.emplace_back()
一般情况下,emplace_back()
的速度要优于push_back()
,(关于两者速度的小测试,可以参考博文《6 Tips to supercharge C++11 vector performance》);
vec.data()
vec.size(); //返回vec中元素的个数
std::map
ValueType: std::pair
具体可以参考[C++_insights_map_no_const]
通过 C++ Insights 解析代码后可以看到会有一个std::pair
对象构造的操作,而并不是直接使用m.begin().operator*()
取地址后获得的对象;
Note
在这个示例中,访问不存在的键值"hye"
,不仅会返回0,而且会插入键值对{"hye": 0}
,此时字典的长度为2。
C++标准中还提供了set和map的unordered实现,即:unordered_set和unordered_map,在某些情况,例如:一次性输入大量数据进行初始化,后续多次进行查找操作的场景,可以提供更快的查找速度;
相关的知识请参考《深蓝学院C++课程:第10章: 序列与关联容器 - 第3节: 关联容器 - 69:unordered set / map / multiset / multimap》
请参考博文《How to use Unordered_set with User defined classes – Tutorial & Example》
C++20
)请参考博文《【C++】Ranges&views(高阶函数)的学习笔记》
std::ranges::reverse_view()
(C++20
)std::ranges::reverse_view()
可以创建一个视图用来反向遍历容器,其中view
的含义是创建视图对象,也就是说,不会改变原始容器的顺序;(请参考[Cpp20_reverse_view])
#include
#include
int main() {
int nums[] = {1, 2, 3, 4, 5};
auto sum = std::accumulate(std::cbegin(nums),
std::cend(nums), 0);
std::cout << "Sum: " << sum << std::endl; // 输出 15
return 0;
}
读取模板:
for (const auto& item: container) {
cout << item << endl;
}
修改元素模板(使用万能引用):
for (auto&& item: container) {
cin >> item;
}
比如,有时候循环条件的值我们也希望在循环体中使用,则可以在循环条件求值时同时将条件表达式赋予给一个变量,从而避免重复的逻辑,可以参考cppreference中遍历字符数组的示例;
If-else在匹配时,只会match最近的一个语句,如果需要匹配多个语句,则需要加上{}
表示一个语句块;
Note
[Chatty]:
需要注意的是,[[likely]]
和[[unlikely]]
属性只是给编译器提供了一些提示,编译器并不一定会采取相应的优化策略。在实际使用中,应该先进行性能测试,再决定是否使用这些属性来优化代码。
警告提示隐式的 fall through 行为:
# g++编译选项
-Wimplicit-fallthourgh
深蓝学院C++课程:第6章: 语句 - 第4节: 语句的综合应用——达夫设备
在函数调用执行时,(实参虽然使用逗号,
隔开),不过实参表达式的执行顺序是不确定的;
return
而不是std::exit()函数程序中止模板:
return EXIT_FAILURE;
Note:
经过查阅资料,我们知道使用return
语句退出会比直接调用std::exit()退出会更加安全一些;因为在使用exit(0)退出程序时,不会调用局部作用域非静态对象的析构函数,(于是某些资源可能会没有被正常释放),请参考文章《return statement vs exit() in main()》。
内联函数最好声明在同一个翻译单元内,例如:一个cpp
中,或者一个cpp
及其h
文件组成翻译单元中。
Constexpr声明的函数既可以在编译期执行,也可以在运行期执行;
运行期执行示例:[Cpp_constexpr_run]
(C++20)
:将可推导结果函数代码编译为立即数Prerequisite: gcc >= 10.1
不同于constexpr的宽松规定,consteval声明的函数仅能在编译期执行,不能在运行期执行;
关于explicit的作用讲解,请参考博文《C++ explicit 关键字》
[[nodiscard]]
:函数返回值需要被显示接收如果调用函数分配了内存,函数返回结果指向了一块内存,如果将返回的指针丢弃,则会造成内存泄露;
const auto param
Note:
I.const auto& param
不够稳定
这是因为如果传入的参数是一个将亡值(xvalue),那么用引用接收参数会存在绑定失效的bug。
II.const decltype(auto) param
编译无法通过
即使使用-std=c++23
,也无法进行编译;
const type param
典型的示例是一维数组退化成指针类型:
std::initializer_list
:相同类型参数列表模板:std::initializer_list
Note:
一般来说,使用 const std::initializer_list &par 也是可以合法的方式,但是这种用法不多,因为std::initializer_list
内部是基于指针实现的,(即是传入的是对象列表),开销也很小(内部不会有拷贝赋值的操作),所以一般不需要通过使用引用来节省开销,直接使用模板声明的形式就可以了。listParams
container.emplace
传递参数列表会比使用 initializer_list 性能更好使用emplace
将元素添加到容器中会比使用 initializer_list 更高效,因为它避免了对象的拷贝操作;
具体示例,请参考博文《std::initializer_list in C++ 2/2 - Caveats and Improvements - 2. The cost of copying elements》
Contracts(合约)是C++20的语言特性,可以被视为“assert”语句的增强版,它可以声明函数的前置条件和后置条件;
auto& func(...) {...}
返回引用避免复制构造的开销;
测试结果:不行。
测试代码:Cpp_return_with_base
代码的输出:
Called Room::copy_ctor()
Base prints name
可以看到,返回值在回传的时候,出现了一次拷贝构造的过程,于是在main()函数获得的其实是基类的对象,于是无法使用虚函数的动态绑定功能,所以不能直接使用基类作为返回值类型,来传递函数内构造的派生类对象;
因为如果返回的是 underlying array (“底层数组”),可以认为是一个临时对象,之前说过 initializer_list 是基于指针实现的,而指针无法延长临时对象的生命周期,所以这就是一个典型的不安全的case,
所以如果要返回列表,推荐返回类型vector<>
。
解析过程:
The compiler will look for the function in the namespaces that contain the types of the function arguments;例如:在实参对应的类域内部进行查找
Overload resolution: If there are multiple functions with the same name, the compiler will choose the best match based on the types of the arguments and the template arguments, if any.
Access control: The compiler will check if the selected function is accessible from the current scope.
Virtual function dispatch: If the function is a virtual function, the compiler will determine the actual function to be called at runtime based on the dynamic type of the object.
Function call: Finally, the compiler will generate code to call the selected function.
文档:Overload resolution - cppreference.com
using K = int(int)
int inc (int x) {
return x + 1;
}
void Demo(K* input) {}
void Demo(K input) {}
Demo(inc);
Demo(&inc);
以上高阶函数定义和调用任意组合,效果都是完全相同的,其本质原因是函数类型跟数组类型一样,不支持复制;
auto
作为返回值示例代码:[Cpp_factorial]
decltype(auto) factorial(auto n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
《【C++】Lambda Expression 的学习笔记》
例如,用户不能在main()
函数的函数体中定义一个函数;
定义域:全局域或者局部域。
例如:可以在main()
函数内部定义一个类。
class 派生类名: 继承方式(访问说明) 基类名1, 继承方式 基类名2, ..., 继承方式 基类名n
{
派生类成员声明
};
Note
关于访问说明对继承基类成员的影响,请参考文章《C++ 公共、受保护和私有继承》。
即使声明初始化给出了数据成员的默认值,也无法使用auto进行声明,
示例:[Cpp_error_auto_member]
Copy constructor是指形参为类引用类型的构造函数;
Copy constructor template:
A(const A &)
移动构造函数主要解决了资源转移的冗余性问题:对于某些类型,如果拷贝一个对象,会产生额外的资源开销,例如拷贝动态内存等。移动构造函数可以将资源直接从旧对象转移到新对象,从而避免重复申请和拷贝;
Move constructor template:
A(A&& o)
因为委托构造函数(delegate constructor)只能单独使用,请参考测试代码[Cpp_delegate constructor_and initializer list]
Move assignment operator function template:
struct Str {
Str& operator=(Str &&x) // this-> m ; x->m
{
if (&x == this) // 自身赋值优化
return *this;
...
}
...
};
default
// ...
// Move constructor
Widget(Widget&&w)=default;
// Move assignment operator
Widget& operator=(Widget&& w)=default;
写作模板:
virtual ~Base() = default;
virtual
:当存在子类时,如果使用父类标记接收了子类变量,在析构时,标记自动寻找子类的析构函数并执行,不然只对父类成员进行析构。
= default
:此写法跟{}
在使用上几乎没有明显的区别,不过可读性会更清晰一些。
delete
delete
的作用是删除函数的调用入口;
我们在dali-doc中看到这样的代码:
Dummy(const Dummy&) = delete;
Dummy& operator=(const Dummy&) = delete;
Dummy(Dummy&&) = delete;
Dummy& operator=(Dummy&&) = delete;
其目的是使得Dummy算子实例无法被复制或者转移;
经过查询资料,我们知道在以下场景中可能出现这种情况:
This class has all copy and move constructors and assignments deleted which makes it not copyable and not movable. As for why I can’t speak of because I am not familiar with the lib, but presumably it models some form of unique non-transferable ownership.
- Objects sometimes represent actual resources (or ownership) of resources of which there can be only one. Copying those instances would not be right. Deleting the move constructor (and assignment) clearly models that you can’t transfer ownership to some other bit of code (and you want to keep control over the objects lifecycle at all the time).
- 对象表示一个独占的文件,不希望被拷贝成两个对象。
const
声明此函数只能被const的对象指针调用(包括引用产生的指针调用);
override
模板:
bool ActualFunc() const override{
return true;
}
写作说明:
override
:这里使用override显式声明此函数是对基类函数的覆写;
const
:显式声明此函数是非原位修改式的函数。
. | ->
使用成员访问操作符访问时:
->
的左操作数是指针,返回左值std::bind + &Str::mem
构造成员调用可以不加&符号,推测是内部会自动将类对象转换成对应的引用形参,
std::mem_fn
:将类成员函数指针包装成可调用对象请参考博文《【C++】模板的学习笔记》
C++中的元编程是指利用编译器执行编译期操作的编程技术。元编程主要通过使用模板(template
)和编译期常量(constexpr
)来实现,可以在编译期操作类型、执行简单的逻辑运算等。
C++ 中的元编程常用于以下场景:
std::cout << std::boolalpha;
请参考博文《【C++】代码调试的学习笔记》
请参考博文《【C++】异常处理的学习笔记》
关于在C++中对CPU计算性能优化的原理,请参考视频教程《Branchless Programming in C++ - Fedor Pikus - CppCon 2021》
-O3
我们可以配置编译器选项来对代码进行编译优化;
constexpr
constexpr
说明符可以使函数在编译期求值,从而优化程序的性能;
请参考博文《【DALI笔记】Python调用C++扩展库联合调试的学习笔记》
请参考博文《DALI Cookbook by Eric》;
conda
可以使用conda
来实现C++的环境管理,trtpy
就是使用这种方式实现C++环境隔离的;
请参考博文《【C++】文本处理的学习笔记》
std::this_thread::sleep_for()
示例:
线程休眠1秒
std::this_thread::sleep_for(1s);
string line;
getline(cin, line);
#include
请参考博文《【C++学习笔记】随机数生成:#include
请参考博文《【C++】常见第三方扩展库的学习笔记》