程序运行内存:
程序运行时,计算机会预留一段内存给它,1、程序指令,计算机需要把这些指令加载到内存 2、静态或全局变量 3、栈区 4、堆区
编译,链接,动态,静态库:
动态:程序运行时再去进行链接 静态:事先编译链接到exe程序中
lib文件:包含了dll中所有函数、变量的位置 dll文件:动态链接时用到 dll.lib文件:包含了dll中函数和符号的指针。与dll一起用,这样就不用询问dll里是否有某些函数的指针
如果某函数、变量是某文件里的,可用extern,表示从外部寻找该定义;
static用在链接时,意思是此变量或者函数只对本文件可见,对其他文件不可见;此外,static也相当于局部变量的意思,作用域为局部。
inline可以直接使用此函数,将调用的函数,直接换成该函数体
#pragma once表示只文件被包含一次;防止多次include该文件;
头文件相关:
<>表示编译器的绝对路径,也就是编译器包含了的路径,而 “” 表示从相对路径找,但是也可从绝对路径找;也就是说“” 的范围比<>大。
.h后缀的文件是c标准库的,而没有.h是c++标准库的 。相当于区分他们属于哪个标准库。
结构体和类的区别:
技术上讲,本质区别是结构体成员的默认为public,class类成员默认为private。
保留这两者的原因是保证c和c++的兼容性,因为c中没有类,只有结构体。
使用习惯上,个人偏向于:结构体尽量保证简洁,只对数据进行操作,复杂性小,不用它来做太多的事情。如果需要用到继承之类的结构,大量函数操作的话,用类比较好。
char字符串相关;
如图,如果是const char* & ref = a,将会报错,因为数组地址其实是不能改变,是const类型的,由此可以得出,如果要创建一个引用,这个引用的类型也要和原始数据一样,比如
const char* pa = a;
这里的a会 被隐式转换为const char*,所以可以。
const char*& ref = a;
而ref引用实际上与a的原始类型有关,因为a是数组,a所代表的首地址不能改变,所以要const类型的引用;而前一个const其实可以不要,因为我们可以修改,也可以不修改这个引用的地址的数据;
char a[] = "char Test";
const char* pa = "ss";
const char* const& ref = a;
如何让字符串更快呢?使用std::string_view内存视图,它是一个指向现有内存的指针,也就是一个const char 指针再加上大小size。使用它就避免了分配新的内存,而是直接指向之前的内存。
string字符串,如果大于设定的值,就会触发堆内存分配,我的电脑是15个字符串。
#include <string>
#include <iostream>
static int a = 0;
void* operator new(size_t size) {
//std::cout << size << " 分配了\n";
a++;
return malloc(size);
}
#if 1
void PrintName(const std::string_view str) {
std::cout << str << "\n";
}
#else
void PrintName(const std::string& str) {
std::cout << str << "\n";
}
#endif
void main() {
//std::string myname = "hys";
const char* myname = "hys";//这个相当于指向字符串常量区
PrintName(myname);
std::cout << a << "次内存分配\n";
}
static:
表示静态,作用域有限
用在类外部时,表示此变量或者函数只对本文件可见,对其他目录的文件不可见;此外,static也相当于局部变量的意思,作用域为局部。
用在类内部时,变成所有类成员共享此变量,在此类中只有一个实例。静态方法不能访问非静态成员
局部静态:用在函数中时,他的作用域就在函数内部,当第一次调用该函数时,它会创建一个实例,之后的函数调用都是用的这个实例。生命周期延续到永远
用法:比如要对结构体的变量进行初始化,用static实现,可以简化很多代码。
New:
使用new的时候,其实底层是调用了malloc函数,区别在于new创建类对象时,会调用构造函数,而malloc只是返回void类型的指针
可以通过new()来指定在特定的内存分配空间。
relloc,malloc,calloc区别:calloc比malloc多了个参数,第一个参数是数量,第二个是大小。最重要的是calloc可以对分配的空间进行初始化默认为0,而malloc不会初始化。
枚举:
示例:name为该枚举类的名称,int为枚举类型。
enum name :int{
A,B,C
};
枚举默认从零开始递增。
构造函数和析构函数:
类或结构体默认有一个无参数的构造函数。如下,构造函数的名称和类名一样,不需要返回类型。可以有多个构造函数,但他们的参数不能一样。如果不想让其他程序创建实例,可以通过把构造函数设为private类型,或者删除构造函数,比如:exam()=delete;
class exam{
exam(){} //构造函数,创建类对象时调用
~exam(){}//析构函数,类被销毁时自动调用
}
隐式构造函数和explict:
构造函数可以隐式调用,隐式转换只会进行一次,explict关键字使其不能隐式转换;
class examp{
public:
int a;
examp(int x)
:a(x)
{}
}
void main(){
examp e1=1;//这里编译器便进行了隐式构造,
examp e2(2);
}
虚函数和纯虚函数(接口):
如图,如果构建一个examp2类型的对象,将它作为examp类型的参数时,如果要调用ptr函数。会先从基类的ptr查找,而不是examp2里的ptr函数。使用虚函数可避免这种情况
class examp{
public:
virtual void ptr(){}//加入virtual关键字,表示为虚函数
virtual void log()=0;//等于0说明他为一个纯虚函数,纯虚函数必须在子类实现,才能创建类的实例
}
class examp2:public examp{
public:
void ptr() override{}//可以加入override,增加代码可读性,说明它是一个覆写的虚函数
}
虚函数缺点:会消耗额外内存来生成和存储v表。其次,会消耗时间来遍历v表。不过通常情况下影响很小。
纯虚函数必须被实现,才能创建类的实例 。
初始化列表:
class examp{
public:
std::string name;
int x,y//注意:如果为int* x,y,只有第一个x才是指针,y是int类型的。
examp()
:name("myname"),x(1),y(2)//如果name不用初始化列表,会创建两个name实例,然后把一个赋值给另外一个,造成额外的开销。使用初始化列表的话就只会创建一个。此外,int类型的除非给它赋值,不然不会创建实例。
{
}
}
如上,在构造函数后加冒号,然后跟上对象以及参数,就可以把参数赋值给该对象。使用时,要按照参数声明的顺序。因为无论你按什么顺序初始化列表,编译器都会按照声明的顺序来赋值,如果打破这个顺序,可能会导致各种依赖问题。
使用初始化列表的方法好处很多。
函数符重载:
如图:Vector2 operator+ (const Vector2 &v)可以对+进行重载,在使用该类对象的时候,就可以使用+号来进行运算;
class Vector2{
private:
int x,y;
public:
Vector2 add(int a,int b)
:x(a),y(b){}
Vector2 add(const Vector2& v){
return Vector2(x+v.x,y+v.y);
}
//重载"+"
Vector2 operator+ (const Vector2 &v){
return Vector2(this.x+v.x,this.y+v.y);
}
void main() {
Vector2 speed(2,3);//构建一个vector2
Vector2 position(3,4);//构建一个vector2
Vector2 result =speed+position;//两个vector2进行运算
)
智能指针:
作用域结束时自动调用析构函数,delete 指针,释放内存;需要导入头文件,include <memory>;
如图,便创建了两个智能指针,第二种方式更好,注意,智能指针不能隐式调用构造函数;
智能指针不能进行copy,赋值给其他指针。因为如果赋值,那这两个智能指针指向的是同一块内存,当一个unique销毁时,那另外一个指向的就是被释放的内存。看源码可以发现,其实赋值操作已经被重载删除了;
class Entity{
private:
int x;
public:
Entity (int a)
:x(a)
{
std::cout<<"create Entity!"<<std::endl;
}
~ Entity (){
std::cout<<"Destroy Entity!"<<std::endl;
}
}
void main(){
std::unique_ptr<Entity> e(new Entity(2)) ;//创建智能指针e
std::unique_ptr<Entity> e0 = std::make_unique<Entity>(3);
//另外一种方式创建智能指针e0,出于异常安全考虑,这种方式更好
}
共享指针、弱指针:
共享指针功能和unique差不多,不过他在创建的时候,会在内存里建一个引用计数内存块,每次进行赋值操作,引用计数加1,离开作用域,引用计数减1,当引用计数为0时,才会释放该指针。而使用弱指针,比如std::weak_ptr<Entity> e4 = e1;这种方式赋值,不会使引用计数增加;
和unique一样,不用new方式创建更好,这里是因为share创建的时候,会额外分配一块叫做控制块的内存,用来存储引用计数,如果先new Entity再传给share的构造函数,会有两次内存分配;如果std::shared_ptr<Entity>e3(new Entity(5))这种方式,他就会两者结合,效率更高;
include<iostream>
include<memory>//需要包含该头文件
class Entity{
private:
int x;
public:
//构造函数
Entity (int a)
:x(a){
std::cout<<"create Entity!"<<std::endl;
}
//析构函数
~ Entity (){
std::cout<<"Destroy Entity!"<<std::endl;
}
}
void main(){
std::shared_ptr<Entity>e1 = std::make_shared<Entity>(4);//创建共享指针
std::shared_ptr<Entity>e3(new Entity(5));//创建共享指针
std::weak_ptr<Entity> e4 = e1; //可以进行赋值操作,通过weak类型的,不会使引用计数增加
}
拷贝构造函数:
#include <iostream>
#include <stdlib.h>
class String {
public:
char* m_buffer;
int m_size;
public:
String(const char* other)
{
m_size = strlen(other);
m_buffer = new char[m_size + 1];
memcpy(m_buffer, other, m_size + 1);
}
char& operator[](unsigned int i) {
return m_buffer[i];
}
String(const String& other)
:m_size(other.m_size)
{
m_buffer = new char[m_size+1];
memcpy(m_buffer, other.m_buffer, m_size+1);
}
};
std::ostream& operator <<(std::ostream& stream, const String& string) {
stream << string.m_buffer;
return stream;
}
void main() {
String str="cherno";
String str2 = str;
str2[1] = 'p';
std::cout << str <<std::endl;
std::cout << str.m_buffer << std::endl;
std::cout << str2 << std::endl;
}
偏移量:
如图:0代表地址从0开始,或者是nullptr,数字几就是从几开始计算偏移量,在游戏或者渲染方面会用到;
#include <iostream>
struct vector3
{
float x, y, z;
};
void main() {
int offst = (int)(&(((vector3*)0)->x));
std::cout << offst << std::endl;
}
Vector:
和array不同,arrayt创建在栈区,vector创建在堆区。通常初始容量是1;在push_backl时,会将参数复制到vector所分配的内存中,会有多余的copy操作;如果vector的数据超出了它的大小,会自动扩容,将旧vector里的数据copy一份到新创建的vector;
常用:
vector.size(),返回vector大小
vector.push_back(),添加元素
verteies.reserve(3); 预先设定vector的容量
verteies.clear();清空该vector
verteies.erase(verteies.begin());移除某个元素,参数为迭代器
#include <iostream>
#include <vector>
struct Vertex
{
int x, y, z;
Vertex(int a, int b, int c)
:x(a), y(b), z(c)
{
std::cout<<"调用了构造函数"<<std::endl;
}
Vertex(const Vertex& v)
:x(v.x), y(v.y), z(v.z)
{
std::cout << "coping!" << std::endl;
}
};
std::ostream& operator<<(std::ostream &stream,Vertex& v) {
stream << v.x << "," << v.y << "," << v.z;
return stream;
}
void main() {
std::vector<Vertex> verteies;//创建vector数组
//###########优化#################
verteies.reserve(3); //预先设定vector的容量,
verteies.emplace_back(1,2,3);//传入构造函数的参数列表
verteies.emplace_back(4, 5, 6); //告诉vector在其构建的内存中
verteies.emplace_back(7, 8, 9);//把这些作为参数, 构建一个vertex对象
//这样就可以省去很多复制操作
//##########优化###################/
// verteies.push_back(Vertex(1,2,3));//添加元素
// verteies.push_back(Vertex(4,5,6));//添加元素
// verteies.push_back(Vertex(7, 8, 9));//添加元素
/*for (int i = 0; i < verteies.size(); i++) {
std::cout << verteies[i] << std::endl;
}*/
//增强for循环
for (Vertex& v :verteies) {
std::cout << v<< std::endl;
}
verteies.erase(verteies.begin());//移除某个元素,参数需要为迭代器
verteies.clear();//清空该vector
}
模板:
#include <iostream>
#include <string>
template <typename T>
void printer(T x) {
std::cout << x << std::endl;
}
template <int N>
class array
{
private:
int m_array[N];
int m_size;
public:
int GetSize(){
m_size =N;
return m_size;
};
};
void main() {
array<5> a;
printer(1);
printer(a.GetSize());
}
自动匹配相应的类型。如果有模板的函数,则函数在编译时不会被创建,也就不会报语法错误。
类型转换:
c++风格 有4种cast类型转换:static_cast 、const_cast 、dynamic_cast。使用cast的类型转换,可以搜索到在哪些地方使用了这些类型转换,还可以使代码安全友好点。
dynamic_cast动态类型转换,像一个函数。保证做的类型转换是有效的。他需要用到rtti(运行时的类型信息),然后再判断是不是该类型,所以有运行时的开销
预编制头文件:
可以将需要大量用到的头文件放在里面,预先编制为二进制文件,这样在编译项目的时候,就不需要重新去预编译这些文件,大大节省编译时间。
应该放什么文件进去呢? 需要大量用到的头文件,比如标准库,如果是少量用到的头文件或者自己写的,就尽量不要放进去,因为如果改变了,就需要重新预编制。也不该把如何文件都放进去。因为不知道这个cpp文件具体用到哪些头文件,代码复用、重建时会很麻烦
计时器:
#include <iostream>
#include <string>
#include <chrono>
static int a = 0;
void* operator new(size_t size) {
//std::cout << size << " 分配了\n";
a++;
return malloc(size);
}
class Timer {
std::chrono::time_point<std::chrono::steady_clock> startTime;
std::string name;
//const char* name;
public:
Timer()
:startTime(std::chrono::high_resolution_clock::now()),name()
{
name = "s";
}
Timer(std::string& s)
:startTime(std::chrono::high_resolution_clock::now()), name(s)
{
}
~Timer() {
auto endTime = std::chrono::high_resolution_clock::now();//计时器
long long end = std::chrono::time_point_cast<std::chrono::milliseconds>(endTime).time_since_epoch().count();
long long start = std::chrono::time_point_cast<std::chrono::milliseconds>(startTime).time_since_epoch().count();
auto time = end - start;
std::cout <<name << " 花费了----" << time << "ms(毫秒)\n";
}
};
void main() {
{
std::string str = "Function";
#if 0
Timer time(str);
#else
//const char* s = "Function1";
//Timer time("Function1");
Timer time(str);
#endif
for (int i = 0; i < 1000;i ++) {
std::cout << i << "\n";
}
}//计时
std::cout << a << "次内存分配\n";
}
内存分配:
左值右值:
左值就是可存储的变量,右值就是临时变量,左值引用只能引用左值,右值引用只能引用右值
很多const常量类型的引用,是因为兼容临时的右值和已存在的左值变量。两个&&就可以表示右值引用,但是此时不能引用左值。
不知道是什么,先存着:
#include <iostream>
#include <thread>
#include <chrono>
#include <algorithm>
#include <vector>
#include <tuple>
#include <string.h>
#include <optional>
#include <variant>
//std::optional<int >a;
//int b=a.value_or(100);//如果没有值,就使用括号里的默认值
如果没有值。。可以通过这个来判断文件是否打开
!a或者a.hasvalue()也可以
//if (!a) {
// return;
//}
std::variant<std::string, int> m;//包含多种类型
//a = 2;
m = "hys";
std::cout << std::get<std::string>(m)<<"\n";
//int v = std:get<1>(m);
//auto a = m.index();//返回索引,是第几个类型的数据
//std::cout << a << "\n";
auto mm = std::get_if<std::string>(&m);//返回指针类型的数据
std::cout << *mm << "\n";