首頁 > 軟體

C++掃盲篇之指標詳解

2022-03-25 19:00:34

前言

指標對於學習C/C++的人來說是一道必須邁過去的坎,就像學習九陽神功必須要打通任督二脈一樣的道理。雖然說隨著智慧指標的普及,很少需要程式設計師再手動操作原始指標, 但是如果你連原始指標的都沒學好,那你怎麼可能用好智慧指標呢?

無論是原始指標還是智慧指標,要想用好它就一定要做到知其然,知其所以然。

因為本文閱讀物件是有了一定指標基礎的童鞋,所以如果你對指標如果是處於一無所知的狀態的話,建議先去溫習下指標的基礎知識,不然可能讀起來會打擊你求知的慾望。

指標為什麼要有型別

是為了指標運算和取值。

當使用指標取值的時候需要知道怎麼取值,比如按照多少個位元組去取值,這是需要確定才能取到正確的值的,要知道用多少個位元組去取就得知道指標的型別是什麼。

我們知道指標的運算增加或者減少1意味著需要偏移指標所表示的型別的大小個位元組數,比如說一個int位元組的指標增加1,表示偏移4個位元組(一般情況下int都是4個位元組),所以這也是需要知道指標的型別。

指標和陣列

本來從字面上來說指標和陣列是八竿子打不著的,它們理應是井水不犯河水的,怎麼就扯上了呢?我們經常聽說陣列指標、指標陣列,這些都是什麼意思呢?他們到底是指標還是陣列呢?下面將一一為你解答。

指標陣列,首先它是一個陣列,陣列裡面的每個元素都是一個指標,例如比如int *p[4] 就是一個指標陣列,因為運運算元[]的優先順序運運算元*的優先順序高,所以p優先和[]組成陣列,然後*和型別int組合成陣列元素的型別。 例如以下程式就是一個指標陣列的範例:

main.c
#include <stdio.h>
int main(){
    char *str[3] = {
        "我是陣列1",
        "我是陣列2",
        "我是陣列3"
    };
    printf("%sn%sn%sn", str[0], str[1], str[2]);
    return 0;
}

陣列指標,首先它是一個指標,這個指標所指向的物件是陣列,比如這個指標是p,那麼通過解除參照*p獲得內容就是一個陣列,例如int (*p)[4],主意帶上括號, 通常陣列指標也作為一個二維陣列來使用。

二級指標

所謂的二級指標其實就是一個指向指標的指標,例如int **p就是一個二級指標,它內部存放的物件是一個指標,通過一次解除參照獲得的是記憶體存放的指標的地址,需要再次對這個內部的指標進行解除參照才能獲取到 這個真是內容的值。

理解起來有點繞,那麼這個拗口的二級指標有什麼作用呢?二級指標在C++中可能用的不多,但是在C中是經常使用的一把利器,它通常作為一個函數的引數,起到在函數內部對一個指標進行初始化的作用, 比如經典的音視訊處理工具FFmpeg中就大量使用了二級指標。 以下例子展示如何通過二級指標對指標形式賦值:

main.cpp
void initP(int **p){
    *p = new int(10);
}

int main() {
    int *p = nullptr; // 一個空的指標
    initP(&p); // 通過二級指標初始化指標p
    std::cout << "*p的值:" << *p << endl;
    delete p;
    return 0;
}

可能在這裡就有人和當初筆者剛接觸C語言一樣迷惑了,難道不能通過給函數傳遞一級指標給指標初始化嗎?這是不行的,這是因為值傳遞的緣故,像深入探討的童鞋們可以寫個例子列印下實參的具體地址對比下研究下其背後的原理。

指標與多型繫結

我們都知道C++是一門物件導向的設計語言,支援多型就是它的一個重要特性之一,在學習C++類別的相關知識的時候老師就告訴我們:*在C++語言中,當我們使用基礎類別的參照(或指標)呼叫一個虛擬函式時將發生動態繫結。*也就是 說使用通過父類別的指標或參照就能按照實參的實際型別是父類別還是子類呼叫不同的虛擬函式。

例如如以下程式碼:

main.cpp
class Base{
public:
    virtual void print() const{
        std::cout << "base print" << endl;
    }
    virtual ~Base(){
    }
};

class Child:public Base{
public:
    void print() const override{
        std::cout << "Child print" << endl;
    }
};

void testPrint(const Base &base){
    base.print();
}

int main() {
    Base a = Child();
    testPrint(a);// 列印Base print
    Child b = Child(); // 注意,不能寫成Base b = Child(),否則列印的是Base的print
    testPrint(b); // 列印Child print
    Base *c = new Child(); // 指標,動態型別與靜態型別不一致
    testPrint(*c); // 列印Child print

    Base &&r = Child(); // 表示式是右值參照,動態型別與靜態型別不一致
    testPrint(r); // 列印Child print
    return 0;
}

為什麼在上面的程式中變數a的實際型別是Child,但是函數testPrint內部呼叫的卻是父類別的列印方法呢?不是說參照會觸發多型嗎?函數testPrint也是通過參照傳遞的呀, 真是百思不得其jie呀。

要解開這個疑惑就得了解下靜態型別和動態型別的知識了。靜態型別在編譯時總是已知的,首先靜態型別是變數宣告時的型別或表示式生成的型別;動態型別則是變數或表示式表示的記憶體中的物件的型別,動態型別直到執行時才可知。 如果變數在定義時表示式既不是參照也不是指標,則它的動態型別永遠與靜態型別一致的,也就是宣告時所指的型別,否則的話靜態型別可能與動態型別不一致。

那麼有了靜態型別與動態型別的概念之後再結合註釋看上面的範例程式碼是不是就有一種撥開雲霧見青天的感覺了呢?

函數指標

函數指標顧名思義就是指向函數的指標,它的定義:函數指標是指向函數的指標變數。因此“函數指標”本身首先應是指標變數,只不過該指標變數指向函數。

其宣告方式是:

返回值型別 (*函數名) (引數)  

函數指標的一個重要用途就是作為函數的引數,用於在函數內部進行指標函數的呼叫,一般用作回撥函數,比如在建立一個POSIX執行緒的就需要傳遞一個函數指標用於指明該執行緒做點什麼事情。

以下程式碼展示了一個簡單的函數指標的使用方法:

void testFunc(int a,int b,void (*func)(int c,int d) ){
    // do something
    func(a,b);
}

void callback(int a,int b){
}

int main() {
    testFunc(1,2,callback);
    return 0;
}

類成員指標

這裡類成員指標表示的是指向類的某個物件的非靜態成員的指標,而不是表示類成員變數的指標,首先需要區分好這是兩個不同的概念,如果不能好好區分這兩個概念的童鞋,需要再好好思考一下。

成員指標的型別囊括了類的型別以及成員的型別。當初始化一個這樣的指標時,我們令其指向類的某個成員,但是不指定該成員所屬的物件;直到使用成員指標時,才提供成員所屬的物件。

和其他指標一樣,在宣告成員指標時我們也使用*來表示當前宣告的名字是一個指標。與普通指標不同的是,成員指標還必須包含成員所屬的類。下面是一個使用的範例:

class Person{
public:
    virtual void print() const{
        std::cout << "base print" << endl;
    }

    virtual ~Person(){
    }

public:
    string lastName;
    string firstName;
};

int main() {
    string Person::*p; // 宣告了一個類成員指標
    p = &Person::firstName; // 成員變數的指標指向了Peron的name
    Person person;
    person.*p = "hello"; // 使用成員指標
    p = &Person::lastName; // 成員變數的指標指向了Peron的lastName
    person.*p = "world"; // 使用成員指標
    std::cout << person.firstName << " " << person.lastName << std::endl;
    return 0;
}

至於這個成員指標有什麼用處,給筆者的感覺就是重新命了一個別名的感覺,甚至有點脫褲子放屁?但是存在即合理,不是沒有用處,只是筆者見過那種場景而已吧。。。

除了有指向類成員變數的指標外還有指向類成員函數的指標,這裡就不多展開探討了!!!

補充:用指標的指標指向指標陣列

#include<stdio.h> 
int change(char **p)
{
	int i, j;
	for (i = 0; i < 5; i++)
	{
		for (j = 0; *(*(p + i) + j) != ''; j++)//利用指標的指標取二維陣列的元素
		{
			*(*(p + i) + j) = 'c';
			printf("%c", *(*(p + i) + j));
		}
		printf("n");
	}
	return 0;
}
 
int main(void)
{
	char *a[5] = { "hello", "zhuyu", "jiajia", "linux","Ubuntu" };//如果想使用 需使用指標陣列即*a[5] 宣告一個有五個字串指標的陣列。
	                                                              //但是由於每個元素都是指標字串,所以只能夠讀取,而不能夠寫入。
	change(a);
	return 0;
}

總結

到此這篇關於C++掃盲篇之指標的文章就介紹到這了,更多相關C++指標掃盲內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com