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