1. 程式人生 > >c++基礎(十)

c++基礎(十)

就是 輸入 進行 沒有 作用 分配內存 under 變量作用域 基於

第四部分:指針與函數

指針作為函數參數:使用指針作為參數的原因:1、需要數據的雙向傳遞;2、需要傳遞一組數據,只傳首地址運行效率比較高。

案例:

#include <iostream>
using namespace std;
void splitFloat(float x, int *intPart, float *fracPart) {
   *intPart = static_cast<int>(x); //取x的整數部分
   *fracPart = x - *intPart; //取x的小數部分
}
int main() {
  cout << "Enter 3 float point numbers:
" << endl; for(int i = 0; i < 3; i++) { float x, f; int n; cin >> x; splitFloat(x, &n, &f); //變量地址作為實參 cout << "Integer Part = " << n << " Fraction Part = " << f << endl; } return 0; }

運行結果:

技術分享圖片

註意:浮點數在c++中是近似存儲的,因此,如果要比較兩個浮點數的大小,不能直接使用

“==”而可以通過判斷兩個浮點數的差值是否小於一個足夠小的數來進行判斷,只要這個差值能夠滿足精度,那麽就可以認為這兩個浮點數是相等的。

案例:

#include <iostream>
using namespace std;
const int N = 6;
void print(const int *p, int n);
int main() {
  int array[N];
  for (int i = 0; i < N; i++)
        cin>>array[i];
  print(array, N);
  return 0;
}
void print(const
int *p, int n) { cout << "{ " << *p; for (int i = 1; i < n; i++) cout << ", " << *(p+i); cout << " }" << endl; }

當我們只想訪問指針指向的對象,而不想對該對象做任何修改時,我們可以定義指向常量的指針。通過指針只能讀取指針指向的對象,不能對對象做修改。在程序設計中,一般會有最小授權的原則,比如在定義類的時候,盡量多的能夠隱藏細節,不要過多的授權。

指針類型的函數:若函數的返回值是指針,該函數就是指針類型的函數。定義語法形式:存儲類型 數據類型 *函數名()

{//函數體語句

}

註意:1、不要將非靜態局部地址作為函數的返回值,因為非靜態的局部變量在函數外就失效了,一個常見的錯誤就是:在子函數中定義局部變量後將其地址返回給主函數,這是非法的地址;

錯誤案例:

int main(){
    int* function();
    int* ptr= function();
    *prt=5; //危險的訪問!
    return 0;
}
int* function(){
    int local=0; //非靜態局部變量作用域和壽命都僅限於本函數體內
    return &local;
}//函數運行結束時,變量local被釋放

因此,返回的指針要確保在主調函數中是有效、合法的地址。例如,可以將在主函數定義的數組傳遞給子函數,並在子函數中返回這個數組其中一個元素的地址,這就是合法有效的地址。除此之外,在子函數中通過動態內存分配new操作取得的內存地址是合法有效的,但是內存分配和釋放不在同一個級別,要註意不能忘記釋放,避免內存泄漏。

指向函數的指針:1、定義:存儲類型數據類型(*函數指針名)(參數表);含義:函數指針指向的是程序代碼的存儲區的起始地址;2、函數指針的典型用途:①通過函數指針調用的函數:例如將一個函數的指針作為參數傳遞給另外一個函數,使得在處理相似事件的時候可以靈活地使用不同的方法;②調用者不關心誰是被調用者:只需知道存在一個具有特定原型和限制條件的被調用函數;

案例:

#include<iostream>
using namespace std;
int compute(int a, int b, int(*func)(int, int))//func是一個函數指針,指向一個返回值
                                  //為int類型,形參為兩個int類型的參數
{
    return func(a, b);
}
int max(int a, int b)
{
    return (a > b) ? a : b;
}
int min(int a, int b)
{
    return (a < b) ? a : b;
}
int sum(int a, int b)
{
    return a + b;
}
int main()
{
    int a = 5, b = 10;
    cout << "The maximum of A and B is:";
cout << compute(a, b, &max) << endl;//也可不寫地址運算符,函數名就代表函數的首地址
    cout << "The minimum of A and B is:";
    cout << compute(a, b, &min) << endl;
    cout << "The sum of A and B is:";
    cout << compute(a, b, &sum) << endl;
    system("pause");
    return 0;
}

對象指針:指向一個對象的指針,定義形式:類名 *對象指針名通過指針訪問對象成員:對象指針名->成員名,相當於(*對象指針名).成員名。

this指針隱含於類的每一個非靜態成員函數中;並能夠指出成員函數所操作的對象,當通過一個對象調用成員函數時,系統先將該對象的地址賦給this指針,然後調用成員函數,成員函數對對象的數據成員進行操作時,就隱含使用了this指針。例如:Point類的getX函數中的語句:return x;相當於:return this->x。

第五部分:對象的復制與移動

Vector對象:為什麽需要vector?之前我們講過可以將動態分配內存與釋放內存封裝在一個類裏面,但是這個類只能封裝一種類型的動態數組。Vector可以封裝任何類型的動態數組,自動創建和刪除,並且能夠進行數組下標越界檢查。定義語法:vector<元素類型>數組對象名(數組長度)。對數組對象的引用:vector對象名[下標表達式];獲得數組長度:vector對象名.size()。

案例:

#include<iostream>
#include<vector>
using namespace std;
double average(const vector<int> &a);//聲明一個求vector對象數組平均值的函數
int main()
{
    unsigned n=0;
    cout << "Please input the value of n:" << endl;
    cin >> n;
    vector <int>array(n);//使用vector對象來保存數組
    for (unsigned i = 0; i < n; i++)
    {
        cin >> array[i];
    }
    cout << "Average of array is  " << average(array) << endl;
    return 0;
}
double average(const vector<int> &a)
{
    double sum=0;
    for (int v:a)//基於範圍的for循環
    {
        sum += v;
    }
    return sum / a.size();//使用vector對象名.size()函數可以返回數組的長度
}

淺層復制與深層復制:

淺層復制:實現對象數據元素之間的一一對應復制。默認復制構造函數是淺拷貝。

深層復制:當被復制的對象數據成員是指針類型時,不是復制該指針成員本身,而是將指針所指的對象進行復制。

案例:

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

class Point {
public:
    Point() : x(0), y(0) {
        cout << "Default Constructor called." << endl;
    }

    Point(int x, int y) : x(x), y(y) {
        cout << "Constructor called." << endl;
    }

    ~Point() { cout << "Destructor called." << endl; }

    int getX() const { return x; }

    int getY() const { return y; }

    void move(int newX, int newY) {
        x = newX;
        y = newY;
    }
private:
    int x, y;
};

//動態數組類
class ArrayOfPoints {
public:
    ArrayOfPoints(int size) : size(size) {
        points = new Point[size];
    }

    ArrayOfPoints(const ArrayOfPoints& v);//復制構造函數

    ~ArrayOfPoints() {
        cout << "Deleting..." << endl;
        delete[] points;
    }

    //獲得下標為index的數組元素
    Point &element(int index) {
        assert(index >= 0 && index < size);    //如果數組下標不會越界,程序中止
        return points[index];
    }
private:
    Point *points;    //指向動態數組首地址
    int size;        //數組大小
};
//深層復制
ArrayOfPoints::ArrayOfPoints(const ArrayOfPoints& v) {
    size = v.size;
    points = new Point[size];
    for (int i = 0; i < size; i++)
        points[i] = v.points[i];
}

int main() {
    int count;
    cout << "Please enter the count of points: ";
    cin >> count;
    ArrayOfPoints pointsArray1(count);        //創建對象數組
    pointsArray1.element(0).move(5, 10);
    pointsArray1.element(1).move(15, 20);

    ArrayOfPoints pointsArray2 = pointsArray1; //創建對象數組副本
    cout << "Copy of pointsArray1:" << endl;
    cout << "Point_0 of array2: " << pointsArray2.element(0).getX() << ", "
        << pointsArray2.element(0).getY() << endl;
    cout << "Point_1 of array2: " << pointsArray2.element(1).getX() << ", "
        << pointsArray2.element(1).getY() << endl;

    pointsArray1.element(0).move(25, 30);
    pointsArray1.element(1).move(35, 40);
    cout << "After the moving of pointsArray1:" << endl;
    cout << "Point_0 of array2: " << pointsArray2.element(0).getX() << ", "
        << pointsArray2.element(0).getY() << endl;
    cout << "Point_1 of array2: " << pointsArray2.element(1).getX() << ", "
        << pointsArray2.element(1).getY() << endl;

    return 0;
}

在這個例子中,我們使用了復制構造函數來進行動態數組的復制。用圖表示該過程:

技術分享圖片

復制之後,數組2與數組1並沒有指向同一塊內存空間,而是指向了不同的內存空間。可見,此時不僅僅是將數組一中的成員一一復制過來,而是將數組中指針指向的對象都復制過來了。如果我們不寫復制構造函數,那麽編譯器會調用默認復制構造函數,進行淺層復制,那麽就是將數組1中的指針復制過來,數組1和數組2占用的是同一塊內存空間。在本例中,淺層復制可以用下圖表示:

技術分享圖片

移動構造

我們先來看一個案例,代碼如下:

#include<iostream>
using namespace std;
class IntNum {
public:
    IntNum(int x = 0) : xptr(new int(x)){ //構造函數
        cout << "Calling constructor..." << endl;
    }
    IntNum(const IntNum & n) : xptr(new int(*n.xptr)){//復制構造函數
        cout << "Calling copy constructor..." << endl;
    };
    ~IntNum(){ //析構函數
        delete xptr;
        cout << "Destructing..." << endl;
    }
    int getInt() { return *xptr; }
private:
    int *xptr;
};
//返回值為IntNum類對象
IntNum getNum() {
    IntNum a;
    return a;
}
int main() {
    cout << getNum().getInt() << endl;
    return 0;
}

運行結果:

技術分享圖片

在程序的執行過程中,先是構建了一個IntNum類臨時對象,然後當需要將a返回到主調函數中時,函數調用了復制構造函數,將臨時對象復制一份給a,然後返回a,並將該臨時對象析構。觀察這個過程,不難發現,我們完全可以把臨時對象的資源直接移動,這樣就避免了多余的復制操作,也避免了多余的析構操作。如下圖所示:

技術分享圖片

我們可以通過移動構造函數來實現這一操作:就是讓這個臨時對象它原本控制的內存的空間轉移給構造出來的對象,這樣就相當於把它移動過去了。

復制構造和移動構造的差別:這種情況下,我們覺得這個臨時對象完成了復制構造後,就不需要它了,我們就沒有必要去首先產生一個副本,然後析構這個臨時對象,這樣費兩遍事,又占用內存空間,索性將臨時對象它的原本的資源直接轉給構造的對象即可了。

當臨時對象在被復制後,就不再被利用了。我們完全可以把臨時對象的資源直接移動,這樣就避免了多余的復制構造。那什麽時候該觸發移動構造呢?

如果臨時對象即將消亡,並且它裏面的資源是需要被再利用的,這個時候我們就可以觸發移動構造。

移動構造函數定義形式:class_name(class_name && )

&&符號表示右值引用,右值是指即將消亡的值,函數返回的臨時變量就是右值。

上述案例使用移動構造函數來實現:

#include<iostream>
using namespace std;
class IntNum {
public:
    IntNum(int x = 0) : xptr(new int(x)){ //構造函數
        cout << "Calling constructor..." << endl;
    }
    IntNum(const IntNum & n) : xptr(new int(*n.xptr)){//復制構造函數
        cout << "Calling copy constructor..." << endl;
    }
    //使用即將消亡的對象n的指針來初始化指針,然後將n的指針置為空指針
    IntNum(IntNum && n) : xptr(n.xptr){ //移動構造函數
        n.xptr = nullptr;//
        cout << "Calling move constructor..." << endl;
    }
    int getInt() { return *xptr; }
    ~IntNum(){ //析構函數
        delete xptr;
        cout << "Destructing..." << endl;
    }
private:
    int *xptr;
};
//返回值為IntNum類對象
IntNum getNum() 
{
    IntNum a;
    return a;
}
int main() {
    cout << getNum().getInt() << endl; return 0;
}

結果:

技術分享圖片

在本例中移動構造函數地代碼為:

IntNum(IntNum && n) : xptr(n.xptr) //移動構造函數
{
    n.xptr = nullptr;
    cout << "Calling move constructor..." << endl;
}

看函數體裏面,我們發現在做完xptr(n.xptr)這種指針對指針的復制(也就是把參數指針所指向的對象轉給了當前正在被構造的指針)後,接著就把參數n裏面的指針置為空指針(n.xptr = nullptr;),對象裏面的指針置為空指針後,將來析構函數析構該指針(delete xpr;)時,是delete一個空指針,不發生任何事情,這就是一個移動構造函數。

移動構造函數中的參數類型,&&符號表示是右值引用;即將消亡的值就是右值,函數返回的臨時變量也是右值,單個這樣的引用可以綁定到左值的,而這個引用它可以綁定到即將消亡的對象,綁定到右值。

左值和右值都是針對表達式而言的,左值是指表達式結束後依然存在的持久對象,右值指表達式結束時就不再存在的臨時對象——顯然右值不可以被取地址。

C++字符串

在C++中,並沒有字符串變量,所以一般可以用字符數組來存儲字符串,例如char str[8]=”program”,或者char str[8]={“p”,”r”,”o”,”g”,”r”,”a”,”m”,”\0”},用字符數組來存儲字符串時,數組的末尾一定是”\0”,否則就不是一個字符串。使用字符數組表示字符串的缺點:①執行連接、拷貝等操作時,需要顯示調用庫函數;②當字符串長度不確定時,需要使用new來進行動態內存分配,不方便;③當內存給數組分配的空間小於數組的長度時,會出現越界的問題。因此,我們推薦在c++中使用string類來操作字符數組。

String類常用的構造函數包括如下幾類:默認構造函數、復制構造函數和用指針s所指向的字符串常量初始化string對象。String對象提供了許多可直接調用的函數。

如何輸入整行字符串:getline(cin,s2)。輸入字符串時,可以使用其他分隔符作為字符串結束的標誌,將分隔符作為getline函數的第三個參數即可,例如:getline(cin,str,’,’)。

c++基礎(十)