天天看點

C++ primer (2) —— 基礎

一年前的部分學習筆記,現在整理并複習它們。

八 域和生命期

名字空間域

是不包含在函數聲明,函數定義或者類定義内的程式文本部分。

程式員也可以利用名字空間定義namespace definition 來定義使用者聲明的user-declared 的名字空間。它們被嵌套在全局域内。

局部域内的名字解析是這樣進行的:

首先查找使用該名字的域, 如果找到一個聲明則該名字被解析. 如果沒有找到則查找包含該域的域, 這個過程會一直繼續下去. 直到找到一個聲明或已經查找完整個全局域. 如果後一種情況發生即沒有找到該名字的聲明, 則這個名字的用法将被标記為錯誤.

要求全局對象和函數或者隻有一個定義或者在一個程式中有多個完全相同的定義, 這樣的要求被稱為一次定義法則: ODR one definition rule

在全局域中定義的對象如果沒有指定顯式的初始值則該存儲區被初始化為0

關鍵字extern 為聲明但不定義一個對象提供了一種方法, 實際上它類似于函數聲明, 承諾了該對象會在其他地方被定義或者在此文本檔案中的其他地方, 或者在程式的其他文本檔案中例如: extern int i;

如果應用程式有很大的頭檔案, 則使用預編譯頭檔案而不是普通頭檔案可以大大降低應用程式的編譯時間

頭檔案不應該含有非inline 函數或對象的定義

例如下面的代碼表示的正是這樣的定義, 是以不應該出現在頭檔案中

extern int ival = 10;

double fica_rate;

符号常量和inline函數可以被定義多次:

為了使編譯器能夠用一個常量值替換它的名字, 該常量的定義它的初始值必須在它被使用的檔案中可見, 因為這個原因符号常量可以在同一程式的不同檔案中被定義多次.

常量指針,指針常量,指向常量的常指針

常量指針

定義為指針指向的變量的值不可通過該指針修改, const在 * 前面

const int*p ( int const * p )

int main(int argc, char *argv[]) {
int a=12;
    int const * p=&a;  // or : const int *p=&a;
    cout<<*p<<endl;
    *p=14;   // [Error] assignment of read-only location '* p' 
    cout<<*p<<endl;   
    return 0;
}       

指針常量

指針指向的數值可以改變,然而指針所儲存的位址卻不可以改變。

int* const p

int main(int argc, char *argv[]) {
int a=12,b=13;
    int * const p=&a;    //指針常量
    cout<<*p<<endl;
    *p=14;   // change its value
    cout<<*p<<endl;   
    p=&b;   // Error: assignment of read-only variable 'p'
    return 0;
}       

指向常量的常指針

指針指向的位址和數值都是不可更改的。

const int const*p

int main(int argc, char *argv[]) {
int a=12,b=13;
    const  int*  const p = &a;
    cout<<*p<<endl;
    *p=15;  // [Error] assignment of read-only location '*(const int*)p' 
    p=&b;   // [Error] assignment of read-only variable 'p' 
    return 0;
}       

建議把那些天生無法内聯的函數不聲明為inline 并且不放在頭檔案中

内聯擴充

是用來消除函數調用時的時間開銷。它通常用于頻繁執行的函數。 一個小記憶體空間的函數非常受益。如果沒有内聯函數,編譯器可以決定哪些函數内聯。

自動對象的存儲配置設定發生在定義它的函數被調用時.

因為與自動對象相關聯的存儲區在函數結束時被釋放, 是以應該小心使用自動對象的位址, 自動對象的位址不應該被用作函數的傳回值. 因為函數一旦結束了該位址就指向一個無效的存儲區. 例如:

#include "Matrix.h"
Matrix* trouble( Matrix *pm )
{
{
Matrix res;
// 用pm 做一些事情
// 把結果指派給res
return &res; // 糟糕!
}
int main()
{
Matrix m1;
// ...
Matrix *mainResult = trouble( &m1 );
// ...      

< 在本例中該位址可能仍然有效, 因為我們還沒有調用其他函數覆寫掉trouble()函數的活動記錄的部分或全部, 是以這樣的錯誤很難檢測.>

在函數中頻繁被使用的自動變量可以用register 聲明, 如果可能的話編譯器會把該對象裝載到機器的寄存器中, 如果不能夠的話則對象仍位于記憶體中. 出現在循環語句中的數組索引和指針是寄存器對象的很好例子.

for ( register int ix = 0; ix < sz; ++ix ) // …

for (register int *p = array ; p < arraySize; ++p ) // …

new 表達式

沒有傳回實際配置設定的對象, 而是傳回指向該對象的指針。對該對象的全部操作都要通過這個指針間接完成. 例如: int *pi = new int;

空閑存儲區的第二個特點是配置設定的記憶體是未初始化的. 空閑存儲區的記憶體包含随機的位模式. 它是程式運作前該記憶體上次被使用留下的結果. 測試: if ( *pi == 0 ) 總是錯誤的。除非:int *pi = new int( 0 );

如果new 表達式調用的new()操作符不能得到要求的記憶體通常會抛出一個bad_alloc 異常

記憶體被釋放–> delete pi;

delete 表達式調用庫操作符delete(), 把記憶體還給空閑存儲區, 因為空閑存儲區是有限的資源. 是以當我們不再需要已配置設定的記憶體時就應該馬上将其返還給空閑存儲區. 這是很重要的.

當程式運作期間遇到delete 表達式時, pi指向的記憶體就被釋放了, 但是指針pi 的記憶體及其内容并沒有受delete 表達式的影響. 在delete表達式之後pi 被稱作空懸指針,即指向無效記憶體的指針, 一個比較好的辦法是在指針指向的對象被釋放後将該指針設定為0.

野指針

它沒有被正确的初始化,于是指向一個随機的記憶體位址

int main()
{
    int *a=(int *)malloc(sizeof(int));
    *a=1;
    printf("a:%d a'addr:%p\n",*a,a);
    delete(a);
    printf("a:%d a'addr:%p\n",*a,a);     //A dangling pointer 空懸指針,原來所指的記憶體釋放了
    int *b;    //A wild pointer 野指針 ,一開始 就沒有賦初值
    printf("b:%d b'addr:%p\n",*b,b);
 return 0;
}
/*
a:1 a'addr:003E2460
a:0 a'addr:003E2460
b:-1869573949 b'addr:7C93154C
*/      

auto_ptr 對象被初始化為指向由new 表達式建立的動态配置設定對象。 當auto_ptr 對象的生命期結束時動态配置設定的對象被自動釋放。 定義pstr_auto 時它知道自己對初始化字元串擁有所有權, 并且有責任删除該字元串。這是所有權授予auto_ptr 對象的責任。相關的頭檔案:#include

string *pstr_type = new string( “Brontosaurus” ); 等價于:auto_ptr< string > pstr_auto( new string( “Brontosaurus” ) );

兩個指針都持有程式空閑存儲區内的字元串位址,我們必須小心地将delete 表達式隻應用在一個指針上。即有兩個指針指向同一個由new創造的位址,用delete釋放一次就可以了。

當一個auto_ptr 對象被用另一個auto_ptr 對象初始化或指派時,左邊被指派或初始化的對象就擁有了空閑存儲區内底層對象的所有權,而右邊的auto_ptr 對象則撤消所有責任。

auto_ptr< int > p_auto_int; 因為p_auto_int 沒有被初始化指向一個對象。是以它的内部指針值被設定為0,這意味着對它解除引用會使程式出現未定義的行為。

操作get()傳回auto_ptr 對象内部的底層指針。是以為了判斷auto_ptr 對象是否指向一個對象我們可以如下程式設計

if ( p_auto_int.get() != 0 &&*p_auto_int != 1024 ) *p_auto_int = 1024;

如果它沒有指向一個對象,那麼怎樣使其指向一個呢——即怎樣設定一個auto_ptr 對象的底層指針。我們可以用reset()操作例如

else // ok, 讓我們設定 p_auto_int 的底層指針

p_auto_int.reset( new int( 1024 ) );

auto_ptr< string >pstr_auto( new string( “Brontosaurus” ) );

// 在重置之前删除對象 Brontosaurus:

pstr_auto.reset( new string( “Long -neck” ) );

//在這種情況下用字元串操作assign()對原有的字元串對象重新指派比删除原有的字元率對象并重新配置設定第二個字元串對象更為有效:

pstr_auto->assign( “Long-neck” );

不能用一個指向記憶體不是通過應用new 表達式配置設定的指針來初始化或指派auto_ptr。

release()不僅像get()操作一樣傳回底層對象的位址而且還釋放這對象的所有權:auto_ptr< string > pstr_auto2( pstr_auto.release() );

// 配置設定單個int 型的對象,用 1024 初始化

int *pi = new int( 1024 );

// 配置設定一個含有1024 個元素的數組,未被初始化

int *pia = new int[ 1024 ];

動态數組的建立:

對于用new 表達式配置設定的數組隻有第一維可以用運作時刻計算的表達式來指定,其他維必須是在編譯時刻已知的常量值。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    puts("please enter height, width, and length:"); 
    int height=10,width=10,length=10;
    scanf("%d%d%d",&height,&width,&length);
    int ***a=new int**[height];
    for(int i=0;i<height;i++){
        a[i]=new int *[width];
    }    
    for(int i=0;i<height;i++){
        for(int j=0;j<width;j++){
            a[i][j]=new int [length];
        }
    }
    for(int i=0;i<height;i++){
        for(int j=0;j<width;j++){
            for(int k=0;k<length;k++){
                a[i][j][k]=i*100+j*10+k;
                printf("%4d",a[i][j][k]);
            }
        }
        puts("");
    }
}       

用來釋放數組的delete 表達式形式如下 delete [] str1;

在空閑存儲區建立的const 對象有一些特殊的屬性:首先const 對象必須被初始化。如果省略了括号中的初始值就會産生編譯錯誤。

const int p; // [Error] uninitialized const ‘p’ [-fpermissive]

如果要在局部域中通路全局聲明的變量(局部中有一樣名字的變量)我們必須使用域操作符:: 例如:

const int m=12;
int cmp(int m){
   m=::m+m;
   return m;
}
int main(int argc, char *argv[]) {
   printf("%d\n",cmp(10));
}       

使用者聲明的名字空間可以包含嵌套的名字空間.

名字空間的定義可以是不連續的可以跨越多個檔案。是以一個名字空間可以在多個檔案中被聲明

// —– SortLib.C —–

namespace {

void swap( double d1, double *d2 ) { / … */ }

}

函數swap()隻在檔案SortLib.C 中可見,如果另一個檔案也含有一個帶有函數swap()定義的未命名名字空間,則該定義引入的是一個不同的函數函數。swap()存在兩種定義但這并不是個錯誤。

因為它們是不同的未命名的名字空間的定義,局部于一個特定的檔案不能跨越多個文本檔案。

自定義命名空間

跨越多個檔案:

a.h:

namespace a{
    void print(){
        printf("here is hero_a\n");  
    } 
}       

b.h:

namespace b{
    void print(){
        printf("here is hero_b\n"); 
    } 
}       

mian.cpp:

#include <stdio.h>
#include <stdlib.h>
#include "a.h"
#include "b.h" 
int main(int argc, char *argv[]) {
    a::print(); 
    b::print(); 
} 
/*
here is hero_a
here is hero_b
*/      

static:

被static修飾的是靜态變量。

靜态局部變量儲存在全局資料區,而不是儲存在棧中

int get(){
    static int g;
    g++;
    return g;
}
int main()
{
    cout<<get()<<endl;
    cout<<get()<<endl;
    cout<<get()<<endl;
    return 0;
}
/*
1
2
3
*/      

靜态全局變量

未經初始化的靜态全局變量會被程式自動初始化為0;

靜态全局變量在聲明它的整個檔案都是可見的,而在檔案之外是不可見的(無法多檔案傳輸);

名字空間别名可以用來把一個較短的同義詞與一個名字空間名關聯起來,例如:namespace International_Business_Machines

{ /* … */ }

namespace IBM = International_Business_Machines;

using 聲明同其他聲明的行為一樣它有一個域,它引入的名字從該聲明開始直到其所在的域結束都是可見的。

using聲明:using cplusplus_primer::matrix;

using訓示符:using namespace cplusplus_primer;

九 重載函數

重載

如果兩個函數的參數表中參數的個數或類型不同, 則認為這兩個函數是重載的.

如果兩個函數的傳回類型和參數表精确比對則第二個聲明被視為第一個的重複聲明.

如果兩個函數的參數表相同但是傳回類型不同, 則第二個聲明被視為第一個的錯誤重複聲明會被标記為編譯錯誤。

當一個參數類型是const 或volatile 時,在識别函數聲明是否相同時,并不考慮const 和volatile 修飾符。

1. 聲明同一函數

void f( int ); void f( const int ); //參數設為const可以防止參數的值改變。

2. 聲明了不同的函數

void f( int* ); void f( const int* ); //const對于指針與引用有影響。

使用者不能在using 聲明中為一個函數指定參數表: using libs_R_us::print( double );  // 錯誤

using 訓示符使名字空間成員就像在名字空間之外被聲明的一樣.

指向重載函數的指針:

extern void ff( unsigned int );
void ( *pf1 )( unsigned int ) = &ff;
extern "C" //聲明的函數告訴編譯器該函數以C語言的方式編譯連結。      

重載函數集的合适體的選擇:

候選函數–可行函數–最佳比對函。

可行函數的參數個數與調用的實參表中的參數數目相同,或者可行函數的參數個數多一些,但是每個多出來的參數都要有相關的預設實參。

可能的轉換被分成三組:提升promotion, 标準轉換standard conversion, 和使用者定義的轉換user defined conversions。

使用者定義的轉換由轉換函數conversion function 來執行。它是類的一個成員函數允許一個類定義自己的标準轉換。

函數轉換被劃分等級如下:精确比對比提升好,提升比标準轉換好,标準轉換比使用者定義的轉換好。

五種标準轉換

1 整值類型轉換:從任何整值類型或枚舉類型向其他整值類型的轉換(不包括前面提升部分中列出的轉換)

2 浮點轉換:從任何浮點類型到其他浮點類型的轉換(不包括前面提升部分中列出的轉換)

3 浮點—整值轉換:從任何浮點類型到任何整值類型或從任何整值類型到任何浮點類型的轉換

4 指針轉換:整數值0 到指針類型的轉換和任何類型的指針到類型void*的轉換

5 bool 轉換:從任何整值類型浮點類型枚舉類型或指針類型到bool 型的轉換

所有的标準轉換都被視為是等價的。例如從char 到unsigned char 的轉換并不比從char到double 的轉換優先級高,類型之間的接近程度不被考慮,即如果有兩個可行函數要求對實參進行标準轉換以便比對各自參數的類型則該調用就是二義的。将被标記為編譯錯誤。

函數指針不能用标準轉換轉換成類型void*。

0 可以被轉換成任何指針類型。

實參是該引用的有效初始值(類型一樣)則該實參是精确比對,如果該實參不是引用的有效初始值則不比對。

十 函數模版 (續讀)

這一部分的内容沒有讀最重要的部分

函數模闆提供一個用來自動生成各種類型函數執行個體的算法。

用預處理器的宏擴充設施,例如#define min(a,b) ((a) < (b) ? (a) : (b)),可以避免為所有的不同類型資料都設計一樣算法的函數。

函數模闆提供了一種機制,通過它我們可以保留函數定義和函數調用的語義。

在一個程式位置上封裝了一段代碼,確定在函數調用之前實參隻被計算一次而無需像宏方案那樣繞過C++的強類型檢查。

min函數的模版:

template <class Type> //Type可以換成任何其他的自定義名字
Type min( Type a, Type b ) {
return a < b ? a : b;
}
template <class Type, int size> //模闆非類型參數由一個普通的參數聲明構成
Type min( Type (&arr) [size] );      

十一 異常處理

throw 表達式可以抛出任何類型的對象, 必須定義可以被抛出的異常, 在C++中異常往往用類class 來實作。

try 塊(try block) 必須包圍能夠抛出異常的語句,try 塊以關鍵字try 開始,後面是花括号括起來的語句序列。在try 塊之後是一組處理代碼,被稱為catch 子句。

當某條語句抛出異常時跟在該語句後面的語句将被跳過程式, 執行權被轉交給處理異常的catch子句。

在main()的函數體中聲明的變量不能在catch 子句中被引用。

一個catch 子句由三部分構成:關鍵字catch,在括号中的異常聲明exception declaration,以及複合語句中的一組語句。

在查找用來處理被抛出異常的catch 子句時因為異常而退出複合語句和函數定義,這個過程被稱作棧展開stack unwinding。

異常對于一個程式非常重要,它表示程式不能夠繼續正常執行,如果沒有找到處理代碼程式就調用C++标準庫中定義的函數terminate()。terminate()的預設行為是調用abort(),訓示從程式非正常退出。

在異常處理過程中也可能存在單個catch 子句不能完全處理異常的情況,在某些修正動作之後catch 子句可能決定該異常必須由函數調用鍊中更上級的函數來處理,那麼catch子句可以通過重新抛出rethrow 該異常。

catch ( exception eObj ) {
if ( canHandle( eObj ) )
// 處理異常
return;
else
// 重新抛出它, 并由另一個catch 子句來處理
throw;
}
void calculate( int op ) {
try {
// 被 mathFunc() 抛出的異常的值為 zeroOp
mathFunc( op );
}
//為了修改原來的異常對象catch 子句中的異常聲明必須被聲明為引用例如
cacth ( EHstate &eObj ) {
// 修改異常對象
eObj = severeErr;
// 被重新抛出的異常的值是 severeErr
throw;
}      

十二 泛型算法

泛型算法

操作在多種容器類型上的算法。

比如find()函數,iterator疊代器,sort()函數。

————————————————————————————————————————

int數組的find()算法:

#include <algorithm>
#include <iostream>
using namespace std;

int main()
{
    int search_value;
    int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
    cout << "enter search value: ";
    cin >> search_value;
    int *presult = find( &ia[0], &ia[6], search_value );
    cout << "The value " << search_value<< ( presult == &ia[6]? " is not present" : " is present" )<< endl;
    return 0;
}      

————————————————————————————————————————-

vector的find()算法:

#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    int search_value;
    int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
    vector<int> vec( ia, ia+6 );
    cout << "enter search value: ";
    cin >> search_value;
    vector<int>::iterator presult;
    presult = find( vec.begin(), vec.end(), search_value );
    cout << "The value " << search_value<< ( presult == vec.end()? " is not present" : " is present" )
    << endl;
    return 0;
}      

——————————————————————————————————————————-

list的find()算法

#include <algorithm>
#include <list>
#include <iostream>
using namespace std;

int main()
{
    int search_value;
    int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
    list<int> ilist( ia, ia+6 );
    cout << "enter search value: ";
    cin >> search_value;
    list<int>::iterator presult;
    presult = find( ilist.begin(), ilist.end(), search_value );
    cout << "The value " << search_value
    << ( presult == ilist.end()
    ? " is not present" : " is present" )
    << endl;
    return 0;
}      

對容器的周遊:iterator. 指針的泛化。

預定義函數對象被分成算術關系和邏輯操作, 每個對象都是一個類模闆, 其中操作數的類型被參數化. 為了使用它們我們必須包含#include

以降序排列容器:

vector< string > svec;

// …

sort( svec.begin(), svec.end(), greater()); // 預定義的類模闆greater 它調用底層元素類型的大于操作符

const 容器隻能被綁定在const iterator 上這樣的要求與const 指針隻能指向const 數組的行為一樣在兩種情況下C++語言都努力保證const 容器的内容不會被改變

為支援泛型算法全集根據它們提供的操作集标準庫定義了

五種iterator

InputIterator, OutputIterator, ForwardIterator, BldirectionalIterator 和RandomAccessIterator

InputIterator 可以被用來讀取容器中的元素, 但是不保證支援向容器的寫入操作

OutputIterator 可以被用來向容器寫入元素, 但是不保證支援讀取容器的内容

ForwardIterator 可以被用來以某一個周遊方向向容器讀或寫

BidirectionalIterator 從兩個方向讀或寫一個容器

RandomAccessIterator 除了支援BidirectionalIterator 所有的功能之外還提供了在常數時間内通路容器的任意位置的支援

元素範圍概念有時稱為左閉合區間通常寫為[ first, last ) // 讀作: 包含 first 以及到 但不包含 last 的所有元素

四個算術算法

#include

adjacent_difference()、 accumulate()、 inner_product()和partial_sum()

查找算法

equal_range()、lower_bound()和upper_bound() (二分查找實作)

int main()
{
    int a[6]={4,10,13,14,19,26};
    int *val=lower_bound(a,a+6,12);  //first value  >= goal
    cout<<*val<<endl;
    val=upper_bound(a,a+6,19);  // first one > goal
    cout<<*val<<endl;
    return 0;
}
/*
13
26
*/      

函數equal_range()傳回first和last之間等于val的元素區間.傳回值是一對疊代器。

排列組合算法

int A[3]={1,2,3};
void show(){
    for(int i=0;i<3;i++){
        printf("%d ",A[i]);
    }
    cout<<endl;
}
int main(){
    int sum=0;
    do{
        show();
        sum++;
    }while(next_permutation(A,A+3));
    cout<<"sum= "<<sum<<endl;
    return 0;
}
/*
A={1,2,3}
next_permutation():
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
sum= 6
--------------------------
A={3,2,1}
prev_permutation():
3 2 1
3 1 2
2 3 1
2 1 3
1 3 2
1 2 3
sum= 6
*/      

繼續閱讀