socket() 返回的 int 是文件描述符而非指针,操作系统用它在内核中查表定位套接字结构体;常见错误是未检查返回值-1就传给bind()导致ebadf。

socket() 返回的 int 为什么能当指针用?
它根本不是指针,而是文件描述符(file descriptor),一个整数索引。操作系统用这个 int 在内核中查找对应的套接字结构体——你看到的“通过描述符操作套接字”,其实是内核在背后查表、解引用。误以为它是指针,容易在调试时绕进地址计算的死胡同。
常见错误现象:socket() 返回 -1 却没检查,直接传给 bind(),结果 bind() 报错 EBADF(Bad file descriptor)。这不是指针空解引用,是无效索引。
- 永远在
socket()后检查返回值:if (sockfd == -1) { perror("socket"); return -1; } - 不要对
sockfd做算术运算(比如sockfd + 1),它不是内存地址 - 关闭后立即设为 -1:
close(sockfd); sockfd = -1;,避免重复 close 或误用已释放句柄
struct sockaddr_in *addr 参数为什么必须取地址?
bind()、connect()、accept() 这些函数的 addr 参数类型是 struct sockaddr *,但你实际传的是 struct sockaddr_in 变量的地址。这是因为:网络 API 设计成协议无关,统一用基类 sockaddr 接口,而 sockaddr_in 是 IPv4 的具体实现。传地址,是为了让函数能读写该结构体里的字段(比如把客户端真实 IP 写回 sin_addr)。
容易踩的坑:
- 忘记调用
htons()转换端口号,导致 bind 失败或连不上——主机字节序 ≠ 网络字节序 - 传了
&addr却把addrlen设成sizeof(struct sockaddr),而实际应为sizeof(struct sockaddr_in);否则accept()可能截断地址信息 - 定义
sockaddr_in addr = {};后没初始化sin_family,Linux 下可能默认为 0,导致bind()返回EAFNOSUPPORT
recv() / send() 的 buf 参数:char* 是指针,但别乱 cast
recv() 和 send() 的缓冲区参数是 void *,你通常传 char * 指针。这里指针真正在起作用:它告诉内核数据从哪来、往哪存。但注意,它指向的是用户空间内存,不是内核缓冲区地址。
典型问题:
- 传栈上局部数组地址,但函数返回后该内存失效,而
send()是异步写入内核缓冲区——只要调用成功就安全,不依赖后续栈状态 - 用
std::string的c_str()传给send():只读没问题;但传&str[0]给recv()前必须确保str已 reserve 足够空间,否则越界 - 传
nullptr给recv()不会崩溃,但返回 0 或 -1,且 errno 可能是EINVAL—— 这不是空指针解引用,是参数校验失败
accept() 返回的新 sockfd 和原监听 sockfd 的关系
accept() 返回一个**新**的 int 描述符,和监听套接字完全独立。它指向内核中另一个套接字结构体,专用于与这个客户端通信。这时候你手上有两个整数:老的监听 sockfd(继续 accept())、新的通信 client_sockfd(用来 recv()/send())。
关键点:
- 这两个
int都是有效文件描述符,但生命周期不同:监听套接字关了,不影响已建立的连接;客户端断开,client_sockfd仍可读(直到 read 返回 0) - 不能把
client_sockfd当作指针去 reinterpret_cast 成某个结构体——它只是个数字索引 - 多线程服务时,常见错误是把
client_sockfd传给子线程后,在主线程里提前close()它——这会破坏子线程的通信,因为文件描述符是进程级共享的
真正容易被忽略的是:accept() 返回的 client_sockfd 继承了监听套接字的部分属性(比如非阻塞标志),但不继承其绑定地址或 listen 状态。它是一张全新的“通信凭证”,不是原套接字的指针副本。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











