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