關于string的‘\0‘與string,vector構造特點,反迭代器與迭代器類等的討論

目錄

問題一:關于string的''\0''問題討論

問題二:C++標準庫中的string內存是分配在堆上面嗎?

問題三:string與vector的capacity大小設計的特點

問題四:string的流提取問題

問題五:迭代器失效

?問題六:Vector 最大 最小值 索引 位置

問題7:反迭代器的實現(包含迭代器類的介紹)


前言:

前幾篇文章我們已經介紹完了string,vector,list的使用與string的使用原理,但是僅僅知道這些對于我們日常使用來說已經夠了,但是在我們日常使用的時候,不免會有報錯與相關的疑惑,那么這里我介紹幾個我認為有問題的地方,后續有問題的話,還會繼續補充。

問題一:關于string的''\0''問題討論

之前在某篇文章中看到,C語言字符串是以'\0'結尾的,但是C++string類型的字符串并不是以'\0'結尾。話不多說,直接放代碼(vsX86環境):

#include<iostream>
#include<string>
using namespace std;
int main()
{string b("abc");cout << b.capacity() << endl;cout << b.size() << endl;if (b[3] == '\0')cout << "yes" << endl;elsecout << "no" << endl;return 0;
}

運行結果:

.

可以看到我們創建的這個string,他的容器大小為15,這個string存儲大小為3,但是我們卻可以通過越界訪問? b[3]? ?,并且通過驗證字符串的結尾就是'\0'。此時我的內心是疑惑的,心想"abc"是C語言風格的字符串給b構造,肯定會把"abc"后面影藏的'\0'給構造進去,如果不會這樣就會在迭代器里面不會遇見結束表示符。那么至于這里的結尾的最后一個'\0',從結果來說是大小size不計算的,所以大小size是3。

但是我們又嘗試別的構造的話又會嘗試別的疑惑,比如這個代碼:

#include<iostream>
#include<string>
using namespace std;
int main()
{string b("abcd",3);//這種構造方法是通過字符串abcd,然后只取前3個字符進行構造string//但是這個字符串存放的其實是 abcd\0cout << b.capacity() << endl;cout << b.size() << endl;if (b[3] == '\0')cout << "yes" << endl;elsecout << "no" << endl;return 0;
}

結果跟上面一模一樣。此刻我又想,構造函數會在末尾自動添加一個'\0',并且size和capacity函數都不計算'\0'的。

但是我們一開始是假設他跟c語言的風格相似的會把abc后面的'\0'會自動添加上,但是我們這個代碼是只取了abcd\0這個字符串的前三個,沒有'\0'啊~!

所以此刻,我肯定是矛盾的!!因為最開始說string字符串是不以'\0'結尾的,但是測試下來,確實是以'\0'結尾的。

哎呀~為什么呢?經過查閱資料后,才得知了其中的奧妙,奧妙如下:

std::string:標準中未明確規定需要\0作為字符串結尾。編譯器在實現時既可以在結尾加\0,也可以不加。(因編譯器不同,就比如vs就不用)

但是,當通過c_str()或data()(二者在 C++11 及以后是等價的)來把std::string轉換為const char *時,會發現最后一個字符是\0。但是C++11,string字符串都是以'\0'結尾(這也是c++祖師爺為以前的自己的規定的優化)。



為什么C語言風格的字符串要以'\0'結尾,C++可以不要?

c語言用char*指針作為字符串時,在讀取字符串時需要一個特殊字符0來標記指針的結束位置,也就是通常認為的字符串結束標記。而c++語言則是面向對象的,長度信息直接被存儲在了對象的成員中,讀取字符串可以直接根據這個長度來讀取,所以就沒必要需要結束標記了。而且結束標記也不利于讀取字符串中夾雜0字符的字符串。



這里我們深入一下string的構造時的細節:

#include<iostream>
#include<string>
using namespace std;
int main()
{int aa = 0;printf("棧區的地址:%p\n", &aa);int* pl = new int;printf("堆區的地址:%p\n", pl);string a("abcddddddddddddddddddddddddd", 20);printf("a的地址:    %p\n", &a);printf("a[0]的地址: %p\n", &a[0]);a[1] = 'X';cout << a << endl;printf("a的地址:    %p\n", &a);printf("a[0]的地址: %p\n", &a[0]);string b("abc");printf("b的地址:    %p\n", &b);printf("b[0]的地址: %p\n", &b[0]);return 0;
}

然后通過運行的知,

用紅色標注出來的是在棧上存儲的,藍色標注的時在堆上存儲的,然而a,b就與指針類似,他們指向一片空間,空間內存儲的對象信息,?對象地址分別是006FF6AC與006FF688,他倆的地址跟棧區地址最為接近所以該對象存儲在棧區上。同理a[0]是堆區上,但是b[0]按道理也應該是在堆區上,但是為什么會是是在棧區上呢?其實這是c++的一個特殊處理,這里留下一個小疑問,(下一個問題進行解答,這里先給出為什么的答案:當string內存存儲的個數在16以內(包括'\0')(后面解釋為什么是16)在棧上,超過以后在堆上。)

所以,string在構造函數的時候,會在堆上開辟一塊內存存放字符串,并且指向這塊字符串。

(這里給大家提問一個小問題:就是為什么a先定義的,但是a對象地址為什么比b的大?)

解答:a、b是兩個局部對象變量,棧是向下增長的,所以先入棧的變量地址高,即&a > &b,



問題二:C++標準庫中的string內存是分配在堆上面嗎?

例如我聲明一個string變量。
string str;
一直不停的str.append("xxxxx");時,str會不停的增長。

我想問的是這個內存的增長,標準庫中的string會把內存放置到堆上嗎?

另外STL中的其他容器是否遵循相同的規則。

首先我們給出結論:16以內在棧上,超過以后在堆上。(這句話的答案省略上面的問題的前提條件:【在棧上構造的 string 對象】,如果string 是 new 出來的即在堆上構造的,當然內部的緩沖區總是在堆上的)。(vector也是如此,但是細節上略有不同)

為什么要這樣做呢?

如果以動態增長來解釋就是:

因為棧通常是一種具有固定大小的數據結構,如數組實現的棧在創建時會指定一個固定的容量。因此,一般情況下,棧是不支持動態增長的。?

所以是存儲在堆上的。

其實還有另一個原因,那么下一個問題給出解答;

問題三:string與vector的capacity大小設計的特點

在我們設計string與vector的時候,你是否觀察過他的capacity的大小呢?就比如vs里面為什么會讓string與vector在其存儲的內存個數小于16時會將數據存儲在棧上,大于16存儲在堆上呢?

這是因為string與vector第一次會在棧上開辟空間,直接開辟16個單位空間,然后挨個進行流提取,這樣的話就會方便很多?,就算要再添加數據,也不需要進行動態增長,然后這個16個單位空間就是string與vector的capacity。這里的證明可以通過調試自己查看他的capacity,當然編譯器不同,可能這個首次開辟空間大小略有不同,但是不影響。

總的來說這兩種解釋都是解決的次要問題,他這樣設計主要為了解決內存碎片的問題;如果存儲的內容大小小于16,他就會先存在棧上的數組里面,當大于16,就會進行拷貝到堆上,然后棧上的數組就會進行浪費,這樣達到了利用空間換時間的效果

問題四:string的流提取問題

首先如果我們自己實現string的流提取,我們會下意識認為會挨個提取輸入的字符,然后挨個與s進行對接,代碼試下如下:?(這個代碼實現的流提取是完全沒有問題的)

istream& operator>>(istream& in, string& s)
{s.clear();char ch;ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

但是這樣寫會有一個弊端,就是會多次進行擴容,俗話常說:擴容本身就是一件麻煩的時,淺拷貝就不多說了,深拷貝就更麻煩了;

所以后來就進行了優化,會先開辟一個數組,然后將流提取的字符挨個放到數組里面,當數組滿的時候(或者流提取的字符提取完了)我們當讓s+=數組;這樣既保證了存儲的數據在堆上,也避免了多次進行擴容;(需要注意的是我們要自己添加 '\0' 在string的末尾)

	istream& operator>>(istream& in, string& s){s.clear();char buff[129];size_t i = 0;char ch;//in >> ch;ch = in.get();s.reserve(128);while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 128){buff[i] = '\0';s += buff;i = 0;}//in >> ch;ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}

當然這上面的兩個問題都是存在于string于vector上的,因為他們存儲的數據是連續的,二list作為鏈表就不存在這樣的問題。?

問題五:迭代器失效

然而迭代器失效就不一樣了,string,vector,list都存在。

在我們使用迭代器進行遍歷的時候,不免會出現不正當的使用而使其迭代器失效;

失效的主要原因就是:迭代器對應的指針所指向的空間已經被銷毀了,而使用一塊已經被釋放的空間的時候,就會造成程序崩潰(即如果繼續使用已經失效的迭代器, 程序可能會崩潰)。俗話來說就是野指針了。

前面我們都在用string來進行解釋,這里我們使用vector來解釋,

1

就比如下面這個代碼:

include<iostream>
#include<vector>
using namespace std;int main()
{vector<int> v(10, 1);auto it = v.begin();v.insert(it, 0);(*it)++;return 0;
}

看起來沒有問題,但是我們是先給迭代器賦值,然后進行插入,但是有一點問題就是如果插入時恰好進行擴容,并且時異地擴容,那么這個it就會變為野指針。從而達到迭代器失效的問題。

2

同樣插入存在異地擴容,當然刪除也存在著迭代器失效的問題;

#include<iostream>
#include<vector>
using namespace std;int main()
{vector<int> v(10, 1);auto it = v.end() - 1;v.erase(it);(*it)++;return 0;
}

這時候如果再進行使用it,那么就會報錯。

注意:

  1. vs 對于迭代器失效檢查很嚴格,如使用了 erase 之后,之前的迭代器就不允許使用,只有重新給迭代器賦值,才可以繼續使用
  2. Linux下,g++編譯器對迭代器失效的檢測并不是非常嚴格,處理也沒有vs下極端。

?問題六:Vector 最大 最小值 索引 位置

#include<iostream>
#include<vector>
using namespace std;int main()
{vector<double> v{ 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 4.0, 5.0 };vector<double>::iterator biggest = max_element(begin(v), end(v));cout << "Max element is " << *biggest << " at position " << distance(begin(v), biggest) << endl;auto smallest = min_element(begin(v), end(v));cout << "min element is " << *smallest << " at position " << distance(begin(v), smallest) << endl;return 0;
}

運行結果:

問題7:反迭代器的實現

在上一篇文章中的list的迭代器是沒有進行實現的,關于list的迭代器他的實現還是有點特殊的地方;?

迭代器類存在的意義

之前模擬實現string和vector時都沒有說要實現一個迭代器類,為什么實現list的時候就需要實現一個迭代器類了呢?

因為string和vector對象都將其數據存儲在了一塊連續的內存空間,我們通過指針進行自增、自減以及解引用等操作,就可以對相應位置的數據進行一系列操作,因此string和vector當中的迭代器就是原生指針。

然而對于list的來說,他的每個結點的存儲都不是連續的,是隨機的,不可以像string,vector那樣僅僅通過與簡單的自增,自減以及進行解引用等操作對相應的結點做操作。?

而迭代器的意義就是,讓使用者可以不必關心容器的底層實現,可以用簡單統一的方式對容器內的數據進行訪問。

既然list的結點指針的行為不滿足迭代器定義,那么我們可以對這個結點指針進行封裝,對結點指針的各種運算符操作進行重載,使得我們可以用和string和vector當中的迭代器一樣的方式使用list當中的迭代器。就比如,當你使用list當中的迭代器進行自增操作時,實際上執行了p = p->next語句,只是你不知道而已,這一步迭代器替你進行了復雜的操作,這樣就可以在各種操作上進行了統一。

總結: list迭代器類,實際上就是對結點指針進行了封裝,對其各種運算符進行了重載,使得結點指針的各種行為看起來和普通指針一樣。(例如,對結點指針自增就能指向下一個結點)

迭代器類的模板參數說明?

查閱相關std源文件庫里面的設計,發現迭代器類的模板參數的設計為3個。

template<class T, class Ref, class Ptr>

這里就引發出來思考為什么要這樣設計呢?

在list的模擬實現當中,我們typedef了兩個迭代器類型,普通迭代器和const迭代器。

typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;

?這里我們就可以看出,迭代器類的模板參數列表當中的Ref和Ptr分別代表的是引用類型和指針類型

?當我們使用普通迭代器時,編譯器就會實例化出一個普通迭代器對象;當我們使用const迭代器時,編譯器就會實例化出一個const迭代器對象。

若該迭代器類不設計三個模板參數,那么就不能很好的區分普通迭代器和const迭代器。(換句話來說,按照與string與vector的思路來寫list的const與非const迭代器再使用的時候會報錯,編譯器不知道走那個迭代器)

那么就再前面文章的基礎上加上迭代器類吧。

template <class T>
struct list_node
{T _data;list_node<T>* _prev;list_node<T>* _next;list_node(const T& x = T()):_data(x), _prev(nullptr), _next(nullptr){}
};
template<class T, class Ref, class Ptr>
struct __list_iterator 
{typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node;		__list_iterator(Node* node):_node(node){}
}
class list
{typedef list_node<T> Node;
public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}iterator begin(){return _head->_next;}iterator end(){return _head;}
private:Node* _head;size_t _size;
};

?我們們迭代器類的構造函數就是用我們傳的結點參數來進行初始化。

?運算符重載需要注意要返回self就行

self是當前迭代器對象的類型:

介紹完迭代器類,下面就介紹反迭代器是怎么實現的吧;

同樣反迭代器我們也需要設計一個反迭代器類;

但是反迭代器的實現由于正向迭代器實現的思路又有所不一樣

其中他的成員變量是正向迭代器

大致如圖所示:

template<class Iterator, class Ref, class Ptr>
class Reserve_iterator
{typedef Reserve_iterator<Iterator, Ref, Ptr> Self;public:Reserve_iterator(Iterator it):_it(it){}
private:Iterator _it;
};

?同樣他與正向迭代器一樣,為了方便會進行typedef

rbegin與rend?

rbegin是其實是返回的end,rend其實是返回的begin,弄清楚這一點就比較好說了,只需要將begin傳到反迭代器類的rendend傳到反迭代器類的rbegin就可以了;

reserve_iterator rbegin()
{return reserve_iterator(end());
}
reserve_iterator rend()
{return reserve_iterator(begin());
}
const_reserve_iterator rbegin() const
{return const_reserve_iterator(end());
}
const_reserve_iterator rend() const
{return const_reserve_iterator(begin());
}
?operator++

對于反迭代器的++其實對應的就是正向迭代器的--

所以在實現的時候只需要進行減減就可以

	Self& operator++(){--_it;return *this;}

?這里返回的是引用其實很好理解,因為這里的++產生的效果是前置++,所以直接在原來的基礎上進行操作就可以,返回進行返回引用;

?operator--

同樣還有--,對應的也是正向迭代器的++,還是返回引用就可以

	Self& operator--(){++_it;return *this;}

?

這里就不一樣了,一個是返回的ref一個是ptr,這是因為我們在開始的情況下?,就將ref為引用,ptr為解引用。



到這里就完了,寫作不易還請點贊;

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/43042.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/43042.shtml
英文地址,請注明出處:http://en.pswp.cn/web/43042.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

個人開發實現AI套殼網站快速搭建(Vue+elementUI+SpringBoot)

目錄 一、效果展示 二、項目概述 三、手把手快速搭建實現本項目 3.1 前端實現 3.2 后端方向 五、后續開發計劃 一、效果展示 默認展示 一般對話展示&#xff1a; 代碼對話展示&#xff1a; 二、項目概述 本項目是一個基于Web的智能對話服務平臺&#xff0c;通過后端與第…

【C語言】指針(4):深入理解指針

目錄 ?編輯 一、回調函數 二、qsort使用舉例 2.1 使用qsort排序整型數據 2.2 使用qsort排序結構體數據 三、qsort的模擬實現 四、NULL、\0、0、0、null、NUL的區別 五、C99中的變長數組 一、回調函數 函數指針是將函數的地址取出來&#xff0c;再通過函數地址去調用&a…

untiy 在菜單欄添加自定義按鈕 點擊按鈕彈出一個Unity窗口,并在窗口里添加屬性

using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.Rendering.PostProcessing;public class AutoGenerateWindow : EditorWindow //這是定義一個窗口 {public string subjecttName "科目名字";//科目的名字public GameOb…

springboot 與 ipv6

ipv6 是個必然趨勢&#xff0c;尤其最近國家在這方面有新的推動。 運營商的項目逐漸有這方面的要求了。 所以&#xff0c;在ipv6環境&#xff0c;http或者https接口&#xff0c;還有數據庫地址&#xff0c;ipv4下是ip:port&#xff0c; 但到了ipv6&#xff0c;ipv6 的 ip就包含…

PIOMAS二進制文件轉nc文件

文章目錄 1. 按年輸出數據2. 按月輸出數據將PIOMAS標量的二進制數據格式轉化成nc格式。 1. 按年輸出數據 # 按年輸出數據 import numpy as np import pandas as pd import struct import xarray as xr import matplotlib.pyplot as plt # from cartoplot import cartoplotgri…

另一種加快大表查詢的方法:將表分區

在 MySQL 中&#xff0c;對表進行分區是一種將大表分成更小、更易于管理和查詢片段的方式。分區能夠顯著提升查詢和維護的性能&#xff0c;特別是對大數據量的表。以下是 MySQL 表分區的基礎知識和具體操作步驟。 分區類型 MySQL 支持如下主要分區類型&#xff1a; RANGE 分…

url鏈接地址,#前的參數 和 #后的參數有什么區別

例如 http://localhost:8080/?beforeParams1#/workSchemelist/index?afterParams1 beforeParams 和 afterParams 區別 打印出來可以發現&#xff1a; beforeParams 是 url 的search參數&#xff0c;通過window.location.search獲取 afterParams 是 route 的query參數&#…

行列視(RCV)是否支持自定義字段、計算公式和數據分析功能,以滿足用戶的不同需求?

行列視&#xff08;RCV&#xff09;確實支持自定義字段、計算公式和數據分析功能&#xff0c;以滿足用戶的不同需求。具體表現如下&#xff1a; 1. 自定義字段&#xff1a;RCV提供自助式數據應用&#xff0c;允許用戶根據自己的需求&#xff0c;選擇所需的字段來構建符合自己業…

外貿網站設計的要點

外貿網站設計是一種專門針對國際貿易領域的網站設計&#xff0c;需要考慮到不同國家和文化背景的用戶&#xff0c;因此設計過程要更加細致和精準。以下是外貿網站設計的關鍵要點&#xff1a; 首先&#xff0c;多語言支持是不可或缺的&#xff0c;因為外貿網站的用戶可能來自不同…

[Python自動化辦公]--從網頁登錄網易郵箱進行郵件搜索并下載郵件附件

[Python自動化辦公]–從網頁登錄網易郵箱進行郵件搜索并下載郵件附件 使用說明 ? 本文使用Python的selenium庫進行操作郵箱登錄、固定名稱搜索郵件并下載附件&#xff0c;Python版本&#xff1a;3.9.16, selenium版本&#xff1a;4.19.0&#xff0c;EdgeBrowser版本:126.0.2…

LVS集群及其它的NAT模式

1.lvs集群作用&#xff1a;是linux的內核層面實現負載均衡的軟件&#xff1b;將多個后端服務器組成一個高可用、高性能的服務器的集群&#xff0c;通過負載均衡的算法將客戶端的請求分發到后端的服務器上&#xff0c;通過這種方式實現高可用和負載均衡。 2.集群和分布式&#…

用戶增長 - 私域 - 社群運營自檢清單SOP(社群運營30問)

Check List: 1.你的目標用戶是誰&#xff1f; 2.你的目標用戶有哪些需要立馬解決的需求&#xff1f;有哪些長期需求&#xff1f;這些需求的優先級是什么&#xff1f; 3.做社群的目的是什么&#xff1f; 4.你的用戶和業務是否適合做社群&#xff1f; 5.你做哪類社群才能更好的幫…

確定適合您需求的負載組

大多數關鍵任務行業都使用 UPS 和發電機等備用電源在停電期間為其設施提供持續電力。負載組允許您在需要時測試電源&#xff0c;以確保在您最需要的時候提供可靠的電力。 選擇正確的負載組對于準確的電源測試至關重要。為了幫助您找到最適合您設施需求的負載組&#xff0c;EAK…

Hudi 索引總結 - Parquet布隆過濾器寫入過程

前言 上篇文章 提到 :索引的邏輯主要是根據 parquet 文件中保存的索引信息,判斷記錄是否存在,如果不存在,代表是新增數據,如果記錄存在則代表是更新數據,需要找到并設置 currentLocation。對于布隆索引來說,這里的索引信息其實是布隆過濾器,本篇文章主要是先總結布隆過…

【機器學習】主成分分析(PCA):數據降維的藝術

&#x1f308;個人主頁: 鑫寶Code &#x1f525;熱門專欄: 閑話雜談&#xff5c; 炫酷HTML | JavaScript基礎 ?&#x1f4ab;個人格言: "如無必要&#xff0c;勿增實體" 文章目錄 主成分分析&#xff08;PCA&#xff09;&#xff1a;數據降維的藝術引言PCA的基…

技術成神之路:設計模式(四)工廠方法模式

1.定義 工廠方法模式&#xff08;Factory Method Pattern&#xff09;是一種創建型設計模式&#xff0c;它提供了一種創建對象的接口&#xff0c;而不是通過具體類來實例化對象。工廠方法模式的主要作用是讓子類決定實例化哪一個類&#xff0c;從而實現對象創建的延遲到具體子類…

2024年6月國產數據庫大事記-墨天輪

本文為墨天輪社區整理的2024年6月國產數據庫大事件和重要產品發布消息。 目錄 2024年6月國產數據庫大事記 TOP102024年6月國產數據庫大事記&#xff08;時間線&#xff09;產品/版本發布兼容認證代表廠商大事記廠商活動相關資料 2024年6月國產數據庫大事記 TOP10 2024年6月國…

最優雅的PHP框架 Laravel

Laravel 之所以被稱為最優雅的 PHP 框架,是因為它在設計和功能上做了很多獨特的創新,極大地提高了開發效率和代碼的可維護性。以下是 Laravel 受歡迎的主要原因: 良好的文檔和社區支持 Laravel 有詳盡的官方文檔,涵蓋了框架的所有功能和用法。此外,Laravel 社區非常活躍…

【Python】已解決:SyntaxError invalid syntax

文章目錄 一、分析問題背景二、可能出錯的原因三、錯誤代碼示例四、正確代碼示例五、注意事項 已解決&#xff1a;SyntaxError invalid syntax 一、分析問題背景 在Python編程中&#xff0c;SyntaxError: invalid syntax是一個常見的錯誤&#xff0c;它通常表示代碼中存在語法…

.net開發:NPOI生成excel文件到磁盤

源碼實測可用 使用.net工具包NPOI&#xff0c;生成excel文件到本地磁盤。 實際項目中可以指定路徑到服務器&#xff0c;把生成的文件存放到服務器指定目錄。 controller層 [HttpPost("ExportExcel")]public void ExportExcel(){_TestService.ExportToExcel();} serv…