從Xcode10再也不支持libstdc++提及

衆所周知從Xcode10起,蘋果摒棄了對libstdc++庫的支持轉而支持libc++庫了。這兩個庫在Xcode9甚至更早的版本就已經同時存在於系統中而且可供開發者選擇,固然在Xcode9時代蘋果就已經宣佈了將要廢棄libstdc++的信息了。html

C++標準庫

一個app應用程序中若是用到C++相關的代碼和類庫那麼就須要連接C++標準庫。C++標準庫是一套基於C++語言之上的函數和類庫,其早期代碼都定義在std命名空間中,大部分類都是用template模板實現的,它主要由IO流,string字符串類,和STL組成。標準庫中的實現代碼除了分佈在沒有後綴的頭文件(好比vector等大部分模板類)外還有一部分代碼被存放到了相應的動態庫中,也就是存放在libstdc++.dylib或者libc++.dylib中。至於爲何一個標準庫由兩個動態庫來實現則會在後面進行詳細介紹。ios

C++的規範版本

一門語言老是不可能一成不變的,C++也是如此,隨着時間的推移它也會有升級變化的改進需求。可是C++這門語言卻不像Swift那樣不負責任,它的標準和規範的升級相對來講比較嚴謹。我的以爲緣由是其自己已經很是龐大並且完善了,能升級的基本都是微小的調整了。也許你會發現其餘不少語言都是C++這門語言的裁剪版。因此能夠說學好C++,走遍天下都不怕! 下面這個表格列出的就是C++的各類版本:c++

Year C++ Standard Informal name
1998 ISO/IEC 14882:1998[20] C++98
2003 ISO/IEC 14882:2003[21] C++03
2011 ISO/IEC 14882:2011[22] C++11, C++0x
2014 ISO/IEC 14882:2014[23] C++14, C++1y
2017 ISO/IEC 14882:2017[8] C++17, C++1z
2020 to be determined C++20

在C++11標準出來之前,市面上的編譯器廠商基本上支持的都是C++98的版本。大部分的書籍或者知識裏面的語法和規則都是基於C++98的。C++11主要添加了: 類型自動推導、線程API支持、智能指針內存管理、lamda表達式、STL擴展等能力(若是你想更加詳細瞭解這些新規範,請參考:C++11新特性介紹)。各大編譯器廠商爲了自身的須要會對規範進行一些定製化處理**(這些語法的標準以及廠商的定製化稱爲方言Dialect)**。目前比較流行的C++編譯器有微軟的VC++,GNU組織的gcc(g++), 蘋果的LLVM(clang++)等。這些廠商或多或少的對C++的規範進行一些裁剪或者擴充以及對C++的各個版本的支持力度也有所不一樣。就目前來講主流的編譯器幾乎都對C++11標準已經徹底支持了。git

libstdc++.dylib和libc++.dylib

正如前面所說的C++有不一樣的版本,其中的libstdc++.dylib所表明的就是C++98版本的標準庫實現動態庫,而libc++.dylib所表明的則是C++11版本的標準庫實現動態庫。也就是說libc++其實一個更加新的C++標準庫實現,它徹底支持C++11標準,而蘋果的Xcode10將再也不支持老版本的標準庫libstdc++實現,而是升級爲只支持新版本的標準庫libc++實現了。某個靜態庫若是之前是依賴於libstdc++庫中的代碼,那麼這個靜態庫在Xcode10中被連接時將會報符號找不到的連接錯誤信息:Undefined symbols for architecture XXX,好比下面的提示:github

Undefined symbols for architecture x86_64:
  "std::__throw_length_error(char const*)", referenced from:
      std::vector<int, std::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&) in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::string::c_str() const", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::~allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
ld: symbol(s) not found for architecture x86_64

複製代碼

可能你會想按理來講libc++庫中的代碼實現應該只是libstdc++中代碼實現的升級版本,應該要存在着兼容的狀況,那爲何還會報符號未定義的錯誤呢?答案我將會在後面詳細說明。bash

libc++abi.dylib

在查看一個程序運行時所加載的全部C++動態庫時,你會發現有一個叫libc++abi.dylib的動態庫存在。這個庫主要是對C++的: new/delete、try/catch/throw、typeid等關鍵字的實現支持。這些關鍵字並非一些簡單的關鍵字,它們還承載着必定的功能。其實在一些語言中爲了使用上的簡化每每會將一些能力提煉成爲一個特殊的關鍵字,這樣在使用這些能力時每每再也不須要編寫任何的代碼,只要藉助對應的關鍵字就能夠簡化這些功能的實現。除了C++外一個典型的例子就是GO語言中的chan 關鍵字。對於C++這門語言來講系統會將上述的那些關鍵字所實現的功能的代碼存放到了一個庫中,這個庫就是libc++abi.dylib庫。下面將簡單的介紹一下libc++abi.dylib中都有那些功能:數據結構

  1. 在C++中是經過new/delete運算符來實現堆內存的分配和銷燬的,所以當在源代碼中使用new/delete關鍵字來分配和銷燬對象時,在不重載運算符的前提下編譯階段就會轉化爲對兩個全局函數的調用:
void * operator new(size_t size);
  void operator delete(void *p);
複製代碼

而這兩個函數的實現代碼就是存放在libc++abi這個動態庫中的。app

  1. 在C++中是經過try/catch/throw這幾個關鍵字來捕獲和拋出異常的。所以當在源代碼中使用這些關鍵字時,在編譯階段就會轉化爲對以下函數的調用:
extern _LIBCXXABI_FUNC_VIS _LIBCXXABI_NORETURN void
__cxa_throw(void *thrown_exception, std::type_info *tinfo,
           void (*dest)(void *));

// 2.5.3 Exception Handlers
extern _LIBCXXABI_FUNC_VIS void *
__cxa_get_exception_ptr(void *exceptionObject) throw();
extern _LIBCXXABI_FUNC_VIS void *
__cxa_begin_catch(void *exceptionObject) throw();
extern _LIBCXXABI_FUNC_VIS void __cxa_end_catch();   

複製代碼

來實現異常處理的,而這些函數的實現代碼也是存放在libc++abi這個動態庫中。ide

  1. 在C++中能夠經過typeid這個關鍵字來獲取對象的類描述信息(RTTI)對象的,C++的類描述類是一個type_info類。你能夠從這個類中查看一個C++類的名稱,數據成員和函數佈局的信息,type_info中的信息就相似於OC的isa所指向的Class類型是同樣的。type_info這個類的定義實現也是存放在libc++abi這個動態庫中的。

能夠看出libc++abi這個動態庫是一個支持C++語法的核心庫。函數

Xcode對C++的支持和設置

Xcode中創建的工程項目能夠選擇使用的C++的方言和C++的標準庫版本,在工程的Build Settings中的Apple Clang - Language - C++中的分組中的C++ Language Dialect中選擇使用的C++方言類型;C++ Standard Library中選擇使用的C++標準庫的版本。

C++方言的選項

咱們能夠經過下面的代碼來驗證C++語言對於方言的支持選項,由於在C++11中才引入了對lamda表達式的支持,所以你能夠在你工程的某個.mm文件的函數實現內寫一段lamda表達式:

//test.mm
void foo()
{
   auto f = []{ NSLog(@"test"); };
   f();   
}

複製代碼

默認狀況下Xcode對於方言的支持是c++14,所以上面的代碼能夠被編譯經過,若是將C++ Language Dialect的選項改成:C++98[-std=c++98]後就會發現編譯時報錯:

xxxxxxx\test.mm:52:16: error: expected identifier
    auto f = [] { NSLog(@"test"); };
               ^
1 error generated.
複製代碼

對於方言的選擇以及語言類型的選擇體如今編譯選項-std= 上,這個選項經過查看Xcode的編譯消息詳情就能夠看出:若是文件的後綴是.m,那麼-std=後面的值就是C Language Dialect中的選項;若是文件的後綴是.mm,那麼-std=後面的值就是C++ Language Dialect中的選項。

C++標準庫的選項

Xcode中對於C++標準庫C++ Stadard Library選項的選擇影響的是連接的標準庫動態庫的版本以及對應的頭文件的搜索路徑。

  • 若是你選擇的標準庫是libc++。那麼頭文件的搜索路徑將會是:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1,而且連接的動態庫就是libc++.dylib。

  • 若是你選擇的標準庫是libstdc++,那麼頭文件的搜索路徑將會是:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/c++/4.2.1,而且連接的動態庫就是libstdc++.dylib。

對於標準庫的選擇,體如今編譯選項 -libstd=上,查看Xcode的編譯消息詳情就能夠看出:若是某個文件的後綴是.mm,那麼-libstd=後面的值就是C++ Standard Library中的選項值。

在低於Xcode10的IDE中還能夠在工程的Build PhasesLink Binary With Libraries中同時添加對libc++.tbd和libstdc++.tbd的連接引用,那麼這裏就會帶來一個問題?爲何能夠在一個工程中能夠同時引入兩個定義了相同內容的類庫呢?難道不會在編譯時報符號衝突或者重名的錯誤嗎?但實際又不會報符號名衝突的錯誤,緣由就是C++11中引入的一個新特性來保證不會處問題的,這個新特性就是內聯命名空間(inline namespace)。

內聯命名空間(inline namespace)

假如你在兩個不一樣的動態庫中定義和導出了一個相同的函數或者類,而且當將這兩個動態庫都加入依賴後。一旦在程序中調用那個同名函數時,就會出現函數重複定義或者引入不明確的連接錯誤。可這個問題卻不會發生在不一樣版本的C++標準庫:libstdc++和libc++中,你能夠在程序中同時依賴這兩個庫,而不會產生編譯連接錯誤。咱們知道libc++中的內容是libstdc++中的超集,爲何在同時引入兩個庫時不會報函數或者類名衝突呢? 答案就是C++11中提供了對inline namespace的支持。前面說過老版本C++標準庫中的全部類的定義都是在std這個命名空間中。當你選擇的是libstdc++是你就會在全部頭文件中內容都定義在兩個宏:_GLIBCXX_BEGIN_NAMESPACE和_GLIBCXX_END_NAMESPACE之間,好比中的標準輸入和輸出流對象的定義片斷:

_GLIBCXX_BEGIN_NAMESPACE(std)

  extern istream cin;		///< Linked to standard input
  extern ostream cout;		///< Linked to standard output
  extern ostream cerr;		///< Linked to standard error (unbuffered)
  extern ostream clog;		///< Linked to standard error (buffered)

#ifdef _GLIBCXX_USE_WCHAR_T
  extern wistream wcin;		///< Linked to standard input
  extern wostream wcout;	///< Linked to standard output
  extern wostream wcerr;	///< Linked to standard error (unbuffered)
  extern wostream wclog;	///< Linked to standard error (buffered)
#endif
  
_GLIBCXX_END_NAMESPACE

複製代碼

上述的兩個宏則定義在<bits/c++config.h>下面,展開這兩個宏定義:

# define _GLIBCXX_BEGIN_NAMESPACE(X) namespace X { 
# define _GLIBCXX_END_NAMESPACE } 
namespace std {
  }
複製代碼

所以能夠明確早期的C++標準庫中的全部類和函數以及變量都是定義在std這個命名空間中的。

當你使用libc++標準庫時,你會發現全部頭文件中的類和方法都定義在_LIBCPP_BEGIN_NAMESPACE_STD和_LIBCPP_END_NAMESPACE_STD以內。好比中的標準輸入和輸出流對象的定義片斷:

LIBCPP_BEGIN_NAMESPACE_STD

#ifndef _LIBCPP_HAS_NO_STDIN
extern _LIBCPP_FUNC_VIS istream cin;
extern _LIBCPP_FUNC_VIS wistream wcin;
#endif
#ifndef _LIBCPP_HAS_NO_STDOUT
extern _LIBCPP_FUNC_VIS ostream cout;
extern _LIBCPP_FUNC_VIS wostream wcout;
#endif
extern _LIBCPP_FUNC_VIS ostream cerr;
extern _LIBCPP_FUNC_VIS wostream wcerr;
extern _LIBCPP_FUNC_VIS ostream clog;
extern _LIBCPP_FUNC_VIS wostream wclog;
複製代碼

上述兩個宏的定義在<__config>中能夠看到,展開後的定義以下:

//爲了更好理解,我把下面的宏和命令空間中的定義進行了簡化處理
#define _LIBCPP_BEGIN_NAMESPACE_STD namespace std {inline namespace __1 {
#define _LIBCPP_END_NAMESPACE_STD } }

namespace std {
  inline namespace __1 {
  }
}

複製代碼

能夠看出在libc++中,全部的類和方法以及變量都不是直接在std這個命名空間中被定義,而是放到其子命名空間std::__1中去了。子命名空間中的 inline關鍵字則是C++11中爲命名空間添加的新關鍵字:**能夠在父命名空間中定義內聯的子命名空間,內聯的子命名空間能夠把其包含的名字導入到父命名空間中,從而在父命名空間中能夠直接訪問子命名空間中定義的名字,而不用經過域限定符Child::name的形式來訪問。**就以下面的例子:

#include <iostream>

void main()
{
     std::__1::cout << "hello1" << std::__1::endl;
     std::cout << "hello2" << std::endl;
}
複製代碼

在C++11中的標準輸出流對象cout真實的定義是在std::__1這個命名空間中,可是由於std::__1::是內聯子命名空間因此能夠經過父命名空間std::來訪問。 正是由於內聯命名空間的使用,因此工程中的代碼是能夠切換不一樣版本的C++標準庫的,並且還能夠同時連接兩個不一樣的C++標準庫libstdc++.dylib和libc++.dylib,由於這兩個不一樣版本中的代碼所在命名空間是不同的,所以不會產生符號重複和衝突的錯誤!其實C++中的命名空間引入inline關鍵字就是爲了解決版本的兼容性和衝突的。 這也就能夠解釋當咱們把一個依賴libstdc++.dylib的靜態庫,引入到Xcode10的工程中時會報以下的錯誤:

Undefined symbols for architecture x86_64:
  "std::__throw_length_error(char const*)", referenced from:
      std::vector<int, std::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&) in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::string::c_str() const", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::allocator<char>::~allocator()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
  "std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()", referenced from:
      -[cpplib testfn] in libcpplib.a(cpplib.o)
ld: symbol(s) not found for architecture x86_64

複製代碼

其緣由就是由於出錯的C++類是在std::這個命名空間中被定義的(由於C++的命名修飾規則的緣由,一個方法或者函數被修飾後的名稱是包含其所在的命名空間的)。可是新版本的C++標準庫中的全部符號都是在std::__1這個命名空間中,所以連接器將沒法找到這個符號。好比標準輸入流對象cin在libc++中和libstdc++中的定義就不同:

__ZNSt3__13cinE    //這是cin在libc++.dylib庫中的被修飾事後的真實名字

__ZSt3cin   //這是cin在libstdc++.dylib庫中的被修飾事後的真實名字
複製代碼

一個問題:剛纔不是說到的內聯子命名空間是能夠直接經過父命名空間來訪問的。爲何這裏又不能夠呢?上述的內聯命名空間的訪問只是在編譯時是沒有問題的,可是在連接這個階段是不會認內聯命名空間的,連接階段只認被修飾事後的符號,也就是在連接階段是沒有內聯命名空間這個概念的。

那既然在Xcode10中報連接錯誤,又怎麼解決這種問題呢?方法有兩個:

  • 一個是將你所導入的靜態庫從新編譯,將靜態庫所依賴的標準庫升級爲libc++.dylib。(推薦方法)
  • 一種就是將老版本中的libstdc++.dylib庫拷貝到Xcode10中去。

Xcode10對libstdc++的支持

在Xcode10中已經找不到libstdc++.dylib這個庫了,並且當工程中有依賴libstdc++這個庫時或者工程設置裏面的C++ Stadard Library選項設置爲libstdc++時,就會報以下的錯誤:

clang: warning: libstdc++ is deprecated; move to libc++ [-Wdeprecated]
ld: library not found for -lstdc++
clang: error: linker command failed with exit code 1 (use -v to see invocation)
複製代碼

前面已經分析了Xcode10對兩個標準庫支持的前因後果,並且也簡單的介紹了只要將老版本中的libstdc++.dylib拷貝到新版本的IDE環境中便可,具體的方法和流程你們能夠參考以下兩篇文章:

blog.csdn.net/box_kun/art… blog.csdn.net/u010960265/…

但其實這樣是有風險的,由於Xcode10中對於C++標準庫的頭文件都是基於C++11的,所以當你經過上述方法引入了老版本的C++標準庫時,雖然在編譯連接時不會報錯正常編譯經過,可是在運行時就可能會出現崩潰的問題,尤爲是當你的靜態庫中將某個老的C++標準庫中類的對象做爲接口或者函數參數暴露出來給外界使用時就有可能由於新老版本的數據結構和內部實現的差別而形成運行時的崩潰!總之爲了完全的解決這些問題,仍是要求將你的靜態庫中的代碼在Xcode10中從新編譯是最好的解決方案。

參考列表

en.wikipedia.org/wiki/C++ blog.csdn.net/ftell/artic… blog.csdn.net/fengbingchu… blog.csdn.net/Jxianxu/art…


歡迎你們訪問個人github地址簡書地址

相關文章
相關標籤/搜索