API這樣設(shè)計(jì)?等著程序掛掉吧!
本文轉(zhuǎn)載自微信公眾號(hào)「編程珠璣」,作者守望先生。轉(zhuǎn)載本文請聯(lián)系編程珠璣公眾號(hào)。
假設(shè)提供的接口的入?yún)⒈容^復(fù)雜,可能有人會(huì)考慮使用結(jié)構(gòu)體作為入?yún)ⅰ.?dāng)你考慮這么做的時(shí)候,災(zāi)難也將會(huì)隨之而來……
示例:
- // 來源:公眾號(hào)【編程珠璣】
- // 作者:守望先生
- // api.h
- #include<iostream>
- struct Param{
- int num;
- std::string str;
- };
- void TestFun(const Param ¶m);
- // api.cc
- #include "api.h"
- void TestFun(const Param ¶m){
- std::cout<<"num:"<<param.num<<" str:"<<param.str.c_str()<<std::endl;
- }
假設(shè)提供TestFun作為一個(gè)對外接口,我們編譯并制作為靜態(tài)庫:
- $ g++ -c api.cc -I./
- $ ar -rcs libapi.a api.o
關(guān)于靜態(tài)庫的制作,請參考《Linux下如何制作靜態(tài)庫?》。
另外一個(gè)程序main.cc這么使用它:
- // 來源:公眾號(hào)編程珠璣
- // 作者:守望先生
- #include "api.h"
- int main(){
- Param param;
- param.num = 10;
- param.str = "24";
- TestFun(param);
- return 0;
- }
編譯鏈接使用:
- $ g++ -o main main.cc -L./ -lapi -I ./
- $ ./main
看起來并沒有什么問題,有新的參數(shù),可以直接在Param中增加即可,擴(kuò)展性也不錯(cuò)。
問題來了
目前來看是沒有什么問題的,但是假設(shè),還有另外一個(gè)庫要使用它,例如:
- // 來源:公眾號(hào)編程珠璣
- // 作者:守望先生
- // use_api.h
- #include"api.h"
- void UseApi();
- // use_api.cc
- #include"use_api.h"
- void UseApi(){
- Param param;
- param.num = 10;
- param.str = "24";
- TestFun(param);
- }
也將它作為靜態(tài)庫:
- $ g++ -c use_api.cc -I./
- $ ar -rcs libuse_api.a use_api.o
這個(gè)時(shí)候同樣主程序會(huì)用到我們的原始api,但是卻使用了不同的版本,比如,新增了Param中新增了一個(gè)字段ext:
- // 來源:公眾號(hào)【編程珠璣】
- // 作者:守望先生
- // api.h
- #include<iostream>
- struct Param{
- int num;
- std::string str;
- std::string ext;
- };
- void TestFun(const Param ¶m);
- // api.cc
- #include "api.h"
- void TestFun(const Param ¶m){
- std::cout<<"num:"<<param.num<<" str:"<<param.str.c_str()<<" ext:"<<param.ext.c_str()<<std::endl;
- }
重新生成靜態(tài)庫:
- $ g++ -c api.cc -I./
- $ ar -rcs libapi.a api.o
這個(gè)時(shí)候,通過use_api使用api接口,但是鏈接新的庫:
- // 來源:公眾號(hào)編程珠璣
- // 作者:守望先生
- #include "use_api.h"
- int main(){
- UseApi();
- return 0;
- }
這個(gè)時(shí)候,再去編譯鏈接,并運(yùn)行:
- $ g++ -o main main.cc -I./ -L./ -luse_api -lapi
- $ ./main
- Segmentation fault (core dumped)
看到?jīng)]有,喜聞樂見的core dumped了,分析core還會(huì)發(fā)現(xiàn),是由于訪問非法地址導(dǎo)致的。
我們再來梳理一下這個(gè)過程:
- 提供庫libapi.a版本A
- libuse_api使用版本A進(jìn)行編譯,使用A版本的頭文件
- libapi.a庫升級到B版本,其中頭文件中增加了字段,并且實(shí)現(xiàn)也引用了新的字段
- 主程序使用了use_api,但是鏈接了版本B的libapi.a庫
這個(gè)時(shí)候,版本B的實(shí)現(xiàn)訪問了新的字段,還是use_api中還是使用A版本,并沒有傳入新字段,因此自然會(huì)導(dǎo)致非法訪問。
如何解決?
很簡單,不直接暴露成員,而是提供setter和getter,而提供方式和前面提到的PIMPL方法類似。
- // api.h
- // 來源:公眾號(hào)編程珠璣
- // 作者:守望先生
- #include<iostream>
- #include<memory>
- class Param{
- public:
- void SetNum(int num);
- int GetNum() const;
- void SetStr(const std::string &str);
- std::string GetStr() const;
- void SetExt(const std::string &str);
- std::string GetExt() const;
- Param();
- private:
- class ParamImpl;
- std::unique_ptr<ParamImpl> param_impl_;
- };
- void TestFun(const Param ¶m);
在這里頭文件中只提供setter和getter,而完全不暴露成員,具體成員的設(shè)置在ParamImpl中實(shí)現(xiàn):
- // api.cc
- // 來源:公眾號(hào)編程珠璣
- // 作者:守望先生
- #include "api.h"
- class Param::ParamImpl{
- public:
- int num;
- std::string str;
- std::string ext;
- };
- Param::Param(){
- param_impl_.reset(new ParamImpl);
- }
- // 析構(gòu)函數(shù)必須要
- Param::~Param() = default;
- void Param::SetNum(int num){
- param_impl_->num = num;
- }
- int Param::GetNum() const {
- return param_impl_->num;
- }
- void Param::SetStr(const std::string &str){
- param_impl_->str = str;
- }
- void Param::SetExt(const std::string &ext){
- param_impl_->ext = ext;
- }
- std::string Param::GetStr() const {
- return param_impl_->str;
- }
- std::string Param::GetExt() const {
- return param_impl_->ext;
- }
- void TestFun(const Param ¶m){
- std::cout<<"num:"<<param.GetNum()<<" str:"<<param.GetStr().c_str()<<"ext:"<<param.GetExt().c_str()<<std::endl;
- }
通過上面的方式,不會(huì)直接暴露成員函數(shù),而是提供接口設(shè)置或者獲取,而在實(shí)現(xiàn)中,即便出現(xiàn)新的版本增加了接口,最多也只是獲取到默認(rèn)值,而不會(huì)導(dǎo)致程序崩潰。
總結(jié)
本文和之前的文章實(shí)現(xiàn)方法是一樣的,這樣不暴露成員的做法,更大程度避免了鏈接庫不一致導(dǎo)致的問題,你學(xué)會(huì)了嗎?
作者:守望,linux應(yīng)用開發(fā)者,目前在公眾號(hào)【編程珠璣】?分享Linux/C/C++/數(shù)據(jù)結(jié)構(gòu)與算法/工具等原創(chuàng)技術(shù)文章和學(xué)習(xí)資源。
原文鏈接:https://mp.weixin.qq.com/s/3SmRDVzDq6NCBTeVPTwiWQ