PartD 將會使用visual studio 2017 作爲開發環境進行測試
tuple元組類型
這個類型非常像swift的元組類型,以下幾個例子就可以理解tuple的作用了。
int main()
{
//初始化一個tuple
tuple<int, double, long, string> tupleA(12, 20.5, 100L, "TONY");
//通過make_tuple創建一個tuple
auto tupleB = make_tuple("TONYA", 'a', 88);
//使用get模板類獲得tupleA_1 第0個元素,並使用auto推測其類型
auto tupleA_1 = get<0>(tupleA);
cout << tupleA_1 << endl;
//定義tupleB 模板類實體的類型別名
typedef decltype(tupleB) trans;
//通過tuple_size模板類獲得tupleB對應的模板實例的元素數量
auto target_size = tuple_size<trans>::value;
cout << target_size << endl;
//通過tuple_element模板類獲得tupleB模板實例的第1個元素類型
tuple_element<1, trans>::type tupleB_1 = get<1>(tupleB);
cout << tupleB_1 << endl;
}
其中tuple的關係運算符==、!=、>、< 均和容器的運算規則相同
使用tuple的場景通常爲返回多個不同的元素:
typedef tuple<double, int, string, char> new_tmp_type;
new_tmp_type new_function();
int main()
{
auto result = new_function();
cout << get<0>(result) << endl;
cout << get<1>(result) << endl;
cout << get<2>(result) << endl;
cout << get<3>(result) << endl;
}
new_tmp_type new_function() {
return { 10.5,25,string("TONY"),'a'};
}
bitset由於使用場景不多,就不作描述了,主要提供很多用於位運算的方便操作,詳細可以參考C++primer(第5版)640頁
隨機數:
在新標準的C++中,已經不使用基於C的rand作爲隨機數生成的唯一方法,我們可以使用隨機數引擎和分佈,其中隨機數引擎是主要生成隨機數的類,而隨機數分佈是控制隨機生成的根據隨機數引擎返回指定類型的、在給定範圍內的、服從特定概率分佈的隨機數。
以下是一個簡單的隨機數生成方案:
int main() {
shared_ptr<vector<unsigned>> vector_p = getRandomVector();
for each (auto item in *vector_p)
{
cout << item << endl;
}
string hold;
cin >> hold;
}
shared_ptr<vector<unsigned>> getRandomVector() {
default_random_engine e;
shared_ptr<vector<unsigned>> vector_p(new vector<unsigned>());
for (int i = 0; i < 10; i++) {
vector_p->push_back(e());
}
return vector_p;
}
我們只使用了default_random_engine隨機數引擎,生成了範圍10個e.min() 到 e.max()的隨機數。
我們使用隨機數分佈限定返回隨機數0-9的範圍:
shared_ptr<vector<unsigned>> getRandomVector() {
default_random_engine e;
uniform_int_distribution<unsigned> u(0, 9);
shared_ptr<vector<unsigned>> vector_p(new vector<unsigned>());
for (int i = 0; i < 10; i++) {
vector_p->push_back(u(e));
}
return vector_p;
}
但是我們發現我們每次生成的隨機數都是一致的,如果我們要連續生成隨機數我們需要將:
default_random_engine e;
uniform_int_distribution<unsigned> u(0, 9);
我們需要將上述兩個變量定義爲static,否則如果我們連續使用該方法生成的隨機數都是一樣的,相關代碼如下:
int main() {
shared_ptr<vector<unsigned>> vector_pA = getRandomVector();
shared_ptr<vector<unsigned>> vector_pB = getRandomVector();
cout << (*vector_pA == *vector_pB) << endl; //返回true
string hold;
cin >> hold;
}
shared_ptr<vector<unsigned>> getRandomVector() {
default_random_engine e;
shared_ptr<vector<unsigned>> vector_p(new vector<unsigned>());
for (int i = 0; i < 10; i++) {
vector_p->push_back(e());
}
return vector_p;
}
如果我們沒有將隨機發生器(隨機數引擎和分佈類型的集合)定義爲static則每次都是一樣的數據內容,我們需要將隨機發生器定義爲static則程序會記錄其生成狀態,此時連續調用getRandomVector就會產生兩個不一樣的隨機vector,相關代碼如下:
int main() {
shared_ptr<vector<unsigned>> vector_pA = getRandomVector();
shared_ptr<vector<unsigned>> vector_pB = getRandomVector();
cout << (*vector_pA == *vector_pB) << endl; //返回false
string hold;
cin >> hold;
}
shared_ptr<vector<unsigned>> getRandomVector() {
static default_random_engine e;
static uniform_int_distribution<unsigned> u(0, 9);
shared_ptr<vector<unsigned>> vector_p(new vector<unsigned>());
for (int i = 0; i < 10; i++) {
vector_p->push_back(u(e));
}
return vector_p;
}
我們可以使用uniform_read_distribution分佈隨機類生成浮點隨機數:
int main() {
shared_ptr<vector<double>> vector_pA = getRandomVector();
for_each((*vector_pA).begin(), (*vector_pA).end(), [](const double &item) {
cout << item << endl;
});
string hold;
cin >> hold;
}
shared_ptr<vector<double>> getRandomVector() {
static default_random_engine e;
static uniform_real_distribution<double> u(0, 1);
shared_ptr<vector<double>> vector_p(new vector<double>());
for (int i = 0; i < 10; i++) {
vector_p->push_back(u(e));
}
return vector_p;
}
由於我們每次生成隨機都是一樣的,唯一影響隨時的是隨機種子,我們可以在隨機數引擎構造函數中設定seed(種子),也可以通過seed()函數進行設置:
int main() {
shared_ptr<vector<unsigned>> vector_pA = getRandomVector(10);
shared_ptr<vector<unsigned>> vector_pB = getRandomVector(10);
cout << (*vector_pA == *vector_pB) << endl; //種子相同隨機數相同(返回true)
shared_ptr<vector<unsigned>> vector_pC = getRandomVector(11);
cout << (*vector_pA == *vector_pC) << endl; //種子不同隨機數不同(返回false)
string hold;
cin >> hold;
}
shared_ptr<vector<unsigned>> getRandomVector(long seed) {
default_random_engine e(seed);
uniform_int_distribution<unsigned> u(0, 1);
shared_ptr<vector<unsigned>> vector_p(new vector<unsigned>());
for (int i = 0; i < 10; i++) {
vector_p->push_back(u(e));
}
return vector_p;
}
同樣可以使用e.seed(seed)方式進行設置隨機種子。
生成隨機BOOL
int main() {
default_random_engine e;
bernoulli_distribution u;
if (u(e)) {
cout << "true" << endl;
}
else {
cout << "false" << endl;
}
}
IO流操縱符
IO有幾個操縱符,以下代碼列出一些操縱符的用法,需要注意的是,操縱符改變了流的輸入輸出的狀態之後,如果想重置會默認的狀態,需要使用相應的操縱符進行恢復:
int main() {
cout << boolalpha << (1 == 1) << endl; //boolalpha將bool值以true或false形式顯示
cout << noboolalpha << (1 == 1) << endl; //恢復原來的狀態,bool值以1或者0形式顯示
cout << "defualt : " << 20 << " " << 1024 << endl; //默認以10進制的形式顯示
cout << "octal : " << oct << 20 << " " << 1024 << endl; //oct以8進制形式顯示
cout << "hex :" << hex << 20 << " " << 1024 << endl; //hex以16進制形式顯示
cout << "decimal : " << dec << 20 << " " << 1024 << endl; //恢復使用10進制形式顯示
//使用showbase 顯示進制符號:
//0表示八進制
//0x表示十六進制
//無前序表示十進制
cout << showbase;
cout << "defualt : " << 20 << " " << 1024 << endl; //默認以10進制的形式顯示
cout << "octal : " << oct << 20 << " " << 1024 << endl; //oct以8進制形式顯示
cout << "hex :" << hex << 20 << " " << 1024 << endl; //hex以16進制形式顯示
cout << "decimal : " << dec << 20 << " " << 1024 << endl; //恢復使用10進制形式顯示
cout << noshowbase; //恢復無顯示進制符狀態
//浮點精度控制
cout << "Precision : " << cout.precision() << endl; //顯示當前當前浮點精度
cout << "Value : " << sqrt(2.0) << endl; //顯示2的平方根
cout.precision(12); //設置保留小數點後12位,注意這裏小數點不是截斷,而是舍入
cout << "Precision(12) : " << cout.precision() << endl; //顯示當前當前浮點精度
cout << "Value(12) : " << sqrt(2.0) << endl; //顯示2的平方根
}
輸出結果:
true
1
defualt : 20 1024
octal : 24 2000
hex :14 400
decimal : 20 1024
defualt : 20 1024
octal : 024 02000
hex :0x14 0x400
decimal : 20 1024
Precision : 6
Value : 1.41421
Precision(12) : 12
Value(12) : 1.41421356237
莫着急後面還有~!
fixed : 浮點數值顯示定點十進制
hexfloat:浮點數值顯示爲十六進制
defaultfload: 浮點數值顯示爲默認十進制
scientific:使用科學計數法顯示
代碼如下:
int main() {
//默認數字格式
cout << "defualt format : " << 100 * sqrt(2.0) << endl;
//使用科學計數法
cout << "scientific : " << scientific << 100 * sqrt(2.0) << endl;
//使用定點是十進制顯示
cout << "fixed decimal : " << fixed << 100 * sqrt(2.0) << endl;
//浮點值顯示顯示十六進制
cout << "hexadecimal : " << hexfloat << 100 * sqrt(2.0) << endl;
//恢復默認
cout << "use default : " << defaultfloat << 100 * sqrt(2.0) << endl;
}
輸出結果爲:
defualt format : 141.421
scientific : 1.414214e+02
fixed decimal : 141.421356
hexadecimal : 0x1.1ad7bcp+7
use default : 141.421
輸出補白
控制輸出格式,純粹爲了輸出好看
setw指定下一個數字或者字符串的最小空間。
left表示左對齊輸出。
right表示右對齊輸出。
Internal 控制負數的符號的位置,它左對齊符號,右對齊值,用空格填滿所有中間空間
Setfill 允許指定一個字符代替默認的空格來補白輸出。
注意:setw和endl一樣都不會改變流內部的狀態,setw只決定下一個輸出的空間大小。
代碼如下:
int main() {
int i = -16;
double d = 3.1415926;
cout << "i:" << setw(12) << i << "next col" << endl;
cout << "d:" << setw(12) << d << "next col" << endl;
cout << left << "i: " << setw(12) << i << "next col" << endl;
cout << left << "d:" << setw(12) << d << "next col" << endl;
cout << right; //恢復右對齊
cout << "i:" << setw(12) << i << "next col" << endl; //右對齊恢復檢查
cout << "d:" << setw(12) << d << "next col" << endl;
cout << internal; //將負數符號和數值分開,中間留白
cout << "i:" << setw(12) << i << "next col" << endl;
cout << "d:" << setw(12) << d << "next col" << endl;
cout << setfill('#'); //使用#作爲留白的站位符號,默認爲空格
cout << "i:" << setw(12) << i << "next col" << endl;
cout << "d:" << setw(12) << d << "next col" << endl;
cout << setfill(' '); //恢復使用空格留白
}
輸出結果如下:
非常整齊出了第二行,效果不錯
需要注意的是這個留白的操縱符,在頭文件:#include <iomanip> 中定義。
默認情況下輸入運算符會忽略空白符(空格、製表符、換行、回車)
我們可以使用noskipw保留空格符號:
int main() {
char ch;
cin >> noskipws; //保留空白符
while (cin >> ch) {
cout << ch;
}
cin >> skipws; //恢復不保留空白符
}
未格式化的輸入輸出操作:
單字節操作:
int main() {
char ch;
while (cin.get(ch)) {
cout.put(ch);
}
}
注意默認單字節操作是保留空白符的。
以下是一些不常用的流操作,方便查閱:
peek:返回輸入流中下一個字符的副本,但是它並不會從流中刪除,peek返回值仍然留在流中;
unget:使得輸入流向後移動,從而最好讀取的值又回到流中。即使我們不知道最後從流中讀取什麼值,仍然可以調用unget;
putback:更特殊版本的unget,它退回從流中讀取的最後一個值,但是它接受一個參數,此參數必須與最後讀取的值相同。
注意:一般情況下,在讀取下一個值之前,標準庫保證我們可以退後回最多一個值。即,標準庫不保證在中間不進行讀取操作的情況下能夠連續調用putback和unget。
從輸入操作返回的int值
使用函數peek和無參的get版本都是以int類型從輸入流返回一個字符:
int main() {
int ch = 0;
while ((ch == cin.get()) != EOF) {
char val = ch;
cout.put(val);
}
}
實時上我在使用的過程之中,這種方法是不可行的,這個取決於使用的機器。
多字節操作
is.get(sink,size,delim)
從is中讀取最多size個字節,並保存在字符數組中,字符數組的起始地址由sink給出,讀取過程直至遇到字符deilm或讀取了size個字節或者遇到文件尾時停止。如果遇到delim,則將其留在流中,不讀取出來存入sink。
is.getline(sink,size,delim)
與上面的三個參數的get函數版本類似,但是從流中讀出delim並丟棄,不會將delim保存到數組中。
is.read(sink,size)
讀取最大size個字節,存入字符數組sink中,返回is
is.gcount()
返回上一個未格式化讀取操作從is讀取的字節數
os.write(source,size)
將字符數組source中的size個字節寫入os,返回os。
is.ignore(size,delim)
讀取並忽略最多size個字符,包括delim。與其他未格式化函數不一樣,ignore有默認參數:size的默認值爲1,delim的默認值爲文件尾。
確定讀取了多少個字符
我們可以使用gcount來確定最後一個未格式化輸入操作讀取了多少個字符。應該在任何後續未格式化輸入操作之前調用gcount。特別是,將字符退回的單字符操作也屬於未格式化操作。如果在調用gcount之前調用peek、unget或putback,則gcount的返回值爲0
流隨機訪問
兩個重點的操作,tell和seek。一個是返回當前的流位置,另外一個是定位到流的某個地方。需要注意的是,輸入流和輸出流所使用的tell和seek,輸入流使用的tellg和seekg函數,而輸出流使用的是tellp和seekp,如果是輸入輸出流則兩個同時使用,但是實質上輸入輸出流也只是維護一堆tell和seek而已。【注意:這些操作主要針對】
tellg() 返回一個輸入流標記的當前位置。
tellp() 返回一個輸出流標記的當前位置。
seekg(pos) 在一個輸入流重新定位,pos爲流的絕對路徑。
seekp(pos) 在一個輸出流重新定位,pos爲流的絕對路徑。
seekp(off,from) 在一個輸入流定位的相對位置,off爲一個正數或者負數,from是一個相對的位置
相對的位置有:
1、beg,相對於開始的位置
2、cur, 相對於目前的位置
3、end,相對於結束的位置
舉例(seekp(-2,end))相對於結束爲止的前兩位
範例代碼如下:
int main(int argc, const char * argv[]) {
fstream fs;
fs.open("/Users/yanzhichao/Desktop/test.applescript",
fstream::app|fstream::in | fstream::out);
cout << "current pos : " << fs.tellg() << endl;
auto end_pos = fs.tellg();
string line;
fs.seekg(0,fstream::beg); //移動到文檔的最前面
cout << "document content : " << endl;
while(end_pos != fs.tellg() && getline(fs, line)){
cout << line << endl;
}
fs.seekp(0,fstream::end);
string new_text = "new text !!!";
fs << new_text << endl;
fs.close();
}
上述範例我是採取xcode進行開發,但是seekp移動到底部再進入輸出操作不成功,之後會在windows的機器中再次嘗試。
C++的異常處理
C++的異常處理與JAVA的異常處理非常相同:
1、以上會向上層調用函數一直拋出異常【這種行爲成爲棧展開】,直到main函數都沒有catch處理直接調用terminate結束應用程序,terminate是直接結束應用程序的方法;
2、異常出現時會調用對象的析構函數,並銷燬資源,同理動態分配的堆內存編譯器不會進行處理,需要在異常出現的時候執行解決,或者在對象內的析構函數中自行釋放;
3、對於匹配catch的規則基本與JAVA相似,是按照catch的順序進行執行的,如果都不匹配,exception繼續往上級函數拋出,一般情況下catch是需要正確匹配,但是也允許以下的轉換規則,但是如果拋出一個派生類,如果catch派生類的位置比catch基類的位置後,這個派生類的exception會由靠前順序的catch處理,所以一般情況下我們會將派生類的catch順序靠前;
4、一般情況下我們都會使用左值引用的方式catch一個exception,因爲有時我們需要將異常往上層拋出,並修改一些參數攜帶給上級函數的catch。需要注意的是,我們不能使用右值引用的形式去catch一個exception;
C++關於exception在catch類型轉型也有一定的限制:
1、允許非常量轉成const的常量引用。(針對於catch之後繼續拋出異常)
2、允許從派生類轉型到基類的引用。
3、數組允許轉爲指向數組的指針,函數允許轉爲指向函數的指針。
以下是一個異常捕獲的例子:
int main(int argc, const char * argv[]) {
try{
serviceTest();
}catch(logic_error &e){
cout << "main error catched : " << e.what() << endl;
}
}
void serviceTest(){
testFunc(0);
}
void testFunc(int i){
Foo foo("TONY");
if(i==0){
cout << "i : " << i << endl;
throw logic_error("testFunc error : i == 0 ");
}else{
cout << "i : " << i << endl;
}
}
有時候我們認爲在構造函數當中捕獲異常,但是構造函數有時出現異常的位置卻是成員變量初始化的位置,所以我們用到以下的try...catch寫發(這種寫法成爲函數測試塊):
Foo(string value) try:value(value){
}catch(exception &e){
};
函數測試塊,可以捕獲構造函數在初始化的過程之中或者是函數體執行過程之中出現的異常情況捕獲。
在C++11標準下,我們可以使用noexcept 關鍵字承諾某函數是不會拋出異常的:
~Foo() noexcept {
cout << "Foo destructor excuted!!! value : " << this->value << endl;
}
使用noexcept關鍵字之後,編譯器就可以做某些優化工作,但是需要注意的是,就算我們使用noexcept關鍵字承諾不會拋出異常也好,我們也可以在noexcept中拋出異常,編譯器也不會阻止,但是如果出現了異常編譯器就會馬上terminate。
關鍵還有一個參數,默認爲true,可以顯性指定函數是可能拋出異常,或者指定函數是不拋出異常:
void printValue() noexcept(false){
//可能會拋出異常
}
~Foo() noexcept(true) {
cout << "Foo destructor excuted!!! value : " << this->value << endl;
//不會拋出異常
}
需要注意的是,如果是虛函數定義了noexcept,派生類必須也要定義爲相同的noexcept,否則其派生類可以定義noexcept或者不定義noexcept;
最後列出exception的類和其派生類:
我們也可以使用繼承的方式定義自己的exception:
class MyException: public runtime_error {
public:
int myVal;
MyException(string msg,int myVal) noexcept : runtime_error(msg),myVal(myVal) { };
};
使用方式和普通的標準庫一樣,以下例子中還會使用...的方式去捕獲所有異常:
int main(int argc, const char * argv[]) {
try{
testFunc(0);
}catch(...){ //使用...捕獲所有異常
cout << "catched error!!!" << endl;
}
}
void testFunc(int i){
Foo foo("TONY");
if(i==0){
cout << "i : " << i << endl;
throw MyException("error cause i == 0 ",i);
}else{
cout << "i : " << i << endl;
}
}
命名空間
這個就應該不用多介紹了,之前用了這麼多namespace,主要是爲了規避一些函數已經全局變量等的重名問題,但是這裏包含非常多的操作和定義,以下就一一介紹。
從最簡單的開始,namespace的定義和使用:
namespace tony {
using namespace std;
class TestEntity
{
public:
TestEntity(int value);
int value;
};
void displayTestEntity(const TestEntity &entity);
}
定義了一個tony的命名空間,我們在源文件中可以使用這樣的定義方式:
namespace tony {
TestEntity::TestEntity(int value) :value(value) {};
}
void tony::displayTestEntity(const TestEntity &entity) {
cout << "TestEntity value : " << entity.value << endl;
}
上面可以主要到,我們可以使用namespace 的方式將定義也放在namespace中,但是我們也可以使用作用域訪問符的方式在命名空間之外去定義,而且可以發現,作用域訪問符的使用規則和類的定義是一樣的,只要作用域訪問符之後的代碼就已經進入命名空間的作用域當中了。
使用定義在命名空間中的成員:
int main() {
tony::TestEntity entity(10);
tony::displayTestEntity(entity);
}
說到這裏注意一下幾點:
1、命名空間的名字也必須定義它的作用域內保持唯一性;
2、命名空間可以定義在全局作用域,也可以定義在其他命名空間當中,但是不能定義在函數或者類的內部;
3、每個命名空間都是一個作用域,命名空間中的每一個名字都必須表示該空間內的唯一實體,但是在不同的命名空間之間,因爲其作用域不同,所以可以定義相同的名字;
4、命名空間可以是不連續的,就是說我們可以在不同的文件中使用相同的namespace,而且不同的文件當中相同的namespace名字,代表他們都屬於同一個namespace;
5、定義namespace時這個使用可能是一個新的空間,也有可能在現有的命名空間中添加成員;
6、通常情況之前我們不會把#include放在命名空間內部。如果我們這麼做了,隱含的意思是把頭文件中的所有的名字定義成該命名空間的成員,這樣會導致程序出錯;
定義命名空間的成員
剛剛上面的代碼也嘗試定義命名空間的成員,有兩種方式一種就通過定義命名空間,在命名空間的作用域範圍內定義其命名空間的成員,第二種方式通過作用域訪問符在命名空間的外部進行定義,但是需要注意的是,我們不能在其他的命名空間的作用域去通過這種方式定義該命名空間的成員,In other word 就是我們這種方式只適合定義在所屬命名空間的外層當中:
namespace tony {
TestEntity::TestEntity(int value) :value(value) {};
}【第一種方式】
void tony::displayTestEntity(const TestEntity &entity) {
cout << "TestEntity value : " << entity.value << endl;
}【第二種方式】
模板特例化
當然模板特例化也是跟上面的方式一樣,都適用上述的兩種方式:
namespace std {
template<> struct hash<string>
{
...
};
}
template<> struct std::hash<string>
{
...
};
全局命名空間
我們默認都一個全局的命名空間,二期全局命名空間以隱式的方式聲明,並在所有程序中都存在,全局作用域中定義名字被隱形添加到全局命名空間當中。
作用域運算符同樣可以用於全局作用域的成員,因爲全局作用域是隱式的,所有它並沒有名字。下面形式:
int global_namespace_member = 10;
int main() {
cout << ::global_namespace_member << endl;
}
嵌套的命名空間
其實非常好理解,上代碼就懂了,不用多說直接上代碼:
namespace com {
namespace ns_a{
int testA = 10;
}
namespace ns_b {
int testB = ns_a::testA;
}
}
int main() {
cout << com::ns_a::testA << endl;
cout << com::ns_b::testB << endl;
}
還用多說什麼嗎?直接過···
C++11還新引入了一種新的嵌套命名空間,成爲內聯命名空間
它和普通的嵌套命名空間最大的區別就在於,在內聯命名空間的上層命名空間不需要使用其內聯命名空間的前綴直接可以使用,在最外部也是直接通過第一層命名空間的前綴即可訪問到第二層的內聯命名空間的的名字,在定義內聯命名空間的時候在關鍵字namespace之前添加關鍵字inline,需要注意的是,我們在第一次定義內聯命名空間的時候必須添加關鍵字inline,其後就不需要了,但是你也可以定義,所以最好還是定義上吧~:
inline_namespace_a.h
inline namespace inline_namespace_a {
int testA = 10;
}
inline_namespace_b.h
inline namespace inline_namespace_a {
int testB = 20;
}
main.cpp
namespace outside_namespace {
#include "inline_namespace_a.h"
#include "inline_namespace_b.h"
}
int main() {
cout << outside_namespace::testA << endl;
cout << outside_namespace::testB << endl;
}
未命名的命名空間
未命名的命名空間中定義的變量擁有靜態的生命週期,也就是說它們在第一次使用前創建,並且知道程序結束才銷燬。
一個未命名的命名空間可以在某個給定的文件內不連續,但是不能跨多個文件。每個文件定義自己的未命名的命名空間,如果有兩個文件都定義了未命名的命名空間,則這兩個命名空間互相併無關係,而且兩個不同文件的未命名空間可以定義相同的成員名字,並且屬於不同的實體。
定義在未命名空間中的名字可以直接使用;同樣的,我們也不能對未命名空間的成員使用作用域訪問符。
如果未命名的命名空間定義在文件的最外層的作用域中,則該命名空間的名字一定要與全局作用域中的名字有所區分:
int i;
namespace { //未命名的命名空間定義
int i;
}
int main() {
i = 10; //錯誤,存在二議性
}
當然未命名的命名空間也可以進行嵌套:
namespace tony {
namespace {
int i = 20;
}
}
int main() {
cout << tony::i << endl;
}
需要注意:在文件中進行靜態聲明的做法已經被C++標準取消了,現在的做法是使用未命名的命名空間。
定義命名空間的別名,不用多上代碼馬上清晰明瞭:
namespace tony {
namespace chao {
int i = 20;
}
}
int main() {
namespace tc = tony::chao;
namespace tt = tony;
cout << tc::i << endl;
cout << tt::chao::i << endl;
}
using 聲明
1、它的有效範圍從using聲明的地方開始,直到using聲明所在的作用域結束爲止。在此過程中,外層的作用域的同名實體將被隱藏;
2、一條using聲明語句可以出現在全局作用域、局部作用域、命名空間作用域以及類的作用域當中。在類的作用域中,這樣的聲明語句智能指向基類成員。
namespace tony{
int i=30;
}
int i = 40;
int main(int argc, const char * argv[]) {
using tony::i; //using聲明
cout << i << endl; //輸出30,隱藏了全局的i,輸出的是tony::i
}
using指示
using指示我們用得比較多了,using namespace namespace_name 的方式進行定義。
1、using指示我們無法空間目標命名空間那些是可見的,因爲所有名字都是可見的。
2、using指示可以出現在全局作用域、局部作用域和命名空間作用域中,但是不能出現在類的作用域中。
3、using指示開始,直到到using指示所在的作用域內有效。
using namespace std;
int main(int argc, const char * argv[]) {
cout << "tony" << endl; //std::cout 和 std::endl
}
關於using指示的作用域實例:
namespace tony{
int i = 1;
int j = 2;
}
int main(int argc, const char * argv[]) {
using namespace tony;
using namespace std;
cout << i << " and " << j << endl; //tony::i tony::j
}
using指示名字衝突
namespace tony{
int i = 1;
int j = 2;
int k = 3;
}
int j = 20;
int main(int argc, const char * argv[]) {
using namespace tony;
using namespace std;
cout << i << endl;
// cout << j << endl; //錯誤,有二義
cout << ::j << endl; //輸出全局的J
cout << tony::j << endl; //輸出tony命名空間下的j
int k = 30;
cout << k << endl; //輸出局部的k,隱藏了tony::k
cout << tony::k << endl; //輸出tony::k
}
頭文件與using聲明或指示
頭文件如果在其頂層作用域含有using 指示或者using聲明,則會將名字注入到所有包含了該頭文件的文件中。通常情況下,頭文件應該只負責定義藉口部分的名字,而不定義實現部分的名字。因此,頭文件最多隻能在它的函數或者命名空間內使用using指示或者using聲明。
注意:
1、using指示一次性輸注入某個命名空間的所有名字,這種用法看似簡單實則充滿了風險
2、建議using指示可以在命名空間本身實現的文件中使用。
實參相關的查找與類類型形參
看看以下這段代碼:
int main(int argc, const char * argv[]) {
std::string a = "a";
std::cout << a << std::endl;
}
上述的<<我們並沒有爲它寫上命名空間的前綴,但是我們卻可以使用,其實調用的應該是:
std::operator<<(std::cout,std::string);
其實我們實際上調用的卻是沒有添加命名空間的版本:
operator<<(std::cout,std::string);
對於命名空間的名字的隱藏規則來說有一個重要的例外,它使得我們可以直接方位輸出運算符。這個例外是,當我們給函數傳遞一個類類型的對象時,除了在常規的作用域查找外還會查找函數實參類的所屬的命名空間。這個例外對於傳遞類的引用或者指針的調用同樣有效。
以下例子讓我們更加清晰明白:
namespace tony{
class tony_Class{
public:
int i;
tony_Class(int i):i(i){};
};
}
namespace tony{
tony_Class sum_tony_class(const tony_Class &a,const tony_Class &b){
tony_Class result(a.i + b.i);
return result;
}
}
int main(int argc, const char * argv[]) {
tony::tony_Class tc1(10);
tony::tony_Class tc2(20);
cout << sum_tony_class(tc1,tc2).i << endl;
}
上面例子中我們在main函數中調用sum_tony_class並沒有添加相應的命名空間前綴,但是我們依然可以訪問,這就是我們上面說的例外,編譯器會從tc1的類型(tony::tony_Class類)所屬的命名空間tony中查找是否有sum_tony_class函數,如果有直接使用該函數。
友員和命名空間
namespace tony{
class tony_Class{
friend int main(int argc, const char * argv[]) ;
int i;
public:
tony_Class(int i):i(i){};
};
}
int main(int argc, const char * argv[]) {
tony::tony_Class tc1(10);
cout << tc1.i << endl;
}
以上的代碼在main函數中調用tc1.i是錯誤的,因爲tony_Class定義的友元只是說明在tony所屬的命名空間的main函數是友元函數,所以我們必須寫清楚是全局命名空間的main函數:
int main(int argc, const char * argv[]);
namespace tony{
class tony_Class{
friend int ::main(int argc, const char * argv[]) ;
int i;
public:
tony_Class(int i):i(i){};
};
}
int main(int argc, const char * argv[]) {
tony::tony_Class tc1(10);
cout << tc1.i << endl;
}
重載與using命名空間
1、需要注意的是using申明不能指明某個函數的參數,則一下聲明是屬於錯誤的:
using print(tony&)
2、當我們爲函數書寫using申明時,該函數的所有版本都被引入到當前作用域中。
3、如果using聲明出現在局部作用域中,則引入的名字將隱藏外層作用域的相關聲明。
4、如果using聲明所在的作用域中已經有一個函數與新引入的函數同名且同參數列表,則該using聲明將會引發錯誤。
5、using聲明將爲引入的名字添加額外的重載實例,並最終擴充候選函數集的規模。
6、using指示,引入一個與已有的函數名字和參數列表完全相同的函數並不會產生錯誤。此時我們需要指明調用的是命名空間中的函數還是當前作用域的版本即可。
以下幾個例子演示using和重載的關係
1、隱藏外層的函數:
namespace tony{
void printf(double d){
cout << "tony::printf(double) : " << d << endl;
}
}
void printf(int i){
cout << " ::printf(int) : " << i << endl;
}
int main(int argc, const char * argv[]) {
using tony::printf;
int i = 12;
double j = 32.99;
printf(i); //輸出 tony::printf(double) : 12
printf(j); //輸出 tony::printf(double) : 32.99
}
2、擴展重載
namespace tony{
void printf(double d){
cout << "tony::printf(double) : " << d << endl;
}
}
void printf(int i){
cout << " ::printf(int) : " << i << endl;
}
using tony::printf;
int main(int argc, const char * argv[]) {
int i = 12;
double j = 32.99;
printf(i); //輸出 ::printf(int) : 12
printf(j); //輸出 tony::printf(double) : 32.99
}
3、using和當前的函數衝突
namespace tony{
void printf(double d){
cout << "tony::printf(double) : " << d << endl;
}
void printf(int i){
cout << "tony::printf(int) : " << i << endl;
}
}
void printf(int i){
cout << " ::printf(int) : " << i << endl;
}
using tony::printf; //錯誤出現兩個printf(int)的函數
int main(int argc, const char * argv[]) {
int i = 12;
double j = 32.99;
printf(i);
printf(j);
}
4、using指示和衝突
namespace tony{
void printf(double d){
cout << "tony::printf(double) : " << d << endl;
}
void printf(int i){
cout << "tony::printf(int) : " << i << endl;
}
}
void printf(int i){
cout << " ::printf(int) : " << i << endl;
}
using namespace tony;
int main(int argc, const char * argv[]) {
int i = 12;
double j = 32.99;
::printf(i); //輸出 ::printf(int) : 12
tony::printf(i); //輸出 tony::printf(int) : 12
printf(j); //輸出 tony::printf(double) : 32.99
}
多繼承
首先不多說先上一個多繼承的事例:
class ThirdLayer : public SecondLayerA,public SecondLayerB
{
public:
int thirdLayer_value;
virtual ~ThirdLayer(){ }
};
相關重點概念:
1、對於派生類能夠繼承的基類個數C++沒有特殊的規定;但是在某個給定的派生列表中,同一個直接基類只能出現一次;
2、構造一個派生類的對象講同時構造並初始化它的所有基類了對象;
多繼承的構造函數
其中基類的構造順序與派生列表中基類的出現順序保持一次,而與派生類構造函數初始化列表中基類的順序無關:
以下例子SecondLayerA 繼承 BaseClass、ThirdLayer直接繼承SecondLayerA 和 SecondLayerB 和間接繼承BaseClas所以初始化的順序如下:
1、先初始化BaseClass
2、然後初始化SecondLayerA
3、然後初始化SecondLayerB
4、最後初始化ThirdLayer
下面是一個多繼承的構造函數事例說明:
class BaseClass
{
public:
int base_value;
string normal_str;
BaseClass(int value, string str) :base_value(value), normal_str(str) {};
virtual ~BaseClass(){ }
};
class SecondLayerA : public BaseClass
{
public:
int secondLayerA_value;
SecondLayerA(int base_value, string base_str, int value) :
BaseClass(base_value, base_str),
secondLayerA_value(value) {};
virtual ~SecondLayerA(){ }
};
class SecondLayerB
{
public:
int secondLayerB_value;
SecondLayerB(int value) :secondLayerB_value(value) {};
virtual ~SecondLayerB() {}
};
class ThirdLayer : public SecondLayerA,public SecondLayerB
{
public:
int thirdLayer_value;
ThirdLayer(int base_val, int secondA_val, string base_str, int secondB_val,int value) :
SecondLayerA(base_val, base_str, secondA_val),
SecondLayerB(secondB_val),
thirdLayer_value(value) {};
virtual ~ThirdLayer(){}
};
析構函數和多繼承
與構造函數的順序正正相反,先析構ThirdLayer部分再析構SecondLayerA 餘此類推。
多重繼承與拷貝、移動操作
基本和正常的繼承一致,拷貝、移動順序和其構造函數的順序一致
多繼承與多態性
基本上和之前說的繼承一致,可以轉換成所有的基類指針或者引用
int main() {
ThirdLayer thridLayer(10, 20, "base", 25, 30);
SecondLayerA &secondA_ref = thridLayer;
SecondLayerB *secondB_p = &thridLayer;
BaseClass &baseC_ref = thridLayer;
}
注意:和普通的單繼承多態性一樣,靜態類型決定我們能夠使用那些成員
多重繼承下的類作用域
1、在多重繼承的情況下,相同的查找過程在所有直接基類中同時進行。如果名字在多個基類中被找到,則對該名字的使用將具有二義性,但是對於派生類來說這是合法的,只不過是在使用該名字的時候需要指定其版本。
class ThirdLayer : public SecondLayerA,public SecondLayerB
{
public:
int thirdLayer_value;
ThirdLayer(int base_val, int secondA_val, string base_str, int secondB_val,int value) :
SecondLayerA(base_val, base_str, secondA_val),
SecondLayerB(secondB_val),
thirdLayer_value(value) {};
string secondA_sameValue() {
return SecondLayerA::sameValue;
}
string secondB_sameValue() {
return SecondLayerB::sameValue;
}
virtual ~ThirdLayer(){}
};
虛基類
我們嘗試做一下的假設,如果我們secondALayer和secondBLayer的直接基類都是BaseLayer,而ThirdLayer直接繼承secondALayer和secondBLayer,此時ThirdLayer存在兩個BaseLayer的子部分。如果我們在ThirdLayer中只有一個BaseLayer子部分,就需要使用虛基類
虛基類使用方式很簡單,在secondALayer和secondBLayer的繼承部分添加virtual:
class SecondLayerA : public virtual BaseClass
class SecondLayerB : public virtual BaseClass
虛繼承的對象的構造方式:
我們需要想一個問題,因爲SecondLayerA和B都分別繼承於BaseClass所以發現一個問題,BaseClass分別讓SecondLayerA和B 進行兩次的構造函數調用,所以在虛繼承中有一個規則,就是SecondLayerA和B 不構造BaseClass 由ThirdLayer進行構造,而且ThirdLayer需要首先構造BaseClass 然後按照繼承順序進行構造直接基類:
class ThirdLayer : public SecondLayerA,public SecondLayerB
{
public:
int thirdLayer_value;
ThirdLayer(int base_val, int secondA_val, string base_str, int secondB_val,int value) :
BaseClass(123,"virtualClass"),
SecondLayerA(base_val, base_str, secondA_val),
SecondLayerB(secondB_val),
thirdLayer_value(value) {};
。。。中間省略。。。
};
int main() {
ThirdLayer thridLayer(10, 20, "base", 25, 30);
cout << thridLayer.normal_str << endl;
}
輸出virtualClass
析構部分也是一樣需要和構造一樣,最後由ThirdLayer析構BaseClass 析構順序也是跟上面的構造順序相反。
合成拷貝和移動構造函數和構造函數的執行順序一致
運行時類型識別
typeid 運算符:返回表達式的類型;
dynameic_cast 運算符:用於將基類指針或引用安全地轉成派生類的指針或者引用
dynamic_cast的三種用法:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
如果是指針類型轉換失敗返回一個0 如果是引用類型轉換失敗則拋出bad_cast異常
typeid運算符使用方式
Derived *dp = new Derived;
Base *bp = dp;
if(typeid(*bp) == typeid(*dp)){
//bp和dp指向同一類型對象
}
if(typeid(*bp) == typeid(Derived)){
//bp實際指向Derived
}
需要注意的是,我們不能直接使用dp因爲dp是屬於指針類型,所以一定需要進行解引用*
typeid()返回的是一個type_info類,這個類的定義隨着編譯器的不同會有所不同,不過C++標準至少需要提供一下的操作
t1 == t2 這個不用說了相同類型返回true
t1 != t2 這個也不用說了
t.name() 這個返回一個C的字符串,表達類型的名字,但是生成名字的方式因系統而異。
t1.before(t2) 表示t1是否位於t2之前,before所採用的順序關係是依賴於編譯器