适合具备一定基础的同学上手,都是一些个人觉得十分重要的小知识点。 解释性语句并不多,具体展开请读者自行搜索。
本人只是c++初学者,如有疏漏,敬请留言指正!
credentials是添加远程连接。 爆红Not found的,请自行在远程服务上下载好(我这里本来有cmake,但是由于版本过低,需要更新)
toolchain选择刚刚配置好的远程选项
回车确定之后,环境就跟远程同步了(windows下可以使用#include <sys/socket.h>测试,看是否找得到这个头文件,因为这个头文件是linux系统调用)
Clion远程小工具 远程路径Depolymentpath自动创建的为临时路径,这里可以自己指定,实现映射远程项目到本地。如下图:
在clion界面右侧打开远程文件系统。如下图:
打开一个类似git的版本差异比较窗口,可以在该窗口实现向远程同步,如下图
其他同步操作都在Tools中,具体操作见名知意即可。
附:更新\下载 cmake(注意clion的支持版本)
卸载旧版
sudo apt-get autoremove cmake
下载新版
cd ~
wget https://cmake.org/files/v3.19/cmake-3.19.8.tar.gz
tar xvf cmake-3.19.8.tar.gz
cd cmake-3.19.8
安装
./bootstrap --prefix=/usr
make
sudo make install
测试
cmake –version
1)stack 栈区:由编译器自动分配和释放 一般存放函数的参数值、局部变量的值等 2)heap 堆区:由程序员分配及释放。若程序员不释放,程序结束后可能由OS回收 3)register 寄存器区:用来保存栈顶指针和指令指针 4)全局区(静态区):全局变量和静态变量是存储在一起的。初始化的和未初始化的是分开的。 程序结束后由系统释放。分为data段(已初始化)和bss段(未初始化) 5)文字常量区:程序结束后由系统释放,存放常量字符串 6)text 程序代码区:存放函数体的二进制代码
与Java相比:
定义变量和数组时,Java默认初始化,C++不初始化; 在类中,方法中定义变量、动态数组时,Java默认初始化,C++不初始化
define仅仅是做简单的替换(或者一些运算表达式),在编译时期完成 typedef 对于类型定义提供了更丰富的支持,在运行时完成
类的继承范围,指的是子类改变父类数据的最高权限:如果是public继承,则改变父类数据最高到public,如果是private,则把父类继承的所有数据改为private。
常量只可以访问常函数,所以对于只读函数必须加上const,不然会导致常量不可调用该函数。 传引用或者传指针,可以在函数内改变该对象,为了告知调用者函数是否真的做了改变,需要通过函数参数是否加了const来辨别(尤其是一些不开源的代码,这个尤为重要)。
const是编译时检查,运行时其实是可以修改的(如const_cast) const在号前面,则值为常量,在后面,则指针为常量。
传值是新建副本传过去(整包传),如果数据太大,则也会消耗较大内存。 尽量不要传值。
传引用则仅仅是传该数据的地址。 传引用跟传指针是一样的,但是传引用更方便。 为了避免传引用被改,改成传const引用即可。
返回值也尽量传引用,但是在函数内部创建的res不能返回引用,栈上分配的“内存空间”结束后直接被回收,因此直接传值,或者将返回对象作为参数传入,最后返回该引用。(栈上分配的static函数调用完毕不会被释放,因此可以返回引用(如懒加载单例))
友元避免了通过get获取private数据,提高速度; 不过友元破坏了封装性; 此外,友元关系不可传递(A是B的朋友,B是C的朋友,但A不是C的朋友); 友元关系不可继承;
左移运算符只能写为全局函数(直接全局或者先友元定义然后类外实现),因为cpp所有操作符都是作用在左值(如果写在类内,则表示obj<<cout(obj是隐含的),这显然不合适),因此需要写在全局,控制cout在左边。
delete xx会被转化为两个语句: 调用析构:~xx() 释放名称空间:free(xx)
delete[] xx:
因此array new要与array delete一一对应,因为delete[] 表示会调用数组个数次析构函数,而delete仅仅是回收开辟的空间,但是析构函数只会调用一次。在析构函数中的释放动作就不会被完全执行。
带指针的类,指的是属性成员中有指针变量。 必须重写三个方法: 拷贝构造:避免浅拷贝(指针直接指向同一块区域导致回收时重复回收) 赋值重载:避免浅拷贝(指针直接指向同一块区域导致回收时重复回收) 析构函数:指针开辟在堆,需要回收
赋值重载首先考虑可能存在自我复制,其次是清空原空间。
个人理解,cpp实质上仅仅允许向上转型。
即便看起来是向下转型转型成功,其实也必须运行时是向上转型。 比如继承链D->C->B->A (A为顶级父类) A a=new D() B b=dynamic_cast(a); 看起来是a向下转成了b,实质还是运行时的d向上转型成了b
必须在父类方法上加virtual,才能在通过父类指针指向子类对象的时候,调用子类重写的方法。 (多态是virtual的多态)
右值只能放在右边(左值两边都能放),临时变量一定是右值。 没有名称的变量一定是右值。
一般情况下,只能获得左值的引用(因为右值没有名称),如果要获得右值的引用,则使用&&。
move拷贝构造,会将所有指针打断然后替换,相当于废弃原有变量(因此临时变量比较合适move)。 如果不是临时变量,想用move拷贝构造,使用构造函数(std::move(变量))即可。 move指的是把参数当做右值使用。
具备特殊功能的指针类。
内置的智能指针:用于解决内存泄露的一种指针自动回收机制(引用计数法): unique_ptr:只允许被引用一次,作用域结束后自动回收 shared_ptr:可以被共享引用,其内存在一个引用计数器,计数器为0时自动回收。 weak_ptr:类似弱引用,查看是否被回收,如果没有被回收,还能再用一次
每个容器都持有自己的itreator,调用iterator进行特征获取的时候,分两步走:
分为两类: sysio:系统io,由操作系统提供,不同的操作系统sysio接口不同。 stdio:标准io,屏蔽系统接口细节,移植性好。
每个FILE只有一个游标,比如打开之后开始写,游标后移;这时候用读函数,也是从游标往后读,想从头读需要移动游标。
打开模式:r和r+读的对象必须存在,其他的模式不存在则会创建。
fwrite\fread返回读写成功的字节数 如果只剩5个字节,fwrite(buf,)
全缓冲:满了刷新,如文件 行缓冲:遇到换行符刷新,当流涉及到一个终端时,一般是行缓冲 无缓冲:即时刷新,标准错误流是无缓冲,保证立即能够被看到
文件载入逻辑: 物理文件 --> inode(FCB) --> 程序打开文件产生结构体X --> X数组 --> X数组的下标就是文件描述符
系统存在一个指针数组(ulimit查看默认长度为1024),该数组中保存着指向控制文件的一个结构体的指针,文件描述符就是指针数组的下标。其中数组的前三个0、1、2固定分别对应stdin、stdout、stderr。
该指针数组由进程独享,各自进程创建各自的。 如下图所示。
钩子函数指的是触发某些动作的时候,调用一系列注册的函数。 钩子函数分两类:exit类与信号类 exit类: exit与_exit,exit调用后还有调用各种处理逻辑如钩子函数,但是如果是一些非法异常,这会导致钩子函数的调用导致故障扩大,此时应该调用_exit(或者abort),立即终止,什么也不动。 信号类: 通过信号注册函数,实现触发信号的时候,触发对应的函数。比如SIGINT信号(ctrl+C会触发),最好关联到exit信号上,避免程序异常退出没有进行资源回收。
fork、wait、exec简称few,基本构建了Unix世界的多进程。 fork用于创建进程(进程复制),wait用于进程收尸,exec用于进程转换。 fork有两个返回值pid,在子线程返回值为0,在父线程非零。 调用fork之前必须fflush(NULL)刷新所有缓冲区,不然可能会导致后面的流输出异常。(同理,线程切换之前需要先刷新缓冲区)
fork是复制父线程为子线程。
exec是替换父线程为执行目标(一般是fork子进程后用exec将子进程替换成别的进程执行)
如果父进程在子进程结束之前结束,子进程会被init接管。如果子进程结束而父进程长期不结束,所有子进程会变成僵尸进程(僵尸进程虽然占用资源很少,但是他们占用了宝贵的进程号,进程号是有上限的),因此在子进程结束的地方,使用n个wait(NULL)来回收子进程。
脱离父进程,直到系统中所有进程都死亡的时候才消亡。(守护线程则是等到所有线程都结束才结束)。
因此,守护进程有以下特点: ppid为1:因为脱离了父进程,由init接管; pid–pgid–sid:因为守护进程脱离父进程后,自己变成了leader,他单独成为了一个group,单独有一个session; TTY为?:终端,表示没有终端
当我们用ps axj查看到ppid为1,pid–pgid–sid,TTY为?的就是守护进程。 通过调用setid()使子进程成为守护进程(必须是子进程调用,因为要脱离父进程),返回一个sessionid。 通过/var/run/name.pid锁文件实现守护进程单例。创建守护进程的时候会创建该文件,该文件中保存着守护进程的进程号,当重复创建守护进程的时候会检查该文件,若存在则禁止创建。
创建守护进程的步骤
信号分为两类:标准信号和实时信号。 kill -l查看,非SIGRTMIN开头的都是标准信号,其余都是实时信号。 两者的最大区别:对于连续的相同信号,标准信号指处理最后一个信号,而实时信号会让他们排队然后逐一执行。
标准信号:
实时信号: 相同信号排队执行,解决了标准信号只能响应一次且响应顺序未定义的情况
单纯使用mut信号量,会造成忙等,结合cond(条件变量)能够等到通知再抢锁/释放锁,避免忙等。
线程取消:pthread_cancel(pthread_t) cancel点:可能引发阻塞的系统调用都是cancel点,pthread_cancel调用后,只会在遇到cancel点之后才真正取消线程(避免突然结束导致钩子函数未执行导致资源泄露)
join:意思类似wait,调用join的线程会等待他创建的所有子线程执行完毕,执行完毕后对其进行收尸。
yield:极短暂的出让调度权(短暂的sleep,但不会引起调度颠簸)
线程分离:指的是抛弃与该线程的关系(本来是谁创建谁回收,分离之后就不管了)
epoll_wait分为ET(边缘触发)和LT(水平触发,默认)两种模式。
应用场景: 一个数据包是500字节,服务端第一次只读到了400字节,第二次客户端再发过来了100字节,此时想读到剩余的100字节,就必须使用LT模式。
右键打开一个文件查看他的属性,这时候只需要读取一个文件head,剩余的文件内容我们并不关心(全部舍弃),这就是ET。
不开源的手法(隐藏代码细节的手法)
详见参考:https://blog.csdn.net/zhaohong_bo/article/details/89552188 或者参考Unix网络编程
如何选择通信方式?
socket用于不同进程或者跨主机跨网络进程之间的通信。 socket的意义:屏蔽不同协议与不同数据传输类型的组合类型差异,全部抽象为文件来操作。
socket带来的问题:
字节序分为大端存储和小端存储,网络字节序一般为大端。 对于 0xA1A2 大端存储:0xA1 0xA2,符合人的阅读习惯,低位地址存高位字节 小端存储:0xA2 0xA1 低位地址存低位字节 注意字节序指的是字节的顺序,所以顺序对调的最小单位是字节(而不是bit)
解决办法,通过函数实现本地字节序(host)以及网络字节序(net)的转换。 如: ntohs , ntohl htons , htonl 其中ntohs指的是net to host short,其他缩写含义类似。
为了提高寻址效率,对于一个结构体(对象),其大小并非简单是所有对象所占字节数的总和,而是会进行对齐(比如算下来13字节的结构体会对齐为4的倍数,16字节) 对齐也不是简单的按倍数对齐,跟结构体对象声明顺序有关。
比如,若int占用4字节,char占用1字节: { int a; int b; char c; char d; } 上述对象总占用42+12 = 10,对齐为12字节。
如果换一下顺序: { int a; char c; int b; char d; } 会变成4+4+4+2=14字节,对齐为16字节。
对其规则必定满足:结构体的总大小是结构体最大成员体的整数倍,此外,对齐是按照(地址%sizeof(type))是否为0来判断的,具体扩展内容请自行搜索。
在socket中解决对齐问题的思路就是取消对齐。
int、short之类的基本类型的长度实际是未定义的,在不同的机器上会表现出不同的长度,因此最好的办法是指定长度,使用int32_t, uint32_t, int64_t 之类的来显示定义。
__attributs__((packed))表示取消对齐。 name其实是一个占位符,用于构建变长结构体,因为我们不能预估名字的长度,而通常使用char*指针表示字符串,但显然不可能传递一个地址到网络上去。 这样之后, 在发送端,操作msg_st实例的时候,初始化为指针,指针指向的空间大小为malloc(sizeof(msg_st))+strlen(name)。 在接收端,接收的msg_st大小直接定义为malloc(MAX),其中MAX为可能的最大值,比如udp建议的包大小为512,那么MAX=512-udp头部 = 512-8
int socket(int domain, int type, int protocol);
domain:下层协议族 type:上层数据传输类型 protocol:协议(0表示使用协议族中的默认协议)
socket大致分为两类,流式套接字(如tcp)和报式套接字(如udp),由于流式面向连接,即点对点通信,因此如果要做广播、多播/组播,只能用报式套接字。 (广播和多播/组播的区别在于,广播是全网发送,所有人必须接收,多播/组播则是自己拉个群,发消息就群里大家自己看的到,但有个特殊的群224.0.0.1默认所有人都在这里面,如果往这里发消息也是广播。)
题外话:所有数据最好在声明的时候都进行初始化,即便看起来不必要。考虑这种情况:一个指针开辟的大小是16字节,这时候没有初始化,它其实指向的是内存中的一块空闲地址,是有内容的,如果不对其进行初始化(通过memset),如果后面给他赋值的时候只用了12个字节,那么剩余的4个字节依旧是脏数据,尽管我们感知不到,但在网络上传输的时候,可以通过抓包看见。这也算内存泄露。
客户端不bind的话,系统会自动分配一个端口。
相对复杂且重要的一个函数:accept。 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
需要注意的是,server端至少有2个socket文件描述符,其中一个socket文件描述符(由socket函数创建得到的)专门用于接收请求,其他的socket文件描述符(由accept得到的)用于与客户端通信。
client端只有一个socket文件描述符,就是socket函数创建的,用于跟server端通信。
TTL:time to life,并不是一个时间单位,而是指可以跳转的路由个数。 一般linux默认64,windows默认128,所以TTL通常来说是足够用的,不会因为TTL耗尽导致丢包。
丢包一般是因为阻塞导致的。
阻塞往往是因为包太多了,所以要进行流控。
通过确认机制、滑动窗口进行流控。 单独的确认机制会导致大量的时间耗费在等待ack上,通过滑动窗口+累计确认+拥塞控制,能够降低这个等待时间。
在Java中,可以使用maven之类的构建工具,通过import关键字就可以实现第三方包的使用,但是对C/C++来说,需要自己下载编译源码包,形成静态/动态库,然后编译的时候使用。
ubuntu环境 ,以libevent为例
下载 官网https://libevent.org/下载tar.gz,上传到服务器 解压 tar zxvf xxxx.tar.gz 进入目录 cd xxx
安装 源码包安装三步走: 1.检查环境,生成makefile ./configure 2.编译 make 生成 .o 和可执行文件 3.[sudo] make install 将必要资源拷贝到系统指定目录(/usr/local/lib)
通常解压后的目录里面也有readme或者README,可以做参考。
验证安装 通常源码包中都会有样例,比如libevent目录下有个sample目录,可以尝试执行sample中的样例查看是否安装成功。
以编译执行hello-world.c为例
gcc hello-world.c -o hello -levent
注意-levent,表示连接libevent库(去掉lib,加上l)
编译完毕之后,执行./hello
如果报错 cannot open shared object file: No such file or directory ,应该是环境变量没有配置。 临时环境变量配置: 执行语句:export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
当前用户环境配置: vim ~/.bashrc 在末尾添加: export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH 保存退出即可
执行hello阻塞,表示成功。
在Java中,回调的实现一般是通过传递接口参数,然后调用接口的方法实现方法回调。
在C/C++中,由于函数指针的存在,可以将函数作为参数传递,这就实现了比较特别的回调机制。
函数指针的格式:返回值(* 函数指针名称)(函数参数)
例子:
#include <stdio.h>
void callbackA(int a){
printf("callbackA :%d\n",a);
}
void test(void(*callback)(int),int arg){
callback(arg);
}
int main(){
test(callbackA,10);
return 0;
}
待续