继承要解决什么问题
先看一个实际场景:项目里有三种I2C传感器,每种都需要I2C读写、设备地址、初始化状态。如果每个传感器都独立定义一遍,I2C相关代码重复三份,改一个地方要改三处。
继承的本质是: 把共性抽到基类,个性留在派生类 。
基础做法
C语言实现继承的核心手段就一个——把基类结构体作为派生类的第一个成员。
typedefstruct { uint8_t i2c_addr; uint8_t bus_id; int initialized; } i2c_device_t; typedefstruct { i2c_device_t base; // 基类,必须放第一个 int32_t t_fine; // BME280特有 uint8_t calib[42]; // BME280特有 } bme280_t; typedefstruct { i2c_device_t base; // 基类,必须放第一个 uint8_t crc_enabled; // SHT30特有 uint8_t repeatability; } sht30_t;为什么必须放第一个?C标准(C99 6.7.2.1)保证:结构体首成员的偏移量为0,即首成员的地址等于结构体本身的地址。这意味着:
bme280_t bme; i2c_device_t *dev = (i2c_device_t *)&bme // 安全,地址相同 i2c_device_t *dev2 = &bme.base // 等价写法两种写法指向同一个地址。这是C语言实现"向上转型"的基础——把派生类指针当成基类指针用,不需要偏移计算。
如果base不是第一个成员,强转后地址就不对了,读出来的字段全是错的,在MCU上大概率HardFault。
基类函数复用
基类定义共性接口,派生类直接调用:
inti2c_device_init(i2c_device_t *dev, uint8_t bus_id, uint8_t addr) { dev->i2c_addr = addr; dev->bus_id = bus_id; dev->initialized = 1; return i2c_bus_check(bus_id); } inti2c_device_read_reg(i2c_device_t *dev, uint8_t reg, uint8_t *buf, uint16_t len) { return i2c_transfer(dev->bus_id, dev->i2c_addr, reg, buf, len); } inti2c_device_write_reg(i2c_device_t *dev, uint8_t reg, constuint8_t *buf, uint16_t len) { return i2c_transfer_write(dev->bus_id, dev->i2c_addr, reg, buf, len); }派生类构造时调用基类构造函数,就像C++的初始化列表:
intbme280_init(bme280_t *dev, uint8_t bus_id, uint8_t addr) { // 先初始化基类 int ret = i2c_device_init(&dev->base, bus_id, addr); if (ret != 0) return ret; // 再初始化派生类自有属性 ret = i2c_device_read_reg(&dev->base, 0x88, dev->calib, 24); if (ret != 0) return ret; ret = i2c_device_read_reg(&dev->base, 0xA1, dev->calib + 24, 1); if (ret != 0) return ret; ret = i2c_device_read_reg(&dev->base, 0xE1, dev->calib + 25, 7); if (ret != 0) return ret; dev->t_fine = 0; return0; }读取传感器数据时,基类函数直接复用:
intbme280_read_raw(bme280_t *dev, uint8_t *raw_data) { // 复用基类的i2c读函数 return i2c_device_read_reg(&dev->base, 0xF7, raw_data, 8); }向上转型
有了结构体嵌套,就可以用一个基类指针数组管理所有派生类对象:
#define MAX_DEVICES 8 statici2c_device_t *g_devices[MAX_DEVICES]; staticint g_device_count = 0; voiddevice_register(i2c_device_t *dev) { if (g_device_count < MAX_DEVICES) { g_devices[g_device_count++] = dev; } } voiddevices_check_all(void) { for (int i = 0; i < g_device_count; i++) { if (!g_devices[i]->initialized) { printf("Device 0xX not initialized\n", g_devices[i]->i2c_addr); } } }注册时传基类指针:
staticbme280_t g_bme280; staticsht30_t g_sht30; bme280_init(&g_bme280, 0, 0x76); sht30_init(&g_sht30, 0, 0x44); device_register(&g_bme280.base); // 向上转型 device_register(&g_sht30.base); // 向上转型device_register 只知道 i2c_device_t ,不关心具体是BME280还是SHT30。新增传感器类型时,注册逻辑不用改。
向下转型
向上转型是安全的(地址相同),向下转型需要额外信息——你得知道这个基类指针实际指向的是哪个派生类。
Linux内核用 container_of 宏从成员指针反推出容器结构体指针:
#define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member)))原理:已知成员指针和它在结构体中的偏移量,用指针减去偏移量就得到结构体首地址。
voidhandle_device_event(i2c_device_t *dev) { // 假设通过地址判断具体类型 if (dev->i2c_addr == 0x76) { bme280_t *bme = container_of(dev, bme280_t, base); float temp, humi; bme280_read_compensated(bme, &temp, &humi); } elseif (dev->i2c_addr == 0x44) { sht30_t *sht = container_of(dev, sht30_t, base); float temp, humi; sht30_read(sht, &temp, &humi); } }因为 base 是第一个成员, offsetof(bme280_t, base) 等于0,所以 container_of 的结果和直接强转 (bme280_t *)dev 一样。但 container_of 更安全——如果有人把base挪到了其他位置,直接强转会出bug, container_of 不会。
多层继承
C语言也可以做多层继承,但层数多了代码可读性会下降,一般不超过三层。
// 第一层:设备基类 typedefstruct { uint8_t type; uint8_t id; int state; } device_t; // 第二层:I2C设备,继承device_t typedefstruct { device_t base; // 继承device_t uint8_t i2c_addr; uint8_t bus_id; } i2c_device_t; // 第三层:BME280,继承i2c_device_t typedefstruct { i2c_device_t base; // 继承i2c_device_t int32_t t_fine; uint8_t calib[42]; } bme280_t;访问最顶层基类:
bme280_t bme; device_t *dev = (device_t *)&bme // 仍然安全,地址相同多层继承的代价是访问中间层成员时要写更长的路径: bme.base.base.id 。如果觉得太长,可以用宏或内联函数封装。
私有成员模拟
C语言没有private关键字,但可以用不透明指针(opaque pointer)模拟私有成员。
头文件只声明前向引用:
// bme280.h typedefstructbme280_privbme280_priv_t; typedefstruct { i2c_device_t base; bme280_priv_t *priv; // 私有数据,外部看不到内容 } bme280_t;源文件定义完整结构体:
// bme280.c structbme280_priv { int32_t t_fine; uint8_t calib[42]; uint8_t oversampling; };外部代码只能看到 bme280_priv_t *priv 这个指针,不知道里面有什么,也没法直接访问。这比C++的private弱——别人还是可以通过指针偏移硬读,但至少表达了"别碰这个字段"的意图。
计算机专业,狗都不学?
电子行业未来重要主线:从芯片性能迈向系统整合bm.756yx.Com
oe.756yx.Com
qtv.756yx.Com
le.756yx.Com
spy.756yx.Com
j.756yx.Com
or.756yx.Com
mvd.756yx.Com
ux.756yx.Com
orf.756yx.Com
zq.756yx.Com
jh.756yx.Com
ju.756yx.Com
fdv.756yx.Com
nh.756yx.Com
gza.756yx.Com
ymc.756yx.Com
vks.756yx.Com
zsd.756yx.Com
gus.756yx.Com
qn.756yx.Com
wh.756yx.Com
xw.756yx.Com
du.756yx.Com
zyl.756yx.Com
fws.756yx.Com
ijq.756yx.Com
fz.756yx.Com
dt.756yx.Com
hpo.756yx.Com
bny.756yx.Com
jfj.756yx.Com
r.756yx.Com
zo.756yx.Com
et.756yx.Com
col.756yx.Com
frq.756yx.Com
fnj.756yx.Com
sl.756yx.Com
ilx.756yx.Com
kys.756yx.Com
iyx.756yx.Com
cdy.756yx.Com
ogm.756yx.Com
hv.756yx.Com
i.756yx.Com
cm.756yx.Com
hyl.756yx.Com
gff.756yx.Com
gb.756yx.Com
ymz.756yx.Com
us.756yx.Com
wp.756yx.Com
es.756yx.Com
lp.756yx.Com
nxb.756yx.Com
js.756yx.Com
tby.756yx.Com
c.756yx.Com
xcf.756yx.Com
av.756yx.Com
qos.756yx.Com
ph.756yx.Com
s.756yx.Com
rju.756yx.Com
xm.756yx.Com
x.756yx.Com
cqf.756yx.Com
ox.756yx.Com
iw.756yx.Com
wq.756yx.Com
ab.756yx.Com
rhh.756yx.Com
ijt.756yx.Com
ei.756yx.Com
nqr.756yx.Com
kar.756yx.Com
nbp.756yx.Com
tb.756yx.Com
hu.756yx.Com
p.756yx.Com
lo.756yx.Com
td.756yx.Com
jo.756yx.Com
q.756yx.Com
yz.756yx.Com
ldm.756yx.Com
ofz.756yx.Com
nq.756yx.Com
jej.756yx.Com
vl.756yx.Com
nse.756yx.Com
qa.756yx.Com
swp.756yx.Com
psv.756yx.Com
otf.756yx.Com
jpk.756yx.Com
huu.756yx.Com
hx.756yx.Com
bp.756yx.Com
jcb.756yx.Com
bgg.756yx.Com
pkq.756yx.Com
jd.756yx.Com
urw.756yx.Com
og.756yx.Com
jaj.756yx.Com
cqr.756yx.Com
lm.756yx.Com
jhq.756yx.Com
zpn.756yx.Com
ijw.756yx.Com
yyx.756yx.Com
ru.756yx.Com
he.756yx.Com
ni.756yx.Com
nd.756yx.Com
vp.756yx.Com
t.756yx.Com
qya.756yx.Com
iti.756yx.Com
bhc.756yx.Com
bkv.756yx.Com
xj.756yx.Com
fsb.756yx.Com
odh.756yx.Com
hqq.756yx.Com
bv.756yx.Com
yq.756yx.Com
flz.756yx.Com
sp.756yx.Com
om.756yx.Com
uw.756yx.Com
ew.756yx.Com
ceq.756yx.Com
jp.756yx.Com
rv.756yx.Com
zfh.756yx.Com
kdp.756yx.Com
fd.756yx.Com
psq.756yx.Com
mf.756yx.Com
zg.756yx.Com
mu.756yx.Com
qs.756yx.Com
inr.756yx.Com
ayk.756yx.Com
kk.756yx.Com
xan.756yx.Com
our.756yx.Com
xkt.756yx.Com
vec.756yx.Com
tm.756yx.Com
xdr.756yx.Com
tp.756yx.Com
qmr.756yx.Com
kj.756yx.Com
gs.756yx.Com
uxl.756yx.Com
nxt.756yx.Com
bq.756yx.Com
cwu.756yx.Com
rk.756yx.Com
ent.756yx.Com
wzg.756yx.Com
uvu.756yx.Com
hvz.756yx.Com
pyj.756yx.Com
wm.756yx.Com
xy.756yx.Com
jx.756yx.Com
nhr.756yx.Com
d.756yx.Com
bc.756yx.Com
ot.756yx.Com
sh.756yx.Com
ej.756yx.Com
dz.756yx.Com
ute.756yx.Com
ba.756yx.Com
wi.756yx.Com
lk.756yx.Com
wqg.756yx.Com
ery.756yx.Com
kkl.756yx.Com
euu.756yx.Com
rz.756yx.Com
rwp.756yx.Com
jt.756yx.Com
xi.756yx.Com
iit.756yx.Com
ntb.756yx.Com
ebk.756yx.Com
ho.756yx.Com
gz.756yx.Com
tw.756yx.Com
kf.756yx.Com
dg.756yx.Com
bue.756yx.Com
lv.756yx.Com
uhd.756yx.Com
rub.756yx.Com
pt.756yx.Com
ivu.756yx.Com
mtd.756yx.Com
cr.756yx.Com
mrz.756yx.Com
vpp.756yx.Com
yl.756yx.Com
pi.756yx.Com
tn.756yx.Com
uyn.756yx.Com
ww.756yx.Com
vfw.756yx.Com
qv.756yx.Com
fcw.756yx.Com
ee.756yx.Com
uj.756yx.Com
inh.756yx.Com
zix.756yx.Com
zs.756yx.Com
kr.756yx.Com
fk.756yx.Com
fu.756yx.Com
ce.756yx.Com
lr.756yx.Com
sf.756yx.Com
ft.756yx.Com
dy.756yx.Com
dyx.756yx.Com
gvb.756yx.Com
iaw.756yx.Com
gj.756yx.Com
sw.756yx.Com
mq.756yx.Com
vi.756yx.Com
zkq.756yx.Com
uzm.756yx.Com
ixq.756yx.Com
it.756yx.Com
qx.756yx.Com
wpa.756yx.Com
lf.756yx.Com
cnp.756yx.Com
gy.756yx.Com
xgc.756yx.Com
aq.756yx.Com
bn.756yx.Com
kon.756yx.Com
maz.756yx.Com
uok.756yx.Com
dsr.756yx.Com
yeq.756yx.Com
nr.756yx.Com
sme.756yx.Com
gwl.756yx.Com
fxv.756yx.Com
xkl.756yx.Com
hg.756yx.Com
xq.756yx.Com
lb.756yx.Com
mx.756yx.Com
ubv.756yx.Com
qqy.756yx.Com
bqs.756yx.Com
pno.756yx.Com
zts.756yx.Com
oa.756yx.Com
uxe.756yx.Com
ae.756yx.Com
see.756yx.Com
xwy.756yx.Com
wrw.756yx.Com
zwj.756yx.Com
tt.756yx.Com
sg.756yx.Com
ast.756yx.Com
awn.756yx.Com
dwn.756yx.Com
zyn.756yx.Com
ujt.756yx.Com
if.756yx.Com
mo.756yx.Com
yti.756yx.Com
spr.756yx.Com
st.756yx.Com
la.756yx.Com
di.756yx.Com
vn.756yx.Com
brm.756yx.Com
Unix时间戳为何从1970年开始?