【c++】笔记

C++基础教程笔记

预编译,编译,链接

预编译:

cpp文件先预编译生成.i文件
处理所有带#的语句
#include打包进来
#define修改参数
#if #endif判断是否成立

编译:

.i文件被编译器编译生成.o文件,语言代码转换为二进制指令
编译参数platform:编译器编译出的代码只能在目标平台运行(如x86,x64,andriod),编译时需选好配置信息
编译参数configuration:debug和release版本在效率上会有区别
编译参数optimization:是否开优化,对运行效率影响很大

链接:

.o文件被链接器(linker)链接成可执行文件
需要看程序是在编译的过程中出错还是链接的过程中出错

h文件只写声明,cpp文件写函数的定义,main.cpp中调用.h文件

或者在.h中写函数的定义,为了重复调用有定义的.h文件,(链接时因为有重复定义一个func,会报链接错误)需要加inline void func()或者static void func()。
Inline:内联函数
把内联的函数赋值到调用的地方,相当于宏命令
​Inline必须与函数的定义放在一起,inline void func()

struct和class区别

1.struct里面的变量和函数默认是public,class默认是private
2.class可以继承

static与extern变量或者函数

static的变量和函数:

只会在该翻译单元内部链接;(该cpp文件的私有变量)
(定义static的变量和函数,与其他cpp文件中的全局或者static的变量或者函数同名时,也能正常编译)

Extern的变量和函数:

表示该定义在其他的翻译单元;(extern linking)
(一个cpp为一个翻译单元)

类和结构体中的static变量

不管实例化多少个,类中的static变量只有一个,类中的静态变量不属于类中的成员,需要单独定义

class test
{
public:
  static int x;  //不属于类中的成员,需单独定义
  int y;
};
int test::x;    //此处单独定义,形式为int test::x, 这样链接器可以找到该变量
int main() 
{
  test e;
  e.y = 5;
  test::x = 9; 
  e.x = 10;    //与test::x = 10相当,重新赋值
  std::cout << e.x << std::endl;
  std::cout << e.y << std::endl;
}

所有实例中的static变量(方法)都是class::变量(方法)的地址,只有一个
但是static方法不能访问非static变量

局部静态Local static

Local static 变量
生存期:整个程序周期
作用范围:函数内或者其他局部域内

void func() 
{
  int i = 0;
  i++;
  std::cout << i << std::endl;
}
int main() 
{
  func();
  func();
  func();
}                   
//输出为 1 1 1,每次调用该func时会使i为0,并加1;
void func()
{
  static int i = 0;
  i++;
  std::cout << i << std::endl;
}
int main()
{
  func();
  func();
  func();
}                  
//输出为 1 2 3
//每次调用的时候i会在之前的基础上加1,因为生成周期变长

类中的构造函数与析构函数

构造:

构造函数在实例化时就会构造完成
可以在构造函数中设置变量或者初始化变量

class func
{
private:
  int X;
public:
  func() {
    X = 0;       //变量初始化
  }
  func(int x){   //函数重载 变量初始化的另一个函数
​    X = x;
}
~func(){}        //析构函数
}

如果写 func()=delete;则构造函数将被删除

析构:

~func(){}        //卸载变量,清理使用内存

继承

class father{};
class child : public father{};
child有father里面所有的变量和函数功能,并外加自己的功能,为了减少代码的重复

虚函数与纯虚函数

虚函数:

虚函数引入动态联编(dynamic dispatch)(对函数生成v表,进行对应分配)
父类中函数前面加入virtual,子类可对相同名字的函数进行重定义
实例化父类和子类,相同函数名执行的功能不同

class father{
  virtual std::string getname(){return "father";}
};
class child : public father {
  std::string getname() override {return "child";}
};
重写函数中加override,表示重写,增加可读性

虚函数不好的地方(但是影响很小,建议使用)
需要额外的内存来存储v表,增加内存开销
需要遍历v表,映射到正确的函数,增加性能开销

纯虚函数:

父类中没有实现的函数(没有函数主体),该父类不能被实例化

class father
{
  virtual std::string getname()=0; //=0表示纯虚函数,father不能被实例化
};

继承纯虚函数的子类如果没有定义该纯虚函数,该子类也不能被实例化
不同子类override的时候可以实现不同的功能,父类的纯虚函数相当于接口的意义

可见性

三个基础的可见性修饰符:private public protected(为了代码的可读性)

Private:

定义的内容只能在该类中可见,一般定义的是非人为赋值的变量
child不能访问父类中private中的内容
Friend(友元)关键字:可以访问私有成员

Protected:

Child可以访问父类中的protected中内容
在父类和子类外,protected中内容不可访问

Public:

类内和类外都可以访问

字符串

char:(c类型)

通常使用const,因为字符串一般不会更改

#include <string.h>
int main() {
  const char * name = "chorme";                      //尽量这么操作,右边的不变常量强制类型转换成指针赋值给左边
  const char name2[7]={'c','h','o','r','m','e',0};   //ok
  std::cout << name << std::endl;                    //打印只需写指针
}

""双引号默认是字符串的const char *,‘’单引号默认是字符,0表示该字符串结束
字符串从指针的内存地址开始,到内存中的0停止,内存中放的是字符串的ascii码
strlen(name):计算字符串长度,变量是const char *

int main() {
  const char * name = "chorme";
  std::cout << strlen(name)<< std::endl;
}                 
//strlen函数: size_t strlen(const char *__s)

std::string(c++类型)

正常都使用std::string 需要包含头文件

#include <string>
int main() 
{
  std::string name = "chorme";
  std::cout << name.size() << std::endl;
  std::cout << name.capacity() << std::endl;
}                    //name.有很多功能的类

字符串复制:

strcpy字符串复制
​ string->char:

std::string str="Pigmansasfkfiuse";
char * ch;
strcpy(ch, str.c_str());  // 把string对象转换成c中的字符串的样式
// char *strcpy(char *, const char *)

​ char—>char:

const char pth1[7] = "cherno";
char* pth;
strcpy(pth, pth1);
//or
const char* pth1 = "cherno";
char pth[7];
strcpy(pth, pth1);

copy字符串复制

char buffer[20];
std::string str ("Test string...");
std::size_t length = str.copy(buffer,6,5);         //计算拷贝的字符串的长度
//从第5个开始,复制6个字符
buffer[length]='\0';                               //加上最后0的截止符
std::cout << "buffer contains: " << buffer <<'\n'; //结果:string

memcpy字符串赋值

const char pth1[7] = "cherno";
char* pth;
memcpy(pth,pth1,strlen(pth1)+1);

字符串追加:

“Test”+ "geo"不行,这是const char array;不是字符串

  1. 显示调用string构造函数
std::string name2 = std::string("test") + "geo";
// 把两个相加的字符数组中的一个,显示调用string构造函数(std::string),创建成字符串,再附加上另一个数组
  1. 用append或者+=
std::string name = "Test"; //+ "geo";
name.append("geo");
name +="!hello";
std::cout << name <<'\n';

寻找字符串中的文本:

std::string name2 = std::string("test") + "geo";
bool iffind = name2.find("tge") != std::string::npos;
std::cout << iffind << std::endl;        
//返回1

字符串在函数中的传递—常量引用传递

传递只读的字符串进函数时,确保采用常量引用传递的方式,左值右值方式传入都可

void printstring(const std::string& string);
// 如果采用std::string string的方式传递实际上是复制了字符串传递进函数,效率低

字符串字面量

定义:双引号之间的一串字符

宽字符

const wchar_t * name2 = L"cherno";
大写的L表示后面的字符串字面值由宽字符组成

const常量

const修饰在const前面的量或者指针

常量指针:const int* 与 int const* 一样(const在*之前,const修饰的是int)

int MAX_AGE = 100;
const int *a = new int;
*a = 2;        //错误,内容不能更改
a = &MAX_AGE;  //指针可以更改

指针常量:int * const (const在*之后,const修饰的是指针)

int MAX_AGE = 100;
int * const a = new int;
*a = 2;        //内容可以修改
a = &MAX_AGE;  //错误,指针不能修改

const int * const

指针和内容都不能修改

类中的函数后面加const

class test
{
private:
  int* m_a, * m_b;    //变量前都要加*,否则后面声明的是int型,不是指针型
  int m_x, m_y;
  mutable int var;    //mutable打破不能修改承诺
public:
  int Get() const     //Get类函数承诺只读,不修改类成员
  {    
    var = 2;          //但可修改mutable变量
    return m_x;
  }
};
void printtest(const test& e)  //常量引用
{
  std::cout << e.Get() << std::endl;
}
// e为const生成的test类型的的变量,所以只能调用e中const修饰的类函数
//传入的是常量引用的形参,所以只能调用有const的函数方法

建议:类中的方法如果不要修改参数,尽量在类的方法后面加上const
​ 调用的时候用常量引用的方式调用

mutable关键字

  1. 在类中的const的函数里面可以有mutable可修改的变量
  2. lambda函数
int main(int argc, char** argv)
{
  int x = 8;
  auto f = [=]() mutable
  {
​    x++;        //不加mutable 这行会报错
​    std::cout << x << std::endl;
  };
  f();
  //因为是=,是通过值来传递,相当于复制了x为局部变量之后再加,所以x还是8
}
// [ ]里面可以写  引用:&x    值:x     所有变量值传递:=      所有变量引用传递:&

成员初始化列表

应该尽可能使用成员初始化列表

不使用成员初始化列表:

class Entity
{
private:
  std::string m_Name;
  int m_x;
public:
  Entity()
  {
    m_Name = "unknown";
  }
  Entity(const std::string& name)
  {
    m_Name = name;
  }
  const std::string Getname() const {return m_Name;}
};
int main() 
{
  Entity e;
  std::cout << e.Getname() << std::endl;
  Entity e1("chorme");
  std::cout << e1.Getname() << std::endl;
}

使用成员初始化列表:

类后面加 :变量(初始化值)
class Entity
{
private:
  std::string m_Name;
  int x;
public:
  Entity()
​    : m_Name("unknown"), x(0){} //成员初始化列表
​	//注意需要按照类成员定义的顺序进行初始化
  Entity(const std::string& name)
​    : m_Name(name){}
  const std::string Getname() const {return m_Name;}
};
int main() {
  Entity e;
  std::cout << e.Getname() << std::endl;
  Entity e1("chorme");
  std::cout << e1.Getname() << std::endl;
}

初始化列表初始化的顺序问题
C++标准规定

类成员变量通过初始化列表初始化时,与构造函数中的初始化列表中的变量顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存的次序有关系,而变量的内存排列顺序,早在编译期就根据变量的定义次序决定了。


class Person
{
public:
  Person(){}
  void show()
  {
      std::cout << "name is " << m_name << ",age is " << m_age;
      std::cout << ",level is " << m_level << std::endl;
  }
private:
  std::string  m_name;
  int          m_level;
  int          m_age;
};

基于列表初始化的构造函数初始化成员变量,我们定义初始化列表构造函数:

Person(const std::string name, const int age) :m_name(name), m_age(age), m_level(m_age/10){}

int main()
{
  Person  me("lucas", 35);
  me.show();
}

运行结果为name is lucas,age is 35,level is -85899346,此既是成员变量的初始化顺序问题
我们根据C++标准分析上述输出,由于m_level定义位置比m_age靠前,所以m_level首先会得到初始化,但是此时m_age还没有完成初始化,按照编译要求未初始化的基本类型将会得到一个随机值,所以就导致m_level也变成了一个随机值。
不能直接初始化一个成员变量的子成员,只能初始化成员变量本身

三元运算符

根据特定条件给变量赋值,if else的替代

static int s_level;
static int s_speed;
if (s_level>5)
  s_speed=10;
else
  s_speed=20;
//上面四行代码相当于下面一行代码  三元运算符
s_speed = s_level>5 ? 10 : 20;  //成立,结果是10,否则是20
s_speed = s_level>5 ? s_level<10 ? 10 : 20 :30; //但是尽量不要这么使用
//5-10之间为10,只满足条件1(大于5)为20,只满足条件2(小于10)为30

内存与new

C#中struct分配在栈区,new的时候也分配在栈区
class分配在栈区,new的时候分配在堆区

new的时候既分配内存,并构造

Entity *e = new Entity(); //分配了entity大小的内存,并构造了entity()函数
Entity *e = (Entity *)malloc(sizeof(Entity)); //只分配了entity大小的内存

new – delete需配套使用

new生成的是指针,delete删除的也是指针
不使用delete可能造成内存泄漏

Entity *e = new Entity();    // 创建一个类
delete e;                    // 删除一个类
Entity *r = new Entity[50];  // 数组形式创建
delete[] r;                  // 数组形式删除

隐式转换与显示转换(explicit)

隐式转换:

尽量减少使用隐式转换,需要什么类型传什么类型

class Entity
{
private:
  std::string m_Name;
  int x;
public:
  Entity()
    : m_Name("unknown"), x(0){}
  Entity(int age)
    : m_Name("unknown"), x(age){}
  Entity(const std::string& name)
    : m_Name(name){}
  const std::string Getname() const {return m_Name;}
};
void printentity(const class Entity& e){}
int main() 
{
  Entity a = 22;                       //隐式转换
  Entity a = "cherno";                 //隐式转换
  printentity(22);                     //阔以,22的int型隐式转换成entity型
  printentity("cherno");               //报错,const char的"cherno"先隐式转换成std::string,再隐式转换成Entity的类,但隐式转换只允许一次,因此类型不匹配
  printentity(std::string ("cherno")); //阔以,string隐式转换entity
  printentity(Entity("cherno"));       //阔以,const char的"cherno"隐式转换成std::string
}

Explicit:显示转换

禁止隐式转换,但是也不会经常使用

class Entity
{
  explicit Entity(int age)
    : m_Name("unknown"), x(age){}
};
int main() 
{
  Entity a = 22;            //报错,添加了explicit,不能隐式转换
  Entity a(22);             //正确写法
  printentity(22);          //报错
  printentity(Entity(22));  //正确写法
}

运算符与重载

operator 运算符与运算符重载(operator+运算符)

struct entity
{
  int X , Y;
  entity (int x, int y) : X(x),Y(y){}
  entity add(const entity& other)
  {
    return entity(X+other.X, Y+other.Y);
  }
  entity operator+(const entity& other)   // 加号+的运算符重载
  {        
    // return entity(X+other.X, Y+other.Y);
    return *this + other;                 // 重载之后可以这么使用
    return operator+(other);              // 也可以
    return add(other);                    // 也阔以
  }
};
int main() 
{
  entity e(2,3);
  entity r(4,5);
  entity w = e.add(r);
  entity b = e + r;                      //与上一行代码意思相同,但是要更加简洁
  //有上面的运算符重载方法可以使用+号
  std::cout << w.X << std::endl;         //输出6
  std::cout << b.X << std::endl;         //输出6
}

this关键字

this定义

指向该类的指针,相当于*const,不能更改该指针的指向

class Entity{
  public:
  int x, y;
  Entity(int x, int y)
  {
​    Entity* const e = this;          //this: *const
​		 Entity& e = *this;
​    this->x = x;
  }
  int Enti(int x, int y) const       //添加const,承诺不修改类成员
  {
  ​   const Entity* const e = this;   //this:const Entity* const
  ​		const Entity& e = *this;
  }
};

this另外用法

在类的内部调用类外部的函数,参数是该类:用this

class Entity
{
public:
  int x, y;
  Entity(int x, int y)
  {
    this->x = x;
    this->x = y;
    printentity(this);   //*this
  }
};
void printentity(Entity* e){}//const Entity& e

对象的生存期

局部变量生存期是大括号内

局部变量在栈上创建,不能在局部创建数组并返回指针:

//错误例子
int* Entity()
{
  int arry[50];  //在局部创建数组
  arry[0] = 0;
  return arry;   //只返回指针
}    
//生存期结束后,只返回指针,但是栈上的arry的内容已经全被清理
int main() 
{
  int* a = Entity();
  //接受了只返回的指针,但是指针对应地址上的内容已经被清除
  std::cout << a[0] << std::endl;
}

可以修改为:(推荐使用)

void Entity(int* e){} //只传进去一个地址,不会重新分配数组的内存
  //fill the array
int main() 
{
  int array[50];
  Entity(array);
}

把new指针自己封装成作用域指针(unique_ptr)

class Entity
{
private:
  int m_x,m_y;
public:
  Entity()
  {
    std::cout << "creat entity" << std::endl;
  }
  Entity(int x, int y) : m_x(x),m_y(y)
  {
    std::cout << "creat parama entity" << std::endl;
  }
  ~Entity()
  {
    std::cout << "delete entity" << std::endl;
  }
};
class scopedptr   //作用域指针实现函数
{
private:
  Entity *m_ptr;
public:
  scopedptr(Entity *e) : m_ptr(e){}
  ~scopedptr()
  {
    delete m_ptr;
  }
};
int main() 
{
  {       //空作用域
  scopedptr e(new Entity()); //new出来的指针通过sco函数变成作用域指针
  std::cout << "dhw" << std::endl;
  }
  //new Entity()生成的指针到此生存期结束,sco函数析构会删除该作用域指针
}

智能指针

推荐优先选用make_unique,然后选用unique_ptr
推荐优先选用make_unique,然后选用make_shared
需包含头文件 #include

unique_ptr :

  1. 不能copy:
    如果copy的话,释放其中一个指针,堆上的内存就会被释放,复制的其他指针可能指向被释放的内存,会报错
  2. 没有什么开销
  3. unique_ptr可以转换成shared_ptr指针,优先选用unique_ptr

shared_ptr:

  1. 可以copy
  2. 会生成引用计数:
    引用计数存储了复制的指针总数量,释放一个,计数-1,最后一个释放后,计数为0,内存才会被释放
  3. shared_ptr不能转换成unique_ptr指针

weak_ptr

通过weak_ptr的方式复制指针,不会增加引用计数

占位大小

在 32 位机器上,std::unique_ptr 占 4 字节,std::shared_ptr 和 std::weak_ptr 占 8 字节。
在 64 位机器上,std::unique_ptr 占 8 字节,std::shared_ptr 和 std::weak_ptr 占 16 字节。
class Entity
{
private:
  int m_x,m_y;
public:
  Entity()
  {
​    std::cout << "creat entity" << std::endl;
  }
  Entity(int x, int y) : m_x(x),m_y(y)
  {
​    std::cout << "creat parama entity" << std::endl;
  }
  ~Entity()
  {
​    std::cout << "delete entity" << std::endl;
  }
  void printe(){}
};
int main() 
{
  std::shared_ptr<Entity> e4;
  {      //新的空作用域
  //unique_ptr:
  std::unique_ptr<Entity> e(new Entity());
  std::unique_ptr<Entity> e1 = std::make_unique<Entity>();
  //比上一行安全:如果构造过程中异常,不会得到悬空指针,而造成内存泄漏,推荐使用std::make_unique生成

  //shared_ptr
  std::shared_ptr<Entity> e2(new Entity());
  //该语句做了两次分配:new Entity分配,shared_ptr控制块内存分配(存储引用计数)
  std::shared_ptr<Entity> e2 = std::make_shared<Entity>();
  //同时分配new和控制块内存,推荐使用std::make_shared
  std::shared_ptr<Entity> e3 = e2;
  std::shared_ptr<Entity> e4 = e2;
  //shared_ptr可以复制

  //weak_ptr的方式会复制指针,但是不会增加引用计数
  std::weak_ptr<Entity> e5 = e2;
  e1->printe(); //得到指针之后就可以正常操作
  }
  //运行到这边时,还有一个Entity的内存存在,因为e4作用域在main中,下面的大括号运行完才会销毁,其他的e的内存已经销毁,因为已经超出空作用域
}

拷贝

拷贝变量:

struct vector2
{
  float x , y;
};
int main() 
{
  vector2 a = {2,5};
  vector2 b = a; //复制了a的变量,存在a和b两个变量
  b.x = 6;       //只改变了b中的变量,a中的变量不变
  vector2* c = new vector2();
  vector2* d = c; //复制了c的指针,没有复制c中的变量内容
  //此时c和d两个指针都指向同一块内容
  c->x = 5;  //同时改变了c和d指针指向的内容
}

浅拷贝:

只复制当前一级的内容,当前级中有指针,不会复制指针指向的内容

class String1  //定义字符串复制的类
{
private:
  char* m_buffer;
  unsigned int m_size;
public:
  String1(const char* string)
  {
    m_size = strlen(string);
    m_buffer = new char[m_size+1];
    // strcpy(m_buffer, string);          //这种写法也可以
    memcpy(m_buffer, string, m_size+1);
  }
  char& operator[](unsigned int index)    //运算符[]重载
  {
    return m_buffer[index];
  }
  friend std::ostream& operator<<(std::ostream& stream, const String1& string);
  //定义了友元函数,友元函数内可以调用该函数的私有变量
  ~String1()
  {
    delete[] m_buffer;                    //new出来的需要delete掉
  }
};
std::ostream& operator<<(std::ostream& stream, const String1& string)
{   //重载<<,使std能输出String1类中的字符串
  stream << string.m_buffer;
  return stream;
}
int main() 
{
  String1 e("cherno");
  String1 w = e;
  //此处为浅拷贝,只拷贝了char*和size,所以e和w的char*是一样的,通过char*修改一个实例,另一个也会跟着修改
  w[2] = 'a';
  //此处虽然看上去只修改了w中字符串的内容,但实际上e和w都修改了
  //运行结束后会造成内存崩溃,因为e和w的char* m_buffer是一样的,delete删除一个,再删第二个的时候会造成内存崩溃
  std::cout << e << std::endl;
  std::cout << w << std::endl;
}
​//结果输出两遍”cherno”,并且有内存报错
//因为是浅拷贝,两个m_buffer地址相同并指向同一个内存,修改一个之后,另一个也会修改,并且析构删除第二个的时候会出现内存错误,可以用深拷贝解决

深拷贝:

​ 在类中定义深拷贝的方法(如果不定义,则c++默认会使用浅拷贝的方式)

class String1  //定义字符串复制的类
{
private:
  char* m_buffer;
  unsigned int m_size;
public:
  String1(const char* string)  //构造函数
  {
    m_size = strlen(string);
    m_buffer = new char[m_size+1];
    // strcpy(m_buffer, string);   这种写法也可以
    memcpy(m_buffer, string, m_size+1);
  }
  // //c++自带的默认的浅拷贝函数
  // String1(const String1& other): m_buffer(other.m_buffer), m_size(other.m_size){}
  // //在类中编写不准拷贝的函数
  // String1(const String1& other) = delete;
  // 改写变成深拷贝的函数  常量引用
  String1(const String1& other):m_size(other.m_size)
  {
    m_buffer = new char[m_size+1];
    memcpy(m_buffer, other.m_buffer, m_size+1);
  }
  char& operator[](unsigned int index)
  {
    return m_buffer[index];
  }
  friend std::ostream& operator<<(std::ostream& stream, const String1& string);
  //定义了友元函数,友元函数内可以调用该函数的私有变量
  ~String1()
  {
    delete[] m_buffer;  //new出来的需要delete掉
  }
};
std::ostream& operator<<(std::ostream& stream, const String1& string)
{   //重载<<,使std能输出String1类中的字符串
  stream << string.m_buffer;
  return stream;
}
int main() 
{
  String1 e("cherno");
  String1 w = e;          // 根据深拷贝函数,w中会new char,并把e的size,和buffer里面的内容拷贝进去,形成w
  w[2] = 'a';             // w和e是独立的,只改变了w中的字符串
  std::cout << e << std::endl;
  std::cout << w << std::endl;
}
// 输出:
// “cherno”和“charno”
// 并且没有delete的内存错误

string使用常量引用的方式避免多次复制

void printstring(String1 string)
{
  std::cout << string << std::endl;
}
int main() 
{
  String1 e("cherno");
  String1 w = e;  //复制第一次
  w[2] = 'a';
  printstring(e); //复制第二次
  printstring(w); //复制第三次
}
//应该把printstring中的变量改为常量引用,避免多次复制

修改为:

void printstring(const String1& string)
{
  std::cout << string << std::endl;
}

上文修改后,上上文的代码只会复制一次(复制的第一次)
总是用const & 常量引用 去传递变量

箭头运算符

箭头操作符重载

class Entity
{
private:
  int m_x;
public:
  Entity(){}
  void printstr() const{std::cout << "hello" << std::endl;}
  ~Entity(){}
};
class scostr
{
private:
  Entity* m_ptr;
public:
  scostr(Entity* e):m_ptr(e){}
  ~scostr()
  {
​    delete m_ptr;
  }
  Entity* Getptr(){return m_ptr;}
  Entity* operator->(){return m_ptr;} 
  //->运算符重载后,可通过->直接得到entity的类指针
};
int main() {
  Entity* w = new Entity();
  w->printstr();
  scostr e = new Entity();
  e.Getptr()->printstr();
  //希望使用e->printstr();就需要重载->运算符
  e->printstr();
  //->运算符重载后,与Entity指针用法类似(w->printstr())
}

获取内存中变量的偏移量

从0开始访问变量(nullptr)

struct vector3
{
  float x, y, z;
};
int main() {
  int e = (int)&((vector3*)0)->x;  //x输出0,y输出4
  std::cout << e << std::endl;
}

vector

包含头文件

#include <vector>

内存分配机制

当超过分配的大小,会在内存创建一个比第一个大的新数组,把所有东西都复制到新的地方,然后删除旧的内存空间

尽量在vector中存储对象,而非指针

在vector中存储对象,是一个连续的内存空间
在vector中存储指针,指针指向的内容存储在内存的各个地方(内存碎片上)
struct vertex
{
  float x,y,z;
};
std::ostream& operator<<(std::ostream& stream, const vertex& e)
{
  stream << e.x << " , " << e.y << " , " << e.z << std::endl;
  return stream;
}
int main()
{
  std::vector<vertex> vertics;
  vertics.push_back({1,2,3});
  vertics.push_back({4,5,6});
  for(int i = 0; i<vertics.size(); i++)
  {
    std::cout << vertics[i] << std::endl;
  }
  vertics.erase(vertics.begin()+1); //移除第二个元素
  for(vertex v : vertics)           //这种方式会复制,用常量引用的方式传递
  {
    std::cout << v << std::endl;
  }
  for(const vertex& v : vertics)    //常量引用的这种方式比上面好
  {
    std::cout << v << std::endl;
  }
}

优化使用vector

struct vertex
{
  float x,y,z;
  vertex(float x, float y, float z)
    :x(x),y(y),z(z){}
  //定义copy函数
  vertex(const vertex& other):x(other.x),y(other.y),z(other.z)
  {
​    std::cout << "copy" << std::endl;
  }
};
int main() 
{
  std::vector<vertex> vertics;
  vertics.push_back(vertex(1,2,3)); //main中复制到vector内存
  vertics.push_back(vertex(4,5,6));
  vertics.push_back(vertex(7,8,9));
}
// 输出结果:6次copy

Copy六次原因
A.创建vertex结构体在main的当前栈中创建的,再复制到vector分配的内存中
因为该原因copy了三次
B.这个vector默认大小是1,push_back的时候扩展到2,再扩展的时候复制到3
因为该原因也copy了三次
修改为

int main() 
{
  std::vector<vertex> vertics;
  vertics.reserve(3);  //分配三个大小的内存
  vertics.emplace_back(1,2,3);
  vertics.emplace_back(4,5,6);
  vertics.emplace_back(7,8,9);
  //push_back传递已经构造的vertex的对象
  //emplace_back只是传递构造函数的参数列表,在vector内存中,使用传进去的参数列表构造vertex对象
}
// 输出结果:无任何copy发生
// 运行的速度会更快

C++中使用库

库的使用

在项目中包含依赖库的源代码,然后将其编译成动态库或者静态库
或者链接依赖库的二进制文件,更快更容易

库的组成

**Includes**
头文件,包含后我们可以实际使用预构建的二进制文件中的函数
additional include directories	
**Library**
预构建的二进制文件:动态库和静态库文件
additional library directories
additional dependencies:动态库/静态库

静态库:

在编译时链接到可执行文件中
更快,并可优化,因为编译器和链接器知道更多,可在执行链接时就优化
推荐优先使用静态库的方式
可执行文件可单独运行

动态库:

在运行可执行文件时链接
是个单独的模块,在运行时被加载
可执行文件和动态链接库需在一起(或者动态链接库能被可执行文件找到)

C++处理多返回值

struct组成结构体返回

最好用struct,也可以用array或者vector的方式
Array会在栈上创建,vector会在堆上创建,技术上说,返回array会更快一点
struct vertexsource
{
  std::string vs;
  std::string fs;
};
std::ostream& operator<<(std::ostream& stream, vertexsource e){
  stream << e.fs << " , " << e.vs << std::endl;
  return stream;
}
static vertexsource vertex()
{
  std::string vs = "che";
  std::string fs = "rno";
  return {vs, fs};  //返回结构体
}
int main() 
{
  vertexsource sources = vertex();
  std::cout << sources << std::endl;
}

模板

定义:

有点像宏,让编译器基于自己定的规则写代码
允许定义一个可以根据用途编译的模板
如果只是输入的参数不同,可以使用模板,减少代码的重复量
模板并不是真实存在的,直到调用他们,才会创建,转换成真正的代码

不用模板1:

void prinftest(int e)
{
  std::cout << e << std::endl;
}
void prinftest(std::string e)
{
  std::cout << e << std::endl;
}
int main() 
{
  prinftest(5);
  prinftest("cherno");
}

使用模板1:

template<typename T> //typename 模板参数的类型,T 名字
void prinftest(T e)
{
  std::cout << e << std::endl;
}
int main() 
{
  prinftest(5);           //调用的时候,基于传递的参数,T会自动识别参数类型,函数才会被创建成源代码
  prinftest<int>(6);      //<>内是模板参数
  prinftest("cherno");
  prinftest<std::string>("cherno");
}

不用模板2:

#define size 5
class Array 
{
public:
  int m_array[size];
public:
  int Getsize() const {return size;}
};
int main() 
{
  Array a;
  a.m_array[0] = 0;
  std::cout << a.m_array[0] << std::endl;
}

使用模板2:

template<typename T,int N> //typename 模板参数的类型,T 名字
class Array
{
public:
  T m_array[N]; //类型和大小都是模板
public:
  int Getsize() const {return N;}
};
int main()
{
  Array<int, 5> a;  //实例化的时候给模板参数赋值
  a.m_array[0] = 2;
  std::cout << a.m_array[0] << std::endl;
}

堆和栈对比

尽量在栈上分配变量,除非太大或者需要生存周期比较长

栈:

预定义大小的内存区域,通常2M字节左右

分配:
分配速度非常快,栈上的变量物理地址都靠的比较近
原理:
在栈上分配变量时,栈指针向前移动变量所需的字节数,再分配再往前移动栈指针,变量在内存中相互叠加存储,从高指针位置往前分配变量,反向生长,我们所做的就是移动栈指针,所以速度很快
生存期:
作用域结束的时候,栈上内存会被释放,栈指针自动移动到进入作用域之前的位置,释放内存的过程没有任何开销

堆:

预定义的区域,但是能增加大小,并随着应用程序的进行而改变

分配:
​ 用new或者make_unique或者make_shared分配(new需要delete)
原理:
​ 在堆上分配的变量,内存地址之间没有关系
​ 实际上调用了malloc函数(memory allocate),启动时会得到一定数量的ram,程序会维护一个空闲列表(free list),malloc的时候,浏览空闲列表,找到≥需要的内存空间,返回内存指针,同时记录分配的内存大小和目前分配的情况
生存期:
​ 作用域结束或者delete的时候会释放

栈和堆的相同点

这两个内存的物理位置在ram中是一样的

栈和堆的不同点

栈和堆最大的区别是分配的方式不同,导致快慢不同
//栈上分配
int value = 5;
//堆上分配 用new或者make_shared来分配
std::shared_ptr<int> hvalur = std::make_shared<int>();
*hvalur = 5;
理论上,如果事先在堆上分配好内存,往里面传内容的话,也会很快

C++的宏

将代码中的文本替换成其他东西

用于不同模式的切换

调试模式或者非调试模式
Ros版本或者DDS版本的切换
#define IF_DEBUG 1
#if IF_DEBUG == 1   //调试模型下,打log
#define LOG(x) std::cout << x << std::endl
#else        
#define LOG(x)    //非调试模型下,不打log
#endif
int main() 
{
  LOG("cherno");
}

array vector

array分配在栈上,vector分配在堆上
尽量使用std::array<int, 6> e,代替int w[5],array有边界检查
std::array<int, 6> e;
e[0] = 0;
std::cout << e[0] << std::endl;
当不知道array的size的时候,用模板:

template<typename T>
void printarray(const T& data)
{
  for(int i = 0;i < data.size();i++)
  {
​    std::cout << data[i] << std::endl;
  }
}
int main() 
{
  std::array<int,1> e;
  e[0] = 0;
  printarray(e);
}

函数指针

将函数赋值给变量,可以把函数指针作为参数给另一个函数(有点像回调函数)

void printvalue (int& e)
{
  std::cout << e << std::endl;
}
void print1(int& e)
{
  std::cout << 1 << std::endl;
}
void foreachvec(const std::vector<int>& vecs, void(*func)(int&))   // void(*func)(int&)是函数指针//指代void print(int& e)的函数指针
{
  for(int vec : vecs)
      func(vec);
}
int main() 
{
  std::vector<int> vec = {1,2,5,3,8};
  foreachvec(vec,print1);
}
// 调用print1的函数指针,输出5个1
// 调用printvalue的函数指针,输出1,2,5,3,8
  1. Lambda

匿名函数,快速的一次性函数
只要有函数指针,都可以在c++中使用lambda

lambda匿名函数形式

[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
函数体;
};

[] 外部变量访问方式说明符

[=]:parse everything by value
[&]:parse everything by reference
[变量名]:parse 变量 by value
[&变量名]:parse 变量 by reference

() 参数

Lambda 传入的参数, 可以省略

mutable

默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。

noexcept/throw()

默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。

返回值类型

指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略 ->返回值类型

{} 函数的结构体

int main()
{
    std::cout << "\n" << std::endl;
    // auto addFunction= [](int a,int b) ->int { return a + b; };
    // auto addFunction= [](int a,int b) ->void { std::cout << a + b << std::endl; };
    auto addFunction = [](int a,int b){ return a + b; };
    int result  = addFunction(1,2);
    std::cout << result << std::endl;
}
#include <algorithm>
int main() {
  std::vector<int> vec = {1,2,5,3,8};
  auto it = std::find_if(vec.begin(), vec.end(),[](int value){return value>3;});  //lambda函数:输出大于3的值
  std::cout << *it << std::endl;
}
//全局变量
int all_num = 0;
int main()
{
    //局部变量
    int num_1 = 1;
    int num_2 = 2;
    int num_3 = 3;
    std::cout << "lambda1:\n";
    auto lambda1 = [=]{   // 值的方式传递
        //全局变量可以访问甚至修改
        all_num = 10;
        //值的方式传递,函数体内只能使用外部变量,而无法对它们进行修改
        std::cout << num_1 << " "
             << num_2 << " "
             << num_3 << std::endl;
    };
    lambda1();
    std::cout << all_num << std::endl;
    std::cout << "lambda2:\n";
    auto lambda2 = [&]{  // 引用的方式传递
        all_num = 100;
        //引用的方式传递,函数体内可以使用外部变量,并且可以修改
        num_1 = 10;
        num_2 = 20;
        num_3 = 30;
        std::cout << num_1 << " "
             << num_2 << " "
             << num_3 << std::endl;
    };
    lambda2();
    std::cout << all_num << std::endl;
    return 0;
}

// 输出的结果:
lambda1:
1 2 3
10
lambda2:
10 20 30
100
  1. 计时

使用chrono的库 #include
Chrono可以高精度计时,几乎适用于所有的平台
建议用这种方式来计算时间

struct Timer // 打包进结构体后,在需要计时的作用域开头创建对象就可以了
{
  const char* Name;
  std::chrono::_V2::system_clock::time_point start, end;
  // 不同的编译系统可能关键字不太一样
  std::chrono::duration<float> duration;
  Timer()
  {
​    start = std::chrono::high_resolution_clock::now();
  }
  Timer(const char* name):Name(name)
  {
​    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<<Name<<" timer took: "<<ms<<" ms"<<std::endl;
  }
};
void Fun()
{
  Timer timer;
  for(int i = 0; i < 100; i++)
  {
  ​  std::cout << "Hello\n" ;
  }
}
int main()
{
  Fun();
}
  1. 多线程

环境配置:

  1. 添加头文件:#include
  2. 修改tasks.json文件
"args": [
​        "-fdiagnostics-color=always",
​        "-g",
​        "${file}",
​        "-o",
​        "${fileDirname}/${fileBasenameNoExtension}",
​        "-lpthread",
​        "-std=c++17"  //std::async需要
​      ],

c++: thread用法:

static bool s_ifend = false;
void Dowork()
{
  using namespace std::literals::chrono_literals; //sleep的库
  while(!s_ifend)
  {
    std::cout << "working...." << std::endl;
    std::this_thread::sleep_for(1s);
  }
}
int main()
{
  std::thread work1(Dowork); // 函数指针
  work1.join(); // wait:阻塞当前的main线程,等待work1线程完成工作
  std::cin.get();
  s_ifend = true;
  std::cout << "Finished" << std::endl;
  std::cin.get();
}

当在terminal中输入enter,Dowork的线程运行结束(进程间通信)

C++11:std::async

需要长时间运算的线程,并需要计算出最终的有效值,但是现在不迫切需要这个数值,std::thread不能接收返回值,需要用到std::async

  1. 函数原型

需要#include 头文件
构造函数:

template <class Fn, class... Args>
  future<typename result_of<Fn(Args...)>::type>async (Fn&& fn, Args&&... args);
template <class Fn, class... Args>
  future<typename result_of<Fn(Args...)>::type>async (launch policy, Fn&& fn, Args&&... args);
  1. launch policy:

这是什么类型的工作(同步还是异步)
1)std::launch::async 传递的可调用对象异步执行;
2)std::launch::deferred 传递的可调用对象同步执行;
3)std::launch::async | std::launch::deferred 异步或同步,取决操作系统,无法控制;
4)如果不指定策略,则相当于上一条。

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <future>
std::string fetchDataFromDB(std::string recvData)
{
  Timer timer("fetchDataFromDB");
  std::cout << "fetchDataFromDB start: " << std::this_thread::get_id() << std::endl;
  std::this_thread::sleep_for(seconds(5));
  return "DB_" + recvData;
}
std::string fetchDataFromFile(std::string recvData)
{
  Timer timer("fetchDataFromFile");
  std::cout << "fetchDataFromFile start: " << std::this_thread::get_id() << std::endl;
  std::this_thread::sleep_for(seconds(3));
  return "File_" + recvData;
}
int main()
{
  Timer timer("main");
  std::cout << "main start: " << std::this_thread::get_id() << std::endl;
  // 从文件获取数据
  // async 传递的可调用对象异步执行
  std::future<std::string>resultFromDB= std::async(std::launch::async, fetchDataFromDB, "Data");
  // deferred 传递的可调用对象同步执行
  std::future<std::string> fileData=std::async(std::launch::deferred, fetchDataFromFile, "Data");
  // 调用get函数fetchDataFromFile才开始执行
  std::string FileData = fileData.get();
  // 如果fetchDataFromDB()执行没有完成,get会一直阻塞当前线程
  std::string dbData = resultFromDB.get();
  // 组装数据
  std::string data = dbData + " :: " + FileData;
  // 输出组装的数据
  std::cout << "Data = " << data << std::endl;
  return 0;
}

输出结果:
main start: 140737473742656
fetchDataFromDB start: 140737473677056
fetchDataFromFile start: 140737473742656
fetchDataFromFile timer took: :3000.69 ms 
fetchDataFromDB timer took: :5000.47 ms 
Data = DB_Data :: File_Data
main timer took: :5003 ms
fileData线程是同步调用,在main的线程中,和main线程的id一样
fileData线程在get的时候才真正开始执行,3s后执行结束
resultFromDB子线程是异步调用,get阻塞main线程等待子线程结束,5s后才会执行组装数据和输出的指令;如果异步的resultFromDB没有get函数,输出结果如下:
main start: 140737473742656
fetchDataFromDB start: 140737473677056
fetchDataFromFile start: 140737473742656
fetchDataFromFile timer took: :3000.62 ms 
FileData = File_Data
fetchDataFromDB timer took: :5000.26 ms 
main timer took: :5001.45 ms
异步的resultFromDB子线程没有get函数,会先执行输出的指令,5s结束后,resultFromDB子线程才运行结束,最后main才执行结束
  1. 执行结果:

可以用get、wait、wait_for、wait_until等待执行结束,

  1. get**:**
    可以获得执行的结果,例子如上
    选择异步:调用get时,如果异步执行没有结束,get会阻塞当前调用线程,直到异步执行结束并获得结果
    选择同步:只有当调用get函数时,同步调用才会被真正执行,也被称为函数调用被延迟
  2. wait:
    等待与当前std::future 对象相关联的共享状态的标志变为 ready。如果共享状态的标志不是 ready,调用该函数会阻塞当前线程,直到共享状态的标志变为 ready,一旦共享状态的标志变为 ready,wait() 函数返回,当前线程被解除阻塞
    例子:在main的主线程中等待resultFromDB子线程运行结束
int main()
{
  Timer timer("main");
  std::cout << "main start: " << std::this_thread::get_id() << std::endl;
  std::future<std::string> resultFromDB = std::async (std::launch::async, fetchDataFromDB, "Data");
  resultFromDB.wait();
  return 0;
}

输出结果:
main start: 140737473742656
fetchDataFromDB start: 140737473677056
fetchDataFromDB timer took: 5000.69 ms
main timer took: 5002.17 ms

  1. wait_for**:**
    wait_for 和wait_until都返回std::future_status
    需要设置一个时间段rel_time,如果共享状态的标志在时间段结束之前没有被Provider设置为valid,则调用wait_for的线程被堵塞,在等待了rel_time时间后,wait_for函数返回
    例子:在main的主线程中等待resultFromDB子线程运行结束
int main()
{
  Timer timer("main");
  std::cout << "main start: " << std::this_thread::get_id() << std::endl;
  std::future<std::string> resultFromDB = std::async (std::launch::async, fetchDataFromDB, "Data");
  std::future_status status = resultFromDB.wait_for(std::chrono::seconds(6));
  if(status == std::future_status::ready)
​    std::cout << "ready" << std::endl;
  return 0;
}

输出结果:
main start: 140737473742656
fetchDataFromDB start: 140737473677056
fetchDataFromDB timer took: 5000.51 ms
ready
main timer took: 5001.87 ms
等待resultFromDB子线程5s后,在执行status比较和ready输出的指令
std::chrono::seconds()里面写多少s,结果都一样??
34. wait_until**:**
需要设置一个系统绝对时间点abs_time,如果共享状态的标志在该时间点到来之前没有被 Provider 设置为 ready,则调用 wait_until 的线程被阻塞,在 abs_time 这一时刻到来之后 wait_until() 返回
等待直到达到每个特定的时间点

  1. std::future

wait_for以及wait_until返回值std::future:提供访问异步操作的结果的状态
1)、deffered:异步操作还没有开始;
2)、ready:异步操作已经完成;
3)、timeout:异步操作超时
源码:

enum class future_status // 源码
{
  ready,
  timeout,
  deferred
};

例子:

std::future<std::string> resultFromDB = std::async(std::launch::async, fetchDataFromDB, "Data");
std::future_status status;
do
{
  status = resultFromDB.wait_for(std::chrono::seconds(1));
  if (status == std::future_status::deferred)
  {
​    std::cout << "deferred\n";
  } 
  else if (status == std::future_status::timeout)
  {
​    std::cout << "timeout\n";
  } 
  else if (status == std::future_status::ready)
  {
​    std::cout << "ready!\n";
  }
}while (status != std::future_status::ready);
  1. 多维数组

尽量避免使用多维数组,因为会造成内存碎片的方式存储

当遍历访问时,跳到下一维数组时,因为碎片的方式存储(跳维时,内存随机存储),会造成cache miss(缓存不命中),浪费时间从ram中获得数据

int main(){
  /*-----------------------二维数组----------------------*/
  int** a2d = new int*[50]; // a2d指向指针的集合,指针的指针
  for (int i = 0; i < 50; i++)
​    a2d[i] = new int[50];
// a2d[i]指针指向200字节的内存块,实现二维的数组
  a2d[0][0] = 0;
// 第一个0:指针的索引,第二个0:整数的索引
/*-----------------------二维数组的删除--------------------*/
// delete[] a2d;
// 只删除了存储指针的内存,指针指向的int数组未删除,造成内存泄漏
// 先删除int型的数组,再删除指向这些数组的指针
  for(int i = 0; i < 50; i++)
​    delete[] a2d[i];
  delete[] a2d;


/*-------------------------三维数组-----------------------*/
  int*** a3d = new int**[50]; // 指针的指针的指针
  for (int i = 0; i < 50; i++)
  {
​    a3d[i] = new int*[20];
​    for(int j = 0; j < 20; j++)
​      a3d[i][j] = new int[10];
  }
  a3d[0][0][0] = 0;
  // 以上多维数组是内存碎片的方式存储,cache miss比较多


  int* array_n = new int[5 * 5];
  for(int y = 0; y < 5; y++){
​    for(int x = 0; x < 5; x++){
​      array_n[x+y*5] = 2;
​    }
  }
  // 这里实际上是内存中同一行的内存
}
  1. 排序

用std::sort来排序

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main(){
  std::vector<int> values = {5,8,3,1,7,6,9,2};
// std::sort(values.begin(), values.end());  
// 从小到大排列
// std::sort(values.begin(), values.end(), std::greater<int>());  
// 从大到小排列,需包含functional头文件
// std::sort(values.begin(), values.end(), [](int a, int b){
//   return a<b;
// });
// lambda中设定从小到大的排序规则
std::sort(values.begin(), values.end(), [](int a, int b){ 
// lambda中设定其他的排序规则:1排到最后
​    if(a == 1)
​      return false;
​    if(b == 1)
​      return true;
​    return a<b; // true的时候,a排在前面
  });
  for(int value : values){
​    std::cout << value << std::endl;
  }
}
  1. 类型双关

把已拥有的内存当作不同类型的内存对待
拿出该类型的指针,转换成需要的类型的指针,(可再解引用)

隐式转换

int a = 50;
double value = a;
std::cout << value << std::endl;

value复制a中的四个字节的内容到自己的八个字节的地址

显示转换

int a = 50;
double value = *(double*)&a; // 有问题
std::cout << value << std::endl;

&a:取a的地址指针
(double*)&a:int型的a的地址指针强制转换成double型的地址指针
(四个字节往后面扩展成八个字节,扩展的四个字节是未初始化的内存)
(double)&a:解引用变成double类型
Value直接复制(引用)了改成double的八个字节内存(其中四个字节有问题)

int a = 50;
double &value = *(double*)&a;  //采用引用的方式代替复制
value = 0.0;  // 赋值时,需要写入8字节,但是只有四字节
// 出现错误segmentation fault
std::cout << value << std::endl;

自由指针转换

struct Entity // 八字节的结构体(两个int)
{
  int x, y;
};
int main()
{
  Entity e = {5,8};
  int* position = (int*)&e; // e:int数组开始的指针
  //把int类型的结构体指针转换成int型指针
  int y = *(int*)((char*)&e+4); // 自由指针,比较危险的操作
  // char是一个字节,+4偏移到e的第二位
  std::cout << position[0] << " , " << position[1] << std::endl;
}
  1. 虚函数析构

原理:

不是覆盖原来的析构函数,而是加上一个析构函数
如果把基类的析构函数改成虚函数,实际上析构的时候会调用多个析构函数,会先调用派生类析构函数,再调用基类的析构函数

内存泄漏:

在内存中开辟了空间,但是指针类的操作方式已删除,没办法继续操作该块内存,也没办法free该内存,造成内存空间浪费,称为内存泄漏

建议:

当允许基类可以被创建子类时,需百分百声明基类中的析构函数为虚析构函数,否则容易造成内存泄漏

class Base
{
public:
  Base(){std::cout << "base constructor\n";}
  ~Base(){std::cout << "base destructor\n";}
};
class Derived : public Base
{
public:
  Derived(){std::cout << "Derived constructor\n";}
  ~Derived(){std::cout << "Derived destructor\n";}
};
int main()
{
  Base* base = new Base();
  delete base;
  std::cout << "----------------------\n";
  Derived* derived = new Derived();
  delete derived;
  // 实例化的时候,先调用base的构造函数,然后调用Derived的构造函数
  // 删除时,先调用derived的析构函数,然后是base的析构函数
  std::cout << "----------------------\n";
  Base* poly = new Derived(); // 子类的指针转换成基类的指针使用
  delete poly;
  // 会造成内存泄漏,构造了base和derived,但是只析构了base类
}

结果:
base constructor
base destructor

base constructor
Derived constructor
Derived destructor
base destructor

base constructor
Derived constructor
base destructor

修改为

class Base
{
public:
  Base(){std::cout << "base constructor\n";}
  virtual ~Base(){std::cout << "base destructor\n";}
};
// 父类中的析构函数前面加virtual,表示析构的时候要先调用子类中的析构函数(如果有的话,避免造成内存泄漏),再调用基类的析构函数
  1. 类型转换

C++是强类型语言,存在类型系统

类型内存范围
char1 个字节-128 到 127 或者 0 到 255
unsigned char1 个字节0 到 255
signed char1 个字节-128 到 127
unsigned int4 个字节0 到 4294967295
signed int4 个字节-2147483648 到 2147483647
int4 个字节-2147483648 到 2147483647
short int2 个字节-32768 到 32767
unsigned short int2 个字节0 到 65,535
signed short int2 个字节-32768 到 32767
long int8 个字节-9,223,372,036,854,775,808 到9,223,372,036,854,775,807
signed long int8 个字节-9,223,372,036,854,775,808 到9,223,372,036,854,775,807
unsigned long int8 个字节0 到 18,446,744,073,709,551,615
float4 个字节精度型占4个字节(32位)内存空间,+/- 3.4e +/- 38 (~7 个数字)
double8 个字节占8个字节(64位)内存空间,+/-1.7e +/- 308 (~15个数字)
long double16 个字节长双精度型16个字节(128位)内存空间,可提供18-19位有效数字。
wchar_t2/4 个字节1 个宽字符
size_t8 个字节

原则上要坚持自己原本的类型,除非:

  1. 隐式转换

c++默认的不同类型的转换
double a = 5.91;
int b = a;
2. ### 显示转换
建议:熟悉的时候用c风格转换(类型双关),不熟悉的时候用c++风格转换,代码更可靠,合作性更强

C风格

(指定要强制转换的类型)要强制转换的变量
double a = 5.91;
int b = (int)a;

C++风格

Cast:强制转换,只能做c风格类型做的转换,但是会做额外的检查,但是也更慢
static_cast
编译时会检查这种转换是否可能
reinterpret_cast
类似于类型双关,把这段内存重新解释成其他类型
const_cast
移除或者添加变量的const限定
dynamic_cast
在运行时检查,只用于多态类类型
成功返回转化后的指针,不成功返回NULL指针

怎么判断:
存储了运行时的类型信息(runtime type information),会增加开销,但是可以再做动态转化的时候进行检查

class Base
class Derived : public Base
class AnotherClass : public Base
int main()
{
  Derived* der = new Derived();
  Base* base = der;
  AnotherClass* ac = dynamic_cast<AnotherClass*>(base);
  if(!ac)
  {
​    std::cout << "transfer not success\n";
  } else
  {
​    std::cout << "transfer success\n";
  }
}
// 结果:转换不成功
  1. 预编译头文件pch

用法和好处:

把c++库,标准库,windows api等(非自己写的代码)写进预编译头文件中,因为这些代码特别多(可能比实际应用程序的代码要多很多),每次每个cpp文件都要编译他们,编译速度太慢了;
把他们放在预编译头文件中,每个cpp文件可以直接得到这些东西,不用每个都带着他们编译。
不要将频繁更改的文件放入预编译的头文件中

劣势:

不方便看出每个cpp得依赖
目前cmake对预编译头文件功能支持不是特别好,vs支持的比较好

  1. 打开文件

C++之前版本读取文件:

#include <fstream>
std::string Readfileasstring(const std::string& filepath, bool& outputsucess)
{
  std::ifstream stream(filepath);
  if (stream)
  {
​    std::string result;
​    // read file;
​    stream.close();
​    outputsucess = true;
​    return result;
  }
  outputsucess = false;
  return std::string();
}
int main()
{
  bool fileopensuccessfully;
  std::string data = Readfileasstring("data.txt", fileopensuccessfully);
  if (fileopensuccessfully)
  {
​    std::cout << "file open success\n";
  }
  else
  {
​    std::cout << "file open error\n";
  }
}

C++17版本读取文件:

std::optional

  1. vscode用c++17进行编译的环境配置:

tasks.json****配置

"args": [
​        "-g",
​        "${file}",
​        "-o",
​        "${fileDirname}/${fileBasenameNoExtension}",
​        "-lpthread",
​        "-std=c++17" //17的版本
​      ],

c_cpp_properties.json****配置

{
  "configurations": [
  {
  "name": "Linux",
  "includePath": [
  "${workspaceFolder}/**"
  ],
  "defines": [],
  "compilerPath": "/usr/bin/gcc", //编译器路径
  "cStandard": "c11",
  "cppStandard": "c++17",
  "intelliSenseMode": "clang-x64"
  }
  ],
  "version": 4
}
  1. 读取文件的代码如下:
#include <optional>  //测下来不带这个也行
#include <fstream>
std::optional<std::string> Readfileasstring(const std::string& filepath)
{
  std::ifstream stream(filepath);
  if (stream)
  {
      std::string result;// read file;
      stream.close();
      return result;
  }
  return {};
}
int main()
{
	std::optional<std::string> data = Readfileasstring("data.txt");
	std::string value = data.value_or("not present"); //当文件中没有值时,赋为not pressent
	std::cout << value << std::endl;
  if (data)
  {
      std::cout << "file open success\n";
  }
  else
  {
      std::cout << "file open error\n";
  }
}
  1. 多类型变量

union

一个联合体只认一个类型(即使有多个类型),联合体大小为成员中最大的类型所占的内存的大小
union可以像使用结构体或者类一样使用,但是不能用虚方法
union通常是和类型双关紧密相连
union通常是匿名使用,但是匿名union不能含有成员函数

struct Union
{
  union
  {
      int a;
      float b;
  };
};
Union e;
e.a = 2;
std::cout << "size of union: " << sizeof(Union) << std::endl;
std::cout << e.a << " , " << e.b << std::endl;

输出:
size of union: 4
2 , 2.8026e-45 //2和2的浮点数形式的字节表示

​ 相当于取了组成浮点数的内存,把它解释成一个整型(类型双关)
​union可以实现变量间一一对应:

struct vector2
{
  float x;
  float y;
};
struct vector4
{
  union
  {
      struct
      {
          float a, b, c, d;
      };
      struct
      {
          vector2 m,n;    // m对应a和b,n对应c和d
      };
  };
};
int main()
{
  vector4 vector = {1.0, 2.0, 3.0, 4.0};
  std::cout << vector.a << " : "<< vector.m.x << std::endl;
  std::cout << vector.c << " : "<< vector.n.x << std::endl;
}

输出:
1 : 1
3 : 3
// 因为union中两个结构体的内存大小一样,所以就一一对应起来了

std::variant

c++17特性,不确定变量的具体类型,大概有几个类型的可能,用variant

#include <variant>
int main()
{
  std::variant<std::string, int> data;
  data = "cherno";// 用get_if判断variant的类型
if(auto value = std::get_if<std::string>(&data))  // 判断是否是string类型
{
	std::cout << std::get<std::string>(data) << std::endl;
	std::string& v = *value;
	std::cout << v << std::endl;
  }else{
    std::cout << std::get<int>(data) << std::endl;
  }
}

Variant和union的区别:

Union:大小是最大类型的大小
Variant:大小是所有类型的总和

union data1
{
  float a;
  int b;
  double c;
};
std::variant<float, int, double> data2;
std::cout << "size of float: " << sizeof(float)<< std::endl;
std::cout << "size of int: " << sizeof(int)<< std::endl;
std::cout << "size of double: " << sizeof(double)<< std::endl;
std::cout << "size of data1: " << sizeof(data1)<< std::endl;
std::cout << "size of data2: " << sizeof(data2)<< std::endl;

输出的结果:
size of float: 4
size of int: 4
size of double: 8
size of data1: 8
size of data2: 16

技术上来说,union仍然是更有效率的,但是variant更加安全,不会造成未定义行为,除非要做底层优化,建议使用variant

  1. 存储任意类型的数据

std::any
c++17特性,变量可以存储所有类型的数据

#include <any>
int main()
{
  std::any data;
  data = std::string("cherno");
  std::string ty_data = std::any_cast<std::string>(data);
  std::cout << ty_data << std::endl;
  data = 2;
  int ty_data1 = std::any_cast<int>(data);
  std::cout << ty_data1 << std::endl;
}

std::variant:更安全,不会动态分配内存(性能更好)
std::any:不太安全,会动态分配内存,更普适
如果真的需要any更普适的特性,建议重新考虑程序的设计
如果需要一个变量有多个类型,尽量使用std::variant,相对比std::any,void*更加无敌

  1. 性能可视化

见demo

  1. 小字符串优化

小于等于15个字符,在栈上分配,大于15个字符,在堆上分配

#include <iostream>
#include <string>
void* operator new(size_t size)
{
  std::cout << "application: " << size << std::endl;
  return malloc(size);
}   // 重载new字符,跟踪在堆上分配内存的大小
int main()
{
  std::string name = "chernodfjwbshwj"; //小于等于15个字符,在栈上分配
  std::string name1 = "chernosfowhfcuwad"; //大于15个字符,在堆上分配
}

输出:
application: 18
  1. 堆上内存使用显示

创建MemoryVisual.h文件,内容如下,可以在任何项目中加这个文件可以看在堆上内存的使用量(只能看在堆上占用的内存,不能看栈上的):
MemoryVisual.h头文件内容如下:

#include <iostream>
#include <memory>
struct AllocationMetrics
{
  uint32_t TotalAllocated = 0;
  uint32_t TotalFree = 0;
  uint32_t CurrentUsage(){return TotalAllocated - TotalFree;}
};
static AllocationMetrics s_AllocationMetrics;
void* operator new(size_t size)
{
  s_AllocationMetrics.TotalAllocated += size;
  return malloc(size);
}
void operator delete(void* memory, size_t size)
{
  s_AllocationMetrics.TotalFree += size;
  free(memory);
}
void PrintMemoryUsage(const std::string& name)
{
  std::cout << name << "-" << "MemoryUsage: " << s_AllocationMetrics.CurrentUsage() << std::endl;
}

使用如下:

int main()
{
  PrintMemoryUsage("1");
  {
      std::unique_ptr<double> e = std::make_unique<double>();
      PrintMemoryUsage("2");
      std::string name = "chernodfjwbshwjw"; //大于15字符,在堆上分配
      PrintMemoryUsage("3");
  }
  PrintMemoryUsage("4");
}

输出结果:
1-MemoryUsage: 0
2-MemoryUsage: 8
3-MemoryUsage: 25
4-MemoryUsage: 17  // string大于15字符,在堆上分配
  1. 左值右值

左值----可以取地址的,有名字的,非临时的就是左值;
右值----不能取地址的,没有名字的,临时的就是右值;
左值是有存储支持的变量,右值是临时值
左值引用&只接收左值
右值引用&&仅仅接收右值
const &接收左值和右值
左值引用基本语法:type &引用名 = 左值表达式;
右值引用基本语法:type &&引用名 = 右值表达式;

#include <iostream>
void PrintName (std::string& name) // 左值引用
{
  std::cout << "[lvalue]" << "name: " << name << std::endl;
}
void PrintName (std::string&& name) // 右值引用
{
  std::cout << "[rvalue]" <<"name: " << name << std::endl;
}
void PrintName (const std::string& name) // 左右值引用
{
  std::cout << "[value]" <<"name: " << name << std::endl;
}
int main()
{
  std::string firstname = "Yan";
  std::string lastname = "Cherno";
  std::string name = firstname + lastname;
  PrintName(name);
  PrintName(firstname + lastname);
}

输出:
[lvalue]name: YanCherno
[rvalue]name: YanCherno

remove_reference剖析

template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };
// 特化版本
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp   type; };
// remove_reference的作用是去除T中的引用部分,只获取其中的类型部分。无论T是左值还是右值,最后只获取它的类型部分

std::forward剖析

std::forward用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中
如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值

// 转发左值
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
// 转发右值

template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
  static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
        " substituting _Tp is an lvalue reference type");
  return static_cast<_Tp&&>(__t);
}

std::move剖析

将左值/右值强制转化为右值引用,继而可以通过右值引用使用该值,所以称为移动语义

模板

template

std::assert()函数

断言函数,作用是如果它的条件返回错误,则终止程序执行

原型定义

#include <assert.h>
void assert( int expression );
如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行

缺点

频繁的调用会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过在包含#include <assert.h>的语句之前插入 #define NDEBUG 来禁用assert调用

线程锁

多线程中的锁主要有五类:互斥锁、条件锁、自旋锁、读写锁、递归锁(递归锁一般不使用)

  1. 互斥锁
    互斥锁用于控制多个线程对它们之间共享资源互斥访问的一个信号量。也就是说为了避免多个线程在某一时刻同时操作一个共享资源,例如一个全局变量,任何一个线程都要使用初始锁互斥地访问,以避免多个线程同时访问发生错乱
    头文件:#include
    类型:std::mutex、std::lock_guard
    用法:在C++中,通过构造std::mutex的实例创建互斥单元,调用成员函数lock()来锁定共享资源,调用unlock()来解锁。不过一般不使用这种解决方案,更多的是使用C++标准库中的std::lock_guard类模板,实现了一个互斥量包装程序,提供了一种方便的RAII风格的机制在作用域块中。
{
  //g_mutex.lock();
  std::lock_guard<std::mutex> m(g_mutex);//互斥量包装程序
  函数主体
  //g_mutex.unlock();
}
  • std::lock_guard和std::unique_lock的区别
    – 都可以对std::mutex进行封装,区别是unique_lock比lock_guard能提供更多的功能特性(但需要付出性能的一些代价)
    – unique_lock可以实现延时锁,即先生成unique_lock对象,然后在有需要的地方调用lock函数,lock_guard在对象创建时就自动进行lock操作了;
    – unique_lock可以在需要的地方调用unlock操作,而lock_guard只能在其对象生命周期结束后自动Unlock;
    正是由于这两个差异特性,unique_lock可以用于一次性锁多个锁以及用于条件变量的搭配使用,而lock_guard做不到。
// 通过unique_lock锁多个锁:
std::unique_lock<std::mutex> lk1(mutex1, std::defer_lock);
std::unique_lock<std::mutex> lk2(mutex2, std::defer_lock);
std::lock(lk1, lk2);
// 通过unique_lock与条件变量一起使用:
std::condition_variable cvar;
std::mutex mmutex;
std::unique_lock<std::mutex> lock(mmutex);
// 等待线程:
cvar.wait(lock, [&, this]() mutable throw() -> bool{ return this->isReady(); });
// 唤醒线程:
std::lock_guard<std::mutex> guard(mmutex);
flag = true;
std::cout<<"Data is ready"<<std::endl;
cvar.notify_one();
  1. 条件锁
    条件锁就是所谓的条件变量,当某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态,一旦条件满足则以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见的就是再线程池中,初始情况下因为没有任务使得任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒该线程来处理这个任务。
std::condition_variable mDataPubCv; // 条件锁
std::mutex mDataPubLock;         // 互斥锁
// 线程1中通知线程2,数据已经到来,可以处理数据了
mDataPubCv.notify_all();
// 线程2中等待 onHalSubInterface 接收数据,条件为:mHalSubDataDealIdx < mHalSubDataIdx
{   
    std::unique_lock<std::mutex> lk(mDataPubLock);     // 条件变量的等待操作通常结合互斥锁使用,以确保在检查条件和进入等待状态之间的原子性
    mDataPubCv.wait(lk, [this] { return mHalSubDataDealIdx < mHalSubDataIdx; });
}

  1. 自旋锁
    如果一个线程想要获得一个被使用的自旋锁,那么它会一直占用CPU请求这个自旋锁使得CPU不能去做其它的事情,知道获取这个锁为止,这就是“自旋”的含义。当发生阻塞时,互斥锁可以让CPU去处理其它的事务,但自旋锁让CPU一直不断循环请求获取这个锁。通过比较,我们可以明显的得出结论:“自旋锁”是比较消耗CPU的。
template<typename T>
class AtomicSpinLock {
private:
    T* m_lock;

public:
    explicit AtomicSpinLock(T& lock) : m_lock(std::addressof(lock)) {
        while (m_lock->test_and_set(std::memory_order_acquire));
    }

    ~AtomicSpinLock() {
        m_lock->clear(std::memory_order_release);
    }
};
// 写的类使用RAII机制,在构造的时候加锁,析构的时候解锁,相当于lock和unlock的操作
std::atomic_flag mDataPubSpinLock = ATOMIC_FLAG_INIT;           // 自旋锁,在数据存储与转移时使用
AtomicSpinLock<std::atomic_flag> lk(mDataPubSpinLock);
{
    AtomicSpinLock<std::atomic_flag> lk(mDataPubSpinLock);
    data = std::move(mHalSubData[mHalSubDataDealIdx++ % 1000]);
}
// 在作用域外面则解锁

线程有三个状态:wait run 和sleep
当用自旋锁的时候,run完会等待下一次run,适用于处理短时间的任务
当用互斥锁的时候,run完会进入sleep,重新唤起的时候sleep进入wait,再进入run,比较消耗时间

自旋锁(Spin Lock):
实现方式: 自旋锁是一种基本的同步机制,它在多线程编程中用于保护临界区。当一个线程尝试获取锁时,如果锁已经被其他线程占用,它会一直循环(自旋)等待,而不是放弃 CPU 时间片。
适用场景: 适用于短暂的临界区和低竞争的情况,因为自旋等待会占用 CPU 资源,如果等待时间过长或竞争激烈,可能会导致性能下降。
tmux互斥锁:
实现方式: tmux 是一个终端复用器,它允许在一个终端窗口中运行多个终端会话。tmux 本身使用了一些同步机制,包括互斥锁,来确保在不同会话之间正确共享状态。
适用场景: 主要用于控制终端会话之间的互斥访问,而不是用于普通的多线程编程。tmux 互斥锁的使用是为了保护对 tmux 状态的修改,以防止竞争条件

  1. 读写锁
    计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。

容器取值

stl库中用[]取值,at()取值
eigen用()取值

类函数的地址

成员变量、函数以及类在内存中都有地址。每个变量、函数或类的实例都在内存中占据一定的空间,并被分配一个地址,这个地址可以用于引用或访问相应的对象或代码

#include <iostream>
class MyClass {
public:
    int32_t x;
    int32_t y;
    void myFunction() {
        std::cout << "Hello from MyClass!" << std::endl;
    }
};
int main() {
    MyClass obj;
    obj.x = 42;
    // 获取成员变量地址
    int32_t* xAddress = &obj.x;
    int32_t* yAddress = &obj.y;
    // 获取成员函数地址
    void (MyClass::*funcPointer)() = &MyClass::myFunction;
    // 获取对象地址
    MyClass* objAddress = &obj;
    // 打印地址
    std::cout << "Address of x: " << xAddress << std::endl;
    std::cout << "Address of y: " << yAddress << std::endl;
    std::cout << "Address of myFunction: " << (void*)funcPointer << std::endl;
    std::cout << "Address of obj: " << objAddress << std::endl;
    std::cout << "size of obj: " << sizeof(obj) << std::endl;
    return 0;
}
//输出结果为:
// Address of x: 0x7ffecc02d400
// Address of y: 0x7ffecc02d404
// Address of myFunction: 0x56318ed04fe0
// Address of obj: 0x7ffecc02d400
// size of obj: 8

当类成员函数使用多线程:

std::thread(&Dispatcher::WriteLidarPoints, this).detach();
// this表示Dispatcher类的地址,WriteLidarPoints函数就可以调用Dispatcher类中的成员变量

多线程+lambda函数+右值+mutex锁使用demo

std::thread([this] (std::vector<Pbox>&& vPbox) {
    std::lock_guard<std::mutex> lk(mPboxLock);
    mPboxFOut.write((const char*)(&vPbox[0]), sizeof(Pbox) * 100);
    // 检查写入是否成功
    if (mPboxFOut.fail()) {
        throw std::runtime_error("mPboxFOut write failed");
    }
    mPboxFOut.flush();
}, std::move(mvPbox)).detach();

或者:

auto dataloggingTask = std::bind([this] (std::vector<wheel_data_t>&& vDatalogging, int8_t dataloggingIdx) {
    std::lock_guard<std::mutex> lk(mDataloggingLock);
    mDataloggingFOut.write((const char*)(&vDatalogging[0]), sizeof(wheel_data_t) * dataloggingIdx);
    // 检查写入是否成功
    if (mDataloggingFOut.fail()) {
        throw std::runtime_error("mDataloggingFOut write failed");
    }
    mDataloggingFOut.flush();
}, std::placeholders::_1, std::placeholders::_2);
std::thread(dataloggingTask, std::move(mvDatalogging), mDataloggingIdx).detach();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值