0%

c++快速进阶-持续更新

原文链接:c++快速进阶-持续更新

  1. 构造
  2. 析构
  3. 继承
  4. 派生

多继承问题

当多继承类有共同基类时,基类会构造多次,为了消除基类的二义性,我们采用虚继承方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include<iostream>


class A{
public:
A(){
std::cout<<"A create\n";
}



~A(){
std::cout<<"A delete\n";
}
};

class B:virtual public A{
public:
B():A(){
std::cout<<"B create\n";
}




~B(){
std::cout<<"B delete\n";
}
};


class C:virtual public A{
public:
C():A(){
std::cout<<"C create\n";
}
~C(){
std::cout<<"C delete\n";
}
};


class D:public B,C {
public:
D():B(),C(){
std::cout<<"D create\n";
}

};


int main(){

D d=D();

std::cin.get();
return 0;
}


虚函数:

基类的派生类是一个具有多个类特性的单类,在赋值时可以互相赋值。
类编译时是静态编译,若A-B-C依次派生,A指针赋值BC,调用的函数实际上还是A的函数。
实现编译器能识别,可以将需要的函数标记virtual,同时各派生类使用override重写(防止函数名写错无效重写),实现动态编译

纯虚函数

接口类声明一个未定义函数,该函数需要被子类实现,否则无法实例化,是一个抽象类。

虚函数不需要重写,但纯虚函数要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<iostream>


class A{
public:
A(){
std::cout<<"A create\n";
}

void print(){
std::cout<<"A print\n";
}
virtual void virtualprint(){
std::cout<<"A virtualprint\n";
}

virtual void interface()=0;

~A(){
std::cout<<"A delete\n";
}
};

class B:virtual public A{
public:
B():A(){
std::cout<<"B create\n";
}

void print(){
std::cout<<"B print\n";
}
void virtualprint() override{
std::cout<<"B virtualprint\n";
}
void interface() override{
std::cout<<"B interface override\n";
}

~B(){
std::cout<<"B delete\n";
}
};


int main(){

B b=B();
A* a=&b;
a->print();
a->virtualprint();
a->interface();
std::cin.get();
return 0;
}

可见性:

pri:仅类内 pro:仅派生类内 pub:全

友元:
friend关键字,标记函数,类,类成员函数,在不破坏可见性条件下操作类内私有对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<iostream>

class B;
class A{

friend void get_private_a(A const &a);
friend class B;
private:
int a;
public:
A(int a):a(a){
std::cout<<"A create:"<<a<<"\n";
}
~A(){
std::cout<<"A delete\n";
}
};

class B:virtual public A{
public:
B(int a):A(a){
std::cout<<"B create:"<<a<<"\n";
}
void get_private_a(A const &a) {
std::cout<<"A.a:"<<a.a<<"\n";
}
~B(){
std::cout<<"B delete\n";
}
};

void get_private_a(A const &a) {
std::cout<<"A.a:"<<a.a<<"\n";
}


int main(){

A a=A(2);
B b=B(0);
get_private_a(a);
b.get_private_a(a);
std::cin.get();
return 0;
}

常量特性

  1. 作用范围
    const int* a/ int const* a:
    a不能修改(指针指向内容不能修改),可以重新赋值指针
    int
    const a:
    a本身不能修改,*a可以修改

  2. 常量限制
    const修饰成员函数表明这个函数不会修改类内成员变量。
    const 对象只能调用const成员函数,这保证了限制的完备性,变量不会有任何变化。

  3. 多个函数
    const可以区分同名函数,但不能区分构造函数

  4. 突破限制
    标记mutable标记的变量可以被const修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class A{
    private:
    int a;
    const int b;
    mutable int c;

    public:
    A(int a):a(a){
    std::cout<<"A create:"<<a<<"\n";
    }
    void seta(int a){
    this->a=a;
    }
    // void seta(int a) const{
    // this->a=a;
    // }
    // void setb(int b){
    // this->b=b;
    // }
    void setc(int c) const{
    this->c=c;
    }

    ~A(){
    std::cout<<"A delete\n";
    }
    };

初始列表

在构造函数时使用构造列表实例化对象。在cpp的类中,classA a作为一个成员变量,在构造函数被赋值a=classA(0),如果存在classA()缺省构造,则A会构造2次,构造列表能避免

三元操作符

a= cond1?cond2:1:2 可嵌套

构造实例

构造空间

栈:classA a;==classA a=classA();
堆:new delete malloc free

构造实例类型

explicit 作用于构造函数,不允许隐式类型转换

运算符重载

operator++/–/+/-/==/!= 运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<iostream>

class Coordinate2D{
private:
int x;
int y;
public:
Coordinate2D(int x,int y):x(x),y(y){
std::cout<<"Coordinate2D create:"<<x<<','<<y<<"\n";
}
Coordinate2D operator+(const Coordinate2D& other){
return Coordinate2D(this->x+other.x,this->y+other.y);
}
Coordinate2D operator-(const Coordinate2D& other){
return Coordinate2D(this->x-other.x,this->y-other.y);
}
Coordinate2D& operator++(){
x++;
y++;
return *this;
}
Coordinate2D operator++(int){
Coordinate2D old=*this;
x++;
y++;
return old;
}
friend std::ostream& operator<<(std::ostream& os,const Coordinate2D& cod){
os<<"("<<cod.x<<','<<cod.y<<')'<<'\n';
return os;
}
// cin overloading is weak
// friend std::istream& operator>>(std::istream& is,Coordinate2D& cod){
// is >>cod.x;
// return is;
// }

~Coordinate2D(){
std::cout<<*this<<" delete\n";
}
};


int main(){
Coordinate2D o=Coordinate2D(0,0);
Coordinate2D a=Coordinate2D(1,0);
Coordinate2D b=Coordinate2D(0,-1);
Coordinate2D c=a+(++b);
std::cout<<'c'<<c;
std::cout<<'a'<<a;

std::cin.get();
return 0;
}



this

this就是类的当前实例的指针,this是否可修改取决于当前函数是否const

智能指针

memory库的unique_ptr
特性:

  1. 该指针只能存在一个,不能同时存在两个指向同一个对象的指针
  2. 使用make unique 安全构造
  3. 使用move指令移动
    1
    2
    std::unique_ptr<Coordinate2D> p=std::make_unique<Coordinate2D>(1,2);
    std::unique_ptr<Coordinate2D> p2=std::move(p);
    shared ptr:使用引用计数控制对象
    weak ptr:shared的弱引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
std::unique_ptr<Coordinate2D> p1=std::make_unique<Coordinate2D>(1,0);
std::unique_ptr<Coordinate2D> p2=std::move(p1);
}

std::shared_ptr<Coordinate2D> p2;
{
std::shared_ptr<Coordinate2D> p1=std::make_unique<Coordinate2D>(2,0);
p2=p1;
}

std::weak_ptr<Coordinate2D> p4;
{
std::shared_ptr<Coordinate2D> p3=std::make_unique<Coordinate2D>(3,0);
p4=p3;
}

copy构造

将一个函数=delete,表示不允许拷贝构造

  1. 使用等于复制对象时,c++实际上是调用了复制构造函数,默认的是shallow copy,当对象的成员是指针时,这会导致两个实例指向了同一个堆中的对象,在析构删除实例的时候,会删除堆两次。
  2. 实现复制构造很重要
  3. 在函数传参时,尽量使用const reference,速度快,保证只读性,如果直接是类参数,每次函数栈都要复制删除一次
1
2
3
4
Coordinate2D(const Coordinate2D& cpy)=delete;
Coordinate2D(const Coordinate2D& cpy){
Coordinate2D(cpy.x,cpy.y);
}

stl

https://zhuanlan.zhihu.com/p/564057584

vector

动态数组,连续对象优于连续指针,保证内存访问局部性

使用push问题:

  1. push入对象每次都要从当前域将对象复制到vector空间中
  2. 每次vector变化都要重新复制所有数据
    这会导致指数式变化

解决:

  1. 使用emplace_back替代push,直接让vector创建对象
  2. reverse指定初始数组大小
1
2
3
4
5
6
std::vector<Coordinate2D> vcod;
vcod.reserve(3); // reduce duplicate malloc memory
// vcod.push_back({0,0}); create on stack and cpy to vec
vcod.emplace_back(0,0); //send args to vec to create
vcod.emplace_back(1,0);
vcod.emplace_back(2,0);

array

array<type,size>,相比于c语言数组,array底层是宏和模板实现的,无性能开销,且提升代码安全和维护性

std::array<Coordinate2D,3> codarr{Coordinate2D(0,0),Coordinate2D(0,0),Coordinate2D(0,0)};

list

set map

unordered_set,unordered_map

链接

详见linux开发中的gcc编译

多值传参

  1. 定义结构体返回
  2. 直接参数指针
  3. 返回数组,tuple,pair或其他的

template

  1. 函数使用template实现函数模板
  2. 类使用template实现成员变量模板化
    1
    2
    3
    4
    5
    template<typename T>
    bool equal(const T& a1,const T& a2){
    return a1==a2;
    }
    std::cout<<equal<int>(1,2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<int N,class type>
class Arr{
private:
int size=N;
type arr[N];
public:
Arr(){
for(int i=0;i++;i<size) arr[i]=0;
}
Arr(type x){

for(int i=0;i<size;i++) arr[i]=x;
}
void print(){
for(int i=0;i<size;i++){
std::cout<<i<<":"<<arr[i]<<'\n';
}
}
};
Arr<10,float> a(1.1);

auto

两面性:auto能够兼容API返回类型变化,但要保证auto变量的抽象性不被破坏

主要在数组迭代器中缩略变量的类型如std::vectorstd::string

函数指针,lambda与回调函数

函数指针是实现函数作为变量的方法
void(*func)(params)=funca;
func(actual params);

为了简化奇怪的函数定义,我们使用typedef
typedef void(*func)(params)
func f=funca;

例如有个函数是遍历vector中对象,并实现一个print操作,这个函数传入vector加一个函数指针,实现指定操作。当传入不同函数指针实现不同操作,从而达到实现回调函数的目的。

为了进一步提升效率,回调函数可能非常多以至于不需要每次都将函数定义出来,直接使用lambda函数实现

[capture](arg){body}

如果lambda复杂,需要使用本地参数,需要指定capture,如=表示直接拷贝所有值,&或直接指定变量,还可以标记mutable修改对象值

当然,由于使用了捕获,需要使用functional库
std::function<void(int)>& f 来支持这种函数指针

回调函数通过函数指针实现传输,可以通过这种方式实现多任务分布式系统下的异步执行。例如点击一个按钮1s后恢复,不需要一直等待只需要传入恢复函数,按钮等待完毕后直接调用恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
#include<String>
void callback(const std::string& msg){
std::cout<<"delay callback:"<<msg;
};

typedef void(*func_ptr)(const std::string&);

void callprocess(func_ptr& func){
std::cout<<"processing...:";
func("callprocess calling callback function\n");
}


int main(){

// 1. using function pointer directly;
void(*func)(const std::string&)=callback;
func("direct function pointer\n");


// 2. using typedef define func_pter type;
func_ptr funcp=&callback;
callprocess(func);

// 3. using lambda func defined in function.
func_ptr lambda_func=[](const std::string& msg){std::cout<<"labmda function:"<<msg;};
callprocess(lambda_func);

std::cin.get();
return 0;
}


命名空间

using namespace 看上去简单,实际上难降低代码阅读性,cpp标准库命名蛇形,我们自定义函数可以使用帕斯卡,实现快速辨别

using类似import,会出现函数重名的现象,通过namespace a{}构建命名空间

时间库

chrono时间库:c++标准时间库
包括time_point duration clock 主要3个类。

需要原因:

  1. 显示时间
  2. 存在规划
  3. 通信同步的需求

解决:
获取系统时间变量并转为时间戳,然后转为localtime获取各时间或者直接使用strftime。
对于时间间隔可以使用时间戳差值进行比较。
线程挂起定义一个duration变量然后使用当前线程的sleep_for函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void time_tick(){
using namespace std::chrono;
std::chrono::system_clock::time_point t1=std::chrono::system_clock::now();
time_t timestamp=system_clock::to_time_t(t1);

std::tm* t3=std::localtime(&timestamp);
char tbuf[80];
std::strftime(tbuf,sizeof(tbuf),"%Y-%m-%d-%H-%M-%S",t3);
std::cout<<"strftime:"<<tbuf<<"\n";
}



int main(){


using namespace std::chrono;

// 1. time point

std::chrono::system_clock::time_point t1=std::chrono::system_clock::now();

time_t timestamp=system_clock::to_time_t(t1);

std::cout<<"timestamp:"<<timestamp<<"\n";

std::tm* t3=std::localtime(&timestamp);
std::cout<<"t3:"<<t3<<"\n";


char tbuf[80];
std::strftime(tbuf,sizeof(tbuf),"%Y-%m-%d-%H-%M-%S",t3);
std::cout<<"strftime:"<<tbuf<<"\n";

std::cout<<"d:"<<t3->tm_mday<<" m:"<<t3->tm_mon+1<<" y:"<<t3->tm_year+1900<<" day in year:"<<t3->tm_yday<<"\n";

duration<int,std::ratio<1>> dur(2); //1s
std::this_thread::sleep_for(dur);
time_tick();


std::cin.get();
return 0;
}

chrono库实现时序计时
要适当挂起保证任务不一直占用
literal的chrono literal空间的sleep实现

io复用与io阻塞

IO复用主要是在linux下实现网络通信的连接管理。

阻塞
BIO阻塞型,当系统调用读写时,系统未准备好时阻塞
NIO非阻塞,立即返回,通过轮询处理

复用
场景:网络通信中,程序需要读写数据,但如果使用阻塞io会导致整个程序会被单个io阻塞,导致可能数据读写不能立即响应。例如服务器与所有客户端通信,每个客户端维护一个fd,我们需要一个方法能够去处理存在事件的fd。
解决:select Poll epoll
思想:使用位图bitmap实现,服务器进程初始化一个位图,这个位图对应所有io,我们将这个位图交给内核去处理,当有事件时,对应fd会置位,我们遍历所有fd然后去读取或处理被置位的io即可
select:当没有事件时内核会一直阻塞,直到有事件,且bitmap每次都需要置位
poll:不会阻塞
epoll:不会阻塞,且会讲有事件的fd前置,只需要遍历前面的fd即可

多线程

thread库

  1. linux系统中gcc的thread库基于pthread实现,可以直接使用。并且linux系统库使用c实现的多进程线程与通信更安全可靠
  2. win下使用mingw的gcc编译会无法使用thread库,需要如下操作:
    1. 下载https://gitcode.com/mirrors/meganz/mingw-std-threads/tree/master项目下的thead相关的.h文件
    2. 将其放置到gcc的lib库中c++部分,如C:\gcc\mingw64\lib\gcc\x86_64-w64-mingw32\8.1.0\include\c++
      3.使用#include<mingw.thread.h>
      如下文件:
      mingw.condition_variable.h
      mingw.future.h
      mingw.invoke.h
      mingw.latch.h
      mingw.mutex.h
      mingw.shared_mutex.h
      mingw.thread.h

除非在win下有应用需求否在不建议使用c++的线程库

算法

字符串
数组
线性表
树:深度遍历和层次遍历
图:广度遍历
区间,矩阵,字典树

双指针,滑动窗口,哈希表,二分查找
递归,回溯,分治,堆(优先队列)
动态规划

unordered_set,unordered_map
min,max
swap
queue dequeue stack priority_queue:greater创建小堆
initializer_list
decltype
lower_bound
fabs
asm

项目

目前计划准备2两个:C++开发项目和一个unity项目

要了解的库有 json,linux内核库,muduo
要了解的工具 cmake,redis

多线程网络项目

unity2.5D项目