上一次整理完了《c++ primer》的第二章的内容。这次整理本书的第3章内容。

这里还是声明一下,我整理的主要是自己不知道的或者需要注意的内容,以我本人的主观意志为准,并不具备普适性。

第三章就开始慢慢的接触连续、线性存储的数据结构了。字符串、数组、vector等都是存储在内存的连续空间中,而且都是线性结构。算是c++语言中的基础数据结构了。

命名空间与using

使用方式如下

using namespace::name;

其中name表示命名空间的具体名字如标准库都在std 这个命名空间,如果要引用这个命名空间的内容就写作 using namespace::std;

另外namespace可以表示作为关键字,也可以作为具体的命名空间,如果作为具体命名空间的话,name此时应该是命名空间中的类或者函数等等成员,例如要引用cin这个函数的话,可以这样写 using std::cin

在使用时除了使用命名空间之外也可以直接带上命名空间的名称,例如要使用cout 做输出时可以这么写 std::cout << "hell world" << std::endl;

使用using 可以直接引入命名空间,减少代码编写的字符数,但是当引入多个命名空间,而命名空间中又有相同的成员时,容易引发冲突。所以在使用命名空间时有下面几条建议

  1. 头文件中不要包含using声明
  2. 尽量做到每个成员单独使用using声明

string 对象

定义和初始化string对象

初始化string对象有如下几种方式:

  1. string() : 初始化一个空字符串
  2. string(const string&): 使用一个字符串来初始化另一个字符串,新字符串是传入字符串的一个副本
  3. string(char*): 使用一个字符数组来初始化字符串
  4. string(int, char): 新字符串是由连续几个相同字符组成

需要注意的是,在定义的语句中使用赋值操作符相当于调用对应的初始化语句。而在其他位置使用赋值操作符在执行复写操作

string str = "hello world"; //此处调用拷贝构造,并没有调用赋值重载函数

string 对象的操作

string的操作主要有:

  1. os << s: 将s的值写入到os流中,返回os
  2. is >> s: 从is流中读取字符串,并赋值给s,字符串以空白分分隔,返回is
  3. getline(is, s): 从is中读取一行,赋值给s,返回is
  4. s.empty(): 判断字符串是否为空,为空则返回true,否则返回false
  5. s.size(): 返回字符串中字符个数, 类型为string::size_type。它是一个无符号类型的值,而且编译器需要保证它能够存放任何string对象的大小。不要使用size()的返回值与int进行混合运算
  6. s[n]: 返回第n个字符
  7. s+s1: 返回s和s1拼接后的结果
  8. s1=s2: 将s2的值赋值给s1,执行深拷贝
  9. s1 == s2: 判断两个字符串是否相等
  10. s1 != s2:判断两个字符串不等
  11. <, <=, >, >=:字符串比较

处理string 中的字符

string 本身是一个字符的容器,我们可以使用迭代的方式来访问其中的每一个字符。例如

// 字符转化为大写
string s = "hello world";
for(auto it = s.begin(); it != s.end(); it++)
{
*it = toupper(*it);
}

针对这种需要在循环中迭代访问每个元素的情况,c++针对for语句进行扩展,使其能够像Java等语言那样支持自动迭代每一个元素,这种语句一般被称之为范围for。

// 统计可打印字符
string s = "hello world";
int punctt_count = 0;
for(auto c : s){
if(ispunct(c)){
++punct_count;
}
}

上述代码中c 只是s中每一个字符的拷贝,如果想像之前那样修改字符串中的字符,可以在迭代时使用引用类型

//字符串转化为大写
s = "hello world";
for(auto& c : s){
c = toupper(c);
}

所有同时具有连续存储和线性存储两个特点的数据结构都可以使用下标访问其中的元素。字符串中字符是采用线性和连续存储的。所以这里它也可以采用下标运算符

// 字符串转化为大写
string s = "hello world";
for(auto index = 0; index < s.size(); ++index)
{
s[index] = toupper(s[index]);
}

在使用下标时需要注意下标是否超过容器中存储的元素个数。由于在编译与链接时不会检查这个,如果超出在运行时将会产生未定义结果。

标准库 vector

标准库vector 表示对象的集合,里面需要存储相同类型的对象。可以看作是一个动态数组。

vector 被定义在头文件 vector

由于vector中存储的是对象,而引用不是对象,所以不存在存储引用的vector

定义和初始化

除了可以使用与string相同的初始化方法外,新的标准还支持使用初始化列表来初始化vector

vector<string> vec = {"Hello", "World", "Boy", "Next", "Door"};

一般来说都是预先定义一个空的vector对象,在需要的时候使用push_back或者push_front添加元素。需要注意的是在使用迭代器的过程中,不要针对容器做删减操作

同样的vector可以使用下标来访问元素,但是需要注意下标只能访问已有元素不能使用下标来添加元素,同时使用下标时需要注意范围。访问超过范围的元素,会引起越界的问题

迭代器

迭代器是一组抽象,是用来统一容器中元素访问方式的抽象。它能保证不管什么类型的容器,只要使用迭代器,就能使用相同的方式方法从头到尾访问到容器中的所有元素。在这里不用过于纠结跌打器究竟是如何实现的,只需要知道如何使用它。

另外提一句,我当初在初学的时候一直把c语言的思路带入到c++中,导致我一直认为跌迭代器就是指针或者下标,我试图使用指针和下标的方式来理解,然后发现很多地方搞的很乱,也很模糊。这个概念我是一直等待学习python和Java这种没有指针、完全面向对象的语言之后,才纠正过来。这里我想起《黑客与画家》书中提到的,编程语言的高度会影响我们看待问题高度。从我的经历来看,我慢慢的理解了这句话的意思。所以这也是我当初学习lisp的一个原因。我想看看被作者称之为数学语言,抽象程度目前最高的语言是什么样的,对我以后看问题有什么影响

迭代器提供了两种重要的抽象:提供统一的接口来遍历容器中所有元素;另外迭代器提供统一接口,让我们实际操作容器中的元素

使用迭代器

迭代器的使用如下:

  1. 迭代器都是使用begin 获取容器中的第一个元素;使用end获取尾元素的下一个元素
  2. 迭代器自身可以像操作对象的指针一样操作容器中的对象
  3. 迭代器比较时,比较的是两个迭代器指向的是否是同一个元素,不支持 >、<比较
  4. ++ 来使迭代器指向容器中下一个位置的对象,--来指向上一个位置的对象

如果不想通过迭代器改变容器中元素的值,可以使用const类型的迭代器,即 const_iterator 类型的迭代器

#+BEGIN_SRC c++
string s = "Hello World";
for(string::const_iterator it = s.begin(); it != s.end(); it++)
{
cout << *it << endl;
}
#+END_SRC

begin 和end返回的是普通类型的迭代器,c++ 11中提供了一套新的方法来获取const类型的迭代器,cbegincend

迭代器的常见运算

迭代器常见运算:

  1. iter + n: 迭代器向前可以加上一个整数,类似于指针加上一个整数,表示迭代器向前移动了若干个元素
  2. iter - n: 迭代器往前移动了若干个元素,类似于指针减去一个整数
  3. iter1 - iter2: 表示两个迭代器之间的间距,类似于指针的减法
  4. 、<、>=、<=:根据迭代器的位置来判断迭代器的大小,类似于指针的大小比较

迭代器与整数运算,如果超过了原先容器中元素的个数,那么最多只会返回容器中最后一个元素的下一个跌打器,也就是返回值为 end函数的返回

迭代器相减得到迭代器之间的距离,这个距离指的是右侧的迭代器移动多少个元素后到达左侧迭代器的位置,其类型定义为difference_type

使用迭代器来访问元素时,与使用指针访问指向的对象的方式一样,它重载了解引用运算符和箭头运算符。使我们能够像使用指针那样使用迭代器

数组

数组与vector相似

  1. 二者都是线性存储
  2. 二者存储的都是相同类型的元素

与vector不同的是:

  1. 数组大小固定
  2. 由于大小在初始化就已经确定,所以在性能上优于vector,灵活性上有些不足

定义和初始化内置数组

在初始化数组的时候需要注意:

  1. 数组大小的值可以是字面值常量、常量表达式、或者普通常量
  2. 定义数组时必须指明类型,不允许用auto由初始化值来进行推断
const unsigned int cnt = 42; //常量
constexpr unsigned int sz = 42; //常量表达式 int arr[10]; //使用字面常量指定大小
int arr2[cnt]; //使用常量初始化
int arr3[sz]; //使用常量表达式初始化

可以在初始化时不指定大小,后续会根据初始化列表中的元素个数自动推导出数组大小

同时指定了数组大小和初始化列表,如果指定大小大于初始化列表中的元素个数,那么前面几个元素按照初始化列表中的值进行初始化,后面多余的元素则初始化为默认值

如果指定大小小于初始化列表中元素个数,则直接报错

const unsigned int sz = 3;
int arr1[sz] = {1, 2, 3};
int arr2[sz] = {1}; // 等价与 arr2[sz] = {1, 0, 0}
int arr3[] = {1, 2, 3};
int arr4[sz] = {1, 2, 3, 4}; //错误,初始化列表中元素个数不能大于数组中定义的元素个数

字符数组可以直接使用字符串常量进行赋值,数组大小等于字符串长度加一

我们可以对数组中某个元素进行赋值,但是数组之间不允许直接进行拷贝和赋值

和vector中一样,数组中存储的也是对象,所以不存在存储引用的数组。

在理解关于数组的复杂声明时,采用的也是从右往左看理解的方式。或者说我们先找到与[] 结合的部分来理解,与[]结合的部分去掉之后就是数组中元素的类型。

int * ptrs[10];
int & refs[10];
int (*Parry)[10];
int (&arrRef)[10];

上面的例子中:

ptrs,首先与[]结合最紧密的是ptrs 去掉这两个部分,剩下的就是int* 这部分表示数组中元素类型是int* , 也就是这里定义了一个包含10个int指针元素的数组

refs, 首先与[]结合最紧密的是ref2,去掉这个部分,剩下的就是int&,这部分表示数组中元素类型是int&,也就是这里定义了一个包含10个指向int数据的引用的数组,由于不存在存储引用的数组,所以这里是错误的

Parry,由于有了括号,与[]结合最紧密的就变成了 int,也就是我们先定义了一个包含10个int类型的数组,而Parry本身是一个指针,所以这里定义的其实是一个指向存储了10个int类型数据的数组的指针

同样的方式分析,得到arrRef 其实是一个指向存储了10个int类型数据的数组的引用

指针和数组

在上面的例子中,已经见过了指针和数组的一些定义方式,例如ptrs 是一个存储了指针的数组,这种数组一般称之为指针数组;Parry是一个指向数组的指针,这种指针被称之为数组指针

在某些时候使用数组的时候,编译器会直接将它转化为指针,其中在使用数组名时,编译器会自动转化为数组首元素的地址。

int ia[] = {1, 2, 3, 4, 5};
auto ia2 = ia;
ia2[2] = 10; // 这里ia2是指向ia数组首元素的指针,这里其实是在修改ia第3个元素的值

需要注意的是在使用decltype时,该现象不会发生,decltype只会根据表达式推断出类型,而不会具体计算表达式的值,所以它遇到数组名时,根据上下文知道它是一个数组,而不会实际取得数组首元素的地址

int ia[] = {1, 2, 3, 4, 5};
decltype(ia) ia2 = {0}; //这里ia2 是一个独立的数组,与ia无关
ia2[2] = 10;

指针也可以看作迭代器的一种,进行迭代时终止条件是数组尾元素下一个位置的地址

int ai[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *pbegin = &ai[0];
int *pend = &ai[10]; for(int* it = pbegin; it != pend; it++)
{
cout << *it << endl;
}

c++ 11中引入两个函数来获取数组的begin位置和end位置,分别为begin() 与 end()

int ai[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for(int *p = begin(ai); p != end(ai); p++)
{
cout << *p << endl;
}

c 风格的字符串

string转化为char* 可以使用string.c_str()函数,该函数返回的是const char*,以取保无法通过这个指针修改字符串本身的值,另外该函数返回的地址一直有效,如果后续修改了string的值,那么根据字符串的算法,字符串中保存字符的地址可能发生变化,此时再使用原来返回的指针访问新的字符串,可能会出现问题

如果执行完c_str函数后,程序想一直访问其返回的数组,最好将该数组重新拷贝一份

string s = "hello world";
const char* pszBuf = s.c_str()
char* pBuff = new char[s.size() + 1];
memset(pBuff, 0x00, sizeof(char) * s.size() + 1);
strcpy(pBuff, pszBuff); //后面可以直接使用pbuf,即使s字符串改变
s = "boy next door"; //do something
delete[] pBuf;

为了与旧代码兼容,允许使用数组来初始化一个vector容器,只需要指明需要拷贝的首元素地址和尾元素地址

int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ,10};
vector<int> va(begin(arr), end(arr));

多维数组

多维数组是数组的数组,数组中每一个成员都是一个数组。当一个数组的元素仍是数组时,需要多个维度来表示,一个表示数组本身的大小,一个维度表示元素中数组大小

对于二维数组来说,一般把第一个维度称之为行,第二个维度称之为列。

int ia[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
}; //等价于
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

多维数组的初始化可以用打括号初始化每个维度的数据,也可以省略中间的大括号,这样它会按照顺序初始化

但是需要注意

int ia[3][4] = {
{0},
{1, 2},
{3, 4, 5}
}; int ia[3][4] = {0, 1, 2, 3, 4, 5};

上述代码中,二者含义完全不一样,上一个表示每个子元素中的数组如何初始化,最终结果为{0, 0, 0, 0, 1, 2, 0, 0, 3, 4, 5, 0}。下面一个是从第一行开始依次初始化所有元素,最终结果为{0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0}

可以使用下标访问数组元素,一个维度对应一个下标

int ai[3][4] = {0};

cout << ai[2][3] << endl; //如果下标个数和数组维度一样,将得到具体类型的值
cout << ai[2] << endl; //下标数小于数组维度,得到对应子数组的首地址

可以使用for循环遍历数组

int a[3][4] = {0};
for(auto row : a){
for(auto i : row) //错误不能对指针使用迭代
{
cout << i << endl;
}
}

上述例子中,由于多维数组中存储的是数组元素,所以row默认是数组元素,也就是数组首地址,是指针类型,也就不能使用内层的迭代了

我们可以稍微做一些修改

int a[3][4] = {0};
for(auto& row : a){
for(auto i : row) //错误不能对指针使用迭代
{
cout << i << endl;
}
}

使用引用声明之后,row就表示指向内层子数组的一个数组的引用,也就是一个子数组本身,针对数组就可以使用范围for了

注意:使用for范围遍历时,除了最内层元素,其余的都需要声明为引用类型

多维数组的名称也是数组的首地址

定义多维数组的指针时,需要明确,多维数组是存储数组的特殊数组

int ai[3][4] = {0};
int (*p)[4] = ai;
// int *p[4] 表示的是指针数组,数组有4个成员,每个成员都是一个int*

上述代码,ai是一个存储3个数组元素的数组,每个元素又是存储4个整型元素的数组,因此定义它的指针的时候,需要明确,指针类型应该是数组元素的类型,也就是有4个int型元素的数组的指针

当然如果嫌麻烦或者不会写,可以使用auto来定义

一般来说,书写多维数组的指针是比较麻烦的一件事,可以使用类型别名让它变得简单点,上面的例子可以改写一下

//typedef int int_array_4[4]; 二者是完全等价的
using int_array_4 = int[4]; int_array_4 *pArr = ai; for(; pArr != ai + 3; ++pArr)
{
for(int *p = *pArr; p != *pArr+4; ++p)
{
cout << *p << " ";
} cout << endl;
}

数组名代表的是数组的首元素,多维数组又可以看作是一个存储数组的数组。所以这里ai的名称代表的是一个存储了3个元素的数组,每个元素都是存储4个整型数据的数组。

pArr 的类型是存储了4个整型元素的数组的指针,所以这里与ai表示的指针的类型相同。这里我们将ai的值赋值给指针。在循环中,外层循环用来找到ai数组中每个子数组的指针。

内层循环中,使用pArr解引用得到指针指向的每一个对象,也就是一个存储了4个整型元素的数组。针对这个数组进行循环,依次取出数组中每一个元素。


c++基础之字符串、向量和数组的更多相关文章

  1. <<C++ Primer>> 第三章 字符串, 向量和数组 术语表

    术语表 第 3 章 字符串, 向量和数组 begin: 是 string 和 vector 的成员,返回指向第一个元素的迭代器.也是一个标准库函数,输入一个数字,返回指向该数字首元素的指针.    缓 ...

  2. [C++ Primer] 第3章: 字符串, 向量和数组

    标准库类型string string初始化 string s2(s1); string s2 = s1; string s3("value"); string s3 = " ...

  3. 1.7 js基础,字符串、数组小结

    一.arguments  实参参数的数组         实参[实际的值],形参[形式上的参数]         当参数个数不固定的时候使用.         示例: script> var g ...

  4. C++ Primer 第3章 字符串、向量和数组

    C++ Primer 第3章 字符串.向量和数组 C Primer 第3章 字符串向量和数组 1 命名空间的using声明 2 标准库类型string 3 标准库类型vector 4 迭代器介绍 5 ...

  5. C++ Primer 5th 第3章 字符串、向量和数组

    *****代码在Debian g++ 5.40 / clang++ 3.8(C++11)下编写调试***** 本章主要是关于字符串.数组的内容,以及一些简单的容器知识. 1.using的声明 usin ...

  6. ajax处理返回的三种格式(json格式 , xml通用格式 , html文本格式)(数据类型:整数、字符串、数组、对象)(基础最重要!)

    ajax方法的参数 常用的ajax参数比如url,data,type,包括预期返回类型dataType,发送到服务器的数据的编码类型contentType,成功方法,失败方法,完成方法.除了这些以外还 ...

  7. sizeof、strlen、字符串、数组,整到一块,你还清楚吗?

    写在前面 sizeof.strlen.字符串.数组,提到这些概念,相信学过C语言的人都能耳熟能详,也能谈得头头是道,但是,在实际运用中,当这些内容交织在一起时,大家却不一定能搞地清清楚楚,本文的目的正 ...

  8. redis基础的字符串类型

    redis —— 第二篇 基础的字符串类型 我们都知道redis是采用C语言开发,那么在C语言中表示string都是采用char[]数组的,然后你可能会想,那还不简单,当我执行如下命令,肯定是直 接塞 ...

  9. GoLang基础数据类型--->字符串处理大全

    GoLang基础数据类型--->字符串处理大全 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 欢迎加入:   高级运维工程师之路               59843264 ...

  10. js字符串和数组操作,容易混淆的方法总结(slice、substring、substr、splice)

    平时工作中,很少静下心来总结基础知识,总觉得自己会用了,有点飘了,直到碰壁之后才懂得基础知识的重要性.大牛告诉我,一次写对,是不是可以不用F12去调试了?是不是省了时间?简直是面红耳赤,无地自容.在这 ...

随机推荐

  1. Bug定级实例

    *1级,**系统崩溃* *定义:*严重阻碍测试和开发工作 *对应**优先级**:**最高* *具体可分为:* 1.功能完全没有实现 2.应用闪退/崩溃无法运行 3*.应用必现安全模式,无法运行* 4. ...

  2. 【Java 进阶篇】使用 Stream 流和 Lambda 组装复杂父子树形结构(List 集合形式)

    目录 前言 一.以部门结构为例 1.1实体 1.2返回VO 1.3具体实现 1.4效果展示 二.以省市县结构为例 2.1实体 2.2返回VO 2.3具体实现 2.4效果展示 三.文章小结 前言 在最近 ...

  3. 【Vue】大总结

    目录 vue大回顾 模板语法处理xss攻击 Vue单页面组件 ts泛型 sass\less\css的区别 ...toRef() defineEmits 练习 根据分数显示颜色 vue大回顾 1 前端发 ...

  4. 微信小程序 wx:for 遍历 Map集合

    如果要在微信小程序wxml中对ES6的Map集合进行wx:for遍历,如下: let map = new Map(); map.set("a",[1,2,3]); map.set( ...

  5. vivo 微服务 API 网关架构实践

    一.背景介绍 网关作为微服务生态中的重要一环,由于历史原因,中间件团队没有统一的微服务API网关,为此准备技术预研打造一个功能齐全.可用性高的业务网关. 二.技术选型 常见的开源网关按照语言分类有如下 ...

  6. java调用本机的命令 如ping、打开文本等

    最近接触到用java代码调用主机的命令部分感觉有点意思整理总结一下 环境jdk1.8  操作系统win10,不用引入其他的包jdk自带的api就可以 一.java调用ping命令 import jav ...

  7. SpringCloud学习 系列十、服务熔断与降级(3-类级别的服务降级)

    系列导航 SpringCloud学习 系列一. 前言-为什么要学习微服务 SpringCloud学习 系列二. 简介 SpringCloud学习 系列三. 创建一个没有使用springCloud的服务 ...

  8. vue 文件路径获取文件名

    例如: url 是//resource//20220819//kfz//调试.zip转换后结果为 调试.zip//文件路径获取文件名 getFileName(url) { let name = &qu ...

  9. HanLP — 汉字转拼音,简繁转换 -- JAVA

    目录 语料库 训练 加载语料库 训练模型 保存模型 加载模型 计算 调用 HanLP 在汉字转拼音时,可以解决多音字问题,显示输出声调,声母.韵母,通过训练语料库, 本文代码为<自然语言处理入门 ...

  10. vue权限管理

    https://www.bilibili.com/video/BV1nq4y1i7BU/?spm_id_from=333.788.recommend_more_video.6&vd_sourc ...