大-杂-烩

本文探讨了网络编程中的关键问题,如TCP send函数的0字节发送含义,粘包现象的处理,以及如何避免死锁。还涵盖了多线程编程、内存管理、智能指针和死锁解决方案,以及网络协议设计和数据结构在这些问题中的应用。
摘要由CSDN通过智能技术生成

计算机网络

  1. 在利用网络socket进行通信时,选择tcp协议进行交互,利用send函数进行数据发送,我们都知道send函数的返回结果为0时代表对端关闭了连接,那么如果发送0字节数据时是不是也会返回0?这样会不会有歧义呢?
    答:调用send函数发送0字节数据时,send(,0,) 第三个参数代表发送的字节数,本端的操作系统的协议栈并不会把数据发送出去(这里可以通过tcpdump截包佐证只有tcp建链的包,后续没有任何数据包),但是返回的结果依然是0,所以这里确实是会有一点歧义。对于接收端而言,并不会有任何响应,另外如果是阻塞式的socket,那么接收端的缓冲区并没有数据收到,所以程序会阻塞在recv处(可以通过gdb运行程序查看)。另外发送0字节的数据是没有任何意义的,所以尽量避免。

    //TODO
    包乱序 丢包重传怎么解决?
    粘包是什么,怎么解决?(固定包长度,固定结束符,包头包体结构)
    

2.陈硕关于网络编程时提出的几个问题

程序在本机测试正常,放到网络运行上就经常出现数据收不全的情况?
TCP 协议真的有所谓的“粘包问题”吗?该如何设计打包拆包的协议?又该如何编码实现才不会掉到陷阱里?
带外数据(OOB)、信号驱动IO这些高级特性到底有没有用?
网络协议格式该怎么设计?发送 C struct 会有对齐方面的问题吗?对方不用 C/C++ 怎么通信? 将来服务端软件升级,需要在协议中增加一个字段,现有的客户端就必须强制升级?
要处理几千上万的并发连接,似乎书上讲的传统 fork() 模型应付不过来,该用哪种并发模型呢? 试试 select、poll、epoll 这种 IO 复用模型吧,又感觉非阻塞IO陷阱重重,怎么程序的 CPU 使用率一直是100%?
要不改用现成的 libevent 网络库吧,怎么查询一下数据库就把其他连接上的请求给耽误了?
再用个线程池吧。万一发回响应的时候对方已经断开连接了怎么办?会不会串话?

读过《UNIX 环境高级编程》,想用多线程来发挥多核 CPU 的效率, 但对程序该用哪种多线程模型感到一头雾水? 有没有值得推荐的适用面广的多线程 IO 模型? 互斥器、条件变量、读写锁、信号量这些底层同步原语哪些该用哪些不该用? 有没有更高级的同步设施能简化开发? 《UNIX 网络编程(第二卷)》介绍的那些琳琅满目的IPC机制到底用哪个才能兼顾开发效率与可伸缩性?
网络编程和多线程编程的基础打得差不多,开始实际做项目了,更多问题扑面而来:
网上听人说服务端开发要做到 7x24 运行,为了防止内存碎片连动态内存分配都不能用, 那岂不是连 C++ STL 也一并禁用了?硬件的可靠性高到值得去这么做吗?
传闻服务端开发主要通过日志来查错,那么日志里该写些什么?日志是写给谁看的?怎样写日志才不会影响性能?
分布式系统跟单机多进程到底有什么本质区别?心跳协议为什么是必须的,该如何实现?
C++ 的大型工程该如何管理?库的接口如何设计才能保证升级的时候不破坏二进制兼容性?
这本《Linux 多线程服务端编程》中,作者凭借多年的工程实践经验试图解答以上疑问。当然,内容还远不止这些……

—摘自陈硕博客

操作系统

数据结构

GDB调试

1.编译时增加编译选项 -fstack -protector 可以在检测到缓冲区溢出时,立刻终止正在执行的程序,并提示其检测到缓冲区存在的溢出问题。

C++语法

1.多继承时,子类的构造函数应该怎么写?
如果父类里面有显示的声明构造函数,一般来说子类不需要再显示的声明构造函数,会生成一个默认构造函数。当多继承时可能会存在多义性,其实也很好理解,比如C继承自类A和类B,而类A和类B都有相同的构造函数(参数列表一样),那么这个时候如果不在类C里面显示的声明一个新的构造函数,则编译器会提示“C::C”不明确,即系统不知道去默认调用A还是B的构造函数。
2.类中的静态成员到底是什么属性?
常常会见到,类里面有一些静态成员的声明,其实static作用在类里面,说明这个成员是属于类的,使用时,需要加域名空间才能使用。与不在类中的static类似,比如在.cpp中定义一个static的成员,不管是静态变量,还是静态函数,都只能在本cpp里面可见。有一点区别是,类中的静态成员,在使用之前需要使用 如下方法初始化

class A
{
	static int status;
}
A::status = 1; //可以与类的定义在不同的文件

3.派生类的构造函数与析构函数的调用顺序

class A
{
//构造和析构在此省略
}
class B
{
//构造和析构在此省略
}
class C:public A,public B
{
//子类的构造和析构
}

利用断点调式逐步执行就能发现执行顺序是
A的构造函数执行
B的构造函数执行
C的构造函数执行
C的析构函数执行
B的析构函数执行
A的析构函数执行

规律是,无论继承是什么顺序,衍生类的构造函数都是最后执行,A和B的构造函数是谁继承在前谁先执行
4.多态(类型转换)
基类的指针可以指向派生类的对象,编译器会隐式的执行这种派生类到基类的转换。之所以能转换是因为每一个派生类都包含一个基类的全部。
5.虚继承
考虑一种情况,基类A被A1和A2同时继承,B继承了A1和A2,此时A就会同时被继承两次(这样说可能不妥),所以在访问变量时,可能会存在一些二意性,而且两个基类也比较占空间。这时候就需要虚基类的出现了。虚基类无论被何种方式继承,都只会被初始化一次,且基类的初始化需要孙子类或者更下级的类来完成初始化。

6.类型转换构造函数
简单点来说,类型转换构造函数就是带有一个参数的构造函数,具体实现例子如下

public:
	Mystring(string x ) :m_string(x)
	{
		cout << "Mystring 构造函数调用了" << endl;
	}
	std::string m_string;
};

int main()
{		
	string z = "zzz";
	Mystring t = z; //隐式类型构造函数就是一个参数的构造函数
	return 0;
}

如果构造函数前面加上explicit(明确的)可以阻止系统进行改类型转换。

7.成员/虚/静态函数指针 成员/静态变量指针

class MyT
{
public:
	void Test1()
	{
		cout << "普通成员函数调用" << endl;
	}
	static void Test2()
	{
		cout << "静态成员函数调用" << endl;
	}
	virtual void Test3()
	{
		cout << "虚成员函数调用" << endl;
	}

public:
	int m_a; //普通成员变量
	static int m_b;
};

int MyT::m_b = 1;

int main()
{		

	MyT t;
	MyT * tp = &t;
	//1.普通成员函数
	//普通成员函数是跟着类走的,不是跟着对象走,只要有类,就能取到类成员函数的指针
	void (MyT::*myp)(void);
	myp = &MyT::Test1; 
	(t.*myp)(); //前面必须拿圆括号括上,因为后面的括号优先级高!
	(tp->*myp)();

	//2.虚函数 与成员函数一致 必须绑定到类对象上
	void (MyT::*myp2) (void);
	myp2 = &MyT::Test3; 
	(t.*myp2)();

	//3.静态成员函数
	//不需要绑定到类对象上,可直接使用
	void (*myp3) (void);
	myp3 = &MyT::Test2;
	(*myp3)();

	//4.成员变量指针 不是某个地址 而是该成员变量与该类对象之间的偏移量
	int MyT::*myp4 = &MyT::m_a;
	//当生成类对象时,如果这个类中有虚函数表,就会有一个指向这个虚函数表的指针,这个指针占四个字节
	//所以这里看到的m_a的偏移是4,如果把 virtual 函数注释后调试模式下查看myp4的值就会是0而不是0x4
	t.*myp4 = 111; // 等价于 t.m_a = 111;
	std::cout << t.m_a << endl;

	//静态成员变量
	int *myp5 = &MyT::m_b;
	*myp5 = 111;

	return 0;
}

8.字节对齐问题
定义一个结构体如下

typedef struct Student {
	char A; 
 	char  B; 
	int64_t D;
	char C; 
};

按照常规的理解,其sizeof的结果应该是11个字节,但是我在64位机器上sizeof出来是24?为什么呢?看一下内存分布
结构体对象内存分布
0x0113FBFC是对象的初始地址,可以看到A和B分别占用了一个字,D占用了八个字节,后面的C也占用了8个字节,最后末尾还有7个字节。首先看看百度百科对于字节对齐规则的描述
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(?);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。(最后7个字节的来源)

在这个结构体的定义前后加上以下定义,就会发现sizeof大小为11,说明取消了默认的字节对齐。这里这个语句的意思就是,接下来的结构体定义,都按照n字节对齐,除去所有的padding字节。使内存更加紧凑,节约内存。

#pragma pack(push,1)
typedef struct Student {
	char A; 
 	char  B; 
	int64_t D;
	char C; 
};
#pragma pack(pop)

9.malloc库函数可能出现的问题以及改进方法?
malloc是线程不安全的,有可能出现分配异常的问题,自定义一个函数

int malloclen= 0;
void *ts_malloc( size_t blksize )
{
   void *blkaddr;
    static pthread_mutex_t
        malloc_lock = PTHREAD_MUTEX_INITIALIZER;

    pthread_cleanup_push( (void(*)(void *))pthread_mutex_unlock,
                          (void *)&malloc_lock );
    pthread_mutex_lock( &malloc_lock );
      malloclen += blksize;
    blkaddr = malloc( blksize );

    pthread_cleanup_pop( 1 );

    return( blkaddr );
}

10.new 和 delete的使用
new和delete成对使用
且 new [] 与delete[]应该对应使用
如果是内部数据类型 如 int char操作时不会内存泄漏,但是是自定义的类如果在new时用new[] 但是delete时用delete,则会出现内存泄漏。多出的内存是用来存储在初始化时分配了几个数组元素,一般多出的内存是4个字节。
自定义的类如果没有自定义的析构函数,那么用delete也不会有内存泄漏。
为什么像 int 这种内部数据类型不用额外的4字节?

11.智能指针自定义删除器
//shared_ptr

class Socket
{
public:
    Socket(){}
    ~Socket(){}
    //关闭资源句柄
    void close(){
    //dosomething,such as close socket fd;
    }
};

自定义删除器方式1

std::shared_ptr<Socket> ptsocket(new Socket(),[](Socket * socket){socket->close();delete socket;})

自定义删除其方式2

std::unique_ptr<Socket, void(*)(Socket * socket)> ptsocket(new Socket(), [](Socket * socket) { //to do });

自定义删除器方式3

    auto deletor = [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        //TODO: 你甚至可以在这里打印一行日志...
        delete pSocket;
    };
    //std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);
    std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);

后面的delete函数用Lambda表达式实现了
shared_ptr和unique_ptr的删除器实现方式不一样,在传递模板参数时,unique_ptr还需要传递一个删除器的类型,因此类实现也就不一样,会导致不同删除器类型的unique_ptr不能放置于同一个容器中。而shared_prt则不一样,只要指向的对象一样,即使删除器不一样,也能放置于同一个容器中。
详解:王建伟C++ 5-7 00:30:32
12.对象的生命周期
以前一直觉得,函数执行结束了,对象的生命周期才会结束,但是最近因为看一个bug才尼玛知道,if while do while () 这些东西里面声明的对象在出其作用域的时候都会释放掉。

#include <iostream>
#include <stdio.h>
using namespace std;
int main()
{
	if (1 == 1)
	{
		int* p1;
	}
	*p1 = 11;
	while (nullptr == nullptr)
	{
		auto p2 = (int)13;
		int p3 = 14;
		break;
	}
	cout << "p2 = " << p2 << endl;
	cout << "p4 = " << p4 << endl;
	for (int p5 = 0; p5 < 10; p5++)
	{
	}
	p5 = 0;
	return 0;
}

以上程序在作用外均无法使用对象,对象与函数出栈时一样,都已经被释放掉了…
13.指针的长度
普通的裸指针是 四字节
shared_ptr 与 weak_ptr 是八个字节,多出来的四个字节是用来存放指向控制块的指针(包括 强引用计数 弱引用计数 自定义删除器指针),实际上智能指针也就是存放了两个裸指针。在这里插入图片描述
另外 unique_ptr 的尺寸与前两个都不一样
unique_ptr p1(new string(“hello world!”));
sizeof(p1) // 输出结果是4 。。
为什么unique_ptr是四个字节呢?因为没有自定义删除器,如果增加了自己的删除器,则 unique_ptr 的尺寸可能增加,比如传递函数指针类型时
14.裸指针初始化只能指针陷阱

	int *p = new int(11);
	shared_ptr<int> p1(p);
	shared_ptr<int> p2(p);

利用裸指针同时初始化两个智能指针,会发现p1和p2的强引用都是1,释放时会释放两次。
15.循环引用导致的智能指针无法释放

class CB;
class CA;
class CB
{
public:
	std::shared_ptr<CA> pca;
};
class CA
{
public:
	std::shared_ptr<CB> pcb;
};
std::shared_ptr<CA> pca(new CA);
std::shared_ptr<CB> pcb(new CB);
pca->pcb = pcb;
pcb->pca = pca;

new了两个对象,等于申请了两块内存,但是pca的成员pcb是利用pcb创建的,会导致pca的强引用加1,同理 pcb的成员pca是利用pca创建的,会导致pcb的强引用也加1,所以在pca和pcb退出作用域时,不会释放其指向的内存。
15.智能指针设计的目的以及auto_ptr被放弃的原因
防止内存泄漏,休业式RAII的一种提现。
auto_ptr使用缺陷是

auto_ptr<string> p1 = new string("zzz");
auto_ptr<string> p2 = p1;

这里定义会导致p1自动被释放,造成不可预计的问题
虽然unique_ptr也是独占式的指针,但是unique_ptr会在编译时报错,编译器不允许存在这种操作,避免后续使用p1导致的程序崩溃问题。
16.死锁的出现以及规避的方法
同一个线程存在对两个及两个以上的锁同时操作时,有可能出现死锁的情况。比较简单的方案就是保证调用的顺序来保证不出现死锁的情况,另外,std下提供了lock方法,如

std::lock(mutex1,mutex2);
std::unlock(mutex1);
std::unlock(mutex2);

这个方法会一直尝试获取1锁与2锁,如果获取不到又会释放等待下一次获取。
上述方法有一点不好的就是没有用到lock_guard,如下可以解决该问题。

std::lock(mutex1,mutex2);
std::lock_guard<std::mutex> guard1(mutex1,std::adopt_lock);
std::lock_guard<std::mutex> guard2(mutex2,std::adopt_lock);

使用adopt_lock关键字会使得guard对象在生成时不直接调用加锁,而且后面也不用手动释放锁,脱离作用域自动释放。

17.为什么字符串在网络传输不需要大小端转换,而像int/long…等却需要转换?
首先
大端模式,是指数据的高字节保存在内存的低地址中
小端模式,是指数据的高字节保存在内存的高地址中
大端模式与人类的识别方式是相似的,从左往右,从高到底开始写

大小端是指在一个字节内,数据存储的不同顺序,以32位小端为例
char a = 65; (字符A)a 在内存中的存储形式是
0x1000 0010 0000 0000
如果是大端模式,那么在内存中的存储形式是
0x0000 0000 0100 0001
//未验证 待验证

18.dynamic_cast 和 static_cast 的区别

static_cast属于静态类型的转换,会在编译的时候进行类型检查。
可以用于相关类型的转换,比如int和double之间的转换,子类转成父类,void*和其他类型之间的转换,一般不能用于指针类型之间的互转。从我的角度来看,static_cast就比C的强制类型转换的作用稍微弱一点,加了一些类型之间转换的限制。

dynamic_cast主要是具有运行时检查功能,转换也是在运行时转换的,比如有三个类 基类是 human man 和women类分别继承自 human
有如下转换,而且human必须具有虚函数,否则dynamic_cast无法正常使用,只有虚函数的存在,才使用指针或引用所指向对象的动态类型。基类有虚函数之后,会有一个指针指向基类的虚函数表,

human*  A = new man;
man*    B = dynamic_cast<man> A;
women*  C = dynamic_cast<women> A;

以上代码在转换时,将A转换成 women* 时,因为dynamic_cast 属于具有运行时检查功能,他发现A 指针实际上不是指向的一个women类时,转换就会失败,得到的C是一个空指针。

另外还有两种强制类型转换
const_cast 和 reinterpret_cast

const_cast 用于去除指针或者引用的常量属性,属于编译时类型转换。
常见错误使用

	const int a = 10;
	const int* p1 = &a;
	int* p2 = const_cast<int*>(p1);
	*p2 = 100;
	std::cout << a << std::endl;;

reinterpret_cast 编译时进行的类型转换
重新解释,把操作数的类型解释为另外一种类型,处理无关类型的转换。
比如下面这个转换,除了C里面的强制类型转换能做到,其他的dynamic_cast static_cast const_cast均无法实现转换,编译无法通过。

	char* p = (char *)calloc(1, 1024);
	Message* pmessage = reinterpret_cast<Message*> (p);

19.引用和指针有何区别?怎么拿到引用变量的地址?
引用和指针本无区别,只是编译器为了方便设置的语法糖,引用本身也是存储了指向对象的指针变量,和 利用 & 取到的变量地址没有任何差别,只是看起来不一样。
至于怎么拿地址?似乎引用变量没有占内存啊?
参考文
引用比指针安全么?也不是,一样也存在安全问题,仅仅就是一个语法糖而已

 int *var = new int(42); 
 int &ref = *var; 
 delete var; 
 ref = 42; 
 return 0;

为了进一步验证引用与指针在本质上的相同,我们看当引用作为函数参数传递时,编译器的行为:

 1 void Swap(int& v1, int& v2); 
 2 void Swap(int* v1, int* v2);
 3 
 4     int var1 = 1; 
 5 00A64AF8  mov         dword ptr [var1],1  
 6     int var2 = 2; 
 7 00A64AFF  mov         dword ptr [var2],2  
 8     Swap(var1,var2); 
 9 00A64B06  lea         eax,[var2]  
10 00A64B09  push        eax  
11 00A64B0A  lea         ecx,[var1]  
12 00A64B0D  push        ecx  
13 00A64B0E  call        Swap (0A6141Fh)  
14 00A64B13  add         esp,8  
15     Swap(&var1, &var2); 
16 00A64B16  lea         eax,[var2]  
17 00A64B19  push        eax  
18 00A64B1A  lea         ecx,[var1]  
19 00A64B1D  push        ecx  
20 00A64B1E  call        Swap (0A61424h)  
21 00A64B23  add         esp,8 

上面代码再次证明了,引用与指针的行为完全一致,只是编译器在编译时对引用作了更严格的限制。
不要用汇编结果来替代概念,引用不占空间意思就是不占对象空间,不表示不占指针的少量空间。实际上指针是汇编工具实现引用的一种方式而已,而有的优化结果可能没有代表自己的指针。
总而言之,引用就是引用,是这种概念,它为方便程序员使用,和方便汇编工具优化而产生。汇编怎么实现和优化是汇编的事,至于出了什么违反该概念的结果,是汇编的错,而不是定义的错,不要本末倒置。
你可以通过汇编来了解编译器怎样实现引用
引用 却不应该用汇编来解释 它只是一个概念
赞同,引用只是编译器之上,给出来的一个抽象定义。接口的实现,由编译器来决定!
仔细想想,确实如此,引用只是一个概念,为我们提供了一个接口。怎么实现,由编译器自己决定。

20.RTTI是什么东西?
run time type identification :运行时类型检查
利用dynamic_cast将基类的指针或引用安全的转换成子类的指针或者引用,并用来调用子类的某个重写的虚函数。

21.获取开机到现在的时间函数

extern int clock_gettime (clockid_t __clock_id, struct timespec *__tp) __THROW;

22.git相关
撤销已add的单个文件修改到modified

git reset 单个文件名

撤销已commit的某个文件的修改
首先查询这个文件的log:

git log <fileName>

其次查找到这个文件的上次commit id xxx,并对其进行reset操作:

git reset <commit-id> <fileName>

再撤销对此文件的修改:

git checkout <fileName>

最后amend一下,再push上去:

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值