C++学习笔记
2024-11-25 17:05:20 17 举报
AI智能生成
会持续更新
作者其他创作
大纲/内容
编译流程
预处理
定义
文本编辑阶段,执行一些预处理的命令,决定什么代码会喂给编译器
宏
#define
可以通过#define Axxx Bxx的方式,将代码中的Axx代码替换成Bxx
#if/#ifdef/#elif defined()/#else/#endif
可以用于控制debug模式和release模式输出不同的内容,比如日志输出之类的
#include
#include可以在编译的时候把被引用的代码包含进来。
c++没有包的概念,也就是java中的package,也无需通过import引入依赖。这点和我以前的写java和python的经验不一样。它需要先将被依赖的项目代码打包成lib文件再通过#include引用到项目中。
#pragma once
xxx.h文件中#pragma once的用法,这个#pragma once是告诉编译器该文件只会被编译一次,以防出现重复编译的问题。
编译
预编译头文件
预编译的头文件可以让我们抓取一些头文件,将他们转换成编译器可以使用的格式,而不需要一遍又一遍的读取这些头文件。
预编译头文件可以避免重复编译代码(被多次引用的),它可以接收很多要编译代码的信息,只编译一次。预编译头文件以二进制格式存储,对编译器来说比单纯的文本处理要快很多,效率也会高很多。
可以把不会经常变更的东西加到预编译头文件中。但是经常变更的就不要加进去。否则会让整个头文件不断重复编译。
预编译头文件最好不要加入比较大的依赖,而是应该加入尽可能小的依赖。避免编译的工作量过大。
汇编
链接
链接阶段在java中是没有的。这个比较有趣。结合翻译单元的概念会比较好理解。每个翻译单元都是单独编译的,然后再进行链接,把所有的代码链接到一起。
分类
静态链接
静态链接在编译阶段发生,会把直接需要链接的资源编译到可执行的exe文件,还会在全局的角度优化代码。执行代码的时候也不需要额外的操作,速度更快效率更高。
动态链接
动态链接是在执行代码的阶段进行。是需要把dll文件放入exe文件目录的。执行exe的时候cpu会把对应exe和ddl文件都加载到内存。因为必须包含这个dll,实际操作确实可能遗忘丢失这个东西。最后补充一下,静态链接和动态链接都可以被#include很好的支持。
静态链接和动态链接,这个需要实操一下才能掌握
指针
智能指针
std::shared_ptr 共享指针
它是通过引用计数的方式,决定一个对象实例是否会被回收。如果引用计数为0,对象实例就可以被销毁。讲解的时候还提到一种弱引用的指针,不会增加引用计数。java中也有这个概念。
std::unique_ptr 唯一指针
唯一指针unique_ptr,使用唯一指针获得的对象实例是无法再次被引用的。
代码
#include <iostream>
#include <memory>
//重写new操作符
void* operator new(size_t size){
std::cout << "allocating..." << size << std::endl;
return malloc(size);
}
struct Object{
int x, y, z;
};
int main(){
Object* obj = new Object;
std::string string = "cherno";
std::unique_ptr<Object> obj2 = std::make_unique<Object>();
}
#include <memory>
//重写new操作符
void* operator new(size_t size){
std::cout << "allocating..." << size << std::endl;
return malloc(size);
}
struct Object{
int x, y, z;
};
int main(){
Object* obj = new Object;
std::string string = "cherno";
std::unique_ptr<Object> obj2 = std::make_unique<Object>();
}
普通指针
神秘的指针其实就是一个整型(int)的内存地址。
指针里存储的内存地址是big endian,大端存储,倒序的。
指针的指针
多维数组
#include <iostream>
int main(){
int** a2d = new int*[50];
for(int i=0; i<50; i++){
a2d[i] = new int[50];
}
for(int i=0; i<50; i++){
delete[] a2d[i];
}
}
int main(){
int** a2d = new int*[50];
for(int i=0; i<50; i++){
a2d[i] = new int[50];
}
for(int i=0; i<50; i++){
delete[] a2d[i];
}
}
多维数组中每个数组存储的地址不一定是连续的,很可能造成cache miss,所以最好通过创建一维数组(逻辑上多维数组)来代替。
模板生成代码(范型)
template<typename T>
类
std::array底层就是用这种方式创建的template<typename T, N>
方法
template<typename T>跟着方法体
缺点
编译阶段对template的代码的处理,有的编译器是不做检查的,也就是说即使在模板代码中同一个变量,前后命名的不一样这种低级的错误也发现不了,只有代码运行的时候才会发现问题
不过Cherno也说因为怕有些程序员过度沉迷于此,写别人很难读懂的代码,所以很多游戏工作室和大公司是禁用模板写代码的。
优点
在编译时期会把T,也就是泛型,替换成真正的数据类型。模板也可以生成class的代码,使用N占位符替换常量。模板这种方式可以减少重复代码,给编程提供了极大的便利性。
线程
join
在当前线程上等待某个特定线程完成它的工作/阻塞当前线程,直到另一个线程完成,再继续执行当前线程。
使用方式
#include <iostream>
#include <thread>
static bool s_Finished = false;
void DoWork(){
while(!s_Finished){
std::cout << "working...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main(){
std::thread worker(DoWork);
std::cin.get();
s_Finished = true;
worker.join();
std::cout << "finished." << std::endl;
std::cin.get();
}
#include <thread>
static bool s_Finished = false;
void DoWork(){
while(!s_Finished){
std::cout << "working...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main(){
std::thread worker(DoWork);
std::cin.get();
s_Finished = true;
worker.join();
std::cout << "finished." << std::endl;
std::cin.get();
}
std::asnyc
std::async会自动创建一个线程去调用线程函数,它返回一个std::future,这个future中存储了线程函数返回的结果,
当我们需要线程函数的结果时,直接从future中获取,非常方便。
当我们需要线程函数的结果时,直接从future中获取,非常方便。
std::launch policy是启动策略,
它控制std::async的异步行为
它控制std::async的异步行为
std::launch::async 参数 保证异步行为,即传递函数将在单独的线程中执行;
std::launch::deferred参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为;
std::launch::async | std::launch::deferred参数 是默认行为。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。
代码
#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <future>
struct X
{
void foo(int x, std::string const& str) {
std::cout << "foo: " << x << " " << str << std::endl;
}
std::string bar(std::string const& str) {
std::cout << "bar: " << str << std::endl;
return str;
}
};
struct Y
{
double operator()(double x) {
std::cout << "Y operator(): " << x << std::endl;
return x; }
};
X baz(X&) {
std::cout << "call baz()" << std::endl;
return X();
}
int main() {
// 在新线程上执行
auto f6=std::async(std::launch::async, Y(), 1.2);
X x;
// 在wait()或get()调用时执行
auto f7=std::async(std::launch::deferred, baz, std::ref(x));
//执行方式由系统决定
auto f8=std::async(
std::launch::deferred | std::launch::async,
baz, std::ref(x));
执行方式由系统决定
auto f9=std::async(baz, std::ref(x));
f7.wait(); // 调用延迟函数
std::cout << "finsh!" <<std::endl;
}
#include <thread>
#include <chrono>
#include <string>
#include <future>
struct X
{
void foo(int x, std::string const& str) {
std::cout << "foo: " << x << " " << str << std::endl;
}
std::string bar(std::string const& str) {
std::cout << "bar: " << str << std::endl;
return str;
}
};
struct Y
{
double operator()(double x) {
std::cout << "Y operator(): " << x << std::endl;
return x; }
};
X baz(X&) {
std::cout << "call baz()" << std::endl;
return X();
}
int main() {
// 在新线程上执行
auto f6=std::async(std::launch::async, Y(), 1.2);
X x;
// 在wait()或get()调用时执行
auto f7=std::async(std::launch::deferred, baz, std::ref(x));
//执行方式由系统决定
auto f8=std::async(
std::launch::deferred | std::launch::async,
baz, std::ref(x));
执行方式由系统决定
auto f9=std::async(baz, std::ref(x));
f7.wait(); // 调用延迟函数
std::cout << "finsh!" <<std::endl;
}
#include <future>
#include <iostream>
#include <thread>
#include <chrono>
int entry() {
std::cout <<"call entry" << std::endl;
return 11;
}
int main() {
std::future<int> the_answer=std::async(entry);
//注释下行代码看同步异步
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "The answer is " << the_answer.get() << "\n"<<std::endl;
}
#include <iostream>
#include <thread>
#include <chrono>
int entry() {
std::cout <<"call entry" << std::endl;
return 11;
}
int main() {
std::future<int> the_answer=std::async(entry);
//注释下行代码看同步异步
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "The answer is " << the_answer.get() << "\n"<<std::endl;
}
std::future
std::mutex
static std::mutex s_mutex;
std::lock_guard<std::mutex> lock(s_mutex);
std::lock_guard<std::mutex> lock(s_mutex);
开发技巧
计时器
用于分析方法的性能,这里代码的实现思路了,是利用了方法一旦执行完,方法栈就会销毁,栈上分配的对象也会销毁。
所以在方法开头,创建对象记录执行开始时间,对象析构函数里记录执行完成时间。
所以在方法开头,创建对象记录执行开始时间,对象析构函数里记录执行完成时间。
RAII, Resource Acquisition is initialization. 只要退出这个函数,资源会被解锁。
代码
#include <iostream>
#include <chrono>
#include <thread>
struct Timer
{
std::chrono::time_point<std::chrono::steady_clock> start, end;
std::chrono::duration<float> duration;
Timer(){
start = std::chrono::high_resolution_clock::now();
}
~Timer(){
end = std::chrono::high_resolution_clock::now();
duration = end - start;
float ms = duration.count() * 1000.0f;
std::cout << ms << "ms" << std::endl;
}
};
void Function(){
Timer timer;
for(int i=0; i<100; i++){
std::cout << "hello\n";
}
}
int main(){
Function();
}
#include <chrono>
#include <thread>
struct Timer
{
std::chrono::time_point<std::chrono::steady_clock> start, end;
std::chrono::duration<float> duration;
Timer(){
start = std::chrono::high_resolution_clock::now();
}
~Timer(){
end = std::chrono::high_resolution_clock::now();
duration = end - start;
float ms = duration.count() * 1000.0f;
std::cout << ms << "ms" << std::endl;
}
};
void Function(){
Timer timer;
for(int i=0; i<100; i++){
std::cout << "hello\n";
}
}
int main(){
Function();
}
{}大括号限定作用域
在C/C++中大括号指明了变量的作用域,
在大括号内声明的局部变量其作用域自变量声明开始,到大括号之后终结。
{ } 里的内容是一个“块”,单独的{ }在执行顺序上没有改变,仍然是顺序执行,
在大括号内声明的局部变量其作用域自变量声明开始,到大括号之后终结。
{ } 里的内容是一个“块”,单独的{ }在执行顺序上没有改变,仍然是顺序执行,
类型拓展
类型双关
指的是在程序中使用一种类型的数据通过另一种不兼容的类型
来访问。这种操作有时可以绕过类型系统,直接操作内存
来访问。这种操作有时可以绕过类型系统,直接操作内存
一种常见的类型双关形式是使用指针强制转换
#include <iostream>
struct Entity{
int x, y;
};
int main(){
Entity e = {5, 8};
int* position = (int*)&e;
std::cout << position[0] << ", " << position[1] << std::endl;
// int y = *(int*)((char*)&e + 4);
// std::cout << y << std::endl;
}
struct Entity{
int x, y;
};
int main(){
Entity e = {5, 8};
int* position = (int*)&e;
std::cout << position[0] << ", " << position[1] << std::endl;
// int y = *(int*)((char*)&e + 4);
// std::cout << y << std::endl;
}
union关键字
联合体 (union) 是 C++ 中一种特殊的数据结构,它允许不同类型的成员共享相同的内存空间。通过联合体可以实现类型双关。
#include <iostream>
struct Vector2{
float x, y;
};
struct Vector4{
union{
struct{
float x, y, z, w;
};
struct{
Vector2 a, b;
};
};
};
void PrintVector2(const Vector2& vector){
std::cout << vector.x << "," << vector.y << std::endl;
}
int main(){
Vector4 vector = {1.0f, 2.0f, 3.0f, 4.0f};
PrintVector2(vector.a);
PrintVector2(vector.b);
vector.z = 500.0f;
std::cout << "--------------------------"<< std::endl;
PrintVector2(vector.a);
PrintVector2(vector.b);
}
struct Vector2{
float x, y;
};
struct Vector4{
union{
struct{
float x, y, z, w;
};
struct{
Vector2 a, b;
};
};
};
void PrintVector2(const Vector2& vector){
std::cout << vector.x << "," << vector.y << std::endl;
}
int main(){
Vector4 vector = {1.0f, 2.0f, 3.0f, 4.0f};
PrintVector2(vector.a);
PrintVector2(vector.b);
vector.z = 500.0f;
std::cout << "--------------------------"<< std::endl;
PrintVector2(vector.a);
PrintVector2(vector.b);
}
类型转换:安全机制
static_cast
dynamic_cast
比static_cast性能开销更高,沿继承层次结构进行强制类型转换,用于父类子类之间的转换,子类转换成父类可以直接进行隐式的转换,父类到子类需要进行dynamic_cast。如果转换失败,dynamic_cast方法返回null
使用
#include <iostream>
#include <string>
class Entity{
public:
virtual void PrintName(){
}
};
class Player : public Entity{
};
class Enemy : public Entity{
};
int main(){
Player* player = new Player();
Entity* e = player;
Enemy* enemy = new Enemy();
Player* p= dynamic_cast<Player*>(e);
}
#include <string>
class Entity{
public:
virtual void PrintName(){
}
};
class Player : public Entity{
};
class Enemy : public Entity{
};
int main(){
Player* player = new Player();
Entity* e = player;
Enemy* enemy = new Enemy();
Player* p= dynamic_cast<Player*>(e);
}
Runtime Type Information(RTTI)会存储我们所有类型的运行时类型信息,所以增加了性能开销,而且dynamic_cast也需要时间。
const_cast
reinterpret_cast
std::string相关性能优化
使用std::string_view 接收字符串,避免复制字符串对象。
不用substr截取字符串,而是使用std::string_view获取字符串片段,避免复制。
在C++17中,std::string_view 的优化主要体现在它的移动语义上。当你创建一个临时的 std::string 对象并将其传递给一个函数时,
该对象会被移动而不是复制。这可以极大地提高性能,尤其是在处理大量字符串的情况下。
该对象会被移动而不是复制。这可以极大地提高性能,尤其是在处理大量字符串的情况下。
visual studio 2019在小字符串内存分配上做了一些优化,如果不超过15个字符的小字符串,会在栈上分配。超过则在堆上分配。
代码
#include <iostream>
#include <string>
static uint32_t s_AllocCount = 0;
void* operator new(size_t size){
s_AllocCount++;
std::cout << "Allocating " << size << " bytes\n";
return malloc(size);
}
void PrintName(const std::string& name){
std::cout << name << std::endl;
}
void PrintName(std::string_view name){
std::cout << name << std::endl;
}
int main(){
std::string name = "Yan chernikov";
#if 0
std::string firstname = name.substr(0,3);
std::string lastname = name.substr(4,9);
#else
std::string_view firstname(name.c_str(), 3);
std::string_view lastname(name.c_str() + 4, 9);
#endif
PrintName(name);
PrintName(firstname);
PrintName(lastname);
std::cout << s_AllocCount << " allocation" << std::endl;
}
#include <string>
static uint32_t s_AllocCount = 0;
void* operator new(size_t size){
s_AllocCount++;
std::cout << "Allocating " << size << " bytes\n";
return malloc(size);
}
void PrintName(const std::string& name){
std::cout << name << std::endl;
}
void PrintName(std::string_view name){
std::cout << name << std::endl;
}
int main(){
std::string name = "Yan chernikov";
#if 0
std::string firstname = name.substr(0,3);
std::string lastname = name.substr(4,9);
#else
std::string_view firstname(name.c_str(), 3);
std::string_view lastname(name.c_str() + 4, 9);
#endif
PrintName(name);
PrintName(firstname);
PrintName(lastname);
std::cout << s_AllocCount << " allocation" << std::endl;
}
单例模式
当我们想要拥有用于某种全局数据集的功能(组织一堆全局变量和静态函数,比如渲染器和随机数生成器),只是想重复使用时,是比较有用的。
代码
优化前
#include <iostream>
class Singleton{
//这里可以防止实例的复制。
Singleton(const Singleton&) = delete;
public:
static Singleton& Get(){
return s_Instance;
};
void Function(){
}
private:
Singleton(){}
float m_Member = 0.0f;
static Singleton s_Instance;
};
Singleton Singleton::s_Instance;
int main(){
Singleton& instance = Singleton::Get();
//这种方式会复制一个新的singleton实例
//Singleton instanceCopy = Singleton::Get();
instance.Function();
}
class Singleton{
//这里可以防止实例的复制。
Singleton(const Singleton&) = delete;
public:
static Singleton& Get(){
return s_Instance;
};
void Function(){
}
private:
Singleton(){}
float m_Member = 0.0f;
static Singleton s_Instance;
};
Singleton Singleton::s_Instance;
int main(){
Singleton& instance = Singleton::Get();
//这种方式会复制一个新的singleton实例
//Singleton instanceCopy = Singleton::Get();
instance.Function();
}
优化后
#include <iostream>
class Random{
//这里可以防止实例的复制。
Random(const Random&) = delete;
public:
static Random& Get(){
static Random s_Instance;
return s_Instance;
};
static float Float(){
return Get().IFloat();
};
private:
Random(){}
float m_RandomGenerator = 0.5f;
float IFloat(){
return m_RandomGenerator;
};
};
//Random Random::s_Instance;
int main(){
//Random& instance = Random::Get();
//这种方式会复制一个新的singleton实例
//Random instanceCopy = Random::Get();
//instance.Float();
float number = Random::Float();
std::cout << number << std::endl;
}
class Random{
//这里可以防止实例的复制。
Random(const Random&) = delete;
public:
static Random& Get(){
static Random s_Instance;
return s_Instance;
};
static float Float(){
return Get().IFloat();
};
private:
Random(){}
float m_RandomGenerator = 0.5f;
float IFloat(){
return m_RandomGenerator;
};
};
//Random Random::s_Instance;
int main(){
//Random& instance = Random::Get();
//这种方式会复制一个新的singleton实例
//Random instanceCopy = Random::Get();
//instance.Float();
float number = Random::Float();
std::cout << number << std::endl;
}
常用关键字
const用法
修饰变量
const *
常量指针:指针指向的数据不可更改 。
* const
指针常量:指针指向的位置不可以更改。
修饰方法
const修饰方法的时候,就不允许方法里的变量被修改,
方法为只读,除非变量前面有mutable关键字。
方法为只读,除非变量前面有mutable关键字。
static
static 关键字在C++里有private的意思,修饰方法时,只能在当前文件调用该方法。
static 修饰class中的方法,可以直接通过类名调用该方法。
三元操作符
bool?exp1:exp2
三元操作符,可以避免一些构造字符串。
this
指向当前对象实例
auto
自动识别数据类型
int a = 5;
auto b = a;
auto b = a;
如果一个方法的返回值类型可能改变,那么用auto类型接收返回值还是比较方便的。
不过也可能造成依赖该返回值类型的代码被破坏。
不过也可能造成依赖该返回值类型的代码被破坏。
这种情况也可使用typedef using重命名类型再使用
如果一个方法有比较复杂的返回值类型,可以用auto,比如下面代码可以改造成右边这种形式
for(auto it = strings.begin();
it != strings.end(); it++)
{
std::cout << *it << std::endl;
}
it != strings.end(); it++)
{
std::cout << *it << std::endl;
}
namespace
使用
namespace主要是为了避免命名冲突而存在的。如果出现两个有相同函数签名,即相同的方法名,相同参数列表的方法,最好把它们包含在不同命名空间。
namespace可以嵌套多层使用
using namespace std;
使用
可以在文件开头使用,也可以在方法里面使用;
优点
会使得代码更加简洁。
缺点
容易让人混淆,搞不清函数是否来自标准库。如果遇到了其他外部的库和std标准库相同命名函数时,编译器编译时可能不会出现编译错误。所以在大型项目中,这种错误也难以追踪。
优化
最好是不要在文件开头使用namespace,而是在足够小的作用域使用namespace
enum
枚举类型
用来定义命名的整数值集合。
枚举成员是枚举类型的值,它们在定义时自动从0开始编号,每个成员依次加1。
enum class ErrorCode{
None = 0, NotFound = 1, NoAccess = 2
};
None = 0, NotFound = 1, NoAccess = 2
};
标准库
std::find_if()
#include <iostream>
#include <vector>
void ForEach(const std::vector<int>& values, void(*func)(int)){
for(int value: values)
func(value);
}
int main(){
std::vector<int> values = {1, 6, 3, 4, 5};
auto it = std::find_if(values.begin(), values.end(), [](int value){return value > 3;});
std::cout << "Larger than 3: " << *it << std::endl;
}
#include <vector>
void ForEach(const std::vector<int>& values, void(*func)(int)){
for(int value: values)
func(value);
}
int main(){
std::vector<int> values = {1, 6, 3, 4, 5};
auto it = std::find_if(values.begin(), values.end(), [](int value){return value > 3;});
std::cout << "Larger than 3: " << *it << std::endl;
}
std::sort()
默认排序
#include <iostream>
#include <vector>
int main(){
std::vector<int> values = {1, 7, 2, 3, 6, 5 };
std::sort(values.begin(), values.end());
for(int value: values){
std::cout << value << std::endl;
}
}
#include <vector>
int main(){
std::vector<int> values = {1, 7, 2, 3, 6, 5 };
std::sort(values.begin(), values.end());
for(int value: values){
std::cout << value << std::endl;
}
}
降序排序
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main(){
std::vector<int> values = {1, 7, 2, 3, 6, 5 };
std::sort(values.begin(), values.end(), std::greater<int>());
for(int value: values){
std::cout << value << std::endl;
}
}
#include <vector>
#include <algorithm>
#include <functional>
int main(){
std::vector<int> values = {1, 7, 2, 3, 6, 5 };
std::sort(values.begin(), values.end(), std::greater<int>());
for(int value: values){
std::cout << value << std::endl;
}
}
比较器排序
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main(){
std::vector<int> values = {1, 7, 2, 3, 6, 5 };
std::sort(values.begin(), values.end(), [](int a, int b){
return a > b;
});
for(int value: values){
std::cout << value << std::endl;
}
}
#include <vector>
#include <algorithm>
#include <functional>
int main(){
std::vector<int> values = {1, 7, 2, 3, 6, 5 };
std::sort(values.begin(), values.end(), [](int a, int b){
return a > b;
});
for(int value: values){
std::cout << value << std::endl;
}
}
c++17
std::optional
存储可能存在或者可能不存在的数据。
代码
#include <iostream>
#include <fstream>
#include <string>
#include <optional>
std::optional<std::string> ReadFileAsString(const std::string& filePath){
std::ifstream stream(filePath);
if(stream){
std::string result;
stream.close();
return result;
}
return {};
}
int main(){
std::optional<std::string> data = ReadFileAsString("data.txt");
std::string value = data.value_or("not present");
std::cout << "value:" << value << std::endl;
if(data.has_value()){
std::cout << "successfully\n";
}else{
std::cout << "file could not be opened!\n";
}
}
#include <fstream>
#include <string>
#include <optional>
std::optional<std::string> ReadFileAsString(const std::string& filePath){
std::ifstream stream(filePath);
if(stream){
std::string result;
stream.close();
return result;
}
return {};
}
int main(){
std::optional<std::string> data = ReadFileAsString("data.txt");
std::string value = data.value_or("not present");
std::cout << "value:" << value << std::endl;
if(data.has_value()){
std::cout << "successfully\n";
}else{
std::cout << "file could not be opened!\n";
}
}
std::variant
单一变量存放多种类型的数据
为我们创建了一个结构体或者类,将两种数据类型存储为那个类或者结构体中的成员
和union相比的话,union只分配占内存大小更大的数据类型的内存,更有效率更好,variant会存储两种数据类型。但是variant更加类型安全,不会造成未定义行为。
#include <iostream>
#include <variant>
int main(){
std::variant<std::string, int> data;
data = "Cherno";
std::cout << std::get<std::string>(data)<< "\n";
data = 2;
std::cout << std::get<int>(data)<< "\n";
}
#include <variant>
int main(){
std::variant<std::string, int> data;
data = "Cherno";
std::cout << std::get<std::string>(data)<< "\n";
data = 2;
std::cout << std::get<int>(data)<< "\n";
}
std::any
也是单一变量存放多种类型
小的类型,在底层把数据存储为union
大的类型,存储为void*,动态分配内存,但是不利于性能
代码
#include <iostream>
#include <variant>
#include <any>
int main(){
std::any anydata;
anydata = std::string("Cherno");
std::string& string = std::any_cast<std::string&>(anydata);
std::cout << "string: " << string;
}
#include <variant>
#include <any>
int main(){
std::any anydata;
anydata = std::string("Cherno");
std::string& string = std::any_cast<std::string&>(anydata);
std::cout << "string: " << string;
}
std::move
用移动语义取代复制语义
#include <iostream>
#include <memory>
class String {
private:
char* m_Data;
uint32_t m_Size;
public:
String() = default;
String(const char* string){
printf("created\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other){
printf("copyed\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other)noexcept {
printf("moved\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
String& operator=(String&& other)noexcept {
printf("operator=\n");
if(this != &other){
delete[] m_Data;
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
return *this;
};
~String(){
std::cout << "Destroy..." << std::endl;
delete m_Data;
}
void Print(){
for(uint32_t i=0; i< m_Size; i++){
printf("%c", m_Data[i]);
}
printf("\n");
}
};
class Entity{
private:
String m_Name;
public:
Entity(const String& name):m_Name(name){
};
//转换成移动语义
Entity(String&& name):m_Name(std::move(name)){
};
void PrintName(){
m_Name.Print();
}
};
int main(){
//Entity entity(String("Cherno"));
//Entity entity("Cherno");
//entity.PrintName();
String apple = "hello";
//下面这种写法不够清晰明了,改造成std::move会好很多
//String dest((String&&)string);
//String dest(std::move(string));
String dest;
apple.Print();
dest.Print();
dest = std::move(apple);
apple.Print();
dest.Print();
}
#include <memory>
class String {
private:
char* m_Data;
uint32_t m_Size;
public:
String() = default;
String(const char* string){
printf("created\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other){
printf("copyed\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other)noexcept {
printf("moved\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
String& operator=(String&& other)noexcept {
printf("operator=\n");
if(this != &other){
delete[] m_Data;
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
return *this;
};
~String(){
std::cout << "Destroy..." << std::endl;
delete m_Data;
}
void Print(){
for(uint32_t i=0; i< m_Size; i++){
printf("%c", m_Data[i]);
}
printf("\n");
}
};
class Entity{
private:
String m_Name;
public:
Entity(const String& name):m_Name(name){
};
//转换成移动语义
Entity(String&& name):m_Name(std::move(name)){
};
void PrintName(){
m_Name.Print();
}
};
int main(){
//Entity entity(String("Cherno"));
//Entity entity("Cherno");
//entity.PrintName();
String apple = "hello";
//下面这种写法不够清晰明了,改造成std::move会好很多
//String dest((String&&)string);
//String dest(std::move(string));
String dest;
apple.Print();
dest.Print();
dest = std::move(apple);
apple.Print();
dest.Print();
}
常用数据类型
整数
int
4个字节
字符串
char*
std:string
也是容器, 可以接收char*类型的数据
浮点型
float
4个字节
布尔类型
bool
1个字节
数组
普通数组
#include <array>
需要自己维护数组大小,没有api可获取数组大小。如果越界访问数组下标会有bounds violation错误
Array(静态数组)
std::array
可以通过size()方法获取数组大小
使用方法
std::array<int, 5> data;
data[0] = 2;
data[4] = 1;
std::cout << data.size() << std::endl;
data[0] = 2;
data[4] = 1;
std::cout << data.size() << std::endl;
数组下标从零开始是为了计算内存地址方便。
静态数组一般在堆上创建。速度很快。
数组的创建如果在堆上,可以避免内存跳跃。
Vector(动态数组)
定义
Vector来自c++的标准库的一种容器,与java里的ArrayList差不多的用法,不过它可以接收原始类型的数据,java中的ArrayList只能传入包装类型和Class的数据,每次容量不够Vector会重新申请更大的内存空间进行自动扩容。
性能优化
问题分析
如果直接通过push_back方法将对象塞入Vector尾部,c++会先构造一遍对象,再复制对象到预先分配好的对应Vector的连续内存空间中。这样会有额外的性能消耗。
如果我们没有预先设置动态数组的大小,也会导致初始长度大小为1的Vector不断进行扩容,这里也会造成对象的二次拷贝,产生性能开销。(overheads)。
解决方法
通过reserve方法预先设置好动态数组大小,这样能有限的减少扩容次数,自然就不需要复制对象到新申请的内存空间。
将push_back方法替换成emplace_back,这种最开始就在已经分配好的内存空间上直接构造对象,不需要复制对象。
补充知识点
Vector底层是储存在堆上的,std:array数组一般存储在栈上。所以技术上来讲std:array更快。
使用
std::vector<std::string> strings;
strings.push_back("Apple");
strings.push_back("Orange");
strings.push_back("Apple");
strings.push_back("Orange");
遍历
for(std::vector<std::string>::iterator it = strings.begin();
it != strings.end(); it++)
{
std::cout << *it << std::endl;
}
it != strings.end(); it++)
{
std::cout << *it << std::endl;
}
class/struct
struct
struct 里申明的变量默认作用域是public
class
class 里申明的变量默认作用域是private,这里的class也有java类中的继承等多态的属性。
多态
继承
如果一个基类有子类的话,需要确保申明的析构函数是虚函数。
也就是包含virtual关键字,销毁子类对象时,子类的析构函数
才能确保被调用。
也就是包含virtual关键字,销毁子类对象时,子类的析构函数
才能确保被调用。
#include <iostream>
class Base{
public:
Base(){
std::cout << "Base Constructor\n" << std::endl;
}
virtual ~Base(){
std::cout << "Base Destructor\n" << std::endl;
}
};
class Derived : public Base{
public:
Derived(){
std::cout << "Derived Constructor\n" << std::endl;
}
~Derived(){
std::cout << "Derived Destructor\n" << std::endl;
}
};
int main(){
Base* base = new Base();
delete base;
std::cout << "--------------------\n";
Derived* derived = new Derived();
delete derived;
std::cout << "--------------------\n";
Base* derived2 = new Derived();
delete derived2;
}
class Base{
public:
Base(){
std::cout << "Base Constructor\n" << std::endl;
}
virtual ~Base(){
std::cout << "Base Destructor\n" << std::endl;
}
};
class Derived : public Base{
public:
Derived(){
std::cout << "Derived Constructor\n" << std::endl;
}
~Derived(){
std::cout << "Derived Destructor\n" << std::endl;
}
};
int main(){
Base* base = new Base();
delete base;
std::cout << "--------------------\n";
Derived* derived = new Derived();
delete derived;
std::cout << "--------------------\n";
Base* derived2 = new Derived();
delete derived2;
}
访问修饰符
private
protected
public
函数/方法
性能提升
方法的参数列表需要加上&操作符去避免数据的复制。他真的很注重性能的提升。
纯虚函数
纯虚函数就是接口里没被实现的抽象方法。
构造函数
隐式构造函数
隐式构造函数很牛逼,是java里面没有的功能,但是咋说呢,我觉得可读性有点差。不过有了explicit这个关键字可以禁止隐式构造函数的使用。
拷贝构造函数
一般通过=实现对象的复制是一种浅拷贝(shallow copy),这个拷贝构造函数是为了实现深拷贝(deep copy)存在的。
无构造函数
操作符重载
使用operator关键字申明方法重载操作符,很神奇。java并不存在这种操作。所以说c++灵活性确实是高。+-*/这些都比较好理解。Cherno还重载了[]和->和new。箭头操作符可以比较方便的获取实例中的属性值。
void* operator new(size_t size){
return malloc(size);
}
return malloc(size);
}
函数指针
函数可以赋值给变量,可以作为参数传给另外一个函数,不带()赋值的时候,这时候赋值的就是一个函数的地址。
编译代码时,函数会被编译成cpu指令在我们的二进制文件中。当函数被调用时,我们要检索执行的指令所在的位置。
使用
作为变量
void hello(){
std::cout << "hello world"<< std::endl;
}
int main(){
auto func = hello;
func();
func();
}
std::cout << "hello world"<< std::endl;
}
int main(){
auto func = hello;
func();
func();
}
void hello(){
std::cout << "hello world"<< std::endl;
}
int main(){
void(*function)() = hello;
function();
function();
}
std::cout << "hello world"<< std::endl;
}
int main(){
void(*function)() = hello;
function();
function();
}
作为函数的参数
#include <iostream>
#include <vector>
void PrintValue(const int value){
std::cout << "Value: "<< value << std::endl;
}
void ForEach(const std::vector<int>& values, void(*func)(int)){
for(int value: values)
func(value);
}
int main(){
std::vector<int> values = {1, 2, 3, 4, 5};
ForEach(values, PrintValue);
}
#include <vector>
void PrintValue(const int value){
std::cout << "Value: "<< value << std::endl;
}
void ForEach(const std::vector<int>& values, void(*func)(int)){
for(int value: values)
func(value);
}
int main(){
std::vector<int> values = {1, 2, 3, 4, 5};
ForEach(values, PrintValue);
}
函数指针的类型
void(*function)();
匿名函数lambda
[]中决定参数以什么方式传递,=传入值,&通过引用的方式传入
#include <iostream>
#include <vector>
void ForEach(const std::vector<int>& values, void(*func)(int)){
for(int value: values)
func(value);
}
int main(){
std::vector<int> values = {1, 6, 3, 4, 5};
ForEach(values,[](int value){std::cout << "Value: "<< value << std::endl;});
}
#include <vector>
void ForEach(const std::vector<int>& values, void(*func)(int)){
for(int value: values)
func(value);
}
int main(){
std::vector<int> values = {1, 6, 3, 4, 5};
ForEach(values,[](int value){std::cout << "Value: "<< value << std::endl;});
}
c++如何处理多返回值的经验
返回一个struct构造的对象,里面包含多个返回值。哈哈哈,这个我在java里经常操作。
函数方法的参数列表写成带引用&操作符的形式,这种传入的就是实参的内存地址,代码会直接修改内存地址的内容。
返回tuple元祖和pair。在java里面也有pair类可以处理多返回值。这节课比较轻松。不过tuple获取数据要通过std:get的方式,或者first,second的方式,可读性很差。
tuple
#include <iostream>
#include <string>
#include <tuple>
std::tuple<std::string, int> CreatePerson(){
return {"Cherno", 24};
}
int main(){
std::tuple<std::string, int> person = CreatePerson();
std::string& name = std::get<0>(person);
int age = std::get<1>(person);
std::cout <<"name:" << name << ", age:" << age << std::endl;
}
#include <string>
#include <tuple>
std::tuple<std::string, int> CreatePerson(){
return {"Cherno", 24};
}
int main(){
std::tuple<std::string, int> person = CreatePerson();
std::string& name = std::get<0>(person);
int age = std::get<1>(person);
std::cout <<"name:" << name << ", age:" << age << std::endl;
}
结构化绑定
#include <iostream>
#include <string>
#include <tuple>
std::tuple<std::string, int> CreatePerson(){
return {"Cherno", 24};
}
int main(){
auto[name, age] = CreatePerson();
std::cout <<"name:" << name << ", age:" << age << std::endl;
}
#include <string>
#include <tuple>
std::tuple<std::string, int> CreatePerson(){
return {"Cherno", 24};
}
int main(){
auto[name, age] = CreatePerson();
std::cout <<"name:" << name << ", age:" << age << std::endl;
}
左值与右值
左值是有某种存储支持的变量,右值是临时值,不会存在很长时间。
#include <iostream>
void PrintName(const std::string& name){
std::cout << "[lvalue]" << name << std::endl;
}
void PrintName(const std::string&& name){
std::cout << "[rvalue]" << name << std::endl;
}
int main(){
std::string firstname = "Yan";
std::string lastname = "chernikov";
std::string fullname = firstname + lastname;
PrintName(fullname);
PrintName(firstname + lastname);
}
void PrintName(const std::string& name){
std::cout << "[lvalue]" << name << std::endl;
}
void PrintName(const std::string&& name){
std::cout << "[rvalue]" << name << std::endl;
}
int main(){
std::string firstname = "Yan";
std::string lastname = "chernikov";
std::string fullname = firstname + lastname;
PrintName(fullname);
PrintName(firstname + lastname);
}
左值引用只能接受左值,除非是用const
内存管理
内存分配
malloc
malloc和free一般会搭配使用申请和回收内存。
new
new关键字这里有点意思,在java中必须要通过new创建对象(在堆上),c++这里new也是可以在堆上创建对象或者其他类型的数据,然后返回对象的指针。
使用不含new普通方式创建对象时候,这种方式其实就只是在栈上创建了对象。
new需要我们使用delete操作符配合使用来做内存的回收。
代码
通过重写new操作符来追踪堆上的内存分配
new
//重写new操作符
void* operator new(size_t size){
std::cout << "allocating..." << size << std::endl;
return malloc(size);
}
void* operator new(size_t size){
std::cout << "allocating..." << size << std::endl;
return malloc(size);
}
delete
void operator delete(void* memory){
free(memory);
}
free(memory);
}
在栈和在堆上分配内存的对比
栈
通过查看汇编代码,能发现在栈中给数据分配内存只是简单一两条cpu指令的事,而且分配的内存位置都很接近,当方法执行完毕弹栈之后,栈和栈里占用内存的变量都会自动随之消亡。所以操作起来很快。
栈的数据量是比较少且连续的,根据局部性原理,大概率栈中数据会都在高速缓存中,不容易出现cache miss(没命中高速缓存,导致要读取磁盘)的问题。
堆
在堆中分配内存,需要现在内存空闲列表中找到空的大小满足要求的内存返回地址,再用malloc分配内存,操作完毕还需要用delete或者free方法,整个流程比较复杂,随之而来的就是更大的性能代价。
堆上分配数据的话,分配的内存地址极大可能是不连续的。cache miss概率会增加。不过Cherno也说目前在实践中这种问题出现的不是很频繁。
拓展
静态分析工具
pvs,要钱的。
0 条评论
下一页
为你推荐
查看更多