C/C++ IO细节

1 C和C++的IO函数

C主要使用scanf/printf及其相关函数( fread, fwrite等 )
C++主要使用std::cin/std::cout流对象

2 IO缓冲区

缓冲区分几个层次:

  • 用户级别缓冲,例如你可以开一个很大的char数组,然后多次调用snprintf,最后输出这个char数组;
  • 库级别缓冲,这是当调用库封装的函数(注意,不是调用操作系统提供的api函数)时,库为了提高性能所设定缓冲区,缓冲区类型有三种(不缓冲,全缓冲(块缓冲),行缓冲),可以通过setvbuf来更改,一个需要注意的地方是库的默认行为。libc一般是这样的:默认情况下就会打开stdin,stdout,stderr,其中stderr是不缓冲的,如果stdin和stdout映射到交互式设备(例如终端),那么是行缓冲的,否则是全缓冲的。[6][7]
    下面举两个例子来说明:
int main() {
    fprintf( stdout, "hello, " );
    fprintf( stderr, "world!\n" );
}

实际输出是:

world!
hello,

这是因为默认情况下stderr不缓冲,而stdout无论是否映射到交互设备,都有缓冲,所以会后输出。

int main() {
    fprintf( stdout, "hello\n" );
    // fflush( stdout );
    write( STDOUT_FILENO, "world\n", 6 );
}

这段代码如果在终端屏显,那么输出

hello
world

如果将结果重定向到一个文件,那么输出

world
hello

这是因为重定向到文件,stdout就是全缓冲了(并不会因为读到’\n’刷新缓冲区),这时”hello\n”还在库缓冲区里,而write是系统调用,直接就把”world\n”写给操作系统了,然后进程退出时,触发刷新库缓冲区,”hello\n”才被提交给操作系统。
上面这个情况需要注意,因为它会因为重定向产生不一样的结果,@aikilis同学就在这个上面吃了大亏。

另外标准库提供了一个fflush函数,它会将库缓存提交给操作系统;如果将上面那个fflush的注释去掉,那么输出结果就一致了。

  • 操作系统级别缓存,磁盘操作是个慢操作,操作系统为了提高效率,可能会提供一块内核的内存作为缓冲区,所以即使调用fflush,也并不意味着数据已经到了磁盘,一般来说操作系统可靠性相当高了,但是如果系统掉电,内存数据就over了,对于某些数据库应用,这个损失也无法忍受,所以系统api会提供刷新缓存操作fsync,FlushFileBuffers。[5]
  • 磁盘cache,它的作用类似于cpu cache,也是提供一个高速缓存,使得效率提高,而这一层缓存,对于我们来说已经无法控制了。

C++的std::endl

它不仅仅是输出一个换行符,还执行了flush操作(也就是将库缓存数据提交给操作系统),这样做的优点是不会因为程序意外退出少打印数据,缺点是无法利用buffer,性能急剧下降。所以对于不那么重要的操作,尤其是OJ的一些输出换行,建议用’\n’而不要用std::endl。

3 性能对比

scanf/printf

a> 从编译器角度,C编译器生成的代码不会比C++编译器差,甚至可能略占优势(之前是这么讲,不知道现在情况如何)
b> 需要解析format字符串,先判断类型,然后才能做相应的处理,这会占用一定的时间
c> mingw版本下的cstdio可能会慢很多,msvcrt的IO不符合ANSI标准,所以mingw自己加入了一段wrapper,并且引入了__USE_MINGW_ANSI_STDIO宏作为开关,默认是关闭的,但是对于C++来讲还需要libstdc++库,在编译libstdc++的时候__USE_MINGW_ANSI_STDIO却是打开的。[1][2]

cin/cout

a> 默认情况下,C++为了在交杂使用cin/cout和scanf/printf不至于出错时,将C和C++输出输入流绑在了一起,导致每次同步都要额外消耗时间,这个可以通过std::ios::sync_with_stdio(false);关闭
b> 默认情况下,C++的cin上会绑定对应的输出流cout,每次对cin进行任何IO操作都会flush cout,可以通过std::cin.tie( 0 );解除这种绑定
c> 因为数据类型编译期间可以确定,所以节省了parse时间,较新版本的编译器已经可以体现出这个优势。

在gcc 4.9.1下测试如下代码(预先生成一个data文件,里面存储1000000个整数):

#ifdef _USE_STDIO
    #include <stdio.h>
#endif

#ifdef _USE_CSTDIO
    #include <cstdio>
#endif

#ifdef _USE_CIN
    #include <stdio.h>
    #include <iostream>
#endif

const int NUM = 1000000;

int main()
{
    freopen( "data", "r", stdin );
    int n;
    for(int i = 0 ; i < NUM ; i++) {

#ifdef _USE_STDIO
        scanf( "%d", &n );
#endif
#ifdef _USE_CSTDIO
        scanf( "%d", &n );
#endif

#ifdef _USE_CIN
#ifdef _NO_SYNC
        std::ios::sync_with_stdio( false );
#endif
#ifdef _NO_TIE
        std::cin.tie( 0 );
#endif
        std::cin >> n;
#endif
    }  
    return 0;
}

使用如下命令行编译和运行

g++ test_cin.cc -D_USE_STDIO -O2 -o test_stdio
g++ test_cin.cc -D_USE_CSTDIO -O2 -o test_cstdio
g++ test_cin.cc -D_USE_CIN -O2 -o test_cin
g++ test_cin.cc -D_USE_CIN -D_NO_SYNC -O2 -o test_cin_nosync
g++ test_cin.cc -D_USE_CIN -D_NO_SYNC -D_NO_TIE -O2 -o test_cin_nosync_notie
time ./test_stdio
time ./test_cstdio
time ./test_cin
time ./test_cin_nosync
time ./test_cin_nosync_notie

测试结果(大于号表示速度快)是:
test_cin_nosync_notie > test_cin_nosync > test_stdio ≈ test_cstdio > test_cin
因为我的测试环境是linux,没有使用mingw,所以stdio.h和cstdio没区别。

更快速度

当然速度更快的还是自己申请比较大的缓冲区,调用fread(间接调用系统api read)直接读一大块数据,然后自己手动解析,这样做可以减少系统调用次数,从而缩短时间,缺点是额外使用空间。
如果确定不会有并发问题,可以调用fread_unlocked,从而去掉同步过程,速度更快,这只有在fread被频繁调用时才能展现优势。

4 格式符format相关

a> 在%后面的接*号
对于scanf来说,是按照相应格式读入,然后忽略;对于printf来说,会先把一个参数替换到*的位置,然后该处格式对应下一个参数。

scanf( "%*d%d", &a ); // 输入1 2,a == 2,因为1被忽略了
printf( "%0*.*f\n", 10, 4, 1.2 );  // 输出 00001.2000

b> 在%或*后面接m$,其中m是一个正整数
这表示使用第几个参数,参数列表索引从1开始,相当于用%m$对应参数代替%,用*m$对应的参数代替*

printf( "%2$*1$d", a, b );  // 等价于printf( "%*d", a, b ); a是第一个参数,b是第二个参数,%2$表示用第二个参数作为%,*1$表示用第一个参数做宽度控制

c> %n的使用
对于printf来说,可以将输出字符个数统计到参数中

printf( "%nhello%n\n", &a, &b );  // a == 0 b == 5

d> %[的使用
对于scanf来说,可以使用部分正则字符串功能,可以用’[’和’]’中间写一个字符串集合,遇到任何一个字符都匹配,用’^’表示不包括

scanf( "%*[ \n]%c", &c );  // 略过输入开头的空格和换行符,将第一个字符读入c中

5 安全性

因为sprintf没有对缓冲区大小做检查,很容易引起缓冲区溢出攻击,所以建议替换成snprintf
printf也因为使用格式符,一旦其可能引入用户输入,就会产生问题,例如你如果从某个地方得到用户输入字符串str,直接将其作为format串输出,printf( str );就会造成安全隐患,应该用printf( “%s”, str );这样的方法调用,更详细的分析可见[4]

参考
[1] http://www.zhihu.com/question/21016898
[2] http://stackoverflow.com/questions/17236352/mingw-w64-slow-sprintf-in-cstdio
[3] http://www.hankcs.com/program/cpp/cin-tie-with-sync_with_stdio-acceleration-input-and-output.html
[4] http://drops.wooyun.org/binary/6259
[5] http://blog.youkuaiyun.com/tianwailaibin/article/details/6709490
[6] http://www.douban.com/note/162435828/
[7] man stdout

### EtherNet/IP 通信中实现 IO 读写的方法(C/C++) EtherNet/IP 是一种基于以太网和 TCP/IP 技术的工业通信协议,它使用 CIP(Common Industrial Protocol)作为应用层协议,用于在工业设备之间进行高效的数据交换。在 C/C++ 中实现 EtherNet/IP 通信的 IO 读写操作,通常需要使用开源库或 SDK 来处理底层协议细节,从而专注于应用层的数据交互。 #### 1. 使用开源库实现 EtherNet/IP IO 通信 目前常用的 EtherNet/IP 协议栈实现包括 `libenip`、`libplctools` 和 `CIP SDK` 等。这些库提供了 EtherNet/IP 的客户端和服务端功能,支持显式消息通信和隐式 IO 通信。 - **显式消息通信**:用于设备配置、状态查询等非实时性要求的场景,通过 TCP 协议完成。 - **隐式 IO 通信**:用于实时控制场景,通常基于 UDP 协议实现,具有更低的延迟和更高的数据吞吐能力。 #### 2. 显式 IO 读写实现(基于 TCP) 显式通信通常用于读写标签(Tag)或设备属性,适用于非实时控制场景。以下是一个基于 `libenip` 的简单 IO 读写示例: ```cpp #include <enip_cip.h> #include <stdio.h> int main() { ENIP_CIP *enip = enip_cip_new(); if (!enip) { printf("无法创建 EtherNet/IP 实例\n"); return -1; } // 连接到目标设备 if (!enip_cip_connect(enip, "192.168.1.10", 44818)) { printf("连接失败\n"); return -1; } // 注册会话 if (!enip_cip_register_session(enip)) { printf("会话注册失败\n"); return -1; } // 读取 IO 标签 uint8_t *data; size_t data_len; if (!enip_cip_read_tag(enip, "MyTag", &data, &data_len)) { printf("读取失败\n"); return -1; } printf("读取到数据长度: %zu\n", data_len); // 输出数据 for (size_t i = 0; i < data_len; i++) { printf("%02X ", data[i]); } printf("\n"); // 写入 IO 标签 uint8_t write_data[] = {0x01, 0x02, 0x03, 0x04}; if (!enip_cip_write_tag(enip, "MyTag", write_data, sizeof(write_data))) { printf("写入失败\n"); return -1; } // 注销会话并断开连接 enip_cip_unregister_session(enip); enip_cip_disconnect(enip); enip_cip_delete(enip); return 0; } ``` #### 3. 隐式 IO 通信实现(基于 UDP) 隐式通信常用于实时控制场景,例如机器人控制、传感器数据采集等。其特点是低延迟、高频率的数据交换,通常使用 UDP 协议进行通信。以下是一个基于 `libenip` 的隐式 IO 接收示例: ```cpp #include <enip_cip.h> #include <stdio.h> #include <stdlib.h> void io_data_callback(uint8_t *data, size_t len, void *user_data) { printf("收到隐式 IO 数据(长度:%zu):", len); for (size_t i = 0; i < len; i++) { printf("%02X ", data[i]); } printf("\n"); } int main() { ENIP_CIP *enip = enip_cip_new(); if (!enip) { printf("无法创建 EtherNet/IP 实例\n"); return -1; } // 设置隐式 IO 接收回调 enip_cip_set_io_callback(enip, io_data_callback, NULL); // 启动隐式 IO 监听 if (!enip_cip_start_io(enip, "192.168.1.10", 0xAF12)) { printf("启动隐式 IO 失败\n"); return -1; } printf("隐式 IO 监听已启动,等待数据...\n"); // 模拟运行一段时间 sleep(10); // 停止监听 enip_cip_stop_io(enip); enip_cip_delete(enip); return 0; } ``` #### 4. 协议结构与数据封装 EtherNet/IP 的通信数据结构通常包括以下几个部分: - **EtherNet/IP 头部**:包含命令、会话句柄、状态等信息。 - **CIP 消息头部**:描述 CIP 消息类型、目标类、实例、属性等。 - **数据载荷**:实际读写的数据内容。 开发者需要根据目标设备的 CIP 对象模型构造正确的请求数据包。例如,读取一个 CIP 类的属性时,需要构造 `CIP_READ_REQ` 消息,并解析响应中的 `CIP_READ_RSP` 数据。 #### 5. 多线程与异步处理 为了提高实时性和响应能力,EtherNet/IP 通信模块通常需要运行在独立线程中。在 C/C++ 中,可以使用 POSIX 线程(`pthread`)或 C++11 的 `std::thread` 实现异步通信。 ```cpp #include <thread> #include <iostream> void io_thread_func() { ENIP_CIP *enip = enip_cip_new(); // ... 配置与通信代码 ... enip_cip_delete(enip); } int main() { std::thread io_thread(io_thread_func); io_thread.join(); return 0; } ``` ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值