• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-08-14 23:05 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

C的结构体与对象模型

概述

  1. C 不是“去掉 classC++
  2. C 主要通过“数据结构 + 普通函数 + 明确约定”组织程序

C没有构造函数

  1. 下面这段

  1. 只是在栈上定义了对象,成员没有自动初始化
    1. 此时读取:
    2. 会读取不确定值,行为可能未定义

  1. 应该明确初始化

  1. C23 新增了空初始化

指定成员初始化

  1. C99 就有的功能
    1. 没有写出的成员会被初始化为零,因此 ip 会全部填充为 '\0'

修改对象通常传指针

C中资源所有权主要靠约定

  1. 当前 Client 没有析构函数,所以必须明确规定
    1. 谁负责关闭 fd
    2. 哪个函数初始化对象
    3. 初始化失败后对象处于什么状态
    4. client_disconnect() 能否重复调用
    5. 对象销毁后是否还存在异步任务引用它

数组、指针与字符串

数组和指针是两种不同对象

  1. buffer:包含 128char 的数组对象
  2. pointer:保存 buffer[0] 地址的指针对象

数组为什么看起来像指针?

  1. 在大多数表达式中,数组会发生“数组到指针转换”:

  1. 但以下两种情况不会发生这个转换

  1. 注意这几个表达式

  1. 虽然它们通常具有相同的数值地址,但类型和指针运算含义不同

数组作为函数参数时会退化

  1. 下面三个函数声明本质相同

  1. 函数真正收到的是指针,不是完整数组

字符数组不一定是字符串

  1. C 字符串必须以 '\0' 结束

字符串字面量的陷阱

  1. 创建了一个可以修改的数组

  1. 字符串字面量通常位于只读存储区域
    1. 虽然历史原因导致 C 中字符串字面量的类型不是 const char[],但程序仍然禁止修改它

strlen 不知道缓冲区容量

  1. strlen() 的工作方式相当于
    1. 它只寻找 '\0'
    2. 不知道数组容量
    3. 不会阻止越界
    4. 时间复杂度是 O(n)
    5. 不能处理任意二进制数据

  1. 示例
    1. recv() 只保证前 received 个字节有效,不会自动添加 `'\0'

正确处理 Socket 数据

  1. 二进制协议
    1. 二进制数据可能包含零字节

  1. 文本协议
    1. 如果确认收到的是文本,并且需要使用字符串函数,应给结束符预留一个字节

memcpy 也需要“指针+长度”

  1. 调用者必须保证:
    1. source 至少有 length 个可读字节
    2. destination 至少有 length 个可写字节
    3. 两块区域不重叠
    4. 长度计算没有整数溢出

  1. 如果内存区域可能重叠,应使用

  1. 错误的容量判断

指针运算、void* 与动态内存

概述

  1. 指针不仅是一个地址,还隐含了它所指向对象的类型、边界和生命周期

指针运算按“元素”移动

  1. pointer + 2 不是地址增加 2 字节,而是前进两个 int

指针运算不能越过对象边界

  1. 允许构造尾后指针

  1. 典型遍历
    1. 原则上,指针加减以及指针比较应当限制在同一个数组对象及其尾后位置内

指针类型影响偏移量

  1. 假设

标准C不允许对 void* 做算术

  1. void 没有确定大小,所以标准C中不允许

  1. 需要按字节移动时,应转换为
    1. unsigned char 非常适合检查和复制对象的原始字节表示

malloc 只分配未初始化的存储

  1. 成功时获得能够容纳 10int 的内存;失败时返回 NULL

  1. 必须检查

  1. malloc 不会初始化内容

  1. calloc 将分配的字节清零
    1. 不过它提供的是“所有位为零”,不要泛化成任何C 类型都必然得到语义上的零值

C中不要转换 malloc 返回值

  1. 推荐

  1. 不推荐

  1. 原因:
    1. C 允许 void* 隐式转换为其他对象指针
    2. sizeof *numbers 自动跟随指针类型
    3. 改变变量类型时不容易忘记同步修改
    4. 强制转换在某些旧代码中可能掩盖缺少 <stdlib.h> 的问题
  2. C++ 中,void* 不能隐式转换为 int*

分配长度也可能溢出

  1. 错误代码
    1. 如果 count 特别大,乘法可能发生 size_t 无符号整数回绕,最终只分配一小块内存

  1. 安全检查

动态数组示例

free 结束内存生命周期

  1. 调用后:
    1. 分配的内存不能再访问
    2. 所有指向该内存的指针都悬空
    3. 只是将其中一个指针设为 NULL,不能修复其他副本

  1. 其他规则

realloc 的正确使用方式

  1. 危险写法

  1. 正确写法

  1. 成功后:
    1. 原内存可能被释放
    2. 数据可能被移动到新地址
    3. 所有旧指针、成员指针、解析出来的 header 指针都应视为失效
    4. 应当重新通过新缓冲区地址计算偏移

  1. 不要依赖 realloc(pointer, 0) 的特殊行为
    1. 需要清空时明确写

动态内存与异步任务

  1. 假设连接对象拥有接收缓冲区

  1. 需要同时解决两个问题:
    1. 状态问题:连接是否已经关闭?
    2. 生命周期问题:任务访问期间,连接对象是否仍存在?
  2. 常见方案包括:
    1. 关闭前等待所有相关任务完成
    2. 任务只保存复制出来的数据,不保存连接裸指针
    3. 使用显式引用计数
    4. 连接统一归事件循环线程销毁
    5. 使用任务取消加安全回收机制

consttypedef 与函数指针

常见的 const 指针

  1. 指向常量的指针
    1. pointer 可以指向其他对象
    2. 不能通过 pointer 修改对象
    3. const星左,底被指

  1. 常量指针
    1. pointer 必须初始化
    2. 不能再指向其他对象
    3. 可以通过它修改当前对象
    4. const星右,顶指针

  1. 指向常量的常量指针

const 并不拥有对象

  1. 它只表示函数承诺不通过 client 修改对象,不代表:
    1. client 指向的对象永久不变
    2. 其他线程不能修改对象
    3. 指针拥有对象
    4. 对象在函数调用后仍然存活
    5. 访问自动获得线程安全
  2. 因此,const 是访问限制,不是生命周期和并发机制
  3. const 通常是浅层的
    1. buffer 指向的结构体不可修改,但结构体里的 data 仍然是 char*,它指向的数据并没有自动变成只读

不要强行修改真正的 const 对象

  1. 强制转换只能改变表达式的类型,不能改变原对象确实是 const 的事实
  2. 如果原始对象不是 const,只是临时通过 const 指针观察它,则原对象仍可由其他非 const 指针修改

C中的const int 不一定是编译期常量

  1. 在函数内部,这通常是 C99 的变长数组 VLA,而不是传统的编译期定长数组
  2. 下面在 C17 中不能作为文件作用域数组长度

  1. 宏可以形成整数常量表达式

typedef 只是类型别名

  1. Port 并不是一个全新的强类型,它仍然与 unsigned short 兼容

指针 typedef 会隐藏 const 位置

  1. 这是一个高频陷阱

  1. 这也是为什么普通对象指针经常不建议隐藏在 typedef

如何阅读复杂声明

  1. values 开始:
    1. values[4]values 是包含 4 个元素的数组
    2. *:每个元素是指针
    3. int:指向 int
    4. 所以它是:包含 4 个 int* 的数组

  1. 括号改变结合顺序:
    1. *values:values 是指针
    2. (*values)[4]:指向一个包含 4 个元素的数组
    3. 元素类型为 int
    4. 所以它是:指向 int[4] 数组的指针

  1. 先看 find_value(void),它是函数;然后看左边的 *
    1. 函数没有参数,返回 int*

  1. 由于括号:
    1. find_value 是指针,指向一个没有参数、返回 int 的函数

C17中,f()f(void)不一样

  1. C17
    1. 表示参数信息没有说明,不是明确的无参数函数

函数指针

  1. 普通函数

  1. 对应函数指针

  1. 使用 typedef 简化

  1. 注意:
    1. 函数指针必须指向兼容签名的函数
    2. 强制转换后以错误类型调用会产生未定义行为

void* context 模拟回调上下文

  1. C没有lambda 捕获和成员函数回调,通常把函数指针与上下文指针组合起来

异步回调的生命周期陷阱

  1. 上面的例子同步执行,因此:

  1. 如果把任务放入线程池
    1. 函数返回后,局部变量 context 生命周期结束
    2. 线程池以后调用回调时,保存的指针已经悬空

  1. 因此异步任务必须保证:
    1. 上下文比任务存活得更久
    2. 或者任务复制上下文
    3. 或者动态分配并明确由谁释放
    4. 或者使用引用计数
    5. 停止线程池时等待任务结束
  2. void* 只携带地址,不携带类型检查、所有权或生命周期

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享