C++ Primer 第三章 字符串、向量和数组

命名空间的using声明

using声明可以更简单,更安全的使用到命名空间中的成员。形式为using std:: name; 如(using std::cin)(每个名字都需要独立的using声明)。

定义和初始化string对象、

  • 初始化形式
  1. string s1
  2. string s2(s1)
  3. string s2 = s1
  4. string s3("value")
  5. string s4 = "value"
  6. string s4(n, 'c')

如果使用等号初始化一个变量,实际上执行的是拷贝初始化,编译器将等号右侧的初始值拷贝到新创建的对象中去,如果不使用等号,则执行的是直接初始化

string::size_type类型

size()函数返回的是string::size_type类型的值,它是一个 无符号型整数 如果在表达式中使用了带符号数和无符号数则会产生意想不到的结果

using std:: cin;
using std:: cout;
using std:: string;
int main()
{
    string s;
    cin >> s;//asb
    int n;
    cin >> n;// -2
    if( s.size() < n)
    {
      cout << "YES"<< '\n';
    }
   return 0;
}//YES

字面值和string对象相加

标准库允许把字符字面值和字符串字面值转换成string对象
当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string。

string s1 = s1 + ",";    //正确
string s2 = "hello" + "," ;   //错误, 两个运算对象都不是string
string s3 = s1 + "," + "world"//正确 相当于(s1 + ",") +"world";
//s1 +","的结果是string对象
string s4 = "hello" + "," + s2;//错误 相当于("hello" + ",")+s2;

处理string对象中的字符


遍历string

for (declaration: expression)//expression部分是一个对象,用于表示一个序列
//declaration部分负责定义一个变量,用于访问序列中的基础元素,每次迭代declaration并与分的变量都会被初始化为expression部分的下一个元素值

for(decltype(s.size()) idx = 0;idx != s.size(); ++idx)
//这里使用decltype关键字声明idx是s.size()函数返回值的类型,也就是string::size_type

如果想改变string对象中字符的值,必须把循环变量定义为引用类型

string s("Hello!")
for(auto &c : s)
c = toupper(c);
cout << s << endl;//HELLO!

字符串下标

无论何时用到字符串下标,都要注意检查其合法性,下标是string::size_type类型,也就是无符号类型,所以可以大于等于0。实际应用时,还需要检查是否小于字符串s.size。

标准库类型vector

c++语言既有类模板,也有函数模板,vector属于类模板。模板本身不是类或函数,可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化
使用模板时,需要指出编译器应把类或函数实例化为何种类型


vector能容纳绝大数类型的对象作为其元素,但因为引用不是对象,所以不存在包含引用的vector。

定义和初始化vector对象

//允许把一个vector对象的元素拷贝给另外一个vector对象,但两个vector对象的类型必须相同
vector<int>ivec;
vector<int>ivec2(ivec);//把ivec的元素拷贝给ivec2
vector<int>ivec3 = ivec;//把ivec的元素拷贝给ivec2
vector<string>svec{ivec2};//错误 svec的元素是string对象,不是int

列表初始化vector对象

C++提供了几种不同的初始化方式。在大多数情况下这些方式可以相互等价使用。


特殊情况

  • 使用拷贝初始化( = )时, 只能提供一个初始值。
  • 如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号形式初始化。
  • 如果提供的是初始值的列表,则只能把初始值都放在花括号里初始化,而不能放在圆括号里。
vector<string> v1{"a", "an", "the"};//列表初始化
vector<string> v2("a", "an", "the");//错误
vector<int> ivec(10, -1); //10个int类型的元素,每个都被初始化为-1
vector<string> svec(10, "hi"); //10个string类型的元素,每个都被初始化为hi

值初始化

一般情况下,可以只提供vector对象容纳的元素数量而不用略去初始值,此时库会创建一个值初始化的元素处置,并把它赋给容器中的所有元素。

vector<int> v1(10);//v1有10个元素,每个都为0
vector<int> v2{10};//v2有1个元素 10
vector<int> v3(10, 1);//v3有十个元素,每个值都为0
vector<int> v4{10, 1};//v4有两个元素,10, 1
  • 如果用的是圆括号,可以说提供的值是用来构造vector对象的。
  • 如果是花括号,可以表述成我们想列表初始化的vector对象,也就是说,初始化过程会尽可能把花括号内的值当成是元素初始值的列表处理。
  • 如果初始化时使用了花括号的形式,但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象了。
vector<string> v5{"hi"};   //列表初始化
vector<string> v6("hi");   //错误,不能使用字符串字面值构建vector对象
vector<string> v7{10};     //v7有10个默认初始化的元素
vector<string> v8{10,"hi"};//v8有10个值为"hi"的元素

vector内对象的索引

vector对象的下标是从0开始计起,下标的类型是size_type类型。(vector对象以及string对象的下标运算符可用于访问已存在的元素,而不能用于添加元素。)

  • 试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,二十在运行的时候产生一个不可预知的值。缓冲区溢出指的就是这类错误。这也是导致PC以及其他设备应用程序出现安全问题的一个重要原因。确保下标合法的一种有效手段就是尽可能使用范围for语句。

迭代器介绍

迭代器可以用来访问容器对象的元素。严格来说string对象不属于容器类型,但是string支持很多与容器类型类似的操作。
所有标准库容器都可以使用迭代器,但是只有少数几种才能同时支持下标运算符。
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时还拥有返回会迭代器的成员。比如这些类型都拥有名为begin和end的成员。begin负责返回指向第一个元素的迭代器,end成员负责返回指向容器“尾元素的下一位置”,end成员返回的迭代器常被称为尾后迭代器(尾迭代器),特殊情况下如果容器为空,则begin和end返回的是同一个迭代器。

  • 迭代器使用递增运算符,迭代器的递增和整数的递增类似,迭代器的递增是将迭代器“向前移动一格位置”。

迭代器类型

一般来说,无须知道迭代器的精确类型。而实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型。

vector<int>::iterator it;//it 能读写 vector<int>中的元素
string::iterator it2;// it2 能读写string对象中的字符
vector<int>::const_iterator it3; //it3 只能读元素,不能写元素
string::const_iterator it4; //it4 只能读字符,不能写字符

const_iterator和常量指针差不多,能读取但不能修改它所指的元素值。相反,iterator的对象可读可写。如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既能使用iterator也可以使用const_iterator。

begin和end运算符

begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。

vector<int>v; 
const vector<int>cv;
auto it1 = v.begin(); //it1的类型是vector<int>::iterator
auto it2 = cv.begin(); //it2的类型是vector<int>::const_iterator

有时候这种默认的行为并非我们所要。如果对象只需读操作而无须写操作的话最好使用常量类型(比如,const_iterator),为了便于专门得到const_iterator类型的返回值,C++11新标准引入两个新函数,分别是cbegin和cend;

auto it = v.cbegin(); //it的类型是vector<int>::const_iterator
//不论vector对象或者string对象本身是否是常量,返回值都是const_iterator

结合解引用和成员的访问操作

  • 解引用迭代器可获得迭代器所指的对象,如果该对象恰好是类,就有可能希望进一步访问它的成员。
(*it).empty() //解引用it,然后调用结果的对象的empty成员。(名为it的empty成员)
*it.empty()   //错误,试图访问it的名为empty的成员,但是it是一个迭代器没有empty成员

为了简化上述表达式,C++定义了箭头运算符(->),箭头运算符把解引用和成员访问两个操作结合在一起,也就是说 it->mem 和 (*it).mem表达的意思相同。
任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。

迭代器运算

string和vector的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。所有这些运算被称为迭代器运算。

可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。
只要两个迭代器指向的是同一个容器中的元素或者元素的下一位置,就能将其相加减(不支持两个迭代器相加),所得的结果是两个迭代器的距离,所谓距离值得是右侧迭代器向前移动多少个位置就能追上左侧的迭代器,其类型名为differenec_type是带符号型整数(可正可负)

数组

数组的大小不变,不能随意向数组中增加元素。(如果不清楚元素的确切个数,使用vector)

  • 定义和初始化内置数组

数组中元素的个数也属于数组类型的一部分,编译的时候维度必须是已知的。维度必须是一个常量表达式。

unsignd cnt = 42; //不是常量表达式
constexpr unsigned sz = 42; //常量表达式
int arr[10]; //含有10个整数的数组
int *parr[sz]; //含有42个整型指针的数组
string bad[cnt]; // 错误 :cnt不是常量表达式
string strs[get_size()];  //当get_size()是constexpr时正确;否则错误
  • 和vector一样,数组的元素应该是对象,因此不存在引用的数组。

* 字符数组的特殊性

字符数组还有一种额外的初始化形式,我们可以用字符串字面值对此类数组进行初始化,当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去

char a1[] = {'c','+','+'}; //列表初始化,没有空字符
char a2[] = {'c', '+','+','\0'}; //列表初始化,含有显示的空字符
char a3[] = "C++"; //自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; // 错误,没有空间可以存放空字符

* 复杂的数组声明

int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10] = /* ? * /; // 不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组(arr)
int *(&arry)[10] = ptrs; //arry是数组的引用,该数组含有10个指针

访问数组元素

在使用数组下标的时候, 通常将其定义为size_t类型,size_t是一种机器相关的无符号类型,他被设计的足够大以便能表示内存中任意对象的的大小,在cstddef头文件中定义了size_t类型,这个文件时C标准库stddef.h头文件的C++语言版本。
与vector和string一样,当需要遍历数组的所有元素时,最好的办法也是使用范围for语句。

指针和数组

使用数组时,编译器一般会把它转换成指针。
数组还有一个特性,在很多用到数组名字的地方,编译器会自动的将其替换为一个指向数组首元素的指针。

string nums[] = {"aa", "bb", "cc"};
string *p = nums; //等价于p2  = &nums[0];

当使用数组作为一个auto变量的初始值时,推断得到的类型时指针而非数组。

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7}; 
auto ia2(ia); // 等价于 int *ia2 = &ia[0],ia2是一个整型指针,指向ia的第一个元素
auto ia2(&ia[0]);
ia2 = 42; // 错误:ia2是一个指针,不能用int给指针赋值

* 当使用decltype关键字时,上述转换不会发生,decltype(ia)返回的类型时由10个整数构成的数组。

decltype(ia) ia3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

指针也是迭代器

通过数组名字或者数组中首元素的地址都能得到指向首元素的指针,获取尾后指针要设法获取数组尾元素之后那个并不存在的元素的地址

int arr[] = {0, 1, 2, 3, 4};
int *e = &arr[5]; //尾后指针
int *b = arr;
for (int *b = arr; b != e; ++b) //循环

标准库函数begin和end

begin和end函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数。这两个函数定义在iterator头文件

int *beg = begin(arr); //指向元素首元素的指针
int *end = end(arr); //指向尾元素的下一位置的指针

尾后指针不能执行解引用和递增操作。

指针运算

  • 给一个指针加上(减去)某整数值,结果仍是指针。新指针指向的元素与原来的指针相比前进(后退)了该整数值个位置。
  • 如果计算所得的指针超出了上述范围就将产生错误,而且这种错误编译器一般发现不了。//如下p2
constexpr size_t sz = 5;
int arr[sz] = {1, 2, 3, 4, 5};
int *ip = arr; //等价于 int *ip = &arr[0]
int *ip2 = ip + 4; // ip2指向arr的尾元素arr[4]
//ip 加上4得到的结果仍然是一个指针,该指针所指元素与ip原来所指的元素相比前进了4个位置。
int *p = arr + sz; //arr转换程指向它首元素的指针;p指向arr尾元素的下一位置
//p不可以解引用;
int *p2 = arr + 10; // 错误:arr只有五个元素,p2 的值未定义
  • 和迭代器一样,两个指针相加的结果是他们之间的距离。参与运算的两个指针必须指向同一个数组中的元素。
auto n = end(arr) - begin(arr); // n的值为5

两个指针相减的结果类型是一种名为ptrdiff_t的标准库类型,和size_t一样,ptrdiff_t也是一种定义在cstddef头文件中的机器相关的类型,因为差值可能为负值,所以ptrdiff_t是一种带符号类型。
两个空指针也允许彼此相减, 结果为0;

解引用和指针运算的交互

int ia[] = {1, 2, 3, 4, 5};
int last = *(ia + 4); // ia[4]的值 ,*(int *ia[0] + 4) ,ia前进4个元素后的新地址
int last1 = *ia + 4; // 5 , ia[0] + 4 = 5;先解引用ia,然后解引用的结果+4

下标和指针

  • 很多情况下使用数组的名字其实用的是一个指向数组首元素的指针,当对数组使用下标运算符时,编译器会自动执行上述转换操作。
int ia[] = {0, 2, 4, 6, 8};
int i = ia[2]; //ia转换成指向数组首元素的指针,ia2得到(ia + 2)所指的元素。

int *p = ia; //p指向ia的首元素
i = *(p + 2); //等价于i = ia[2]

//只要指针指向的是数组中的元素(或者数组尾元素的下一位置),都可以执行下标运算。
int *p = &ia[2]; //p指向索引为2的元素
int j = p[1]; //p[1] 等价于*(p + 1),就是ia[3]表示的元素
int k = p[-2]; //p[-2]  等价于 *(p - 2) ,是ia[0]表示的元素
  • 虽然标准库string和vector也能执行下标运算,但是数组与他们相比还是有所不同,标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,如上面的k = p[-2],内置的下标运算符可以处负值,结果地址也必须只像原来指针所指的同一数组中的元素(或者同一数组尾元素的下一位置)

C风格字符串

尽管C++支持C风格字符串,但在C++程序中最好还是不要使用,C风格字符串不仅使用起来不太方便, 而且极易引发程序漏洞,是诸多安全问题的根本原因.

传入此类函数的指针必须指向空字符作为结束的数组.

char ca[] = {'C','+', '+'};
cout << strlen(ca) << endl;//错误,ca没以空字符结束
// 比较字符串
string s1 = "AAA";
string s2 = "BBB";
if(s1 < s2) ...

C风格字符串,实际比较的是指针而非字符串本身

const char ca[] = "AAA";
const char cb[] = "BAA";
if(ca < cb) //未定义的,比较的是两个无关地址。
//上面的if比较的是两个const char *的值,这两个指针指向的并非同一个对象, 所以将得到未定义的结果。

对大多数应用来说,使用标准库string要比使用C风格字符串更安全,更高效。


与旧代码的接口

任何出现字符串字面值的地方都可以用空字符结束的字符数组来替代。

  • 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值
  • string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string对象的符合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
    不能用string对象初始化指向字符的指针,为了完成这个功能,string专门提供了一个名为c_str成员函数
string s("Hello);
char *str = s;//错误,不能用string对象初始化char *
const char *str = s.c_str();//正确

使用数组初始化vector对象

int int_arr = {0, 1, 2, 3};
vector<int>vec (begin(int_arr),end(int_arr));
//用于初始化vector对象的值也可能仅是数组的一部i分
vcetor<int>vec1 (int__arr + 1, int_arr + 3); //拷贝 int_arr[1],int_arr[2]
//左闭右开

尽量使用标准库类型而非数组!

posted @ 2023-08-10 15:41  Aaaa_aa  阅读(10)  评论(0编辑  收藏  举报