习惯
1.声明变量时记得初始化
2.普通形参和引用形参
普通形参:不改变主函数中实参
引用形参:改变主函数中实参
(一)类相关
1.基类的析构函数必须设置为virtual虚函数,防止子类无法正确释放内存导致内存泄漏。
2.explicit关键字阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。
struct: 结构体初始化的四种方法
例子:
struct InitMember
{
int first;
double second;
char* third;
float four;
};
方法一:定义时赋值。需要注意对应的顺序,不能错位。
struct InitMember test = {-10,3.141590,"method one",0.25};
方法二:定义后逐个赋值。因为是逐个确定的赋值,无所谓顺序。
struct InitMember test;
test.first = -10;
test.second = 3.141590;
test.third = "method two";
test.four = 0.25;
方法三:定义时乱序赋值(C风格)
这种方法类似于第一种方法和第二种方法的结合体,既能初始化时赋值,也可以不考虑顺序。
注意: 你必须在定义时进行赋值,不可分成两步: 先声明变量的名字,然后拿着这个变量名开始赋值
struct InitMember test = {
.second = 3.141590,
.third = "method three",
.first = -10,
.four = 0.25
};
类
一、成员变量
private:用来指定私有成员。一个类的私有成员,不论是成员变量还是成员函数,都只能在该类的成员函数内部才能被访问。
public:用来指定公有成员。一个类的公有成员在任何地方都可以被访问。类外可以直接通过对象访问。
二、构造函数
构造函数概念:
一个类的对象被创建的时候,编译系统对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作。因此,构造函数的核心作用就是,初始化对象的数据成员
https://blog.youkuaiyun.com/Viewinfinitely/article/details/115017678
构造函数的特点:
(1)名字与类名相同,可以有参数,但是不能有返回值(连void也不行)。
(2)构造函数是在实例化对象时自动执行的,不需要手动调用。
(3)作用是对对象进行初始化工作,如给成员变量赋值等。
(4)如果定义类时没有写构造函数,系统会生成一个默认的无参构造函数,默认构造函数没有参数,不做任何工作。
(5)如果定义了构造函数,系统不再生成默认的无参构造函数.
(6)对象生成时构造函数自动调用,对象一旦生成,不能在其上再次执行构造函数
一个类可以有多个构造函数,为重载关系。
1.默认构造函数
什么是默认构造函数?
https://blog.youkuaiyun.com/bear_n/article/details/72798301
默认构造函数是可以不用实参进行调用的构造函数,它包括了以下两种情况:
1)没有带明显形参的构造函数。
2)提供了默认实参的构造函数。
个人理解:没有明显形参表示没有形参或者形参有缺失;提供默认实参表示对类中的成员变量进行初始化。
https://blog.youkuaiyun.com/zhizhengguan/article/details/114990126
虽然我们并没有定义Point类的构造函数,我们依然可以定义Point类的pt对象并使用它,其原因是编译器会自动生成一个缺省的构造函数,其效果相当于下图
但是,一旦添加了其他有参数的构造函数,编译器就不再生成缺省的构造函数了。这时候如果没有无参数的构造函数,那么程序就无法通过Point pt;
来定义对象如果是自己编写的无参构造函数的话,就需要指定成员的构造方式。默认构造函数会对数据成员进行默认初始化,不需要另外指定。 这样可以省去一些麻烦。
C++11允许我们使用=default来要求编译器生成一个默认构造函数:
2.C++中构造函数的显式调用和隐式调用与explicit 关键字
https://zhuanlan.zhihu.com/p/52152355
https://blog.youkuaiyun.com/gaoyu1253401563/article/details/79740268
#include <iostream>
using namespace std;
class Point {
public:
int x, y;
Point(int x = 0, int y = 0)
: x(x), y(y) {}
};
void displayPoint(const Point& p)
{
cout << "(" << p.x << ","
<< p.y << ")" << endl;
}
int main()
{
displayPoint(1);
Point p = 1;
}
定义了一个再简单不过的Point类, 它的构造函数使用了默认参数(int x = 0, int y = 0). 这时主函数里的两句话都会触发该构造函数的隐式调用. (如果构造函数不使用默认参数, 会在编译时报错) 。这样悄悄发生的事情, 有时可以带来便利, 而有时却会带来意想不到的后果. explicit关键字用来避免这样的情况发生.
这篇文章我们关注的就是第一点. 构造函数被explicit修饰后, 就不能再被隐式调用了. 也就是说, 之前的代码, 在Point(int x = 0, int y = 0)前加了explicit修饰, 就无法通过编译了.
explicit关键字C++ 参考手册如下解释:
指定构造函数或转换函数 (C++11起)为显式, 即它不能用于隐式转换和复制初始化.explicit 指定符可以与常量表达式一同使用. 函数若且唯若该常量表达式求值为 true 才为显式. (C++20起)
Effective C++中也写:
被声明为explicit的构造函数通常比其 non-explicit 兄弟更受欢迎, 因为它们禁止编译器执行非预期 (往往也不被期望) 的类型转换. 除非我有一个好理由允许构造函数被用于隐式类型转换, 否则我会把它声明为explicit. 我鼓励你遵循相同的政策.
三、多态—虚函数virtual及override
1.C++多态
C++多态(polymorphism)是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,动态绑定。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。
用下面代码演示多态和非多态:
#include<iostream>
using namespace std;
class A
{
public:
void foo()
{
printf("1\n");
}
virtual void fun()
{
printf("2\n");
}
};
class B : public A
{
public:
void foo() //隐藏:派生类的函数屏蔽了与其同名的基类函数
{
printf("3\n");
}
void fun() //多态、覆盖
{
printf("4\n");
}
};
int main(void)
{
A a;
B b;
A *p = &a;
p->foo(); //输出1
p->fun(); //输出2
p = &b;
p->foo(); //取决于指针类型,输出1
p->fun(); //取决于对象类型,输出4,体现了多态
return 0;
}
2.C++纯虚函数及虚函数
1)纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 。
引入纯虚函数的原因:
(1)为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
(2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。
2)虚函数
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
虚函数是C++中用于实现多态的机制。核心理念就是通过基类访问派生类定义的函数。如果父类或者祖先类中函数func()为虚函数,则子类及后代类中,函数func()是否加virtual关键字,都将是虚函数。为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。
3.C++保留字override
override 仅在成员函数声明之后使用时才是区分上下文的且具有特殊含义;否则,它不是保留的关键字。使用 override 有助于防止代码中出现意外的继承行为。以下示例演示在未使用override 的情况下,可能不打算使用派生类的成员函数行为。编译器不会发出此代码的任何错误。
class BaseClass
{
virtual void funcA();
virtual void funcB() const;
virtual void funcC(int = 0);
void funcD();
};
class DerivedClass: public BaseClass
{
virtual void funcA(); // ok, works as intended
virtual void funcB(); // DerivedClass::funcB() is non-const, so it does not
// override BaseClass::funcB() const and it is a new member function
virtual void funcC(double = 0.0); // DerivedClass::funcC(double) has a different
// parameter type than BaseClass::funcC(int), so
// DerivedClass::funcC(double) is a new member function
};
当使用 override时,编译器会生成错误,而不会在不提示的情况下创建新的成员函数。
class BaseClass
{
virtual void funcA();
virtual void funcB() const;
virtual void funcC(int = 0);
void funcD();
};
class DerivedClass: public BaseClass
{
virtual void funcA() override; // ok
virtual void funcB() override; // compiler error: DerivedClass::funcB() does not
// override BaseClass::funcB() const
virtual void funcC( double = 0.0 ) override; // compiler error:
// DerivedClass::funcC(double) does not
// override BaseClass::funcC(int)
void funcD() override; // compiler error: DerivedClass::funcD() does not
// override the non-virtual BaseClass::funcD()
};
下面代码展示了手动调用虚函数的过程:
#include<iostream>
using namespace std;
class A {
public:
virtual void vfunc1() { cout << "A::vfunc1()" << endl; };
virtual void vfunc2() { cout << "A::vfunc2()" << endl; };
void func1() { cout << "A::func1()" << endl; };
void func2() { cout << "A::func2()" << endl; };
private:
int data1_;
int data2_;
};
class B :public A {
public:
virtual void vfunc1() override { cout << "B::vfunc1()" << endl; };
void func2() { cout << "B::func2()" << endl; };
private:
int data3_;
};
class C :public B {
public:
virtual void vfunc1() override { cout << "C::vfunc1()" << endl; };
void func2() { cout << "C::func2()" << endl; };
private:
int data1_, data4_;
};
//演示了手动调用虚函数的过程
int main() {
B a;
typedef void(*Fun)(void);
Fun pFun = nullptr;
cout << "虚函数表地址:" << (int*)(&a) << endl;
cout << "虚函数表第1个函数地址:"<<(int*)*(int*)(&a) << endl;
cout << "虚函数表第2个函数地址:" << (int*)*(int*)(&a) + 1 << endl;
pFun = (Fun)*((int*)*(int*)(&a));
pFun();
pFun = (Fun)*((int*)*(int*)(&a) + 1);
pFun();
return 0;
}
函数指针、std::function、std::bind
一、函数指针
(一)函数的类型与地址
int foo()
{
return 5;
}
显然foo 是函数名,而int是函数返回值的类型。但是,函数有类型吗?有,函数有自己的类型,比如上面这个函数的类型即为“无参数且返回类型为整型”的函数。我们可以这么表示这种类型int (*somefunction)(),同样的,如果是“有两个整形参数且返回值是布尔型”的我们可以这么表示bool (*someotherfunction)(int, int)。(有人认为这个不属于函数的类型,其实吧我只是觉着这么解释容易理解,你当然也可以不这么想。)
和变量一样,函数在内存中有固定的地址。函数的实质也是内存中一块固定的空间。
(二)函数指针的使用
int(*funcPtr)(); //funcPtr is short for 'function pointer'/函数指针
int (*const funcPtr)();//或者我们也可以这么写,如果你需要一个静态的函数指针
const int(*funcPtr)//意思是这个指针指向的函数的返回值是常量
例子:把一个函数赋值给函数指针
千万不要写成funcPtr = goo();这是把goo的返回值赋值给了funcPtr
int foo()
{
return 5;
}
int goo()
{
return 6;
}
int main()
{
int (*funcPtr)() = foo; // funcPtr 现在指向了函数foo
funcPtr = goo; // funcPtr 现在又指向了函数goo
//但是千万不要写成funcPtr = goo();这是把goo的返回值赋值给了funcPtr
return 0;
}
c++会隐式得把foo转换成&foo,所以你无需再加入&
int foo(){
return 5;
}
int main()
{
int (*funcPtr1)() = foo;
int (*funcPtr2)() = &foo; // c++会隐式得把foo转换成&foo,所以你无需再加入&
std::cout << funcPtr1() << std::endl;
std::cout << funcPtr2() << std::endl;
}
结果:
5
5
通过函数指针调用函数
int foo(int x)
{
return x;
}
int main()
{
int (*funcPtr)(int) = foo;
(*funcPtr)(5); // 通过funcPtr调用foo(5)
funcPtr(5) // 也可以这么使用,在一些古老的编译器上可能不行
return 0;
}
把函数作为参数传入另一个函数
#include <iostream>
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}
void func(int e, int d, int(*f)(int a, int b)){ // 这里才是我想说的,
// 传入了一个int型,双参数,返回值为int的函数
std::cout<<f(e,d)<<std::endl;
}
int main()
{
func(2,3,add);
func(2,3,sub);
return 0;
二、std::function
由于可调用对象的定义方式比较多,但是函数的调用方式较为类似,因此需要使用一个统一的方式保存可调用对象或者传递可调用对象。于是,std::function就诞生了。
std::function是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。
定义function的一般形式:
# include <functional>
std::function<函数类型>
例如:
# include <iostream>
# include <functional>
typedef std::function<int(int, int)> comfun;
// 普通函数
int add(int a, int b) { return a + b; }
// lambda表达式
auto mod = [](int a, int b){ return a % b; };
// 函数对象类
struct divide{
int operator()(int denominator, int divisor){
return denominator/divisor;
}
};
int main(){
comfun a = add;
comfun b = mod;
comfun c = divide();
std::cout << a(5, 3) << std::endl;
std::cout << b(5, 3) << std::endl;
std::cout << c(5, 3) << std::endl;
}
std::function的作用可以归结于:
1.std::function对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的封装,形成一个新的可调用的std::function对象,简化调用;
2.std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(如:函数指针这类可调用实体,是类型不安全的)。
3.std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。
三、std::bind
std::bind的头文件是 ,它是一个函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。
std::bind返回一个函数对象,其参数被绑定到args上。
f的参数要么被绑定到值,要么被绑定到placeholders(占位符,如_1, _2, …, _n)。std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。
std::bind主要有以下两个作用:
1.将可调用对象和其参数绑定成一个模函数;
2.只绑定部分参数,减少可调用对象传入的参数。
1. std::bind绑定普通函数
double callableFunc (double x, double y) {return x/y;}
auto NewCallable = std::bind (callableFunc, std::placeholders::_1,2);
std::cout << NewCallable (10) << '\n';
1.bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。因此std::bind(callableFunc,_1,2)等价于std::bind (&callableFunc,_1,2);
2._1表示占位符,位于中,std::placeholders::_1;
3.第一个参数被占位符占用,表示这个参数以调用时传入的参数为准,在这里调用NewCallable时,给它传入了10,其实就想到于调用callableFunc(10,2);
2. std::bind绑定一个成员函数
class Base
{
public:
void display_sum(int a1, int a2)
{
std::cout << a1 + a2 << '\n';
}
int m_data = 30;
};
int main()
{
Base base;
auto newiFunc = std::bind(&Base::display_sum, &base, 100, std::placeholders::_1);
f(20); // should out put 120.
}
1.bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
2.必须显式地指定&Base::diplay_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Base::display_sum前添加&;
3.使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &base;
3. 绑定一个引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定的参数希望以引用的方式传递,或是要绑定参数的类型无法拷贝。
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
#include <sstream>
using namespace std::placeholders;
using namespace std;
ostream & printInfo(ostream &os, const string& s, char c)
{
os << s << c;
return os;
}
int main()
{
vector<string> words{"welcome", "to", "C++11"};
ostringstream os;
char c = ' ';
for_each(words.begin(), words.end(),
[&os, c](const string & s){os << s << c;} );
cout << os.str() << endl;
ostringstream os1;
// ostream不能拷贝,若希望传递给bind一个对象,
// 而不拷贝它,就必须使用标准库提供的ref函数
for_each(words.begin(), words.end(),
bind(printInfo, ref(os1), _1, c));
cout << os1.str() << endl;
}
回调函数
https://blog.youkuaiyun.com/zhoupian/article/details/119495949
二、普通函数用作回调函数。
这个不是一个好的方法,影响了我们cpp的封装特性,也和标题不符。但是我们可以看到,我们的调用函数成功调用了回调函数,并且可以访问我们的类成员变量,但是他是通过参数传递来访问我们类的成员变量的。
#include <iostream>
using namespace std;
class TestCallback {
public:
int num = 5;
void call(void(*callback)(int));
};
void callBack1(int num)
{
cout << "func1的num = " << num << endl;
}
void TestCallback::call(void(*callback)(int))
{
cout << "调用回调" << endl;
callback(this->num);
}
int main()
{
TestCallback t;
t.call(callBack1);
}
运行结果:
调用回调
func1的num = 5
三、成员函数用作回调函数的方法
实际一句话:需要知道类对象的地址,这样就知道了类的成员地址,这样就可以调用这个函数了。
1.使用静态成员函数
能用作回调函数的前提是该函数的地址是确定的,因为函数指针传递的也就是函数的地址,那么类的成员函数的地址只有在实例化了类的对象后,对象的成员函数的地址才确定,所以我们不能直接用类的成员函数来做函数指针。
但是静态成员函数相当于是类的全局函数,他的地址是确定的,我们可以通过类名::静态成员函数名这种方式来访问这个函数的地址。
使用了静态成员函数的问题:
1)静态成员函数无法访问类的非静态成员变量,因此我们例子中的num还是得像方法一那样通过参数传给回调函数,那么我们例子中的num2因为是静态的变量所以就也可以通过类名::静态变量名这种方式来访问。
2)类的静态成员变量需要在类外面使用前进行初始化,这个是必须的,少了的话编译器会报错找不到这个变量。
#include <iostream>
using namespace std;
class TestCallback {
public:
int num = 5;
static int num2;
void call(void(*callback)(int));
static void callBack2(int num);
};
int TestCallback::num2 = 6;//静态变量必须初始化
void callBack1(int num)
{
cout << "------start func1------" << endl;
cout << "func1的num = " << num << endl;
cout << "------end func1------" << endl;
}
void TestCallback::call(void(*callback)(int))
{
cout << "------调用回调------" << endl;
callback(this->num);
cout << "------调用结束" << endl;
cout << endl;
}
void TestCallback::callBack2(int num)
{
cout << "------start func2------" << endl;
cout << "func2的num = " << num << endl;
cout << "func2访问类的静态成员变量num2 = " << TestCallback::num2 << endl;
cout << "------end func2------" << endl;
}
int main()
{
TestCallback t;
t.call(callBack1);
t.call(TestCallback::callBack2);
}
2.直接使用类成员函数当作回调函数
知道回调函数的类的地址,又知道这个类的回调函数相对于类的地址,这样我们相当于明确了函数的具体地址,就可以直接用成员函数当作回调函数。这种方法不需要定义静态函数就可以访问类的成员函数和变量。
#include "pch.h"
#include <iostream>
using namespace std;
class TestCallback {
public:
int num = 5;
static int num2;
void call(void *p,void (TestCallback::*callback)(void));
void callBack5();
};
int TestCallback::num2 = 6;//静态变量必须初始化
void TestCallback::call(void *p,void (TestCallback::*callback)(void))
{
cout << "------调用回调------" << endl;
TestCallback *temp = (TestCallback *)p;
(temp->*callback)();
cout << "------调用结束" << endl;
cout << endl;
}
void TestCallback::callBack5()
{
cout << "------start func5------" << endl;
cout << "func5的num = " << this->num << endl;
cout << "func5的num2 = " << this->num2 << endl;
cout << "------end func5------" << endl;
}
int main()
{
TestCallback t;
t.call(&t,&TestCallback::callBack5);
}
3.利用lambda表达式使用类成员函数当作回调函数
#include <stdio.h>
#include <iostream>
#include <functional>
//声明未初始化的function函数包装器
typedef std::function<void(int)> Fun;
class Bird{
public:
Bird(){}
// 我们就是想尽办法调这个move函数,还是非静态的!
void move(int h){
std::cout<<"I am flying:"<<h<<std::endl;
}
};
// 调用者示例
class Caller{
public:
void callOthers(Fun cb){
if(cb != nullptr)
cb(3);
}
};
int main()
{
Bird bird;
//2.把实例化的对象和成员函数绑定,函数指针赋值给function
Fun fun = std::bind(&Bird::move,&bird,std::placeholders::_1);
// 直接调用函数
fun(2);
Caller call;
// 或者传给另一个函数内部进行调用
call.callOthers(fun);
// 使用lambda表达式进行回调
auto lamb = [&](int h){
bird.move(h);
};
call.callOthers(lamb);
return 0;
}
指针
一、指针的使用
1.指针作为函数形参时,调用函数会直接对传递进来的实参的内存进行操作。函数调用结束后程序中的实参也会发生变化。(普通变量作为形参,那么调用函数时,形参会对实参进行拷贝,函数结束后实参不变。)
二、野指针
野指针(Dangling Pointer)是指指向已经释放或无效的内存地址的指针。当你释放了一块内存区域或者该内存区域已经超出其作用域,但仍然保留了指向该内存区域的指针,那么这个指针就成为野指针。
野指针可能会导致程序出现未定义的行为,例如访问无效内存,导致程序崩溃、数据损坏等问题。
以下是一个野指针的示例:
int* ptr = new int; // 分配一个新的整型对象的内存,并将其地址赋值给指针
delete ptr; // 释放内存
// 在这之后,ptr 指针成为了野指针
// 这时使用 ptr 访问或修改内存会产生未定义行为
在上述示例中,ptr 指针在释放内存后没有被置为 nullptr,而继续使用该指针进行操作会出现问题。
为了避免野指针的出现,应该养成以下良好的编程习惯:
在释放内存后,将指针设置为 nullptr,避免产生野指针。
1.避免在超出作用域的情况下继续使用指针。
//用于安全释放动态分配的内存
/** Memory Safe Free */
#define SAFE_FREE(p) \
do { \
if (nullptr != (p)) { \
delete (p); \
(p) = nullptr; \
} \
} while (0)
#define SAFE_FREE_ARRAY(p) \
do { \
if (nullptr != (p)) { \
delete[] (p); \
(p) = nullptr; \
} \
} while (0)
//宏定义为什么要使用do{……}while(0)形式:
//https://blog.youkuaiyun.com/xiaoyilong2007101095/article/details/77067686
2.尽量使用智能指针(如 std::unique_ptr 和 std::shared_ptr),它们可以自动管理指针生命周期,避免忘记释放内存或重复释放内存的问题。
三、常量指针,指针常量、指向常量的常指针
https://blog.youkuaiyun.com/as480133937/article/details/120804503
总结:
首先,使用const修饰的变量,其值是不允许改变的。
1.指针常量
int * const p =&a;
指针常量,指针指向的地址不能改变,内容可变。
int a=10;
int * const p =&a; //定义指针常量,指向int a的地址
*p = 20; //正确,指向的内存地址中的数据可以修改
p=&b; //错误,指向的内存地址不可以修改
2.常量指针
const int *p=&a;
常量指针,指针指向的内容不能改变,地址可变。
int a=10;
int b=10;
const int *p=&a; //定义常量指针,指向int a的地址
*p = 20; //错误,指向的内存地址中的数据不可以修改
p=&b; //正确,指向的内存地址可以修改
static关键字
1.static静态变量是局部变量
1)用于函数体内部修饰变量,这种变量的生存期长于该函数。随着第一次函数的调用而初始化,却不随着函数的调用结束而销毁,该变量被多次调用时也不会重新初始化(看StaticTest函数和Increase函数的对比)
2)和全局静态变量的对比:使用全局变量的话,变量就不属于函数本身了,不再仅受函数的控制,给程序的维护带来不便。静态局部变量正好可以解决这个问题。
void StaticTest(){
static int count = 0;
count++;
std::cout << "count:" << count << std::endl;
}
void Increase(){
int count = 0;
count++;
std::cout << "count:" << count << std::endl;
}
2.静态全局变量
定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见。
static int i = 1; //note:3
//int i = 1; //note:4
int foo()
{
i += 1;
return i;
}
用foo(),无论调用几次,他们的结果都是一样的。也就是说在本文件内调用他们是完全相同的。
静态全局变量的作用:文件隔离
假设我有一个文件a.c,我们再新建一个b.c,内容如下。
//file a.c
//static int n = 15; //note:5
int n = 15; //note:6
//file b.c
#include <stdio.h>
extern int n;
//备注:extern是一个关键字,它告诉编译器存在着一个变量或者一个函数,如果在当前编译语句的前面中没有找到相应的变量或者函数,也会在当前文件的后面或者其它文件中定义
void fn()
{
n++;
printf("after: %d\n",n);
}
void main()
{
printf("before: %d\n",n);
fn();
}
先使用 note:6,也就是 非静态全局变量,发现输出为:
before: 15
after: 16
也就是我们的 b.c 通过 extern 使用了 a.c 定义的全局变量。 那么我们改成使用 note:5,也就是使用静态全局变量呢?
gcc a.c b.c -o output.out
会出现类似 undeference to “n” 的报错,它是找不到n的,因为 static 进行了文件隔离,你是没办法访问 a.c 定义的静态全局变量的,当然你用 #include “a.c” 那就不一样了。
以上我们就可以得出静态全局变量的特点:
1.静态全局变量不能被其它文件所用(全局变量可以);
2.其它文件中可以定义相同名字的变量,不会发生冲突(自然了,因为static隔离了文件,其它文件使用相同的名字的变量,也跟它没关系了);
3.静态函数
准确的说,静态函数跟静态全局变量的作用类似:
//file a.c
#include <stdio.h>
void fn()
{
printf("this is non-static func in a");
}
//file b.c
#include <stdio.h>
extern void fn(); //我们用extern声明其他文件的fn(),供本文件使用。
void main()
{
fn();
}
可以正常输出:
this is non-static func in a。
当给void fn()加上static的关键字之后呢?
undefined reference to “fn”.
所以,静态函数的好处跟静态全局变量的好处就类似了:
1.静态函数不能被其它文件所用;
2.其它文件中可以定义相同名字的函数,不会发生冲突;
上面一共说了三种用法,为什么说准确来说是两种呢?
1.一种是修饰变量,一种是修饰函数,所以说是两种(这种解释不多)。
2.静态全局变量和修饰静态函数的作用是一样的,一般合并为一种。(这是比较多的分法)。
4.静态数据成员
用于修饰 class 的数据成员,即所谓“静态成员”。这种数据成员的生存期大于 class 的对象(实体 instance)。静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份,因此静态数据成员也叫做类变量,而普通数据成员也叫做实例变量。
那么 static 在全局数据区(静态区)分配内存。 static 只会被初始化一次,于实例无关。静态数据成员定义时要在全局数据区分配空间,和实力分配空间的位置不同,所以不能在类声明中定义。
#include<iostream>
using namespace std;yu
class Rectangle
{
private:
int m_w,m_h;
static int s_sum;
public:
Rectangle(int w,int h)
{
this->m_w = w;
this->m_h = h;
s_sum += (this->m_w * this->m_h);
}
void GetSum()
{
cout<<"sum = "<<s_sum<<endl;
}
};
int Rectangle::s_sum = 0; //初始化。需要在类外
int main()
{
cout<<"sizeof(Rectangle)="<<sizeof(Rectangle)<<endl;
Rectangle *rect1 = new Rectangle(3,4);
rect1->GetSum();
cout<<"sizeof(rect1)="<<sizeof(*rect1)<<endl;
Rectangle rect2(2,3);
rect2.GetSum();
cout<<"sizeof(rect2)="<<sizeof(rect2)<<endl;
system("pause");
return 0;
}
结果如下:
sizeof(Rectangle)=8
sum = 12
sizeof(rect1)=8
sum = 12
sizeof(rect1)=8
结论:
1.对于非静态数据成员,每个类对象(实例)都有自己的拷贝。而静态数据成员被当作是类的成员,由该类型的所有对象共享访问,对该类的多个对象来说,静态数据成员只分配一次内存。 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。
2.也就是说,你每 new 一个 Rectangle,并不会为 static int s_sum 的构建一份内存拷贝,它是不管你 new 了多少 Rectangle 的实例,因为它只与类Rectangle挂钩,而跟你每一个Rectangle的对象没关系。
5.静态成员函数
#include<iostream>
using namespace std;
class Rectangle
{
private:
int m_w,m_h;
static int s_sum;
public:
Rectangle(int w,int h)
{
this->m_w = w;
this->m_h = h;
s_sum += (this->m_w * this->m_h);
}
static void GetSum() //这里加上static
{
cout<<"sum = "<<s_sum<<endl;
}
};
int Rectangle::s_sum = 0; //初始化
int main()
{
cout<<"sizeof(Rectangle)="<<sizeof(Rectangle)<<endl;
Rectangle *rect1 = new Rectangle(3,4);
rect1->GetSum();
cout<<"sizeof(rect1)="<<sizeof(*rect1)<<endl;
Rectangle rect2(2,3);
rect2.GetSum(); //可以用对象名.函数名访问
cout<<"sizeof(rect2)="<<sizeof(rect2)<<endl;
Rectangle::GetSum(); //也可以可以用类名::函数名访问
system("pause");
return 0;
}
那么静态成员函数有特点呢?
1.上面注释可见: 对 GetSum() 加上 static,使它变成一个静态成员函数,可以用类名::函数名进行访问。(非静态成员函数不能直接通过累名调用,需要通过具体的对象调用)
2.静态成员函数不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态。
3.调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为他本来就是属于类的,用类名调用很正常)
理解:静态是属于类的,它是不知道你创建了10个还是100个对象,所以它对你对象的函数或者数据是一无所知的,所以它没办法调用,而反过来,你创建的对象是对类一清二楚的(不然你怎么从它那里实例化呢),所以你是可以调用类函数和类成员的,就像不管 GetSum 是不是 static,都可以调用 static 的 s_sum 一样。
operator重载运算符
operator 是 C++ 的一个关键字,它和运算符(如 =)一起使用,表示一个运算符重载函数,在理解时可将 operator 和待重载的运算符整体(如 operator=)视为一个函数名。
一、使用 operator 扩展运算符功能的原因如下:
1.使重载后的运算符的使用方法与重载前一致;
2.扩展运算符的功能只能通过函数的方式实现。(实际上,C++ 中各种“功能”都是通过函数实现的)
二、实现运算符重载的方式通常有以下两种:
运算符重载实现为类的成员函数;
运算符重载实现为非类的成员函数(即全局函数)。
在类内使用可以隐式指定左侧运算符,但是定义非类内函数(全局函数)必须显式指定。
https://liitdar.blog.youkuaiyun.com/article/details/80654324?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-3-80654324-blog-79420979.235%5Ev38%5Epc_relevant_default_base3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-3-80654324-blog-79420979.235%5Ev38%5Epc_relevant_default_base3&utm_relevant_index=6
C++ 右值引用及其作用
https://blog.youkuaiyun.com/caojianfa969/article/details/118927852
https://blog.youkuaiyun.com/baidu_41388533/article/details/106468153
一、对象移动概述
1.C++11标准引入了“对象移动”的概念
2.对象移动的特性是:可以移动而非拷贝对象
3.在C++旧标准中,没有直接的方法移动对象。因此会有很多不必要的资源拷贝
4.标准库容器、string、share_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝
5.在很多情况下会发生对象拷贝的现象,对象拷贝之后就被销毁了,在这种情况下,对象移动而非对象拷贝会大幅度提升性能
6.使用移动而非拷贝的另一个原因是:类似于IO类或unique_ptr这样的类,这些类都不能被共享资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝但可以移动
二、什么是左值和右值?
在掌握右值引用前,必须先知道什么是右值,既然有右值,那么肯定有左值。当我们在赋值的时候a=b,能够被放到=号左边的值即为左值,反则为右值。
那么什么值可以作为左值呢?显然变量肯定是可以作为左值的。什么值不能作为左值呢?显然常数、表达式、函数返回值等,是不能作为左值的,也就是右值。显然作为左值的都是可以长期保存起来的,对应是保存在内存中;但常数、表达式、函数返回值等都是临时值,这些值都保存在寄存器中。可以总结:
左值:可以长时间保存,可以存在于=左边的值,可以取地址;
右值:临时值,不能存在于=左边的值,不可以取地址。
友元
作用:想在类的成员函数外部直接访问对象的私有成员,因此,C++ 就有了友元(friend)的概念。
一、友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。
friend class 类名;
例子:
//友元类
class CCar
{
public:
CCar() = default;
~CCar() = default;
CCar(int price):m_price(price){}
private:
int m_price;
friend class CDriver; //声明 CDriver 为CCar友元类。所有CDriver成员函数就都可以访问类 CCar对象的私有成员
};
class CDriver
{
public:
CDriver() = default;
~CDriver() = default;
CDriver(CCar car1):m_myCar(car1){}
CCar m_myCar;
void ModifyCar() //改装汽车
{
m_myCar.m_price += 1000; //因CDriver是CCar的友元类,CDriver的函数可以访问其私有成员
std::cout << "myCar price is " << m_myCar.m_price << std::endl;
}
};
第 5 行将 CDriver 声明为 CCar 的友元类。这条语句本来就是在声明 CDriver 是一个类,所以 CCar 类定义前面就不用声明 CDriver 类了。第 5 行使得 CDriver 类的所有成员函数都能访问 CCar 对象的私有成员。如果没有第 5 行,第 13 行对 myCar 私有成员 price 的访问就会导致编译错误。
一般来说,类 A 将类 B 声明为友元类,则类 B 最好从逻辑上和类 A 有比较接近的关系。例如上面的例子,CDriver 代表司机,CCar 代表车,司机拥有车,所以 CDriver 类和 CCar 类从逻辑上来讲关系比较密切,把 CDriver 类声明为 CCar 类的友元比较合理。
友元关系在类之间不能传递,即类 A 是类 B 的友元,类 B 是类 C 的友元,并不能导出类 A 是类 C 的友元。“咱俩是朋友,所以你的朋友就是我的朋友”这句话在 C++ 的友元关系上 不成立。
二、友元函数
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。
将全局函数声明为友元的写法如下:
friend 返回值类型 函数名(参数表);
将其他类的成员函数声明为友元的写法如下:
friend 返回值类型 其他类的类名::成员函数名(参数表);
但是,不能把其他类的私有成员函数声明为友元。
#include<iostream>
using namespace std;
class CCar; //提前声明CCar类,以便后面的CDriver类使用
class CDriver
{
public:
void ModifyCar(CCar* pCar); //改装汽车
};
class CCar
{
private:
int price;
friend int MostExpensiveCar(CCar cars[], int total); //声明全局函数为友元
friend void CDriver::ModifyCar(CCar* pCar); //声明类的成员函数为友元
};
void CDriver::ModifyCar(CCar* pCar)
{
pCar->price += 1000; //汽车改装后价值增加
}
int MostExpensiveCar(CCar cars[], int total) //求最贵气车的价格
{
int tmpMax = -1;
for (int i = 0; i<total; ++i)
if (cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
int main()
{
return 0;
}
virtual关键字
https://blog.youkuaiyun.com/u010802169/article/details/88537490
1.虚函数
虚函数源于c++中的类继承,是多态的一种。在c++中,一个基类的指针或者引用可以指向或者引用派生类的对象。同时,派生类可以重写基类中的成员函数。这里**“重写”的要求是函数的特征标(包括参数的数目、类型和顺序)以及返回值都必须与基类中的函数一致**。如下所示:
class Base{
public:
Base()= default;
virtual ~Base() = default;
virtual void test(){std::cout << "Base test" << std::endl;}
};
class Inheriter: public Base{
public:
Inheriter()= default;
void test(){std::cout << "Inheriter test" << std::endl;}
};
int main()
{
//都用基类指针接受基类和继承类对象
Base* pbase = new Base;
Base* pinh = new Inheriter;
pbase->test();
pinh->test();
}
可以在基类中将被重写的成员函数设置为虚函数,其含义是:当通过基类的指针或者引用调用该成员函数时,将根据指针指向的对象类型确定调用的函数,而非指针的类型。如下,是未将test()函数设置为虚函数前的执行结果:
Base test
Base test
在将test()函数设置为virtual后,执行结果如下:
Base test
Inheriter test
如此,便可以将基类与派生类的同名方法区分开,实现多态。
说明:
1.只需将基类中的成员函数声明为虚函数即可,派生类中重写的virtual函数自动成为虚函数;
2.基类中的析构函数必须为虚函数,否则会出现对象释放错误。以上例说明,如果不将基类的析构函数声明为virtual,那么在调用delete p2;语句时将调用基类的析构函数,而不是应当调用的派生类的析构函数,从而出现对象释放错误的问题。
3.虚函数的使用将导致类对象占用更大的内存空间。对这一点的解释涉及到虚函数调用的原理:编译器给每一个包括虚函数的对象添加了一个隐藏成员:指向虚函数表的指针。虚函数表(virtual function table)包含了虚函数的地址,由所有虚函数对象共享。当派生类重新定义虚函数时,则将该函数的地址添加到虚函数表中。无论一个类对象中定义了多少个虚函数,虚函数指针只有一个。相应地,每个对象在内存中的大小要比没有虚函数时大4个字节(32位主机,不包括虚析构函数)。如下:
cout<<sizeof(base)<<endl; //12
cout<<sizeof(inheriter)<<endl; //12
base类中包括了两个整型的成员变量,各占4个字节大小,再加上一个虚函数指针,共计占12个字节;inheriter类继承了base类的两个成员变量以及虚函数表指针,因此大小与基类一致。如果inheriter多重继承自另外一个也包括了虚函数的基类,那么隐藏成员就包括了两个虚函数表指针。
4.重写函数的特征标必须与基类函数一致,否则将覆盖基类函数;
5.重写不同于重载。我对重载的理解是:同一个类,内部的同名函数具有不同的参数列表称为重载;重写则是派生类对基类同名函数的“本地改造”,要求函数特征标完全相同。当然,返回值类型不一定相同(可能会出现返回类型协变的特殊情况)。
Lambda表达式
https://blog.youkuaiyun.com/A1138474382/article/details/111149792
https://blog.youkuaiyun.com/yedawei_1/article/details/109490347
https://blog.youkuaiyun.com/yedawei_1/article/details/109508875
捕获列表:
一般情况下Lambda表达式函数体中不能访问程序在之前定义的非静态局部变量,可以通过捕获列表对其进行捕获再进行过修改和访问。
1.值捕获:只能在函数体中访问捕获的变量,不可以修改。(如果非要修改值捕获的变量,可以加上关键字mutable。但是,修改对原变量无任何影响!)
2.引用捕获:可以在函数体中修改捕获的变量
一、概念
lambda表达式(也称为lambda函数)是在调用或作为函数参数传递的位置处定义匿名函数对象的便捷方法。通常,lambda用于封装传递给算法或异步方法的几行代码 。
简单理解:在需要写一个及其简单的函数时,为了避免要再定义一个函数,可以直接用lambada表达式替代。
内存对齐与#pragma pack的作用
1:https://zhuanlan.zhihu.com/p/30007037
一、什么是内存对齐
1.现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
以下面代码为例:
//32位系统
#include<stdio.h>
struct{
int x;
char y;
}s;
int main()
{
printf("%d\n",sizeof(s); // 输出8
return 0;
}
理论上,32位系统下,int占4byte,char占一个byte,那么将它们放到一个结构体中应该占4+1=5byte;但是实际上,通过运行程序得到的结果是8 byte。32位系统每次对4字节进行处理,int占用4字节后char类型虽然在内存中只占用1字节,但是为了下次能够从4的倍数开始读取地址所以会在char后面补3个字节。这就是内存对齐所导致的。
2.以结构体为例,详细见下面网址。
https://mp.weixin.qq.com/s?__biz=MzU4MDc5NTA0Mw==&mid=2247485598&idx=1&sn=c7edf77f6d3c2e546d5b171d410ee6c9&chksm=fd502752ca27ae44509829d821829dba3b417e805474df80b3e26f7f95fe57fd283ca2c67175&mpshare=1&scene=1&srcid=1102dpMS5rfyrK8agrDcxD3s&sharer_shareinfo=44528a390dfef3c419a35fdec2a52c99&sharer_shareinfo_first=44528a390dfef3c419a35fdec2a52c99#rd
二、为什么要进行内存对齐
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度.
现在考虑4字节存取粒度的处理器取int类型变量(32位系统,一次处理32位,也就是4字节),该处理器只能从地址为4的倍数的内存开始读取数据。
假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作.
三种类成员初始化方式
https://zhuanlan.zhihu.com/p/384928500
初始化方式一:初始化列表
class A
{
public:
int a; // 初始化列表
A(int a_):a(a_){}
};
初始化方式二:构造函数初始化
class A
{
public:
int a; // 初始化列表
A(int a_, bool b) { a = a_; }
};
初始化方式三:声明时初始化(也称就地初始化,c++11后支持)
class A
{
public:
int a = 1; // 声明时初始化
A() {}
};
从C++11之后,这三种初始化的方法都可以使用,并不会存在冲突,但是,他们之间是有优先级顺序的,这个优先级来源于他们在初始化的时间顺序,后面初始化的会把前面的覆盖掉,成员变量的初始化顺序是声明时初始化->初始化列表->构造函数初始化。因此假如三种初始化方式同时存在的话,那么最后保留的成员变量值肯定是构造函数中初始化的值。
(一)声明时初始化的使用场景
一个优点是直观,你在声明的时候顺便给一个初始值。别人在看你代码的时候,点一下调到声明也能看到你赋予的初始值,不用再去看构造函数那里给的什么值
第二个优点更有用了,比如你要定义多个构造函数,每个构造函数都用列表初始化的方法初始化,多麻烦呀,请看下面的例子,妈妈看了再也不用担心我想用其他初始化方法了
class Group {
public:
Group() {}
Group(int a): data(a) {}
Group(Mem m): mem(m) {}
Group(int a, Mem m, string n): data(a), mem(m), name(n) {}
private:
int data = 1;
Mem mem{0};
string name{"Group"};
};
(二)列表初始化的使用场景
1.const成员变量只能用成员初始化列表来完成初始化,而不能在构造函数内赋值
2.初始化的数据成员是对象
3.需要初始化引用成员数据
但是,需要注意列表初始化的顺序,不过IDE会提示你的
(三) 构造函数初始化的使用场景
1.第一个就是拷贝和赋值构造函数里(不然怎么叫赋值构造函数呢)
2.第二个就是比较无聊的情况了,
explicit的用法
https://blog.youkuaiyun.com/qq_35524916/article/details/58178072
C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用。
#include <iostream>
using namespace std;
class Test1
{
public :
Test1(int num):n(num){}
private:
int n;
};
class Test2
{
public :
explicit Test2(int num):n(num){}
private:
int n;
};
int main()
{
Test1 t1 = 12;
Test2 t2(13);
Test2 t3 = 14;
return 0;
}
编译时,会指出 t3那一行error:无法从“int”转换为“Test2”。而t1却编译通过。注释掉t3那行,调试时,t1已被赋值成功。
注意:当类的声明和定义分别在两个文件中时,explicit只能写在在声明中,不能写在定义中。
chrono
https://blog.youkuaiyun.com/qfturauyls/article/details/107051902
c++11提供了日期时间相关的库chrono,通过chrono相关的库我们可以很方便的处理日期和时间。
chrono库主要包含了三种类型:时间间隔Duration、时钟Clocks和时间点Time point。
一、Duration
duration表示一段时间间隔,用来记录时间长度。可以表示几秒钟、几分钟或者几个小时的时间间隔。
duration的原型是:
template<class Rep, class Period = std::ratio<1>> class duration;
第一个模板参数Rep是一个数值类型,表示时钟个数;
第二个模板参数是一个默认模板参数std::ratio
(一)ratio
std::ratio的原型是:
template<std::intmax_t Num, std::intmax_t Denom = 1> class ratio;
ratio表示每个时钟周期的秒数,其中第一个模板参数Num代表分子,Denom代表分母,分母默认为1,ratio代表的是一个分子除以分母的分数值,比如ratio<2>代表一个时钟周期是两秒,ratio<60>代表了一分钟,ratio<6060>代表一个小时,ratio<6060*24>代表一天。而ratio<1, 1000>代表的则是1/1000秒即一毫秒,ratio<1, 1000000>代表一微秒,ratio<1, 1000000000>代表一纳秒。标准库为了方便使用,就定义了一些常用的时间间隔,如时、分、秒、毫秒、微秒和纳秒,在chrono命名空间下,它们的定义如下:
typedef duration <Rep, ratio<3600,1>> hours;
typedef duration <Rep, ratio<60,1>> minutes;
typedef duration <Rep, ratio<1,1>> seconds;
typedef duration <Rep, ratio<1,1000>> milliseconds;
typedef duration <Rep, ratio<1,1000000>> microseconds;
typedef duration <Rep, ratio<1,1000000000>> nanoseconds;
通过定义这些常用的时间间隔类型,我们能方便的使用它们,比如线程的休眠:
std::this_thread::sleep_for(std::chrono::seconds(3)); //休眠三秒
std::this_thread::sleep_for(std::chrono:: milliseconds (100)); //休眠100毫秒
自定义ratio
// 设置一秒钟有30个周期,计算1min、1s有多少个周期
std::chrono::duration<int, std::ratio<1, 30>> min_hz30(std::chrono::minutes(1));
std::chrono::duration<int, std::ratio<1, 30>> secon_hz30(std::chrono::seconds(1));
std::cout << "1min content " << min_hz30.count() << "tick" <<std::endl;
std::cout << "1s content " << secon_hz30.count() << "tick" << std::endl;
//输出结果
//1min content 1800tick
//1s content 30tick
(二)获取时间间隔的时钟周期个数的方法:count()
std::chrono::milliseconds ms{ 3 }; // 3 毫秒
// 6000 microseconds constructed from 3 milliseconds
std::chrono::microseconds us = 2 * ms; //6000微秒
std::cout << "3 ms duration has " << ms.count() << " ticks\n" << "6000 us duration has " << us.count() << " ticks\n";
//输出结果:
//3 ms duration has 3 ticks
//6000 us duration has 6000 ticks
(三)时间间隔之间可以做运算
比如下面的例子中计算两端时间间隔的差值:
std::chrono::seconds t1(1);
std::chrono::milliseconds t2(60);
std::chrono::milliseconds t3 = t1 - t2;
std::cout << "1s - 60ms =" << t3.count() << " milliseconds: " << "ms" << std::endl;
//计算结果
//1s - 60ms =940 milliseconds: ms
二、Time point
time_point表示一个时间点,用来获取1970.1.1以来的秒数和当前的时间, 可以做一些时间的比较和算术运算,可以和ctime库结合起来显示时间。time_point必须要clock来计时,time_point有一个函数time_since_epoch()用来获得1970年1月1日到time_point时间经过的duration。下面的例子计算当前时间距离1970年1月一日有多少天:
#include <iostream>
#include <ratio>
#include <chrono>
int main ()
{
using namespace std::chrono;
typedef duration<int,std::ratio<60*60*24>> days_type;
time_point<system_clock,days_type> today = time_point_cast<days_type>(system_clock::now());
std::cout << today.time_since_epoch().count() << " days since epoch" << std::endl;
return 0;
}
time_point还支持一些算术元算,比如两个time_point的差值时钟周期数,还可以和duration相加减。下面的例子输出前一天和后一天的日期:
#include <iostream>
#include <iomanip>// std::put_time所在头文件
#include <ctime>
#include <chrono>
int main()
{
using namespace std::chrono;
system_clock::time_point now = system_clock::now();
std::time_t last = system_clock::to_time_t(now - std::chrono::hours(24));
std::time_t next= system_clock::to_time_t(now + std::chrono::hours(24));
std::cout << "One day ago, the time was "<< std::put_time(std::localtime(&last), "%F %T") << '\n';
std::cout << "Next day, the time was "<< std::put_time(std::localtime(&next), "%F %T") << '\n';
}
输出:
One day ago, the time was 2014-3-2622:38:27
Next day, the time was 2014-3-2822:38:27
三、Clocks.
Clocks表示当前的系统时钟,内部有time_point, duration, Rep, Period等信息,它主要用来获取当前时间,以及实现time_t和time_point的相互转换。Clocks包含三种时钟:
1.system_clock:从系统获取的时钟;
2.steady_clock:不能被修改的时钟;(steady_clock可以获取稳定可靠的时间间隔,后一次调用now()的值和前一次的差值是不因为修改了系统时间而改变,它保证了稳定的时间间隔。它的用法和system用法一样。)
3.high_resolution_clock:高精度时钟,实际上是system_clock或者steady_clock的别名。
示例代码:可以通过now()来获取当前时间点:
#include <iostream>
#include <chrono>
int main()
{
std::chrono::steady_clock::time_point t1 = std::chrono::system_clock::now();
std::cout << "Hello World\n";
std::chrono::steady_clock::time_point t2 = std::chrono:: system_clock::now();
std::cout << (t2-t1).count()<<” tick count”<<endl;
}
输出:
Hello World
20801tick count
【-】可以通过时钟获取两个时间点之相差多少个时钟周期,我们可以通过duration_cast将其转换为其它时钟周期的duration:
cout << std::chrono::duration_cast<std::chrono::microseconds>( t2-t1 ).count() <<” microseconds”<< endl;
输出:
20 microseconds
利用系统时间Clocks求时间间隔,并使用duration_cast强制转换成其他时间周期类型。
std::chrono::system_clock::time_point time0 = std::chrono::system_clock::now();
std::this_thread::sleep_for(std::chrono::seconds(1));
std::chrono::system_clock::time_point time1 = std::chrono::system_clock::now();
auto cost = std::chrono::duration_cast < std::chrono::seconds >(time1 - time0).count();
std::cout << "time1 - time0 cost:" << cost << "s" << std::endl;
//输出
//time1 - time0 cost:1s
四 、timer :程序耗时定时器
可以利用high_resolution_clock来实现一个类似于boost.timer的定时器,这样的timer在测试性能时会经常用到。c++11中增加了chrono库,现在用来实现一个定时器是很简单的事情,还可以移除对boost的依赖。它的实现比较简单,下面是具体实现:
#include<chrono>
usingnamespace std;
usingnamespace std::chrono;
class Timer
{
public:
Timer() : m_begin(high_resolution_clock::now()) {}
void reset() { m_begin = high_resolution_clock::now(); }
//默认输出秒
double elapsed() const
{
return duration_cast<duration<double>>(high_resolution_clock::now() - m_begin).count();
}
//默认输出毫秒
//int64_t elapsed() const
//{
//return duration_cast<chrono::milliseconds>(high_resolution_clock::now() - m_begin).count();
//}
//微秒
int64_t elapsed_micro() const
{
return duration_cast<chrono::microseconds>(high_resolution_clock::now() - m_begin).count();
}
private:
time_point<high_resolution_clock> m_begin;
};
测试代码:
void fun(){ cout<<”hello word”<<endl;}
int main()
{
timer t; //开始计时
fun()
cout<<t.elapsed()<<endl; //打印fun函数耗时多少毫秒
cout<<t.elapsed_micro ()<<endl; //打印微秒
}
五、duration_cast<>时间周期转化
可以通过duration_cast<>()来将当前的时钟周期转换为其它的时钟周期,比如我可以把毫秒的时钟周期转换为微秒的时钟周期,然后通过count来获取转换后的分钟时间间隔:
std::chrono::seconds t1(1);
std::chrono::milliseconds t2(60);
std::chrono::milliseconds t3 = t1 - t2;
std::cout << "1s - 60ms = " << t3.count() << " ms" << std::endl;
std::cout << "1s - 60ms = " << std::chrono::duration_cast<std::chrono::microseconds>(t3).count() << " us" << std::endl;
std::move 原理实现与用法总结
https://blog.youkuaiyun.com/p942005405/article/details/84644069
原子性操作库(atomic)
原子性操作库(atomic)是C++11中新增的标准库,它提供了一种线程安全的方式来访问和修改共享变量,避免了数据竞争的问题。在多线程程序中,如果多个线程同时对同一个变量进行读写操作,就可能会导致数据不一致的问题。原子性操作库通过使用原子操作来保证多个线程同时访问同一个变量时的正确性。
1.每个原子变量都有以下几个特点:
1.原子变量的读写操作是原子的,即不会被其他线程中断。
2.原子变量的值可以被多个线程同时访问和修改。
3.原子变量的修改操作是按照一定顺序进行的,保证了多个线程对同一个变量进行操作时的正确性。
🚨注意:原子类型和原子操作函数需要包含 头文件才能使用
2.原子变量支持的类型
3.原子变量支持的操作
C++深拷贝与浅拷贝的区别 (简单易懂版)
https://blog.youkuaiyun.com/joshgu958/article/details/35302413
1.深拷贝与浅拷贝的区别
浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。浅拷贝就比如像引用类型,而深拷贝就比如值类型。对其中任何一个对象的改动都会影响另外一个对象。举个例子,一个人一开始叫张三,后来改名叫李四了,可是还是同一个人,不管是张三缺胳膊少腿还是李四缺胳膊少腿,都是这个人倒霉。
深拷贝是指 源对象与拷贝对象互相独立 ,其中任何一个对象的改动都不会对另外一个对象造成影响。举个例子,一个人名叫张三,后来用他克隆(假设法律允许)了另外一个人,叫李四,不管是张三缺胳膊少腿还是李四缺胳膊少腿都不会影响另外一个人。比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等。
浅拷贝就是对象的数据成员之间的简单赋值, 如你设计了一个没有类而没有提供它的复制构造函数,当用该类的一个对象去给令一个对象赋值时所执行的过程就是浅拷贝,如:
#include <iostream>
using namespace std;
class A
{
public:
A(int _data) : data(_data){}
int GetX() {return data;}
A() {}
private:
int data;
};
int main()
{
A a(5);
A b;
b = a;
cout << b.GetX() << endl;
return 0;
}
运行结果为 5. 这就是一个浅拷贝的过程。
如果对象中没有其他的资源(如:堆,文件,系统资源等),则深拷贝和浅拷贝没有什么区别,
但当对象中有这些资源时,例子:
#include <iostream>
using namespace std;
class A
{
public:
A(int _size) : size(_size){ data = new int[size]; } //假如其中有一段动态分配的内存
int Get_Val() {return *data;}
int *Get_Val_add() { return data; }
A() {}
~A(){delete[] data; data = NULL; } //析构时释放资源
private:
int *data;
int size;
};
int main()
{
A a(5);
A b(a);
//b = a;
cout << b.Get_Val() << endl;
return 0;
}
2.程序运行时报错:free(): double free detected in tcache 2
因为类A中的复制构造函数是编译器生成的,所以A b(a)执行一个浅拷贝过程,
这里b的指针data 和a的指针指向了堆上的同一块内存,a和b析构时,b先把其data指向的动态分配的内存释放了一次,而后a析构时又将这块已经释放过的内存再释放一次。
对同一块内存释放执行2次及2次以上的释放会造成内存泄露或者是程序crash!
这时就需要用 深拷贝 来解决这个问题:
深拷贝就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。
也就是深拷贝会在堆内存中另外申请空间来储存数据,从而解决了指针悬挂问题。当数据成员中有指针时,必须要用深拷贝
#include <iostream>
using namespace std;
class A
{
public:
A(int _size) : size(_size){ data = new int[size]; } //假如其中有一段动态分配的内存
int Get_Val() {return *data;}
int *Get_Val_add() { return data; }
A() {}
A( const A& _A) : size(_A.size){ data = new int[size]; } //深拷贝
~A(){delete[] data; data = NULL; } //析构时释放资源
private:
int *data;
int size;
};
int main()
{
A a(5);
A b(a);
cout << b.Get_Val() << endl;
return 0;
}
Linux C语言中open函数
https://blog.youkuaiyun.com/zjhkobe/article/details/6633435#t3
相关函数
open(打开文件)
相关函数
read,write,fcntl,close,link,stat,umask,unlink,fopen
头文件
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
定义函数
int open( const char * pathname, int flags);
int open( const char * pathname,int flags, mode_t mode);
函数说明
参数pathname 指向欲打开的文件路径字符串。下列是参数flags 所能使用的flag:
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以可读写方式打开文件。
上述三种旗标是互斥的,也就是不可同时使用,但可与下列的旗标利用OR(|)运算符组合。
O_CREAT 若欲打开的文件不存在则自动建立该文件。
O_EXCL 如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则建立该文件,否则将导致打开文件错误。此外,若O_CREAT与O_EXCL同时设置,并且欲打开的文件为符号连接,则会打开文件失败。
O_NOCTTY 如果欲打开的文件为终端机设备时,则不会将该终端机当成进程控制终端机。
O_TRUNC 若文件存在并且以可写的方式打开时,此旗标会令文件长度清为0,而原来存于该文件的 资料也会消失。
O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。
O_NONBLOCK 以不可阻断的方式打开文件,也就是无论有无数据读取或等待,都会立即返回进程之中。
O_NDELAY 同O_NONBLOCK。
O_SYNC 以同步的方式打开文件。
O_NOFOLLOW 如果参数pathname 所指的文件为一符号连接,则会令打开文件失败。
O_DIRECTORY 如果参数pathname 所指的文件并非为一目录,则会令打开文件失败。
参数mode 组合
此为Linux2.2以后特有的旗标,以避免一些系统安全问题。参数mode 则有下列数种组合,只有在建立新文件时才会生效,此外真正建文件时的权限会受到umask值所影响,因此该文件权限应该为(mode-umaks)。
S_IRWXU 00700 权限,代表该文件所有者具有可读、可写及可执行的权限。
S_IRUSR 或S_IREAD, 00400权限,代表该文件所有者具有可读取的权限。
S_IWUSR 或S_IWRITE,00200 权限,代表该文件所有者具有可写入的权限。
S_IXUSR 或S_IEXEC, 00100 权限,代表该文件所有者具有可执行的权限。
S_IRWXG 00070权限,代表该文件用户组具有可读、可写及可执行的权限。
S_IRGRP 00040 权限,代表该文件用户组具有可读的权限。
S_IWGRP 00020权限,代表该文件用户组具有可写入的权限。
S_IXGRP 00010 权限,代表该文件用户组具有可执行的权限。
S_IRWXO 00007权限,代表其他用户具有可读、可写及可执行的权限。
S_IROTH 00004 权限,代表其他用户具有可读的权限
S_IWOTH 00002权限,代表其他用户具有可写入的权限。
S_IXOTH 00001 权限,代表其他用户具有可执行的权限。
返回值
若所有欲核查的权限都通过了检查则返回文件描述符,表示成功,只要有一个权限被禁止则返回-1。
C++中memset函数详解
https://blog.youkuaiyun.com/weixin_43790779/article/details/114489612
Linux fcntl函数详解
https://www.cnblogs.com/xuyh/p/3273082.html
功能描述:根据文件描述词来操作文件的特性。
文件控制函数 fcntl – file control
#头文件:
#include <unistd.h>
#include <fcntl.h>
#函数原型:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
//描述:
//fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符。
//针对cmd的值,fcntl能够接受第三个参数(arg)。
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
程序
一、避免开启重复同名进程
int detect_process(const char *process_name,int &count)
{
FILE *strm;
char cmd[128];
int pid = -1;
// 使用 pidof 命令获取指定进程名的所有进程 ID
sprintf(cmd, "pidof %s", process_name);
if ((strm = popen(cmd, "r")) != NULL)
{
char buf[256];
if (fgets(buf, sizeof(buf), strm) != NULL)
{
// 从 pidof 命令输出中解析出进程 ID 的数量
count = 0;
for (char *token = strtok(buf, " \t\n"); token; token = strtok(nullptr, " \t\n"))
{
count++;
if (count > 1)
{
break; // 如果进程 ID 数量超过 1,直接退出
}
pid = atoi(token);
}
}
}
pclose(strm);
return pid;
}
(一)c语言调用shell命令: popen使用以及获取命令返回值
https://blog.youkuaiyun.com/fuyuande/article/details/87859313
在程序里调用系统命令的话,可以使用的是system()函数,不过system函数无法获取命令的输出。popen和system类似,也能够执行系统命令,区别在于它能够获取命令的输出或者给系统命令传递参数,类似与管道的作用。
popen函数会创建一个管道,并且创建一个子进程来执行shell,shell会创建一个子进程来执行command,根据type的值不同,分成两种情况:
1.如果type是r: command执行的标准输出,就会写入管道,从而被调用popen的进程读到,通过对popen返回的FILE类型指针执行read或fgets操作,就可以读取到command的标准输出。
2.如果type是w:调用popen的进程可以通过对FILE类型指针执行write、fputs等操作,负责往管道里面写
入,写入的内容经过管道传递给执行command的进程,作为命令的的输入。
I/O结束后,可以调用pclose函数来关闭管道,并且等待子进程的退出。pclose函数成功时会返回
子进程shell的终止状态。popen函数和system函数类似,如果command对应命令无法执行,就如同
执行了exit(127)一样,如果发生其它错误,pclose函数则返回-1.可以从errno中获取到失败的原因。
1.popen接口定义:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
2.返回值:
popen函数成功时,会返回stdio库封装的FILE类型的指针,失败时会返回NULL,并且设置errno,
常见的失败有fork失败、pipe失败,或者分配内存失败。
3.命令执行后需要获取命令的返回值,可以通过如下几个宏来获取:
1. 进程正常退出
WIFEXITED(status) : 如果子进程正常退出,则返回true,否则返回false
WEXITSTATUS(status):如果子进程正常退出,则本宏用来获取进程的退出状态
2. 进程收到信号,导致退出
WIFSIGNALED(status) : 如果进程是被信号杀死的,则返回true,否则返回false
WTREMSIG(status):如果进程是被信号杀死的,则返回杀死进程信号的值
WCOREDUMP(status) : 如果子进程产生了core dump,则返回true,否则返回false
(二)读字符串函数fgets
https://blog.youkuaiyun.com/yihe_xinyi/article/details/78728379
函数原型:
char fgets ( char* str, int size, FILE* stream)*
*str: 字符型指针,用来存储所得数据的地址。字符数组。
size: 整型数据,要复制到str中的字符串的长度,包含终止NULL。
*stream:文件结构体指针,将要读取的文件流。
意义:从stream所指向的文件中读取size-1个字符送入字符串数组str中。
功能:从文件中读取字符串,每次只读取一行。
注意:
- fgets每次最多只能读取n-1个字2.符,第n个为NULL。
- 当遇到换行符或者EOF时,即使当前位置在n-1之前也读出结束。
- 若函数返回成功,则返回 字符串数组str的首地址。
举例:
文件flie:
This is a text.
my file.
如果用fgets(str1,5,file);去读取,则执行:
str1=This,实际只读取5-1=4个字符。
如果用fgets(str2,20,flie);去读取,则执行:
str2=This is a text. 遇到换行符就停止读出。
/* fgets example */
#include <stdio.h>
int main()
{
FILE * pFile;
char mystring [100];
pFile = fopen ("myfile.txt" , "r");
if (pFile == NULL) perror ("Error opening file");
else {
if ( fgets (mystring , 100 , pFile) != NULL )
puts (mystring);
fclose (pFile);
}
return 0;
}
(三)C/C++——字符串分割(strtok, strtok_s)
1.定义
分解字符串为一组字符串。s为要分解的字符,delim为分隔符字符(如果传入字符串,则传入的字符串中每个字符均为分割符)。首次调用时,s指向要分解的字符串,之后再次调用要把s设成NULL。在头文件#include<string.h>中。
2.原型
char *strtok(char s[], const char *delim);
3.说明
(1)当strtok()在参数s的字符串中发现参数delim中包含的分割字符时,则会将该字符改为\0 字符(nullptr)。在第一次调用时,strtok()必需给予参数s字符串,往后的调用则将参数s设置成NULL。每次调用成功则返回指向被分割出片段的指针。
(2)返回值
从s开头开始的一个个被分割的串。当s中的字符查找到末尾时,返回NULL。如果查找不到delim中的字符时,返回当前strtok的字符串的指针。所有delim中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。
(3)需要注意的是,使用该函数进行字符串分割时,会破坏被分解字符串的完整,调用前和调用后的s已经不一样了。第一次分割之后,原字符串str是分割完成之后的第一个字符串,剩余的字符串存储在一个静态变量中,因此多线程同时访问该静态变量时,则会出现错误。
4.使用
strtok函数会破坏被分解字符串的完整,调用前和调用后的s已经不一样了。如果要保持原字符串的完整,可以使用strchr和sscanf的组合等。
5.例子:
for (char *token = strtok(buf, " \t\n"); token; token = strtok(nullptr, " \t\n"))
这个 for 循环初始化语句可以拆分为三个部分:
1.char *token = strtok(buf, " \t\n"):将字符串 buf 划分为孤立的标记,分隔符为空格、制表符和换行符,并将第一个标记指针赋值给变量 token。
2.token:循环条件,检查 token 是否为 null 指针。如果 token 为 null 指针,则循环终止。
3.token = strtok(nullptr, " \t\n"):在每次循环结束时,将 token 更新为下一个标记。
因此,这个 for 循环的作用是通过 strtok 函数逐个解析字符串 buf 中的孤立标记,并将每个标记指针赋值给变量 token,然后在每次循环中对标记执行特定操作,直到所有标记都被处理完毕为止。在这个过程中,分隔符为空格、制表符和换行符。如果 token 为 null 指针,则结束循环。