2021-07-14:C++基礎知識03

C++基礎知識03

Section01:指向基本數據類型的指針

曾有人這麼談到計算機內存:「內存就像是一羣連續的房子,每一個房子能夠存儲一些內容,每一個房子也有本身的門牌號,在必要的時候咱們能夠經過門牌號去找到對應的房子訪問須要的內容」。這個說法是否準確呢?咱們先來看看內存模型:ios

Section-1.1:內存模型

計算機中存儲的最小的單位是bit,它的值只有0、1。可是這麼小的位也不能表示不少信息,因而有了更普通的一種存儲單位,就是字節byte,一般 1byte=8bit,能夠表示無符號數[0,255];也能夠表示有符號數[-128,127]。c++

如今,存放信息的「房子」咱們明白了,那麼如何去找尋房子呢?在硬件層面,咱們能夠用地址來找尋信息,任何變量、對象都有存儲它的內存地址!至因而這塊地址的左端仍是右端,就不是硬件層面說了算了,是由編譯器決定的!程序員

譬如:面試

int a = 5;
double d = 3.14;
int* pa = &a;
double* pd = &d;

程序員很難記住地址(畢竟一個現代的地址有8位),爲了簡便,咱們用變量名來代替地址,能夠認爲是 變量名是地址的別名編程

對於普通變量的地址是在聲明的時候就肯定了,隨後的賦值,無論對象/變量如何變化,地址都是同樣的!數組

而指針,其實就能夠理解爲一種地址!在初始化、賦值的時候,給普通變量賦予的是值,也就是對象值!可是,指針,在初始化和賦值的時候,直接賦予的是地址,表示它接下來的地址!ide

示例程序以下:函數

#include <stdio.h>

int main() {
    int a = 10;
    printf("%x\n", &a);
    a = 100;
    printf("%x\n", &a);
    int b = 5;
    printf("%x\n", &b);
    b = a;
    printf("%x\n", &b);
    int c = b;
    printf("%x\n", &c);
    printf("\n--------------pointer----------------\n");
    int* pa = &a;
    printf("%x\n", pa);
    pa = &b;
    printf("%x\n", pa);
    int* pb = pa;
    printf("%x\n", pb);
    return 0;
}

運行上述程序,會看到運行結果的表現是:學習

前三個普通基本數據類型變量a、b、c的地址不會改變!而指針在初始化後的地址是指向變量的地址!但當指針再次賦值後,則地址會發生改變,會與右值同時指向同一個地址!測試

好比個人輸出結果:

61fe0c
61fe0c
61fe08
61fe08
61fe04
--------------pointer----------------
61fe0c
61fe08
61fe08

Section-1.2:指針與指針的訪問

分析下面程序的內存模型:

#include <stdio.h>

int main() {
    int a = 112, b = -1;
    double c = 3.14;
    int* pa = &a;
    double* pc = &c;
    return 0;
}

模型圖爲:
在這裏插入圖片描述

因而能夠獲得兩個結論:

結論1 :指針的聲明與初始化方法

Type* ptr = &Var;

結論2 :指針與普通變量的賦值

任何變量的值,都是存儲在分配的內存地址上的值,指針變量也不例外!

什麼意思呢?咱們來看一下程序輸出:

#include <stdio.h>

int main() {
    int a = 112, b = -1;
    double c = 3.14;
    int* pa = &a;
    double* pc = &c;
    printf("%d %d %d", &pa, pa, *pa);
    return 0;
}

它輸出的結果是:

6422008 6422028 112

這說明了什麼???

更具體的結論:

指針變量其實也有本身的地址,不能是指針變量就是地址,而是指針變量的值是地址!那麼輸出的內容112是什麼呢?實際上是:指針變量所指向的地址的值!並非指針變量的值!指針變量的值是地址!!!

用更細緻的圖來描述:

在這裏插入圖片描述

pa指針變量的地址是6422008存儲的變量是指向對象的地址6422028,而後所指向地址的值是a的值也就是*pa,是112。

Section-1.3:未初始化的指針與非法指針

典型錯誤1:未初始化的指針

指針若是沒有初始化,那麼該指針只有本身的地址,並無一個能容納對象/變量/值的內存空間!例如錯誤範例程序:

int* pa;
*pa = 100;

雖然這種異常,在新標準的C/C++下修復了,可是仍然是一個潛在的問題,不能使用!

典型錯誤2:忽略了指針的值是地址

先看錯誤的範例程序,再看有啥問題:

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

int main() {
    int a = 112;
    int* pa = (int*)malloc(sizeof(int*));
    *pa = a;
    printf("%d %d\n", *pa, a);
    *pa = 200;
    printf("%d %d\n", *pa, a);
    return 0;
}

運行的結果是:

112 112
200 112

總結:必需要讓指針指向某一個對象的地址,才能起到指針的做用,不然只是一個臨時的值拷貝!

典型錯誤3:空指針的使用

不建議使用NULL,有的編譯器源碼規定NULL是0也有的不是,爲了不指針與整型的差異,從C++11開始就用nullptr替代NULL了!而空指針、野指針也會帶來許多危害,例如範例程序:

#include <iostream>
using namespace std;

int main() {
    int* ptr = new int(20);
    delete ptr;
    ptr = nullptr;
    return 0;
}

開始調試,會發現,在delete指針後,雖然對象被delete了,可是ptr仍是垂懸在內存的,因此須要置空,給你們可靠垂懸指針的樣子:
在這裏插入圖片描述
再看看置空後的樣子:

在這裏插入圖片描述

Section02:指針編程訓練

訓練1:實現字符串求長度函數strlen

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::cin;

size_t strlen(const char* pstr) {
    size_t length = 0;
    while (*pstr++)
        length++;
    return length;
}

int main() {
    std::string s;
    cin >> s;
    cout << strlen(s.data()) << endl;
    cout << s[0] << endl;
    return 0;
}

測試的輸入是:

Hello

輸出是:

5
H

總結本題:

這裏呢,strlen函數的傳參,傳參方式實際上是值傳遞,由於傳的自己就是值而不是指針!總的來講,要實現指針傳遞,就必須傳當前實參的地址!而當前實參若是是指針就必須傳指針的地址也就是說,須要形參用指針的指針來接收指針的地址!

訓練2:題目描述以下

在這裏插入圖片描述

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::cin;

char* find_char(char const* source, char const* chars) {
    const int MAX_SIZE = 128;
    char* pRes = new char[MAX_SIZE]();
    char const* pChars = chars;
    char* res = pRes;
    while (*source) {
        while (*pChars) {
            if (*pChars == *source) {
                *pRes++ = *source;
                break;
            }
            ++pChars;
        }
        pChars = chars;
        ++source;
    }
    *pRes = '\0';
    return res;
}

int main() {
    const char* src = "Hello World, my baby!";
    const char* chs = "emb";
    cout << find_char(src, chs) << endl;
    return 0;
}

按照個人測試用例,輸出結果是:

embb

訓練3:題目描述以下

在這裏插入圖片描述

#include <iostream>

using std::endl;
using std::cout;

void reverse_string(char* string) {
    char* pEnd = string;
    char* pStr = string;
    while (*(pEnd + 1) != '\0')
        ++pEnd;
    while (pStr < pEnd) {
        char cTemp = *pEnd;
        *pEnd = *pStr;
        *pStr = cTemp;
        ++pStr, --pEnd;
    }
}

int main() {
    char src[] = "Hello World";
    reverse_string(src);
    cout << src << endl;
    return 0;
}

輸出的結果是:

dlroW olleH

總結《訓練3》

在本題中,要強調和學習字符指針和字符數組的區別!!!

區別1:字符指針不可寫

字符數組是知道大小的,因此你能夠對其中某個元素進行寫操做!而字符指針只是指向一個字符串,並不知道其具體大小,因而不可寫!也就是不能訪問!這就意味着,若是本體你傳入的不是字符數組src[],而是一個指針,那麼就會段錯誤!由於不可寫!

區別2:字符指針有常量池,字符數組是獨立的空間

看下面這個示例:

#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::cin;
 
int main () {
    char* ps1 = (char*)"Hello";
    char* ps2 = (char*)"Hello";
    cout << (ps1 == ps2) << endl;

    char arr1[] = "Hello";
    char arr2[] = "Hello";
    cout << (arr1 == arr2) << endl;
    return 0;
}

輸出結果是:

1
0

這是由於,C/C++中也有字符串常量池,和Java同樣!若是是指針,都會指向常量池的同一個地址!而若是是數組呢?就不會了!由於每個數組都是獨立分配的空間!

訓練4:本身實現一個strcpy(面試題)

#include <iostream>

using std::endl;
using std::cout;

void strcpy(char* dest, const char* src) {
    while ('\0' != *dest)
        dest++;
    while ('\0' != *src)
        *dest++ = *src++;
    *dest = '\0';
}

int main() {
    char dest[] = "Hello";
    char src[] = " World";
    strcpy(dest, src);
    cout << dest << endl;
    return 0;
}

輸出的結果是:

Hello World

Section03:引用初步

什麼是引用呢?其實引用的概念比起指針來講好理解的多!

引用相對於給對象/變量起了一個別名,程序員不管是訪問引用仍是訪問對象/變量,都是起到了同樣的效果!不管是改變了引用仍是改變了對象/變量,另外一方也會隨之改變!引用的定義格式是:

Type& ref = Var;

要注意,容許沒有初始化的指針,可是絕對不容許沒有初始化的引用!爲何呢?你想一想,指針其實本身也是一個變量,它有本身的地址有本身的空間,而指針呢?一旦沒有肯定是誰的別名,它就不可能存在!就像一我的的外號確定是和這我的自己息息相關的,本名都沒有,還談什麼別名呢???

另外,指針隨時能夠指向其餘的對象/變量,而引用不能夠,例如:

示例:指針能夠隨時指向別的變量/對象

#include <iostream>

using std::endl;
using std::cout;

int main() {
    int i1 = 10, i2 = 20;
    int* p1 = &i1;
    *p1 = 100;
    cout << i1 << endl;
    p1 = &i2;
    i2 = 200;
    cout << *p1 << endl;
    return 0;
}

輸出結果是:

100
200

這是由於,在執行p1=&i2;後,執行了i2的地址!而引用,則是從一而終的!

示例:引用的從一而終

#include <iostream>

using std::endl;
using std::cout;

int main() {
    int i1 = 10, i2 = 20;
    int& r1 = i1;
    r1 = 100;
    cout << i1 << endl;
    r1 = i2;
    cout << i1 << endl;
    r1 = 200;
    cout << i1 << endl;
    return 0;
}

輸出的結果是:

100
20
200

這是由於執行語句r1=i2;引用沒有起到真正的做用!引用r1仍是i1的別名!那第二個20是如何出現的呢?其實,雖然引用從一而終,可是當執行r1=i2的時候,會將i2的值賦給r1,而不是讓r1指向i2!

總結重點:

  1. 引用必須是引用變量,不能是常量或表達式!
  2. 能夠有常量的引用,可是隻能保證不能經過常引用本身來改變被引用的對象/變量!
  3. 定義引用的同時要當即初始化!
  4. 引用是從一而終的,及時執行的賦值操做,也只是賦值,而不是改變引用的對象!

Section04:引用傳參與引用返回值

Subsection-4.1:引用傳參

在傳參的時候,我建議,能用引用的就儘可能都用引用!這是由於,不用引用會形成拷貝!哪怕你是指針都會拷貝的!若是是單一的變量、對象都還好,若是是容器呢?那豈不是會形成巨大的拷貝,並且是每次調用都會浪費這個拷貝的資源!

進一步說,若是能用常引用那就是最好的,常引用有更強的功能,不光是能實現常量,還能包容左值引用和右值引用!

這個還得看一些OJ題,我記得之前我作PTA的題,就遇到一個,排序的時候若是參數不用引用,因爲拷貝過於頻繁,會超時!若是是引用,就能AC!

Subsection-4.2:引用返回值

這個引用的返回值,功能就大了!先舉一個例子:

#include <iostream>

using std::endl;
using std::cout;

int n;

int& getValue() {
    return n;
}

int main() {
    getValue() = 100;
    cout << n << endl;
    return 0;
}

輸出的內容是:

100

怎麼樣?這個功能是否是有點奇妙了???

總結:引用做爲返回值的時候,該函數能夠做爲對象左值!

再例如,咱們經過一個成員函數給對象賦值:

#include <iostream>

using std::endl;
using std::cout;

class Integer {
private:
    int v;
public:
    Integer() {}

    Integer(int v_) : v(v_) {}

    Integer& getObject() {
        return *this;
    }

    int getValue() {
        return v;
    }
};

int main() {
    Integer i;
    i.getObject() = 20;
    cout << i.getValue() << endl;
    return 0;
}

輸出的結果是:

20

總結:返回引用的編程技法

使用條件:

必須是,在調用函數前,返回的對象就已經完成了聲明!也就是說,返回對象的引用,該對象的做用域必須包含該函數的調用段。

能實現的效果:

  1. 可以做爲左值,改變對象,至關於直接對對象賦值!那是由於這是引用,對引用的操做能實際的落實到被引用的對象上!
  2. 可以做爲左值,實現鏈式反應!這一點很關鍵,可讓程序簡化!

Subsection-4.3:返回引用帶來的鏈式反應

示例:重載輸出運算符的鏈式反應
#include <iostream>
#include <string>

using std::endl;
using std::cout;

class Person {
    friend std::ostream& operator<<(std::ostream& os, const Person& rhs);
private:
    std::string name;
    int age;
public:
    Person(std::string name_, int age_) : name(name_), age(age_) {

    }
};

std::ostream& operator<<(std::ostream& os, const Person& rhs) {
    os << rhs.name << "\t" << rhs.age;
    return os;
}

int main() {
    Person p1("Name1", 21);
    Person p2("Name2", 22);
    cout << p1 << endl << p2 << endl;
    return 0;
}

輸出結果是:

Name1 21
Name2 22

若是重載輸出流運算符的函數,不返回引用,大家猜一猜會發生什麼事情???

其實很簡單,若是不返回引用,則在執行:

cout << p1 << endl << p2 << endl;

的時候,獲得輸出流式對象p1後,流對象就銷燬了,由於不是引用做用域不能一直從一而終的!爲了能鏈式地,輸出完p1後接着輸出p2,就必須用返回引用的方式!

示例:深刻理解左值在setter的用法

仍是Person類的例子,看看下面的程序:

#include <iostream>
#include <string>

using std::endl;
using std::cout;

class Person {
    friend std::ostream& operator<<(std::ostream& os, const Person& rhs);
private:
    std::string name;
    int age;
public:
    Person& setName(std::string name) {
        this->name = name;
        return *this;
    }

    Person& setAge(int age) {
        this->age = age;
        return *this;
    }
};

std::ostream& operator<<(std::ostream& os, const Person& rhs) {
    os << rhs.name << "\t" << rhs.age;
    return os;
}

int main() {
    Person p1, p2;
    p1.setName("Name1").setAge(18);
    p2.setName("Name2").setAge(21);
    cout << p1 << endl << p2 << endl;
    return 0;
}

輸出的結果是:

Name1 18
Name2 21

這是由於,若是不是引用,就不是左值!返回值若是是對象是不能做爲左值的!只有左值才能繼續鏈式操做!這一點,能大大提高編程的效率!

後記

今天詳細闡述了,指針、引用的關係和用法,而且針對一些問題設計和展現了許多編程訓練題和編程案例!歡迎一塊兒交流討論!

相關文章
相關標籤/搜索