一个的错误的演化

一个的错误的演化


OVERVIEW

  • 一个的错误的演化
      • 一道算法题引出的问题
        • 1.问题引出:
        • 2.问题再引出:
        • 3.未能及时排查问题的原因
        • 4.程序的修改与总结
        • 6.原则问题
      • 项目实践中问题的暴露

频繁遇到的一个问题,多次排查错误最终终于找到问题的根源,现在还原这几个遇到问题的场景:

一道算法题引出的问题

1.问题引出:

  • 某场算法竞赛的一道简单题,部分函数功能:将一个int型的数字num,转化为string型的字符串str,并将str进行输出:代码如下
#include
#include

int main() {
    char *str;
    int num;
    scanf("%d", &num);
    int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
    int i = len - 1;
    while (num) {
        str[i] = (num % 10 + '0');
        num /= 10;
        i--;
    }
    printf("%s", str);
    return 0;
}

在Dev C++5.11中进行C代码的测试,

一个的错误的演化_第1张图片

测试结果如下:

可以看到在终端输入num的数值之后,没有按照预期的将str字符串进行打印,就直接结束了程序的运行。

并且程序在编译和链接时,也没有给出任何的错误信息(这直接导致如果不是很仔细,问题将十分难以发现),问题十分隐蔽:

一个的错误的演化_第2张图片

2.问题再引出:

  • 由于竞赛时间紧迫,没时间去排查问题,错误的怀疑是C语言的语法问题,立刻使用C++语言将功能重新实现了一遍,代码如下:
#include
using namespace std;

int main() {
	string str;
    int num; cin >> num;
    int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
    int i = len - 1;
    while (num) {
        str[i] = (num % 10 + '0');
        num /= 10;
        i--;
    }
    cout << str << endl;
    return 0;
}

在Dev C++5.11中进行C++代码的测试,

一个的错误的演化_第3张图片

测试结果如下:毫无疑问,结果和C语言实现是一致的,在输入num的数值之后,程序没有输出str的值并直接结束了运行,

并且没有给出任何的错误提示信息!

一个的错误的演化_第4张图片

此刻心情开始变得十分复杂,为什么一道这样的简单题写的如此狼狈?

3.未能及时排查问题的原因

未能及时定位问题的原因:

  • 使用cmd终端窗口来测试程序,而终端窗口不会给出任何的错误信息提示!
  • 在出现终端输入数据后无响应,不会第一时间想到利用更高级的gdb调试工具进行错误定位,而是使用printf函数在程序中测试:导致问题的发现进一步延缓。
  • 如果会使用gdb进行调试,就能很快的定位到终端在输入num数值之后没有任何响应的原因,debug调试如图:

一个的错误的演化_第5张图片

非常明显的segmentfault段错误,访问非法的内存空间。发现问题后立即对代码进行修改。

4.程序的修改与总结

  • C程序中的问题的本质:在于定义了char *str但是没有为其分配内存空间,添加上一行calloc操作为其分配内存空间:
#include
#include

int main() {
    char *str;
    str = (char *)calloc(100, sizeof(char *));
    int num;
    scanf("%d", &num);
    int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
    int i = len - 1;
    while (num) {
        str[i] = (num % 10 + '0');
        num /= 10;
        i--;
    }
    printf("%s", str);
    return 0;
}

一个的错误的演化_第6张图片

  • C++程序中的问题也是相同的string str;没有进行初始化,直接使用str[i]去引用string中的某个字符导致访问非法内存访问。可使用new操作符为string开辟一段内存空间。

一个的错误的演化_第7张图片

可以看到虽然已经使用*string,输出仍然只有一个1,这涉及到string类的底层实现,具体参考:https://blog.csdn.net/qq_28082757/article/details/72782973:

此处可对第14行的输出部分做一个简单的处理,使其能够顺利的将字符串内容进行输出,

#include
using namespace std;

int main() {
	string *str = new string[100];
    int num; cin >> num;
    int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
    int i = len - 1;
    while (num) {
        str[i] = (num % 10 + '0');
        num /= 10;
        i--;
    }
    for (int i = 0; i < len; ++i) cout << *(str + i);
    return 0;
}

正常的输出了结果:

一个的错误的演化_第8张图片

  • 问题总结:这其实是一个很基础很低级的错误,即访问了非法的内存空间,在定义一个字符数组串指针时没有为其分配内存空间,或者未初始化string对象,就直接尝试利用string[i]对其进行赋值操作。

6.原则问题

回到算法问题解决的角度,

  • 问题1:将一个int类型的num数字,快速转换为string类型的字符串str,真要有必要这么繁琐吗?

这里给出一种快速将num转为str的解决方案,利用sprintf函数直接进行转换,

函数原型:int sprintf(string, const char *format, ...);

#include
using namespace std;

int main() {
    int num; cin >> num;
    char *str = (char *)calloc(100, sizeof(char *));
    sprintf(str, "%d", num);
    cout << str << endl;
    return 0;
}

一个的错误的演化_第9张图片

很快的就将int类型的num数字转为了字符串str,sprintf函数还有其他很多十分有用的应用,例如将ip地址进行拼接。

  • 问题2:注意到在求num数字长度的过程中使用了这样一段代码int len = 0; int x = num; while (x) {x /= 10; len++;}这样求一个数字的长度太不优雅了。

这里给出另外一种快速求num数字长度的解决方案,利用printf函数的返回值输出数字长度,

#include 
int main(){
    int n;
    scanf("%d", &n);
    printf(" has %d digits!\n",printf("%d",n));//利用prinf函数返回值输出数字的位数
    return 0;
}

一个的错误的演化_第10张图片

项目实践中问题的暴露

在项目中也碰到了同样类似的问题,具体参见:使用epoll实现一个echo服务器

#include "head.h"
#include "common.h"
#include "thread_pool.h"

#define QUEUESIZE 100//任务队列大小
#define INS 4//线程数量
#define MAXEVENTS 5//epoll_event的最大数量
#define MAXCLIENTS 2000//最大客户端数量为2000

int epollfd;
int clients[MAXCLIENTS];//每次出现新的套接字时便存储到该数组中 保证临时文件描述符fd在循环中被修改后 操作的对象也不会改变
pthread_mutex_t mutex[MAXCLIENTS];//对用户临时数据上锁

char *data[MAXCLIENTS];//线程进行具体业务操作时 存储每个客户端产生的临时数据

#define handle_error(msg) \
	do { perror(msg); exit(EXIT_FAILURE); } while (0)

void do_work(int fd) {
	/* 对fd文件进行的具体逻辑操作 */
	int rsize;
	char buff[4096] = {0};
	DBG(BLUE" : data is ready on %d.\n"NONE, fd);
	//1.数据接收recv
	if ((rsize = recv(fd, buff, sizeof(buff), 0)) < 0) {
		/* 如果接受数据出错 则使用epoll_ctl将fd文件描述符删除 */
		epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
		DBG(RED" : %d is close!\n"NONE, fd);
		close(fd);//检测到客户端关闭连接 服务端也关闭连接
		return;
	}
	//2.对数据进行简单的处理 并进行最后的send操作 
	int ind = strlen(data[fd]);
	pthread_mutex_lock(&mutex[fd]);
	for (int i = 0; i < rsize; ++i) {
		if (buff[i] >= 'A' && buff[i] <= 'Z') {
			data[fd][ind++] = buff[i] + 32;//大写转小写
		} else if (buff[i] >= 'a' && buff[i] <= 'z') {
			data[fd][ind++] = buff[i] - 32;//小写转大写
		} else {
			data[fd][ind++] = buff[i];//其他字符不处理
			if (buff[i] == '\n') {
				DBG(GREEN" : \\n recved!\n"NONE);
				send(fd, data[fd], ind, 0);
			}
		}
	}
	pthread_mutex_unlock(&mutex[fd]);
}

void *thread_run(void *arg) {
	pthread_detach(pthread_self());
	struct task_queue *taskQueue = (struct task_queue *)arg;
	while (1) {
		int *fd = task_queue_pop(taskQueue);
		do_work(*fd);
	}
}

int main(int argc, char *argv[]) {
	int opt;
	int port;
	while ((opt = getopt(argc, argv, "p:")) != -1) {
		switch (opt) {
			case 'p':
				port = atoi(optarg);
				break;
			default:
				fprintf(stderr, "Usage : %s -p port!\n", argv[0]);
				exit(1);
		}
	}
	DBG(YELLOW" : Server will listen on port [%d]\n"NONE, port);

	//1.创建监听套接字
	int server_listen;
	if ((server_listen = socket_create(port)) < 0) handle_error("socket_create");
	clients[server_listen] = server_listen;//文件描述符作为数组下标 存储新出现的listen_socket文件描述符
	DBG(YELLOW" : Server_listen starts.\n"NONE);

	//2.初始化任务队列和锁
	for (int i = 0; i < MAXCLIENTS; ++i) data[i] = (char *)calloc(1, 4096 + 10);//使用calloc在reacotr thread中开辟内存空间
	struct task_queue *taskQueue = (struct task_queue *)calloc(1, sizeof(struct task_queue));
	task_queue_init(taskQueue, QUEUESIZE);
	DBG(YELLOW" : task queue init successfully.\n"NONE);
	for (int i = 0; i < MAXCLIENTS; ++i) pthread_mutex_init(&mutex[i], NULL);
	DBG(YELLOW" : mutex init successfully.\n"NONE);

	//3.创建多个线程进行工作
	pthread_t *tid = (pthread_t *)calloc(INS, sizeof(pthread_t));
	for (int i = 0; i < INS; ++i) pthread_create(&tid[i], NULL, thread_run, (void *)taskQueue);
	DBG(YELLOW" : threads create successfully.\n"NONE);

	//4.利用反应堆模式epoll进行事件分发
	int sockfd;
	// 4-1 epoll_create
	if ((epollfd = epoll_create(1)) < 0) handle_error("epoll_create");//文件描述符被占用完就可能会出错

	// 4-2 epoll_ctl将server_listen文件描述符注册到epoll实例中
	struct epoll_event events[MAXEVENTS], ev;
	ev.data.fd = server_listen;//关注的文件描述符
	ev.events = EPOLLIN;//关注的事件、需要注册的事件
	if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_listen, &ev) < 0) handle_error("epoll_ctl");//注册操作
	DBG(YELLOW" : server_listen is added to epollfd successfully.\n"NONE);
	
	for (;;) {
		// 4-3 epoll_wait开始监听
		int nfds = epoll_wait(epollfd, events, MAXEVENTS, -1);//nfds epoll检测到事件发生的个数
		if (nfds == -1) handle_error("epoll_wait");//可能被时钟中断 or 其他问题

		for (int i = 0; i < nfds; ++i) {
			// 4-3-1 对epoll_wait监测到发生的所有事件进行遍历,进行事件分发
			int fd = events[i].data.fd;
			if (fd == server_listen && (events[i].events & EPOLLIN)) {
				/* 返回的fd为server_listen可读 表示已经有客户端进行3次握手了 */
				/* events[i].events & EPOLLIN 表示至少有一个可读 */
				// 4-3-2将accept到的文件描述符注册到epoll实例中,实现文件监听
				if ((sockfd = accept(server_listen, NULL, NULL)) < 0) handle_error("accept");
				//如果是实际应用情况,如果出现错误应该想办法处理错误,并恢复实际业务
				clients[sockfd] = sockfd;//文件描述符作为数组下标 存储新出现的conn_socket文件描述符
				ev.data.fd = sockfd;
				ev.events = EPOLLIN | EPOLLET;//设置为边缘触发模式
				make_nonblock(sockfd);
				if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) < 0) handle_error("epoll_ctl");//注册操作
			} else {
				/* 返回的fd不是server_listen可读 是普通的套接字 */
				// 4-3-3 将监测到事件event的文件描述符fd加入任务队列中,交给线程池处理
				if (events[i].events & EPOLLIN) {
					/* 套接字属于就绪状态 有数据输入需要执行 */
					task_queue_push(taskQueue, (void *)&clients[fd]);
					//不可直接将fd传入 需要保证传入的fd值总是不同,创建clients[]数组保证每次传入的fd值不同
					//当把地址作为参数传递给函数后(特别是在循环中),下一次fd的值会不断的被修改(传入的值是会变化的)
				} else {
					/* 套接字不属于就绪状态出错 将该事件的文件描述符从注册的epoll实例中删除 */
					epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
					close(fd);
				}
			}
		}//for
	}//for
	return 0;
}

一个的错误的演化_第11张图片

同样出现了segmentfault错误,问题定位到do_work函数中的int ind = strlen(data[fd])操作,

由于data[fd]的指针是NULL,对一个空字符指针使用strlen都会报segmentfault错误,本质原因是data[fd]指针没有初始化,立即使用calloc在reacotr thread中为data[fd]开辟内存空间。

for (int i = 0; i < MAXCLIENTS; ++i) data[i] = (char *)calloc(1, 4096 + 10);

问题成功解决,感悟:任何小问题都可能会引发严重的错误,一定要重视细节问题,更要重视扎实的语言基础。

你可能感兴趣的:(报错记录,c++,算法)