bytebuf池_图文分析ByteBuf是什么

ByteBuf是什么

ByteBuf是Netty中非常重要的一个组件,他就像物流公司的运输工具:卡车,火车,甚至是飞机。而物流公司靠什么盈利,就是靠运输货物,可想而知ByteBuf在Netty中是多么的重要。没有了ByteBuf,Netty就失去了灵魂,其他所有的都将变得毫无意义。

ByteBuf是由Byte和Buffer两个词组合成的一个词,但是因为JDK中已经有了一个ByteBuffer,并且使用非常复杂,API及其不友好,可谓是千夫所指。为了扭转ByteBuffer在大家心目中的形象,Netty重新设计了一个ByteBuffer,即 ByteBuf。

从字面上我们可以知道 ByteBuf 是处理字节的,并且还有一种缓冲的能力。

ByteBuf在官方中是这样定义的:

A random and sequential accessible sequence of zero or more bytes (octets).

This interface provides an abstract view for one or more primitive byte

arrays ({@code byte[]}) and {@linkplain ByteBuffer NIO buffers}.

就是说 ByteBuf 是一个字节序列,可以随机或连续存取零到多个字节。他提供了一个统一的抽象,通过 ByteBuf 可以操作基础的字节数组和ByteBuffer缓冲区。

需要注意的是这里说的 interface 是不准确的,因为Trustin Lee在2013/7/8将ByteBuffer从接口改成了抽象类,具体的原因不得而知。

ByteBuf的结构

ByteBuf比JDK中原生的ByteBuffer好的原因是前者的设计比后者优秀,ByteBuf有读和写两个指针,而ByteBuffer只有一个指针,需要通过flip()方法在读和写之间进行模式切换,需要操作的越多往往犯错的概率就越大。ByteBuf将读和写进行了分离,使用者不用再关心现在是读还是写的模式,可以把更多的精力用在具体的业务上。

官方定义中指出,ByteBuf主要是通过两个指针进行数据的读和写,分别是 readerIndex 和 writerIndex ,并且整个ByteBuf被这两个指针最多分成三个部分,分别是可丢弃部分,可读部分和可写部分,可以用一张图直观的描述ByteBuf的结构,如下图所示:

可能有人注意到了我说ByteBuf最多被分成三个部分,那是因为某些情况下可能只有一到两部分:

刚初始化的时候

刚初始化的时候,读写指针都是0,所有的内容都是可写部分,此时还没有可读部分和可丢弃部分。

刚写完数据后

刚写完一些数据后,读指针仍然是0,写指针向后移动了n,这里的n就是写入的字节数。

读完一部分数据并丢弃之后

写入完数据之后,紧接着读取一部分数据,然后立刻丢弃掉,此时ByteBuf的结构就会变成跟第二步中的一样。因为丢弃的动作会将读指针向左移动到0的位置,写指针向左移动的距离=原来读指针的值

ByteBuf的读写操作

写操作

ByteBuf中定义了两类方法可以往ByteBuf中写入内容:writeXX() 和 setXX()。

具体的setXX()类的方法可以用下面的一张表格来描述:

方法名

描述

setByte(int index, int value)

将指定位置上的内容修改为指定的byte的值

高24位上的内容将被丢弃

setBoolean(int index, boolean value)

将指定位置上的内容修改为指定的boolean的值

setBytes(int index,byte src)

将指定的字节内容

可以从byte[],ByteBuf,ByteBuffer,InputStream,Channel等中获取

转移到指定的位置

setChar*(int index, int value)

将指定位置上的内容修改为指定的character的UTF-16编码下2-byte的值

高16位上的内容将被丢弃

setShort*(int index, int value)

将指定位置上的内容修改为指定的integer的低16-bit的值

高16位上的内容将被丢弃

setMidium*(int index, int value)

将指定位置上的内容修改为指定的integer的中间24-bit的值

大多数重要的内容将被丢弃

setInt*(int index, int value)

将指定位置上的内容修改为指定的32-bit的integer的值

setFloat*(int index, float value)

将指定位置上的内容修改为指定的32-bit的float的值

setDouble*(int index, double value)

将指定位置上的内容修改为指定的64-bit的float的值

setLong*(int index, long value)

将指定位置上的内容修改为指定的64-bit的long的值

setZero(int index, int length)

将从指定位置index开始之后的length个长度的值设置为0x00

我们知道java中一个int占4个字节,即32bit,一个short占2个字节,一个int可以拆成2个short,所以就会存在当写入一个short时,参数用int来传值时,高16位的内容会被丢弃。这是因为一个int被拆成了两个short,而写入一个short到指定的位置时,那么另一个short就被丢弃了,且是高16位的这个short。

有的人注意到了上面好多方法后面都有*,这是表示这些方法还有一种兄弟方法,如setInt对应的是setIntLE,这表示以小端字节序的方式写入内容。简单来说一般网络传输采用大端字节序,另外我们人类写字节的顺序也是大端字节序,而计算机处理字节的顺序一般是小端字节序(但是也不绝对,计算机从低电平开始读取字节时效率更高),具体什么是大端字节序,什么是小端字节序不是本篇文章深入研究的范围,大家可以自行查阅有关资料。

PS:需要注意的是如果写入的位置index小于0,或者index加上写入内容的值超过capcity的话,会抛出 IndexOutOfBoundsException,所以就存在两个比较重要的方法:isWritable(),isReadable(),他们将返回当前ByteBuf中是否还有足够的空间可以写和可以读

具体的writeXX()方法与上面的setXX()方法类似,不同的是writeXX()方法会更新写指针,即向ByteBuf中写入具体的内容后,writeIndex会向后移动与写入的内容字节数长度相同的距离。

读操作

跟写操作一样,ByteBuf的读操作也有两种方法,分别是getXX()和readXX()。

读操作包含的具体方法与写操作也是一一对应的,具体的可以把上面的那张表格中的set改为get,并且将第二个value参数移除即可,例如:getShort(int index),getInt(int index)等等。

与getXX()方法相关的另一类方法就是readXX()方法了,与get方法不同的是,read方法会更改读指针的值。

ByteBuf的种类

我们知道ByteBuf在4.x的版本中是一个抽象类,他有很多的抽象子类以及各种实现类。

画了一个简单的ByteBuf的各个实现类之间的关系,其中蓝色的类是被弃用的。

上图只是简单的列举的一些常用的ByteBuf类,如果你想知道ByteBuf所有的实现类,那么可以在IDEA中选

则ByteBuf类之后,然后在菜单 navigate 中点击 Type Hierarchy 或用快捷键:control+H,即可打开ByteBuf的类层次结构图,具体的层级结构如下图所示:

本篇文章只简单的让大家对于ByteBuf的种类有个大概的了解,具体的每一种ByteBuf的作用我将在后续的章节中进行介绍。

ByteBuf的使用

有一点我们需要知道的是,ByteBuf的jar包,是可以单独使用的。比如某个项目中有一个场景,需要处理某个自定义的协议,那么我们在解析协议时,就可以将接收到的将字节内容写入一个ByteBuf,然后从ByteBuf中慢慢的将内容读取出来。下面让我们用一个例子简单的了解下ByteBuf的使用。

ByteBuf的创建

要想使用ByteBuf,首先肯定是要创建一个ByteBuf,更确切的说法就是要申请一块内存,后续可以在这块内存中执行写入数据读取数据等等一系列的操作。

那么如何创建一个ByteBuf呢?Netty中设计了一个专门负责分配ByteBuf的接口:ByteBufAllocator。该接口有一个抽象子类和两个实现类,分别对应了用来分配池化的ByteBuf和非池化的ByteBuf。

具体的层级关系如下图所示:

有了Allocator之后,Netty又为我们提供了两个工具类:Pooled、Unpooled,分类用来分配池化的和未池化的ByteBuf,进一步简化了创建ByteBuf的步骤,只需要调用这两个工具类的静态方法即可。

不同的创建方法

我们以Unpooled类为例,查看Unpooled的源码可以发现,他为我们提供了许多创建ByteBuf的方法,但最终都是以下这几种,只是参数不一样而已:

// 在堆上分配一个ByteBuf,并指定初始容量和最大容量

public static ByteBuf buffer(int initialCapacity, int maxCapacity) {

return ALLOC.heapBuffer(initialCapacity, maxCapacity);

}

// 在堆外分配一个ByteBuf,并指定初始容量和最大容量

public static ByteBuf directBuffer(int initialCapacity, int maxCapacity) {

return ALLOC.directBuffer(initialCapacity, maxCapacity);

}

// 使用包装的方式,将一个byte[]包装成一个ByteBuf后返回

public static ByteBuf wrappedBuffer(byte[] array) {

if (array.length == 0) {

return EMPTY_BUFFER;

}

return new UnpooledHeapByteBuf(ALLOC, array, array.length);

}

// 返回一个组合ByteBuf,并指定组合的个数

public static CompositeByteBuf compositeBuffer(int maxNumComponents){

return new CompositeByteBuf(ALLOC, false, maxNumComponents);

}

其中包装方法除了上述这个方法之外,还有一些其他常用的包装方法,比如参数是一个ByteBuf的包装方法,比如参数是一个原生的ByteBuffer的包装方法,比如指定一个内存地址和大小的包装方法等等。

另外还有一些copy*开头的方法,实际是调用了buffer(int initialCapacity, int maxCapacity)或directBuffer(int initialCapacity, int maxCapacity)方法,然后将具体的内容write进生成的ByteBuf中返回。

以上所有的这些方法都实际通过一个叫ALLOC的静态变量进行了调用,来实现具体的ByteBuf的创建,而这个ALLOC实际是一个ByteBufAllocator:

private static final ByteBufAllocator

ALLOC = UnpooledByteBufAllocator.DEFAULT;

ByteBufAllocator是一个专门负责ByteBuf分配的接口,对应的Unpooled实现类就是UnpooledByteBufAllocator。在UnpooledByteBufAllocator类中可以看到UnpooledByteBufAllocator.DEFAULT变量是一个final类型的静态变量

/**

* Default instance which uses leak-detection for direct buffers.

* 默认的UnpooledByteBufAllocator实例,并且会对堆外内存进行泄漏检测

*/

public static final UnpooledByteBufAllocator

DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());

涉及的设计模式

ByteBuf和ByteBufAllocator之间是一种相辅相成的关系,ByteBufAllocator用来创建一个ByteBuf,而ByteBuf亦可以返回创建他的Allocator。ByteBuf和ByteBufAllocator之间是一种 抽象工厂模式,具体可以用一张图描述如下:

下面我来用一个实际的例子来说明ByteBuf的使用,并通过观察在不同阶段ByteBuf的读写指针的值和ByteBuf的容量变化来更加深入的了解ByteBuf的设计,为了方便,我会用非池化的分配器来创建ByteBuf。

使用示例

我构造了一个demo,来演示在ByteBuf中插入数据、读取数据、清空读写指针、数据清零、扩容等等方法,具体的代码如下:

private static void simpleUse(){

// 1.创建一个非池化的ByteBuf,大小为10个字节

ByteBuf buf = Unpooled.buffer(10);

System.out.println("原始ByteBuf为====================>"+buf.toString());

System.out.println("1.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");

// 2.写入一段内容

byte[] bytes = {1,2,3,4,5};

buf.writeBytes(bytes);

System.out.println("写入的bytes为====================>"+Arrays.toString(bytes));

System.out.println("写入一段内容后ByteBuf为===========>"+buf.toString());

System.out.println("2.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");

// 3.读取一段内容

byte b1 = buf.readByte();

byte b2 = buf.readByte();

System.out.println("读取的bytes为====================>"+Arrays.toString(new byte[]{b1,b2}));

System.out.println("读取一段内容后ByteBuf为===========>"+buf.toString());

System.out.println("3.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");

// 4.将读取的内容丢弃

buf.discardReadBytes();

System.out.println("将读取的内容丢弃后ByteBuf为========>"+buf.toString());

System.out.println("4.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");

// 5.清空读写指针

buf.clear();

System.out.println("将读写指针清空后ByteBuf为==========>"+buf.toString());

System.out.println("5.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");

// 6.再次写入一段内容,比第一段内容少

byte[] bytes2 = {1,2,3};

buf.writeBytes(bytes2);

System.out.println("写入的bytes为====================>"+Arrays.toString(bytes2));

System.out.println("写入一段内容后ByteBuf为===========>"+buf.toString());

System.out.println("6.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");

// 7.将ByteBuf清零

buf.setZero(0,buf.capacity());

System.out.println("将内容清零后ByteBuf为==============>"+buf.toString());

System.out.println("7.ByteBuf中的内容为================>"+Arrays.toString(buf.array())+"\n");

// 8.再次写入一段超过容量的内容

byte[] bytes3 = {1,2,3,4,5,6,7,8,9,10,11};

buf.writeBytes(bytes3);

System.out.println("写入的bytes为====================>"+Arrays.toString(bytes3));

System.out.println("写入一段内容后ByteBuf为===========>"+buf.toString());

System.out.println("8.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");

}

执行结果如下:

原始ByteBuf为====================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)

1.ByteBuf中的内容为===============>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

写入的bytes为====================>[1, 2, 3, 4, 5]

写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)

2.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

读取的bytes为====================>[1, 2]

读取一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 2, widx: 5, cap: 10)

3.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

将读取的内容丢弃后ByteBuf为========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)

4.ByteBuf中的内容为===============>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]

将读写指针清空后ByteBuf为==========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)

5.ByteBuf中的内容为===============>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]

写入的bytes为====================>[1, 2, 3]

写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)

6.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

将内容清零后ByteBuf为==============>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)

7.ByteBuf中的内容为================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

写入的bytes为====================>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 14, cap: 64)

8.ByteBuf中的内容为===============>[0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

执行过程分析

下面让我们来仔细的研究下执行的过程,并分析下为什么会产生这样的执行结果。

1.初始化一个大小为10的ByteBuf

刚初始化的ByteBuf对象,容量为10,读写指针都为0,且每个字节的值都为0,并且这些字节都是“可写”的,我们用红色来表示。

2.写入一段内容

当写入一段内容后(这里写入的是5个字节),写指针向后移动了5个字节,写指针的值变成了5,而读指针没有发生变化还是0,但是读指针和写指针之间的字节现在变成了“可读”的状态了,我们用紫色来表示。

3.读取一段内容

接着我们有读取了2个字节的内容,这时读指针向后移动了2个字节,读指针的值变成了2,写指针不变,此时0和读指针之间的内容变成了“可丢弃”的状态了,我们用粉色来表示。

4.将读取的内容丢弃

紧接着,我们将刚刚读取完的2个字节丢弃掉,这时ByteBuf把读指针与写指针之间的内容(即 3、4、5 三个字节)移动到了0的位置,并且将读指针更新为0,写指针更新为原来写指针的值减去原来读指针的值。但是需要注意的是,第4和第5个字节的位置上,还保留的原本的内容,只是这两个字节由原来的“可读”变成了现在的“可写”。

5.将读写指针清空

然后,我们执行了一个 clear 方法,将读写指针同时都置为0了,此时所有的字节都变成“可写”了,但是需要注意的是,clear方法只是更改的读写指针的值,每个位置上原本的字节内容并没有发生改变。

6.再次写入一段较少的内容

然后再次写入一段内容后,读指针不变,写指针向后移动了具体的字节数,这里是向后移动了三个字节。且写入的这三个字节变成了“可读”状态。

7.将ByteBuf中的内容清零

清零(setZero)和清空(clear)的方法是两个概念完全不同的方法,“清零”是把指定位置上的字节的值设置为0,除此之外不改变任何的值,所以读写指针的值和字节的“可读写”状态与上次保持一致,而“清空”则只是将读写指针都置为0,并且所有字节都变成了“可写”状态。

8.写入一段超过容量的内容

最后我们往ByteBuf中写入超过ByteBuf容量的内容,这里是写入了11个字节,此时ByteBuf原本的容量不足以写入这些内容了,所以ByteBuf发生了扩容。其实只要写入的字节数超过可写字节数,就会发生扩容了。

扩容分析

那么扩容是怎么扩的呢,为什么容量从10扩容到64呢?我们从源码中找答案。

扩容肯定发生在写入字节的时候,让我们找到 writeBytes(byte[] bytes) 方法,具体如下:

@Override

public ByteBuf writeBytes(byte[] src) {

writeBytes(src, 0, src.length);

return this;

}

@Override

public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {

// 该方法检查是否有足够的可写空间,是否需要进行扩容

ensureWritable(length);

setBytes(writerIndex, src, srcIndex, length);

writerIndex += length;

return this;

}

在进入 ensureWritable(length) 方法内部查看,具体的代码如下:

@Override

public ByteBuf ensureWritable(int minWritableBytes) {

if (minWritableBytes < 0) {

throw new IllegalArgumentException(String.format(

"minWritableBytes: %d (expected: >= 0)", minWritableBytes));

}

ensureWritable0(minWritableBytes);

return this;

}

final void ensureWritable0(int minWritableBytes) {

// 检查该ByteBuf对象的引用计数是否为0,保证该对象在写入之前是可访问的

ensureAccessible();

if (minWritableBytes <= writableBytes()) {

return;

}

if (minWritableBytes > maxCapacity - writerIndex) {

throw new IndexOutOfBoundsException(String.format(

"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",

writerIndex, minWritableBytes, maxCapacity, this));

}

// Normalize the current capacity to the power of 2.

// 计算新的容量,即为当前容量扩容至2的幂次方大小

int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);

// Adjust to the new capacity.

// 设置扩容后的容量

capacity(newCapacity);

}

从上面的代码中可以很清楚的看出来,计算新的容量的方法是调用的 ByteBufAllocator 的 calculateNewCapacity() 方法,继续跟进去该方法,这里的 ByteBufAllocator 的实现类是 AbstractByteBufAllocator ,具体的代码如下:

@Override

public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {

if (minNewCapacity < 0) {

throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");

}

if (minNewCapacity > maxCapacity) {

throw new IllegalArgumentException(String.format(

"minNewCapacity: %d (expected: not greater than maxCapacity(%d)",

minNewCapacity, maxCapacity));

}

// 扩容的阈值,4兆字节大小

final int threshold = CALCULATE_THRESHOLD; // 4 MiB page

if (minNewCapacity == threshold) {

return threshold;

}

// If over threshold, do not double but just increase by threshold.

// 如果要扩容后新的容量大于扩容的阈值,那么扩容的方式改为用新的容量加上阈值,

// 否则将新容量改为双倍大小进行扩容

if (minNewCapacity > threshold) {

int newCapacity = minNewCapacity / threshold * threshold;

if (newCapacity > maxCapacity - threshold) {

newCapacity = maxCapacity;

} else {

newCapacity += threshold;

}

return newCapacity;

}

// Not over threshold. Double up to 4 MiB, starting from 64.

// 如果要扩容后新的容量小于4兆字节,则从64字节开始扩容,每次双倍扩容,

// 直到小于指定的新容量位置

int newCapacity = 64;

while (newCapacity < minNewCapacity) {

newCapacity <<= 1;

}

return Math.min(newCapacity, maxCapacity);

}

到这里就很清楚了,每次扩容时,有一个阈值t(4MB),计划扩容的大小为c,扩容后的值为n。

扩容的规则可以用下面的逻辑表示:

如果c

如果c>t,则n=c/t*t+t

得出的结论

ByteBuf有读和写两个指针,用来标记“可读”、“可写”、“可丢弃”的字节

调用write*方法写入数据后,写指针将会向后移动

调用read*方法读取数据后,读指针将会向后移动

写入数据或读取数据时会检查是否有足够多的空间可以写入和是否有数据可以读取

写入数据之前,会进行容量检查,当剩余可写的容量小于需要写入的容量时,需要执行扩容操作

扩容时有一个4MB的阈值,需要扩容的容量小于阈值或大于阈值所对应的扩容逻辑不同

clear等修改读写指针的方法,只会更改读写指针的值,并不会影响ByteBuf中已有的内容

setZero等修改字节值的方法,只会修改对应字节的值,不会影响读写指针的值以及字节的可读写状态

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/504128.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c语言入门经典18个程序

C语言程序设计 QQ群&#xff1a;731871503 功能介绍 从零开始精通C语言教程用于C语言学习交流&#xff0c;分享C语言相关的技术文章&#xff0c;无论是小白还是大白&#xff0c;在这里我们之讲C语言。 1 、 /* 输出 99 口诀。共 9 行 9 列&#xff0c; i 控制行&#xff0c; j …

pytorch 矩阵相乘_深度学习 — — PyTorch入门(三)

点击关注我哦autograd和动态计算图可以说是pytorch中非常核心的部分&#xff0c;我们在之前的文章中提到&#xff1a;autograd其实就是反向求偏导的过程&#xff0c;而在求偏导的过程中&#xff0c;链式求导法则和雅克比矩阵是其实现的数学基础&#xff1b;Tensor构成的动态计算…

python分配 使最大的最小_python3中的heapq模块使用

###heapq-堆排序算法heapq实现了一个适合与Python的列表一起使用的最小堆排序算法。二叉树树中每个节点至多有两个子节点满二叉树树中除了叶子节点&#xff0c;每个节点都有两个子节点什么是完全二叉树在满足满二叉树的性质后&#xff0c;最后一层的叶子节点均需在最左边什么是…

codeblocks如何让输出结果 空格_简单讲讲如何实现两个正整数相加,然后输出这个结果...

首先吧&#xff0c;两个整数123 456&#xff0c;相加得到579&#xff0c;我们就得输出579&#xff0c;这个很容易操作&#xff0c;但是如果是&#xff1a;1212161596156198115645646886148461554 2671232162176217624372497590415915915029125 呢&#xff1f;long ? long lo…

C语言和其他高级语言的最大的区别是什么?

提到C语言&#xff0c;我们知道C语言和其他高级语言的最大的区别就是C语言是要操作内存的&#xff01; 我们需要知道——变量&#xff0c;其实是内存地址的一个抽像名字罢了。在静态编译的程序中&#xff0c;所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字…

python3 web服务器_python3 简单web服务器

补充&#xff1a;tcp长连接和短连接长连接&#xff1a;客户端向服务器发起连接请求&#xff0c;服务器接收到请求回应给客户端&#xff0c;双方完成三次握手&#xff0c;然后客户端发送消息&#xff0c;服务端回应消息&#xff0c;每一次完成读写操作&#xff0c;套接字不不关闭…

零基础学C语言必备书籍,抖音编程达人推荐(进群交流学习互动)

C语言从入门到进阶的书籍推荐。 【基础】 这本谭浩强写的【C语言程序设计】可谓是广大人事的入门书籍。我曾经用的教材就是这本&#xff0c;里面大概涵盖了 C语言 语法的 80% 。一个很适合自学的入门书。 【c prime puls】 是 C语言 最经典的入门书籍&#xff0c;极力推荐。每…

网站如何进行渠道跟踪_网站如何进行搜索引擎优化?

这是一个很一般的平台标题&#xff0c;没有任何吸引力&#xff0c;但是它真的可以被一个很好的基层站长估计的很少&#xff0c;我问一个做了多年基层站长的朋友&#xff0c;我说如何做好搜索引擎优化的SEO&#xff0c;他给我的答案很难&#xff0c;答案太大了&#xff0c;所以我…

axure 图片切换图片的交互_AxureRP8中实现伸缩式的图片展示交互效果

上午的时候&#xff0c;在一个群里看到这样的实现需求。伸伸缩缩&#xff0c;感觉很爽的样子。那么&#xff0c;这样的一个交互效果如何实现呢&#xff1f;详细的教程我就不写了&#xff0c;直接上交互截图和源文件就好了。好多操作步骤是吧&#xff1f;实际上&#xff0c;只需…

什么是编程语言,大神教你为什么要学C语言?

首先来说说编程语言这个概念。 编程&#xff0c;其实就是让计算机听懂自己的话&#xff0c;让计算机帮自己想干的事情。编程语言&#xff0c;就是让你能够和计算机进行交流的一种语言。说白了就是让你的软件按你的命令干活。 打比方说&#xff0c;我们经常在僵尸片里面看到&a…

xshell vim 不能粘贴_linux基础知识:vim(vi)的知识

### vim三种模式命令行模式&#xff1a;在该模式下不能对文本进行- 直接编辑&#xff0c;可以输入一些操作(删除行&#xff0c;复制行&#xff0c;移动光标&#xff0c;粘贴)【打开之后默认进入的模式】编辑模式&#xff1a;在该模式下可以对文件内容进行编辑末行模式&#xff…

新手如何学习C语言/C++,教你一年时间是拿到年薪50万

最近会有一些初中高中大学的同学问&#xff0c;C语言C不知道怎么学习不会写代码怎么办&#xff1f;大致上都是一些类似的问题吧&#xff0c;回想一下自己走过的路&#xff0c;反复的了很久思考然后写了这篇文章&#xff0c;希望可以对一些迷惘新手小白程序员同学一丝帮助&#…

html横线标记_html中横线怎么写代码

html代码中:如何输入一条长长的横线呀? 用input页面标签 并将下Border设为直线 input[type="text"]{border-bottom:solid 1px #ccc;} html代码中:如何输入一条长长的横线? 你可以用div标签去实现: 横线中间有字 【html5与html的区别】 html通常指的是用来写网页的…

pycharm导入模块不智能显示_Pycharm自动导入模块小技巧

原标题&#xff1a;Pycharm自动导入模块小技巧作者 | 刘志军来源 | Python之禅如果能把工具熟练运用&#xff0c;往往能达到事半功倍的效果&#xff0c;Pycharm 是很多Python开发者的首选IDE&#xff0c;提供各种快捷键、重构功能、调试技巧等&#xff0c;Python是动态语言&…

mac adb 找不到设备_win/Mac办公软件下载找不到资源?试试这三个强大的神器

大家好&#xff0c;我们在办公时&#xff0c;office、wps等办公软件是我们必须用到的&#xff0c;还有一些专业软件&#xff1a;CAD、PS、PROE、UG、SolidWorks、keyshot、VRay、3DsMax、Labview、Maya、AE、AI、ANSYS等也是各行各业的专业人员工作时必不可少的&#xff0c;但是…

C语言入门经验:零基础如何学习C语言?

工作中&#xff0c;接触过很多想到学习c语言而又没有一点计算机基础的人。经常有人问”我没有一点基础&#xff0c;该怎么入门c语言啊”。看过很多初学者久久摸不到门路&#xff0c;每到这个时候&#xff0c;脑海中总会浮现一个问题&#xff1a;学习c语言真的就那么难吗? 不难…

手机1像素线粗_关于移动端一像素线的解决方案

为什么会有一像素线这个问题因为移动端布局我们大家都知道根据不同的手机会有不同的dpr 例如 爱疯6plus就是3 爱疯6就是2 当我们定义1px的时候就会在不同手机里面显示不同的粗细长度&#xff0c;dpr为3的时候就是3px&#xff0c;dpr为2的时候就是2px&#xff0c;具体dpr是什么…

20天掌握C语言,C语言零基础到项目实战,玩转C语言

一般来说&#xff0c;和其他编程语言相比&#xff0c;C/C语言学起来会比较累&#xff0c;如果想达到项目开发的状态&#xff0c;更是需要花费很长时间的努力。那么&#xff0c;如何才能更好地把所学知识用到实际工作中去呢&#xff1f;今天小编就带大家来看看学习C/C语言到什么…

32岁了学python来的及吗_现在27岁学python来得及吗?

答案是肯定的&#xff0c;27岁学习Python语言是来得及的。 学习Python语言是当前一个比较好的选择&#xff0c;原因有以下几点&#xff1a; 第一&#xff0c;Python语言有健全的生态。Python语言虽然在最近几年被广泛关注&#xff0c;但是Python语言并不是一个新出现的语言&…

怎样快速画出一个正方体_小学数学非常有效的“画图”解题法,快速解题的“金钥匙”...

小学阶段的数学主要培养的是孩子的逻辑思维能力&#xff0c;是从形象思维逐步过度到抽象思维的过程&#xff0c;如果在小学阶段没有将基础打牢&#xff0c;那么等孩子上初中后面对更复杂的学习内容&#xff0c;就会变得更吃力。在小学数学中&#xff0c;“画图”是帮助孩子建立…